From 6b474ed0580185357cfb16f04f752bd754644feb Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Wed, 27 Aug 2025 15:53:04 +0200 Subject: [PATCH 1/9] add envelope --- CHANGELOG.md | 17 + src/compas_tna/envelope/__init__.py | 3 + src/compas_tna/envelope/crossvault.py | 540 +++++++++++++ src/compas_tna/envelope/dome.py | 292 +++++++ src/compas_tna/envelope/envelope.py | 906 ++++++++++++++++++++++ src/compas_tna/envelope/pavillionvault.py | 437 +++++++++++ src/compas_tna/envelope/pointedvault.py | 660 ++++++++++++++++ 7 files changed, 2855 insertions(+) create mode 100644 src/compas_tna/envelope/__init__.py create mode 100644 src/compas_tna/envelope/crossvault.py create mode 100644 src/compas_tna/envelope/dome.py create mode 100644 src/compas_tna/envelope/envelope.py create mode 100644 src/compas_tna/envelope/pavillionvault.py create mode 100644 src/compas_tna/envelope/pointedvault.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b53bdef6..c206cefe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added `Envelope` class for masonry structure boundaries with intrados, extrados, and middle meshes +* Added direct envelope creation methods to `Envelope` class: + * `from_crossvault()` - Creates cross vault envelopes with configurable spans and thickness + * `from_dome()` - Creates dome envelopes with configurable radius, center, and oculus + * `from_pavillionvault()` - Creates pavillion vault envelopes with spring angle support + * `from_pointedvault()` - Creates pointed vault envelopes with height control parameters +* Added envelope creation functions for each vault type: + * `create_crossvault_envelope()` - Generates cross vault meshes and callables + * `create_dome_envelope()` - Generates dome meshes with circular topology + * `create_pavillionvault_envelope()` - Generates pavillion vault meshes with expanded option + * `create_pointedvault_envelope()` - Generates pointed vault meshes with height parameters +* Added vault-specific update functions for optimization: + * `crossvault_middle_update()`, `crossvault_ub_lb_update()`, `crossvault_dub_dlb()` + * `dome_middle_update()`, `dome_ub_lb_update()`, `dome_dub_dlb()` + * `pavillionvault_middle_update()`, `pavillionvault_ub_lb_update()`, `pavillionvault_dub_dlb()` + * `pointedvault_middle_update()`, `pointedvault_ub_lb_update()`, `pointedvault_dub_dlb()` + * Added comprehensive form diagram creation methods to `FormDiagram` class: * `create_cross()` - Creates cross discretisation with orthogonal arrangement and quad diagonals * `create_fan()` - Creates fan discretisation with straight lines to corners diff --git a/src/compas_tna/envelope/__init__.py b/src/compas_tna/envelope/__init__.py new file mode 100644 index 00000000..e5529efb --- /dev/null +++ b/src/compas_tna/envelope/__init__.py @@ -0,0 +1,3 @@ +from .envelope import Envelope + +__all__ = ["Envelope"] diff --git a/src/compas_tna/envelope/crossvault.py b/src/compas_tna/envelope/crossvault.py new file mode 100644 index 00000000..249cf279 --- /dev/null +++ b/src/compas_tna/envelope/crossvault.py @@ -0,0 +1,540 @@ +from numpy import array +from numpy import ones +from numpy import zeros + +import math + +from compas.datastructures import Mesh +from compas_tna.diagrams.diagram_rectangular import create_cross_mesh + + +def create_crossvault_envelope( + cls, + x_span: tuple = (0.0, 10.0), + y_span: tuple = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + rho: float = 25.0, +): + """Create an envelope for a cross vault geometry with given parameters. + + Parameters + ---------- + cls : class + The Envelope class to use for creating the envelope. + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + thickness : float, optional + Thickness of the vault, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n : int, optional + Number of vertices for the mesh, by default 100 + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + envelope : Envelope + The created envelope with intrados, extrados, and middle meshes. + """ + # Create base topology + base_topology = create_cross_mesh(x_span=x_span, y_span=y_span, n=n) + xyz0, faces_i = base_topology.to_vertices_and_faces() + xi, yi, _ = array(xyz0).transpose() + + # Create middle surface + zt = crossvault_middle_update( + xi, yi, min_lb, x_span=x_span, y_span=y_span, tol=1e-6 + ) + xyzt = array([xi, yi, zt.flatten()]).transpose() + middle = Mesh.from_vertices_and_faces(xyzt, faces_i) + middle.update_default_vertex_attributes(thickness=thickness) + + # Create upper and lower bounds + zub, zlb = crossvault_ub_lb_update( + xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, tol=1e-6 + ) + xyzub = array([xi, yi, zub.flatten()]).transpose() + xyzlb = array([xi, yi, zlb.flatten()]).transpose() + + extrados = Mesh.from_vertices_and_faces(xyzub, faces_i) + intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) + + # Create envelope using the class method + envelope = cls.from_meshes(intrados, extrados, middle) + + # Set material properties + envelope.thickness = thickness + envelope.rho = rho + + envelope.type = 'crossvault' + + return envelope + + +def crossvault_middle_update( + x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 +): + """Update middle of a crossvault based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + span of the vault in y direction, by default (0.0, 10.0) + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + z : array + Values of the middle surface in the points + """ + + y1 = y_span[1] + y0 = y_span[0] + x1 = x_span[1] + x0 = x_span[0] + + rx = (x1 - x0) / 2 + ry = (y1 - y0) / 2 + hc = max(rx, ry) + + z = zeros((len(x), 1)) + + for i in range(len(x)): + xi, yi = x[i], y[i] + if yi > y1: + yi = y1 + if yi < y0: + yi = y0 + if xi > x1: + xi = x1 + if xi < x0: + xi = x0 + xd = x0 + (x1 - x0) / (y1 - y0) * (yi - y0) + yd = y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + hxd = math.sqrt(abs((rx) ** 2 - ((xd - x0) - rx) ** 2)) + hyd = math.sqrt(abs((ry) ** 2 - ((yd - y0) - ry) ** 2)) + if ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q1 + z[i] = hc * (hxd + math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2)) / (rx + ry) + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q3 + z[i] = hc * (hyd + math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2)) / (rx + ry) + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q2 + z[i] = hc * (hxd + math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2)) / (rx + ry) + elif ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q4 + z[i] = hc * (hyd + math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2)) / (rx + ry) + else: + print("Vertex did not belong to any Q. (x,y) = ({0},{1})".format(xi, yi)) + z[i] = -min_lb + + return z + + +def crossvault_ub_lb_update( + x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 +): + """Update upper and lower bounds of an crossvault based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the arch + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + span of the vault in y direction, by default (0.0, 10.0) + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + ub : array + Values of the upper bound in the points + lb : array + Values of the lower bound in the points + """ + + y1 = y_span[1] + y0 = y_span[0] + x1 = x_span[1] + x0 = x_span[0] + + y1_ub = y1 + thk / 2 + y0_ub = y0 - thk / 2 + x1_ub = x1 + thk / 2 + x0_ub = x0 - thk / 2 + + y1_lb = y1 - thk / 2 + y0_lb = y0 + thk / 2 + x1_lb = x1 - thk / 2 + x0_lb = x0 + thk / 2 + + rx_ub = (x1_ub - x0_ub) / 2 + ry_ub = (y1_ub - y0_ub) / 2 + rx_lb = (x1_lb - x0_lb) / 2 + ry_lb = (y1_lb - y0_lb) / 2 + + hc_ub = max(rx_ub, ry_ub) + hc_lb = max(rx_lb, ry_lb) + + ub = ones((len(x), 1)) + lb = ones((len(x), 1)) * -min_lb + + for i in range(len(x)): + xi, yi = x[i], y[i] + xd_ub = x0_ub + (x1_ub - x0_ub) / (y1_ub - y0_ub) * (yi - y0_ub) + yd_ub = y0_ub + (y1_ub - y0_ub) / (x1_ub - x0_ub) * (xi - x0_ub) + hxd_ub = math.sqrt((rx_ub) ** 2 - ((xd_ub - x0_ub) - rx_ub) ** 2) + hyd_ub = math.sqrt((ry_ub) ** 2 - ((yd_ub - y0_ub) - ry_ub) ** 2) + + intrados_null = False + + if (yi > y1_lb and (xi > x1_lb or xi < x0_lb)) or ( + yi < y0_lb and (xi > x1_lb or xi < x0_lb) + ): + intrados_null = True + else: + yi_intra = yi + xi_intra = xi + if yi > y1_lb: + yi_intra = y1_lb + if yi < y0_lb: + yi_intra = y0_lb + elif xi > x1_lb: + xi_intra = x1_lb + elif xi < x0_lb: + xi_intra = x0_lb + + xd_lb = x0_lb + (x1_lb - x0_lb) / (y1_lb - y0_lb) * (yi_intra - y0_lb) + yd_lb = y0_lb + (y1_lb - y0_lb) / (x1_lb - x0_lb) * (xi_intra - x0_lb) + hxd_lb = _sqrt(((rx_lb) ** 2 - ((xd_lb - x0_lb) - rx_lb) ** 2)) + hyd_lb = _sqrt(((ry_lb) ** 2 - ((yd_lb - y0_lb) - ry_lb) ** 2)) + + if ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q1 + ub[i] = ( + hc_ub + * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) + / (rx_ub + ry_ub) + ) + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hxd_lb + + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q3 + ub[i] = ( + hc_ub + * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) + / (rx_ub + ry_ub) + ) + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hyd_lb + + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q2 + ub[i] = ( + hc_ub + * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) + / (rx_ub + ry_ub) + ) + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hxd_lb + + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + elif ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q4 + ub[i] = ( + hc_ub + * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) + / (rx_ub + ry_ub) + ) + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hyd_lb + + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + else: + print("Error Q. (x,y) = ({0},{1})".format(xi, yi)) + + return ub, lb + + +def crossvault_dub_dlb( + x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 +): + """Computes the sensitivities of upper and lower bounds in the x, y coordinates and thickness specified. + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the arch + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + dub : array + Values of the sensitivities for the upper bound in the points + dlb : array + Values of the sensitivities for the lower bound in the points + """ + + y1 = y_span[1] + y0 = y_span[0] + x1 = x_span[1] + x0 = x_span[0] + + y1_ub = y1 + thk / 2 + y0_ub = y0 - thk / 2 + x1_ub = x1 + thk / 2 + x0_ub = x0 - thk / 2 + + y1_lb = y1 - thk / 2 + y0_lb = y0 + thk / 2 + x1_lb = x1 - thk / 2 + x0_lb = x0 + thk / 2 + + rx_ub = (x1_ub - x0_ub) / 2 + ry_ub = (y1_ub - y0_ub) / 2 + rx_lb = (x1_lb - x0_lb) / 2 + ry_lb = (y1_lb - y0_lb) / 2 + + hc_ub = max(rx_ub, ry_ub) + hc_lb = max(rx_lb, ry_lb) + + ub = ones((len(x), 1)) + lb = ones((len(x), 1)) * -min_lb + dub = zeros((len(x), 1)) # dzub / dt + dlb = zeros((len(x), 1)) # dzlb / dt + + dubdx = zeros((len(x), len(x))) + dubdy = zeros((len(x), len(x))) + dlbdx = zeros((len(x), len(x))) + dlbdy = zeros((len(x), len(x))) + + yc = ry_ub + y0_ub # Only works for square + xc = rx_ub + x0_ub + + for i in range(len(x)): + xi, yi = x[i], y[i] + xd_ub = x0_ub + (x1_ub - x0_ub) / (y1_ub - y0_ub) * (yi - y0_ub) + yd_ub = y0_ub + (y1_ub - y0_ub) / (x1_ub - x0_ub) * (xi - x0_ub) + hxd_ub = math.sqrt((rx_ub) ** 2 - ((xd_ub - x0_ub) - rx_ub) ** 2) + hyd_ub = math.sqrt((ry_ub) ** 2 - ((yd_ub - y0_ub) - ry_ub) ** 2) + + intrados_null = False + + if (yi > y1_lb and (xi > x1_lb or xi < x0_lb)) or ( + yi < y0_lb and (xi > x1_lb or xi < x0_lb) + ): + intrados_null = True + else: + yi_intra = yi + xi_intra = xi + if yi > y1_lb: + yi_intra = y1_lb + elif yi < y0_lb: + yi_intra = y0_lb + if xi > x1_lb: + xi_intra = x1_lb + elif xi < x0_lb: + xi_intra = x0_lb + + xd_lb = x0_lb + (x1_lb - x0_lb) / (y1_lb - y0_lb) * (yi_intra - y0_lb) + yd_lb = y0_lb + (y1_lb - y0_lb) / (x1_lb - x0_lb) * (xi_intra - x0_lb) + hxd_lb = _sqrt(((rx_lb) ** 2 - ((xd_lb - x0_lb) - rx_lb) ** 2)) + hyd_lb = _sqrt(((ry_lb) ** 2 - ((yd_lb - y0_lb) - ry_lb) ** 2)) + + if ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q1 + ub[i] = ( + hc_ub + * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) + / (rx_ub + ry_ub) + ) + dub[i] = 1 / 2 * ry_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) + # dubdx[i, i] += 0.0 + dubdy[i, i] += -(yi - yc) / ub[i] + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hxd_lb + + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + dlb[i] = -1 / 2 * ry_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) + # dlbdx[i, i] += 0.0 + dlbdy[i, i] += -(yi - yc) / lb[i] + if ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q3 + ub[i] = ( + hc_ub + * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) + / (rx_ub + ry_ub) + ) + dub[i] = 1 / 2 * rx_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) + # dubdy[i, i] += 0.0 + dubdx[i, i] += -(xi - xc) / ub[i] + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hyd_lb + + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + dlb[i] = -1 / 2 * rx_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) + # dlbdy[i, i] += 0.0 + dlbdx[i, i] += -(xi - xc) / lb[i] + if ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q2 + ub[i] = ( + hc_ub + * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) + / (rx_ub + ry_ub) + ) + dub[i] = 1 / 2 * ry_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) + # dubdx[i, i] += 0.0 + dubdy[i, i] += -(yi - yc) / ub[i] + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hxd_lb + + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + dlb[i] = -1 / 2 * ry_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) + # dlbdx[i, i] += 0.0 + dlbdy[i, i] += -(yi - yc) / lb[i] + if ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q4 + ub[i] = ( + hc_ub + * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) + / (rx_ub + ry_ub) + ) + dub[i] = 1 / 2 * rx_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) + # dubdy[i, i] += 0.0 + dubdx[i, i] += -(xi - xc) / ub[i] + if not intrados_null: + lb[i] = ( + hc_lb + * ( + hyd_lb + + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) + ) + / (rx_lb + ry_lb) + ) + dlb[i] = -1 / 2 * rx_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) + # dlbdy[i, i] += 0.0 + dlbdx[i, i] += -(xi - xc) / lb[i] + # else: + # print('Error Q. (x,y) = ({0},{1})'.format(xi, yi)) + + return dub, dlb, dubdx, dubdy, dlbdx, dlbdy + + +def crossvault_bound_react_update( + x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 +): + """Compute the bounds on the reaction vector of the crossvault.""" + pass + + +def crossvault_db(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): + """Compute the sensitivities of the bounds on the reaction vector of the crossvault.""" + pass + + +def _sqrt(x): + try: + sqrt_x = math.sqrt(x) + except BaseException: + if x > -10e4: + sqrt_x = math.sqrt(abs(x)) + else: + sqrt_x = 0.0 + print("Problems to sqrt: ", x) + return sqrt_x diff --git a/src/compas_tna/envelope/dome.py b/src/compas_tna/envelope/dome.py new file mode 100644 index 00000000..6aa67d09 --- /dev/null +++ b/src/compas_tna/envelope/dome.py @@ -0,0 +1,292 @@ +import math + +from numpy import array +from numpy import ones +from numpy import zeros + +from compas.datastructures import Mesh +from compas_tna.diagrams.diagram_circular import create_circular_radial_spaced_mesh + + +def create_dome_envelope( + cls, + center: tuple = (5.0, 5.0), + radius: float = 5.0, + thickness: float = 0.50, + min_lb: float = 0.0, + n_hoops: int = 24, + n_parallels: int = 40, + r_oculus: float = 0.0, + rho: float = 25.0, +): + """Create an envelope for a dome geometry with given parameters. + + Parameters + ---------- + cls : class + The Envelope class to use for creating the envelope. + center : tuple, optional + x, y coordinates of the center of the dome, by default (5.0, 5.0) + radius : float, optional + The radius of the dome, by default 5.0 + thickness : float, optional + Thickness of the dome, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n_hoops : int, optional + Number of hoops for the mesh, by default 24 + n_parallels : int, optional + Number of parallels for the mesh, by default 40 + r_oculus : float, optional + Radius of the oculus (opening at the top), by default 0.0 + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + envelope : Envelope + The created envelope with intrados, extrados, and middle meshes. + """ + # Create meshes for different radii + for radius_current in [radius, radius - thickness / 2, radius + thickness / 2]: + base_topology = create_circular_radial_spaced_mesh( + center=center, + radius=radius_current, + n_hoops=n_hoops, + n_parallels=n_parallels, + r_oculus=r_oculus, + ) + xyz0, faces_i = base_topology.to_vertices_and_faces() + xi, yi, _ = array(xyz0).transpose() + zt = dome_middle_update(xi, yi, radius_current, min_lb, center=center) + xyzt = array([xi, yi, zt.flatten()]).transpose() + + if radius_current == radius: + middle = Mesh.from_vertices_and_faces(xyzt, faces_i) + elif radius_current == radius - thickness / 2: + intrados = Mesh.from_vertices_and_faces(xyzt, faces_i) + elif radius_current == radius + thickness / 2: + extrados = Mesh.from_vertices_and_faces(xyzt, faces_i) + + # Set thickness attributes + middle.update_default_vertex_attributes(thickness=thickness) + intrados.update_default_vertex_attributes(thickness=thickness) + extrados.update_default_vertex_attributes(thickness=thickness) + + # Create envelope using the class method + envelope = cls.from_meshes(intrados, extrados, middle) + + # Set material properties + envelope.thickness = thickness + envelope.rho = rho + + envelope.type = 'dome' + + return envelope + + +def dome_middle_update(x, y, radius, min_lb, center=(5.0, 5.0)): + """Update middle of the dome based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + radius : float, optional + The radius of the dome, by default 5.0 + min_lb : float + Parameter for lower bound in nodes in the boundary + center : tuple, optional + x, y coordinates of the center of the dome, by default (5.0, 5.0) + + Returns + ------- + zt : array + Values of the middle surface in the points + """ + + xc = center[0] + yc = center[1] + zt = ones((len(x), 1)) + + for i in range(len(x)): + zt2 = radius**2 - (x[i] - xc) ** 2 - (y[i] - yc) ** 2 + if zt2 > 0: + zt[i] = math.sqrt(zt2) + else: + zt[i] = 0.0 + + return zt + + +def dome_ub_lb_update(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): + """Update upper and lower bounds of the dome based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the dome + min_lb : float + Parameter for lower bound in nodes in the boundary + center : tuple, optional + x, y coordinates of the center of the dome, by default (5.0, 5.0) + radius : float, optional + The radius of the dome, by default 5.0 + + Returns + ------- + ub : array + Values of the upper bound in the points + lb : array + Values of the lower bound in the points + """ + + xc = center[0] + yc = center[1] + ri = radius - thk / 2 + re = radius + thk / 2 + ub = ones((len(x), 1)) + lb = ones((len(x), 1)) * -min_lb + + for i in range(len(x)): + zi2 = ri**2 - (x[i] - xc) ** 2 - (y[i] - yc) ** 2 + ze2 = re**2 - (x[i] - xc) ** 2 - (y[i] - yc) ** 2 + ub[i] = math.sqrt(ze2) + if zi2 > 0.0: + lb[i] = math.sqrt(zi2) + + return ub, lb + + +def dome_dub_dlb(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): + """Update sensitivities of upper and lower bounds of the dome based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the dome + min_lb : float + Parameter for lower bound in nodes in the boundary + center : tuple, optional + x, y coordinates of the center of the dome, by default (5.0, 5.0) + radius : float, optional + The radius of the dome, by default 5.0 + + Returns + ------- + dub : array + Values of the sensitivities of upper bound in the points + dlb : array + Values of the sensitivities of lower bound in the points + """ + + xc = center[0] + yc = center[1] + ri = radius - thk / 2 + re = radius + thk / 2 + dub = zeros((len(x), 1)) + dlb = zeros((len(x), 1)) + dubdx = zeros((len(x), len(x))) + dubdy = zeros((len(x), len(x))) + dlbdx = zeros((len(x), len(x))) + dlbdy = zeros((len(x), len(x))) + + for i in range(len(x)): + zi2 = ri**2 - (x[i] - xc) ** 2 - (y[i] - yc) ** 2 + ze2 = re**2 - (x[i] - xc) ** 2 - (y[i] - yc) ** 2 + ze = math.sqrt(ze2) + dub[i] = 1 / 2 * re / ze + dubdx[i, i] = 1 / 2 / ze * -2 * (x[i] - xc) + dubdy[i, i] = 1 / 2 / ze * -2 * (y[i] - yc) + if zi2 > 0.0: + zi = math.sqrt(zi2) + dlb[i] = -1 / 2 * ri / zi + dlbdx[i, i] = 1 / 2 / zi * -2 * (x[i] - xc) + dlbdy[i, i] = 1 / 2 / zi * -2 * (y[i] - yc) + + return dub, dlb, dubdx, dubdy, dlbdx, dlbdy + + +def dome_bound_react_update(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): + """Updates the ``b`` parameter of a dome for a given thickness + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the dome + fixed : list + List with indexes of the fixed vertices + center : tuple, optional + x, y coordinates of the center of the dome, by default (5.0, 5.0) + radius : float, optional + The radius of the dome, by default 5.0 + + Returns + ------- + b : array + The ``b`` parameter + """ + + [xc, yc] = center[:2] + b = zeros((len(fixed), 2)) + + for i in range(len(fixed)): + i_ = fixed[i] + theta = math.atan2((y[i_] - yc), (x[i_] - xc)) + x_ = abs(thk / 2 * math.cos(theta)) + y_ = abs(thk / 2 * math.sin(theta)) + b[i, 0] = x_ + b[i, 1] = y_ + + return b + + +def dome_db_sensitivity(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): + """Updates the ``db`` parameter of a dome for a given thickness + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the dome + fixed : list + List with indexes of the fixed vertices + center : tuple, optional + x, y coordinates of the center of the dome, by default (5.0, 5.0) + radius : float, optional + The radius of the dome, by default 5.0 + + Returns + ------- + db : array + The sensitivity of the ``b`` parameter + """ + + [xc, yc] = center[:2] + db = zeros((len(fixed), 2)) + + for i in range(len(fixed)): + i_ = fixed[i] + theta = math.atan2((y[i_] - yc), (x[i_] - xc)) + x_ = abs(1 / 2 * math.cos(theta)) + y_ = abs(1 / 2 * math.sin(theta)) + db[i, :] = [x_, y_] + + return db diff --git a/src/compas_tna/envelope/envelope.py b/src/compas_tna/envelope/envelope.py new file mode 100644 index 00000000..c2ebcff8 --- /dev/null +++ b/src/compas_tna/envelope/envelope.py @@ -0,0 +1,906 @@ +from typing import Optional, Type, Callable +from numpy import asarray +from scipy.interpolate import griddata +import math +import numpy as np + +from compas.data import Data +from compas.datastructures import Mesh +from compas_tna.diagrams import FormDiagram + +from .crossvault import create_crossvault_envelope +from .dome import create_dome_envelope +from .pavillionvault import create_pavillionvault_envelope +from .pointedvault import create_pointedvault_envelope + + +# TODO: What if intrados and extrados are surfaces? +def interpolate_middle_mesh(intrados: Mesh, extrados: Mesh) -> Mesh: + """Interpolate a middle mesh between intrados and extrados meshes. + + This function properly calculates thickness by considering the normal vector + at each point, ensuring accurate thickness measurements on curved surfaces. + + Parameters + ---------- + intrados : Mesh + The intrados surface mesh. + extrados : Mesh + The extrados surface mesh. + + Returns + ------- + Mesh + The interpolated middle mesh with proper normal-based thickness stored. + """ + # Use the intrados as base topology + middle = intrados.copy() + + # Get point clouds for interpolation + intrados_points = asarray(intrados.vertices_attributes("xyz")) + extrados_points = asarray(extrados.vertices_attributes("xyz")) + + # Get XY coordinates of middle mesh + middle_xy = asarray(middle.vertices_attributes("xy")) + + # Interpolate Z coordinates from both surfaces + zi = griddata( + intrados_points[:, :2], intrados_points[:, 2], middle_xy, method='linear' + ) + ze = griddata( + extrados_points[:, :2], extrados_points[:, 2], middle_xy, method='linear' + ) + + # First loop: set middle Z as average + for i, key in enumerate(middle.vertices()): + middle_z = (zi[i] + ze[i]) / 2.0 + middle.vertex_attribute(key, 'z', middle_z) + + # Second loop: calculate and set thickness using correct normals + for i, key in enumerate(middle.vertices()): + nx, ny, nz = middle.vertex_normal(key) + z_diff = ze[i] - zi[i] + if abs(nz) > 0.1: + thickness = abs(z_diff) * abs(nz) + else: + thickness = abs(z_diff) + middle.vertex_attribute(key, 'thickness', thickness) + + return middle + + +# TODO: What if middle is a surface and not a mesh? +def offset_from_middle(middle: Mesh, fixed_xy: bool = True) -> tuple[Mesh, Mesh]: + """ + Offset a middle surface mesh to obtain extrados and intrados meshes using thickness attributes. + + This function properly handles curved surfaces by using the normal vector + and the thickness measured perpendicular to the surface. + + Parameters + ---------- + middle : Mesh + The middle surface mesh with thickness attributes per vertex. + fixed_xy : bool, optional + If True, extrados/intrados will have the same XY as the middle mesh, + and only Z will be offset (with normal correction). + If False, full 3D normal offset is used. + + Returns + ------- + tuple[Mesh, Mesh] + (intrados, extrados) offset meshes. + """ + extrados = middle.copy() + intrados = middle.copy() + + for key in middle.vertices(): + x, y, z = middle.vertex_coordinates(key) + nx, ny, nz = middle.vertex_normal(key) + + # Get thickness for this specific vertex (should be normal-based) + thickness = middle.vertex_attribute(key, 'thickness') + if thickness is None: + thickness = 0.5 + half_thick = 0.5 * thickness + + if fixed_xy: + # Prevent division by zero for horizontal normals + if abs(nz) < 1e-8: + raise ValueError( + f"Normal at vertex {key} is (almost) horizontal: {nx, ny, nz}" + ) + dz = half_thick / nz + extrados_z = z + dz + intrados_z = z - dz + extrados.vertex_attribute(key, 'z', extrados_z) + intrados.vertex_attribute(key, 'z', intrados_z) + else: + # Full 3D normal offset - this is the most accurate for curved surfaces + extrados.vertex_attributes( + key, + 'xyz', + [x + half_thick * nx, y + half_thick * ny, z + half_thick * nz], + ) + intrados.vertex_attributes( + key, + 'xyz', + [x - half_thick * nx, y - half_thick * ny, z - half_thick * nz], + ) + + return intrados, extrados + + +# TODO: What if the target is a surface and not a mesh? +def project_mesh_to_target_vertical(mesh: Mesh, target: Mesh) -> None: + """Project a mesh vertically (in Z direction) onto a target mesh. + + Parameters + ---------- + mesh : Mesh + The mesh to be projected. + target : Mesh + The target mesh to project onto. + + Returns + ------- + None + The mesh is modified in place. + """ + # Get target mesh vertices for simple vertical projection + target_vertices = list(target.vertices()) + target_points = [target.vertex_point(v) for v in target_vertices] + + for vertex in mesh.vertices(): + point = mesh.vertex_point(vertex) + + # Find the closest target vertex in XY plane + min_distance = float('inf') + closest_z = point.z + + for target_point in target_points: + # Calculate XY distance (ignore Z) + xy_distance = ( + (point.x - target_point.x) ** 2 + (point.y - target_point.y) ** 2 + ) ** 0.5 + + if xy_distance < min_distance: + min_distance = xy_distance + closest_z = target_point.z + + # Update vertex to closest Z value + new_point = point.copy() + new_point.z = closest_z + mesh.vertex_attributes(vertex, "xyz", new_point) + + +def pattern_inverse_height_thickness( + pattern: Mesh, tmin: Optional[float] = None, tmax: Optional[float] = None +) -> None: + """Set variable thickness based on inverse height. + + Parameters + ---------- + pattern : Mesh + The mesh to apply thickness to. + tmin : float, optional + Minimum thickness. If None, will be calculated as 3/1000 of the diagonal of the xy bounding box. + tmax : float, optional + Maximum thickness. If None, will be calculated as 50/1000 of the diagonal of the xy bounding box. + """ + x: list[float] = pattern.vertices_attribute(name="x") + xmin = min(x) + xmax = max(x) + dx = xmax - xmin + + y: list[float] = pattern.vertices_attribute(name="y") + ymin = min(y) + ymax = max(y) + dy = ymax - ymin + + d = (dx**2 + dy**2) ** 0.5 + + tmin = tmin or 3 * d / 1000 + tmax = tmax or 50 * d / 1000 + + pattern.update_default_vertex_attributes(thickness=0) + zvalues: list[float] = pattern.vertices_attribute(name="z") + zmin = min(zvalues) + zmax = max(zvalues) + + for vertex in pattern.vertices(): + point = pattern.vertex_point(vertex) + z = (point.z - zmin) / (zmax - zmin) + thickness = (1 - z) * (tmax - tmin) + tmin + pattern.vertex_attribute(vertex, name="thickness", value=thickness) + + +class Envelope(Data): + """Pure geometric envelope representing masonry structure boundaries.""" + + def __init__(self, name=None): + super().__init__(name) + + # Core geometric surfaces (required) + self.intrados: Optional[Mesh] = None + self.extrados: Optional[Mesh] = None + self.middle: Optional[Mesh] = None + + # Material properties + self._thickness = 0.5 + self._rho = 20.0 + + # Computed properties (cached) + self._area = 0.0 + self._volume = 0.0 + self._total_selfweight = 0.0 + + # Optional fill surface + self.fill: Optional[Mesh] = None + + # Optional template to be used latter in the optimization process + self.type: Optional[str] = None + + def __str__(self): + return f"Envelope(name={self.name})" + + # ============================================================================= + # Factory methods + # ============================================================================= + + @classmethod + def from_meshes( + cls, intrados: Mesh, extrados: Mesh, middle: Optional[Mesh] = None + ) -> "Envelope": + """Construct an envelope from intrados and extrados meshes. + + Parameters + ---------- + intrados : Mesh + The intrados surface mesh of the envelope. + extrados : Mesh + The extrados surface mesh of the envelope. + middle : Mesh, optional + The middle surface mesh of the envelope. + + Returns + ------- + :class:`Envelope` + + """ + envelope = cls() + envelope.intrados = intrados + envelope.extrados = extrados + if middle is not None: + envelope.middle = middle + else: + envelope.middle = interpolate_middle_mesh(intrados, extrados) + + return envelope + + @classmethod + def from_formdiagram( + cls, formdiagram: FormDiagram, thickness: Optional[float] = None + ) -> "Envelope": + """Construct an envelope from a FormDiagram with specified thickness. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to create the envelope from. + thickness : float, optional + The thickness of the envelope. If None, uses thickness values stored in formdiagram vertices. + + Returns + ------- + :class:`Envelope` + + """ + return cls.from_middle_mesh(formdiagram, thickness) + + @classmethod + def from_middle_mesh( + cls, mesh: Mesh, thickness: Optional[float] = None + ) -> "Envelope": + """Construct an envelope from a mesh with specified thickness. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to create the envelope from. + thickness : float, optional + The thickness of the envelope. If None, uses thickness values stored in formdiagram vertices. + + Returns + ------- + :class:`Envelope` + + """ + envelope = cls() + + envelope.middle = mesh.copy(cls=Mesh) + + if thickness is not None: + envelope.thickness = thickness + + # Create intrados and extrados using thickness from middle mesh + intrados, extrados = offset_from_middle(envelope.middle) + envelope.intrados = intrados + envelope.extrados = extrados + + return envelope + + @classmethod + def from_crossvault( + cls, + x_span: tuple[float, float] = (0.0, 10.0), + y_span: tuple[float, float] = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + rho: float = 25.0, + ) -> "Envelope": + """Construct an envelope from a crossvault. + + Parameters + ---------- + x_span : tuple[float, float], optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple[float, float], optional + Span of the vault in y direction, by default (0.0, 10.0) + thickness : float, optional + Thickness of the vault, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n : int, optional + Number of vertices for the mesh, by default 100 + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + :class:`Envelope` + The created envelope with intrados, extrados, and middle meshes. + """ + return create_crossvault_envelope( + cls, x_span, y_span, thickness, min_lb, n, rho + ) + + @classmethod + def from_dome( + cls, + center: tuple[float, float] = (5.0, 5.0), + radius: float = 5.0, + thickness: float = 0.50, + min_lb: float = 0.0, + n_hoops: int = 24, + n_parallels: int = 40, + r_oculus: float = 0.0, + rho: float = 25.0, + ) -> "Envelope": + """Construct an envelope from a dome. + + Parameters + ---------- + center : tuple[float, float], optional + x, y coordinates of the center of the dome, by default (5.0, 5.0) + radius : float, optional + The radius of the dome, by default 5.0 + thickness : float, optional + Thickness of the dome, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n_hoops : int, optional + Number of hoops for the mesh, by default 24 + n_parallels : int, optional + Number of parallels for the mesh, by default 40 + r_oculus : float, optional + Radius of the oculus (opening at the top), by default 0.0 + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + :class:`Envelope` + The created envelope with intrados, extrados, and middle meshes. + """ + return create_dome_envelope( + cls, center, radius, thickness, min_lb, n_hoops, n_parallels, r_oculus, rho + ) + + @classmethod + def from_pavillionvault( + cls, + x_span: tuple[float, float] = (0.0, 10.0), + y_span: tuple[float, float] = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + spr_angle: float = 0.0, + expanded: bool = False, + rho: float = 25.0, + ) -> "Envelope": + """Construct an envelope from a pavillion vault. + + Parameters + ---------- + x_span : tuple[float, float], optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple[float, float], optional + Span of the vault in y direction, by default (0.0, 10.0) + thickness : float, optional + Thickness of the vault, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n : int, optional + Number of vertices for the mesh, by default 100 + spr_angle : float, optional + Springing angle, by default 0.0 + expanded : bool, optional + If the extrados should extend beyond the floor plan, by default False + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + :class:`Envelope` + The created envelope with intrados, extrados, and middle meshes. + """ + return create_pavillionvault_envelope( + cls, x_span, y_span, thickness, min_lb, n, spr_angle, expanded, rho + ) + + @classmethod + def from_pointedvault( + cls, + x_span: tuple[float, float] = (0.0, 10.0), + y_span: tuple[float, float] = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + hc: float = 8.0, + he: list = None, + hm: list = None, + rho: float = 25.0, + ) -> "Envelope": + """Construct an envelope from a pointed cross vault. + + Parameters + ---------- + x_span : tuple[float, float], optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple[float, float], optional + Span of the vault in y direction, by default (0.0, 10.0) + thickness : float, optional + Thickness of the vault, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n : int, optional + Number of vertices for the mesh, by default 100 + hc : float, optional + Height in the middle point of the vault, by default 8.0 + he : list, optional + Height of the opening mid-span for each of the quadrants, by default None + hm : list, optional + Height of each quadrant center (spadrel), by default None + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + :class:`Envelope` + The created envelope with intrados, extrados, and middle meshes. + """ + return create_pointedvault_envelope( + cls, x_span, y_span, thickness, min_lb, n, hc, he, hm, rho + ) + + # ============================================================================= + # Properties + # ============================================================================= + + @property + def area(self): + if not self._area: + if self.middle is not None: + self._area = self.middle.area() + else: + raise ValueError("Middle mesh is not available. Cannot compute area.") + return self._area + + @property + def volume(self): + if not self._volume: + self._volume = self.compute_volume() + return self._volume + + @property + def total_selfweight(self): + if not self._total_selfweight: + self._total_selfweight = self.compute_selfweight() + return self._total_selfweight + + @property + def thickness(self) -> float: + """Get the average thickness of the envelope. + + Returns + ------- + float + The average thickness of the envelope. + """ + if self.middle is not None: + # Return average thickness from middle mesh vertices + thicknesses = [] + for key in self.middle.vertices(): + thickness = self.middle.vertex_attribute(key, 'thickness') + if thickness is not None: + thicknesses.append(thickness) + + if thicknesses: + return sum(thicknesses) / len(thicknesses) + + return self._thickness + + @thickness.setter + def thickness(self, value: float) -> None: + """Set a uniform thickness for all vertices of the envelope. + + Parameters + ------- + value : float + The thickness value to set for all vertices. + """ + self._thickness = value + + # Update middle mesh if it exists + if self.middle is not None: + for key in self.middle.vertices(): + self.middle.vertex_attribute(key, 'thickness', value) + + @property + def rho(self) -> float: + """Get the density of the envelope. + + Returns + ------- + float + The density of the envelope in kg/m³. + """ + return self._rho + + @rho.setter + def rho(self, value: float) -> None: + """Set the density of the envelope. + + Parameters + ------- + value : float + The density value to set in kg/m³. + """ + self._rho = value + + # ============================================================================= + # Geometric operations + # ============================================================================= + + def set_variable_thickness( + self, tmin: Optional[float] = None, tmax: Optional[float] = None + ) -> None: + """Set variable thickness based on inverse height using the pattern_inverse_height_thickness function. + + This method applies thickness variation based on the height of vertices in the middle mesh, + where higher vertices get thinner thickness and lower vertices get thicker thickness. + + Parameters + ------- + tmin : float, optional + Minimum thickness. If None, will be calculated as 3/1000 of the diagonal of the xy bounding box. + tmax : float, optional + Maximum thickness. If None, will be calculated as 50/1000 of the diagonal of the xy bounding box. + """ + if self.middle is None: + raise ValueError( + "Middle mesh is not available. Cannot set variable thickness." + ) + + # Apply the pattern_inverse_height_thickness function to the middle mesh + pattern_inverse_height_thickness(self.middle, tmin=tmin, tmax=tmax) + + # Update intrados and extrados meshes + self.intrados, self.extrados = offset_from_middle(self.middle) + + def compute_volume(self) -> float: + """Compute and returns the volume of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total volume of the structure. + + """ + if self.middle is None: + raise ValueError("Middle mesh is not available. Cannot compute volume.") + + middle = self.middle + total_volume = 0.0 + + # Use variable thickness from middle mesh vertices + for vertex in middle.vertices(): + thickness = middle.vertex_attribute(vertex, 'thickness') + if thickness is None: + thickness = self._thickness + vertex_area = middle.vertex_area(vertex) # should be projected area + vertex_volume = thickness * vertex_area + total_volume += vertex_volume + + return total_volume + + def compute_selfweight(self) -> float: + """Compute and returns the total selfweight of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total selfweight of the structure. + + """ + if self.middle is None: + if self.intrados is not None and self.extrados is not None: + self.middle = interpolate_middle_mesh(self.intrados, self.extrados) + else: + raise ValueError( + "Middle mesh is not available and cannot be interpolated." + ) + + middle = self.middle + rho = self.rho + total_selfweight = 0.0 + + # Use variable thickness from middle mesh vertices + for vertex in middle.vertices(): + thickness = middle.vertex_attribute(vertex, 'thickness') + if thickness is None: + thickness = self._thickness + vertex_area = middle.vertex_area(vertex) + vertex_volume = thickness * vertex_area + vertex_weight = vertex_volume * rho + total_selfweight += vertex_weight + + return total_selfweight + + # ============================================================================= + # TNA-specific operations (accept formdiagram as parameter) + # ============================================================================= + + def apply_selfweight_to_formdiagram( + self, formdiagram: FormDiagram, normalize=True + ) -> None: + """Apply selfweight to the nodes of a form diagram based on the middle surface and local thicknesses. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply selfweight to. + normalize : bool, optional + Whether or not normalize the selfweight to match the computed total selfweight, by default True + + Returns + ------- + None + The FormDiagram is modified in place + + """ + # Step 1: Check that middle mesh is present + if self.middle is None: + if self.intrados is not None and self.extrados is not None: + self.middle = interpolate_middle_mesh(self.intrados, self.extrados) + else: + raise ValueError( + "Middle mesh is not set. Please set the middle mesh before applying selfweight." + ) + + # Step 2: Compute the selfweight of the shell + total_selfweight = self.compute_selfweight() + + # Step 3: Sync thickness to the form diagram + self.sync_thickness_to_formdiagram(formdiagram) + + # Step 4: Copy the form diagram and project it onto the middle mesh vertically + form_ = formdiagram.copy() + + if not self.callable_zt: + project_mesh_to_target_vertical(form_, self.middle) + else: + xy = np.array(form_.vertices_attributes("xy")) + zt = list(self.callable_zt(xy[:, 0], xy[:, 1]).flatten().tolist()) + for i, key in enumerate(form_.vertices()): + form_.vertex_attribute(key, 'z', zt[i]) + + # Step 5: Compute and lump selfweight at vertices + total_pz = 0.0 + for vertex in form_.vertices(): + # Get vertex area and thickness + vertex_area = form_.vertex_area(vertex) + thickness = form_.vertex_attribute(vertex, 'thickness') + + # Compute selfweight contribution (negative for downward direction) + pz = -vertex_area * thickness * self.rho + + # Store in form diagram + formdiagram.vertex_attribute(vertex, 'pz', pz) + total_pz += abs(pz) # Sum absolute values for normalization + + # Step 6: Scale to match total selfweight if normalize=True + if normalize and total_pz > 0: + scale_factor = total_selfweight / total_pz + if scale_factor != 1.0: + print(f"Scaled selfweight by factor: {scale_factor}") + + for vertex in formdiagram.vertices(): + pz = formdiagram.vertex_attribute(vertex, 'pz') + formdiagram.vertex_attribute(vertex, 'pz', pz * scale_factor) + + print( + f"Selfweight applied to form diagram. Total load: {sum(abs(formdiagram.vertex_attribute(vertex, 'pz')) for vertex in formdiagram.vertices())}" + ) + + def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply envelope bounds to a form diagram based on the intrados and extrados surfaces. + + This method projects the form diagram onto both intrados and extrados surfaces + and assigns the heights to 'ub' (upper bound) and 'lb' (lower bound) properties. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply bounds to. + + Returns + ------- + None + The FormDiagram is modified in place. + """ + # Step 1: Check that intrados and extrados are present + if self.intrados is None or self.extrados is None: + raise ValueError( + "Intra/Extrados not set. Please set them before applying bounds." + ) + + # Step 2: Copy the form diagram for projection + form_ub = formdiagram.copy() # For upper bound (extrados) + form_lb = formdiagram.copy() # For lower bound (intrados) + + # Step 3: Project form diagram onto extrados (upper bound) + if not self.callable_ub_lb: + project_mesh_to_target_vertical(form_ub, self.extrados) + project_mesh_to_target_vertical(form_lb, self.intrados) + else: + xy = np.array(form_ub.vertices_attributes("xy")) + zub, zlb = self.callable_ub_lb(xy[:, 0], xy[:, 1]) + for i, key in enumerate(form_ub.vertices()): + form_ub.vertex_attribute(key, 'z', float(zub[i])) + form_lb.vertex_attribute(key, 'z', float(zlb[i])) + + # Step 4: Collect heights and assign to form diagram + for vertex in formdiagram.vertices(): + if vertex in form_ub.vertices() and vertex in form_lb.vertices(): + # Get z coordinates from projected meshes + _, _, z_ub = form_ub.vertex_coordinates(vertex) + _, _, z_lb = form_lb.vertex_coordinates(vertex) + + # Assign to form diagram + formdiagram.vertex_attribute(vertex, 'ub', z_ub) + formdiagram.vertex_attribute(vertex, 'lb', z_lb) + else: + print(f"Warning: Vertex {vertex} not found in projected meshes") + # Set default values if vertex not found + formdiagram.vertex_attribute(vertex, 'ub', float('inf')) + formdiagram.vertex_attribute(vertex, 'lb', float('-inf')) + + def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply target heights to a form diagram based on the Envelope middle surface. + + This method projects the form diagram onto the Envelope middle surface + and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + """ + if self.middle is None: + raise ValueError( + "Middle mesh is not set. Please set the middle mesh before applying target heights." + ) + + # Step 1: Copy the form diagram for projection + form_target = formdiagram.copy() # For upper bound (extrados) + + # Step 2: Project form diagram onto the middle mesh + if not self.callable_zt: + project_mesh_to_target_vertical(form_target, self.middle) + else: + xy = np.array(form_target.vertices_attributes("xy")) + zt = list(self.callable_zt(xy[:, 0], xy[:, 1]).flatten().tolist()) + for i, key in enumerate(form_target.vertices()): + form_target.vertex_attribute(key, 'z', zt[i]) + + # Step 3: Collect heights and assign to form diagram + for vertex in formdiagram.vertices(): + if vertex in form_target.vertices(): + z_target = form_target.vertex_attribute(vertex, 'z') + formdiagram.vertex_attribute(vertex, 'target', z_target) + + def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply reaction bounds to a form diagram based on the Envelope middle surface. + + This method projects the form diagram onto the Envelope middle surface + and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + """ + + if not self.callable_bound_react: + raise ValueError( + "Callable bound reaction is not set. Please set this limit manually." + ) + + ## TODO: Implement this + + def sync_thickness_to_formdiagram( + self, formdiagram: FormDiagram, method='linear', extrapolate=True + ) -> None: + """Synchronize thickness attributes from middle mesh to form diagram using continuous interpolation. + + This method creates a continuous thickness map from the middle mesh and interpolates + thickness values at the form diagram vertex locations. This ensures compatibility + even when the middle mesh and form diagram have different topologies. + + Parameters + ------- + formdiagram : FormDiagram + The form diagram to sync thickness to. + """ + if self.middle is None: + raise ValueError("Middle mesh must be set to sync thickness.") + + # Get middle mesh XY coordinates and thickness values + middle_xy = self.middle.vertices_attributes("xy") + middle_thickness = self.middle.vertices_attribute("thickness") + + # Validate data + if not middle_xy or not middle_thickness: + raise ValueError( + "Middle mesh must have both 'xy' and 'thickness' attributes." + ) + + # Convert to numpy arrays + middle_xy_array = asarray(middle_xy) + middle_thickness_array = asarray(middle_thickness) + + # Get form diagram XY coordinates + form_xy = formdiagram.vertices_attributes("xy") + if not form_xy: + raise ValueError("Form diagram must have 'xy' attributes.") + + form_xy_array = asarray(form_xy) + + # Interpolate thickness values from middle mesh to form diagram locations + try: + # Use griddata for 2D interpolation + interpolated_thickness = griddata( + middle_xy_array, + middle_thickness_array, + form_xy_array, + method=method, + fill_value=self._thickness, # Use default thickness for points outside convex hull + # extrapolate=extrapolate + ) + + # Assign interpolated thickness values to form diagram vertices + for i, vertex in enumerate(formdiagram.vertices()): + thickness_value = float(interpolated_thickness[i]) + # Ensure thickness is positive and reasonable + if thickness_value <= 0 or math.isnan(thickness_value): + thickness_value = self._thickness + formdiagram.vertex_attribute(vertex, 'thickness', thickness_value) + + except Exception as e: + print(f"Warning: Interpolation failed, using default thickness. Error: {e}") + # Fallback: assign default thickness to all vertices + for vertex in formdiagram.vertices(): + formdiagram.vertex_attribute(vertex, 'thickness', self._thickness) diff --git a/src/compas_tna/envelope/pavillionvault.py b/src/compas_tna/envelope/pavillionvault.py new file mode 100644 index 00000000..cdfc500a --- /dev/null +++ b/src/compas_tna/envelope/pavillionvault.py @@ -0,0 +1,437 @@ +import math + +from numpy import array +from numpy import ones +from numpy import zeros + +from compas.datastructures import Mesh +from compas_tna.diagrams.diagram_rectangular import create_cross_mesh + + +def create_pavillionvault_envelope( + cls, + x_span: tuple = (0.0, 10.0), + y_span: tuple = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + spr_angle: float = 0.0, + expanded: bool = False, + rho: float = 25.0, +): + """Create an envelope for a pavillion vault geometry with given parameters. + + Parameters + ---------- + cls : class + The Envelope class to use for creating the envelope. + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + thickness : float, optional + Thickness of the vault, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n : int, optional + Number of vertices for the mesh, by default 100 + spr_angle : float, optional + Springing angle, by default 0.0 + expanded : bool, optional + If the extrados should extend beyond the floor plan, by default False + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + envelope : Envelope + The created envelope with intrados, extrados, and middle meshes. + """ + # Create base topology + base_topology = create_cross_mesh(x_span=x_span, y_span=y_span, n=n) + xyz0, faces_i = base_topology.to_vertices_and_faces() + xi, yi, _ = array(xyz0).transpose() + + # Create middle surface + zt = pavillionvault_middle_update( + xi, yi, x_span=x_span, y_span=y_span, spr_angle=spr_angle, tol=1e-6 + ) + xyzt = array([xi, yi, zt.flatten()]).transpose() + middle = Mesh.from_vertices_and_faces(xyzt, faces_i) + middle.update_default_vertex_attributes(thickness=thickness) + + # Create upper and lower bounds + zub, zlb = pavillionvault_ub_lb_update( + xi, + yi, + thickness, + min_lb, + x_span=x_span, + y_span=y_span, + spr_angle=spr_angle, + tol=1e-6, + ) + xyzub = array([xi, yi, zub.flatten()]).transpose() + xyzlb = array([xi, yi, zlb.flatten()]).transpose() + + extrados = Mesh.from_vertices_and_faces(xyzub, faces_i) + intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) + + # Create envelope using the class method + envelope = cls.from_meshes(intrados, extrados, middle) + + # Set material properties + envelope.thickness = thickness + envelope.rho = rho + + envelope.type = 'pavillionvault' + + return envelope + + +def pavillionvault_middle_update( + x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6 +): + """Update middle of a pavillion vault based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + spr_angle : float, optional + Springing angle, by default 0.0 + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + z : array + Values of the middle surface in the points + """ + + x0, x1 = x_span + y0, y1 = y_span + + if spr_angle == 0.0: + z_ = 0.0 + else: + alpha = 1 / math.cos(math.radians(spr_angle)) + z_ = (x1 - x0) / 2 * math.tan(math.radians(spr_angle)) + L = x1 * alpha + Ldiff = L - x1 + x0, x1 = -Ldiff / 2, x1 + Ldiff / 2 + y0, y1 = -Ldiff / 2, y1 + Ldiff / 2 + + rx = (x1 - x0) / 2 + ry = (y1 - y0) / 2 + + z = zeros((len(x), 1)) + + for i in range(len(x)): + xi, yi = x[i], y[i] + if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - ( + xi - x0 + ) + tol: # Q1 + z[i] = math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2) - z_ + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - ( + xi - x0 + ) - tol: # Q3 + z[i] = math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2) - z_ + elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - ( + xi - x0 + ) - tol: # Q2 + z[i] = math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2) - z_ + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - ( + xi - x0 + ) + tol: # Q4 + z[i] = math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2) - z_ + else: + print("Error Q. (x,y) = ({0},{1})".format(xi, yi)) + + return z + + +def pavillionvault_ub_lb_update( + x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6 +): + """Update upper and lower bounds of a pavillionvault based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the vault + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + spr_angle : float, optional + Springing angle, by default 0.0 + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + ub : array + Values of the upper bound in the points + lb : array + Values of the lower bound in the points + """ + + x0, x1 = x_span + y0, y1 = y_span + + if spr_angle == 0.0: + z_ = 0.0 + else: + alpha = 1 / math.cos(math.radians(spr_angle)) + z_ = (x1 - x0) / 2 * math.tan(math.radians(spr_angle)) + L = x1 * alpha + Ldiff = L - x1 + x0, x1 = -Ldiff / 2, x1 + Ldiff / 2 + y0, y1 = -Ldiff / 2, y1 + Ldiff / 2 + + y1_ub = y1 + thk / 2 + y0_ub = y0 - thk / 2 + x1_ub = x1 + thk / 2 + x0_ub = x0 - thk / 2 + + y1_lb = y1 - thk / 2 + y0_lb = y0 + thk / 2 + x1_lb = x1 - thk / 2 + x0_lb = x0 + thk / 2 + + rx_ub = (x1_ub - x0_ub) / 2 + ry_ub = (y1_ub - y0_ub) / 2 + rx_lb = (x1_lb - x0_lb) / 2 + ry_lb = (y1_lb - y0_lb) / 2 + + ub = ones((len(x), 1)) + lb = ones((len(x), 1)) * -min_lb + + for i in range(len(x)): + xi, yi = x[i], y[i] + intrados_null = False + if yi > y1_lb or xi > x1_lb or xi < x0_lb or yi < y0_lb: + intrados_null = True + if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - ( + xi - x0 + ) + tol: # Q1 + ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) - z_ + if not intrados_null: + lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) - z_ + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - ( + xi - x0 + ) - tol: # Q3 + ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) - z_ + if not intrados_null: + lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) - z_ + elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - ( + xi - x0 + ) - tol: # Q2 + ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) - z_ + if not intrados_null: + lb[i] = math.sqrt((rx_lb) ** 2 - ((xi - x0_lb) - rx_lb) ** 2) - z_ + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - ( + xi - x0 + ) + tol: # Q4 + ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) - z_ + if not intrados_null: + lb[i] = math.sqrt((rx_lb) ** 2 - ((xi - x0_lb) - rx_lb) ** 2) - z_ + else: + print("Error Q. (x,y) = ({0},{1})".format(xi, yi)) + + return ub, lb + + +def pavillionvault_dub_dlb( + x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 +): + """Computes the sensitivities of upper and lower bounds in the x, y coordinates and thickness specified. + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the vault + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + dub : array + Values of the sensitivities for the upper bound in the points + dlb : array + Values of the sensitivities for the lower bound in the points + """ + + x0, x1 = x_span + y0, y1 = y_span + + y1_ub = y1 + thk / 2 + y0_ub = y0 - thk / 2 + x1_ub = x1 + thk / 2 + x0_ub = x0 - thk / 2 + + y1_lb = y1 - thk / 2 + y0_lb = y0 + thk / 2 + x1_lb = x1 - thk / 2 + x0_lb = x0 + thk / 2 + + rx_ub = (x1_ub - x0_ub) / 2 + ry_ub = (y1_ub - y0_ub) / 2 + rx_lb = (x1_lb - x0_lb) / 2 + ry_lb = (y1_lb - y0_lb) / 2 + + ub = ones((len(x), 1)) + lb = ones((len(x), 1)) * -min_lb + dub = zeros((len(x), 1)) + dlb = zeros((len(x), 1)) + + for i in range(len(x)): + xi, yi = x[i], y[i] + intrados_null = False + if yi > y1_lb or xi > x1_lb or xi < x0_lb or yi < y0_lb: + intrados_null = True + if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - ( + xi - x0 + ) + tol: # Q1 + ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) + dub[i] = 1 / 2 * ry_ub / ub[i] + if not intrados_null: + lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) + dlb[i] = -1 / 2 * ry_lb / lb[i] + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - ( + xi - x0 + ) - tol: # Q3 + ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) + dub[i] = 1 / 2 * ry_ub / ub[i] + if not intrados_null: + lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) + dlb[i] = -1 / 2 * ry_lb / lb[i] + elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - ( + xi - x0 + ) - tol: # Q2 + ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) + dub[i] = 1 / 2 * rx_ub / ub[i] + if not intrados_null: + lb[i] = math.sqrt((rx_lb) ** 2 - ((xi - x0_lb) - rx_lb) ** 2) + dlb[i] = -1 / 2 * rx_lb / lb[i] + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - ( + xi - x0 + ) + tol: # Q4 + ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) + dub[i] = 1 / 2 * rx_ub / ub[i] + if not intrados_null: + lb[i] = math.sqrt((rx_lb) ** 2 - ((xi - x0_lb) - rx_lb) ** 2) + dlb[i] = -1 / 2 * rx_lb / lb[i] + else: + print("Error Q. (x,y) = ({0},{1})".format(xi, yi)) + + return dub, dlb # ub, lb + + +def pavillionvault_bound_react_update(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): + """Computes the ``b`` of parameter x, y coordinates and thickness specified. + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the vault + fixed : list + The list with indexes of the fixed vertices + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + + Returns + ------- + b : array + Values of the ``b`` parameter + """ + + x0, x1 = x_span + y0, y1 = y_span + b = zeros((len(fixed), 2)) + + for i in range(len(fixed)): + index = fixed[i] + xi, yi = x[index], y[index] + if xi == x0: + b[[i], :] += [-thk / 2, 0] + elif xi == x1: + b[i, :] += [+thk / 2, 0] + if yi == y0: + b[i, :] += [0, -thk / 2] + elif yi == y1: + b[i, :] += [0, +thk / 2] + + return abs(b) + + +def pavillionvault_db(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): + """Computes the sensitivities of the ``b`` parameter in the x, y coordinates and thickness specified. + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the vault + fixed : list + The list with indexes of the fixed vertices + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + + Returns + ------- + db : array + Values of the sensitivities of the ``b`` parameter in the points + """ + + x0, x1 = x_span + y0, y1 = y_span + db = zeros((len(fixed), 2)) + + for i in range(len(fixed)): + index = fixed[i] + xi, yi = x[index], y[index] + if xi == x0: + db[i, :] += [-1 / 2, 0] + elif xi == x1: + db[i, :] += [+1 / 2, 0] + if yi == y0: + db[i, :] += [0, -1 / 2] + elif yi == y1: + db[i, :] += [0, +1 / 2] + + return abs(db) diff --git a/src/compas_tna/envelope/pointedvault.py b/src/compas_tna/envelope/pointedvault.py new file mode 100644 index 00000000..ec885dfa --- /dev/null +++ b/src/compas_tna/envelope/pointedvault.py @@ -0,0 +1,660 @@ +import math + +from numpy import array +from numpy import ones +from numpy import zeros + +from compas.datastructures import Mesh +from compas_tna.diagrams.diagram_rectangular import create_cross_mesh + + +def create_pointedvault_envelope( + cls, + x_span: tuple = (0.0, 10.0), + y_span: tuple = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + hc: float = 8.0, + he: list = None, + hm: list = None, + rho: float = 25.0, +): + """Create an envelope for a pointed cross vault geometry with given parameters. + + Parameters + ---------- + cls : class + The Envelope class to use for creating the envelope. + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + thickness : float, optional + Thickness of the vault, by default 0.50 + min_lb : float, optional + Parameter for lower bound in nodes in the boundary, by default 0.0 + n : int, optional + Number of vertices for the mesh, by default 100 + hc : float, optional + Height in the middle point of the vault, by default 8.0 + he : list, optional + Height of the opening mid-span for each of the quadrants, by default None + hm : list, optional + Height of each quadrant center (spadrel), by default None + rho : float, optional + Density of the material in kN/m³, by default 25.0 + + Returns + ------- + envelope : Envelope + The created envelope with intrados, extrados, and middle meshes. + """ + # Create base topology + base_topology = create_cross_mesh(x_span=x_span, y_span=y_span, n=n) + xyz0, faces_i = base_topology.to_vertices_and_faces() + xi, yi, _ = array(xyz0).transpose() + + # Create middle surface + zt = pointedvault_middle_update( + xi, yi, min_lb, x_span=x_span, y_span=y_span, hc=hc, he=he, hm=hm, tol=1e-6 + ) + xyzt = array([xi, yi, zt.flatten()]).transpose() + middle = Mesh.from_vertices_and_faces(xyzt, faces_i) + middle.update_default_vertex_attributes(thickness=thickness) + + # Create upper and lower bounds + zub, zlb = pointedvault_ub_lb_update( + xi, + yi, + thickness, + min_lb, + x_span=x_span, + y_span=y_span, + hc=hc, + he=he, + hm=hm, + tol=1e-6, + ) + xyzub = array([xi, yi, zub.flatten()]).transpose() + xyzlb = array([xi, yi, zlb.flatten()]).transpose() + + extrados = Mesh.from_vertices_and_faces(xyzub, faces_i) + intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) + + # Create envelope using the class method + envelope = cls.from_meshes(intrados, extrados, middle) + + # Set material properties + envelope.thickness = thickness + envelope.rho = rho + + envelope.type = 'pointedvault' + + return envelope + + +def pointedvault_middle_update( + x, + y, + min_lb, + x_span=(0.0, 10.0), + y_span=(0.0, 10.0), + hc=8.0, + he=None, + hm=None, + tol=1e-6, +): + """Update middle of a pointed vault based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + hc : float, optional + Height in the middle point of the vault, by default 8.0 + he : [float, float, float, float], optional + Height of the opening mid-span for each of the quadrants, by default None + hm : [float, float, float, float], optional + Height of each quadrant center (spadrel), by default None + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + middle : array + Values of the middle surface in the points + """ + + y1 = y_span[1] + y0 = y_span[0] + x1 = x_span[1] + x0 = x_span[0] + lx = x1 - x0 + ly = y1 - y0 + + if he and hm is None: + h1, k1, r1 = _circle_3points_xy([x0, he[1]], [(x1 + x0) / 2, hc], [x1, he[0]]) + h2, k2, r2 = h1, k1, r1 + h3, k3, r3 = _circle_3points_xy([y0, he[3]], [(y1 + y0) / 2, hc], [y1, he[2]]) + h4, k4, r4 = h3, k3, r3 + elif hm and he: + h1, k1, r1 = _circle_3points_xy( + [(x1 + x0) / 2, hc], [3 * (x1 + x0) / 4, hm[0]], [x1, he[0]] + ) + h2, k2, r2 = _circle_3points_xy( + [(x1 + x0) / 2, hc], [1 * (x1 + x0) / 4, hm[1]], [x0, he[1]] + ) + h3, k3, r3 = _circle_3points_xy( + [(y1 + y0) / 2, hc], [3 * (y1 + y0) / 4, hm[2]], [y1, he[2]] + ) + h4, k4, r4 = _circle_3points_xy( + [(y1 + y0) / 2, hc], [1 * (y1 + y0) / 4, hm[3]], [y0, he[3]] + ) + + middle = zeros((len(x), 1)) + + for i in range(len(x)): + xi, yi = x[i], y[i] + + if ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q1 + # Equation (xi - hx) ** 2 + (hi - kx) ** 2 = rx **2 to find the height of the pointed part (middle of quadrant) with that height one find the equivalent radius + if he: + hi = k1 + math.sqrt(r1**2 - (xi - h1) ** 2) + else: + hi = hc + ri = _find_r_given_h_l( + hi, ly + ) # This in the equation ri ** 2 = (xi - xc_) ** 2 + (zi - zc_) ** 2 -> zc = 0.0 and xc_ = (x0 + x1)/2 + if yi <= (y1 + y0) / 2: + zi = _sqrt((ri) ** 2 - (yi - (y0 + ri)) ** 2) + else: + zi = _sqrt((ri) ** 2 - (yi - (y1 - ri)) ** 2) + + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q3 + # Equation (xi - hy) ** 2 + (hi - ky) ** 2 = ry **2 to find the height of the pointed part (middle of quadrant) with that height one find the equivalent radius + if he: + hi = k3 + math.sqrt(r3**2 - (yi - h3) ** 2) + else: + hi = hc + ri = _find_r_given_h_l( + hi, lx + ) # This in the equation ri ** 2 = (xi - xc_) ** 2 + (zi - zc_) ** 2 -> zc = 0.0 and xc_ = (x0 + x1)/2 + if xi <= (x0 + x1) / 2: + zi = _sqrt((ri) ** 2 - (xi - (x0 + ri)) ** 2) + else: + zi = _sqrt((ri) ** 2 - (xi - (x1 - ri)) ** 2) + + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q2 + if he: + hi = k2 + math.sqrt(r2**2 - (xi - h2) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, ly) + if yi <= (y1 + y0) / 2: + zi = _sqrt((ri) ** 2 - (yi - (y0 + ri)) ** 2) + else: + zi = _sqrt((ri) ** 2 - (yi - (y1 - ri)) ** 2) + + elif ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q4 + if he: + hi = k4 + math.sqrt(r4**2 - (yi - h4) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, lx) + if xi <= (x0 + x1) / 2: + zi = _sqrt((ri) ** 2 - (xi - (x0 + ri)) ** 2) + else: + zi = _sqrt((ri) ** 2 - (xi - (x1 - ri)) ** 2) + + else: + print("Vertex did not belong to any Q. (x,y) = ({0},{1})".format(xi, yi)) + + middle[i] = zi + + return middle + + +def pointedvault_ub_lb_update( + x, + y, + thk, + min_lb, + x_span=(0.0, 10.0), + y_span=(0.0, 10.0), + hc=8.0, + he=None, + hm=None, + tol=1e-6, +): + """Update upper and lower bounds of a pointed vault based in the parameters + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the arch + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + hc : float, optional + Height in the middle point of the vault, by default 8.0 + he : [float, float, float, float], optional + Height of the opening mid-span for each of the quadrants, by default None + hm : [float, float, float, float], optional + Height of each quadrant center (spadrel), by default None + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + ub : array + Values of the upper bound in the points + lb : array + Values of the lower bound in the points + """ + + y1 = y_span[1] + y0 = y_span[0] + x1 = x_span[1] + x0 = x_span[0] + + y1_lb = y1 - thk / 2 + y0_lb = y0 + thk / 2 + x1_lb = x1 - thk / 2 + x0_lb = x0 + thk / 2 + + lx = x1 - x0 + ly = y1 - y0 + + if he: + he_ub = he.copy() + he_lb = he.copy() + for i in range(len(he)): + he_ub[i] += thk / 2 + he_lb[i] -= thk / 2 + if hm: + raise NotImplementedError() + + if he and hm is None: + + h1, k1, r1 = _circle_3points_xy([x0, he[1]], [(x1 + x0) / 2, hc], [x1, he[0]]) + h2, k2, r2 = h1, k1, r1 + h3, k3, r3 = _circle_3points_xy([y0, he[3]], [(y1 + y0) / 2, hc], [y1, he[2]]) + h4, k4, r4 = h3, k3, r3 + + ub = ones((len(x), 1)) + lb = ones((len(x), 1)) * -min_lb + + for i in range(len(x)): + xi, yi = x[i], y[i] + + if ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q1 + if he: + hi = k1 + math.sqrt(r1**2 - (xi - h1) ** 2) + else: + hi = hc + + ri = _find_r_given_h_l(hi, ly) + ri_ub = ri + thk / 2 + ri_lb = ri - thk / 2 + if yi <= (y1 + y0) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y1 - ri)) ** 2) + + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q3 + if he: + hi = k3 + math.sqrt(r3**2 - (yi - h3) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, lx) + ri_ub = ri + thk / 2 + ri_lb = ri - thk / 2 + if xi <= (x0 + x1) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x1 - ri)) ** 2) + + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q2 + if he: + hi = k2 + math.sqrt(r2**2 - (xi - h2) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, ly) + ri_lb = ri - thk / 2 + ri_ub = ri + thk / 2 + if yi <= (y1 + y0) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y1 - ri)) ** 2) + + elif ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q4 + if he: + hi = k4 + math.sqrt(r4**2 - (yi - h4) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, lx) + ri_ub = ri + thk / 2 + ri_lb = ri - thk / 2 + if xi <= (x0 + x1) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x1 - ri)) ** 2) + else: + print("Vertex did not belong to any Q. (x,y) = ({0},{1})".format(xi, yi)) + + if ((yi) > y1_lb and ((xi) > x1_lb or (xi) < x0_lb)) or ( + (yi) < y0_lb and ((xi) > x1_lb or (xi) < x0_lb) + ): + lb[i] = -1 * min_lb + + return ub, lb + + +def pointedvault_dub_dlb( + x, + y, + thk, + min_lb, + x_span=(0.0, 10.0), + y_span=(0.0, 10.0), + hc=8.0, + he=None, + hm=None, + tol=1e-6, +): + """Computes the sensitivities of upper and lower bounds in the x, y coordinates and thickness specified. + + Parameters + ---------- + x : list + x-coordinates of the points + y : list + y-coordinates of the points + thk : float + Thickness of the arch + min_lb : float + Parameter for lower bound in nodes in the boundary + x_span : tuple, optional + Span of the vault in x direction, by default (0.0, 10.0) + y_span : tuple, optional + Span of the vault in y direction, by default (0.0, 10.0) + hc : float, optional + Height in the middle point of the vault, by default 8.0 + he : [float, float, float, float], optional + Height of the opening mid-span for each of the quadrants, by default None + hm : [float, float, float, float], optional + Height of each quadrant center (spadrel), by default None + tol : float, optional + Tolerance, by default 1e-6 + + Returns + ------- + dub : array + Values of the sensitivities for the upper bound in the points + dlb : array + Values of the sensitivities for the lower bound in the points + """ + + y1 = y_span[1] + y0 = y_span[0] + x1 = x_span[1] + x0 = x_span[0] + + y1_lb = y1 - thk / 2 + y0_lb = y0 + thk / 2 + x1_lb = x1 - thk / 2 + x0_lb = x0 + thk / 2 + + lx = x1 - x0 + ly = y1 - y0 + + if he: + he_ub = he.copy() + he_lb = he.copy() + for i in range(len(he)): + he_ub[i] += thk / 2 + he_lb[i] -= thk / 2 + if hm: + raise NotImplementedError() + hm_ub = hm.copy() + hm_lb = hm.copy() + for i in range(len(hm)): + hm_ub[i] += thk / 2 + hm_lb[i] -= thk / 2 + + if he and hm is None: + h1, k1, r1 = _circle_3points_xy([x0, he[1]], [(x1 + x0) / 2, hc], [x1, he[0]]) + h2, k2, r2 = h1, k1, r1 + h3, k3, r3 = _circle_3points_xy([y0, he[3]], [(y1 + y0) / 2, hc], [y1, he[2]]) + h4, k4, r4 = h3, k3, r3 + + ub = ones((len(x), 1)) + lb = ones((len(x), 1)) * -min_lb + dub = zeros((len(x), 1)) + dlb = zeros((len(x), 1)) + + for i in range(len(x)): + xi, yi = x[i], y[i] + + if ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q1 + if he: + hi = k1 + math.sqrt(r1**2 - (xi - h1) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, ly) + ri_ub = ri + thk / 2 + ri_lb = ri - thk / 2 + if yi <= (y1 + y0) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y1 - ri)) ** 2) + dub[i] = 1 / 2 * ri_ub / ub[i] + dlb[i] = -1 / 2 * ri_lb / lb[i] + + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol + ): # Q3 + if he: + hi = k3 + math.sqrt(r3**2 - (yi - h3) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, lx) + ri_ub = ri + thk / 2 + ri_lb = ri - thk / 2 + if xi <= (x0 + x1) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x1 - ri)) ** 2) + dub[i] = 1 / 2 * ri_ub / ub[i] + dlb[i] = -1 / 2 * ri_lb / lb[i] + + elif ( + yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q2 + if he: + hi = k2 + math.sqrt(r2**2 - (xi - h2) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, ly) + ri_lb = ri - thk / 2 + ri_ub = ri + thk / 2 + if yi <= (y1 + y0) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y1 - ri)) ** 2) + dub[i] = 1 / 2 * ri_ub / ub[i] + dlb[i] = -1 / 2 * ri_lb / lb[i] + + elif ( + yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol + and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol + ): # Q4 + if he: + hi = k4 + math.sqrt(r4**2 - (yi - h4) ** 2) + else: + hi = hc + ri = _find_r_given_h_l(hi, lx) + ri_ub = ri + thk / 2 + ri_lb = ri - thk / 2 + if xi <= (x0 + x1) / 2: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x0 + ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x0 + ri)) ** 2) + else: + ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x1 - ri)) ** 2) + lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x1 - ri)) ** 2) + dub[i] = 1 / 2 * ri_ub / ub[i] + dlb[i] = -1 / 2 * ri_lb / lb[i] + else: + print("Vertex did not belong to any Q. (x,y) = ({0},{1})".format(xi, yi)) + + if ((yi) > y1_lb and ((xi) > x1_lb or (xi) < x0_lb)) or ( + (yi) < y0_lb and ((xi) > x1_lb or (xi) < x0_lb) + ): + lb[i] = -1 * min_lb + dlb[i] = 0.0 + + return dub, dlb # ub, lb + + +def pointedvault_bound_react_update( + x, + y, + thk, + min_lb, + x_span=(0.0, 10.0), + y_span=(0.0, 10.0), + hc=8.0, + he=None, + hm=None, + tol=1e-6, +): + """Compute the bounds on the reaction vector of the pointed cross vault.""" + pass + + +def pointedvault_db( + x, + y, + thk, + min_lb, + x_span=(0.0, 10.0), + y_span=(0.0, 10.0), + hc=8.0, + he=None, + hm=None, + tol=1e-6, +): + """Compute the sensitivities of the bounds on the reaction vector of the pointed cross vault.""" + pass + +def _find_r_given_h_l(h, length): + r = h**2 / length + length / 4 + + return r + + +def _circle_3points_xy(p1, p2, p3): + x1 = p1[0] + z1 = p1[1] + x2 = p2[0] + z2 = p2[1] + x3 = p3[0] + z3 = p3[1] + + x12 = x1 - x2 + x13 = x1 - x3 + z12 = z1 - z2 + z13 = z1 - z3 + z31 = z3 - z1 + z21 = z2 - z1 + x31 = x3 - x1 + x21 = x2 - x1 + + sx13 = x1**2 - x3**2 + sz13 = z1**2 - z3**2 + sx21 = x2**2 - x1**2 + sz21 = z2**2 - z1**2 + + f = ((sx13) * (x12) + (sz13) * (x12) + (sx21) * (x13) + (sz21) * (x13)) / ( + 2 * ((z31) * (x12) - (z21) * (x13)) + ) + g = ((sx13) * (z12) + (sz13) * (z12) + (sx21) * (z13) + (sz21) * (z13)) / ( + 2 * ((x31) * (z12) - (x21) * (z13)) + ) + c = -(x1**2) - z1**2 - 2 * g * x1 - 2 * f * z1 + h = -g + k = -f + r2 = h * h + k * k - c + r = math.sqrt(r2) + + return h, k, r + + +def _sqrt(x): + try: + sqrt_x = math.sqrt(x) + except BaseException: + if x > -10e4: + sqrt_x = math.sqrt(abs(x)) + else: + sqrt_x = 0.0 + return sqrt_x From 3940fd4836eb03a83eddfd024ed2e55be3772728 Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Fri, 29 Aug 2025 13:34:08 +0200 Subject: [PATCH 2/9] envelope class hierarchy update --- src/compas_tna/envelope/__init__.py | 6 +- src/compas_tna/envelope/crossvault.py | 307 +++++-------- src/compas_tna/envelope/dome.py | 105 ++++- src/compas_tna/envelope/envelope.py | 513 ++++++++++------------ src/compas_tna/envelope/pavillionvault.py | 162 ++++--- src/compas_tna/envelope/pointedvault.py | 203 +++++---- 6 files changed, 647 insertions(+), 649 deletions(-) diff --git a/src/compas_tna/envelope/__init__.py b/src/compas_tna/envelope/__init__.py index e5529efb..8bf31ac8 100644 --- a/src/compas_tna/envelope/__init__.py +++ b/src/compas_tna/envelope/__init__.py @@ -1,3 +1,7 @@ +from .crossvault import CrossVaultEnvelope +from .dome import DomeEnvelope from .envelope import Envelope +from .pavillionvault import PavillionVaultEnvelope +from .pointedvault import PointedVaultEnvelope -__all__ = ["Envelope"] +__all__ = ["Envelope", "PavillionVaultEnvelope", "PointedVaultEnvelope", "DomeEnvelope", "CrossVaultEnvelope"] diff --git a/src/compas_tna/envelope/crossvault.py b/src/compas_tna/envelope/crossvault.py index 249cf279..ea135958 100644 --- a/src/compas_tna/envelope/crossvault.py +++ b/src/compas_tna/envelope/crossvault.py @@ -1,28 +1,25 @@ +import math + from numpy import array from numpy import ones from numpy import zeros -import math - from compas.datastructures import Mesh from compas_tna.diagrams.diagram_rectangular import create_cross_mesh +from compas_tna.envelope.envelope import Envelope def create_crossvault_envelope( - cls, x_span: tuple = (0.0, 10.0), y_span: tuple = (0.0, 10.0), thickness: float = 0.50, min_lb: float = 0.0, n: int = 100, - rho: float = 25.0, ): """Create an envelope for a cross vault geometry with given parameters. Parameters ---------- - cls : class - The Envelope class to use for creating the envelope. x_span : tuple, optional Span of the vault in x direction, by default (0.0, 10.0) y_span : tuple, optional @@ -33,13 +30,15 @@ def create_crossvault_envelope( Parameter for lower bound in nodes in the boundary, by default 0.0 n : int, optional Number of vertices for the mesh, by default 100 - rho : float, optional - Density of the material in kN/m³, by default 25.0 Returns ------- - envelope : Envelope - The created envelope with intrados, extrados, and middle meshes. + middle : Mesh + Middle mesh + intrados : Mesh + Intrados mesh + extrados : Mesh + Extrados mesh """ # Create base topology base_topology = create_cross_mesh(x_span=x_span, y_span=y_span, n=n) @@ -47,38 +46,23 @@ def create_crossvault_envelope( xi, yi, _ = array(xyz0).transpose() # Create middle surface - zt = crossvault_middle_update( - xi, yi, min_lb, x_span=x_span, y_span=y_span, tol=1e-6 - ) + zt = crossvault_middle_update(xi, yi, min_lb, x_span=x_span, y_span=y_span, tol=1e-6) xyzt = array([xi, yi, zt.flatten()]).transpose() middle = Mesh.from_vertices_and_faces(xyzt, faces_i) middle.update_default_vertex_attributes(thickness=thickness) # Create upper and lower bounds - zub, zlb = crossvault_ub_lb_update( - xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, tol=1e-6 - ) + zub, zlb = crossvault_ub_lb_update(xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, tol=1e-6) xyzub = array([xi, yi, zub.flatten()]).transpose() xyzlb = array([xi, yi, zlb.flatten()]).transpose() extrados = Mesh.from_vertices_and_faces(xyzub, faces_i) intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) - # Create envelope using the class method - envelope = cls.from_meshes(intrados, extrados, middle) - - # Set material properties - envelope.thickness = thickness - envelope.rho = rho + return intrados, extrados, middle - envelope.type = 'crossvault' - return envelope - - -def crossvault_middle_update( - x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 -): +def crossvault_middle_update(x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Update middle of a crossvault based in the parameters Parameters @@ -127,25 +111,13 @@ def crossvault_middle_update( yd = y0 + (y1 - y0) / (x1 - x0) * (xi - x0) hxd = math.sqrt(abs((rx) ** 2 - ((xd - x0) - rx) ** 2)) hyd = math.sqrt(abs((ry) ** 2 - ((yd - y0) - ry) ** 2)) - if ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q1 + if yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q1 z[i] = hc * (hxd + math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2)) / (rx + ry) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q3 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q3 z[i] = hc * (hyd + math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2)) / (rx + ry) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q2 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q2 z[i] = hc * (hxd + math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2)) / (rx + ry) - elif ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q4 + elif yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q4 z[i] = hc * (hyd + math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2)) / (rx + ry) else: print("Vertex did not belong to any Q. (x,y) = ({0},{1})".format(xi, yi)) @@ -154,9 +126,7 @@ def crossvault_middle_update( return z -def crossvault_ub_lb_update( - x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 -): +def crossvault_ub_lb_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Update upper and lower bounds of an crossvault based in the parameters Parameters @@ -219,9 +189,7 @@ def crossvault_ub_lb_update( intrados_null = False - if (yi > y1_lb and (xi > x1_lb or xi < x0_lb)) or ( - yi < y0_lb and (xi > x1_lb or xi < x0_lb) - ): + if (yi > y1_lb and (xi > x1_lb or xi < x0_lb)) or (yi < y0_lb and (xi > x1_lb or xi < x0_lb)): intrados_null = True else: yi_intra = yi @@ -240,87 +208,29 @@ def crossvault_ub_lb_update( hxd_lb = _sqrt(((rx_lb) ** 2 - ((xd_lb - x0_lb) - rx_lb) ** 2)) hyd_lb = _sqrt(((ry_lb) ** 2 - ((yd_lb - y0_lb) - ry_lb) ** 2)) - if ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q1 - ub[i] = ( - hc_ub - * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) - / (rx_ub + ry_ub) - ) + if yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q1 + ub[i] = hc_ub * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) / (rx_ub + ry_ub) if not intrados_null: - lb[i] = ( - hc_lb - * ( - hxd_lb - + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q3 - ub[i] = ( - hc_ub - * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) - / (rx_ub + ry_ub) - ) + lb[i] = hc_lb * (hxd_lb + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2)) / (rx_lb + ry_lb) + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q3 + ub[i] = hc_ub * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) / (rx_ub + ry_ub) if not intrados_null: - lb[i] = ( - hc_lb - * ( - hyd_lb - + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q2 - ub[i] = ( - hc_ub - * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) - / (rx_ub + ry_ub) - ) + lb[i] = hc_lb * (hyd_lb + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2)) / (rx_lb + ry_lb) + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q2 + ub[i] = hc_ub * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) / (rx_ub + ry_ub) if not intrados_null: - lb[i] = ( - hc_lb - * ( - hxd_lb - + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) - elif ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q4 - ub[i] = ( - hc_ub - * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) - / (rx_ub + ry_ub) - ) + lb[i] = hc_lb * (hxd_lb + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2)) / (rx_lb + ry_lb) + elif yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q4 + ub[i] = hc_ub * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) / (rx_ub + ry_ub) if not intrados_null: - lb[i] = ( - hc_lb - * ( - hyd_lb - + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) + lb[i] = hc_lb * (hyd_lb + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2)) / (rx_lb + ry_lb) else: print("Error Q. (x,y) = ({0},{1})".format(xi, yi)) return ub, lb -def crossvault_dub_dlb( - x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 -): +def crossvault_dub_dlb(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Computes the sensitivities of upper and lower bounds in the x, y coordinates and thickness specified. Parameters @@ -393,9 +303,7 @@ def crossvault_dub_dlb( intrados_null = False - if (yi > y1_lb and (xi > x1_lb or xi < x0_lb)) or ( - yi < y0_lb and (xi > x1_lb or xi < x0_lb) - ): + if (yi > y1_lb and (xi > x1_lb or xi < x0_lb)) or (yi < y0_lb and (xi > x1_lb or xi < x0_lb)): intrados_null = True else: yi_intra = yi @@ -414,99 +322,43 @@ def crossvault_dub_dlb( hxd_lb = _sqrt(((rx_lb) ** 2 - ((xd_lb - x0_lb) - rx_lb) ** 2)) hyd_lb = _sqrt(((ry_lb) ** 2 - ((yd_lb - y0_lb) - ry_lb) ** 2)) - if ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q1 - ub[i] = ( - hc_ub - * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) - / (rx_ub + ry_ub) - ) + if yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q1 + ub[i] = hc_ub * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) / (rx_ub + ry_ub) dub[i] = 1 / 2 * ry_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) # dubdx[i, i] += 0.0 dubdy[i, i] += -(yi - yc) / ub[i] if not intrados_null: - lb[i] = ( - hc_lb - * ( - hxd_lb - + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) + lb[i] = hc_lb * (hxd_lb + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2)) / (rx_lb + ry_lb) dlb[i] = -1 / 2 * ry_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) # dlbdx[i, i] += 0.0 dlbdy[i, i] += -(yi - yc) / lb[i] - if ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q3 - ub[i] = ( - hc_ub - * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) - / (rx_ub + ry_ub) - ) + if yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q3 + ub[i] = hc_ub * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) / (rx_ub + ry_ub) dub[i] = 1 / 2 * rx_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) # dubdy[i, i] += 0.0 dubdx[i, i] += -(xi - xc) / ub[i] if not intrados_null: - lb[i] = ( - hc_lb - * ( - hyd_lb - + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) + lb[i] = hc_lb * (hyd_lb + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2)) / (rx_lb + ry_lb) dlb[i] = -1 / 2 * rx_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) # dlbdy[i, i] += 0.0 dlbdx[i, i] += -(xi - xc) / lb[i] - if ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q2 - ub[i] = ( - hc_ub - * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) - / (rx_ub + ry_ub) - ) + if yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q2 + ub[i] = hc_ub * (hxd_ub + math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2)) / (rx_ub + ry_ub) dub[i] = 1 / 2 * ry_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) # dubdx[i, i] += 0.0 dubdy[i, i] += -(yi - yc) / ub[i] if not intrados_null: - lb[i] = ( - hc_lb - * ( - hxd_lb - + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) + lb[i] = hc_lb * (hxd_lb + math.sqrt((ry_lb) ** 2 - ((yi_intra - y0_lb) - ry_lb) ** 2)) / (rx_lb + ry_lb) dlb[i] = -1 / 2 * ry_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) # dlbdx[i, i] += 0.0 dlbdy[i, i] += -(yi - yc) / lb[i] - if ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q4 - ub[i] = ( - hc_ub - * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) - / (rx_ub + ry_ub) - ) + if yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q4 + ub[i] = hc_ub * (hyd_ub + math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2)) / (rx_ub + ry_ub) dub[i] = 1 / 2 * rx_ub / ub[i] * hc_ub / ((rx_ub + ry_ub) / 2) # dubdy[i, i] += 0.0 dubdx[i, i] += -(xi - xc) / ub[i] if not intrados_null: - lb[i] = ( - hc_lb - * ( - hyd_lb - + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2) - ) - / (rx_lb + ry_lb) - ) + lb[i] = hc_lb * (hyd_lb + math.sqrt((rx_lb) ** 2 - ((xi_intra - x0_lb) - rx_lb) ** 2)) / (rx_lb + ry_lb) dlb[i] = -1 / 2 * rx_lb / lb[i] * hc_lb / ((rx_lb + ry_lb) / 2) # dlbdy[i, i] += 0.0 dlbdx[i, i] += -(xi - xc) / lb[i] @@ -516,9 +368,7 @@ def crossvault_dub_dlb( return dub, dlb, dubdx, dubdy, dlbdx, dlbdy -def crossvault_bound_react_update( - x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 -): +def crossvault_bound_react_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Compute the bounds on the reaction vector of the crossvault.""" pass @@ -538,3 +388,68 @@ def _sqrt(x): sqrt_x = 0.0 print("Problems to sqrt: ", x) return sqrt_x + + +class CrossVaultEnvelope(Envelope): + def __init__( + self, + x_span: tuple = (0.0, 10.0), + y_span: tuple = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + **kwargs, + ): + super().__init__(thickness=thickness, **kwargs) + self.x_span = x_span + self.y_span = y_span + self.min_lb = min_lb + self.n = n + + intrados, extrados, middle = create_crossvault_envelope(x_span=x_span, y_span=y_span, thickness=thickness, min_lb=min_lb, n=n) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + + @property + def __data__(self): + data = super().__data__ + data["x_span"] = self.x_span + data["y_span"] = self.y_span + data["min_lb"] = self.min_lb + data["n"] = self.n + return data + + def __str__(self): + return f"CrossVaultEnvelope(name={self.name})" + + def callable_middle(self, x, y): + return crossvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, tol=1e-6) + + def callable_ub_lb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return crossvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) + + def callable_dub_dlb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return crossvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) + + def callable_bound_react(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return crossvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) + + def callable_db(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return crossvault_db(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) diff --git a/src/compas_tna/envelope/dome.py b/src/compas_tna/envelope/dome.py index 6aa67d09..809d26e2 100644 --- a/src/compas_tna/envelope/dome.py +++ b/src/compas_tna/envelope/dome.py @@ -6,10 +6,10 @@ from compas.datastructures import Mesh from compas_tna.diagrams.diagram_circular import create_circular_radial_spaced_mesh +from compas_tna.envelope.envelope import Envelope def create_dome_envelope( - cls, center: tuple = (5.0, 5.0), radius: float = 5.0, thickness: float = 0.50, @@ -17,14 +17,11 @@ def create_dome_envelope( n_hoops: int = 24, n_parallels: int = 40, r_oculus: float = 0.0, - rho: float = 25.0, ): """Create an envelope for a dome geometry with given parameters. Parameters ---------- - cls : class - The Envelope class to use for creating the envelope. center : tuple, optional x, y coordinates of the center of the dome, by default (5.0, 5.0) radius : float, optional @@ -39,13 +36,15 @@ def create_dome_envelope( Number of parallels for the mesh, by default 40 r_oculus : float, optional Radius of the oculus (opening at the top), by default 0.0 - rho : float, optional - Density of the material in kN/m³, by default 25.0 Returns ------- - envelope : Envelope - The created envelope with intrados, extrados, and middle meshes. + middle : Mesh + Middle mesh + intrados : Mesh + Intrados mesh + extrados : Mesh + Extrados mesh """ # Create meshes for different radii for radius_current in [radius, radius - thickness / 2, radius + thickness / 2]: @@ -73,16 +72,7 @@ def create_dome_envelope( intrados.update_default_vertex_attributes(thickness=thickness) extrados.update_default_vertex_attributes(thickness=thickness) - # Create envelope using the class method - envelope = cls.from_meshes(intrados, extrados, middle) - - # Set material properties - envelope.thickness = thickness - envelope.rho = rho - - envelope.type = 'dome' - - return envelope + return intrados, extrados, middle def dome_middle_update(x, y, radius, min_lb, center=(5.0, 5.0)): @@ -290,3 +280,82 @@ def dome_db_sensitivity(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): db[i, :] = [x_, y_] return db + + +class DomeEnvelope(Envelope): + def __init__( + self, + center: tuple = (5.0, 5.0), + radius: float = 5.0, + thickness: float = 0.50, + min_lb: float = 0.0, + n_hoops: int = 24, + n_parallels: int = 40, + r_oculus: float = 0.0, + **kwargs, + ): + super().__init__(thickness=thickness, **kwargs) + self.center = center + self.radius = radius + self.min_lb = min_lb + self.n_hoops = n_hoops + self.n_parallels = n_parallels + self.r_oculus = r_oculus + + intrados, extrados, middle = create_dome_envelope( + center=center, + radius=radius, + thickness=thickness, + min_lb=min_lb, + n_hoops=n_hoops, + n_parallels=n_parallels, + r_oculus=r_oculus, + ) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + + @property + def __data__(self): + data = super().__data__ + data["center"] = self.center + data["radius"] = self.radius + data["min_lb"] = self.min_lb + data["n_hoops"] = self.n_hoops + data["n_parallels"] = self.n_parallels + data["r_oculus"] = self.r_oculus + return data + + def __str__(self): + return f"DomeEnvelope(name={self.name})" + + def callable_middle(self, x, y): + return dome_middle_update(x, y, self.radius, self.min_lb, self.center) + + def callable_ub_lb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return dome_ub_lb_update(x, y, thickness, self.min_lb, self.center, self.radius) + + def callable_dub_dlb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return dome_dub_dlb(x, y, thickness, self.min_lb, self.center, self.radius) + + def callable_bound_react(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return dome_bound_react_update(x, y, thickness, fixed, self.center, self.radius) + + def callable_db(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return dome_db_sensitivity(x, y, thickness, fixed, self.center, self.radius) diff --git a/src/compas_tna/envelope/envelope.py b/src/compas_tna/envelope/envelope.py index c2ebcff8..2d4a091b 100644 --- a/src/compas_tna/envelope/envelope.py +++ b/src/compas_tna/envelope/envelope.py @@ -1,18 +1,14 @@ -from typing import Optional, Type, Callable -from numpy import asarray -from scipy.interpolate import griddata import math +from typing import Optional + import numpy as np +from numpy import asarray +from scipy.interpolate import griddata from compas.data import Data from compas.datastructures import Mesh from compas_tna.diagrams import FormDiagram -from .crossvault import create_crossvault_envelope -from .dome import create_dome_envelope -from .pavillionvault import create_pavillionvault_envelope -from .pointedvault import create_pointedvault_envelope - # TODO: What if intrados and extrados are surfaces? def interpolate_middle_mesh(intrados: Mesh, extrados: Mesh) -> Mesh: @@ -44,17 +40,13 @@ def interpolate_middle_mesh(intrados: Mesh, extrados: Mesh) -> Mesh: middle_xy = asarray(middle.vertices_attributes("xy")) # Interpolate Z coordinates from both surfaces - zi = griddata( - intrados_points[:, :2], intrados_points[:, 2], middle_xy, method='linear' - ) - ze = griddata( - extrados_points[:, :2], extrados_points[:, 2], middle_xy, method='linear' - ) + zi = griddata(intrados_points[:, :2], intrados_points[:, 2], middle_xy, method="linear") + ze = griddata(extrados_points[:, :2], extrados_points[:, 2], middle_xy, method="linear") # First loop: set middle Z as average for i, key in enumerate(middle.vertices()): middle_z = (zi[i] + ze[i]) / 2.0 - middle.vertex_attribute(key, 'z', middle_z) + middle.vertex_attribute(key, "z", middle_z) # Second loop: calculate and set thickness using correct normals for i, key in enumerate(middle.vertices()): @@ -64,7 +56,7 @@ def interpolate_middle_mesh(intrados: Mesh, extrados: Mesh) -> Mesh: thickness = abs(z_diff) * abs(nz) else: thickness = abs(z_diff) - middle.vertex_attribute(key, 'thickness', thickness) + middle.vertex_attribute(key, "thickness", thickness) return middle @@ -99,7 +91,7 @@ def offset_from_middle(middle: Mesh, fixed_xy: bool = True) -> tuple[Mesh, Mesh] nx, ny, nz = middle.vertex_normal(key) # Get thickness for this specific vertex (should be normal-based) - thickness = middle.vertex_attribute(key, 'thickness') + thickness = middle.vertex_attribute(key, "thickness") if thickness is None: thickness = 0.5 half_thick = 0.5 * thickness @@ -107,24 +99,22 @@ def offset_from_middle(middle: Mesh, fixed_xy: bool = True) -> tuple[Mesh, Mesh] if fixed_xy: # Prevent division by zero for horizontal normals if abs(nz) < 1e-8: - raise ValueError( - f"Normal at vertex {key} is (almost) horizontal: {nx, ny, nz}" - ) + raise ValueError(f"Normal at vertex {key} is (almost) horizontal: {nx, ny, nz}") dz = half_thick / nz extrados_z = z + dz intrados_z = z - dz - extrados.vertex_attribute(key, 'z', extrados_z) - intrados.vertex_attribute(key, 'z', intrados_z) + extrados.vertex_attribute(key, "z", extrados_z) + intrados.vertex_attribute(key, "z", intrados_z) else: # Full 3D normal offset - this is the most accurate for curved surfaces extrados.vertex_attributes( key, - 'xyz', + "xyz", [x + half_thick * nx, y + half_thick * ny, z + half_thick * nz], ) intrados.vertex_attributes( key, - 'xyz', + "xyz", [x - half_thick * nx, y - half_thick * ny, z - half_thick * nz], ) @@ -155,14 +145,12 @@ def project_mesh_to_target_vertical(mesh: Mesh, target: Mesh) -> None: point = mesh.vertex_point(vertex) # Find the closest target vertex in XY plane - min_distance = float('inf') + min_distance = float("inf") closest_z = point.z for target_point in target_points: # Calculate XY distance (ignore Z) - xy_distance = ( - (point.x - target_point.x) ** 2 + (point.y - target_point.y) ** 2 - ) ** 0.5 + xy_distance = ((point.x - target_point.x) ** 2 + (point.y - target_point.y) ** 2) ** 0.5 if xy_distance < min_distance: min_distance = xy_distance @@ -174,9 +162,7 @@ def project_mesh_to_target_vertical(mesh: Mesh, target: Mesh) -> None: mesh.vertex_attributes(vertex, "xyz", new_point) -def pattern_inverse_height_thickness( - pattern: Mesh, tmin: Optional[float] = None, tmax: Optional[float] = None -) -> None: +def pattern_inverse_height_thickness(pattern: Mesh, tmin: Optional[float] = None, tmax: Optional[float] = None) -> None: """Set variable thickness based on inverse height. Parameters @@ -218,28 +204,34 @@ def pattern_inverse_height_thickness( class Envelope(Data): """Pure geometric envelope representing masonry structure boundaries.""" - def __init__(self, name=None): - super().__init__(name) + def __init__(self, intrados: Mesh = None, extrados: Mesh = None, middle: Mesh = None, fill: Mesh = None, rho: float = 20.0, thickness: float = 0.5, **kwargs): + super().__init__(**kwargs) - # Core geometric surfaces (required) - self.intrados: Optional[Mesh] = None - self.extrados: Optional[Mesh] = None - self.middle: Optional[Mesh] = None + # # Core geometric surfaces (required - already in BaseEnvelope) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + self.fill = fill + self.rho = rho - # Material properties - self._thickness = 0.5 - self._rho = 20.0 + # Thickness property + self._thickness = thickness # Computed properties (cached) self._area = 0.0 self._volume = 0.0 self._total_selfweight = 0.0 - # Optional fill surface - self.fill: Optional[Mesh] = None - - # Optional template to be used latter in the optimization process - self.type: Optional[str] = None + @property + def __data__(self): + data = {} + data["intrados"] = self.intrados + data["extrados"] = self.extrados + data["middle"] = self.middle + data["fill"] = self.fill + data["rho"] = self.rho + data["thickness"] = self._thickness + return data def __str__(self): return f"Envelope(name={self.name})" @@ -249,9 +241,7 @@ def __str__(self): # ============================================================================= @classmethod - def from_meshes( - cls, intrados: Mesh, extrados: Mesh, middle: Optional[Mesh] = None - ) -> "Envelope": + def from_meshes(cls, intrados: Mesh, extrados: Mesh, middle: Optional[Mesh] = None) -> "Envelope": """Construct an envelope from intrados and extrados meshes. Parameters @@ -279,9 +269,7 @@ def from_meshes( return envelope @classmethod - def from_formdiagram( - cls, formdiagram: FormDiagram, thickness: Optional[float] = None - ) -> "Envelope": + def from_formdiagram(cls, formdiagram: FormDiagram, thickness: Optional[float] = None) -> "Envelope": """Construct an envelope from a FormDiagram with specified thickness. Parameters @@ -299,9 +287,7 @@ def from_formdiagram( return cls.from_middle_mesh(formdiagram, thickness) @classmethod - def from_middle_mesh( - cls, mesh: Mesh, thickness: Optional[float] = None - ) -> "Envelope": + def from_middle_mesh(cls, mesh: Mesh, thickness: Optional[float] = None) -> "Envelope": """Construct an envelope from a mesh with specified thickness. Parameters @@ -330,170 +316,162 @@ def from_middle_mesh( return envelope - @classmethod - def from_crossvault( - cls, - x_span: tuple[float, float] = (0.0, 10.0), - y_span: tuple[float, float] = (0.0, 10.0), - thickness: float = 0.50, - min_lb: float = 0.0, - n: int = 100, - rho: float = 25.0, - ) -> "Envelope": - """Construct an envelope from a crossvault. - - Parameters - ---------- - x_span : tuple[float, float], optional - Span of the vault in x direction, by default (0.0, 10.0) - y_span : tuple[float, float], optional - Span of the vault in y direction, by default (0.0, 10.0) - thickness : float, optional - Thickness of the vault, by default 0.50 - min_lb : float, optional - Parameter for lower bound in nodes in the boundary, by default 0.0 - n : int, optional - Number of vertices for the mesh, by default 100 - rho : float, optional - Density of the material in kN/m³, by default 25.0 - - Returns - ------- - :class:`Envelope` - The created envelope with intrados, extrados, and middle meshes. - """ - return create_crossvault_envelope( - cls, x_span, y_span, thickness, min_lb, n, rho - ) - - @classmethod - def from_dome( - cls, - center: tuple[float, float] = (5.0, 5.0), - radius: float = 5.0, - thickness: float = 0.50, - min_lb: float = 0.0, - n_hoops: int = 24, - n_parallels: int = 40, - r_oculus: float = 0.0, - rho: float = 25.0, - ) -> "Envelope": - """Construct an envelope from a dome. - - Parameters - ---------- - center : tuple[float, float], optional - x, y coordinates of the center of the dome, by default (5.0, 5.0) - radius : float, optional - The radius of the dome, by default 5.0 - thickness : float, optional - Thickness of the dome, by default 0.50 - min_lb : float, optional - Parameter for lower bound in nodes in the boundary, by default 0.0 - n_hoops : int, optional - Number of hoops for the mesh, by default 24 - n_parallels : int, optional - Number of parallels for the mesh, by default 40 - r_oculus : float, optional - Radius of the oculus (opening at the top), by default 0.0 - rho : float, optional - Density of the material in kN/m³, by default 25.0 - - Returns - ------- - :class:`Envelope` - The created envelope with intrados, extrados, and middle meshes. - """ - return create_dome_envelope( - cls, center, radius, thickness, min_lb, n_hoops, n_parallels, r_oculus, rho - ) - - @classmethod - def from_pavillionvault( - cls, - x_span: tuple[float, float] = (0.0, 10.0), - y_span: tuple[float, float] = (0.0, 10.0), - thickness: float = 0.50, - min_lb: float = 0.0, - n: int = 100, - spr_angle: float = 0.0, - expanded: bool = False, - rho: float = 25.0, - ) -> "Envelope": - """Construct an envelope from a pavillion vault. - - Parameters - ---------- - x_span : tuple[float, float], optional - Span of the vault in x direction, by default (0.0, 10.0) - y_span : tuple[float, float], optional - Span of the vault in y direction, by default (0.0, 10.0) - thickness : float, optional - Thickness of the vault, by default 0.50 - min_lb : float, optional - Parameter for lower bound in nodes in the boundary, by default 0.0 - n : int, optional - Number of vertices for the mesh, by default 100 - spr_angle : float, optional - Springing angle, by default 0.0 - expanded : bool, optional - If the extrados should extend beyond the floor plan, by default False - rho : float, optional - Density of the material in kN/m³, by default 25.0 - - Returns - ------- - :class:`Envelope` - The created envelope with intrados, extrados, and middle meshes. - """ - return create_pavillionvault_envelope( - cls, x_span, y_span, thickness, min_lb, n, spr_angle, expanded, rho - ) - - @classmethod - def from_pointedvault( - cls, - x_span: tuple[float, float] = (0.0, 10.0), - y_span: tuple[float, float] = (0.0, 10.0), - thickness: float = 0.50, - min_lb: float = 0.0, - n: int = 100, - hc: float = 8.0, - he: list = None, - hm: list = None, - rho: float = 25.0, - ) -> "Envelope": - """Construct an envelope from a pointed cross vault. - - Parameters - ---------- - x_span : tuple[float, float], optional - Span of the vault in x direction, by default (0.0, 10.0) - y_span : tuple[float, float], optional - Span of the vault in y direction, by default (0.0, 10.0) - thickness : float, optional - Thickness of the vault, by default 0.50 - min_lb : float, optional - Parameter for lower bound in nodes in the boundary, by default 0.0 - n : int, optional - Number of vertices for the mesh, by default 100 - hc : float, optional - Height in the middle point of the vault, by default 8.0 - he : list, optional - Height of the opening mid-span for each of the quadrants, by default None - hm : list, optional - Height of each quadrant center (spadrel), by default None - rho : float, optional - Density of the material in kN/m³, by default 25.0 - - Returns - ------- - :class:`Envelope` - The created envelope with intrados, extrados, and middle meshes. - """ - return create_pointedvault_envelope( - cls, x_span, y_span, thickness, min_lb, n, hc, he, hm, rho - ) + # @classmethod + # def from_crossvault( + # cls, + # x_span: tuple[float, float] = (0.0, 10.0), + # y_span: tuple[float, float] = (0.0, 10.0), + # thickness: float = 0.50, + # min_lb: float = 0.0, + # n: int = 100, + # rho: float = 25.0, + # ) -> "Envelope": + # """Construct an envelope from a crossvault. + + # Parameters + # ---------- + # x_span : tuple[float, float], optional + # Span of the vault in x direction, by default (0.0, 10.0) + # y_span : tuple[float, float], optional + # Span of the vault in y direction, by default (0.0, 10.0) + # thickness : float, optional + # Thickness of the vault, by default 0.50 + # min_lb : float, optional + # Parameter for lower bound in nodes in the boundary, by default 0.0 + # n : int, optional + # Number of vertices for the mesh, by default 100 + # rho : float, optional + # Density of the material in kN/m³, by default 25.0 + + # Returns + # ------- + # :class:`Envelope` + # The created envelope with intrados, extrados, and middle meshes. + # """ + # return create_crossvault_envelope(cls, x_span, y_span, thickness, min_lb, n, rho) + + # @classmethod + # def from_dome( + # cls, + # center: tuple[float, float] = (5.0, 5.0), + # radius: float = 5.0, + # thickness: float = 0.50, + # min_lb: float = 0.0, + # n_hoops: int = 24, + # n_parallels: int = 40, + # r_oculus: float = 0.0, + # rho: float = 25.0, + # ) -> "Envelope": + # """Construct an envelope from a dome. + + # Parameters + # ---------- + # center : tuple[float, float], optional + # x, y coordinates of the center of the dome, by default (5.0, 5.0) + # radius : float, optional + # The radius of the dome, by default 5.0 + # thickness : float, optional + # Thickness of the dome, by default 0.50 + # min_lb : float, optional + # Parameter for lower bound in nodes in the boundary, by default 0.0 + # n_hoops : int, optional + # Number of hoops for the mesh, by default 24 + # n_parallels : int, optional + # Number of parallels for the mesh, by default 40 + # r_oculus : float, optional + # Radius of the oculus (opening at the top), by default 0.0 + # rho : float, optional + # Density of the material in kN/m³, by default 25.0 + + # Returns + # ------- + # :class:`Envelope` + # The created envelope with intrados, extrados, and middle meshes. + # """ + # return create_dome_envelope(cls, center, radius, thickness, min_lb, n_hoops, n_parallels, r_oculus, rho) + + # @classmethod + # def from_pavillionvault( + # cls, + # x_span: tuple[float, float] = (0.0, 10.0), + # y_span: tuple[float, float] = (0.0, 10.0), + # thickness: float = 0.50, + # min_lb: float = 0.0, + # n: int = 100, + # spr_angle: float = 0.0, + # expanded: bool = False, + # rho: float = 25.0, + # ) -> "Envelope": + # """Construct an envelope from a pavillion vault. + + # Parameters + # ---------- + # x_span : tuple[float, float], optional + # Span of the vault in x direction, by default (0.0, 10.0) + # y_span : tuple[float, float], optional + # Span of the vault in y direction, by default (0.0, 10.0) + # thickness : float, optional + # Thickness of the vault, by default 0.50 + # min_lb : float, optional + # Parameter for lower bound in nodes in the boundary, by default 0.0 + # n : int, optional + # Number of vertices for the mesh, by default 100 + # spr_angle : float, optional + # Springing angle, by default 0.0 + # expanded : bool, optional + # If the extrados should extend beyond the floor plan, by default False + # rho : float, optional + # Density of the material in kN/m³, by default 25.0 + + # Returns + # ------- + # :class:`Envelope` + # The created envelope with intrados, extrados, and middle meshes. + # """ + # return create_pavillionvault_envelope(cls, x_span, y_span, thickness, min_lb, n, spr_angle, expanded, rho) + + # @classmethod + # def from_pointedvault( + # cls, + # x_span: tuple[float, float] = (0.0, 10.0), + # y_span: tuple[float, float] = (0.0, 10.0), + # thickness: float = 0.50, + # min_lb: float = 0.0, + # n: int = 100, + # hc: float = 8.0, + # he: list = None, + # hm: list = None, + # rho: float = 25.0, + # ) -> "Envelope": + # """Construct an envelope from a pointed cross vault. + + # Parameters + # ---------- + # x_span : tuple[float, float], optional + # Span of the vault in x direction, by default (0.0, 10.0) + # y_span : tuple[float, float], optional + # Span of the vault in y direction, by default (0.0, 10.0) + # thickness : float, optional + # Thickness of the vault, by default 0.50 + # min_lb : float, optional + # Parameter for lower bound in nodes in the boundary, by default 0.0 + # n : int, optional + # Number of vertices for the mesh, by default 100 + # hc : float, optional + # Height in the middle point of the vault, by default 8.0 + # he : list, optional + # Height of the opening mid-span for each of the quadrants, by default None + # hm : list, optional + # Height of each quadrant center (spadrel), by default None + # rho : float, optional + # Density of the material in kN/m³, by default 25.0 + + # Returns + # ------- + # :class:`Envelope` + # The created envelope with intrados, extrados, and middle meshes. + # """ + # return create_pointedvault_envelope(cls, x_span, y_span, thickness, min_lb, n, hc, he, hm, rho) # ============================================================================= # Properties @@ -533,7 +511,7 @@ def thickness(self) -> float: # Return average thickness from middle mesh vertices thicknesses = [] for key in self.middle.vertices(): - thickness = self.middle.vertex_attribute(key, 'thickness') + thickness = self.middle.vertex_attribute(key, "thickness") if thickness is not None: thicknesses.append(thickness) @@ -556,7 +534,7 @@ def thickness(self, value: float) -> None: # Update middle mesh if it exists if self.middle is not None: for key in self.middle.vertices(): - self.middle.vertex_attribute(key, 'thickness', value) + self.middle.vertex_attribute(key, "thickness", value) @property def rho(self) -> float: @@ -584,9 +562,7 @@ def rho(self, value: float) -> None: # Geometric operations # ============================================================================= - def set_variable_thickness( - self, tmin: Optional[float] = None, tmax: Optional[float] = None - ) -> None: + def set_variable_thickness(self, tmin: Optional[float] = None, tmax: Optional[float] = None) -> None: """Set variable thickness based on inverse height using the pattern_inverse_height_thickness function. This method applies thickness variation based on the height of vertices in the middle mesh, @@ -600,9 +576,7 @@ def set_variable_thickness( Maximum thickness. If None, will be calculated as 50/1000 of the diagonal of the xy bounding box. """ if self.middle is None: - raise ValueError( - "Middle mesh is not available. Cannot set variable thickness." - ) + raise ValueError("Middle mesh is not available. Cannot set variable thickness.") # Apply the pattern_inverse_height_thickness function to the middle mesh pattern_inverse_height_thickness(self.middle, tmin=tmin, tmax=tmax) @@ -627,7 +601,7 @@ def compute_volume(self) -> float: # Use variable thickness from middle mesh vertices for vertex in middle.vertices(): - thickness = middle.vertex_attribute(vertex, 'thickness') + thickness = middle.vertex_attribute(vertex, "thickness") if thickness is None: thickness = self._thickness vertex_area = middle.vertex_area(vertex) # should be projected area @@ -649,9 +623,7 @@ def compute_selfweight(self) -> float: if self.intrados is not None and self.extrados is not None: self.middle = interpolate_middle_mesh(self.intrados, self.extrados) else: - raise ValueError( - "Middle mesh is not available and cannot be interpolated." - ) + raise ValueError("Middle mesh is not available and cannot be interpolated.") middle = self.middle rho = self.rho @@ -659,7 +631,7 @@ def compute_selfweight(self) -> float: # Use variable thickness from middle mesh vertices for vertex in middle.vertices(): - thickness = middle.vertex_attribute(vertex, 'thickness') + thickness = middle.vertex_attribute(vertex, "thickness") if thickness is None: thickness = self._thickness vertex_area = middle.vertex_area(vertex) @@ -673,9 +645,7 @@ def compute_selfweight(self) -> float: # TNA-specific operations (accept formdiagram as parameter) # ============================================================================= - def apply_selfweight_to_formdiagram( - self, formdiagram: FormDiagram, normalize=True - ) -> None: + def apply_selfweight_to_formdiagram(self, formdiagram: FormDiagram, normalize=True) -> None: """Apply selfweight to the nodes of a form diagram based on the middle surface and local thicknesses. Parameters @@ -696,9 +666,7 @@ def apply_selfweight_to_formdiagram( if self.intrados is not None and self.extrados is not None: self.middle = interpolate_middle_mesh(self.intrados, self.extrados) else: - raise ValueError( - "Middle mesh is not set. Please set the middle mesh before applying selfweight." - ) + raise ValueError("Middle mesh is not set. Please set the middle mesh before applying selfweight.") # Step 2: Compute the selfweight of the shell total_selfweight = self.compute_selfweight() @@ -715,20 +683,20 @@ def apply_selfweight_to_formdiagram( xy = np.array(form_.vertices_attributes("xy")) zt = list(self.callable_zt(xy[:, 0], xy[:, 1]).flatten().tolist()) for i, key in enumerate(form_.vertices()): - form_.vertex_attribute(key, 'z', zt[i]) + form_.vertex_attribute(key, "z", zt[i]) # Step 5: Compute and lump selfweight at vertices total_pz = 0.0 for vertex in form_.vertices(): # Get vertex area and thickness vertex_area = form_.vertex_area(vertex) - thickness = form_.vertex_attribute(vertex, 'thickness') + thickness = form_.vertex_attribute(vertex, "thickness") # Compute selfweight contribution (negative for downward direction) pz = -vertex_area * thickness * self.rho # Store in form diagram - formdiagram.vertex_attribute(vertex, 'pz', pz) + formdiagram.vertex_attribute(vertex, "pz", pz) total_pz += abs(pz) # Sum absolute values for normalization # Step 6: Scale to match total selfweight if normalize=True @@ -738,12 +706,10 @@ def apply_selfweight_to_formdiagram( print(f"Scaled selfweight by factor: {scale_factor}") for vertex in formdiagram.vertices(): - pz = formdiagram.vertex_attribute(vertex, 'pz') - formdiagram.vertex_attribute(vertex, 'pz', pz * scale_factor) + pz = formdiagram.vertex_attribute(vertex, "pz") + formdiagram.vertex_attribute(vertex, "pz", pz * scale_factor) - print( - f"Selfweight applied to form diagram. Total load: {sum(abs(formdiagram.vertex_attribute(vertex, 'pz')) for vertex in formdiagram.vertices())}" - ) + print(f"Selfweight applied to form diagram. Total load: {sum(abs(formdiagram.vertex_attribute(vertex, 'pz')) for vertex in formdiagram.vertices())}") def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: """Apply envelope bounds to a form diagram based on the intrados and extrados surfaces. @@ -763,9 +729,7 @@ def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: """ # Step 1: Check that intrados and extrados are present if self.intrados is None or self.extrados is None: - raise ValueError( - "Intra/Extrados not set. Please set them before applying bounds." - ) + raise ValueError("Intra/Extrados not set. Please set them before applying bounds.") # Step 2: Copy the form diagram for projection form_ub = formdiagram.copy() # For upper bound (extrados) @@ -779,8 +743,8 @@ def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: xy = np.array(form_ub.vertices_attributes("xy")) zub, zlb = self.callable_ub_lb(xy[:, 0], xy[:, 1]) for i, key in enumerate(form_ub.vertices()): - form_ub.vertex_attribute(key, 'z', float(zub[i])) - form_lb.vertex_attribute(key, 'z', float(zlb[i])) + form_ub.vertex_attribute(key, "z", float(zub[i])) + form_lb.vertex_attribute(key, "z", float(zlb[i])) # Step 4: Collect heights and assign to form diagram for vertex in formdiagram.vertices(): @@ -790,13 +754,13 @@ def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: _, _, z_lb = form_lb.vertex_coordinates(vertex) # Assign to form diagram - formdiagram.vertex_attribute(vertex, 'ub', z_ub) - formdiagram.vertex_attribute(vertex, 'lb', z_lb) + formdiagram.vertex_attribute(vertex, "ub", z_ub) + formdiagram.vertex_attribute(vertex, "lb", z_lb) else: print(f"Warning: Vertex {vertex} not found in projected meshes") # Set default values if vertex not found - formdiagram.vertex_attribute(vertex, 'ub', float('inf')) - formdiagram.vertex_attribute(vertex, 'lb', float('-inf')) + formdiagram.vertex_attribute(vertex, "ub", float("inf")) + formdiagram.vertex_attribute(vertex, "lb", float("-inf")) def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: """Apply target heights to a form diagram based on the Envelope middle surface. @@ -805,9 +769,7 @@ def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. """ if self.middle is None: - raise ValueError( - "Middle mesh is not set. Please set the middle mesh before applying target heights." - ) + raise ValueError("Middle mesh is not set. Please set the middle mesh before applying target heights.") # Step 1: Copy the form diagram for projection form_target = formdiagram.copy() # For upper bound (extrados) @@ -819,13 +781,13 @@ def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: xy = np.array(form_target.vertices_attributes("xy")) zt = list(self.callable_zt(xy[:, 0], xy[:, 1]).flatten().tolist()) for i, key in enumerate(form_target.vertices()): - form_target.vertex_attribute(key, 'z', zt[i]) + form_target.vertex_attribute(key, "z", zt[i]) # Step 3: Collect heights and assign to form diagram for vertex in formdiagram.vertices(): if vertex in form_target.vertices(): - z_target = form_target.vertex_attribute(vertex, 'z') - formdiagram.vertex_attribute(vertex, 'target', z_target) + z_target = form_target.vertex_attribute(vertex, "z") + formdiagram.vertex_attribute(vertex, "target", z_target) def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: """Apply reaction bounds to a form diagram based on the Envelope middle surface. @@ -835,15 +797,11 @@ def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None """ if not self.callable_bound_react: - raise ValueError( - "Callable bound reaction is not set. Please set this limit manually." - ) + raise ValueError("Callable bound reaction is not set. Please set this limit manually.") ## TODO: Implement this - def sync_thickness_to_formdiagram( - self, formdiagram: FormDiagram, method='linear', extrapolate=True - ) -> None: + def sync_thickness_to_formdiagram(self, formdiagram: FormDiagram, method="linear", extrapolate=True) -> None: """Synchronize thickness attributes from middle mesh to form diagram using continuous interpolation. This method creates a continuous thickness map from the middle mesh and interpolates @@ -864,9 +822,7 @@ def sync_thickness_to_formdiagram( # Validate data if not middle_xy or not middle_thickness: - raise ValueError( - "Middle mesh must have both 'xy' and 'thickness' attributes." - ) + raise ValueError("Middle mesh must have both 'xy' and 'thickness' attributes.") # Convert to numpy arrays middle_xy_array = asarray(middle_xy) @@ -897,10 +853,25 @@ def sync_thickness_to_formdiagram( # Ensure thickness is positive and reasonable if thickness_value <= 0 or math.isnan(thickness_value): thickness_value = self._thickness - formdiagram.vertex_attribute(vertex, 'thickness', thickness_value) + formdiagram.vertex_attribute(vertex, "thickness", thickness_value) except Exception as e: print(f"Warning: Interpolation failed, using default thickness. Error: {e}") # Fallback: assign default thickness to all vertices for vertex in formdiagram.vertices(): - formdiagram.vertex_attribute(vertex, 'thickness', self._thickness) + formdiagram.vertex_attribute(vertex, "thickness", self._thickness) + + def callable_middle(self, x, y): + return NotImplementedError("Callable middle update only available for analytical envelopes") + + def callable_ub_lb(self, x, y, thickness): + return NotImplementedError("Callable ub_lb update only available for analytical envelopes") + + def callable_dub_dlb(self, x, y): + return NotImplementedError("Callable dub_dlb only available for analytical envelopes") + + def callable_bound_react(self, x, y, thickness, fixed): + return NotImplementedError("Callable bound_react update only available for analytical envelopes") + + def callable_db(self, x, y, thickness, fixed): + return NotImplementedError("Callable db only available for analytical envelopes") diff --git a/src/compas_tna/envelope/pavillionvault.py b/src/compas_tna/envelope/pavillionvault.py index cdfc500a..4486b87d 100644 --- a/src/compas_tna/envelope/pavillionvault.py +++ b/src/compas_tna/envelope/pavillionvault.py @@ -6,25 +6,21 @@ from compas.datastructures import Mesh from compas_tna.diagrams.diagram_rectangular import create_cross_mesh +from compas_tna.envelope.envelope import Envelope def create_pavillionvault_envelope( - cls, x_span: tuple = (0.0, 10.0), y_span: tuple = (0.0, 10.0), thickness: float = 0.50, min_lb: float = 0.0, n: int = 100, spr_angle: float = 0.0, - expanded: bool = False, - rho: float = 25.0, ): """Create an envelope for a pavillion vault geometry with given parameters. Parameters ---------- - cls : class - The Envelope class to use for creating the envelope. x_span : tuple, optional Span of the vault in x direction, by default (0.0, 10.0) y_span : tuple, optional @@ -37,15 +33,15 @@ def create_pavillionvault_envelope( Number of vertices for the mesh, by default 100 spr_angle : float, optional Springing angle, by default 0.0 - expanded : bool, optional - If the extrados should extend beyond the floor plan, by default False - rho : float, optional - Density of the material in kN/m³, by default 25.0 Returns ------- - envelope : Envelope - The created envelope with intrados, extrados, and middle meshes. + intrados : Mesh + Intrados mesh + extrados : Mesh + Extrados mesh + middle : Mesh + Middle mesh """ # Create base topology base_topology = create_cross_mesh(x_span=x_span, y_span=y_span, n=n) @@ -53,9 +49,7 @@ def create_pavillionvault_envelope( xi, yi, _ = array(xyz0).transpose() # Create middle surface - zt = pavillionvault_middle_update( - xi, yi, x_span=x_span, y_span=y_span, spr_angle=spr_angle, tol=1e-6 - ) + zt = pavillionvault_middle_update(xi, yi, x_span=x_span, y_span=y_span, spr_angle=spr_angle, tol=1e-6) xyzt = array([xi, yi, zt.flatten()]).transpose() middle = Mesh.from_vertices_and_faces(xyzt, faces_i) middle.update_default_vertex_attributes(thickness=thickness) @@ -77,21 +71,10 @@ def create_pavillionvault_envelope( extrados = Mesh.from_vertices_and_faces(xyzub, faces_i) intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) - # Create envelope using the class method - envelope = cls.from_meshes(intrados, extrados, middle) - - # Set material properties - envelope.thickness = thickness - envelope.rho = rho - - envelope.type = 'pavillionvault' - - return envelope + return intrados, extrados, middle -def pavillionvault_middle_update( - x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6 -): +def pavillionvault_middle_update(x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): """Update middle of a pavillion vault based in the parameters Parameters @@ -135,21 +118,13 @@ def pavillionvault_middle_update( for i in range(len(x)): xi, yi = x[i], y[i] - if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - ( - xi - x0 - ) + tol: # Q1 + if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - (xi - x0) + tol: # Q1 z[i] = math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2) - z_ - elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - ( - xi - x0 - ) - tol: # Q3 + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - (xi - x0) - tol: # Q3 z[i] = math.sqrt((ry) ** 2 - ((yi - y0) - ry) ** 2) - z_ - elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - ( - xi - x0 - ) - tol: # Q2 + elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - (xi - x0) - tol: # Q2 z[i] = math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2) - z_ - elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - ( - xi - x0 - ) + tol: # Q4 + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - (xi - x0) + tol: # Q4 z[i] = math.sqrt((rx) ** 2 - ((xi - x0) - rx) ** 2) - z_ else: print("Error Q. (x,y) = ({0},{1})".format(xi, yi)) @@ -157,9 +132,7 @@ def pavillionvault_middle_update( return z -def pavillionvault_ub_lb_update( - x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6 -): +def pavillionvault_ub_lb_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): """Update upper and lower bounds of a pavillionvault based in the parameters Parameters @@ -225,27 +198,19 @@ def pavillionvault_ub_lb_update( intrados_null = False if yi > y1_lb or xi > x1_lb or xi < x0_lb or yi < y0_lb: intrados_null = True - if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - ( - xi - x0 - ) + tol: # Q1 + if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - (xi - x0) + tol: # Q1 ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) - z_ if not intrados_null: lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) - z_ - elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - ( - xi - x0 - ) - tol: # Q3 + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - (xi - x0) - tol: # Q3 ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) - z_ if not intrados_null: lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) - z_ - elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - ( - xi - x0 - ) - tol: # Q2 + elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - (xi - x0) - tol: # Q2 ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) - z_ if not intrados_null: lb[i] = math.sqrt((rx_lb) ** 2 - ((xi - x0_lb) - rx_lb) ** 2) - z_ - elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - ( - xi - x0 - ) + tol: # Q4 + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - (xi - x0) + tol: # Q4 ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) - z_ if not intrados_null: lb[i] = math.sqrt((rx_lb) ** 2 - ((xi - x0_lb) - rx_lb) ** 2) - z_ @@ -255,9 +220,7 @@ def pavillionvault_ub_lb_update( return ub, lb -def pavillionvault_dub_dlb( - x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6 -): +def pavillionvault_dub_dlb(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Computes the sensitivities of upper and lower bounds in the x, y coordinates and thickness specified. Parameters @@ -313,33 +276,25 @@ def pavillionvault_dub_dlb( intrados_null = False if yi > y1_lb or xi > x1_lb or xi < x0_lb or yi < y0_lb: intrados_null = True - if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - ( - xi - x0 - ) + tol: # Q1 + if (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) <= (y1 - y0) - (xi - x0) + tol: # Q1 ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) dub[i] = 1 / 2 * ry_ub / ub[i] if not intrados_null: lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) dlb[i] = -1 / 2 * ry_lb / lb[i] - elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - ( - xi - x0 - ) - tol: # Q3 + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) >= (y1 - y0) - (xi - x0) - tol: # Q3 ub[i] = math.sqrt((ry_ub) ** 2 - ((yi - y0_ub) - ry_ub) ** 2) dub[i] = 1 / 2 * ry_ub / ub[i] if not intrados_null: lb[i] = math.sqrt((ry_lb) ** 2 - ((yi - y0_lb) - ry_lb) ** 2) dlb[i] = -1 / 2 * ry_lb / lb[i] - elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - ( - xi - x0 - ) - tol: # Q2 + elif (yi - y0) <= y1 / x1 * (xi - x0) + tol and (yi - y0) >= (y1 - y0) - (xi - x0) - tol: # Q2 ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) dub[i] = 1 / 2 * rx_ub / ub[i] if not intrados_null: lb[i] = math.sqrt((rx_lb) ** 2 - ((xi - x0_lb) - rx_lb) ** 2) dlb[i] = -1 / 2 * rx_lb / lb[i] - elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - ( - xi - x0 - ) + tol: # Q4 + elif (yi - y0) >= y1 / x1 * (xi - x0) - tol and (yi - y0) <= (y1 - y0) - (xi - x0) + tol: # Q4 ub[i] = math.sqrt((rx_ub) ** 2 - ((xi - x0_ub) - rx_ub) ** 2) dub[i] = 1 / 2 * rx_ub / ub[i] if not intrados_null: @@ -435,3 +390,72 @@ def pavillionvault_db(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): db[i, :] += [0, +1 / 2] return abs(db) + + +class PavillionVaultEnvelope(Envelope): + def __init__( + self, + x_span: tuple = (0.0, 10.0), + y_span: tuple = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + spr_angle: float = 0.0, + **kwargs, + ): + super().__init__(thickness=thickness, **kwargs) + self.x_span = x_span + self.y_span = y_span + self.min_lb = min_lb + self.n = n + self.spr_angle = spr_angle + + intrados, extrados, middle = create_pavillionvault_envelope(x_span=x_span, y_span=y_span, thickness=thickness, min_lb=min_lb, n=n, spr_angle=spr_angle) + + self.intrados = intrados + self.extrados = extrados + self.middle = middle + + @property + def __data__(self): + data = super().__data__ + data["x_span"] = self.x_span + data["y_span"] = self.y_span + data["min_lb"] = self.min_lb + data["n"] = self.n + data["spr_angle"] = self.spr_angle + return data + + def __str__(self): + return f"PavillionVaultEnvelope(name={self.name})" + + def callable_middle(self, x, y): + return pavillionvault_middle_update(x, y, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) + + def callable_ub_lb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pavillionvault_ub_lb_update(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) + + def callable_dub_dlb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pavillionvault_dub_dlb(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, tol=1e-6) + + def callable_bound_react(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pavillionvault_bound_react_update(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) + + def callable_db(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pavillionvault_db(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) diff --git a/src/compas_tna/envelope/pointedvault.py b/src/compas_tna/envelope/pointedvault.py index ec885dfa..56c50a1a 100644 --- a/src/compas_tna/envelope/pointedvault.py +++ b/src/compas_tna/envelope/pointedvault.py @@ -6,10 +6,10 @@ from compas.datastructures import Mesh from compas_tna.diagrams.diagram_rectangular import create_cross_mesh +from compas_tna.envelope.envelope import Envelope def create_pointedvault_envelope( - cls, x_span: tuple = (0.0, 10.0), y_span: tuple = (0.0, 10.0), thickness: float = 0.50, @@ -18,7 +18,6 @@ def create_pointedvault_envelope( hc: float = 8.0, he: list = None, hm: list = None, - rho: float = 25.0, ): """Create an envelope for a pointed cross vault geometry with given parameters. @@ -56,9 +55,7 @@ def create_pointedvault_envelope( xi, yi, _ = array(xyz0).transpose() # Create middle surface - zt = pointedvault_middle_update( - xi, yi, min_lb, x_span=x_span, y_span=y_span, hc=hc, he=he, hm=hm, tol=1e-6 - ) + zt = pointedvault_middle_update(xi, yi, min_lb, x_span=x_span, y_span=y_span, hc=hc, he=he, hm=hm, tol=1e-6) xyzt = array([xi, yi, zt.flatten()]).transpose() middle = Mesh.from_vertices_and_faces(xyzt, faces_i) middle.update_default_vertex_attributes(thickness=thickness) @@ -82,16 +79,7 @@ def create_pointedvault_envelope( extrados = Mesh.from_vertices_and_faces(xyzub, faces_i) intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) - # Create envelope using the class method - envelope = cls.from_meshes(intrados, extrados, middle) - - # Set material properties - envelope.thickness = thickness - envelope.rho = rho - - envelope.type = 'pointedvault' - - return envelope + return intrados, extrados, middle def pointedvault_middle_update( @@ -147,62 +135,41 @@ def pointedvault_middle_update( h3, k3, r3 = _circle_3points_xy([y0, he[3]], [(y1 + y0) / 2, hc], [y1, he[2]]) h4, k4, r4 = h3, k3, r3 elif hm and he: - h1, k1, r1 = _circle_3points_xy( - [(x1 + x0) / 2, hc], [3 * (x1 + x0) / 4, hm[0]], [x1, he[0]] - ) - h2, k2, r2 = _circle_3points_xy( - [(x1 + x0) / 2, hc], [1 * (x1 + x0) / 4, hm[1]], [x0, he[1]] - ) - h3, k3, r3 = _circle_3points_xy( - [(y1 + y0) / 2, hc], [3 * (y1 + y0) / 4, hm[2]], [y1, he[2]] - ) - h4, k4, r4 = _circle_3points_xy( - [(y1 + y0) / 2, hc], [1 * (y1 + y0) / 4, hm[3]], [y0, he[3]] - ) + h1, k1, r1 = _circle_3points_xy([(x1 + x0) / 2, hc], [3 * (x1 + x0) / 4, hm[0]], [x1, he[0]]) + h2, k2, r2 = _circle_3points_xy([(x1 + x0) / 2, hc], [1 * (x1 + x0) / 4, hm[1]], [x0, he[1]]) + h3, k3, r3 = _circle_3points_xy([(y1 + y0) / 2, hc], [3 * (y1 + y0) / 4, hm[2]], [y1, he[2]]) + h4, k4, r4 = _circle_3points_xy([(y1 + y0) / 2, hc], [1 * (y1 + y0) / 4, hm[3]], [y0, he[3]]) middle = zeros((len(x), 1)) for i in range(len(x)): xi, yi = x[i], y[i] - if ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q1 + if yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q1 # Equation (xi - hx) ** 2 + (hi - kx) ** 2 = rx **2 to find the height of the pointed part (middle of quadrant) with that height one find the equivalent radius if he: hi = k1 + math.sqrt(r1**2 - (xi - h1) ** 2) else: hi = hc - ri = _find_r_given_h_l( - hi, ly - ) # This in the equation ri ** 2 = (xi - xc_) ** 2 + (zi - zc_) ** 2 -> zc = 0.0 and xc_ = (x0 + x1)/2 + ri = _find_r_given_h_l(hi, ly) # This in the equation ri ** 2 = (xi - xc_) ** 2 + (zi - zc_) ** 2 -> zc = 0.0 and xc_ = (x0 + x1)/2 if yi <= (y1 + y0) / 2: zi = _sqrt((ri) ** 2 - (yi - (y0 + ri)) ** 2) else: zi = _sqrt((ri) ** 2 - (yi - (y1 - ri)) ** 2) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q3 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q3 # Equation (xi - hy) ** 2 + (hi - ky) ** 2 = ry **2 to find the height of the pointed part (middle of quadrant) with that height one find the equivalent radius if he: hi = k3 + math.sqrt(r3**2 - (yi - h3) ** 2) else: hi = hc - ri = _find_r_given_h_l( - hi, lx - ) # This in the equation ri ** 2 = (xi - xc_) ** 2 + (zi - zc_) ** 2 -> zc = 0.0 and xc_ = (x0 + x1)/2 + ri = _find_r_given_h_l(hi, lx) # This in the equation ri ** 2 = (xi - xc_) ** 2 + (zi - zc_) ** 2 -> zc = 0.0 and xc_ = (x0 + x1)/2 if xi <= (x0 + x1) / 2: zi = _sqrt((ri) ** 2 - (xi - (x0 + ri)) ** 2) else: zi = _sqrt((ri) ** 2 - (xi - (x1 - ri)) ** 2) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q2 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q2 if he: hi = k2 + math.sqrt(r2**2 - (xi - h2) ** 2) else: @@ -213,10 +180,7 @@ def pointedvault_middle_update( else: zi = _sqrt((ri) ** 2 - (yi - (y1 - ri)) ** 2) - elif ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q4 + elif yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q4 if he: hi = k4 + math.sqrt(r4**2 - (yi - h4) ** 2) else: @@ -303,7 +267,6 @@ def pointedvault_ub_lb_update( raise NotImplementedError() if he and hm is None: - h1, k1, r1 = _circle_3points_xy([x0, he[1]], [(x1 + x0) / 2, hc], [x1, he[0]]) h2, k2, r2 = h1, k1, r1 h3, k3, r3 = _circle_3points_xy([y0, he[3]], [(y1 + y0) / 2, hc], [y1, he[2]]) @@ -315,10 +278,7 @@ def pointedvault_ub_lb_update( for i in range(len(x)): xi, yi = x[i], y[i] - if ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q1 + if yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q1 if he: hi = k1 + math.sqrt(r1**2 - (xi - h1) ** 2) else: @@ -334,10 +294,7 @@ def pointedvault_ub_lb_update( ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y1 - ri)) ** 2) lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y1 - ri)) ** 2) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q3 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q3 if he: hi = k3 + math.sqrt(r3**2 - (yi - h3) ** 2) else: @@ -352,10 +309,7 @@ def pointedvault_ub_lb_update( ub[i] = _sqrt((ri_ub) ** 2 - (xi - (x1 - ri)) ** 2) lb[i] = _sqrt((ri_lb) ** 2 - (xi - (x1 - ri)) ** 2) - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q2 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q2 if he: hi = k2 + math.sqrt(r2**2 - (xi - h2) ** 2) else: @@ -370,10 +324,7 @@ def pointedvault_ub_lb_update( ub[i] = _sqrt((ri_ub) ** 2 - (yi - (y1 - ri)) ** 2) lb[i] = _sqrt((ri_lb) ** 2 - (yi - (y1 - ri)) ** 2) - elif ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q4 + elif yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q4 if he: hi = k4 + math.sqrt(r4**2 - (yi - h4) ** 2) else: @@ -390,9 +341,7 @@ def pointedvault_ub_lb_update( else: print("Vertex did not belong to any Q. (x,y) = ({0},{1})".format(xi, yi)) - if ((yi) > y1_lb and ((xi) > x1_lb or (xi) < x0_lb)) or ( - (yi) < y0_lb and ((xi) > x1_lb or (xi) < x0_lb) - ): + if ((yi) > y1_lb and ((xi) > x1_lb or (xi) < x0_lb)) or ((yi) < y0_lb and ((xi) > x1_lb or (xi) < x0_lb)): lb[i] = -1 * min_lb return ub, lb @@ -484,10 +433,7 @@ def pointedvault_dub_dlb( for i in range(len(x)): xi, yi = x[i], y[i] - if ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q1 + if yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q1 if he: hi = k1 + math.sqrt(r1**2 - (xi - h1) ** 2) else: @@ -504,10 +450,7 @@ def pointedvault_dub_dlb( dub[i] = 1 / 2 * ri_ub / ub[i] dlb[i] = -1 / 2 * ri_lb / lb[i] - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol - ): # Q3 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi >= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) - tol: # Q3 if he: hi = k3 + math.sqrt(r3**2 - (yi - h3) ** 2) else: @@ -524,10 +467,7 @@ def pointedvault_dub_dlb( dub[i] = 1 / 2 * ri_ub / ub[i] dlb[i] = -1 / 2 * ri_lb / lb[i] - elif ( - yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q2 + elif yi >= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) - tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q2 if he: hi = k2 + math.sqrt(r2**2 - (xi - h2) ** 2) else: @@ -544,10 +484,7 @@ def pointedvault_dub_dlb( dub[i] = 1 / 2 * ri_ub / ub[i] dlb[i] = -1 / 2 * ri_lb / lb[i] - elif ( - yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol - and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol - ): # Q4 + elif yi <= y0 + (y1 - y0) / (x1 - x0) * (xi - x0) + tol and yi <= y1 - (y1 - y0) / (x1 - x0) * (xi - x0) + tol: # Q4 if he: hi = k4 + math.sqrt(r4**2 - (yi - h4) ** 2) else: @@ -566,9 +503,7 @@ def pointedvault_dub_dlb( else: print("Vertex did not belong to any Q. (x,y) = ({0},{1})".format(xi, yi)) - if ((yi) > y1_lb and ((xi) > x1_lb or (xi) < x0_lb)) or ( - (yi) < y0_lb and ((xi) > x1_lb or (xi) < x0_lb) - ): + if ((yi) > y1_lb and ((xi) > x1_lb or (xi) < x0_lb)) or ((yi) < y0_lb and ((xi) > x1_lb or (xi) < x0_lb)): lb[i] = -1 * min_lb dlb[i] = 0.0 @@ -606,6 +541,7 @@ def pointedvault_db( """Compute the sensitivities of the bounds on the reaction vector of the pointed cross vault.""" pass + def _find_r_given_h_l(h, length): r = h**2 / length + length / 4 @@ -634,12 +570,8 @@ def _circle_3points_xy(p1, p2, p3): sx21 = x2**2 - x1**2 sz21 = z2**2 - z1**2 - f = ((sx13) * (x12) + (sz13) * (x12) + (sx21) * (x13) + (sz21) * (x13)) / ( - 2 * ((z31) * (x12) - (z21) * (x13)) - ) - g = ((sx13) * (z12) + (sz13) * (z12) + (sx21) * (z13) + (sz21) * (z13)) / ( - 2 * ((x31) * (z12) - (x21) * (z13)) - ) + f = ((sx13) * (x12) + (sz13) * (x12) + (sx21) * (x13) + (sz21) * (x13)) / (2 * ((z31) * (x12) - (z21) * (x13))) + g = ((sx13) * (z12) + (sz13) * (z12) + (sx21) * (z13) + (sz21) * (z13)) / (2 * ((x31) * (z12) - (x21) * (z13))) c = -(x1**2) - z1**2 - 2 * g * x1 - 2 * f * z1 h = -g k = -f @@ -658,3 +590,86 @@ def _sqrt(x): else: sqrt_x = 0.0 return sqrt_x + + +class PointedVaultEnvelope(Envelope): + def __init__( + self, + x_span: tuple = (0.0, 10.0), + y_span: tuple = (0.0, 10.0), + thickness: float = 0.50, + min_lb: float = 0.0, + n: int = 100, + hc: float = 5.0, + he: list = None, + hm: list = None, + **kwargs, + ): + super().__init__(thickness=thickness, **kwargs) + self.x_span = x_span + self.y_span = y_span + self.min_lb = min_lb + self.n = n + self.hc = hc + self.he = he + self.hm = hm + + intrados, extrados, middle = create_pointedvault_envelope( + x_span=x_span, + y_span=y_span, + thickness=thickness, + min_lb=min_lb, + n=n, + hc=hc, + he=he, + hm=hm, + ) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + + @property + def __data__(self): + data = super().__data__ + data["x_span"] = self.x_span + data["y_span"] = self.y_span + data["min_lb"] = self.min_lb + data["n"] = self.n + data["hc"] = self.hc + data["he"] = self.he + data["hm"] = self.hm + return data + + def __str__(self): + return f"PointedVaultEnvelope(name={self.name})" + + def callable_middle(self, x, y): + return pointedvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + + def callable_ub_lb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pointedvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + + def callable_dub_dlb(self, x, y, thickness): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pointedvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + + def callable_bound_react(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pointedvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + + def callable_db(self, x, y, thickness, fixed): + if thickness is None: + thickness = self.thickness + else: + self.thickness = thickness + return pointedvault_db(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) From 2025d3a4d091f610696370b472a29e5682faa71c Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Fri, 29 Aug 2025 14:38:20 +0200 Subject: [PATCH 3/9] updated syntax --- src/compas_tna/envelope/crossvault.py | 19 ++++++------ src/compas_tna/envelope/dome.py | 35 ++++++++++++----------- src/compas_tna/envelope/envelope.py | 16 +++++------ src/compas_tna/envelope/pavillionvault.py | 22 ++++++++------ src/compas_tna/envelope/pointedvault.py | 30 +++++++++---------- 5 files changed, 64 insertions(+), 58 deletions(-) diff --git a/src/compas_tna/envelope/crossvault.py b/src/compas_tna/envelope/crossvault.py index ea135958..cdfa5b2d 100644 --- a/src/compas_tna/envelope/crossvault.py +++ b/src/compas_tna/envelope/crossvault.py @@ -406,10 +406,7 @@ def __init__( self.min_lb = min_lb self.n = n - intrados, extrados, middle = create_crossvault_envelope(x_span=x_span, y_span=y_span, thickness=thickness, min_lb=min_lb, n=n) - self.intrados = intrados - self.extrados = extrados - self.middle = middle + self.update() @property def __data__(self): @@ -423,31 +420,37 @@ def __data__(self): def __str__(self): return f"CrossVaultEnvelope(name={self.name})" + def update(self): + intrados, extrados, middle = create_crossvault_envelope(x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + def callable_middle(self, x, y): return crossvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_ub_lb(self, x, y, thickness): + def callable_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return crossvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_dub_dlb(self, x, y, thickness): + def callable_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return crossvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_bound_react(self, x, y, thickness, fixed): + def callable_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return crossvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_db(self, x, y, thickness, fixed): + def callable_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: diff --git a/src/compas_tna/envelope/dome.py b/src/compas_tna/envelope/dome.py index 809d26e2..b81981c7 100644 --- a/src/compas_tna/envelope/dome.py +++ b/src/compas_tna/envelope/dome.py @@ -302,18 +302,7 @@ def __init__( self.n_parallels = n_parallels self.r_oculus = r_oculus - intrados, extrados, middle = create_dome_envelope( - center=center, - radius=radius, - thickness=thickness, - min_lb=min_lb, - n_hoops=n_hoops, - n_parallels=n_parallels, - r_oculus=r_oculus, - ) - self.intrados = intrados - self.extrados = extrados - self.middle = middle + self.update() @property def __data__(self): @@ -329,31 +318,45 @@ def __data__(self): def __str__(self): return f"DomeEnvelope(name={self.name})" + def update(self): + intrados, extrados, middle = create_dome_envelope( + center=self.center, + radius=self.radius, + thickness=self.thickness, + min_lb=self.min_lb, + n_hoops=self.n_hoops, + n_parallels=self.n_parallels, + r_oculus=self.r_oculus, + ) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + def callable_middle(self, x, y): return dome_middle_update(x, y, self.radius, self.min_lb, self.center) - def callable_ub_lb(self, x, y, thickness): + def callable_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return dome_ub_lb_update(x, y, thickness, self.min_lb, self.center, self.radius) - def callable_dub_dlb(self, x, y, thickness): + def callable_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return dome_dub_dlb(x, y, thickness, self.min_lb, self.center, self.radius) - def callable_bound_react(self, x, y, thickness, fixed): + def callable_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return dome_bound_react_update(x, y, thickness, fixed, self.center, self.radius) - def callable_db(self, x, y, thickness, fixed): + def callable_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: diff --git a/src/compas_tna/envelope/envelope.py b/src/compas_tna/envelope/envelope.py index 2d4a091b..db69ba0d 100644 --- a/src/compas_tna/envelope/envelope.py +++ b/src/compas_tna/envelope/envelope.py @@ -677,11 +677,11 @@ def apply_selfweight_to_formdiagram(self, formdiagram: FormDiagram, normalize=Tr # Step 4: Copy the form diagram and project it onto the middle mesh vertically form_ = formdiagram.copy() - if not self.callable_zt: + if not self.callable_middle: project_mesh_to_target_vertical(form_, self.middle) else: xy = np.array(form_.vertices_attributes("xy")) - zt = list(self.callable_zt(xy[:, 0], xy[:, 1]).flatten().tolist()) + zt = list(self.callable_middle(xy[:, 0], xy[:, 1]).flatten().tolist()) for i, key in enumerate(form_.vertices()): form_.vertex_attribute(key, "z", zt[i]) @@ -775,7 +775,7 @@ def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: form_target = formdiagram.copy() # For upper bound (extrados) # Step 2: Project form diagram onto the middle mesh - if not self.callable_zt: + if not self.callable_middle: project_mesh_to_target_vertical(form_target, self.middle) else: xy = np.array(form_target.vertices_attributes("xy")) @@ -862,16 +862,16 @@ def sync_thickness_to_formdiagram(self, formdiagram: FormDiagram, method="linear formdiagram.vertex_attribute(vertex, "thickness", self._thickness) def callable_middle(self, x, y): - return NotImplementedError("Callable middle update only available for analytical envelopes") + return def callable_ub_lb(self, x, y, thickness): - return NotImplementedError("Callable ub_lb update only available for analytical envelopes") + return def callable_dub_dlb(self, x, y): - return NotImplementedError("Callable dub_dlb only available for analytical envelopes") + return def callable_bound_react(self, x, y, thickness, fixed): - return NotImplementedError("Callable bound_react update only available for analytical envelopes") + return def callable_db(self, x, y, thickness, fixed): - return NotImplementedError("Callable db only available for analytical envelopes") + return diff --git a/src/compas_tna/envelope/pavillionvault.py b/src/compas_tna/envelope/pavillionvault.py index 4486b87d..173b8b60 100644 --- a/src/compas_tna/envelope/pavillionvault.py +++ b/src/compas_tna/envelope/pavillionvault.py @@ -410,11 +410,7 @@ def __init__( self.n = n self.spr_angle = spr_angle - intrados, extrados, middle = create_pavillionvault_envelope(x_span=x_span, y_span=y_span, thickness=thickness, min_lb=min_lb, n=n, spr_angle=spr_angle) - - self.intrados = intrados - self.extrados = extrados - self.middle = middle + self.update() @property def __data__(self): @@ -429,31 +425,39 @@ def __data__(self): def __str__(self): return f"PavillionVaultEnvelope(name={self.name})" + def update(self): + intrados, extrados, middle = create_pavillionvault_envelope( + x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n, spr_angle=self.spr_angle + ) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + def callable_middle(self, x, y): return pavillionvault_middle_update(x, y, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) - def callable_ub_lb(self, x, y, thickness): + def callable_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pavillionvault_ub_lb_update(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) - def callable_dub_dlb(self, x, y, thickness): + def callable_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pavillionvault_dub_dlb(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, tol=1e-6) - def callable_bound_react(self, x, y, thickness, fixed): + def callable_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pavillionvault_bound_react_update(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) - def callable_db(self, x, y, thickness, fixed): + def callable_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: diff --git a/src/compas_tna/envelope/pointedvault.py b/src/compas_tna/envelope/pointedvault.py index 56c50a1a..5ddaf09c 100644 --- a/src/compas_tna/envelope/pointedvault.py +++ b/src/compas_tna/envelope/pointedvault.py @@ -614,19 +614,7 @@ def __init__( self.he = he self.hm = hm - intrados, extrados, middle = create_pointedvault_envelope( - x_span=x_span, - y_span=y_span, - thickness=thickness, - min_lb=min_lb, - n=n, - hc=hc, - he=he, - hm=hm, - ) - self.intrados = intrados - self.extrados = extrados - self.middle = middle + self.update() @property def __data__(self): @@ -643,31 +631,39 @@ def __data__(self): def __str__(self): return f"PointedVaultEnvelope(name={self.name})" + def update(self): + intrados, extrados, middle = create_pointedvault_envelope( + x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n, hc=self.hc, he=self.he, hm=self.hm + ) + self.intrados = intrados + self.extrados = extrados + self.middle = middle + def callable_middle(self, x, y): return pointedvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_ub_lb(self, x, y, thickness): + def callable_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pointedvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_dub_dlb(self, x, y, thickness): + def callable_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pointedvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_bound_react(self, x, y, thickness, fixed): + def callable_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pointedvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_db(self, x, y, thickness, fixed): + def callable_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: From 7e8062dbe83fef36fbac72ccba58fc8cf2fb1f60 Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Mon, 1 Sep 2025 15:29:01 +0200 Subject: [PATCH 4/9] break subclasses of envelope --- src/compas_tna/envelope/__init__.py | 17 +- src/compas_tna/envelope/brepenvelope.py | 12 + src/compas_tna/envelope/crossvault.py | 18 +- src/compas_tna/envelope/dome.py | 18 +- src/compas_tna/envelope/envelope.py | 821 +----------------- src/compas_tna/envelope/meshenvelope.py | 654 ++++++++++++++ src/compas_tna/envelope/parametricenvelope.py | 217 +++++ src/compas_tna/envelope/pavillionvault.py | 18 +- src/compas_tna/envelope/pointedvault.py | 18 +- 9 files changed, 979 insertions(+), 814 deletions(-) create mode 100644 src/compas_tna/envelope/brepenvelope.py create mode 100644 src/compas_tna/envelope/meshenvelope.py create mode 100644 src/compas_tna/envelope/parametricenvelope.py diff --git a/src/compas_tna/envelope/__init__.py b/src/compas_tna/envelope/__init__.py index 8bf31ac8..bab07f13 100644 --- a/src/compas_tna/envelope/__init__.py +++ b/src/compas_tna/envelope/__init__.py @@ -1,7 +1,20 @@ +from .envelope import Envelope +from .brepenvelope import BrepEnvelope +from .meshenvelope import MeshEnvelope +from .parametricenvelope import ParametricEnvelope + from .crossvault import CrossVaultEnvelope from .dome import DomeEnvelope -from .envelope import Envelope from .pavillionvault import PavillionVaultEnvelope from .pointedvault import PointedVaultEnvelope -__all__ = ["Envelope", "PavillionVaultEnvelope", "PointedVaultEnvelope", "DomeEnvelope", "CrossVaultEnvelope"] +__all__ = [ + "Envelope", + "BrepEnvelope", + "MeshEnvelope", + "ParametricEnvelope", + "PavillionVaultEnvelope", + "PointedVaultEnvelope", + "DomeEnvelope", + "CrossVaultEnvelope", +] diff --git a/src/compas_tna/envelope/brepenvelope.py b/src/compas_tna/envelope/brepenvelope.py new file mode 100644 index 00000000..a94268a1 --- /dev/null +++ b/src/compas_tna/envelope/brepenvelope.py @@ -0,0 +1,12 @@ +from compas.geometry import Brep +from compas_tna.envelope import Envelope + + +class BrepEnvelope(Envelope): + """An Envelope defined by a BRep at intrados, extrados, and middle.""" + + def __init__(self, intrados: Brep = None, extrados: Brep = None, middle: Brep = None, **kwargs): + super().__init__(**kwargs) + self.intrados = intrados + self.extrados = extrados + self.middle = middle diff --git a/src/compas_tna/envelope/crossvault.py b/src/compas_tna/envelope/crossvault.py index cdfa5b2d..04e98338 100644 --- a/src/compas_tna/envelope/crossvault.py +++ b/src/compas_tna/envelope/crossvault.py @@ -6,7 +6,7 @@ from compas.datastructures import Mesh from compas_tna.diagrams.diagram_rectangular import create_cross_mesh -from compas_tna.envelope.envelope import Envelope +from compas_tna.envelope.parametricenvelope import ParametricEnvelope def create_crossvault_envelope( @@ -390,7 +390,7 @@ def _sqrt(x): return sqrt_x -class CrossVaultEnvelope(Envelope): +class CrossVaultEnvelope(ParametricEnvelope): def __init__( self, x_span: tuple = (0.0, 10.0), @@ -406,7 +406,7 @@ def __init__( self.min_lb = min_lb self.n = n - self.update() + self.update_envelope() # Generate the intra/extra/middle meshes @property def __data__(self): @@ -420,37 +420,37 @@ def __data__(self): def __str__(self): return f"CrossVaultEnvelope(name={self.name})" - def update(self): + def update_envelope(self): intrados, extrados, middle = create_crossvault_envelope(x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n) self.intrados = intrados self.extrados = extrados self.middle = middle - def callable_middle(self, x, y): + def compute_middle(self, x, y): return crossvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_ub_lb(self, x, y, thickness=None): + def compute_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return crossvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_dub_dlb(self, x, y, thickness=None): + def compute_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return crossvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_bound_react(self, x, y, thickness=None, fixed=None): + def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return crossvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def callable_db(self, x, y, thickness=None, fixed=None): + def compute_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: diff --git a/src/compas_tna/envelope/dome.py b/src/compas_tna/envelope/dome.py index b81981c7..11851e58 100644 --- a/src/compas_tna/envelope/dome.py +++ b/src/compas_tna/envelope/dome.py @@ -6,7 +6,7 @@ from compas.datastructures import Mesh from compas_tna.diagrams.diagram_circular import create_circular_radial_spaced_mesh -from compas_tna.envelope.envelope import Envelope +from compas_tna.envelope.parametricenvelope import ParametricEnvelope def create_dome_envelope( @@ -282,7 +282,7 @@ def dome_db_sensitivity(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): return db -class DomeEnvelope(Envelope): +class DomeEnvelope(ParametricEnvelope): def __init__( self, center: tuple = (5.0, 5.0), @@ -302,7 +302,7 @@ def __init__( self.n_parallels = n_parallels self.r_oculus = r_oculus - self.update() + self.update_envelope() # Generate the intra/extra/middle meshes @property def __data__(self): @@ -318,7 +318,7 @@ def __data__(self): def __str__(self): return f"DomeEnvelope(name={self.name})" - def update(self): + def update_envelope(self): intrados, extrados, middle = create_dome_envelope( center=self.center, radius=self.radius, @@ -332,31 +332,31 @@ def update(self): self.extrados = extrados self.middle = middle - def callable_middle(self, x, y): + def compute_middle(self, x, y): return dome_middle_update(x, y, self.radius, self.min_lb, self.center) - def callable_ub_lb(self, x, y, thickness=None): + def compute_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return dome_ub_lb_update(x, y, thickness, self.min_lb, self.center, self.radius) - def callable_dub_dlb(self, x, y, thickness=None): + def compute_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return dome_dub_dlb(x, y, thickness, self.min_lb, self.center, self.radius) - def callable_bound_react(self, x, y, thickness=None, fixed=None): + def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return dome_bound_react_update(x, y, thickness, fixed, self.center, self.radius) - def callable_db(self, x, y, thickness=None, fixed=None): + def compute_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: diff --git a/src/compas_tna/envelope/envelope.py b/src/compas_tna/envelope/envelope.py index db69ba0d..2130a93c 100644 --- a/src/compas_tna/envelope/envelope.py +++ b/src/compas_tna/envelope/envelope.py @@ -1,222 +1,20 @@ -import math from typing import Optional +from typing import Tuple import numpy as np -from numpy import asarray -from scipy.interpolate import griddata from compas.data import Data -from compas.datastructures import Mesh from compas_tna.diagrams import FormDiagram -# TODO: What if intrados and extrados are surfaces? -def interpolate_middle_mesh(intrados: Mesh, extrados: Mesh) -> Mesh: - """Interpolate a middle mesh between intrados and extrados meshes. - - This function properly calculates thickness by considering the normal vector - at each point, ensuring accurate thickness measurements on curved surfaces. - - Parameters - ---------- - intrados : Mesh - The intrados surface mesh. - extrados : Mesh - The extrados surface mesh. - - Returns - ------- - Mesh - The interpolated middle mesh with proper normal-based thickness stored. - """ - # Use the intrados as base topology - middle = intrados.copy() - - # Get point clouds for interpolation - intrados_points = asarray(intrados.vertices_attributes("xyz")) - extrados_points = asarray(extrados.vertices_attributes("xyz")) - - # Get XY coordinates of middle mesh - middle_xy = asarray(middle.vertices_attributes("xy")) - - # Interpolate Z coordinates from both surfaces - zi = griddata(intrados_points[:, :2], intrados_points[:, 2], middle_xy, method="linear") - ze = griddata(extrados_points[:, :2], extrados_points[:, 2], middle_xy, method="linear") - - # First loop: set middle Z as average - for i, key in enumerate(middle.vertices()): - middle_z = (zi[i] + ze[i]) / 2.0 - middle.vertex_attribute(key, "z", middle_z) - - # Second loop: calculate and set thickness using correct normals - for i, key in enumerate(middle.vertices()): - nx, ny, nz = middle.vertex_normal(key) - z_diff = ze[i] - zi[i] - if abs(nz) > 0.1: - thickness = abs(z_diff) * abs(nz) - else: - thickness = abs(z_diff) - middle.vertex_attribute(key, "thickness", thickness) - - return middle - - -# TODO: What if middle is a surface and not a mesh? -def offset_from_middle(middle: Mesh, fixed_xy: bool = True) -> tuple[Mesh, Mesh]: - """ - Offset a middle surface mesh to obtain extrados and intrados meshes using thickness attributes. - - This function properly handles curved surfaces by using the normal vector - and the thickness measured perpendicular to the surface. - - Parameters - ---------- - middle : Mesh - The middle surface mesh with thickness attributes per vertex. - fixed_xy : bool, optional - If True, extrados/intrados will have the same XY as the middle mesh, - and only Z will be offset (with normal correction). - If False, full 3D normal offset is used. - - Returns - ------- - tuple[Mesh, Mesh] - (intrados, extrados) offset meshes. - """ - extrados = middle.copy() - intrados = middle.copy() - - for key in middle.vertices(): - x, y, z = middle.vertex_coordinates(key) - nx, ny, nz = middle.vertex_normal(key) - - # Get thickness for this specific vertex (should be normal-based) - thickness = middle.vertex_attribute(key, "thickness") - if thickness is None: - thickness = 0.5 - half_thick = 0.5 * thickness - - if fixed_xy: - # Prevent division by zero for horizontal normals - if abs(nz) < 1e-8: - raise ValueError(f"Normal at vertex {key} is (almost) horizontal: {nx, ny, nz}") - dz = half_thick / nz - extrados_z = z + dz - intrados_z = z - dz - extrados.vertex_attribute(key, "z", extrados_z) - intrados.vertex_attribute(key, "z", intrados_z) - else: - # Full 3D normal offset - this is the most accurate for curved surfaces - extrados.vertex_attributes( - key, - "xyz", - [x + half_thick * nx, y + half_thick * ny, z + half_thick * nz], - ) - intrados.vertex_attributes( - key, - "xyz", - [x - half_thick * nx, y - half_thick * ny, z - half_thick * nz], - ) - - return intrados, extrados - - -# TODO: What if the target is a surface and not a mesh? -def project_mesh_to_target_vertical(mesh: Mesh, target: Mesh) -> None: - """Project a mesh vertically (in Z direction) onto a target mesh. - - Parameters - ---------- - mesh : Mesh - The mesh to be projected. - target : Mesh - The target mesh to project onto. - - Returns - ------- - None - The mesh is modified in place. - """ - # Get target mesh vertices for simple vertical projection - target_vertices = list(target.vertices()) - target_points = [target.vertex_point(v) for v in target_vertices] - - for vertex in mesh.vertices(): - point = mesh.vertex_point(vertex) - - # Find the closest target vertex in XY plane - min_distance = float("inf") - closest_z = point.z - - for target_point in target_points: - # Calculate XY distance (ignore Z) - xy_distance = ((point.x - target_point.x) ** 2 + (point.y - target_point.y) ** 2) ** 0.5 - - if xy_distance < min_distance: - min_distance = xy_distance - closest_z = target_point.z - - # Update vertex to closest Z value - new_point = point.copy() - new_point.z = closest_z - mesh.vertex_attributes(vertex, "xyz", new_point) - - -def pattern_inverse_height_thickness(pattern: Mesh, tmin: Optional[float] = None, tmax: Optional[float] = None) -> None: - """Set variable thickness based on inverse height. - - Parameters - ---------- - pattern : Mesh - The mesh to apply thickness to. - tmin : float, optional - Minimum thickness. If None, will be calculated as 3/1000 of the diagonal of the xy bounding box. - tmax : float, optional - Maximum thickness. If None, will be calculated as 50/1000 of the diagonal of the xy bounding box. - """ - x: list[float] = pattern.vertices_attribute(name="x") - xmin = min(x) - xmax = max(x) - dx = xmax - xmin - - y: list[float] = pattern.vertices_attribute(name="y") - ymin = min(y) - ymax = max(y) - dy = ymax - ymin - - d = (dx**2 + dy**2) ** 0.5 - - tmin = tmin or 3 * d / 1000 - tmax = tmax or 50 * d / 1000 - - pattern.update_default_vertex_attributes(thickness=0) - zvalues: list[float] = pattern.vertices_attribute(name="z") - zmin = min(zvalues) - zmax = max(zvalues) - - for vertex in pattern.vertices(): - point = pattern.vertex_point(vertex) - z = (point.z - zmin) / (zmax - zmin) - thickness = (1 - z) * (tmax - tmin) + tmin - pattern.vertex_attribute(vertex, name="thickness", value=thickness) - - class Envelope(Data): """Pure geometric envelope representing masonry structure boundaries.""" - def __init__(self, intrados: Mesh = None, extrados: Mesh = None, middle: Mesh = None, fill: Mesh = None, rho: float = 20.0, thickness: float = 0.5, **kwargs): + def __init__(self, rho: Optional[float] = 20.0, **kwargs): super().__init__(**kwargs) - # # Core geometric surfaces (required - already in BaseEnvelope) - self.intrados = intrados - self.extrados = extrados - self.middle = middle - self.fill = fill self.rho = rho - # Thickness property - self._thickness = thickness - # Computed properties (cached) self._area = 0.0 self._volume = 0.0 @@ -225,254 +23,12 @@ def __init__(self, intrados: Mesh = None, extrados: Mesh = None, middle: Mesh = @property def __data__(self): data = {} - data["intrados"] = self.intrados - data["extrados"] = self.extrados - data["middle"] = self.middle - data["fill"] = self.fill data["rho"] = self.rho - data["thickness"] = self._thickness return data def __str__(self): return f"Envelope(name={self.name})" - # ============================================================================= - # Factory methods - # ============================================================================= - - @classmethod - def from_meshes(cls, intrados: Mesh, extrados: Mesh, middle: Optional[Mesh] = None) -> "Envelope": - """Construct an envelope from intrados and extrados meshes. - - Parameters - ---------- - intrados : Mesh - The intrados surface mesh of the envelope. - extrados : Mesh - The extrados surface mesh of the envelope. - middle : Mesh, optional - The middle surface mesh of the envelope. - - Returns - ------- - :class:`Envelope` - - """ - envelope = cls() - envelope.intrados = intrados - envelope.extrados = extrados - if middle is not None: - envelope.middle = middle - else: - envelope.middle = interpolate_middle_mesh(intrados, extrados) - - return envelope - - @classmethod - def from_formdiagram(cls, formdiagram: FormDiagram, thickness: Optional[float] = None) -> "Envelope": - """Construct an envelope from a FormDiagram with specified thickness. - - Parameters - ---------- - formdiagram : FormDiagram - The form diagram to create the envelope from. - thickness : float, optional - The thickness of the envelope. If None, uses thickness values stored in formdiagram vertices. - - Returns - ------- - :class:`Envelope` - - """ - return cls.from_middle_mesh(formdiagram, thickness) - - @classmethod - def from_middle_mesh(cls, mesh: Mesh, thickness: Optional[float] = None) -> "Envelope": - """Construct an envelope from a mesh with specified thickness. - - Parameters - ---------- - formdiagram : FormDiagram - The form diagram to create the envelope from. - thickness : float, optional - The thickness of the envelope. If None, uses thickness values stored in formdiagram vertices. - - Returns - ------- - :class:`Envelope` - - """ - envelope = cls() - - envelope.middle = mesh.copy(cls=Mesh) - - if thickness is not None: - envelope.thickness = thickness - - # Create intrados and extrados using thickness from middle mesh - intrados, extrados = offset_from_middle(envelope.middle) - envelope.intrados = intrados - envelope.extrados = extrados - - return envelope - - # @classmethod - # def from_crossvault( - # cls, - # x_span: tuple[float, float] = (0.0, 10.0), - # y_span: tuple[float, float] = (0.0, 10.0), - # thickness: float = 0.50, - # min_lb: float = 0.0, - # n: int = 100, - # rho: float = 25.0, - # ) -> "Envelope": - # """Construct an envelope from a crossvault. - - # Parameters - # ---------- - # x_span : tuple[float, float], optional - # Span of the vault in x direction, by default (0.0, 10.0) - # y_span : tuple[float, float], optional - # Span of the vault in y direction, by default (0.0, 10.0) - # thickness : float, optional - # Thickness of the vault, by default 0.50 - # min_lb : float, optional - # Parameter for lower bound in nodes in the boundary, by default 0.0 - # n : int, optional - # Number of vertices for the mesh, by default 100 - # rho : float, optional - # Density of the material in kN/m³, by default 25.0 - - # Returns - # ------- - # :class:`Envelope` - # The created envelope with intrados, extrados, and middle meshes. - # """ - # return create_crossvault_envelope(cls, x_span, y_span, thickness, min_lb, n, rho) - - # @classmethod - # def from_dome( - # cls, - # center: tuple[float, float] = (5.0, 5.0), - # radius: float = 5.0, - # thickness: float = 0.50, - # min_lb: float = 0.0, - # n_hoops: int = 24, - # n_parallels: int = 40, - # r_oculus: float = 0.0, - # rho: float = 25.0, - # ) -> "Envelope": - # """Construct an envelope from a dome. - - # Parameters - # ---------- - # center : tuple[float, float], optional - # x, y coordinates of the center of the dome, by default (5.0, 5.0) - # radius : float, optional - # The radius of the dome, by default 5.0 - # thickness : float, optional - # Thickness of the dome, by default 0.50 - # min_lb : float, optional - # Parameter for lower bound in nodes in the boundary, by default 0.0 - # n_hoops : int, optional - # Number of hoops for the mesh, by default 24 - # n_parallels : int, optional - # Number of parallels for the mesh, by default 40 - # r_oculus : float, optional - # Radius of the oculus (opening at the top), by default 0.0 - # rho : float, optional - # Density of the material in kN/m³, by default 25.0 - - # Returns - # ------- - # :class:`Envelope` - # The created envelope with intrados, extrados, and middle meshes. - # """ - # return create_dome_envelope(cls, center, radius, thickness, min_lb, n_hoops, n_parallels, r_oculus, rho) - - # @classmethod - # def from_pavillionvault( - # cls, - # x_span: tuple[float, float] = (0.0, 10.0), - # y_span: tuple[float, float] = (0.0, 10.0), - # thickness: float = 0.50, - # min_lb: float = 0.0, - # n: int = 100, - # spr_angle: float = 0.0, - # expanded: bool = False, - # rho: float = 25.0, - # ) -> "Envelope": - # """Construct an envelope from a pavillion vault. - - # Parameters - # ---------- - # x_span : tuple[float, float], optional - # Span of the vault in x direction, by default (0.0, 10.0) - # y_span : tuple[float, float], optional - # Span of the vault in y direction, by default (0.0, 10.0) - # thickness : float, optional - # Thickness of the vault, by default 0.50 - # min_lb : float, optional - # Parameter for lower bound in nodes in the boundary, by default 0.0 - # n : int, optional - # Number of vertices for the mesh, by default 100 - # spr_angle : float, optional - # Springing angle, by default 0.0 - # expanded : bool, optional - # If the extrados should extend beyond the floor plan, by default False - # rho : float, optional - # Density of the material in kN/m³, by default 25.0 - - # Returns - # ------- - # :class:`Envelope` - # The created envelope with intrados, extrados, and middle meshes. - # """ - # return create_pavillionvault_envelope(cls, x_span, y_span, thickness, min_lb, n, spr_angle, expanded, rho) - - # @classmethod - # def from_pointedvault( - # cls, - # x_span: tuple[float, float] = (0.0, 10.0), - # y_span: tuple[float, float] = (0.0, 10.0), - # thickness: float = 0.50, - # min_lb: float = 0.0, - # n: int = 100, - # hc: float = 8.0, - # he: list = None, - # hm: list = None, - # rho: float = 25.0, - # ) -> "Envelope": - # """Construct an envelope from a pointed cross vault. - - # Parameters - # ---------- - # x_span : tuple[float, float], optional - # Span of the vault in x direction, by default (0.0, 10.0) - # y_span : tuple[float, float], optional - # Span of the vault in y direction, by default (0.0, 10.0) - # thickness : float, optional - # Thickness of the vault, by default 0.50 - # min_lb : float, optional - # Parameter for lower bound in nodes in the boundary, by default 0.0 - # n : int, optional - # Number of vertices for the mesh, by default 100 - # hc : float, optional - # Height in the middle point of the vault, by default 8.0 - # he : list, optional - # Height of the opening mid-span for each of the quadrants, by default None - # hm : list, optional - # Height of each quadrant center (spadrel), by default None - # rho : float, optional - # Density of the material in kN/m³, by default 25.0 - - # Returns - # ------- - # :class:`Envelope` - # The created envelope with intrados, extrados, and middle meshes. - # """ - # return create_pointedvault_envelope(cls, x_span, y_span, thickness, min_lb, n, hc, he, hm, rho) - # ============================================================================= # Properties # ============================================================================= @@ -480,10 +36,7 @@ def from_middle_mesh(cls, mesh: Mesh, thickness: Optional[float] = None) -> "Env @property def area(self): if not self._area: - if self.middle is not None: - self._area = self.middle.area() - else: - raise ValueError("Middle mesh is not available. Cannot compute area.") + self._area = self.compute_area() return self._area @property @@ -493,49 +46,11 @@ def volume(self): return self._volume @property - def total_selfweight(self): + def selfweight(self): if not self._total_selfweight: self._total_selfweight = self.compute_selfweight() return self._total_selfweight - @property - def thickness(self) -> float: - """Get the average thickness of the envelope. - - Returns - ------- - float - The average thickness of the envelope. - """ - if self.middle is not None: - # Return average thickness from middle mesh vertices - thicknesses = [] - for key in self.middle.vertices(): - thickness = self.middle.vertex_attribute(key, "thickness") - if thickness is not None: - thicknesses.append(thickness) - - if thicknesses: - return sum(thicknesses) / len(thicknesses) - - return self._thickness - - @thickness.setter - def thickness(self, value: float) -> None: - """Set a uniform thickness for all vertices of the envelope. - - Parameters - ------- - value : float - The thickness value to set for all vertices. - """ - self._thickness = value - - # Update middle mesh if it exists - if self.middle is not None: - for key in self.middle.vertices(): - self.middle.vertex_attribute(key, "thickness", value) - @property def rho(self) -> float: """Get the density of the envelope. @@ -559,319 +74,73 @@ def rho(self, value: float) -> None: self._rho = value # ============================================================================= - # Geometric operations + # Geometry operations # ============================================================================= - def set_variable_thickness(self, tmin: Optional[float] = None, tmax: Optional[float] = None) -> None: - """Set variable thickness based on inverse height using the pattern_inverse_height_thickness function. - - This method applies thickness variation based on the height of vertices in the middle mesh, - where higher vertices get thinner thickness and lower vertices get thicker thickness. + def compute_area(self) -> float: + """Compute and returns the total area of the structure based on the appropriate method.""" - Parameters - ------- - tmin : float, optional - Minimum thickness. If None, will be calculated as 3/1000 of the diagonal of the xy bounding box. - tmax : float, optional - Maximum thickness. If None, will be calculated as 50/1000 of the diagonal of the xy bounding box. - """ - if self.middle is None: - raise ValueError("Middle mesh is not available. Cannot set variable thickness.") - - # Apply the pattern_inverse_height_thickness function to the middle mesh - pattern_inverse_height_thickness(self.middle, tmin=tmin, tmax=tmax) - - # Update intrados and extrados meshes - self.intrados, self.extrados = offset_from_middle(self.middle) + raise NotImplementedError("Implement compute_area for specific envelope type.") def compute_volume(self) -> float: - """Compute and returns the volume of the structure based on the area and thickness in the data. - - Returns - ------- - float - The total volume of the structure. - - """ - if self.middle is None: - raise ValueError("Middle mesh is not available. Cannot compute volume.") + """Compute and returns the total volume of the structure based on the appropriate method.""" - middle = self.middle - total_volume = 0.0 - - # Use variable thickness from middle mesh vertices - for vertex in middle.vertices(): - thickness = middle.vertex_attribute(vertex, "thickness") - if thickness is None: - thickness = self._thickness - vertex_area = middle.vertex_area(vertex) # should be projected area - vertex_volume = thickness * vertex_area - total_volume += vertex_volume - - return total_volume + raise NotImplementedError("Implement compute_volume for specific envelope type.") def compute_selfweight(self) -> float: - """Compute and returns the total selfweight of the structure based on the area and thickness in the data. + """Compute and returns the total selfweight of the structure based on the appropriate method.""" - Returns - ------- - float - The total selfweight of the structure. - - """ - if self.middle is None: - if self.intrados is not None and self.extrados is not None: - self.middle = interpolate_middle_mesh(self.intrados, self.extrados) - else: - raise ValueError("Middle mesh is not available and cannot be interpolated.") - - middle = self.middle - rho = self.rho - total_selfweight = 0.0 - - # Use variable thickness from middle mesh vertices - for vertex in middle.vertices(): - thickness = middle.vertex_attribute(vertex, "thickness") - if thickness is None: - thickness = self._thickness - vertex_area = middle.vertex_area(vertex) - vertex_volume = thickness * vertex_area - vertex_weight = vertex_volume * rho - total_selfweight += vertex_weight - - return total_selfweight + raise NotImplementedError("Implement compute_selfweight for specific envelope type.") # ============================================================================= - # TNA-specific operations (accept formdiagram as parameter) + # TNA-related operations (accept formdiagram) # ============================================================================= - def apply_selfweight_to_formdiagram(self, formdiagram: FormDiagram, normalize=True) -> None: - """Apply selfweight to the nodes of a form diagram based on the middle surface and local thicknesses. + def apply_selfweight_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply selfweight to the nodes of a form diagram based on the appropriate method.""" - Parameters - ---------- - formdiagram : FormDiagram - The form diagram to apply selfweight to. - normalize : bool, optional - Whether or not normalize the selfweight to match the computed total selfweight, by default True + raise NotImplementedError("Implement apply_selfweight_to_formdiagram for specific envelope type.") - Returns - ------- - None - The FormDiagram is modified in place + def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply envelope bounds to a form diagram based on the appropriate method.""" - """ - # Step 1: Check that middle mesh is present - if self.middle is None: - if self.intrados is not None and self.extrados is not None: - self.middle = interpolate_middle_mesh(self.intrados, self.extrados) - else: - raise ValueError("Middle mesh is not set. Please set the middle mesh before applying selfweight.") - - # Step 2: Compute the selfweight of the shell - total_selfweight = self.compute_selfweight() - - # Step 3: Sync thickness to the form diagram - self.sync_thickness_to_formdiagram(formdiagram) - - # Step 4: Copy the form diagram and project it onto the middle mesh vertically - form_ = formdiagram.copy() - - if not self.callable_middle: - project_mesh_to_target_vertical(form_, self.middle) - else: - xy = np.array(form_.vertices_attributes("xy")) - zt = list(self.callable_middle(xy[:, 0], xy[:, 1]).flatten().tolist()) - for i, key in enumerate(form_.vertices()): - form_.vertex_attribute(key, "z", zt[i]) - - # Step 5: Compute and lump selfweight at vertices - total_pz = 0.0 - for vertex in form_.vertices(): - # Get vertex area and thickness - vertex_area = form_.vertex_area(vertex) - thickness = form_.vertex_attribute(vertex, "thickness") - - # Compute selfweight contribution (negative for downward direction) - pz = -vertex_area * thickness * self.rho - - # Store in form diagram - formdiagram.vertex_attribute(vertex, "pz", pz) - total_pz += abs(pz) # Sum absolute values for normalization - - # Step 6: Scale to match total selfweight if normalize=True - if normalize and total_pz > 0: - scale_factor = total_selfweight / total_pz - if scale_factor != 1.0: - print(f"Scaled selfweight by factor: {scale_factor}") - - for vertex in formdiagram.vertices(): - pz = formdiagram.vertex_attribute(vertex, "pz") - formdiagram.vertex_attribute(vertex, "pz", pz * scale_factor) - - print(f"Selfweight applied to form diagram. Total load: {sum(abs(formdiagram.vertex_attribute(vertex, 'pz')) for vertex in formdiagram.vertices())}") + raise NotImplementedError("Implement apply_envelope_to_formdiagram for specific envelope type.") - def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: - """Apply envelope bounds to a form diagram based on the intrados and extrados surfaces. + def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply target heights to a form diagram based on the appropriate method.""" - This method projects the form diagram onto both intrados and extrados surfaces - and assigns the heights to 'ub' (upper bound) and 'lb' (lower bound) properties. + raise NotImplementedError("Implement apply_target_heights_to_formdiagram for specific envelope type.") - Parameters - ---------- - formdiagram : FormDiagram - The form diagram to apply bounds to. + def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply reaction bounds to a form diagram based on the appropriate method.""" - Returns - ------- - None - The FormDiagram is modified in place. - """ - # Step 1: Check that intrados and extrados are present - if self.intrados is None or self.extrados is None: - raise ValueError("Intra/Extrados not set. Please set them before applying bounds.") - - # Step 2: Copy the form diagram for projection - form_ub = formdiagram.copy() # For upper bound (extrados) - form_lb = formdiagram.copy() # For lower bound (intrados) - - # Step 3: Project form diagram onto extrados (upper bound) - if not self.callable_ub_lb: - project_mesh_to_target_vertical(form_ub, self.extrados) - project_mesh_to_target_vertical(form_lb, self.intrados) - else: - xy = np.array(form_ub.vertices_attributes("xy")) - zub, zlb = self.callable_ub_lb(xy[:, 0], xy[:, 1]) - for i, key in enumerate(form_ub.vertices()): - form_ub.vertex_attribute(key, "z", float(zub[i])) - form_lb.vertex_attribute(key, "z", float(zlb[i])) - - # Step 4: Collect heights and assign to form diagram - for vertex in formdiagram.vertices(): - if vertex in form_ub.vertices() and vertex in form_lb.vertices(): - # Get z coordinates from projected meshes - _, _, z_ub = form_ub.vertex_coordinates(vertex) - _, _, z_lb = form_lb.vertex_coordinates(vertex) - - # Assign to form diagram - formdiagram.vertex_attribute(vertex, "ub", z_ub) - formdiagram.vertex_attribute(vertex, "lb", z_lb) - else: - print(f"Warning: Vertex {vertex} not found in projected meshes") - # Set default values if vertex not found - formdiagram.vertex_attribute(vertex, "ub", float("inf")) - formdiagram.vertex_attribute(vertex, "lb", float("-inf")) + raise NotImplementedError("Implement apply_reaction_bounds_to_formdiagram for specific envelope type.") - def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: - """Apply target heights to a form diagram based on the Envelope middle surface. + # ============================================================================= + # Numerical methods to be implemented at sub classes + # ============================================================================= - This method projects the form diagram onto the Envelope middle surface - and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. - """ - if self.middle is None: - raise ValueError("Middle mesh is not set. Please set the middle mesh before applying target heights.") - - # Step 1: Copy the form diagram for projection - form_target = formdiagram.copy() # For upper bound (extrados) - - # Step 2: Project form diagram onto the middle mesh - if not self.callable_middle: - project_mesh_to_target_vertical(form_target, self.middle) - else: - xy = np.array(form_target.vertices_attributes("xy")) - zt = list(self.callable_zt(xy[:, 0], xy[:, 1]).flatten().tolist()) - for i, key in enumerate(form_target.vertices()): - form_target.vertex_attribute(key, "z", zt[i]) - - # Step 3: Collect heights and assign to form diagram - for vertex in formdiagram.vertices(): - if vertex in form_target.vertices(): - z_target = form_target.vertex_attribute(vertex, "z") - formdiagram.vertex_attribute(vertex, "target", z_target) + def compute_middle(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: + """Compute the middle of the envelope based on the appropriate method.""" - def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: - """Apply reaction bounds to a form diagram based on the Envelope middle surface. + raise NotImplementedError("Implement compute_middle for specific envelope type.") - This method projects the form diagram onto the Envelope middle surface - and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. - """ + def compute_ub_lb(self, x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Compute the upper and lower bounds of the envelope based on the appropriate method.""" - if not self.callable_bound_react: - raise ValueError("Callable bound reaction is not set. Please set this limit manually.") + raise NotImplementedError("Implement compute_ub_lb for specific envelope type.") - ## TODO: Implement this + def compute_dub_dlb(self, x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Compute the upper and lower bounds of the envelope based on the appropriate method.""" - def sync_thickness_to_formdiagram(self, formdiagram: FormDiagram, method="linear", extrapolate=True) -> None: - """Synchronize thickness attributes from middle mesh to form diagram using continuous interpolation. + raise NotImplementedError("Implement compute_dub_dlb for specific envelope type.") - This method creates a continuous thickness map from the middle mesh and interpolates - thickness values at the form diagram vertex locations. This ensures compatibility - even when the middle mesh and form diagram have different topologies. + def compute_bound_react(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: + """Compute the reaction bounds of the envelope based on the appropriate method.""" - Parameters - ------- - formdiagram : FormDiagram - The form diagram to sync thickness to. - """ - if self.middle is None: - raise ValueError("Middle mesh must be set to sync thickness.") - - # Get middle mesh XY coordinates and thickness values - middle_xy = self.middle.vertices_attributes("xy") - middle_thickness = self.middle.vertices_attribute("thickness") - - # Validate data - if not middle_xy or not middle_thickness: - raise ValueError("Middle mesh must have both 'xy' and 'thickness' attributes.") - - # Convert to numpy arrays - middle_xy_array = asarray(middle_xy) - middle_thickness_array = asarray(middle_thickness) - - # Get form diagram XY coordinates - form_xy = formdiagram.vertices_attributes("xy") - if not form_xy: - raise ValueError("Form diagram must have 'xy' attributes.") - - form_xy_array = asarray(form_xy) - - # Interpolate thickness values from middle mesh to form diagram locations - try: - # Use griddata for 2D interpolation - interpolated_thickness = griddata( - middle_xy_array, - middle_thickness_array, - form_xy_array, - method=method, - fill_value=self._thickness, # Use default thickness for points outside convex hull - # extrapolate=extrapolate - ) - - # Assign interpolated thickness values to form diagram vertices - for i, vertex in enumerate(formdiagram.vertices()): - thickness_value = float(interpolated_thickness[i]) - # Ensure thickness is positive and reasonable - if thickness_value <= 0 or math.isnan(thickness_value): - thickness_value = self._thickness - formdiagram.vertex_attribute(vertex, "thickness", thickness_value) - - except Exception as e: - print(f"Warning: Interpolation failed, using default thickness. Error: {e}") - # Fallback: assign default thickness to all vertices - for vertex in formdiagram.vertices(): - formdiagram.vertex_attribute(vertex, "thickness", self._thickness) - - def callable_middle(self, x, y): - return - - def callable_ub_lb(self, x, y, thickness): - return - - def callable_dub_dlb(self, x, y): - return - - def callable_bound_react(self, x, y, thickness, fixed): - return - - def callable_db(self, x, y, thickness, fixed): - return + raise NotImplementedError("Implement compute_bound_react for specific envelope type.") + + def compute_db(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: + """Compute the db of the envelope based on the appropriate method.""" + + raise NotImplementedError("Implement compute_db for specific envelope type.") diff --git a/src/compas_tna/envelope/meshenvelope.py b/src/compas_tna/envelope/meshenvelope.py new file mode 100644 index 00000000..12de21a5 --- /dev/null +++ b/src/compas_tna/envelope/meshenvelope.py @@ -0,0 +1,654 @@ +import math +from typing import Optional + +from numpy import asarray +from scipy.interpolate import griddata + +from compas.datastructures import Mesh +from compas_tna.diagrams import FormDiagram +from compas_tna.envelope import Envelope + + +# TODO: What if intrados and extrados are surfaces? +def interpolate_middle_mesh(intrados: Mesh, extrados: Mesh) -> Mesh: + """Interpolate a middle mesh between intrados and extrados meshes. + + This function properly calculates thickness by considering the normal vector + at each point, ensuring accurate thickness measurements on curved surfaces. + + Parameters + ---------- + intrados : Mesh + The intrados surface mesh. + extrados : Mesh + The extrados surface mesh. + + Returns + ------- + Mesh + The interpolated middle mesh with proper normal-based thickness stored. + """ + # Use the intrados as base topology + middle = intrados.copy() + + # Get point clouds for interpolation + intrados_points = asarray(intrados.vertices_attributes("xyz")) + extrados_points = asarray(extrados.vertices_attributes("xyz")) + + # Get XY coordinates of middle mesh + middle_xy = asarray(middle.vertices_attributes("xy")) + + # Interpolate Z coordinates from both surfaces + zi = griddata(intrados_points[:, :2], intrados_points[:, 2], middle_xy, method="linear") + ze = griddata(extrados_points[:, :2], extrados_points[:, 2], middle_xy, method="linear") + + # First loop: set middle Z as average + for i, key in enumerate(middle.vertices()): + middle_z = (zi[i] + ze[i]) / 2.0 + middle.vertex_attribute(key, "z", middle_z) + + # Second loop: calculate and set thickness using correct normals + for i, key in enumerate(middle.vertices()): + nx, ny, nz = middle.vertex_normal(key) + z_diff = ze[i] - zi[i] + if abs(nz) > 0.1: + thickness = abs(z_diff) * abs(nz) + else: + thickness = abs(z_diff) + middle.vertex_attribute(key, "thickness", thickness) + + return middle + + +# TODO: What if middle is a surface and not a mesh? +def offset_from_middle(middle: Mesh, fixed_xy: bool = True) -> tuple[Mesh, Mesh]: + """ + Offset a middle surface mesh to obtain extrados and intrados meshes using thickness attributes. + + This function properly handles curved surfaces by using the normal vector + and the thickness measured perpendicular to the surface. + + Parameters + ---------- + middle : Mesh + The middle surface mesh with thickness attributes per vertex. + fixed_xy : bool, optional + If True, extrados/intrados will have the same XY as the middle mesh, + and only Z will be offset (with normal correction). + If False, full 3D normal offset is used. + + Returns + ------- + tuple[Mesh, Mesh] + (intrados, extrados) offset meshes. + """ + extrados = middle.copy() + intrados = middle.copy() + + for key in middle.vertices(): + x, y, z = middle.vertex_coordinates(key) + nx, ny, nz = middle.vertex_normal(key) + + # Get thickness for this specific vertex (should be normal-based) + thickness = middle.vertex_attribute(key, "thickness") + if thickness is None: + thickness = 0.5 + half_thick = 0.5 * thickness + + if fixed_xy: + # Prevent division by zero for horizontal normals + if abs(nz) < 1e-8: + raise ValueError(f"Normal at vertex {key} is (almost) horizontal: {nx, ny, nz}") + dz = half_thick / nz + extrados_z = z + dz + intrados_z = z - dz + extrados.vertex_attribute(key, "z", extrados_z) + intrados.vertex_attribute(key, "z", intrados_z) + else: + # Full 3D normal offset - this is the most accurate for curved surfaces + extrados.vertex_attributes( + key, + "xyz", + [x + half_thick * nx, y + half_thick * ny, z + half_thick * nz], + ) + intrados.vertex_attributes( + key, + "xyz", + [x - half_thick * nx, y - half_thick * ny, z - half_thick * nz], + ) + + return intrados, extrados + + +# TODO: What if the target is a surface and not a mesh? +def project_mesh_to_target_vertical(mesh: Mesh, target: Mesh) -> None: + """Project a mesh vertically (in Z direction) onto a target mesh. + + Parameters + ---------- + mesh : Mesh + The mesh to be projected. + target : Mesh + The target mesh to project onto. + + Returns + ------- + None + The mesh is modified in place. + """ + # Get target mesh vertices for simple vertical projection + target_vertices = list(target.vertices()) + target_points = [target.vertex_point(v) for v in target_vertices] + + for vertex in mesh.vertices(): + point = mesh.vertex_point(vertex) + + # Find the closest target vertex in XY plane + min_distance = float("inf") + closest_z = point.z + + for target_point in target_points: + # Calculate XY distance (ignore Z) + xy_distance = ((point.x - target_point.x) ** 2 + (point.y - target_point.y) ** 2) ** 0.5 + + if xy_distance < min_distance: + min_distance = xy_distance + closest_z = target_point.z + + # Update vertex to closest Z value + new_point = point.copy() + new_point.z = closest_z + mesh.vertex_attributes(vertex, "xyz", new_point) + + +def pattern_inverse_height_thickness(pattern: Mesh, tmin: Optional[float] = None, tmax: Optional[float] = None) -> None: + """Set variable thickness based on inverse height. + + Parameters + ---------- + pattern : Mesh + The mesh to apply thickness to. + tmin : float, optional + Minimum thickness. If None, will be calculated as 3/1000 of the diagonal of the xy bounding box. + tmax : float, optional + Maximum thickness. If None, will be calculated as 50/1000 of the diagonal of the xy bounding box. + """ + x: list[float] = pattern.vertices_attribute(name="x") + xmin = min(x) + xmax = max(x) + dx = xmax - xmin + + y: list[float] = pattern.vertices_attribute(name="y") + ymin = min(y) + ymax = max(y) + dy = ymax - ymin + + d = (dx**2 + dy**2) ** 0.5 + + tmin = tmin or 3 * d / 1000 + tmax = tmax or 50 * d / 1000 + + pattern.update_default_vertex_attributes(thickness=0) + zvalues: list[float] = pattern.vertices_attribute(name="z") + zmin = min(zvalues) + zmax = max(zvalues) + + for vertex in pattern.vertices(): + point = pattern.vertex_point(vertex) + z = (point.z - zmin) / (zmax - zmin) + thickness = (1 - z) * (tmax - tmin) + tmin + pattern.vertex_attribute(vertex, name="thickness", value=thickness) + + +class MeshEnvelope(Envelope): + """An Envelope defined by meshes at intrados and extrados.""" + + def __init__(self, intrados: Mesh = None, extrados: Mesh = None, middle: Mesh = None, fill: Mesh = None, thickness: float = 0.5, **kwargs): + super().__init__(**kwargs) + + self.intrados = intrados + self.extrados = extrados + self.middle = middle + self.fill = fill + + # Thickness property + self._thickness = thickness + self.is_parametric = False + + @property + def __data__(self): + data = super().__data__ + data["intrados"] = self.intrados + data["extrados"] = self.extrados + data["middle"] = self.middle + data["fill"] = self.fill + data["thickness"] = self._thickness + data["is_parametric"] = self.is_parametric + return data + + def __str__(self): + return f"Envelope(name={self.name})" + + # ============================================================================= + # Factory methods + # ============================================================================= + + @classmethod + def from_meshes(cls, intrados: Mesh, extrados: Mesh, middle: Optional[Mesh] = None) -> "Envelope": + """Construct an envelope from intrados and extrados meshes. + + Parameters + ---------- + intrados : Mesh + The intrados surface mesh of the envelope. + extrados : Mesh + The extrados surface mesh of the envelope. + middle : Mesh, optional + The middle surface mesh of the envelope. + + Returns + ------- + :class:`Envelope` + + """ + envelope = cls() + envelope.intrados = intrados + envelope.extrados = extrados + if middle is not None: + envelope.middle = middle + else: + envelope.middle = interpolate_middle_mesh(intrados, extrados) + + return envelope + + @classmethod + def from_middle_mesh(cls, mesh: Mesh, thickness: Optional[float] = None) -> "Envelope": + """Construct an envelope from a mesh with specified thickness. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to create the envelope from. + thickness : float, optional + The thickness of the envelope. If None, uses thickness values stored in formdiagram vertices. + + Returns + ------- + :class:`Envelope` + + """ + envelope = cls() + + envelope.middle = mesh.copy(cls=Mesh) + + if thickness is not None: + envelope.thickness = thickness + + # Create intrados and extrados using thickness from middle mesh + intrados, extrados = offset_from_middle(envelope.middle) + envelope.intrados = intrados + envelope.extrados = extrados + + return envelope + + @classmethod + def from_formdiagram(cls, formdiagram: FormDiagram, thickness: Optional[float] = None) -> "Envelope": + """Construct an envelope from a FormDiagram with specified thickness. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to create the envelope from. + thickness : float, optional + The thickness of the envelope. If None, uses thickness values stored in formdiagram vertices. + + Returns + ------- + :class:`Envelope` + + """ + return cls.from_middle_mesh(formdiagram, thickness) + + # ============================================================================= + # Properties + # ============================================================================= + + @property + def thickness(self) -> float: + """Get the average thickness of the envelope. + + Returns + ------- + float + The average thickness of the envelope. + """ + if self.middle is not None: + # Return average thickness from middle mesh vertices + thicknesses = [] + for key in self.middle.vertices(): + thickness = self.middle.vertex_attribute(key, "thickness") + if thickness is not None: + thicknesses.append(thickness) + + if thicknesses: + return sum(thicknesses) / len(thicknesses) + + return self._thickness + + @thickness.setter + def thickness(self, value: float) -> None: + """Set a uniform thickness for all vertices of the envelope. + + Parameters + ------- + value : float + The thickness value to set for all vertices. + """ + self._thickness = value + + # Update middle mesh if it exists + if self.middle is not None: + for key in self.middle.vertices(): + self.middle.vertex_attribute(key, "thickness", value) + + # ============================================================================= + # Geometric operations + # ============================================================================= + + def set_variable_thickness(self, tmin: Optional[float] = None, tmax: Optional[float] = None) -> None: + """Set variable thickness based on inverse height using the pattern_inverse_height_thickness function. + + This method applies thickness variation based on the height of vertices in the middle mesh, + where higher vertices get thinner thickness and lower vertices get thicker thickness. + + Parameters + ------- + tmin : float, optional + Minimum thickness. If None, will be calculated as 3/1000 of the diagonal of the xy bounding box. + tmax : float, optional + Maximum thickness. If None, will be calculated as 50/1000 of the diagonal of the xy bounding box. + """ + if self.middle is None: + raise ValueError("Middle mesh is not available. Cannot set variable thickness.") + + # Apply the pattern_inverse_height_thickness function to the middle mesh + pattern_inverse_height_thickness(self.middle, tmin=tmin, tmax=tmax) + + # Update intrados and extrados meshes + self.intrados, self.extrados = offset_from_middle(self.middle) + + def compute_volume(self) -> float: + """Compute and returns the volume of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total volume of the structure. + + """ + if self.middle is None: + raise ValueError("Middle mesh is not available. Cannot compute volume.") + + middle = self.middle + total_volume = 0.0 + + # Use variable thickness from middle mesh vertices + for vertex in middle.vertices(): + thickness = middle.vertex_attribute(vertex, "thickness") + if thickness is None: + thickness = self._thickness + vertex_area = middle.vertex_area(vertex) # should be projected area + vertex_volume = thickness * vertex_area + total_volume += vertex_volume + + return total_volume + + def compute_selfweight(self) -> float: + """Compute and returns the total selfweight of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total selfweight of the structure. + + """ + if self.middle is None: + if self.intrados is not None and self.extrados is not None: + self.middle = interpolate_middle_mesh(self.intrados, self.extrados) + else: + raise ValueError("Middle mesh is not available and cannot be interpolated.") + + middle = self.middle + rho = self.rho + total_selfweight = 0.0 + + # Use variable thickness from middle mesh vertices + for vertex in middle.vertices(): + thickness = middle.vertex_attribute(vertex, "thickness") + if thickness is None: + thickness = self._thickness + vertex_area = middle.vertex_area(vertex) + vertex_volume = thickness * vertex_area + vertex_weight = vertex_volume * rho + total_selfweight += vertex_weight + + return total_selfweight + + def compute_area(self) -> float: + """Compute and returns the total selfweight of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total selfweight of the structure. + """ + if self.middle is None: + raise ValueError("Middle mesh is not available. Cannot compute area.") + + return self.middle.area() + + # ============================================================================= + # TNA-specific operations (accept formdiagram as parameter) + # ============================================================================= + + def apply_selfweight_to_formdiagram(self, formdiagram: FormDiagram, normalize=True) -> None: + """Apply selfweight to the nodes of a form diagram based on the middle surface and local thicknesses. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply selfweight to. + normalize : bool, optional + Whether or not normalize the selfweight to match the computed total selfweight, by default True + + Returns + ------- + None + The FormDiagram is modified in place + + """ + # Step 1: Check that middle mesh is present + if self.middle is None: + if self.intrados is not None and self.extrados is not None: + self.middle = interpolate_middle_mesh(self.intrados, self.extrados) + else: + raise ValueError("Middle mesh is not set. Please set the middle mesh before applying selfweight.") + + # Step 2: Compute the selfweight of the shell + total_selfweight = self.compute_selfweight() + + # Step 3: Sync thickness to the form diagram + self.sync_thickness_to_formdiagram(formdiagram) + + # Step 4: Copy the form diagram and project it onto the middle mesh vertically + form_ = formdiagram.copy() + project_mesh_to_target_vertical(form_, self.middle) + + # Step 5: Compute and lump selfweight at vertices + total_pz = 0.0 + for vertex in form_.vertices(): + # Get vertex area and thickness + vertex_area = form_.vertex_area(vertex) + thickness = form_.vertex_attribute(vertex, "thickness") + + # Compute selfweight contribution (negative for downward direction) + pz = -vertex_area * thickness * self.rho + + # Store in form diagram + formdiagram.vertex_attribute(vertex, "pz", pz) + total_pz += abs(pz) # Sum absolute values for normalization + + # Step 6: Scale to match total selfweight if normalize=True + if normalize and total_pz > 0: + scale_factor = total_selfweight / total_pz + if scale_factor != 1.0: + print(f"Scaled selfweight by factor: {scale_factor}") + + for vertex in formdiagram.vertices(): + pz = formdiagram.vertex_attribute(vertex, "pz") + formdiagram.vertex_attribute(vertex, "pz", pz * scale_factor) + + print(f"Selfweight applied to form diagram. Total load: {sum(abs(formdiagram.vertex_attribute(vertex, 'pz')) for vertex in formdiagram.vertices())}") + + def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply envelope bounds to a form diagram based on the intrados and extrados surfaces. + + This method projects the form diagram onto both intrados and extrados surfaces + and assigns the heights to 'ub' (upper bound) and 'lb' (lower bound) properties. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply bounds to. + + Returns + ------- + None + The FormDiagram is modified in place. + """ + # Step 1: Check that intrados and extrados are present + if self.intrados is None or self.extrados is None: + raise ValueError("Intra/Extrados not set. Please set them before applying bounds.") + + # Step 2: Copy the form diagram for projection + form_ub = formdiagram.copy() # For upper bound (extrados) + form_lb = formdiagram.copy() # For lower bound (intrados) + + # Step 3: Project form diagram onto extrados (upper bound) + project_mesh_to_target_vertical(form_ub, self.extrados) + project_mesh_to_target_vertical(form_lb, self.intrados) + + # Step 4: Collect heights and assign to form diagram + for vertex in formdiagram.vertices(): + if vertex in form_ub.vertices() and vertex in form_lb.vertices(): + # Get z coordinates from projected meshes + _, _, z_ub = form_ub.vertex_coordinates(vertex) + _, _, z_lb = form_lb.vertex_coordinates(vertex) + + # Assign to form diagram + formdiagram.vertex_attribute(vertex, "ub", z_ub) + formdiagram.vertex_attribute(vertex, "lb", z_lb) + else: + print(f"Warning: Vertex {vertex} not found in projected meshes") + # Set default values if vertex not found + formdiagram.vertex_attribute(vertex, "ub", float("inf")) + formdiagram.vertex_attribute(vertex, "lb", float("-inf")) + + def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply target heights to a form diagram based on the Envelope middle surface. + + This method projects the form diagram onto the Envelope middle surface + and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + """ + if self.middle is None: + raise ValueError("Middle mesh is not set. Please set the middle mesh before applying target heights.") + + # Step 1: Copy the form diagram for projection + form_target = formdiagram.copy() # For upper bound (extrados) + + project_mesh_to_target_vertical(form_target, self.middle) + + # Step 2: Collect heights and assign to form diagram + for vertex in formdiagram.vertices(): + if vertex in form_target.vertices(): + z_target = form_target.vertex_attribute(vertex, "z") + formdiagram.vertex_attribute(vertex, "target", z_target) + + def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply reaction bounds to a form diagram based on the Envelope middle surface. + + This method projects the form diagram onto the Envelope middle surface + and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + """ + + # if not self.callable_bound_react: + # raise ValueError("Callable bound reaction is not set. Please set this limit manually.") + + ## TODO: Implement this + + def sync_thickness_to_formdiagram(self, formdiagram: FormDiagram, method="linear", extrapolate=True) -> None: + """Synchronize thickness attributes from middle mesh to form diagram using continuous interpolation. + + This method creates a continuous thickness map from the middle mesh and interpolates + thickness values at the form diagram vertex locations. This ensures compatibility + even when the middle mesh and form diagram have different topologies. + + Parameters + ------- + formdiagram : FormDiagram + The form diagram to sync thickness to. + """ + if self.middle is None: + raise ValueError("Middle mesh must be set to sync thickness.") + + # Get middle mesh XY coordinates and thickness values + middle_xy = self.middle.vertices_attributes("xy") + middle_thickness = self.middle.vertices_attribute("thickness") + + # Validate data + if not middle_xy or not middle_thickness: + raise ValueError("Middle mesh must have both 'xy' and 'thickness' attributes.") + + # Convert to numpy arrays + middle_xy_array = asarray(middle_xy) + middle_thickness_array = asarray(middle_thickness) + + # Get form diagram XY coordinates + form_xy = formdiagram.vertices_attributes("xy") + if not form_xy: + raise ValueError("Form diagram must have 'xy' attributes.") + + form_xy_array = asarray(form_xy) + + # Use griddata for 2D interpolation + interpolated_thickness = griddata( + middle_xy_array, + middle_thickness_array, + form_xy_array, + method=method, + fill_value=self._thickness, # Use default thickness for points outside convex hull + # extrapolate=extrapolate + ) + + # Assign interpolated thickness values to form diagram vertices + for i, vertex in enumerate(formdiagram.vertices()): + thickness_value = float(interpolated_thickness[i]) + # Ensure thickness is positive and reasonable + if thickness_value <= 0 or math.isnan(thickness_value): + thickness_value = self._thickness + formdiagram.vertex_attribute(vertex, "thickness", thickness_value) + + def compute_middle(self, x, y): + raise NotImplementedError("Implement compute_middle for specific envelope type.") + + def compute_ub_lb(self, x, y, thickness): + raise NotImplementedError("Implement compute_ub_lb for specific envelope type.") + + def compute_dub_dlb(self, x, y): + raise NotImplementedError("Implement compute_dub_dlb for specific envelope type.") + + def compute_bound_react(self, x, y, thickness, fixed): + raise NotImplementedError("Implement compute_bound_react for specific envelope type.") + + def compute_db(self, x, y, thickness, fixed): + return diff --git a/src/compas_tna/envelope/parametricenvelope.py b/src/compas_tna/envelope/parametricenvelope.py new file mode 100644 index 00000000..65cbee41 --- /dev/null +++ b/src/compas_tna/envelope/parametricenvelope.py @@ -0,0 +1,217 @@ +import numpy as np + +from compas_tna.diagrams import FormDiagram +from compas_tna.envelope import Envelope + + +class ParametricEnvelope(Envelope): + """Pure geometric envelope representing masonry structure boundaries created parametrically.""" + + def __init__(self, thickness: float = 0.50, **kwargs): + super().__init__(**kwargs) + + self._thickness = thickness + self.is_parametric = True + + def __str__(self): + return f"ParametricEnvelope(name={self.name})" + + @property + def __data__(self): + data = super().__data__ + data["thickness"] = self._thickness + data["is_parametric"] = self.is_parametric + return data + + # ============================================================================= + # Parametric Common Prepoperties + # ============================================================================= + + @property + def thickness(self) -> float: + """Get the thickness of the envelope. + + Returns + ------- + float + The thickness of the envelope. + """ + return self._thickness + + @thickness.setter + def thickness(self, value: float) -> None: + """Set the thickness of the envelope. + + Parameters + ------- + value : float + The thickness value to set. + """ + self._thickness = value + + # ============================================================================= + # Envelope Generator + # ============================================================================= + + def update_envelope(self) -> None: + """Update the envelope based on the appropriate method.""" + + raise NotImplementedError("Implement update_envelope for specific envelope type.") + + # ============================================================================= + # Geometry operations + # ============================================================================= + + def compute_volume(self) -> float: + """Compute and returns the volume of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total volume of the structure. + + """ + if self.middle is None: + self.update_envelope() + + return self.compute_area() * self.thickness + + def compute_selfweight(self) -> float: + """Compute and returns the total selfweight of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total selfweight of the structure. + + """ + if self.middle is None: + self.update_envelope() + + return self.compute_volume() * self.rho + + def compute_area(self) -> float: + """Compute and returns the total selfweight of the structure based on the area and thickness in the data. + + Returns + ------- + float + The total selfweight of the structure. + """ + if self.middle is None: + self.update_envelope() + + return self.middle.area() + + # ============================================================================= + # TNA-specific operations (accept formdiagram as parameter) + # ============================================================================= + + def apply_selfweight_to_formdiagram(self, formdiagram: FormDiagram, normalize=True) -> None: + """Apply selfweight to the nodes of a form diagram based on the middle surface and local thicknesses. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply selfweight to. + normalize : bool, optional + Whether or not normalize the selfweight to match the computed total selfweight, by default True + + Returns + ------- + None + The FormDiagram is modified in place + + """ + # Step 2: Compute the selfweight of the shell + total_selfweight = self.compute_selfweight() + + # Step 3: Copy the form diagram and project it onto the middle mesh vertically + form_: FormDiagram = formdiagram.copy() + + xy = np.array(form_.vertices_attributes("xy")) + zt = list(self.compute_middle(xy[:, 0], xy[:, 1]).flatten().tolist()) + for i, key in enumerate(form_.vertices()): + form_.vertex_attribute(key, "z", zt[i]) + + # Step 4: Compute and lump selfweight at vertices + total_pz = 0.0 + for vertex in form_.vertices(): + vertex_area = form_.vertex_area(vertex) + thickness = self.thickness + pz = -vertex_area * thickness * self.rho + formdiagram.vertex_attribute(vertex, "pz", pz) + total_pz += abs(pz) + + # Step 5: Scale to match total selfweight if normalize=True + if normalize and total_pz > 0: + scale_factor = total_selfweight / total_pz + if scale_factor != 1.0: + print(f"Scaled selfweight by factor: {scale_factor}") + + for vertex in formdiagram.vertices(): + pz = formdiagram.vertex_attribute(vertex, "pz") + formdiagram.vertex_attribute(vertex, "pz", pz * scale_factor) + + print(f"Selfweight applied to form diagram. Total load: {sum(abs(formdiagram.vertex_attribute(vertex, 'pz')) for vertex in formdiagram.vertices())}") + + def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply envelope bounds to a form diagram based on the intrados and extrados surfaces. + + This method projects the form diagram onto both intrados and extrados surfaces + and assigns the heights to 'ub' (upper bound) and 'lb' (lower bound) properties. + + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply bounds to. + + Returns + ------- + None + The FormDiagram is modified in place. + """ + + xy = np.array(formdiagram.vertices_attributes("xy")) + zub, zlb = self.compute_ub_lb(xy[:, 0], xy[:, 1]) + for i, key in enumerate(formdiagram.vertices()): + formdiagram.vertex_attribute(key, "ub", float(zub[i])) + formdiagram.vertex_attribute(key, "lb", float(zlb[i])) + # TODO: Future Cached properties could be added here + + def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply target heights to a form diagram based on the Envelope middle surface. + + This method projects the form diagram onto the Envelope middle surface + and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + """ + + xy = np.array(formdiagram.vertices_attributes("xy")) + zt = list(self.compute_middle(xy[:, 0], xy[:, 1]).flatten().tolist()) + for i, key in enumerate(formdiagram.vertices()): + formdiagram.vertex_attribute(key, "z", zt[i]) + # TODO: Future Cached properties could be added here + + def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: + """Apply reaction bounds to a form diagram based on the Envelope middle surface. + + This method projects the form diagram onto the Envelope middle surface + and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + """ + raise NotImplementedError("Implement apply_reaction_bounds_to_formdiagram for specific envelope type.") + ## TODO: Implement this + + def compute_middle(self, x, y): + raise NotImplementedError("Implement compute_middle for specific envelope type.") + + def compute_ub_lb(self, x, y, thickness): + raise NotImplementedError("Implement compute_ub_lb for specific envelope type.") + + def compute_dub_dlb(self, x, y): + raise NotImplementedError("Implement compute_dub_dlb for specific envelope type.") + + def compute_bound_react(self, x, y, thickness, fixed): + raise NotImplementedError("Implement compute_bound_react for specific envelope type.") + + def compute_db(self, x, y, thickness, fixed): + return diff --git a/src/compas_tna/envelope/pavillionvault.py b/src/compas_tna/envelope/pavillionvault.py index 173b8b60..98830958 100644 --- a/src/compas_tna/envelope/pavillionvault.py +++ b/src/compas_tna/envelope/pavillionvault.py @@ -6,7 +6,7 @@ from compas.datastructures import Mesh from compas_tna.diagrams.diagram_rectangular import create_cross_mesh -from compas_tna.envelope.envelope import Envelope +from compas_tna.envelope.parametricenvelope import ParametricEnvelope def create_pavillionvault_envelope( @@ -392,7 +392,7 @@ def pavillionvault_db(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): return abs(db) -class PavillionVaultEnvelope(Envelope): +class PavillionVaultEnvelope(ParametricEnvelope): def __init__( self, x_span: tuple = (0.0, 10.0), @@ -410,7 +410,7 @@ def __init__( self.n = n self.spr_angle = spr_angle - self.update() + self.update_envelope() # Generate the intra/extra/middle meshes @property def __data__(self): @@ -425,7 +425,7 @@ def __data__(self): def __str__(self): return f"PavillionVaultEnvelope(name={self.name})" - def update(self): + def update_envelope(self): intrados, extrados, middle = create_pavillionvault_envelope( x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n, spr_angle=self.spr_angle ) @@ -433,31 +433,31 @@ def update(self): self.extrados = extrados self.middle = middle - def callable_middle(self, x, y): + def compute_middle(self, x, y): return pavillionvault_middle_update(x, y, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) - def callable_ub_lb(self, x, y, thickness=None): + def compute_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pavillionvault_ub_lb_update(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) - def callable_dub_dlb(self, x, y, thickness=None): + def compute_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pavillionvault_dub_dlb(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, tol=1e-6) - def callable_bound_react(self, x, y, thickness=None, fixed=None): + def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pavillionvault_bound_react_update(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) - def callable_db(self, x, y, thickness=None, fixed=None): + def compute_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: diff --git a/src/compas_tna/envelope/pointedvault.py b/src/compas_tna/envelope/pointedvault.py index 5ddaf09c..b8fdc0b5 100644 --- a/src/compas_tna/envelope/pointedvault.py +++ b/src/compas_tna/envelope/pointedvault.py @@ -6,7 +6,7 @@ from compas.datastructures import Mesh from compas_tna.diagrams.diagram_rectangular import create_cross_mesh -from compas_tna.envelope.envelope import Envelope +from compas_tna.envelope.parametricenvelope import ParametricEnvelope def create_pointedvault_envelope( @@ -592,7 +592,7 @@ def _sqrt(x): return sqrt_x -class PointedVaultEnvelope(Envelope): +class PointedVaultEnvelope(ParametricEnvelope): def __init__( self, x_span: tuple = (0.0, 10.0), @@ -614,7 +614,7 @@ def __init__( self.he = he self.hm = hm - self.update() + self.update_envelope() # Generate the intra/extra/middle meshes @property def __data__(self): @@ -631,7 +631,7 @@ def __data__(self): def __str__(self): return f"PointedVaultEnvelope(name={self.name})" - def update(self): + def update_envelope(self): intrados, extrados, middle = create_pointedvault_envelope( x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n, hc=self.hc, he=self.he, hm=self.hm ) @@ -639,31 +639,31 @@ def update(self): self.extrados = extrados self.middle = middle - def callable_middle(self, x, y): + def compute_middle(self, x, y): return pointedvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_ub_lb(self, x, y, thickness=None): + def compute_ub_lb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pointedvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_dub_dlb(self, x, y, thickness=None): + def compute_dub_dlb(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pointedvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_bound_react(self, x, y, thickness=None, fixed=None): + def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness return pointedvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def callable_db(self, x, y, thickness=None, fixed=None): + def compute_db(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: From 08166e1c49196bc06c82ee1cc14f0841986f62de Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Tue, 2 Sep 2025 00:09:55 +0200 Subject: [PATCH 5/9] is_parametric --- src/compas_tna/envelope/envelope.py | 22 ------------------- src/compas_tna/envelope/meshenvelope.py | 7 ++++-- src/compas_tna/envelope/parametricenvelope.py | 7 ++++-- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/compas_tna/envelope/envelope.py b/src/compas_tna/envelope/envelope.py index 2130a93c..e2235bb9 100644 --- a/src/compas_tna/envelope/envelope.py +++ b/src/compas_tna/envelope/envelope.py @@ -51,28 +51,6 @@ def selfweight(self): self._total_selfweight = self.compute_selfweight() return self._total_selfweight - @property - def rho(self) -> float: - """Get the density of the envelope. - - Returns - ------- - float - The density of the envelope in kg/m³. - """ - return self._rho - - @rho.setter - def rho(self, value: float) -> None: - """Set the density of the envelope. - - Parameters - ------- - value : float - The density value to set in kg/m³. - """ - self._rho = value - # ============================================================================= # Geometry operations # ============================================================================= diff --git a/src/compas_tna/envelope/meshenvelope.py b/src/compas_tna/envelope/meshenvelope.py index 12de21a5..91dcc4ea 100644 --- a/src/compas_tna/envelope/meshenvelope.py +++ b/src/compas_tna/envelope/meshenvelope.py @@ -213,7 +213,6 @@ def __init__(self, intrados: Mesh = None, extrados: Mesh = None, middle: Mesh = # Thickness property self._thickness = thickness - self.is_parametric = False @property def __data__(self): @@ -223,7 +222,6 @@ def __data__(self): data["middle"] = self.middle data["fill"] = self.fill data["thickness"] = self._thickness - data["is_parametric"] = self.is_parametric return data def __str__(self): @@ -351,6 +349,11 @@ def thickness(self, value: float) -> None: for key in self.middle.vertices(): self.middle.vertex_attribute(key, "thickness", value) + @property + def is_parametric(self) -> bool: + """Check if the envelope is parametric.""" + return False + # ============================================================================= # Geometric operations # ============================================================================= diff --git a/src/compas_tna/envelope/parametricenvelope.py b/src/compas_tna/envelope/parametricenvelope.py index 65cbee41..0cab1405 100644 --- a/src/compas_tna/envelope/parametricenvelope.py +++ b/src/compas_tna/envelope/parametricenvelope.py @@ -11,7 +11,6 @@ def __init__(self, thickness: float = 0.50, **kwargs): super().__init__(**kwargs) self._thickness = thickness - self.is_parametric = True def __str__(self): return f"ParametricEnvelope(name={self.name})" @@ -20,7 +19,6 @@ def __str__(self): def __data__(self): data = super().__data__ data["thickness"] = self._thickness - data["is_parametric"] = self.is_parametric return data # ============================================================================= @@ -49,6 +47,11 @@ def thickness(self, value: float) -> None: """ self._thickness = value + @property + def is_parametric(self) -> bool: + """Check if the envelope is parametric.""" + return True + # ============================================================================= # Envelope Generator # ============================================================================= From 49e27ea0745ca1e330d344b077b6df551a3e9006 Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Tue, 2 Sep 2025 11:30:34 +0200 Subject: [PATCH 6/9] naming & parameter consistency --- src/compas_tna/envelope/crossvault.py | 34 ++++++------ src/compas_tna/envelope/dome.py | 32 +++++------ src/compas_tna/envelope/envelope.py | 20 +++---- src/compas_tna/envelope/meshenvelope.py | 12 ++--- src/compas_tna/envelope/parametricenvelope.py | 20 +++---- src/compas_tna/envelope/pavillionvault.py | 53 +++++++++---------- src/compas_tna/envelope/pointedvault.py | 45 ++++++---------- 7 files changed, 100 insertions(+), 116 deletions(-) diff --git a/src/compas_tna/envelope/crossvault.py b/src/compas_tna/envelope/crossvault.py index 04e98338..6e6c5b87 100644 --- a/src/compas_tna/envelope/crossvault.py +++ b/src/compas_tna/envelope/crossvault.py @@ -9,7 +9,7 @@ from compas_tna.envelope.parametricenvelope import ParametricEnvelope -def create_crossvault_envelope( +def crossvault_envelope( x_span: tuple = (0.0, 10.0), y_span: tuple = (0.0, 10.0), thickness: float = 0.50, @@ -46,13 +46,13 @@ def create_crossvault_envelope( xi, yi, _ = array(xyz0).transpose() # Create middle surface - zt = crossvault_middle_update(xi, yi, min_lb, x_span=x_span, y_span=y_span, tol=1e-6) + zt = crossvault_middle(xi, yi, min_lb, x_span=x_span, y_span=y_span, tol=1e-6) xyzt = array([xi, yi, zt.flatten()]).transpose() middle = Mesh.from_vertices_and_faces(xyzt, faces_i) middle.update_default_vertex_attributes(thickness=thickness) # Create upper and lower bounds - zub, zlb = crossvault_ub_lb_update(xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, tol=1e-6) + zub, zlb = crossvault_bounds(xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, tol=1e-6) xyzub = array([xi, yi, zub.flatten()]).transpose() xyzlb = array([xi, yi, zlb.flatten()]).transpose() @@ -62,7 +62,7 @@ def create_crossvault_envelope( return intrados, extrados, middle -def crossvault_middle_update(x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): +def crossvault_middle(x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Update middle of a crossvault based in the parameters Parameters @@ -126,7 +126,7 @@ def crossvault_middle_update(x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0 return z -def crossvault_ub_lb_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): +def crossvault_bounds(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Update upper and lower bounds of an crossvault based in the parameters Parameters @@ -230,7 +230,7 @@ def crossvault_ub_lb_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, return ub, lb -def crossvault_dub_dlb(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): +def crossvault_bounds_derivatives(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Computes the sensitivities of upper and lower bounds in the x, y coordinates and thickness specified. Parameters @@ -368,12 +368,12 @@ def crossvault_dub_dlb(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0) return dub, dlb, dubdx, dubdy, dlbdx, dlbdy -def crossvault_bound_react_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): +def crossvault_bound_react(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Compute the bounds on the reaction vector of the crossvault.""" pass -def crossvault_db(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): +def crossvault_bound_react_derivatives(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Compute the sensitivities of the bounds on the reaction vector of the crossvault.""" pass @@ -421,38 +421,38 @@ def __str__(self): return f"CrossVaultEnvelope(name={self.name})" def update_envelope(self): - intrados, extrados, middle = create_crossvault_envelope(x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n) + intrados, extrados, middle = crossvault_envelope(x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n) self.intrados = intrados self.extrados = extrados self.middle = middle def compute_middle(self, x, y): - return crossvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, tol=1e-6) + return crossvault_middle(x, y, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def compute_ub_lb(self, x, y, thickness=None): + def compute_bounds(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return crossvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) + return crossvault_bounds(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def compute_dub_dlb(self, x, y, thickness=None): + def compute_bounds_derivatives(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return crossvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) + return crossvault_bounds_derivatives(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return crossvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) + return crossvault_bound_react(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) - def compute_db(self, x, y, thickness=None, fixed=None): + def compute_bound_react_derivatives(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return crossvault_db(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) + return crossvault_bound_react_derivatives(x, y, thickness, self.min_lb, self.x_span, self.y_span, tol=1e-6) diff --git a/src/compas_tna/envelope/dome.py b/src/compas_tna/envelope/dome.py index 11851e58..03d017dd 100644 --- a/src/compas_tna/envelope/dome.py +++ b/src/compas_tna/envelope/dome.py @@ -9,7 +9,7 @@ from compas_tna.envelope.parametricenvelope import ParametricEnvelope -def create_dome_envelope( +def dome_envelope( center: tuple = (5.0, 5.0), radius: float = 5.0, thickness: float = 0.50, @@ -57,7 +57,7 @@ def create_dome_envelope( ) xyz0, faces_i = base_topology.to_vertices_and_faces() xi, yi, _ = array(xyz0).transpose() - zt = dome_middle_update(xi, yi, radius_current, min_lb, center=center) + zt = dome_middle(xi, yi, radius_current, min_lb, center=center) xyzt = array([xi, yi, zt.flatten()]).transpose() if radius_current == radius: @@ -75,7 +75,7 @@ def create_dome_envelope( return intrados, extrados, middle -def dome_middle_update(x, y, radius, min_lb, center=(5.0, 5.0)): +def dome_middle(x, y, radius, min_lb, center=(5.0, 5.0)): """Update middle of the dome based in the parameters Parameters @@ -111,7 +111,7 @@ def dome_middle_update(x, y, radius, min_lb, center=(5.0, 5.0)): return zt -def dome_ub_lb_update(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): +def dome_bounds(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): """Update upper and lower bounds of the dome based in the parameters Parameters @@ -154,7 +154,7 @@ def dome_ub_lb_update(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): return ub, lb -def dome_dub_dlb(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): +def dome_bounds_derivatives(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): """Update sensitivities of upper and lower bounds of the dome based in the parameters Parameters @@ -207,7 +207,7 @@ def dome_dub_dlb(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): return dub, dlb, dubdx, dubdy, dlbdx, dlbdy -def dome_bound_react_update(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): +def dome_bound_react(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): """Updates the ``b`` parameter of a dome for a given thickness Parameters @@ -245,7 +245,7 @@ def dome_bound_react_update(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): return b -def dome_db_sensitivity(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): +def dome_bound_react_derivatives(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): """Updates the ``db`` parameter of a dome for a given thickness Parameters @@ -319,7 +319,7 @@ def __str__(self): return f"DomeEnvelope(name={self.name})" def update_envelope(self): - intrados, extrados, middle = create_dome_envelope( + intrados, extrados, middle = dome_envelope( center=self.center, radius=self.radius, thickness=self.thickness, @@ -333,32 +333,32 @@ def update_envelope(self): self.middle = middle def compute_middle(self, x, y): - return dome_middle_update(x, y, self.radius, self.min_lb, self.center) + return dome_middle(x, y, self.radius, self.min_lb, self.center) - def compute_ub_lb(self, x, y, thickness=None): + def compute_bounds(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return dome_ub_lb_update(x, y, thickness, self.min_lb, self.center, self.radius) + return dome_bounds(x, y, thickness, self.min_lb, self.center, self.radius) - def compute_dub_dlb(self, x, y, thickness=None): + def compute_bounds_derivatives(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return dome_dub_dlb(x, y, thickness, self.min_lb, self.center, self.radius) + return dome_bounds_derivatives(x, y, thickness, self.min_lb, self.center, self.radius) def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return dome_bound_react_update(x, y, thickness, fixed, self.center, self.radius) + return dome_bound_react(x, y, thickness, fixed, self.center, self.radius) - def compute_db(self, x, y, thickness=None, fixed=None): + def compute_bound_react_derivatives(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return dome_db_sensitivity(x, y, thickness, fixed, self.center, self.radius) + return dome_bound_react_derivatives(x, y, thickness, fixed, self.center, self.radius) diff --git a/src/compas_tna/envelope/envelope.py b/src/compas_tna/envelope/envelope.py index e2235bb9..d39afc3e 100644 --- a/src/compas_tna/envelope/envelope.py +++ b/src/compas_tna/envelope/envelope.py @@ -10,10 +10,11 @@ class Envelope(Data): """Pure geometric envelope representing masonry structure boundaries.""" - def __init__(self, rho: Optional[float] = 20.0, **kwargs): + def __init__(self, rho: Optional[float] = 20.0, is_parametric: bool = False, **kwargs): super().__init__(**kwargs) self.rho = rho + self.is_parametric = is_parametric # Computed properties (cached) self._area = 0.0 @@ -24,6 +25,7 @@ def __init__(self, rho: Optional[float] = 20.0, **kwargs): def __data__(self): data = {} data["rho"] = self.rho + data["is_parametric"] = self.is_parametric return data def __str__(self): @@ -103,22 +105,22 @@ def compute_middle(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: raise NotImplementedError("Implement compute_middle for specific envelope type.") - def compute_ub_lb(self, x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + def compute_bounds(self, x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Compute the upper and lower bounds of the envelope based on the appropriate method.""" - raise NotImplementedError("Implement compute_ub_lb for specific envelope type.") + raise NotImplementedError("Implement compute_bounds for specific envelope type.") - def compute_dub_dlb(self, x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Compute the upper and lower bounds of the envelope based on the appropriate method.""" + def compute_bounds_derivatives(self, x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Compute the upper and lower bounds derivativesof the envelope based on the appropriate method.""" - raise NotImplementedError("Implement compute_dub_dlb for specific envelope type.") + raise NotImplementedError("Implement compute_bounds_derivatives for specific envelope type.") def compute_bound_react(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: """Compute the reaction bounds of the envelope based on the appropriate method.""" raise NotImplementedError("Implement compute_bound_react for specific envelope type.") - def compute_db(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: - """Compute the db of the envelope based on the appropriate method.""" + def compute_bound_react_derivatives(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: + """Compute the bound_react_derivatives of the envelope based on the appropriate method.""" - raise NotImplementedError("Implement compute_db for specific envelope type.") + raise NotImplementedError("Implement compute_bound_react_derivatives for specific envelope type.") diff --git a/src/compas_tna/envelope/meshenvelope.py b/src/compas_tna/envelope/meshenvelope.py index 91dcc4ea..da3898de 100644 --- a/src/compas_tna/envelope/meshenvelope.py +++ b/src/compas_tna/envelope/meshenvelope.py @@ -644,14 +644,14 @@ def sync_thickness_to_formdiagram(self, formdiagram: FormDiagram, method="linear def compute_middle(self, x, y): raise NotImplementedError("Implement compute_middle for specific envelope type.") - def compute_ub_lb(self, x, y, thickness): - raise NotImplementedError("Implement compute_ub_lb for specific envelope type.") + def compute_bounds(self, x, y, thickness): + raise NotImplementedError("Implement compute_bounds for specific envelope type.") - def compute_dub_dlb(self, x, y): - raise NotImplementedError("Implement compute_dub_dlb for specific envelope type.") + def compute_bounds_derivatives(self, x, y): + raise NotImplementedError("Implement compute_bounds_derivatives for specific envelope type.") def compute_bound_react(self, x, y, thickness, fixed): raise NotImplementedError("Implement compute_bound_react for specific envelope type.") - def compute_db(self, x, y, thickness, fixed): - return + def compute_bound_react_derivatives(self, x, y, thickness, fixed): + raise NotImplementedError("Implement compute_bound_react_derivatives for specific envelope type.") diff --git a/src/compas_tna/envelope/parametricenvelope.py b/src/compas_tna/envelope/parametricenvelope.py index 0cab1405..3faf93c7 100644 --- a/src/compas_tna/envelope/parametricenvelope.py +++ b/src/compas_tna/envelope/parametricenvelope.py @@ -10,6 +10,7 @@ class ParametricEnvelope(Envelope): def __init__(self, thickness: float = 0.50, **kwargs): super().__init__(**kwargs) + self.is_parametric = True self._thickness = thickness def __str__(self): @@ -47,11 +48,6 @@ def thickness(self, value: float) -> None: """ self._thickness = value - @property - def is_parametric(self) -> bool: - """Check if the envelope is parametric.""" - return True - # ============================================================================= # Envelope Generator # ============================================================================= @@ -176,7 +172,7 @@ def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: """ xy = np.array(formdiagram.vertices_attributes("xy")) - zub, zlb = self.compute_ub_lb(xy[:, 0], xy[:, 1]) + zub, zlb = self.compute_bounds(xy[:, 0], xy[:, 1]) for i, key in enumerate(formdiagram.vertices()): formdiagram.vertex_attribute(key, "ub", float(zub[i])) formdiagram.vertex_attribute(key, "lb", float(zlb[i])) @@ -207,14 +203,14 @@ def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None def compute_middle(self, x, y): raise NotImplementedError("Implement compute_middle for specific envelope type.") - def compute_ub_lb(self, x, y, thickness): - raise NotImplementedError("Implement compute_ub_lb for specific envelope type.") + def compute_bounds(self, x, y, thickness): + raise NotImplementedError("Implement compute_bounds for specific envelope type.") - def compute_dub_dlb(self, x, y): - raise NotImplementedError("Implement compute_dub_dlb for specific envelope type.") + def compute_bounds_derivatives(self, x, y): + raise NotImplementedError("Implement compute_bounds_derivatives for specific envelope type.") def compute_bound_react(self, x, y, thickness, fixed): raise NotImplementedError("Implement compute_bound_react for specific envelope type.") - def compute_db(self, x, y, thickness, fixed): - return + def compute_bound_react_derivatives(self, x, y, thickness, fixed): + raise NotImplementedError("Implement compute_bound_react_derivatives for specific envelope type.") diff --git a/src/compas_tna/envelope/pavillionvault.py b/src/compas_tna/envelope/pavillionvault.py index 98830958..15fc1fc9 100644 --- a/src/compas_tna/envelope/pavillionvault.py +++ b/src/compas_tna/envelope/pavillionvault.py @@ -9,7 +9,7 @@ from compas_tna.envelope.parametricenvelope import ParametricEnvelope -def create_pavillionvault_envelope( +def pavillionvault_envelope( x_span: tuple = (0.0, 10.0), y_span: tuple = (0.0, 10.0), thickness: float = 0.50, @@ -49,32 +49,29 @@ def create_pavillionvault_envelope( xi, yi, _ = array(xyz0).transpose() # Create middle surface - zt = pavillionvault_middle_update(xi, yi, x_span=x_span, y_span=y_span, spr_angle=spr_angle, tol=1e-6) + zt = pavillionvault_middle(xi, yi, x_span=x_span, y_span=y_span, spr_angle=spr_angle, tol=1e-6) xyzt = array([xi, yi, zt.flatten()]).transpose() middle = Mesh.from_vertices_and_faces(xyzt, faces_i) middle.update_default_vertex_attributes(thickness=thickness) # Create upper and lower bounds - zub, zlb = pavillionvault_ub_lb_update( - xi, - yi, - thickness, - min_lb, - x_span=x_span, - y_span=y_span, - spr_angle=spr_angle, - tol=1e-6, - ) - xyzub = array([xi, yi, zub.flatten()]).transpose() + zub, zlb = pavillionvault_bounds(xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, spr_angle=spr_angle, tol=1e-6) xyzlb = array([xi, yi, zlb.flatten()]).transpose() + intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) + x_span_extra = (x_span[0] - thickness / 2 / math.cos(math.radians(spr_angle)), x_span[1] + thickness / 2 / math.cos(math.radians(spr_angle))) + y_span_extra = (y_span[0] - thickness / 2 / math.cos(math.radians(spr_angle)), y_span[1] + thickness / 2 / math.cos(math.radians(spr_angle))) + extra_topology = create_cross_mesh(x_span=x_span_extra, y_span=y_span_extra, n=n) + xyz0, faces_i = extra_topology.to_vertices_and_faces() + xi, yi, _ = array(xyz0).transpose() + zub, zlb = pavillionvault_bounds(xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, spr_angle=spr_angle, tol=1e-6) + xyzub = array([xi, yi, zub.flatten()]).transpose() extrados = Mesh.from_vertices_and_faces(xyzub, faces_i) - intrados = Mesh.from_vertices_and_faces(xyzlb, faces_i) return intrados, extrados, middle -def pavillionvault_middle_update(x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): +def pavillionvault_middle(x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): """Update middle of a pavillion vault based in the parameters Parameters @@ -132,7 +129,7 @@ def pavillionvault_middle_update(x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), s return z -def pavillionvault_ub_lb_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): +def pavillionvault_bounds(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): """Update upper and lower bounds of a pavillionvault based in the parameters Parameters @@ -220,7 +217,7 @@ def pavillionvault_ub_lb_update(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0 return ub, lb -def pavillionvault_dub_dlb(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): +def pavillionvault_bounds_derivatives(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): """Computes the sensitivities of upper and lower bounds in the x, y coordinates and thickness specified. Parameters @@ -306,7 +303,7 @@ def pavillionvault_dub_dlb(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 1 return dub, dlb # ub, lb -def pavillionvault_bound_react_update(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): +def pavillionvault_bound_react(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): """Computes the ``b`` of parameter x, y coordinates and thickness specified. Parameters @@ -349,7 +346,7 @@ def pavillionvault_bound_react_update(x, y, thk, fixed, x_span=(0.0, 10.0), y_sp return abs(b) -def pavillionvault_db(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): +def pavillionvault_bound_react_derivatives(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): """Computes the sensitivities of the ``b`` parameter in the x, y coordinates and thickness specified. Parameters @@ -426,7 +423,7 @@ def __str__(self): return f"PavillionVaultEnvelope(name={self.name})" def update_envelope(self): - intrados, extrados, middle = create_pavillionvault_envelope( + intrados, extrados, middle = pavillionvault_envelope( x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n, spr_angle=self.spr_angle ) self.intrados = intrados @@ -434,32 +431,32 @@ def update_envelope(self): self.middle = middle def compute_middle(self, x, y): - return pavillionvault_middle_update(x, y, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) + return pavillionvault_middle(x, y, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) - def compute_ub_lb(self, x, y, thickness=None): + def compute_bounds(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pavillionvault_ub_lb_update(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) + return pavillionvault_bounds(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, spr_angle=self.spr_angle, tol=1e-6) - def compute_dub_dlb(self, x, y, thickness=None): + def compute_bounds_derivatives(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pavillionvault_dub_dlb(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, tol=1e-6) + return pavillionvault_bounds_derivatives(x, y, thickness, self.min_lb, x_span=self.x_span, y_span=self.y_span, tol=1e-6) def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pavillionvault_bound_react_update(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) + return pavillionvault_bound_react(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) - def compute_db(self, x, y, thickness=None, fixed=None): + def compute_bound_react_derivatives(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pavillionvault_db(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) + return pavillionvault_bound_react_derivatives(x, y, thickness, fixed, x_span=self.x_span, y_span=self.y_span, tol=1e-6) diff --git a/src/compas_tna/envelope/pointedvault.py b/src/compas_tna/envelope/pointedvault.py index b8fdc0b5..1fece0dc 100644 --- a/src/compas_tna/envelope/pointedvault.py +++ b/src/compas_tna/envelope/pointedvault.py @@ -9,7 +9,7 @@ from compas_tna.envelope.parametricenvelope import ParametricEnvelope -def create_pointedvault_envelope( +def pointedvault_envelope( x_span: tuple = (0.0, 10.0), y_span: tuple = (0.0, 10.0), thickness: float = 0.50, @@ -55,24 +55,13 @@ def create_pointedvault_envelope( xi, yi, _ = array(xyz0).transpose() # Create middle surface - zt = pointedvault_middle_update(xi, yi, min_lb, x_span=x_span, y_span=y_span, hc=hc, he=he, hm=hm, tol=1e-6) + zt = pointedvault_middle(xi, yi, min_lb, x_span=x_span, y_span=y_span, hc=hc, he=he, hm=hm, tol=1e-6) xyzt = array([xi, yi, zt.flatten()]).transpose() middle = Mesh.from_vertices_and_faces(xyzt, faces_i) middle.update_default_vertex_attributes(thickness=thickness) # Create upper and lower bounds - zub, zlb = pointedvault_ub_lb_update( - xi, - yi, - thickness, - min_lb, - x_span=x_span, - y_span=y_span, - hc=hc, - he=he, - hm=hm, - tol=1e-6, - ) + zub, zlb = pointedvault_bounds(xi, yi, thickness, min_lb, x_span=x_span, y_span=y_span, hc=hc, he=he, hm=hm, tol=1e-6) xyzub = array([xi, yi, zub.flatten()]).transpose() xyzlb = array([xi, yi, zlb.flatten()]).transpose() @@ -82,7 +71,7 @@ def create_pointedvault_envelope( return intrados, extrados, middle -def pointedvault_middle_update( +def pointedvault_middle( x, y, min_lb, @@ -199,7 +188,7 @@ def pointedvault_middle_update( return middle -def pointedvault_ub_lb_update( +def pointedvault_bounds( x, y, thk, @@ -347,7 +336,7 @@ def pointedvault_ub_lb_update( return ub, lb -def pointedvault_dub_dlb( +def pointedvault_bounds_derivatives( x, y, thk, @@ -510,7 +499,7 @@ def pointedvault_dub_dlb( return dub, dlb # ub, lb -def pointedvault_bound_react_update( +def pointedvault_bound_react( x, y, thk, @@ -526,7 +515,7 @@ def pointedvault_bound_react_update( pass -def pointedvault_db( +def pointedvault_bound_react_derivatives( x, y, thk, @@ -632,7 +621,7 @@ def __str__(self): return f"PointedVaultEnvelope(name={self.name})" def update_envelope(self): - intrados, extrados, middle = create_pointedvault_envelope( + intrados, extrados, middle = pointedvault_envelope( x_span=self.x_span, y_span=self.y_span, thickness=self.thickness, min_lb=self.min_lb, n=self.n, hc=self.hc, he=self.he, hm=self.hm ) self.intrados = intrados @@ -640,32 +629,32 @@ def update_envelope(self): self.middle = middle def compute_middle(self, x, y): - return pointedvault_middle_update(x, y, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + return pointedvault_middle(x, y, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def compute_ub_lb(self, x, y, thickness=None): + def compute_bounds(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pointedvault_ub_lb_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + return pointedvault_bounds(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def compute_dub_dlb(self, x, y, thickness=None): + def compute_bounds_derivatives(self, x, y, thickness=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pointedvault_dub_dlb(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + return pointedvault_bounds_derivatives(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) def compute_bound_react(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pointedvault_bound_react_update(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + return pointedvault_bound_react(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) - def compute_db(self, x, y, thickness=None, fixed=None): + def compute_bound_react_derivatives(self, x, y, thickness=None, fixed=None): if thickness is None: thickness = self.thickness else: self.thickness = thickness - return pointedvault_db(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) + return pointedvault_bound_react_derivatives(x, y, thickness, self.min_lb, self.x_span, self.y_span, self.hc, self.he, self.hm) From 78a5171c4efc3cae0f8c3c45a1d8e1785d7ae9e8 Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Tue, 2 Sep 2025 11:35:44 +0200 Subject: [PATCH 7/9] chagelog update --- CHANGELOG.md | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c206cefe..856fe730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,23 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Added `Envelope` class for masonry structure boundaries with intrados, extrados, and middle meshes -* Added direct envelope creation methods to `Envelope` class: - * `from_crossvault()` - Creates cross vault envelopes with configurable spans and thickness - * `from_dome()` - Creates dome envelopes with configurable radius, center, and oculus - * `from_pavillionvault()` - Creates pavillion vault envelopes with spring angle support - * `from_pointedvault()` - Creates pointed vault envelopes with height control parameters -* Added envelope creation functions for each vault type: - * `create_crossvault_envelope()` - Generates cross vault meshes and callables - * `create_dome_envelope()` - Generates dome meshes with circular topology - * `create_pavillionvault_envelope()` - Generates pavillion vault meshes with expanded option - * `create_pointedvault_envelope()` - Generates pointed vault meshes with height parameters -* Added vault-specific update functions for optimization: - * `crossvault_middle_update()`, `crossvault_ub_lb_update()`, `crossvault_dub_dlb()` - * `dome_middle_update()`, `dome_ub_lb_update()`, `dome_dub_dlb()` - * `pavillionvault_middle_update()`, `pavillionvault_ub_lb_update()`, `pavillionvault_dub_dlb()` - * `pointedvault_middle_update()`, `pointedvault_ub_lb_update()`, `pointedvault_dub_dlb()` - +* Added `Envelope` base class for masonry structure boundaries with intrados, extrados, and middle meshes +* Added especialized `MeshEnvelope`, `ParametricEnvelope`, and `BrepEnvelope` to deal with different evelope inputs. +* Added direct parametric envelope creation methods inheriting from `ParametricEnvelope` class: + * `CrossVaultEnvelope` - Creates cross vault envelopes with configurable spans and thickness + * `DomeEnvelope` - Creates dome envelopes with configurable radius, center, and oculus + * `PavillionVaultEnvelope` - Creates pavillion vault envelopes with spring angle support + * `PointedVaultEnvelope` - Creates pointed vault envelopes with height control parameters +* The infrastructure around these classes has been updated to enable assigning constraints to the form diagrams. * Added comprehensive form diagram creation methods to `FormDiagram` class: * `create_cross()` - Creates cross discretisation with orthogonal arrangement and quad diagonals * `create_fan()` - Creates fan discretisation with straight lines to corners @@ -42,18 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added corner detection functionality: * `corner_vertices()` - Identifies corner vertices on boundary with configurable angle threshold * Added comprehensive mesh creation functions in `diagram_rectangular.py`: - * `create_cross_mesh()` - Creates cross pattern meshes - * `create_fan_mesh()` - Creates fan pattern meshes - * `create_parametric_fan_mesh()` - Creates parametric fan meshes with lambda interpolation - * `create_cross_with_diagonal_mesh()` - Creates cross meshes with diagonals - * `create_ortho_mesh()` - Creates orthogonal grid meshes * Added circular mesh creation functions in `diagram_circular.py`: - * `create_circular_radial_mesh()` - Creates circular radial meshes - * `create_circular_radial_spaced_mesh()` - Creates hemispherically spaced circular meshes - * `create_circular_spiral_mesh()` - Creates circular spiral meshes * Added arch mesh creation functions in `diagram_arch.py`: - * `create_arch_linear_mesh()` - Creates arch meshes with semicircular projection - * `create_arch_linear_equally_spaced_mesh()` - Creates arch meshes with equally spaced nodes ### Changed From 335b92e722493e66a1095a36a01f47ca9932bc29 Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Tue, 2 Sep 2025 11:39:12 +0200 Subject: [PATCH 8/9] update changelog --- CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 856fe730..7b64eece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,15 +38,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* Refactored parameter passing from `xy_span=[[x0, x1], [y0, y1]]` to `x_span=(x0, x1), y_span=(y0, y1)` for better API consistency -* Changed `center` parameter from list to tuple in circular diagram functions for immutability and consistency -* Updated function signatures to use single-line format for Black compatibility -* Improved code formatting and linting across all diagram generation files -* Fixed various spelling errors and documentation formatting issues -* Renamed `create_delta_form()` to `create_cross_with_diagonal()` for better clarity -* Updated support assignment to use `is_support` attribute instead of deprecated `is_fixed` -* Removed deprecated `form.parameters` usage from all form creation methods - ### Removed From 4df400346fb359ff2f3ef4eb6761b9114def5b03 Mon Sep 17 00:00:00 2001 From: Ricardo Maia Avelino Date: Tue, 2 Sep 2025 13:03:44 +0200 Subject: [PATCH 9/9] language --- src/compas_tna/envelope/crossvault.py | 4 ++-- src/compas_tna/envelope/dome.py | 16 ++++++++-------- src/compas_tna/envelope/pavillionvault.py | 12 ++++++------ src/compas_tna/envelope/pointedvault.py | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/compas_tna/envelope/crossvault.py b/src/compas_tna/envelope/crossvault.py index 6e6c5b87..a35f6838 100644 --- a/src/compas_tna/envelope/crossvault.py +++ b/src/compas_tna/envelope/crossvault.py @@ -63,7 +63,7 @@ def crossvault_envelope( def crossvault_middle(x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): - """Update middle of a crossvault based in the parameters + """Compute middle of a crossvault based on the parameters. Parameters ---------- @@ -127,7 +127,7 @@ def crossvault_middle(x, y, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol= def crossvault_bounds(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), tol=1e-6): - """Update upper and lower bounds of an crossvault based in the parameters + """Compute upper and lower bounds of an crossvault based on the parameters. Parameters ---------- diff --git a/src/compas_tna/envelope/dome.py b/src/compas_tna/envelope/dome.py index 03d017dd..19ce1b0e 100644 --- a/src/compas_tna/envelope/dome.py +++ b/src/compas_tna/envelope/dome.py @@ -76,7 +76,7 @@ def dome_envelope( def dome_middle(x, y, radius, min_lb, center=(5.0, 5.0)): - """Update middle of the dome based in the parameters + """Compute middle of the dome based on the parameters. Parameters ---------- @@ -112,7 +112,7 @@ def dome_middle(x, y, radius, min_lb, center=(5.0, 5.0)): def dome_bounds(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): - """Update upper and lower bounds of the dome based in the parameters + """Compute upper and lower bounds of the dome based on the parameters. Parameters ---------- @@ -155,7 +155,7 @@ def dome_bounds(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): def dome_bounds_derivatives(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): - """Update sensitivities of upper and lower bounds of the dome based in the parameters + """Compute sensitivities of upper and lower bounds of the dome based on the parameters. Parameters ---------- @@ -204,11 +204,11 @@ def dome_bounds_derivatives(x, y, thk, min_lb, center=(5.0, 5.0), radius=5.0): dlbdx[i, i] = 1 / 2 / zi * -2 * (x[i] - xc) dlbdy[i, i] = 1 / 2 / zi * -2 * (y[i] - yc) - return dub, dlb, dubdx, dubdy, dlbdx, dlbdy + return dub, dlb def dome_bound_react(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): - """Updates the ``b`` parameter of a dome for a given thickness + """Computes the reaction bounds of a dome for a given thickness Parameters ---------- @@ -228,7 +228,7 @@ def dome_bound_react(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): Returns ------- b : array - The ``b`` parameter + The reaction bounds """ [xc, yc] = center[:2] @@ -246,7 +246,7 @@ def dome_bound_react(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): def dome_bound_react_derivatives(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0): - """Updates the ``db`` parameter of a dome for a given thickness + """Computes the reaction bounds derivatives of a dome for a given thickness Parameters ---------- @@ -266,7 +266,7 @@ def dome_bound_react_derivatives(x, y, thk, fixed, center=(5.0, 5.0), radius=5.0 Returns ------- db : array - The sensitivity of the ``b`` parameter + The sensitivity of the reaction bounds """ [xc, yc] = center[:2] diff --git a/src/compas_tna/envelope/pavillionvault.py b/src/compas_tna/envelope/pavillionvault.py index 15fc1fc9..58ab0fbc 100644 --- a/src/compas_tna/envelope/pavillionvault.py +++ b/src/compas_tna/envelope/pavillionvault.py @@ -72,7 +72,7 @@ def pavillionvault_envelope( def pavillionvault_middle(x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): - """Update middle of a pavillion vault based in the parameters + """Compute middle of a pavillion vault based on the parameters. Parameters ---------- @@ -130,7 +130,7 @@ def pavillionvault_middle(x, y, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angl def pavillionvault_bounds(x, y, thk, min_lb, x_span=(0.0, 10.0), y_span=(0.0, 10.0), spr_angle=0.0, tol=1e-6): - """Update upper and lower bounds of a pavillionvault based in the parameters + """Compute upper and lower bounds of a pavillionvault based on the parameters. Parameters ---------- @@ -304,7 +304,7 @@ def pavillionvault_bounds_derivatives(x, y, thk, min_lb, x_span=(0.0, 10.0), y_s def pavillionvault_bound_react(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): - """Computes the ``b`` of parameter x, y coordinates and thickness specified. + """Computes the reaction bounds of a pavillion vault for a given thickness Parameters ---------- @@ -324,7 +324,7 @@ def pavillionvault_bound_react(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0 Returns ------- b : array - Values of the ``b`` parameter + Values of the reaction bounds """ x0, x1 = x_span @@ -347,7 +347,7 @@ def pavillionvault_bound_react(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0 def pavillionvault_bound_react_derivatives(x, y, thk, fixed, x_span=(0.0, 10.0), y_span=(0.0, 10.0)): - """Computes the sensitivities of the ``b`` parameter in the x, y coordinates and thickness specified. + """Computes the sensitivities of the reaction bounds of a pavillion vault for a given thickness Parameters ---------- @@ -367,7 +367,7 @@ def pavillionvault_bound_react_derivatives(x, y, thk, fixed, x_span=(0.0, 10.0), Returns ------- db : array - Values of the sensitivities of the ``b`` parameter in the points + Values of the sensitivities of the reaction bounds in the points """ x0, x1 = x_span diff --git a/src/compas_tna/envelope/pointedvault.py b/src/compas_tna/envelope/pointedvault.py index 1fece0dc..a8568413 100644 --- a/src/compas_tna/envelope/pointedvault.py +++ b/src/compas_tna/envelope/pointedvault.py @@ -82,7 +82,7 @@ def pointedvault_middle( hm=None, tol=1e-6, ): - """Update middle of a pointed vault based in the parameters + """Compute middle of a pointed vault based on the parameters. Parameters ---------- @@ -200,7 +200,7 @@ def pointedvault_bounds( hm=None, tol=1e-6, ): - """Update upper and lower bounds of a pointed vault based in the parameters + """Compute upper and lower bounds of a pointed vault based on the parameters. Parameters ----------