From 7a473ae063cd52b5f2d1324ce16086dd0e6c9302 Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 8 Sep 2025 16:38:13 -0600 Subject: [PATCH 01/37] adding new options for torque tube radius, sep. distance, and modules_per_span --- pvade/IO/input_schema.yaml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index 1c99362..335dacb 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -136,7 +136,7 @@ properties: minimum: 0.0 maximum: 10.0 type: "number" - description: "The vertical distance between the center of the panel and the ground." + description: "The vertical distance between the center of the torque tube and the ground." units: "meter" stream_spacing: default: 7.0 @@ -198,6 +198,23 @@ properties: be unrolled such that tracking_angle_0 is applied to the panel at (x_min, y_min), tracking_angle_1 is applied to the panel at (x_min + x_spacing, y_min), etc., i.e., x-major direction. If argument is a single value, that tracking angle will be applied to the every panel." units: "degree" + torque_tube_separation: + default: 0.4 + minimum: 0.0 + maximum: 1.0 + type: "number" + description: "The distance between the bottom of the panel structure and the centerline of the torque tube, in meters." + torque_tube_radius: + default: 0.1 + minimum: 0.0 + maximum: 1.0 + type: "number" + description: "The radius of the torque tube, in meters." + modules_per_span: + default: 1 + minimum: 1 + type: "integer" + description: "The number of modules/panels per every panel_span distance. This can be thought of as the number of modules/panels per each table, counted in the spanwise direction only (i.e., 2-in-portait systems should report only the number in the spanwise direction, vs 2x that value)." solver: additionalProperties: false type: "object" From e5e8bb7220d7334dce4086f2f55527c917b81114 Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 8 Sep 2025 16:38:43 -0600 Subject: [PATCH 02/37] making panels differently, including torque tube standoff and discrete modules per span --- pvade/geometry/panels3d/DomainCreation.py | 393 +++++++++++++--------- 1 file changed, 225 insertions(+), 168 deletions(-) diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 5ead1e4..920eda6 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -152,7 +152,11 @@ def Rz(theta): panel_tag_list = [] panel_ct = 0 - prev_surf_tag = [] + module_distances = np.linspace( + -half_span, half_span, params.pv_array.modules_per_span + 1 + ) + + transformed_com = {} for panel_id_y, yy in enumerate(y_centers): for panel_id_x, xx in enumerate(x_centers): @@ -169,29 +173,48 @@ def Rz(theta): else: tracker_angle_rad = np.radians(params.pv_array.tracker_angle) - # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) - panel_id = self.gmsh_model.occ.addBox( - -half_chord, - -half_span, - -half_thickness, - params.pv_array.panel_chord, - params.pv_array.panel_span, - params.pv_array.panel_thickness, - ) + this_panel_tag_list = [] + this_panel_transformed_com = {} - panel_tag = (self.ndim, panel_id) - panel_tag_list.append(panel_tag) + for module_id in range(params.pv_array.modules_per_span): + + module_span = ( + module_distances[module_id + 1] - module_distances[module_id] + ) + + # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) + this_module = self.gmsh_model.occ.addBox( + -half_chord, + module_distances[module_id], + params.pv_array.torque_tube_separation, + params.pv_array.panel_chord, + module_span, + params.pv_array.panel_thickness, + ) + + this_standoff = self.gmsh_model.occ.addBox( + -params.pv_array.torque_tube_radius, + module_distances[module_id], + 0.0, + 2.0 * params.pv_array.torque_tube_radius, + module_span, + params.pv_array.torque_tube_separation, + ) + + panel_tag_list.append((self.ndim, this_module)) + panel_tag_list.append((self.ndim, this_standoff)) + + this_panel_tag_list.append((self.ndim, this_module)) + this_panel_tag_list.append((self.ndim, this_standoff)) numpy_pt_list = [] embedded_lines_tag_list = [] # Add a bisecting line to the bottom of the panel in the spanwise direction - pt_1 = self.gmsh_model.occ.addPoint(0, -half_span, -half_thickness) - pt_2 = self.gmsh_model.occ.addPoint(0, half_span, -half_thickness) + pt_1 = self.gmsh_model.occ.addPoint(0, -half_span, 0.0) + pt_2 = self.gmsh_model.occ.addPoint(0, half_span, 0.0) - numpy_pt_list.append( - [0, -half_span, -half_thickness, 0, half_span, -half_thickness] - ) + numpy_pt_list.append([0, -half_span, 0.0, 0, half_span, 0.0]) torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) torque_tube_tag = (1, torque_tube_id) @@ -224,150 +247,149 @@ def Rz(theta): fixation_pts_list = params.pv_array.span_fixation_pts for fp in fixation_pts_list: - pt_1 = self.gmsh_model.occ.addPoint( - -half_chord, -half_span + fp, -half_thickness - ) - pt_2 = self.gmsh_model.occ.addPoint( - half_chord, -half_span + fp, -half_thickness - ) - # FIXME: don't add the fixation points into the numpy tagging for now numpy_pt_list.append( [ - -half_chord, + -params.pv_array.torque_tube_radius, -half_span + fp, - -half_thickness, - half_chord, + 0.0, + params.pv_array.torque_tube_radius, -half_span + fp, - -half_thickness, + 0.0, ] ) - fixed_pt_id = self.gmsh_model.occ.addLine(pt_1, pt_2) - fixed_pt_tag = (1, fixed_pt_id) - - embedded_lines_tag_list.append(fixed_pt_tag) - - # Store the result of fragmentation, it holds all the small surfaces we need to tag - panel_frags = self.gmsh_model.occ.fragment( - [panel_tag], embedded_lines_tag_list + # Fragment the lines into the surfaces (equivalent to embedding these lines) + self.gmsh_model.occ.fragment( + this_panel_tag_list, embedded_lines_tag_list ) - # extract just the first entry, and remove the 3d entry in position 0 - panel_surfs = panel_frags[0] - panel_surfs.pop(0) - panel_surfs = [k[1] for k in panel_surfs] - - # TODO: USE THESE UNAMBIGUOUS NAMES IN A FUTURE REFACTOR - # self._add_to_domain_markers(f"x_min_{panel_ct:.0f}", [panel_surfs[0]], "facet") - # self._add_to_domain_markers(f"x_max_{panel_ct:.0f}", [panel_surfs[1]], "facet") - # self._add_to_domain_markers(f"y_min_{panel_ct:.0f}", [panel_surfs[2]], "facet") - # self._add_to_domain_markers(f"y_max_{panel_ct:.0f}", [panel_surfs[3]], "facet") - # self._add_to_domain_markers(f"z_min_{panel_ct:.0f}", panel_surfs[4:-1], "facet") - # self._add_to_domain_markers(f"z_max_{panel_ct:.0f}", [panel_surfs[-1]], "facet") - - # Translate the panel by (x_center, y_center, elev) - self.gmsh_model.occ.translate( - [panel_tag], - xx, - yy, - params.pv_array.elevation, - ) + for panel_tag in this_panel_tag_list: + self.gmsh_model.occ.synchronize() - # Get the bounding box for this panel - panel_bounding_box = self.gmsh_model.occ.getBoundingBox( - panel_tag[0], panel_tag[1] - ) + # Get the list of 2D surfaces (surfaces) that make up this panel + surf_tags_for_this_panel = self.gmsh_model.getBoundary( + [panel_tag], oriented=False + ) - # Store the x_min, y_min, x_max, y_max points - # corresponding to the un-rotated (tracking_angle = 0) configuration - x_min_panel = panel_bounding_box[0] - y_min_panel = panel_bounding_box[1] - z_min_panel = panel_bounding_box[2] - x_max_panel = panel_bounding_box[3] - y_max_panel = panel_bounding_box[4] - z_max_panel = panel_bounding_box[5] + for surf_tag in surf_tags_for_this_panel: + surf_dim = surf_tag[0] + surf_id = surf_tag[1] + com = self.gmsh_model.occ.getCenterOfMass(surf_dim, surf_id) + + # sturctures tagging + if np.isclose(com[0], -half_chord) or np.isclose( + com[0], -params.pv_array.torque_tube_radius + ): + key = f"left_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] + + elif np.isclose(com[0], half_chord) or np.isclose( + com[0], params.pv_array.torque_tube_radius + ): + key = f"right_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] + + elif np.isclose(com[1], -half_span): + key = f"front_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] + + elif np.isclose(com[1], half_span): + key = f"back_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] + + elif np.isclose( + com[2], params.pv_array.torque_tube_separation + ) and not np.isclose(com[0], 0.0): + key = f"bottom_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] + + elif np.isclose(com[2], 0.0): + key = f"torque_tube_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] + + elif np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness, + ): + key = f"top_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] + else: + self._add_to_domain_markers( + f"trash_{panel_ct:.0f}", [surf_id], "facet" + ) - self.gmsh_model.occ.synchronize() + for key, val in this_panel_transformed_com.items(): + for row_num, com in enumerate(val): + com_array = np.array(com) - # Get the list of 1D surfaces (edges) that make up this panel - surf_tags_for_this_panel = self.gmsh_model.getBoundary( - [panel_tag], oriented=False - ) + com_array = np.dot(com_array, Ry(tracker_angle_rad).T) - for surf_tag in surf_tags_for_this_panel: - surf_dim = surf_tag[0] - surf_id = surf_tag[1] - com = self.gmsh_model.occ.getCenterOfMass(surf_dim, surf_id) - # sturctures tagging - if np.isclose(com[0], x_min_panel): - self._add_to_domain_markers( - f"left_{panel_ct:.0f}", [surf_id], "facet" - ) + com_array[0] += xx + com_array[1] += yy + com_array[2] += params.pv_array.elevation - elif np.isclose(com[0], x_max_panel): - self._add_to_domain_markers( - f"right_{panel_ct:.0f}", [surf_id], "facet" - ) + com_array[0] -= x_center_of_mass + com_array[1] -= y_center_of_mass - elif np.isclose(com[1], y_min_panel): - self._add_to_domain_markers( - f"front_{panel_ct:.0f}", [surf_id], "facet" - ) + com_array = np.dot(com_array, Rz(array_rotation_rad).T) - elif np.isclose(com[1], y_max_panel): - self._add_to_domain_markers( - f"back_{panel_ct:.0f}", [surf_id], "facet" - ) + com_array[0] += x_center_of_mass + com_array[1] += y_center_of_mass - elif np.isclose(com[2], z_min_panel): - self._add_to_domain_markers( - f"bottom_{panel_ct:.0f}", [surf_id], "facet" - ) - - elif np.isclose(com[2], z_max_panel): - self._add_to_domain_markers( - f"top_{panel_ct:.0f}", [surf_id], "facet" - ) + if key in transformed_com: + transformed_com[key].append(com_array) + else: + transformed_com[key] = [com_array] panel_ct += 1 # Rotate the panel by its tracking angle along the y-axis - # (currently centered at (xx, yy, params.pv_array.elevation)) + # (currently centered at (0.0, 0.0, 0.0)) self.gmsh_model.occ.rotate( - [panel_tag], - xx, - yy, - params.pv_array.elevation, + this_panel_tag_list, + 0.0, + 0.0, + 0.0, 0, 1, 0, tracker_angle_rad, ) - # Note that the numpy pt panel array has NOT been shifted, i.e., - # it still represents a panel centered at (0, 0, 0), so the rotation - # can be applied directly about the point (0, 0, 0) without any - # shifting like that which needs to be done above - numpy_pt_panel_array = np.array(numpy_pt_list) - numpy_pt_panel_array = np.reshape(numpy_pt_panel_array, (-1, self.ndim)) - - numpy_pt_panel_array = np.dot( - numpy_pt_panel_array, Ry(tracker_angle_rad).T + # Translate the panel by (x_center, y_center, elev) + self.gmsh_model.occ.translate( + this_panel_tag_list, + xx, + yy, + params.pv_array.elevation, ) - # if not hasattr(self, "numpy_pt_array"): - # numpy_pt_array = np.array(numpy_pt_list) - # else: - # numpy_pt_array = np.vcat(numpy_pt_array, np.array(numpy_pt_list)) - - numpy_pt_panel_array[:, 0] += xx - numpy_pt_panel_array[:, 1] += yy - numpy_pt_panel_array[:, 2] += params.pv_array.elevation - # Rotate the panel about the center of the full array as a proxy for changing wind direction (x_center, y_center, 0) self.gmsh_model.occ.rotate( - [panel_tag], + this_panel_tag_list, x_center_of_mass, y_center_of_mass, 0, @@ -377,6 +399,22 @@ def Rz(theta): array_rotation_rad, ) + # Now, apply the same transformations to the numpy representation + # Rotate the panel by its tracking angle along the y-axis + # (currently centered at (0.0, 0.0, 0.0)) + numpy_pt_panel_array = np.array(numpy_pt_list) + numpy_pt_panel_array = np.reshape(numpy_pt_panel_array, (-1, self.ndim)) + + numpy_pt_panel_array = np.dot( + numpy_pt_panel_array, Ry(tracker_angle_rad).T + ) + + # Translate the panel by (x_center, y_center, elev) + numpy_pt_panel_array[:, 0] += xx + numpy_pt_panel_array[:, 1] += yy + numpy_pt_panel_array[:, 2] += params.pv_array.elevation + + # Rotate the panel about the center of the full array as a proxy for changing wind direction (x_center, y_center, 0) numpy_pt_panel_array[:, 0] -= x_center_of_mass numpy_pt_panel_array[:, 1] -= y_center_of_mass @@ -394,25 +432,26 @@ def Rz(theta): else: self.numpy_pt_total_array = np.copy(numpy_pt_panel_array) - # Check that this panel still exists in the confines of the domain - bbox = self.gmsh_model.occ.get_bounding_box(panel_tag[0], panel_tag[1]) - - if bbox[0] < params.domain.x_min: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past x_min wall." - ) - if bbox[1] < params.domain.y_min: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past y_min wall." - ) - if bbox[3] > params.domain.x_max: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past x_max wall." - ) - if bbox[4] > params.domain.y_max: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past y_max wall." - ) + # # Check that this panel still exists in the confines of the domain + # bbox = self.gmsh_model.occ.get_bounding_box(panel_tag_list) + + # if bbox[0] < params.domain.x_min: + # raise ValueError( + # f"Panel with location (x, y) = ({xx}, {yy}) extends past x_min wall." + # ) + # if bbox[1] < params.domain.y_min: + # raise ValueError( + # f"Panel with location (x, y) = ({xx}, {yy}) extends past y_min wall." + # ) + # if bbox[3] > params.domain.x_max: + # raise ValueError( + # f"Panel with location (x, y) = ({xx}, {yy}) extends past x_max wall." + # ) + # if bbox[4] > params.domain.y_max: + # raise ValueError( + # f"Panel with location (x, y) = ({xx}, {yy}) extends past y_max wall." + # ) + # self.gmsh_model.occ.synchronize() # Fragment all panels from the overall domain self.gmsh_model.occ.fragment(domain_tag_list, panel_tag_list) @@ -423,49 +462,69 @@ def Rz(theta): self.numpy_pt_total_array, (-1, int(2 * self.ndim)) ) - # import matplotlib.pyplot as plt - # for k in self.numpy_pt_total_array: - # plt.plot([k[0], k[3]], [k[1], k[4]]) - # plt.show() - - # it is not necessary to loop over all surfaces, since the panel - # surfaces have been tagged already, but this ensures any ordering change - # doesn't cause problems in the future + # Loop over all the finalized surfaces after fragmentation and tag everything all_surf_tag_list = self.gmsh_model.occ.getEntities(self.ndim - 1) for surf_tag in all_surf_tag_list: surf_id = surf_tag[1] com = self.gmsh_model.occ.getCenterOfMass(self.ndim - 1, surf_id) + tagged_this_surface = False + # sturctures tagging if np.isclose(com[0], params.domain.x_min): self._add_to_domain_markers("x_min", [surf_id], "facet") + tagged_this_surface = True elif np.allclose(com[0], params.domain.x_max): self._add_to_domain_markers("x_max", [surf_id], "facet") + tagged_this_surface = True elif np.allclose(com[1], params.domain.y_min): self._add_to_domain_markers("y_min", [surf_id], "facet") + tagged_this_surface = True elif np.allclose(com[1], params.domain.y_max): self._add_to_domain_markers("y_max", [surf_id], "facet") + tagged_this_surface = True elif np.allclose(com[2], params.domain.z_min): self._add_to_domain_markers("z_min", [surf_id], "facet") + tagged_this_surface = True elif np.allclose(com[2], params.domain.z_max): self._add_to_domain_markers("z_max", [surf_id], "facet") + tagged_this_surface = True + + else: + for key, val in transformed_com.items(): + for target_com in val: + # print(target_com) + if np.allclose(np.array(com), target_com): + self._add_to_domain_markers(key, [surf_id], "facet") + tagged_this_surface = True + + # if not tagged_this_surface: + # print(f"Warning: Surface {surf_tag} has not been added to domain markers") + + # print(self.domain_markers) # Volumes are the entities with dimension equal to the mesh dimension vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) structure_vol_list = [] fluid_vol_list = [] + num_solids = len(vol_tag_list) + for vol_tag in vol_tag_list: vol_id = vol_tag[1] - if vol_id <= params.pv_array.stream_rows * params.pv_array.span_rows: + if vol_id <= num_solids - 1: # Solid Cell + # Since all panel/table structures were created after the box domain + # the final fragmentation removes the original vol_tag of the box and appends + # it to the end, thus, everything with id < num_solids is structure, and + # vol_tag = num_solids (the last-added, fragmented box) is fluid structure_vol_list.append(vol_id) else: # Fluid Cell @@ -1144,23 +1203,21 @@ def set_length_scales(self, params, domain_markers): internal_surface_tags = [] for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): - internal_surface_tags.append( - domain_markers[f"bottom_{panel_id}"]["gmsh_tags"][0] + internal_surface_tags.extend( + domain_markers[f"bottom_{panel_id}"]["gmsh_tags"] ) - internal_surface_tags.append( - domain_markers[f"top_{panel_id}"]["gmsh_tags"][0] + internal_surface_tags.extend(domain_markers[f"top_{panel_id}"]["gmsh_tags"]) + internal_surface_tags.extend( + domain_markers[f"left_{panel_id}"]["gmsh_tags"] ) - internal_surface_tags.append( - domain_markers[f"left_{panel_id}"]["gmsh_tags"][0] - ) - internal_surface_tags.append( - domain_markers[f"right_{panel_id}"]["gmsh_tags"][0] + internal_surface_tags.extend( + domain_markers[f"right_{panel_id}"]["gmsh_tags"] ) - internal_surface_tags.append( - domain_markers[f"front_{panel_id}"]["gmsh_tags"][0] + internal_surface_tags.extend( + domain_markers[f"front_{panel_id}"]["gmsh_tags"] ) - internal_surface_tags.append( - domain_markers[f"back_{panel_id}"]["gmsh_tags"][0] + internal_surface_tags.extend( + domain_markers[f"back_{panel_id}"]["gmsh_tags"] ) min_dist = [] From b1f4f5e26fec8e4d1b9e8c2e5f6861bc3bbc3a7a Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 8 Sep 2025 17:25:43 -0600 Subject: [PATCH 03/37] ignore panel surfaces marked trash, add back in checks on bbox, add new check on z_min/max clipping --- pvade/geometry/panels3d/DomainCreation.py | 70 +++++++++++------------ 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 920eda6..ae793e9 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -337,9 +337,11 @@ def Rz(theta): else: this_panel_transformed_com[key] = [com] else: - self._add_to_domain_markers( - f"trash_{panel_ct:.0f}", [surf_id], "facet" - ) + key = f"trash_{panel_ct:.0f}" + if key in this_panel_transformed_com: + this_panel_transformed_com[key].append(com) + else: + this_panel_transformed_com[key] = [com] for key, val in this_panel_transformed_com.items(): for row_num, com in enumerate(val): @@ -432,27 +434,6 @@ def Rz(theta): else: self.numpy_pt_total_array = np.copy(numpy_pt_panel_array) - # # Check that this panel still exists in the confines of the domain - # bbox = self.gmsh_model.occ.get_bounding_box(panel_tag_list) - - # if bbox[0] < params.domain.x_min: - # raise ValueError( - # f"Panel with location (x, y) = ({xx}, {yy}) extends past x_min wall." - # ) - # if bbox[1] < params.domain.y_min: - # raise ValueError( - # f"Panel with location (x, y) = ({xx}, {yy}) extends past y_min wall." - # ) - # if bbox[3] > params.domain.x_max: - # raise ValueError( - # f"Panel with location (x, y) = ({xx}, {yy}) extends past x_max wall." - # ) - # if bbox[4] > params.domain.y_max: - # raise ValueError( - # f"Panel with location (x, y) = ({xx}, {yy}) extends past y_max wall." - # ) - # self.gmsh_model.occ.synchronize() - # Fragment all panels from the overall domain self.gmsh_model.occ.fragment(domain_tag_list, panel_tag_list) @@ -469,45 +450,62 @@ def Rz(theta): surf_id = surf_tag[1] com = self.gmsh_model.occ.getCenterOfMass(self.ndim - 1, surf_id) - tagged_this_surface = False + located_this_surface = False # sturctures tagging if np.isclose(com[0], params.domain.x_min): self._add_to_domain_markers("x_min", [surf_id], "facet") - tagged_this_surface = True elif np.allclose(com[0], params.domain.x_max): self._add_to_domain_markers("x_max", [surf_id], "facet") - tagged_this_surface = True elif np.allclose(com[1], params.domain.y_min): self._add_to_domain_markers("y_min", [surf_id], "facet") - tagged_this_surface = True elif np.allclose(com[1], params.domain.y_max): self._add_to_domain_markers("y_max", [surf_id], "facet") - tagged_this_surface = True elif np.allclose(com[2], params.domain.z_min): self._add_to_domain_markers("z_min", [surf_id], "facet") - tagged_this_surface = True elif np.allclose(com[2], params.domain.z_max): self._add_to_domain_markers("z_max", [surf_id], "facet") - tagged_this_surface = True else: for key, val in transformed_com.items(): for target_com in val: # print(target_com) if np.allclose(np.array(com), target_com): - self._add_to_domain_markers(key, [surf_id], "facet") - tagged_this_surface = True + located_this_surface = True + if "trash" not in key: + self._add_to_domain_markers(key, [surf_id], "facet") - # if not tagged_this_surface: - # print(f"Warning: Surface {surf_tag} has not been added to domain markers") + if not located_this_surface: + print( + f"Warning: Surface {surf_tag} has not been added to domain markers" + ) + + # Since this is not one of the exterior walls, we should check if it extends + # past the boundaries x_min, x_max, ... + this_surf_bbox = self.gmsh_model.occ.get_bounding_box( + self.ndim - 1, surf_id + ) - # print(self.domain_markers) + # Test that the rotated point still exists in the box domain + if this_surf_bbox[0] < params.domain.x_min: + raise ValueError(f"A panel extends past the x_min wall.") + if this_surf_bbox[0] > params.domain.x_max: + raise ValueError(f"A panel extends past the x_max wall.") + if this_surf_bbox[1] < params.domain.y_min: + raise ValueError(f"A panel extends past the y_min wall.") + if this_surf_bbox[1] > params.domain.y_max: + raise ValueError(f"A panel extends past the y_max wall.") + if this_surf_bbox[2] < 0.0: + raise ValueError( + f"A panel extends past the z_min wall (ground level = 0.0)." + ) + if this_surf_bbox[2] > params.domain.z_max: + raise ValueError(f"A panel extends past the z_max wall.") # Volumes are the entities with dimension equal to the mesh dimension vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) From 185b1fe6e1713c4e2e903b43538d02f961d573dd Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 12:40:42 -0600 Subject: [PATCH 04/37] change defaults for torque tube to zero to preserve old mesh behavior by default --- pvade/IO/input_schema.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index 335dacb..ddae4bb 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -199,13 +199,13 @@ properties: (x_min + x_spacing, y_min), etc., i.e., x-major direction. If argument is a single value, that tracking angle will be applied to the every panel." units: "degree" torque_tube_separation: - default: 0.4 + default: 0.0 minimum: 0.0 maximum: 1.0 type: "number" description: "The distance between the bottom of the panel structure and the centerline of the torque tube, in meters." torque_tube_radius: - default: 0.1 + default: 0.0 minimum: 0.0 maximum: 1.0 type: "number" From 96672ad83dbbc461372232e347b2563a30ba1957 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 12:41:25 -0600 Subject: [PATCH 05/37] clean up facet marking tests and revert to old meshing behavior if torque tube is not specified (length zero) --- pvade/geometry/panels3d/DomainCreation.py | 228 ++++++++++++++-------- 1 file changed, 145 insertions(+), 83 deletions(-) diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index ae793e9..31f4d42 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -158,6 +158,14 @@ def Rz(theta): transformed_com = {} + if ( + params.pv_array.torque_tube_separation > 0.0 + and params.pv_array.torque_tube_radius > 0.0 + ): + modeling_torque_tube = True + else: + modeling_torque_tube = False + for panel_id_y, yy in enumerate(y_centers): for panel_id_x, xx in enumerate(x_centers): if isinstance(params.pv_array.tracker_angle, list): @@ -182,30 +190,45 @@ def Rz(theta): module_distances[module_id + 1] - module_distances[module_id] ) - # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) - this_module = self.gmsh_model.occ.addBox( - -half_chord, - module_distances[module_id], - params.pv_array.torque_tube_separation, - params.pv_array.panel_chord, - module_span, - params.pv_array.panel_thickness, - ) + if modeling_torque_tube: + # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) + this_module = self.gmsh_model.occ.addBox( + -half_chord, + module_distances[module_id], + params.pv_array.torque_tube_separation, + params.pv_array.panel_chord, + module_span, + params.pv_array.panel_thickness, + ) - this_standoff = self.gmsh_model.occ.addBox( - -params.pv_array.torque_tube_radius, - module_distances[module_id], - 0.0, - 2.0 * params.pv_array.torque_tube_radius, - module_span, - params.pv_array.torque_tube_separation, - ) + this_standoff = self.gmsh_model.occ.addBox( + -params.pv_array.torque_tube_radius, + module_distances[module_id], + 0.0, + 2.0 * params.pv_array.torque_tube_radius, + module_span, + params.pv_array.torque_tube_separation, + ) + + panel_tag_list.append((self.ndim, this_module)) + panel_tag_list.append((self.ndim, this_standoff)) + + this_panel_tag_list.append((self.ndim, this_module)) + this_panel_tag_list.append((self.ndim, this_standoff)) + + else: + this_module = self.gmsh_model.occ.addBox( + -half_chord, + module_distances[module_id], + 0.0, + params.pv_array.panel_chord, + module_span, + params.pv_array.panel_thickness, + ) - panel_tag_list.append((self.ndim, this_module)) - panel_tag_list.append((self.ndim, this_standoff)) + panel_tag_list.append((self.ndim, this_module)) - this_panel_tag_list.append((self.ndim, this_module)) - this_panel_tag_list.append((self.ndim, this_standoff)) + this_panel_tag_list.append((self.ndim, this_module)) numpy_pt_list = [] embedded_lines_tag_list = [] @@ -248,16 +271,65 @@ def Rz(theta): for fp in fixation_pts_list: # FIXME: don't add the fixation points into the numpy tagging for now - numpy_pt_list.append( - [ - -params.pv_array.torque_tube_radius, - -half_span + fp, - 0.0, - params.pv_array.torque_tube_radius, - -half_span + fp, - 0.0, - ] - ) + if modeling_torque_tube: + numpy_pt_list.append( + [ + -params.pv_array.torque_tube_radius, + -half_span + fp, + 0.0, + params.pv_array.torque_tube_radius, + -half_span + fp, + 0.0, + ] + ) + + else: + numpy_pt_list.append( + [ + -half_chord, + -half_span + fp, + 0.0, + half_chord, + -half_span + fp, + 0.0, + ] + ) + + # If the separation line already exists at a module division, + # there's no need to redraw it, but otherwise, add the gmsh points + # to the model for embedding + if not np.any(np.isclose(-half_span + fp, module_distances)): + print( + f"Embedding a line for a no-deformation boundary condition." + ) + + if modeling_torque_tube: + pt_1 = self.gmsh_model.occ.addPoint( + -params.pv_array.torque_tube_radius, + -half_span + fp, + 0.0, + ) + pt_2 = self.gmsh_model.occ.addPoint( + params.pv_array.torque_tube_radius, + -half_span + fp, + 0.0, + ) + + else: + pt_1 = self.gmsh_model.occ.addPoint( + -half_chord, -half_span + fp, 0.0 + ) + pt_2 = self.gmsh_model.occ.addPoint( + half_chord, -half_span + fp, 0.0 + ) + + fixation_line_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + fixation_line_tag = (1, fixation_line_id) + embedded_lines_tag_list.append(fixation_line_tag) + else: + print( + f"Applying no-deformation boundary condition at {fp}." + ) # Fragment the lines into the surfaces (equivalent to embedding these lines) self.gmsh_model.occ.fragment( @@ -277,71 +349,61 @@ def Rz(theta): surf_id = surf_tag[1] com = self.gmsh_model.occ.getCenterOfMass(surf_dim, surf_id) + target_key = None + # sturctures tagging - if np.isclose(com[0], -half_chord) or np.isclose( - com[0], -params.pv_array.torque_tube_radius + if ( + np.isclose(com[0], -half_chord) + or np.isclose(com[0], -params.pv_array.torque_tube_radius) + and modeling_torque_tube ): - key = f"left_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) - else: - this_panel_transformed_com[key] = [com] + target_key = f"left_{panel_ct:.0f}" - elif np.isclose(com[0], half_chord) or np.isclose( - com[0], params.pv_array.torque_tube_radius + elif ( + np.isclose(com[0], half_chord) + or np.isclose(com[0], params.pv_array.torque_tube_radius) + and modeling_torque_tube ): - key = f"right_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) - else: - this_panel_transformed_com[key] = [com] + target_key = f"right_{panel_ct:.0f}" elif np.isclose(com[1], -half_span): - key = f"front_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) - else: - this_panel_transformed_com[key] = [com] + target_key = f"front_{panel_ct:.0f}" elif np.isclose(com[1], half_span): - key = f"back_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) - else: - this_panel_transformed_com[key] = [com] - - elif np.isclose( - com[2], params.pv_array.torque_tube_separation - ) and not np.isclose(com[0], 0.0): - key = f"bottom_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) - else: - this_panel_transformed_com[key] = [com] + target_key = f"back_{panel_ct:.0f}" + + elif ( + np.isclose(com[2], params.pv_array.torque_tube_separation) + and not np.isclose(com[0], 0.0) + and modeling_torque_tube + or np.isclose(com[2], 0.0) + and not modeling_torque_tube + ): + target_key = f"bottom_{panel_ct:.0f}" - elif np.isclose(com[2], 0.0): - key = f"torque_tube_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) - else: - this_panel_transformed_com[key] = [com] + elif np.isclose(com[2], 0.0) and modeling_torque_tube: + target_key = f"torque_tube_{panel_ct:.0f}" - elif np.isclose( - com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness, + elif ( + np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness, + ) + and modeling_torque_tube + or np.isclose(com[2], params.pv_array.panel_thickness) + and not modeling_torque_tube ): - key = f"top_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) - else: - this_panel_transformed_com[key] = [com] + target_key = f"top_{panel_ct:.0f}" + else: - key = f"trash_{panel_ct:.0f}" - if key in this_panel_transformed_com: - this_panel_transformed_com[key].append(com) + target_key = f"trash_{panel_ct:.0f}" + + if target_key is not None: + if target_key in this_panel_transformed_com: + this_panel_transformed_com[target_key].append(com) else: - this_panel_transformed_com[key] = [com] + this_panel_transformed_com[target_key] = [com] for key, val in this_panel_transformed_com.items(): for row_num, com in enumerate(val): From bb62814666b81785342c93cee1caf8996a230a25 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 12:42:19 -0600 Subject: [PATCH 06/37] lower elevation by half panel thickness to coincide with new meshing strategy (panel bottom surface = 0 (z)) --- pvade/tests/input/yaml/sim_params.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvade/tests/input/yaml/sim_params.yaml b/pvade/tests/input/yaml/sim_params.yaml index 7b6e4a6..2e63349 100644 --- a/pvade/tests/input/yaml/sim_params.yaml +++ b/pvade/tests/input/yaml/sim_params.yaml @@ -16,11 +16,11 @@ domain: pv_array: stream_rows: 7 span_rows: 1 - elevation: 1.5 + elevation: 1.45 # 1.5 - 0.5*0.1 = 1.4 stream_spacing: 7.0 panel_chord: 2.0 panel_span: 7.0 - panel_thickness: 0.1 + panel_thickness: 0.1 tracker_angle: -30.0 solver: dt: 0.005 From 90a7db61356b38174f311e03ee2cd2b555141cb8 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 13:36:45 -0600 Subject: [PATCH 07/37] change the box to span [0,2] in the z direction vs being 0 centered --- pvade/tests/input/yaml/embedded_box.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvade/tests/input/yaml/embedded_box.yaml b/pvade/tests/input/yaml/embedded_box.yaml index 0bd12d5..ad13c66 100644 --- a/pvade/tests/input/yaml/embedded_box.yaml +++ b/pvade/tests/input/yaml/embedded_box.yaml @@ -9,15 +9,15 @@ domain: x_max: 1.0 y_min: -1.0 y_max: 1.0 - z_min: -1.0 - z_max: 1.0 + z_min: 0.0 + z_max: 2.0 l_char: 0.05 pv_array: stream_rows: 1 span_rows: 1 stream_spacing: 1.0 span_spacing: 1.0 - elevation: 0.0 + elevation: 0.5 # 0.0 panel_chord: 1.2 panel_span: 1.1 panel_thickness: 1.0 From 251e117391c9030366aeadb7c34709ac24705dc3 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 13:37:58 -0600 Subject: [PATCH 08/37] update so that a panel's elevation is defined as the bottom of the panel to the ground --- pvade/tests/test_fsi_mesh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvade/tests/test_fsi_mesh.py b/pvade/tests/test_fsi_mesh.py index 2d4f475..eba906e 100644 --- a/pvade/tests/test_fsi_mesh.py +++ b/pvade/tests/test_fsi_mesh.py @@ -148,10 +148,10 @@ def test_meshing_3dpanels_rotations(wind_direction, num_stream_rows, num_span_ro # Create the 4 corners of this table corresponding to the *top* surface (+0.5*thickness) top_surface_corners = np.array( [ - [-0.5 * chord, -0.5 * span, 0.5 * thickness], - [0.5 * chord, -0.5 * span, 0.5 * thickness], - [0.5 * chord, 0.5 * span, 0.5 * thickness], - [-0.5 * chord, 0.5 * span, 0.5 * thickness], + [-0.5 * chord, -0.5 * span, thickness], + [0.5 * chord, -0.5 * span, thickness], + [0.5 * chord, 0.5 * span, thickness], + [-0.5 * chord, 0.5 * span, thickness], ] ) From 9d77229ed174560e523c5b71f7540762d387b93c Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 13:38:58 -0600 Subject: [PATCH 09/37] change dz to account for new panel position, change truth values to account for new positioning conventions --- pvade/tests/test_mesh_movement.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pvade/tests/test_mesh_movement.py b/pvade/tests/test_mesh_movement.py index 4de08c8..ec0798d 100644 --- a/pvade/tests/test_mesh_movement.py +++ b/pvade/tests/test_mesh_movement.py @@ -49,7 +49,7 @@ def test_calc_distance_to_panel_surface(): dx = params.domain.x_max - 0.5 * params.pv_array.panel_chord dy = params.domain.y_max - 0.5 * params.pv_array.panel_span - dz = params.domain.z_max - 0.5 * params.pv_array.panel_thickness + dz = params.pv_array.elevation truth_max_dist = np.sqrt(dx * dx + dy * dy + dz * dz) assert np.isclose(max_dist, truth_max_dist) @@ -113,9 +113,7 @@ def __init__(self, domain, x_shift, y_shift, z_shift): assert np.isclose( np.amin(structure_coords_before[:, 1]), -0.5 * params.pv_array.panel_span ) - assert np.isclose( - np.amin(structure_coords_before[:, 2]), -0.5 * params.pv_array.panel_thickness - ) + assert np.isclose(np.amin(structure_coords_before[:, 2]), params.pv_array.elevation) assert np.isclose( np.amax(structure_coords_before[:, 0]), 0.5 * params.pv_array.panel_chord @@ -124,7 +122,8 @@ def __init__(self, domain, x_shift, y_shift, z_shift): np.amax(structure_coords_before[:, 1]), 0.5 * params.pv_array.panel_span ) assert np.isclose( - np.amax(structure_coords_before[:, 2]), 0.5 * params.pv_array.panel_thickness + np.amax(structure_coords_before[:, 2]), + params.pv_array.elevation + params.pv_array.panel_thickness, ) # Move the mesh by the amount prescribed in u_delta From ca9df0043520112fac50c1b39f8bd9c16b0812f6 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 16:42:10 -0600 Subject: [PATCH 10/37] change the location of the tracked corner acceleration consistent with new meshing convention --- pvade/structure/StructureMain.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pvade/structure/StructureMain.py b/pvade/structure/StructureMain.py index 6e55265..67b5cba 100644 --- a/pvade/structure/StructureMain.py +++ b/pvade/structure/StructureMain.py @@ -110,9 +110,21 @@ def _north_east_corner(x): else: tracker_angle_rad = np.radians(params.pv_array.tracker_angle) - x1 = 0.5 * params.pv_array.panel_chord * np.cos(tracker_angle_rad) - x2 = 0.5 * params.pv_array.panel_thickness * np.sin(tracker_angle_rad) - corner = [x1 - x2, 0.5 * params.pv_array.panel_span] + """ + y + + ^ + | + |---o NE corner (measured on bottom of panel) + | | + | | + |-----> x + """ + + corner = [ + 0.5 * params.pv_array.panel_chord * np.cos(tracker_angle_rad), + 0.5 * params.pv_array.panel_span, + ] east_edge = np.logical_and(corner[0] - eps < x[0], x[0] < corner[0] + eps) north_edge = np.logical_and(corner[1] - eps < x[1], x[1] < corner[1] + eps) From b6a5fd3471209a4a093b0f1796573f156b9a64da Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 16:44:22 -0600 Subject: [PATCH 11/37] changing variable names from north_west to north_east --- pvade/structure/ElasticityAnalysis.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 4a84d98..331bd1a 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -485,25 +485,27 @@ def solve(self, params, dataIO, structure): try: idx = structure.north_east_corner_dofs[0] # idx = self.north_east_corner_dofs[0] - nw_corner_accel = self.u.x.array[ + north_east_corner_acccel = self.u.x.array[ structure.ndim * idx : structure.ndim * idx + structure.ndim ].astype(np.float64) - print(nw_corner_accel) + print(north_east_corner_acccel) except: - nw_corner_accel = np.zeros(structure.ndim, dtype=np.float64) + north_east_corner_acccel = np.zeros(structure.ndim, dtype=np.float64) - nw_corner_accel_global = np.zeros( + north_east_corner_accel_global = np.zeros( (self.num_procs, structure.ndim), dtype=np.float64 ) - self.comm.Gather(nw_corner_accel, nw_corner_accel_global, root=0) + self.comm.Gather( + north_east_corner_acccel, north_east_corner_accel_global, root=0 + ) - # print(f"Acceleration at North West corner = {nw_corner_accel}") + # print(f"Acceleration at North West corner = {north_east_corner_acccel}") if self.rank == 0: - norm2 = np.sum(nw_corner_accel_global**2, axis=1) + norm2 = np.sum(north_east_corner_accel_global**2, axis=1) max_norm2_idx = np.argmax(norm2) - np_accel = nw_corner_accel_global[max_norm2_idx, :] + np_accel = north_east_corner_accel_global[max_norm2_idx, :] accel_pos_filename = os.path.join( params.general.output_dir_sol, "accel_pos.csv" From 1929dc97be90bdcd6e41e276f6b7528fee13a377 Mon Sep 17 00:00:00 2001 From: Young Date: Tue, 9 Sep 2025 16:46:01 -0600 Subject: [PATCH 12/37] remove misleading comment --- pvade/structure/ElasticityAnalysis.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 331bd1a..92c43c0 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -500,8 +500,6 @@ def solve(self, params, dataIO, structure): north_east_corner_acccel, north_east_corner_accel_global, root=0 ) - # print(f"Acceleration at North West corner = {north_east_corner_acccel}") - if self.rank == 0: norm2 = np.sum(north_east_corner_accel_global**2, axis=1) max_norm2_idx = np.argmax(norm2) From 9b14d506369cd5423c10a50c879b286c1609ed47 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 10 Sep 2025 09:18:59 -0600 Subject: [PATCH 13/37] naive parsing to avoid issue of gha node name having underscore --- .github/workflows/test_pvade.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_pvade.yaml b/.github/workflows/test_pvade.yaml index 897d46c..477ec3d 100644 --- a/.github/workflows/test_pvade.yaml +++ b/.github/workflows/test_pvade.yaml @@ -33,6 +33,8 @@ jobs: shell: bash -l {0} run: PYTHONPATH=. pytest -sv pvade/tests/ - name: test all inputs + env: + OMPI_MCA_regx: "naive" shell: bash -l {0} run: pytest -sv test_all_inputs.py From d10aec8719d80acf4a5587f36db2e210c732c6e2 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 10 Sep 2025 09:52:35 -0600 Subject: [PATCH 14/37] save both acceleration and deformation in csv file, clean up unclear variable names --- pvade/structure/ElasticityAnalysis.py | 69 ++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 92c43c0..85ea43e 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -484,26 +484,52 @@ def solve(self, params, dataIO, structure): try: idx = structure.north_east_corner_dofs[0] - # idx = self.north_east_corner_dofs[0] - north_east_corner_acccel = self.u.x.array[ + north_east_corner_deformation = self.u.x.array[ structure.ndim * idx : structure.ndim * idx + structure.ndim ].astype(np.float64) - print(north_east_corner_acccel) + print(f"Deformation: {north_east_corner_deformation}") + + except: + north_east_corner_deformation = np.zeros(structure.ndim, dtype=np.float64) + + try: + idx = structure.north_east_corner_dofs[0] + north_east_corner_acceleration = self.a_old.x.array[ + structure.ndim * idx : structure.ndim * idx + structure.ndim + ].astype(np.float64) + print(f"Acceleration: {north_east_corner_acceleration}") + except: - north_east_corner_acccel = np.zeros(structure.ndim, dtype=np.float64) + north_east_corner_acceleration = np.zeros(structure.ndim, dtype=np.float64) - north_east_corner_accel_global = np.zeros( + # Initialize a buffer to collect everything into + north_east_corner_deformation_global = np.zeros( (self.num_procs, structure.ndim), dtype=np.float64 ) + north_east_corner_acceleration_global = np.zeros( + (self.num_procs, structure.ndim), dtype=np.float64 + ) + + # Gather all points (many of which are zeros) to rank 0 + self.comm.Gather( + north_east_corner_deformation, north_east_corner_deformation_global, root=0 + ) + self.comm.Gather( - north_east_corner_acccel, north_east_corner_accel_global, root=0 + north_east_corner_acceleration, + north_east_corner_acceleration_global, + root=0, ) if self.rank == 0: - norm2 = np.sum(north_east_corner_accel_global**2, axis=1) + norm2 = np.sum(north_east_corner_deformation_global**2, axis=1) + max_norm2_idx = np.argmax(norm2) + np_deformation = north_east_corner_deformation_global[max_norm2_idx, :] + + norm2 = np.sum(north_east_corner_acceleration_global**2, axis=1) max_norm2_idx = np.argmax(norm2) - np_accel = north_east_corner_accel_global[max_norm2_idx, :] + np_acceleration = north_east_corner_acceleration_global[max_norm2_idx, :] accel_pos_filename = os.path.join( params.general.output_dir_sol, "accel_pos.csv" @@ -512,18 +538,35 @@ def solve(self, params, dataIO, structure): if self.first_call_to_solver: with open(accel_pos_filename, "w") as fp: - fp.write("#x-pos,y-pos,z-pos\n") if structure.ndim == 3: - fp.write(f"{np_accel[0]},{np_accel[1]},{np_accel[2]}\n") + fp.write( + "#x-deformation,y-deformation,z-deformation,x-acceleration,y-acceleration,z-acceleration\n" + ) + fp.write( + f"{np_deformation[0]},{np_deformation[1]},{np_deformation[2]}," + ) + fp.write( + f"{np_acceleration[0]},{np_acceleration[1]},{np_acceleration[2]}\n" + ) elif structure.ndim == 2: - fp.write(f"{np_accel[0]},{np_accel[1]}\n") + fp.write( + "#x-deformation,y-deformation,x-acceleration,y-acceleration\n" + ) + fp.write(f"{np_deformation[0]},{np_deformation[1]},") + fp.write(f"{np_acceleration[0]},{np_acceleration[1]}\n") else: with open(accel_pos_filename, "a") as fp: if structure.ndim == 3: - fp.write(f"{np_accel[0]},{np_accel[1]},{np_accel[2]}\n") + fp.write( + f"{np_deformation[0]},{np_deformation[1]},{np_deformation[2]}," + ) + fp.write( + f"{np_acceleration[0]},{np_acceleration[1]},{np_acceleration[2]}\n" + ) elif structure.ndim == 2: - fp.write(f"{np_accel[0]},{np_accel[1]}\n") + fp.write(f"{np_deformation[0]},{np_deformation[1]},") + fp.write(f"{np_acceleration[0]},{np_acceleration[1]}\n") if self.first_call_to_solver: self.first_call_to_solver = False From 2a0a73ab4aff10ee8ae49f7e1244358ea61b4931 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 10 Sep 2025 09:52:56 -0600 Subject: [PATCH 15/37] change facets to vertices in variable name for clarity --- pvade/structure/StructureMain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvade/structure/StructureMain.py b/pvade/structure/StructureMain.py index 67b5cba..ed3a756 100644 --- a/pvade/structure/StructureMain.py +++ b/pvade/structure/StructureMain.py @@ -133,12 +133,12 @@ def _north_east_corner(x): return north_east_corner - north_east_corner_facets = dolfinx.mesh.locate_entities_boundary( + north_east_corner_vertices = dolfinx.mesh.locate_entities_boundary( domain.structure.msh, 0, _north_east_corner ) self.north_east_corner_dofs = dolfinx.fem.locate_dofs_topological( - self.elasticity.V, 0, north_east_corner_facets + self.elasticity.V, 0, north_east_corner_vertices ) def build_boundary_conditions(self, domain, params): From a2033edcb4706ecdafa9b42e39910b789a1081a2 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 10 Sep 2025 09:53:35 -0600 Subject: [PATCH 16/37] only compare the deformation outputs (first two cols) during the check to avoid dimension mismatch --- pvade/tests/test_solve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvade/tests/test_solve.py b/pvade/tests/test_solve.py index 8734207..77d5457 100644 --- a/pvade/tests/test_solve.py +++ b/pvade/tests/test_solve.py @@ -310,7 +310,7 @@ def test_fsi2(): # print('pos_data = ', pos_data) - assert np.allclose(pos_data, pos_data_truth) + assert np.allclose(pos_data[:, 0:2], pos_data_truth) print(lift_and_drag_data) # assert np.allclose(lift_and_drag_data[:, 0:3], lift_and_drag_data_truth[:, 0:3]) # needs new truth values to pass, mesh has changed From 2a18d4b366f5c57a8fe609c80a1854e01ee3d46f Mon Sep 17 00:00:00 2001 From: xinhe Date: Thu, 23 Oct 2025 13:25:53 -0600 Subject: [PATCH 17/37] update the case study input --- input/duramat_case_study.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/input/duramat_case_study.yaml b/input/duramat_case_study.yaml index fb5939e..d590670 100644 --- a/input/duramat_case_study.yaml +++ b/input/duramat_case_study.yaml @@ -23,6 +23,11 @@ pv_array: elevation: 2.1 tracker_angle: 0.0 span_fixation_pts: [13.2] + torque_tube_separation: 0.2 # gap between panel and tube center + torque_tube_radius: 0.1 # radius of the torque tube + modules_per_span: 10 + block_chord_div_by_panel_chord: 0.02 + solver: dt: 0.01 t_final: 20.0 From 5e9a2f98d72d2ffd41269082ee7bec1c354bcbe4 Mon Sep 17 00:00:00 2001 From: He Date: Fri, 7 Nov 2025 13:59:01 -0700 Subject: [PATCH 18/37] implement structure simplification --- input/duramat_case_study.yaml | 8 +- pvade/IO/input_schema.yaml | 28 +- pvade/fluid/FlowManager.py | 238 +++++++---- pvade/geometry/MeshManager.py | 2 +- pvade/geometry/panels3d/DomainCreation.py | 495 +++++++++++++++------- pvade/structure/ElasticityAnalysis.py | 180 +++++++- pvade/structure/StructureMain.py | 18 +- pvade/structure/boundary_conditions.py | 34 +- pvade_main.py | 8 +- 9 files changed, 749 insertions(+), 262 deletions(-) diff --git a/input/duramat_case_study.yaml b/input/duramat_case_study.yaml index d590670..2221b4e 100644 --- a/input/duramat_case_study.yaml +++ b/input/duramat_case_study.yaml @@ -24,7 +24,8 @@ pv_array: tracker_angle: 0.0 span_fixation_pts: [13.2] torque_tube_separation: 0.2 # gap between panel and tube center - torque_tube_radius: 0.1 # radius of the torque tube + torque_tube_outer_radius: 0.1 # radius of the torque tube + torque_tube_inner_radius: 0.09 # radius of the torque tube modules_per_span: 10 block_chord_div_by_panel_chord: 0.02 @@ -63,4 +64,7 @@ structure: bc_list: [] motor_connection: true tube_connection: true - beta_relaxation: 0.5 \ No newline at end of file + beta_relaxation: 0.5 + elasticity_modulus_tube: 2.0e+11 + poissons_ratio_tube: 0.3 + density_tube: 7800.0 \ No newline at end of file diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index c127d0c..9822740 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -204,7 +204,13 @@ properties: maximum: 1.0 type: "number" description: "The distance between the bottom of the panel structure and the centerline of the torque tube, in meters." - torque_tube_radius: + torque_tube_outer_radius: + default: 0.0 + minimum: 0.0 + maximum: 1.0 + type: "number" + description: "The radius of the torque tube, in meters." + torque_tube_inner_radius: default: 0.0 minimum: 0.0 maximum: 1.0 @@ -215,6 +221,12 @@ properties: minimum: 1 type: "integer" description: "The number of modules/panels per every panel_span distance. This can be thought of as the number of modules/panels per each table, counted in the spanwise direction only (i.e., 2-in-portait systems should report only the number in the spanwise direction, vs 2x that value)." + block_chord_div_by_panel_chord: + default: 0.1 + minimum: 0.01 + type: "number" + description: "block length or width divided by panel chord" + solver: additionalProperties: false type: "object" @@ -665,6 +677,20 @@ properties: type: "number" description: "poissons ratio of the panel structure." units: "None" + elasticity_modulus_tube: + default: 200.0e+9 + minimum: 1.0e+2 + maximum: 500.0e+9 + type: "number" + description: "The effective Young's modulus of the torque tube." + units: "Pascal" + poissons_ratio_tube: + default: 0.3 + minimum: 0.1 + maximum: 0.9 + type: "number" + description: "poissons ratio of the torque tube." + units: "None" body_force_x: default: 100 type: "number" diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index bd2c5c4..0f2a1c8 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -242,6 +242,54 @@ def build_forms(self, domain, params): self.p_k = dolfinx.fem.Function(self.Q, name="pressure") self.p_k1 = dolfinx.fem.Function(self.Q) + # define function to save x, y, z coordinates + self.spatial_X_coords = dolfinx.fem.Function(self.Q, name="X_coords") + self.spatial_Y_coords = dolfinx.fem.Function(self.Q, name="Y_coords") + self.spatial_Z_coords = dolfinx.fem.Function(self.Q, name="Z_coords") + + class FillFunctionWithXCoords: + def __init__(self): + pass + + def __call__(self, x): + coords = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType) + + + coords[0] = x[0] + + return coords + + self.spatial_X_coords.interpolate(FillFunctionWithXCoords()) + + class FillFunctionWithYCoords: + def __init__(self): + pass + + def __call__(self, x): + coords = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType) + + + coords[0] = x[1] + + return coords + + self.spatial_Y_coords.interpolate(FillFunctionWithYCoords()) + + class FillFunctionWithZCoords: + def __init__(self): + pass + + def __call__(self, x): + coords = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType) + + + coords[0] = x[2] + + + return coords + + self.spatial_Z_coords.interpolate(FillFunctionWithZCoords()) + # initial conditions if params.fluid.initialize_with_inflow_bc: # self.inflow_profile = dolfinx.fem.Function(self.V) @@ -534,94 +582,132 @@ def _all_interior_surfaces(x): ds_fluid = ufl.Measure( "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags ) + + def compute_panel_torques(self, domain, params): - self.integrated_force_x_form = [] - self.integrated_force_y_form = [] - self.integrated_force_z_form = [] + ds_fluid = ufl.Measure( + "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags + ) for panel_id in range( int(params.pv_array.stream_rows * params.pv_array.span_rows) ): - # for loc in ["top_0", "bottom_0", "left_0", "right_0"]: - # idx = domain.domain_markers[loc]["idx"] - # s = dolfinx.fem.assemble_scalar(dolfinx.fem.form(1.0*ds_fluid(idx))) - # print(f"loc = {loc}, idx = {idx}, s = {s}") - - self.integrated_force_x_form.append(0) - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"left_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"top_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"right_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"bottom_{panel_id:.0f}"]["idx"] - ) - if self.ndim == 3: - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"front_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"back_{panel_id:.0f}"]["idx"] - ) - - self.integrated_force_x_form[-1] = dolfinx.fem.form( - self.integrated_force_x_form[-1] - ) - - self.integrated_force_y_form.append(0) - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"left_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"top_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"right_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"bottom_{panel_id:.0f}"]["idx"] - ) - if self.ndim == 3: - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"front_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"back_{panel_id:.0f}"]["idx"] - ) - - self.integrated_force_y_form[-1] = dolfinx.fem.form( - self.integrated_force_y_form[-1] - ) + for module_id in range(params.pv_array.modules_per_span): + + total_torque = 0 - self.integrated_force_z_form.append(0) - if self.ndim == 3: - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"left_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"top_{panel_id:.0f}"]["idx"] + total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"right_{panel_id:.0f}"]["idx"] + total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( + domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"bottom_{panel_id:.0f}"]["idx"] + total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( + domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"front_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"back_{panel_id:.0f}"]["idx"] + total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) ) + attr_name = f"total_torque_panel_{panel_id:.0f}_{module_id:.0f}" + setattr(self, attr_name, total_torque) - self.integrated_force_z_form[-1] = dolfinx.fem.form( - self.integrated_force_z_form[-1] - ) + def compute_double_integral_panel_torques(self, domain, params): + torque_function_on_panels = dolfinx.fem.Function(self.Q) + + torque_function_on_panels.x.array[:] = self.spatial_X_coords.x.array[:]*self.traction[2].x.array[:]-self.spatial_Z_coords.x.array[:]*self.traction[0].x.array[:] + ds_fluid = ufl.Measure( + "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags + ) + for panel_id in range( + int(params.pv_array.stream_rows * params.pv_array.span_rows) + ): + for module_id in range(params.pv_array.modules_per_span): + + torque_double_integral = 0 + + whole_top_surface_facet_index = domain.fluid.facet_tags.find(domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"]) + whole_bot_surface_facet_index = domain.fluid.facet_tags.find(domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"]) + whole_top_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(self.fluid.mesh, 2, whole_top_surface_facet_index) + whole_bot_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(self.fluid.mesh, 2, whole_bot_surface_facet_index) + + dx_top_whole = ufl.Measure("dx", domain=whole_top_surf_submesh) + dx_bot_whole = ufl.Measure("dx", domain=whole_bot_surf_submesh) + + whole_top_submesh_function_space = dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)) + whole_top_submesh_func = dolfinx.fem.Function(whole_top_submesh_function_space) + whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)) + whole_bot_submesh_func = dolfinx.fem.Function(whole_bot_submesh_function_space) + + coords_top = whole_top_submesh_function_space.tabulate_dof_coordinates() + coords_bot = whole_bot_submesh_function_space.tabulate_dof_coordinates() + + top_integrated_func_along_x = np.zeros_like(whole_top_submesh_func.x.array[:]) + + bot_integrated_func_along_x = np.zeros_like(whole_bot_submesh_func.x.array[:]) + + # Sort by x first, then y and z (group by x-levels) + sort_idx_top = np.lexsort((coords_top[:, 1], coords_top[:, 0])) # x is sorted, from small to large + sort_idx_bot = np.lexsort((coords_bot[:, 1], coords_bot[:, 0])) # x is sorted, from small to large + + top_coords_sorted = coords_top[sort_idx_top] #sorted from smallest x to largest x + bot_coords_sorted = coords_bot[sort_idx_bot] #sorted from smallest x to largest x + + + ### integral of top surface: + # Unique x-values (up to numerical tolerance) + xs_top = np.unique(np.round(top_coords_sorted[:, 0], decimals=6)) + + for x_value in xs_top: + # Mask for points at this x-level + x_mask_top = np.isclose(top_coords_sorted[:, 0], x_value, atol=1e-6) + + facet_centers_top = dolfinx.mesh.compute_midpoints(whole_top_surf_submesh, 2, np.array(np.arange(whole_top_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) + # Identify cells where the midpoint x-coordinate is less than 1 + marked_facet_top = np.where(facet_centers_top[:, 0] <= x_value)[0] + + # Create a submesh from those cells + submesh_top_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_top_surf_submesh, 2, marked_facet_top.astype(np.int32)) + top_sub_function_space = dolfinx.fem.FunctionSpace(submesh_top_surface_part, ('CG', 1)) + top_sub_func = dolfinx.fem.Function(top_sub_function_space) + top_sub_func.interpolate(torque_function_on_panels) + + dx_top = ufl.Measure("dx", domain=submesh_top_surface_part) + integrated_top = dolfinx.fem.assemble_scalar(dolfinx.fem.form(top_sub_func * dx_top)) + top_integrated_func_along_x[sort_idx_top[x_mask_top]] = integrated_top + + ### integral of bot surface: + # Unique x-values (up to numerical tolerance) + xs_bot = np.unique(np.round(bot_coords_sorted[:, 0], decimals=6)) + + for x_value in xs_bot: + + # Mask for points at this x-level + x_mask_bot = np.isclose(bot_coords_sorted[:, 0], x_value, atol=1e-6) + facet_centers_bot = dolfinx.mesh.compute_midpoints(whole_bot_surf_submesh, 2, np.array(np.arange(whole_bot_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) + # Identify cells where the midpoint x-coordinate is less than 1 + marked_facet_bot = np.where(facet_centers_bot[:, 0] <= x_value)[0] + # Create a submesh from those cells + submesh_bot_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_bot_surf_submesh, 2, marked_facet_bot.astype(np.int32)) + bot_sub_function_space = dolfinx.fem.FunctionSpace(submesh_bot_surface_part, ('CG', 1)) + bot_sub_func = dolfinx.fem.Function(bot_sub_function_space) + bot_sub_func.interpolate(torque_function_on_panels) + + dx_bot = ufl.Measure("dx", domain=submesh_bot_surface_part) + integrated_bot = dolfinx.fem.assemble_scalar(dolfinx.fem.form(bot_sub_func * dx_bot)) + bot_integrated_func_along_x[sort_idx_bot[x_mask_bot]] = integrated_bot + + single_integral_function_top = dolfinx.fem.Function(whole_top_submesh_function_space, name='single_integral_function_top') + single_integral_function_top.x.array[:] = top_integrated_func_along_x + + single_integral_function_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name='single_integral_function_bot') + single_integral_function_bot.x.array[:] = bot_integrated_func_along_x + + torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_top * dx_top_whole))/self.pvarry.panel_span*params.pv_array.modules_per_span + torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_bot * dx_bot_whole))/self.pvarry.panel_span*params.pv_array.modules_per_span + attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{module_id:.0f}" + setattr(self, attr_name, torque_double_integral) + def _assemble_system(self, params): """Pre-assemble all LHS matrices and RHS vectors diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index 13f1764..ebd8acd 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -309,7 +309,7 @@ def _transfer_mesh_tags_to_submeshes(self, params): num_facets = f_map.size_local + f_map.num_ghosts all_values = np.zeros(num_facets, dtype=np.int32) - # Assign non-zero facet tags using the facet tag indices + # Assign non-zero facet tags using the facet tag indices, save the facet marker to all_values all_values[self.facet_tags.indices] = self.facet_tags.values cell_to_facet = self.msh.topology.connectivity(self.ndim, facet_dim) diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 31f4d42..8ad4a7e 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -149,14 +149,20 @@ def Rz(theta): domain_tag_list = [] domain_tag_list.append(domain_tag) + # panel_tag_list includes all structure, panel+connector panel_tag_list = [] panel_ct = 0 + + # only include panels + structure_panel_only_list = [] + # only include connectors + structure_connector_only_list = [] module_distances = np.linspace( -half_span, half_span, params.pv_array.modules_per_span + 1 ) - transformed_com = {} + transformed_com = {} # all panels if ( params.pv_array.torque_tube_separation > 0.0 @@ -166,6 +172,8 @@ def Rz(theta): else: modeling_torque_tube = False + + # start to add panel for panel_id_y, yy in enumerate(y_centers): for panel_id_x, xx in enumerate(x_centers): if isinstance(params.pv_array.tracker_angle, list): @@ -181,8 +189,11 @@ def Rz(theta): else: tracker_angle_rad = np.radians(params.pv_array.tracker_angle) - this_panel_tag_list = [] - this_panel_transformed_com = {} + this_panel_tag_list = [] # for each row + this_panel_transformed_com = {} # for each row + + numpy_pt_list = [] # for each row + embedded_lines_tag_list = [] # for each row for module_id in range(params.pv_array.modules_per_span): @@ -200,22 +211,33 @@ def Rz(theta): module_span, params.pv_array.panel_thickness, ) + + panel_tag_list.append((self.ndim, this_module)) # all panels + this_panel_tag_list.append((self.ndim, this_module)) # this panel array/row + structure_panel_only_list.append((self.ndim, this_module)) + this_standoff = self.gmsh_model.occ.addBox( - -params.pv_array.torque_tube_radius, + -params.pv_array.block_chord_div_by_panel_chord * half_chord, module_distances[module_id], 0.0, - 2.0 * params.pv_array.torque_tube_radius, - module_span, + 2.0 * params.pv_array.block_chord_div_by_panel_chord * half_chord, + params.pv_array.block_chord_div_by_panel_chord * half_chord, params.pv_array.torque_tube_separation, ) - - panel_tag_list.append((self.ndim, this_module)) panel_tag_list.append((self.ndim, this_standoff)) - - this_panel_tag_list.append((self.ndim, this_module)) this_panel_tag_list.append((self.ndim, this_standoff)) + structure_connector_only_list.append((self.ndim, this_standoff)) + + # Add a bisecting line to the bottom of the connector in the spanwise direction + pt_1 = self.gmsh_model.occ.addPoint(0, module_distances[module_id], 0.0) + pt_2 = self.gmsh_model.occ.addPoint(0, module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0) + numpy_pt_list.append([0, module_distances[module_id], 0.0, 0, module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0]) # for this row + torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + torque_tube_tag = (1, torque_tube_id) + embedded_lines_tag_list.append(torque_tube_tag) # for this panel row + else: this_module = self.gmsh_model.occ.addBox( -half_chord, @@ -225,118 +247,134 @@ def Rz(theta): module_span, params.pv_array.panel_thickness, ) - panel_tag_list.append((self.ndim, this_module)) - this_panel_tag_list.append((self.ndim, this_module)) - numpy_pt_list = [] - embedded_lines_tag_list = [] - - # Add a bisecting line to the bottom of the panel in the spanwise direction - pt_1 = self.gmsh_model.occ.addPoint(0, -half_span, 0.0) - pt_2 = self.gmsh_model.occ.addPoint(0, half_span, 0.0) - - numpy_pt_list.append([0, -half_span, 0.0, 0, half_span, 0.0]) - - torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) - torque_tube_tag = (1, torque_tube_id) - embedded_lines_tag_list.append(torque_tube_tag) - - # Add lines in the streamwise direction to mimic sections of panel held rigid by motor - if params.pv_array.span_fixation_pts is not None: - if not isinstance(params.pv_array.span_fixation_pts, list): - num_fixation_pts = int( - np.floor( - params.pv_array.panel_span - / params.pv_array.span_fixation_pts - ) - ) - - fixation_pts_list = [] - - for k in range(1, num_fixation_pts + 1): - next_pt = k * params.pv_array.span_fixation_pts - - eps = 1e-9 - - if ( - next_pt > eps - and next_pt < params.pv_array.panel_span - eps - ): - fixation_pts_list.append(next_pt) - - else: - fixation_pts_list = params.pv_array.span_fixation_pts - - for fp in fixation_pts_list: - # FIXME: don't add the fixation points into the numpy tagging for now - if modeling_torque_tube: - numpy_pt_list.append( - [ - -params.pv_array.torque_tube_radius, - -half_span + fp, - 0.0, - params.pv_array.torque_tube_radius, - -half_span + fp, - 0.0, - ] - ) - - else: - numpy_pt_list.append( - [ - -half_chord, - -half_span + fp, - 0.0, - half_chord, - -half_span + fp, - 0.0, - ] - ) - - # If the separation line already exists at a module division, - # there's no need to redraw it, but otherwise, add the gmsh points - # to the model for embedding - if not np.any(np.isclose(-half_span + fp, module_distances)): - print( - f"Embedding a line for a no-deformation boundary condition." - ) - - if modeling_torque_tube: - pt_1 = self.gmsh_model.occ.addPoint( - -params.pv_array.torque_tube_radius, - -half_span + fp, - 0.0, - ) - pt_2 = self.gmsh_model.occ.addPoint( - params.pv_array.torque_tube_radius, - -half_span + fp, + structure_panel_only_list.append((self.ndim, this_module)) + + pt_1 = self.gmsh_model.occ.addPoint(0, module_distances[module_id], 0.0) + pt_2 = self.gmsh_model.occ.addPoint(0, module_distances[module_id+1], 0.0) + numpy_pt_list.append([0, module_distances[module_id], 0.0, 0, module_distances[module_id+1], 0.0]) + torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + torque_tube_tag = (1, torque_tube_id) + embedded_lines_tag_list.append(torque_tube_tag) # for this panel row + + if modeling_torque_tube: # add the last connector + last_standoff = self.gmsh_model.occ.addBox( + -params.pv_array.block_chord_div_by_panel_chord * half_chord, + module_distances[module_id+1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0, + 2.0 * params.pv_array.block_chord_div_by_panel_chord * half_chord, + params.pv_array.block_chord_div_by_panel_chord * half_chord, + params.pv_array.torque_tube_separation, ) - - else: - pt_1 = self.gmsh_model.occ.addPoint( - -half_chord, -half_span + fp, 0.0 - ) - pt_2 = self.gmsh_model.occ.addPoint( - half_chord, -half_span + fp, 0.0 - ) - - fixation_line_id = self.gmsh_model.occ.addLine(pt_1, pt_2) - fixation_line_tag = (1, fixation_line_id) - embedded_lines_tag_list.append(fixation_line_tag) - else: - print( - f"Applying no-deformation boundary condition at {fp}." - ) + this_panel_tag_list.append((self.ndim, last_standoff)) + panel_tag_list.append((self.ndim, last_standoff)) + + structure_connector_only_list.append((self.ndim, last_standoff)) + + pt_1 = self.gmsh_model.occ.addPoint(0, module_distances[module_id+1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0) + pt_2 = self.gmsh_model.occ.addPoint(0, module_distances[module_id+1], 0.0) + numpy_pt_list.append([0, module_distances[module_id+1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0, 0, module_distances[module_id+1], 0.0]) + torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + torque_tube_tag = (1, torque_tube_id) + embedded_lines_tag_list.append(torque_tube_tag) + + """ this is not used as we are using block surface to simulate the fixed bc applied by motor """ + # # Add lines in the streamwise direction to mimic sections of panel held rigid by motor + # if params.pv_array.span_fixation_pts is not None: + # if not isinstance(params.pv_array.span_fixation_pts, list): + # num_fixation_pts = int( + # np.floor( + # params.pv_array.panel_span + # / params.pv_array.span_fixation_pts + # ) + # ) + + # fixation_pts_list = [] + + # for k in range(1, num_fixation_pts + 1): + # next_pt = k * params.pv_array.span_fixation_pts + + # eps = 1e-9 + + # if ( + # next_pt > eps + # and next_pt < params.pv_array.panel_span - eps + # ): + # fixation_pts_list.append(next_pt) + + # else: + # fixation_pts_list = params.pv_array.span_fixation_pts + + # for fp in fixation_pts_list: + # # FIXME: don't add the fixation points into the numpy tagging for now + # if modeling_torque_tube: + # numpy_pt_list.append( + # [ + # -params.pv_array.torque_tube_radius, + # -half_span + fp, + # 0.0, + # params.pv_array.torque_tube_radius, + # -half_span + fp, + # 0.0, + # ] + # ) + + # else: + # numpy_pt_list.append( + # [ + # -half_chord, + # -half_span + fp, + # 0.0, + # half_chord, + # -half_span + fp, + # 0.0, + # ] + # ) + + # # If the separation line already exists at a module division, + # # there's no need to redraw it, but otherwise, add the gmsh points + # # to the model for embedding + # if not np.any(np.isclose(-half_span + fp, module_distances)): + # print( + # f"Embedding a line for a no-deformation boundary condition." + # ) + + # if modeling_torque_tube: + # pt_1 = self.gmsh_model.occ.addPoint( + # -params.pv_array.torque_tube_radius, + # -half_span + fp, + # 0.0, + # ) + # pt_2 = self.gmsh_model.occ.addPoint( + # params.pv_array.torque_tube_radius, + # -half_span + fp, + # 0.0, + # ) + + # else: + # pt_1 = self.gmsh_model.occ.addPoint( + # -half_chord, -half_span + fp, 0.0 + # ) + # pt_2 = self.gmsh_model.occ.addPoint( + # half_chord, -half_span + fp, 0.0 + # ) + + # fixation_line_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + # fixation_line_tag = (1, fixation_line_id) + # embedded_lines_tag_list.append(fixation_line_tag) + # else: + # print( + # f"Applying no-deformation boundary condition at {fp}." + # ) # Fragment the lines into the surfaces (equivalent to embedding these lines) self.gmsh_model.occ.fragment( this_panel_tag_list, embedded_lines_tag_list ) - for panel_tag in this_panel_tag_list: + for panel_tag in this_panel_tag_list: # 3d cell domain self.gmsh_model.occ.synchronize() # Get the list of 2D surfaces (surfaces) that make up this panel @@ -351,61 +389,189 @@ def Rz(theta): target_key = None + surface_located_or_not = False + # sturctures tagging if ( - np.isclose(com[0], -half_chord) - or np.isclose(com[0], -params.pv_array.torque_tube_radius) - and modeling_torque_tube + np.isclose(com[0], -half_chord) ): - target_key = f"left_{panel_ct:.0f}" + target_key = f"panel_left_{panel_ct:.0f}" + surface_located_or_not = True - elif ( + if ( np.isclose(com[0], half_chord) - or np.isclose(com[0], params.pv_array.torque_tube_radius) - and modeling_torque_tube ): - target_key = f"right_{panel_ct:.0f}" - - elif np.isclose(com[1], -half_span): - target_key = f"front_{panel_ct:.0f}" + target_key = f"panel_right_{panel_ct:.0f}" + surface_located_or_not = True - elif np.isclose(com[1], half_span): - target_key = f"back_{panel_ct:.0f}" + if ( + np.isclose(com[1], module_distances[0]) + and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) + ): + target_key = f"panel_front_{panel_ct:.0f}" + surface_located_or_not = True - elif ( - np.isclose(com[2], params.pv_array.torque_tube_separation) - and not np.isclose(com[0], 0.0) - and modeling_torque_tube - or np.isclose(com[2], 0.0) - and not modeling_torque_tube + if ( + np.isclose(com[1], module_distances[params.pv_array.modules_per_span]) + and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) ): - target_key = f"bottom_{panel_ct:.0f}" + target_key = f"panel_back_{panel_ct:.0f}" + surface_located_or_not = True - elif np.isclose(com[2], 0.0) and modeling_torque_tube: - target_key = f"torque_tube_{panel_ct:.0f}" + for module_id in range(params.pv_array.modules_per_span): + if ( + com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord + and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord + and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation) + ): + target_key = f"panel_bottom_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break - elif ( - np.isclose( - com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness, - ) - and modeling_torque_tube - or np.isclose(com[2], params.pv_array.panel_thickness) - and not modeling_torque_tube + if ( + com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord + and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord + and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness) + ): + target_key = f"panel_top_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break + # if ( + # np.isclose( + # com[2], + # params.pv_array.torque_tube_separation + # + params.pv_array.panel_thickness + # ) + + # ): + # target_key = f"panel_top_{panel_ct:.0f}" + # surface_located_or_not = True + + # mark the last block in each row + if ( + np.isclose(com[0], params.pv_array.block_chord_div_by_panel_chord * half_chord) + and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) + ): + target_key = f"block_right_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose(com[0], -params.pv_array.block_chord_div_by_panel_chord * half_chord) + and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) ): - target_key = f"top_{panel_ct:.0f}" + target_key = f"block_left_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True - else: - target_key = f"trash_{panel_ct:.0f}" + if ( + np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord) + ): + target_key = f"block_front_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span]) + ): + target_key = f"block_back_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose(com[2], 0.0) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) + ): + target_key = f"block_bottom_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + if ( + np.isclose(com[2], params.pv_array.torque_tube_separation) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + + if not surface_located_or_not: + for module_id in range(params.pv_array.modules_per_span): + + # sturctures tagging + + if ( + np.isclose(com[1], module_distances[module_id]) + and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[1], module_distances[module_id+1]) + and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + # if ( + # np.isclose(com[2], params.pv_array.torque_tube_separation) + # and np.isclose(com[0], 0.0) and com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 + # and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 + # ): + # target_key = f"panel_bottom_{panel_ct:.0f}" + # surface_located_or_not = True + # break + + if ( + (np.isclose(com[2], params.pv_array.torque_tube_separation) + and np.isclose(com[0], 0.0) and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) + and modeling_torque_tube) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[0], params.pv_array.block_chord_div_by_panel_chord * half_chord) + and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) + ): + target_key = f"block_right_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[0], -params.pv_array.block_chord_div_by_panel_chord * half_chord) + and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) + ): + target_key = f"block_left_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[module_id]) + ): + target_key = f"block_front_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord) + ): + target_key = f"block_back_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[2], 0.0) and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) + ): + target_key = f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break + + if not surface_located_or_not: + target_key = f"trash_{panel_ct:.0f}" + if target_key is not None: if target_key in this_panel_transformed_com: this_panel_transformed_com[target_key].append(com) else: this_panel_transformed_com[target_key] = [com] - for key, val in this_panel_transformed_com.items(): + for key, val in this_panel_transformed_com.items(): # all facets for each panel row. for row_num, com in enumerate(val): com_array = np.array(com) @@ -426,8 +592,9 @@ def Rz(theta): if key in transformed_com: transformed_com[key].append(com_array) else: - transformed_com[key] = [com_array] + transformed_com[key] = [com_array] # including all rows + # actually this is panel array count panel_ct += 1 # Rotate the panel by its tracking angle along the y-axis @@ -505,6 +672,10 @@ def Rz(theta): self.numpy_pt_total_array, (-1, int(2 * self.ndim)) ) + # print(transformed_com) + # exit() + + # for all panels # Loop over all the finalized surfaces after fragmentation and tag everything all_surf_tag_list = self.gmsh_model.occ.getEntities(self.ndim - 1) @@ -590,6 +761,9 @@ def Rz(theta): # Fluid Cell fluid_vol_list.append(vol_id) + structure_panel_only_list = [] + structure_connector_only_list = [] + self._add_to_domain_markers("structure", structure_vol_list, "cell") self._add_to_domain_markers("fluid", fluid_vol_list, "cell") @@ -1263,22 +1437,35 @@ def set_length_scales(self, params, domain_markers): internal_surface_tags = [] for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): + internal_surface_tags.extend( - domain_markers[f"bottom_{panel_id}"]["gmsh_tags"] + domain_markers[f"panel_left_{panel_id}"]["gmsh_tags"] ) - internal_surface_tags.extend(domain_markers[f"top_{panel_id}"]["gmsh_tags"]) internal_surface_tags.extend( - domain_markers[f"left_{panel_id}"]["gmsh_tags"] + domain_markers[f"panel_right_{panel_id}"]["gmsh_tags"] ) internal_surface_tags.extend( - domain_markers[f"right_{panel_id}"]["gmsh_tags"] + domain_markers[f"panel_front_{panel_id}"]["gmsh_tags"] ) internal_surface_tags.extend( - domain_markers[f"front_{panel_id}"]["gmsh_tags"] + domain_markers[f"panel_back_{panel_id}"]["gmsh_tags"] ) - internal_surface_tags.extend( - domain_markers[f"back_{panel_id}"]["gmsh_tags"] + + for module_id in range(params.pv_array.modules_per_span): + internal_surface_tags.extend( + domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["gmsh_tags"] + ) + internal_surface_tags.extend( + domain_markers[ + f"panel_top_{panel_id:.0f}_{module_id:.0f}" + ]["gmsh_tags"] ) + + # print(internal_surface_tags) + # print(len(internal_surface_tags)) + # exit() min_dist = [] diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 85ea43e..862dbeb 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -87,6 +87,143 @@ def build_boundary_conditions(self, domain, params): self.bc = build_structure_boundary_conditions(domain, params, self.V) + def calculate_K_for_Robin_BC(self, domain, flow, params): + + # shear modulus of tube + Gt = params.structure.elasticity_modulus_tube / (2 * (1 + params.structure.poissons_ratio_tube)) + + Ipt = np.pi/2*(params.pv_array.torque_tube_outer_radius**4 - params.pv_array.torque_tube_inner_radius**4) + + Gp = params.structure.elasticity_modulus / (2 * (1 + params.structure.poissons_ratio)) + + Ipp = 1/3.0*params.pv_array.panel_thickness * params.pv_array.panel_chord**3 + + # total number of pv rows + total_num_panels = params.pv_array.stream_rows * params.pv_array.span_rows + + num_panel_right_fixed = params.pv_array.modules_per_span // 2 + num_panel_left_fixed = params.pv_array.modules_per_span - num_panel_right_fixed + + # for right fixed part: + + for panel_id in range(total_num_panels): + total_torque_right_fixed = 0 + total_torque_left_fixed = 0 + + for i in range(num_panel_left_fixed, params.pv_array.modules_per_span): + name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" + total_torque_right_fixed += getattr(flow, name) + + for i in range(num_panel_left_fixed): + name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" + total_torque_left_fixed += getattr(flow, name) + + theo_matrix_right_fixed = np.zeros((num_panel_right_fixed+1, num_panel_right_fixed+1)) + theo_vector_right_fixed = np.zeros(num_panel_right_fixed+1) + + theo_matrix_right_fixed[0, :] = 1.0 + theo_vector_right_fixed[0] = total_torque_right_fixed + theo_matrix_right_fixed[1:, :-1] = params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt + + # as the double integral of torque along the span from front to back, left fixed part to right fixed part, it need to be flipped + T_double_integral_right_fixed = [] + T_right_fixed = [] + for i in range(params.pv_array.modules_per_span): + name = f"double_integral_total_torque_panel_{panel_id:.0f}_{params.pv_array.modules_per_span-1-i:.0f}" + T_double_integral_right_fixed.append(getattr(flow, name)) + name = f"total_torque_panel_{panel_id:.0f}_{params.pv_array.modules_per_span-1-i:.0f}" + T_right_fixed.append(getattr(flow, name)) + for i in range(num_panel_right_fixed): + for j in range(i+1): + theo_vector_right_fixed[i+1] += T_double_integral_right_fixed[-1-j]/(Gp*Ipp) + theo_vector_right_fixed[i+1] -= (i+1-j)*T_right_fixed[-1-j]/(Gp*Ipp)*params.pv_array.panel_span/params.pv_array.modules_per_span + for j in range(i): + theo_matrix_right_fixed[i+1, :-1-j-1] += params.pv_array.panel_span/params.pv_array.modules_per_span/(Gt*Ipt) + + + for i in range(num_panel_right_fixed): + for j in range(i+1): + theo_matrix_right_fixed[i+1, num_panel_right_fixed-j:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gp/Ipp + + R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_right_fixed), theo_vector_right_fixed) + + tube_rotate_matrix = np.ones((num_panel_right_fixed, num_panel_right_fixed))*params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) + + for i in range(num_panel_right_fixed): + for j in range(i): + tube_rotate_matrix[i, :num_panel_right_fixed-1-j] += params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) + + # check the standalone code, why it need to be flipped. + phi = np.flip(np.dot(tube_rotate_matrix, R_reaction_torque[:-1])) + + # rotation of connector + x_panel = np.arange(num_panel_right_fixed)*(params.pv_array.panel_span/params.pv_array.modules_per_span) # location of panels + + block_length = params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord + block_width = params.pv_array.block_chord_div_by_panel_chord*params.pv_array.panel_span/params.pv_array.modules_per_span/2 + + # To Do: check the angle, is this correct? + array_rotation = (params.fluid.wind_direction + 90.0) % 360.0 + array_rotation_rad = np.radians(array_rotation) + + for i in range(num_panel_right_fixed): + + phi_i = phi[i] + + K_i = 12*(R_reaction_torque[i])/((block_length)**3)/np.cos(array_rotation_rad+phi_i)/(np.sin(array_rotation_rad+phi_i)-np.sin(array_rotation_rad))/block_width + + name_K = f"spring_stiffness_{panel_id:.0f}_{params.pv_array.modules_per_span+1-i:.0f}" + + setattr(self, name_K, K_i) + + # for left fixed part: + theo_matrix_left_fixed = np.zeros((num_panel_left_fixed+1, num_panel_left_fixed+1)) + theo_vector_left_fixed = np.zeros(num_panel_left_fixed+1) + theo_matrix_left_fixed[0, :] = 1.0 + theo_vector_left_fixed[0] = total_torque_left_fixed + + T_double_integral_left_fixed = [] + T_left_fixed = [] + + for i in range(num_panel_left_fixed): + name = f"double_integral_total_torque_panel_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" + T_double_integral_left_fixed.append(getattr(flow, name)) + name = f"total_torque_panel_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" + T_left_fixed.append(getattr(flow, name)) + + for i in range(num_panel_left_fixed): + for j in range(i+1): + theo_matrix_left_fixed[i+1, j] = (i+1-j)*params.pv_array.panel_span/params.pv_array.modules_per_span/(Gp*Ipp) + theo_vector_left_fixed[i+1] += T_double_integral_left_fixed[j]/(Gp*Ipp) + for j in range(i): + theo_vector_left_fixed[i+1] += (i-j)*T_left_fixed[j]/(Gp*Ipp)*params.pv_array.panel_span/params.pv_array.modules_per_span + + for i in range(num_panel_left_fixed): + theo_matrix_left_fixed[i+1:, i+1:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt + + R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_left_fixed), theo_vector_left_fixed) # this is the torque applied to tube + + + tube_rotate_matrix = np.zeros((num_panel_left_fixed, num_panel_left_fixed)) + for i in range(num_panel_left_fixed): + tube_rotate_matrix[i:, i:] += params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt + + phi = np.dot(tube_rotate_matrix, R_reaction_torque[1:]) + + # rotation of connector + x_panel = np.arange(num_panel_left_fixed)*(params.pv_array.panel_span/params.pv_array.modules_per_span)+params.pv_array.panel_span/params.pv_array.modules_per_span # location of panles + + + for i in range(num_panel_left_fixed): + + phi_i = phi[i] + + K_i = 12*(R_reaction_torque[i+1])/((block_length)**3)/np.cos(array_rotation_rad+phi_i)/(np.sin(array_rotation_rad+phi_i)-np.sin(array_rotation_rad))/block_width + + name_K = f"spring_stiffness_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" + + setattr(self, name_K, K_i) + def update_a(self, u, u_old, v_old, a_old, dt, beta, ufl=True): # Update formula for acceleration # a = 1/(2*beta)*((u - u0 - v0*dt)/(0.5*dt*dt) - (1-2*beta)*a0) @@ -126,7 +263,7 @@ def update_fields(self, u, u_old, v_old, a_old, dt, beta, gamma): def avg(self, x_old, x_new, alpha): return alpha * x_old + (1 - alpha) * x_new - def build_forms(self, domain, params, structure): + def build_forms(self, domain, params, structure, flow): """Builds all variational statements This method creates all the functions, expressions, and variational @@ -212,6 +349,9 @@ def c(u, u_): def k_nominal(u, u_): return ufl.inner(P_(u), ufl.grad(u_)) + + def k_nominal_connector(u, u_): + return ufl.inner(P_connector(u), ufl.grad(u_)) # The deformation gradient, F = I + dy/dX def F_(u): @@ -241,6 +381,17 @@ def S_(u): S_svk = structure.lame_lambda * ufl.tr(E) * I + 2.0 * structure.lame_mu * E return S_svk + + # The second Piola–Kirchhoff stress, S + def S_connector(u): + E = E_(u) + I = ufl.Identity(len(u)) + + # return lamda * ufl.tr(E) * I + 2.0 * mu * (E - ufl.tr(E) * I / 3.0) + # TODO: Why does the above form give a better result and where does it come from? + + S_svk = structure.lame_lambda_connector * ufl.tr(E) * I + 2.0 * structure.lame_mu_connector * E + return S_svk # The first Piola–Kirchhoff stress tensor, P = F*S def P_(u): @@ -248,6 +399,12 @@ def P_(u): S = S_(u) # return ufl.inv(F) * S return F * S + + def P_connector(u): + F = F_(u) + S = S_connector(u) + # return ufl.inv(F) * S + return F * S # self.uh_exp = dolfinx.fem.Function(self.V, name="Deformation") @@ -302,15 +459,32 @@ def P_(u): F = ufl.grad(self.u) + ufl.Identity(len(self.u)) J = ufl.det(F) + + self.z_unit_vector = dolfinx.fem.Constant(domain.structure.msh, [0.0,0.0,1.0]) # surface traction, N/m^2 + + self.calculate_K_for_Robin_BC(domain, flow, params) + + # To Do: how to differentiate the connector part and the panel part, dx_connector and dx_panel self.res = ( m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * ufl.dx + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * ufl.dx - + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * ufl.dx - - structure.rho * ufl.inner(self.f, self.u_) * ufl.dx + + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * ufl.dx_panel + + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * ufl.dx_connector + - structure.rho * ufl.inner(self.f, self.u_) * ufl.dx_panel + - structure.rho_connector * ufl.inner(self.f, self.u_) * ufl.dx_connector - ufl.dot(ufl.dot(self.stress_predicted * J * ufl.inv(F.T), n), self.u_) * self.ds ) # - Wext(self.u) + # Robin boundary condition terms + for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): + for i in range(params.pv_array.modules_per_span): + name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" + K_springs = dolfinx.fem.Constant(domain.structure.msh, float(getattr(self, name_K))) + self.res += ufl.dot(K_springs * self.u_, self.z_unit_vector)*ds_bottom.panel_connector_markers[panel_id][i] + for i in range(params.pv_array.modules_per_span): + self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector)*ds_bottom.... + # self.a = dolfinx.fem.form(ufl.lhs(res)) # self.L = dolfinx.fem.form(ufl.rhs(res)) diff --git a/pvade/structure/StructureMain.py b/pvade/structure/StructureMain.py index ed3a756..8f81ea2 100644 --- a/pvade/structure/StructureMain.py +++ b/pvade/structure/StructureMain.py @@ -80,6 +80,10 @@ def __init__(self, domain, params): domain.structure.msh, params.structure.rho ) # Constant(0.) + self.rho_connector = dolfinx.fem.Constant( + domain.structure.msh, params.structure.rho_tube + ) + # Define structural properties self.E = params.structure.elasticity_modulus # 1.0e9 self.poissons_ratio = params.structure.poissons_ratio # 0.3 @@ -89,6 +93,16 @@ def __init__(self, domain, params): * self.poissons_ratio / ((1.0 + self.poissons_ratio) * (1.0 - 2.0 * self.poissons_ratio)) ) + + # we are assuming the connectors has same mechanical properties as the tubes + self.E_connector = params.structure.elasticity_modulus_tube # 1.0e9 + self.poissons_ratio_connector = params.structure.poissons_ratio_tube # 0.3 + self.lame_mu_connector = self.E_connector / (2.0 * (1.0 + self.poissons_ratio_connector)) + self.lame_lambda_connector = ( + self.E_connector + * self.poissons_ratio_connector + / ((1.0 + self.poissons_ratio_connector) * (1.0 - 2.0 * self.poissons_ratio_connector)) + ) if self.rank == 0: print( @@ -201,7 +215,7 @@ def update_fields(self, u, u_old, v_old, a_old, dt, beta, gamma): def avg(self, x_old, x_new, alpha): return alpha * x_old + (1 - alpha) * x_new - def build_forms(self, domain, params): + def build_forms(self, domain, params, flow): """Builds all variational statements This method creates all the functions, expressions, and variational @@ -217,7 +231,7 @@ def build_forms(self, domain, params): params (:obj:`pvade.Parameters.SimParams`): A SimParams object """ - self.elasticity.build_forms(domain, params, self) + self.elasticity.build_forms(domain, params, self, flow) def _assemble_system(self, params): """Pre-assemble all LHS matrices and RHS vectors diff --git a/pvade/structure/boundary_conditions.py b/pvade/structure/boundary_conditions.py index 0aa0ea0..ebfe5f7 100644 --- a/pvade/structure/boundary_conditions.py +++ b/pvade/structure/boundary_conditions.py @@ -344,20 +344,12 @@ def connection_point_up_helper(nodes_to_pin_between): return fn_handle - # Start pinning along the lines expressed byt numpy_pt_total_array - # First, determine the total number of rows (number of lines to pin) - num_nodes = np.shape(domain.numpy_pt_total_array)[0] - - # Determine how many pinning lines exist per each panel (e.g., 24 lines distributed on 8 panels means 3 lines per panel) - nodes_per_panel = int(num_nodes / total_num_panels) - - # The torque tube entry (oriented spanwise along the middle, divides panel into upstream and downstream rectangular halves) - # is always the first entry, e.g., [0, ..., ..., 3, ..., ..., 6, ..., ...], [0, 3, 6] are the torque tubes - tube_nodes_idx = np.arange(0, num_nodes, nodes_per_panel, dtype=np.int64) + # # Start pinning along the lines expressed byt numpy_pt_total_array + # The center line of connectors bottom surface is fixed to remove rigid body motion if params.structure.tube_connection == True: # If making torque tube connections, pass only those pinning lines to the BC identification function - tube_nodes = domain.numpy_pt_total_array[tube_nodes_idx, :] + tube_nodes = domain.numpy_pt_total_array[:, :] facet_uppoint = dolfinx.mesh.locate_entities( domain.structure.msh, 1, connection_point_up_helper(tube_nodes) @@ -369,18 +361,18 @@ def connection_point_up_helper(nodes_to_pin_between): bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) if params.structure.motor_connection == True: - # If making motor mount connections, pass only those pinning lines to the BC identification function - # this is done by making a copy of the numpy_pt_total_array with the torque tube lines *deleted* - # not done in place, so numpy_pt_total_array remains unaltered. - motor_nodes = np.delete(domain.numpy_pt_total_array, tube_nodes_idx, axis=0) + # The bottom surface of the center connector is fixed to represent the motor mount + motor_location = params.pv_array.modules_per_span // 2 + for panel_id in range(total_num_panels): - facet_uppoint = dolfinx.mesh.locate_entities( - domain.structure.msh, 1, connection_point_up_helper(motor_nodes) - ) - dofs_disp = dolfinx.fem.locate_dofs_topological( - functionspace, 1, [facet_uppoint] + mount_facet = domain.structure.facet_tags.find( + domain.domain_markers[f"block_bottom_{panel_id:.0f}_{motor_location:.0f}"]["idx"] ) + + dofs_disp = dolfinx.fem.locate_dofs_topological( + functionspace, 2, [mount_facet] + ) - bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) + bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) return bc diff --git a/pvade_main.py b/pvade_main.py index 8bc322a..6f262f5 100644 --- a/pvade_main.py +++ b/pvade_main.py @@ -16,6 +16,10 @@ import os from mpi4py import MPI +""" +to run: python -u pvade_main.py --input_file=input/duramat_case_study.yaml --domain.l_char=3.0 --general.mesh_only=true --pv_array.torque_tube_radius=0.1 --pv_array.torque_tube_separation=0.4 --pv_array.stream_rows=2 --pv_array.tracker_angle=[-40,40] --pv_array.modules_per_span=10 +""" + def main(input_file=None): # Get the path to the input file from the command line @@ -40,7 +44,7 @@ def main(input_file=None): domain.read_mesh_files(params.general.input_mesh_dir, params) else: domain.build(params) - + # If we only want to create the mesh, we can stop here if params.general.mesh_only: list_timings(params.comm, [TimingType.wall]) @@ -60,7 +64,7 @@ def main(input_file=None): structure = Structure(domain, params) structure.build_boundary_conditions(domain, params) # # # Build the fluid forms - structure.build_forms(domain, params) + structure.build_forms(domain, params, flow) else: structure = None From 64f4efccae199a39bd9f65cd4da9f0369889d69d Mon Sep 17 00:00:00 2001 From: He Date: Tue, 11 Nov 2025 17:14:25 -0700 Subject: [PATCH 19/37] dx and ds for modules and connectors --- pvade/IO/input_schema.yaml | 7 ++++ pvade/fluid/FlowManager.py | 3 ++ pvade/geometry/MeshManager.py | 42 +++++++++++++++++++--- pvade/geometry/panels3d/DomainCreation.py | 43 ++++++++++++++++++++--- pvade/structure/ElasticityAnalysis.py | 18 +++++----- 5 files changed, 96 insertions(+), 17 deletions(-) diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index 9822740..f9911da 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -691,6 +691,13 @@ properties: type: "number" description: "poissons ratio of the torque tube." units: "None" + density_tube: + default: 1.0 + # minimum: 0.001 + # maximum: 1000.0 + type: "number" + description: "The density of the structure." + units: "kg/meter^3" body_force_x: default: 100 type: "number" diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index 0f2a1c8..f52ec07 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -856,6 +856,9 @@ def solve(self, domain, params, current_time): self.compute_lift_and_drag(params, current_time) + self.compute_panel_torques(domain, params) + self.compute_double_integral_panel_torques(domain, params) + # Compute the pressure drop between the inlet and outlet if params.pv_array.stream_rows > 0: self.compute_pressure_drop_between_points(domain, params) diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index ebd8acd..618f064 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -207,6 +207,8 @@ def build(self, params): gdim=self.ndim, ) + + self.msh.topology.create_connectivity(self.ndim, self.ndim - 1) # Specify names for the mesh elements @@ -214,6 +216,11 @@ def build(self, params): self.cell_tags.name = "cell_tags" self.facet_tags.name = "facet_tags" + with dolfinx.io.XDMFFile(self.comm, f"parent_mesh_only.xdmf", "w") as fp: + fp.write_mesh(self.msh) + fp.write_meshtags(self.cell_tags) + fp.write_meshtags(self.facet_tags) + # if ( # params.general.geometry_module == "panels3d" # or params.general.geometry_module == "heliostats3d" @@ -276,10 +283,17 @@ def _create_submeshes_from_parent(self, params): print(f"Creating {sub_domain_name} submesh") # Get the idx associated with either "fluid" or "structure" - marker_id = self.domain_markers[sub_domain_name]["idx"] - - # Find all cells where cell tag = marker_id - submesh_cells = self.cell_tags.find(marker_id) + if sub_domain_name == "structure" and "structure" not in self.domain_markers: + marker_id = self.domain_markers["modules"]["idx"] + # Find all cells where cell tag = marker_id + submesh_cells_modules = self.cell_tags.find(marker_id) + marker_id = self.domain_markers["connectors"]["idx"] + submesh_cells = np.hstack((self.cell_tags.find(marker_id), submesh_cells_modules)) + + else: + marker_id = self.domain_markers[sub_domain_name]["idx"] + # Find all cells where cell tag = marker_id + submesh_cells = self.cell_tags.find(marker_id) # Use those found cells to construct a new mesh submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh( @@ -345,14 +359,32 @@ def _transfer_mesh_tags_to_submeshes(self, params): for child, parent in zip(child_facets, parent_facets): sub_values[child] = all_values[parent] + # sub_cell_map = sub_domain.msh.topology.index_map(self.ndim) + f_map_cell = self.msh.topology.index_map(self.ndim) + + # Get the total number of cells in the parent mesh + num_cells = f_map_cell.size_local + f_map_cell.num_ghosts + all_cell_values = np.zeros(num_cells, dtype=np.int32) + all_cell_values[self.cell_tags.indices] = self.cell_tags.values + + sub_cell_map = sub_domain.msh.topology.index_map(self.ndim) sub_num_cells = sub_cell_map.size_local + sub_cell_map.num_ghosts + sub_cell_values = np.empty(sub_num_cells, dtype=np.int32) + + + for k, entity in enumerate(sub_domain.entity_map): + sub_cell_values[k] = all_cell_values[entity] + + + # sub_num_cells = sub_cell_map.size_local + sub_cell_map.num_ghosts + sub_domain.cell_tags = dolfinx.mesh.meshtags( sub_domain.msh, sub_domain.msh.topology.dim, np.arange(sub_num_cells, dtype=np.int32), - np.ones(sub_num_cells, dtype=np.int32), + sub_cell_values, ) sub_domain.cell_tags.name = "cell_tags" diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 8ad4a7e..4709c9b 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -166,13 +166,14 @@ def Rz(theta): if ( params.pv_array.torque_tube_separation > 0.0 - and params.pv_array.torque_tube_radius > 0.0 + and params.pv_array.torque_tube_outer_radius > 0.0 ): modeling_torque_tube = True else: modeling_torque_tube = False - + vol_tags_modules = [] + vol_tags_connectors = [] # start to add panel for panel_id_y, yy in enumerate(y_centers): for panel_id_x, xx in enumerate(x_centers): @@ -382,6 +383,20 @@ def Rz(theta): [panel_tag], oriented=False ) + vol_com = self.gmsh_model.occ.getCenterOfMass(self.ndim, panel_tag[1]) + if np.isclose(vol_com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness / 2.0): + target_key = f"modules" + elif np.isclose(vol_com[2], params.pv_array.torque_tube_separation / 2.0): + target_key = f"connectors" + + + + if target_key is not None: + if target_key in this_panel_transformed_com: + this_panel_transformed_com[target_key].append(vol_com) + else: + this_panel_transformed_com[target_key] = [vol_com] + for surf_tag in surf_tags_for_this_panel: surf_dim = surf_tag[0] surf_id = surf_tag[1] @@ -739,6 +754,24 @@ def Rz(theta): ) if this_surf_bbox[2] > params.domain.z_max: raise ValueError(f"A panel extends past the z_max wall.") + + + # Loop over all the finalized volumes after fragmentation and tag everything + all_vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) + + for vol_tag in all_vol_tag_list: + vol_id = vol_tag[1] + com = self.gmsh_model.occ.getCenterOfMass(self.ndim, vol_id) + + located_this_volume = False + + for key, val in transformed_com.items(): + for target_com in val: + # print(target_com) + if np.allclose(np.array(com), target_com): + located_this_volume = True + if "trash" not in key: + self._add_to_domain_markers(key, [vol_id], "cell") # Volumes are the entities with dimension equal to the mesh dimension vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) @@ -763,8 +796,10 @@ def Rz(theta): structure_panel_only_list = [] structure_connector_only_list = [] - - self._add_to_domain_markers("structure", structure_vol_list, "cell") +# + # # self._add_to_domain_markers("structure", structure_vol_list, "cell") + # for individual_vol_id in structure_vol_list: + # self._add_to_domain_markers(f"structure_{individual_vol_id:03.0f}", [individual_vol_id], "cell") self._add_to_domain_markers("fluid", fluid_vol_list, "cell") # Record all the data collected to domain_markers as physics groups with physical names diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 862dbeb..76d61cc 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -464,14 +464,18 @@ def P_connector(u): self.calculate_K_for_Robin_BC(domain, flow, params) + dx_structure = ufl.Measure( + "dx", domain=domain.structure.msh, subdomain_data=domain.structure.cell_tags + ) + # To Do: how to differentiate the connector part and the panel part, dx_connector and dx_panel self.res = ( m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * ufl.dx + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * ufl.dx - + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * ufl.dx_panel - + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * ufl.dx_connector - - structure.rho * ufl.inner(self.f, self.u_) * ufl.dx_panel - - structure.rho_connector * ufl.inner(self.f, self.u_) * ufl.dx_connector + + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure(domain.domain_markers["modules"]["idx"]) + + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure(domain.domain_markers["connectors"]["idx"]) + - structure.rho * ufl.inner(self.f, self.u_) * dx_structure(domain.domain_markers["modules"]["idx"]) + - structure.rho_connector * ufl.inner(self.f, self.u_) * dx_structure(domain.domain_markers["connectors"]["idx"]) - ufl.dot(ufl.dot(self.stress_predicted * J * ufl.inv(F.T), n), self.u_) * self.ds ) # - Wext(self.u) @@ -481,10 +485,8 @@ def P_connector(u): for i in range(params.pv_array.modules_per_span): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" K_springs = dolfinx.fem.Constant(domain.structure.msh, float(getattr(self, name_K))) - self.res += ufl.dot(K_springs * self.u_, self.z_unit_vector)*ds_bottom.panel_connector_markers[panel_id][i] - for i in range(params.pv_array.modules_per_span): - self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector)*ds_bottom.... - + self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector)*self.ds(domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]) + # self.a = dolfinx.fem.form(ufl.lhs(res)) # self.L = dolfinx.fem.form(ufl.rhs(res)) From dc61252eb6af24e8501b385d37f2af5f3ccbc423 Mon Sep 17 00:00:00 2001 From: He Date: Tue, 16 Dec 2025 14:14:46 -0700 Subject: [PATCH 20/37] new derivations without split the left fixed and right fixed part --- input/duramat_case_study.yaml | 1 + pvade/IO/input_schema.yaml | 4 + pvade/fluid/FlowManager.py | 231 +++++++++------- pvade/geometry/MeshManager.py | 2 + pvade/geometry/panels3d/DomainCreation.py | 6 +- pvade/structure/ElasticityAnalysis.py | 321 ++++++++++++++++------ pvade/structure/boundary_conditions.py | 6 +- 7 files changed, 377 insertions(+), 194 deletions(-) diff --git a/input/duramat_case_study.yaml b/input/duramat_case_study.yaml index 2221b4e..bbb8277 100644 --- a/input/duramat_case_study.yaml +++ b/input/duramat_case_study.yaml @@ -27,6 +27,7 @@ pv_array: torque_tube_outer_radius: 0.1 # radius of the torque tube torque_tube_inner_radius: 0.09 # radius of the torque tube modules_per_span: 10 + fixed_location: 5 # fixed location of the panel along the span (from 0 (fixed at left) to modules_per_span (fixed at right)) block_chord_div_by_panel_chord: 0.02 solver: diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index f9911da..f9f3bf2 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -221,6 +221,10 @@ properties: minimum: 1 type: "integer" description: "The number of modules/panels per every panel_span distance. This can be thought of as the number of modules/panels per each table, counted in the spanwise direction only (i.e., 2-in-portait systems should report only the number in the spanwise direction, vs 2x that value)." + fixed_location: + default: 5 + type: "integer" + description: "If an integer between 0 and modules_per_span, indicates the fixed location block_chord_div_by_panel_chord: default: 0.1 minimum: 0.01 diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index f52ec07..bf892f4 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -583,33 +583,35 @@ def _all_interior_surfaces(x): "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags ) - def compute_panel_torques(self, domain, params): + # def compute_total_torques_on_each_panel(self, domain, params): - ds_fluid = ufl.Measure( - "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags - ) + # ds_fluid = ufl.Measure( + # "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags + # ) - for panel_id in range( - int(params.pv_array.stream_rows * params.pv_array.span_rows) - ): - for module_id in range(params.pv_array.modules_per_span): - - total_torque = 0 + # for panel_id in range( + # int(params.pv_array.stream_rows * params.pv_array.span_rows) + # ): + # total_torque = 0 - total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - ) - total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( - domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - ) - total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( - domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - ) - total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - ) - attr_name = f"total_torque_panel_{panel_id:.0f}_{module_id:.0f}" - setattr(self, attr_name, total_torque) + # for module_id in range(params.pv_array.modules_per_span): + + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( + # domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( + # domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( + # domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( + # domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + + # attr_name = f"total_torque_panel_{panel_id:.0f}" + + # setattr(self, attr_name, total_torque) def compute_double_integral_panel_torques(self, domain, params): torque_function_on_panels = dolfinx.fem.Function(self.Q) @@ -619,94 +621,125 @@ def compute_double_integral_panel_torques(self, domain, params): ds_fluid = ufl.Measure( "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags ) + for panel_id in range( int(params.pv_array.stream_rows * params.pv_array.span_rows) ): + whole_top_surface_facet_index = [] # facet index of the whole panel top surface in a row + whole_bot_surface_facet_index = [] # facet index of the whole panel bottom surface in a row + for module_id in range(params.pv_array.modules_per_span): - torque_double_integral = 0 + whole_top_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() + whole_bot_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() - whole_top_surface_facet_index = domain.fluid.facet_tags.find(domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"]) - whole_bot_surface_facet_index = domain.fluid.facet_tags.find(domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"]) - whole_top_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(self.fluid.mesh, 2, whole_top_surface_facet_index) - whole_bot_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(self.fluid.mesh, 2, whole_bot_surface_facet_index) + whole_top_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_top_surface_facet_index)) + whole_bot_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_bot_surface_facet_index)) - dx_top_whole = ufl.Measure("dx", domain=whole_top_surf_submesh) - dx_bot_whole = ufl.Measure("dx", domain=whole_bot_surf_submesh) + dx_top_whole = ufl.Measure("dx", domain=whole_top_surf_submesh) + dx_bot_whole = ufl.Measure("dx", domain=whole_bot_surf_submesh) - whole_top_submesh_function_space = dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)) - whole_top_submesh_func = dolfinx.fem.Function(whole_top_submesh_function_space) - whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)) - whole_bot_submesh_func = dolfinx.fem.Function(whole_bot_submesh_function_space) + whole_top_submesh_function_space = dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)) + whole_top_submesh_func = dolfinx.fem.Function(whole_top_submesh_function_space) + whole_top_submesh_func.interpolate()(torque_function_on_panels) + whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)) + whole_bot_submesh_func = dolfinx.fem.Function(whole_bot_submesh_function_space) + whole_bot_submesh_func.interpolate()(torque_function_on_panels) - coords_top = whole_top_submesh_function_space.tabulate_dof_coordinates() - coords_bot = whole_bot_submesh_function_space.tabulate_dof_coordinates() + total_torque_on_panel_array = dolfinx.fem.assemble_scalar(dolfinx.fem.form(whole_top_submesh_func * dx_top_whole)) \ + + dolfinx.fem.assemble_scalar(dolfinx.fem.form(whole_bot_submesh_func * dx_bot_whole)) + + attr_name = f"total_torque_panel_{panel_id:.0f}" - top_integrated_func_along_x = np.zeros_like(whole_top_submesh_func.x.array[:]) + setattr(self, attr_name, total_torque_on_panel_array) - bot_integrated_func_along_x = np.zeros_like(whole_bot_submesh_func.x.array[:]) - - # Sort by x first, then y and z (group by x-levels) - sort_idx_top = np.lexsort((coords_top[:, 1], coords_top[:, 0])) # x is sorted, from small to large - sort_idx_bot = np.lexsort((coords_bot[:, 1], coords_bot[:, 0])) # x is sorted, from small to large - - top_coords_sorted = coords_top[sort_idx_top] #sorted from smallest x to largest x - bot_coords_sorted = coords_bot[sort_idx_bot] #sorted from smallest x to largest x - + coords_top = whole_top_submesh_function_space.tabulate_dof_coordinates() + coords_bot = whole_bot_submesh_function_space.tabulate_dof_coordinates() - ### integral of top surface: - # Unique x-values (up to numerical tolerance) - xs_top = np.unique(np.round(top_coords_sorted[:, 0], decimals=6)) + top_integrated_func_along_y = np.zeros_like(whole_top_submesh_func.x.array[:]) + bot_integrated_func_along_y = np.zeros_like(whole_bot_submesh_func.x.array[:]) + + # in PVade, the front has the minimum y coordinates, if we integral from large y to small y, it is negative, + # to keep it consistant as the theoretical model, we need to flip the sign of the integral - for x_value in xs_top: - # Mask for points at this x-level - x_mask_top = np.isclose(top_coords_sorted[:, 0], x_value, atol=1e-6) - - facet_centers_top = dolfinx.mesh.compute_midpoints(whole_top_surf_submesh, 2, np.array(np.arange(whole_top_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) - # Identify cells where the midpoint x-coordinate is less than 1 - marked_facet_top = np.where(facet_centers_top[:, 0] <= x_value)[0] - - # Create a submesh from those cells - submesh_top_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_top_surf_submesh, 2, marked_facet_top.astype(np.int32)) - top_sub_function_space = dolfinx.fem.FunctionSpace(submesh_top_surface_part, ('CG', 1)) - top_sub_func = dolfinx.fem.Function(top_sub_function_space) - top_sub_func.interpolate(torque_function_on_panels) - - dx_top = ufl.Measure("dx", domain=submesh_top_surface_part) - integrated_top = dolfinx.fem.assemble_scalar(dolfinx.fem.form(top_sub_func * dx_top)) - top_integrated_func_along_x[sort_idx_top[x_mask_top]] = integrated_top - - ### integral of bot surface: - # Unique x-values (up to numerical tolerance) - xs_bot = np.unique(np.round(bot_coords_sorted[:, 0], decimals=6)) - - for x_value in xs_bot: - - # Mask for points at this x-level - x_mask_bot = np.isclose(bot_coords_sorted[:, 0], x_value, atol=1e-6) - facet_centers_bot = dolfinx.mesh.compute_midpoints(whole_bot_surf_submesh, 2, np.array(np.arange(whole_bot_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) - # Identify cells where the midpoint x-coordinate is less than 1 - marked_facet_bot = np.where(facet_centers_bot[:, 0] <= x_value)[0] - # Create a submesh from those cells - submesh_bot_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_bot_surf_submesh, 2, marked_facet_bot.astype(np.int32)) - bot_sub_function_space = dolfinx.fem.FunctionSpace(submesh_bot_surface_part, ('CG', 1)) - bot_sub_func = dolfinx.fem.Function(bot_sub_function_space) - bot_sub_func.interpolate(torque_function_on_panels) - - dx_bot = ufl.Measure("dx", domain=submesh_bot_surface_part) - integrated_bot = dolfinx.fem.assemble_scalar(dolfinx.fem.form(bot_sub_func * dx_bot)) - bot_integrated_func_along_x[sort_idx_bot[x_mask_bot]] = integrated_bot - - single_integral_function_top = dolfinx.fem.Function(whole_top_submesh_function_space, name='single_integral_function_top') - single_integral_function_top.x.array[:] = top_integrated_func_along_x - - single_integral_function_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name='single_integral_function_bot') - single_integral_function_bot.x.array[:] = bot_integrated_func_along_x - - torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_top * dx_top_whole))/self.pvarry.panel_span*params.pv_array.modules_per_span - torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_bot * dx_bot_whole))/self.pvarry.panel_span*params.pv_array.modules_per_span - attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{module_id:.0f}" + # Sort by y first, then z and z (group by y-levels) + sort_idx_top = np.lexsort((coords_top[:, 0], -coords_top[:, 1])) # y is sorted, from large to small + sort_idx_bot = np.lexsort((coords_bot[:, 0], -coords_bot[:, 1])) # y is sorted, from large to small + + top_coords_sorted = coords_top[sort_idx_top] #sorted from largest y to smallest y + bot_coords_sorted = coords_bot[sort_idx_bot] #sorted from largest y to smallest y + + ### integral of top surface: + # Unique x-values (up to numerical tolerance) + ys_top = np.unique(np.round(top_coords_sorted[:, 0], decimals=6)) + + # get f(x) = integral of torque on top surface from left end to x (x is all unique x coordinates on top surface) + + for y_value in ys_top: + # Mask for points at this y-level + y_mask_top = np.isclose(top_coords_sorted[:, 1], y_value, atol=1e-6) + + facet_centers_top = dolfinx.mesh.compute_midpoints(whole_top_surf_submesh, 2, np.array(np.arange(whole_top_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) + # Identify cells where the midpoint y-coordinate is higher + marked_facet_top = np.where(facet_centers_top[:, 1] >= y_value)[0] + + # Create a submesh from those cells + submesh_top_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_top_surf_submesh, 2, marked_facet_top.astype(np.int32)) + top_sub_function_space = dolfinx.fem.FunctionSpace(submesh_top_surface_part, ('CG', 1)) + top_sub_func = dolfinx.fem.Function(top_sub_function_space) + top_sub_func.interpolate(torque_function_on_panels) + + dx_top = ufl.Measure("dx", domain=submesh_top_surface_part) + integrated_top = dolfinx.fem.assemble_scalar(dolfinx.fem.form(top_sub_func * dx_top)) + top_integrated_func_along_y[sort_idx_top[y_mask_top]] = integrated_top + + ### integral of bot surface: + # Unique y-values (up to numerical tolerance) + ys_bot = np.unique(np.round(bot_coords_sorted[:, 1], decimals=6)) + + for y_value in ys_bot: + + # Mask for points at this y-level + y_mask_bot = np.isclose(bot_coords_sorted[:, 1], y_value, atol=1e-6) + facet_centers_bot = dolfinx.mesh.compute_midpoints(whole_bot_surf_submesh, 2, np.array(np.arange(whole_bot_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) + # Identify cells where the midpoint y-coordinate is less than 1 + marked_facet_bot = np.where(facet_centers_bot[:, 1] >= y_value)[0] + # Create a submesh from those cells + submesh_bot_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_bot_surf_submesh, 2, marked_facet_bot.astype(np.int32)) + bot_sub_function_space = dolfinx.fem.FunctionSpace(submesh_bot_surface_part, ('CG', 1)) + bot_sub_func = dolfinx.fem.Function(bot_sub_function_space) + bot_sub_func.interpolate(torque_function_on_panels) + + dx_bot = ufl.Measure("dx", domain=submesh_bot_surface_part) + integrated_bot = dolfinx.fem.assemble_scalar(dolfinx.fem.form(bot_sub_func * dx_bot)) + bot_integrated_func_along_y[sort_idx_bot[y_mask_bot]] = integrated_bot + + single_integral_function_top = dolfinx.fem.Function(whole_top_submesh_function_space, name='single_integral_function_top') + single_integral_function_top.x.array[:] = top_integrated_func_along_y + + single_integral_function_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name='single_integral_function_bot') + single_integral_function_bot.x.array[:] = bot_integrated_func_along_y + + torque_double_integral = 0.0 + + attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_0" + setattr(self, attr_name, torque_double_integral) + + single_integral_function_top_fluid = dolfinx.fem.Function(self.Q) + single_integral_function_top_fluid.interpolate(single_integral_function_top) + single_integral_function_bot_fluid = dolfinx.fem.Function(self.Q) + single_integral_function_bot_fluid.interpolate(single_integral_function_bot) + + for connector_id in range(params.pv_array.modules_per_span): + torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_top_fluid * ds_fluid( + domain.domain_markers[f"panel_top_{panel_id:.0f}_{params.panel_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord + torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_bot_fluid * ds_fluid( + domain.domain_markers[f"panel_bot_{panel_id:.0f}_{params.panel_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord + attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{connector_id+1:.0f}" setattr(self, attr_name, torque_double_integral) + + # double_integral_total_torque_panel_0_0 is the double integral of first panel array at the most back connector (maximum y), + # double_integral_total_torque_panel_0_10 is the double integral of first panel array at the most front connector (smallest y) def _assemble_system(self, params): """Pre-assemble all LHS matrices and RHS vectors @@ -856,7 +889,7 @@ def solve(self, domain, params, current_time): self.compute_lift_and_drag(params, current_time) - self.compute_panel_torques(domain, params) + # self.compute_panel_torques(domain, params) self.compute_double_integral_panel_torques(domain, params) # Compute the pressure drop between the inlet and outlet diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index 618f064..11f54be 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -283,6 +283,8 @@ def _create_submeshes_from_parent(self, params): print(f"Creating {sub_domain_name} submesh") # Get the idx associated with either "fluid" or "structure" + + # if structure includes modules and connectors if sub_domain_name == "structure" and "structure" not in self.domain_markers: marker_id = self.domain_markers["modules"]["idx"] # Find all cells where cell tag = marker_id diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 4709c9b..fdd46af 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -498,9 +498,10 @@ def Rz(theta): if ( np.isclose(com[2], params.pv_array.torque_tube_separation) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) ): - target_key = f"interior_surface_{panel_ct:.0f}" + target_key = f"interior_surface_{panel_ct:.0f}" # block/panel interface surface_located_or_not = True + # if not the most left/right panel boundary, it is panel/panel interface if not surface_located_or_not: for module_id in range(params.pv_array.modules_per_span): @@ -756,7 +757,7 @@ def Rz(theta): raise ValueError(f"A panel extends past the z_max wall.") - # Loop over all the finalized volumes after fragmentation and tag everything + # mark the panel and connector volumes all_vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) for vol_tag in all_vol_tag_list: @@ -773,6 +774,7 @@ def Rz(theta): if "trash" not in key: self._add_to_domain_markers(key, [vol_id], "cell") + # Mark the fluid volume # Volumes are the entities with dimension equal to the mesh dimension vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) structure_vol_list = [] diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 76d61cc..24d344c 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -101,128 +101,269 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # total number of pv rows total_num_panels = params.pv_array.stream_rows * params.pv_array.span_rows - num_panel_right_fixed = params.pv_array.modules_per_span // 2 - num_panel_left_fixed = params.pv_array.modules_per_span - num_panel_right_fixed + for panel_id in range(total_num_panels): + # construct the matrix, size: modules_per_span+1 by modules_per_span+1 + theo_matrix = np.zeros((params.pv_array.modules_per_span+1, params.pv_array.modules_per_span+1)) + theo_vector = np.zeros(params.pv_array.modules_per_span+1) + + connector_locations = np.linspace( + 0, params.pv_array.panel_span, params.pv_array.modules_per_span + 1 + ) - # for right fixed part: + total_torque_on_this_panel_name = f"total_torque_panel_{panel_id:.0f}" + total_torque_on_this_panel = getattr(flow, total_torque_on_this_panel_name) - for panel_id in range(total_num_panels): - total_torque_right_fixed = 0 - total_torque_left_fixed = 0 + theo_matrix[params.pv_array.modules_per_span, :] = 1/Gp/Ipp + theo_vector[params.pv_array.modules_per_span] = -total_torque_on_this_panel/Gp/Ipp + + # contribution from panel: + + # contribution from C0 to matrix: + + theo_matrix[:params.pv_array.modules_per_span, :params.pv_array.fixed_location] += connector_locations[params.pv_array.fixed_location]/(Gp*Ipp) + theo_matrix[:params.pv_array.modules_per_span, 1:params.pv_array.fixed_location] -= connector_locations[1:params.pv_array.fixed_location]/(Gp*Ipp) + + # contribution from C0 to vector: + T_double_integral_at_fixed_location_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{params.pv_array.fixed_location:.0f}" + T_double_integral_at_fixed_location = getattr(flow, T_double_integral_at_fixed_location_name) + theo_vector[:params.pv_array.modules_per_span] += -T_double_integral_at_fixed_location/Gp/Ipp + + # contribution of panels to matrix + theo_matrix[0, 0] += -connector_locations[0]/(Gp*Ipp) + theo_matrix[1, 0] += -connector_locations[1]/(Gp*Ipp) + + for i in range(2, params.pv_array.fixed_location): + theo_matrix[i, :i] += -connector_locations[i]/(Gp*Ipp) + theo_matrix[i, 1:i] += connector_locations[1:i]/(Gp*Ipp) + + for i in range(params.pv_array.fixed_location, params.pv_array.modules_per_span): + theo_matrix[i, :i+1] += -connector_locations[i+1]/(Gp*Ipp) + theo_matrix[i, 1:i+1] += connector_locations[1:i+1]/(Gp*Ipp) + + # contribution of panels to vector + T_double_integral_array = [] + for i in range(params.pv_array.modules_per_span+1): + name = f"double_integral_total_torque_panel_{panel_id:.0f}_{i:.0f}" + T_double_integral_array.append(getattr(flow, name)) + T_double_integral_array = np.array(T_double_integral_array) + theo_vector[:params.pv_array.modules_per_span] += np.delete(T_double_integral_array, params.pv_array.fixed_location)/Gp/Ipp + + # contribution from tube: + + # contribution from C0 to matrix: + + theo_matrix[:params.pv_array.modules_per_span, params.pv_array.fixed_location:params.pv_array.modules_per_span+1] += -connector_locations[params.pv_array.fixed_location]/(Gt*Ipt) + theo_matrix[:params.pv_array.modules_per_span, 1:params.pv_array.fixed_location] += -connector_locations[1:params.pv_array.fixed_location]/(Gt*Ipt) + + # contribution from C0 to vector: + theo_vector[:params.pv_array.modules_per_span] += total_torque_on_this_panel*connector_locations[params.pv_array.fixed_location]/Gt/Ipt + + # contribution of tube to matrix + theo_matrix[0, 1:params.pv_array.modules_per_span+1] += connector_locations[0]/(Gt*Ipt) + theo_matrix[1, 1:params.pv_array.modules_per_span+1] += connector_locations[1]/(Gt*Ipt) + + for i in range(2, params.pv_array.fixed_location): + theo_matrix[i, i:params.pv_array.modules_per_span+1] += connector_locations[i]/(Gt*Ipt) + theo_matrix[i, 1:i] += connector_locations[1:i]/(Gt*Ipt) + + for i in range(params.pv_array.fixed_location, params.pv_array.modules_per_span): + theo_matrix[i, i+1:params.pv_array.modules_per_span+1] += connector_locations[i+1]/(Gt*Ipt) + theo_matrix[i, 1:i+1] += connector_locations[1:i+1]/(Gt*Ipt) + + # contribution of tube to vector + theo_vector[:params.pv_array.fixed_location] += -total_torque_on_this_panel/Gt/Ipt*connector_locations[:params.pv_array.fixed_location] + theo_vector[params.pv_array.fixed_location:params.pv_array.modules_per_span] += -total_torque_on_this_panel/Gt/Ipt*connector_locations[params.pv_array.fixed_location] + + R_reaction_torque = np.dot(np.linalg.inv(theo_matrix), theo_vector) # this is the torque applied to tube + + # rotation of tube at each connector location + tube_rotate_matrix = np.zeros((params.pv_array.modules_per_span, params.pv_array.modules_per_span+1)) + tube_rotate_vector = np.zeros(params.pv_array.modules_per_span) + + # contribution from panel: + + # contribution from C0 to matrix: + + tube_rotate_matrix[:params.pv_array.modules_per_span, :params.pv_array.fixed_location] += connector_locations[params.pv_array.fixed_location]/(Gp*Ipp) + tube_rotate_matrix[:params.pv_array.modules_per_span, 1:params.pv_array.fixed_location] -= connector_locations[1:params.pv_array.fixed_location]/(Gp*Ipp) + + # contribution from C0 to vector: + tube_rotate_vector[:params.pv_array.modules_per_span] += T_double_integral_array[params.pv_array.fixed_location]/Gp/Ipp + + + # contribution of panels to matrix + tube_rotate_matrix[0, 0] += -connector_locations[0]/(Gp*Ipp) + tube_rotate_matrix[1, 0] += -connector_locations[1]/(Gp*Ipp) + + for i in range(2, params.pv_array.fixed_location): + tube_rotate_matrix[i, :i] += -connector_locations[i]/(Gp*Ipp) + tube_rotate_matrix[i, 1:i] += connector_locations[1:i]/(Gp*Ipp) + + for i in range(params.pv_array.fixed_location, params.pv_array.modules_per_span): + tube_rotate_matrix[i, :i+1] += -connector_locations[i+1]/(Gp*Ipp) + tube_rotate_matrix[i, 1:i+1] += connector_locations[1:i+1]/(Gp*Ipp) + + # contribution of panels to vector + tube_rotate_vector[:params.pv_array.modules_per_span] += -np.delete(T_double_integral_array, params.pv_array.fixed_location)/Gp/Ipp + + + phi = np.dot(tube_rotate_matrix, R_reaction_torque) + tube_rotate_vector + + if isinstance(params.pv_array.tracker_angle, list): + if panel_id == 0: + assert ( + len(params.pv_array.tracker_angle) + == params.pv_array.stream_rows * params.pv_array.span_rows + ), f"Length of tracker angle list ({len(params.pv_array.tracker_angle)}) not equal to total number of PV tables ({params.pv_array.stream_rows * params.pv_array.span_rows})." + + tracker_angle_rad = np.radians( + params.pv_array.tracker_angle[panel_id] + ) + else: + tracker_angle_rad = np.radians(params.pv_array.tracker_angle) + + + K = np.abs(12*(np.delete(R_reaction_torque, params.pv_array.fixed_location))/((params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord)**3)/np.cos(tracker_angle_rad+phi)/(np.sin(tracker_angle_rad+phi)-np.sin(tracker_angle_rad))/(params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord/2)) + + # assume K is 0 at the fixed connector + K = np.flip(np.insert(K, params.pv_array.fixed_location, 0)) + + # K[0] is the stiffness at the most back connector (highest y) + # K[10] is the stiffness at the most front connector (lowest y) + # for block_bot_surface, it is numbered from lowest y to highest y, so flip K - for i in range(num_panel_left_fixed, params.pv_array.modules_per_span): - name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" - total_torque_right_fixed += getattr(flow, name) + # if there are 10 modules per array, there are 11 connectors, K has shape of 10, the K at the fixed connector is not included. + for i in range(params.pv_array.modules_per_span+1): + + name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" + setattr(self, name_K, K[i]) + + + + + + + # num_panel_right_fixed = params.pv_array.modules_per_span // 2 + # num_panel_left_fixed = params.pv_array.modules_per_span - num_panel_right_fixed + + # # for right fixed part: + + # for panel_id in range(total_num_panels): + # total_torque_right_fixed = 0 + # total_torque_left_fixed = 0 + + # for i in range(num_panel_left_fixed, params.pv_array.modules_per_span): + # name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" + # total_torque_right_fixed += getattr(flow, name) - for i in range(num_panel_left_fixed): - name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" - total_torque_left_fixed += getattr(flow, name) + # for i in range(num_panel_left_fixed): + # name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" + # total_torque_left_fixed += getattr(flow, name) - theo_matrix_right_fixed = np.zeros((num_panel_right_fixed+1, num_panel_right_fixed+1)) - theo_vector_right_fixed = np.zeros(num_panel_right_fixed+1) + # theo_matrix_right_fixed = np.zeros((num_panel_right_fixed+1, num_panel_right_fixed+1)) + # theo_vector_right_fixed = np.zeros(num_panel_right_fixed+1) - theo_matrix_right_fixed[0, :] = 1.0 - theo_vector_right_fixed[0] = total_torque_right_fixed - theo_matrix_right_fixed[1:, :-1] = params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt + # theo_matrix_right_fixed[0, :] = 1.0 + # theo_vector_right_fixed[0] = total_torque_right_fixed + # theo_matrix_right_fixed[1:, :-1] = params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt - # as the double integral of torque along the span from front to back, left fixed part to right fixed part, it need to be flipped - T_double_integral_right_fixed = [] - T_right_fixed = [] - for i in range(params.pv_array.modules_per_span): - name = f"double_integral_total_torque_panel_{panel_id:.0f}_{params.pv_array.modules_per_span-1-i:.0f}" - T_double_integral_right_fixed.append(getattr(flow, name)) - name = f"total_torque_panel_{panel_id:.0f}_{params.pv_array.modules_per_span-1-i:.0f}" - T_right_fixed.append(getattr(flow, name)) - for i in range(num_panel_right_fixed): - for j in range(i+1): - theo_vector_right_fixed[i+1] += T_double_integral_right_fixed[-1-j]/(Gp*Ipp) - theo_vector_right_fixed[i+1] -= (i+1-j)*T_right_fixed[-1-j]/(Gp*Ipp)*params.pv_array.panel_span/params.pv_array.modules_per_span - for j in range(i): - theo_matrix_right_fixed[i+1, :-1-j-1] += params.pv_array.panel_span/params.pv_array.modules_per_span/(Gt*Ipt) - - - for i in range(num_panel_right_fixed): - for j in range(i+1): - theo_matrix_right_fixed[i+1, num_panel_right_fixed-j:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gp/Ipp - - R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_right_fixed), theo_vector_right_fixed) + # # as the double integral of torque along the span from front to back, left fixed part to right fixed part, it need to be flipped + # T_double_integral_right_fixed = [] + # T_right_fixed = [] + # for i in range(params.pv_array.modules_per_span): + # name = f"double_integral_total_torque_panel_{panel_id:.0f}_{params.pv_array.modules_per_span-1-i:.0f}" + # T_double_integral_right_fixed.append(getattr(flow, name)) + # name = f"total_torque_panel_{panel_id:.0f}_{params.pv_array.modules_per_span-1-i:.0f}" + # T_right_fixed.append(getattr(flow, name)) + # for i in range(num_panel_right_fixed): + # for j in range(i+1): + # theo_vector_right_fixed[i+1] += T_double_integral_right_fixed[-1-j]/(Gp*Ipp) + # theo_vector_right_fixed[i+1] -= (i+1-j)*T_right_fixed[-1-j]/(Gp*Ipp)*params.pv_array.panel_span/params.pv_array.modules_per_span + # for j in range(i): + # theo_matrix_right_fixed[i+1, :-1-j-1] += params.pv_array.panel_span/params.pv_array.modules_per_span/(Gt*Ipt) + + + # for i in range(num_panel_right_fixed): + # for j in range(i+1): + # theo_matrix_right_fixed[i+1, num_panel_right_fixed-j:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gp/Ipp + + # R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_right_fixed), theo_vector_right_fixed) - tube_rotate_matrix = np.ones((num_panel_right_fixed, num_panel_right_fixed))*params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) + # tube_rotate_matrix = np.ones((num_panel_right_fixed, num_panel_right_fixed))*params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) - for i in range(num_panel_right_fixed): - for j in range(i): - tube_rotate_matrix[i, :num_panel_right_fixed-1-j] += params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) + # for i in range(num_panel_right_fixed): + # for j in range(i): + # tube_rotate_matrix[i, :num_panel_right_fixed-1-j] += params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) - # check the standalone code, why it need to be flipped. - phi = np.flip(np.dot(tube_rotate_matrix, R_reaction_torque[:-1])) + # # check the standalone code, why it need to be flipped. + # phi = np.flip(np.dot(tube_rotate_matrix, R_reaction_torque[:-1])) - # rotation of connector - x_panel = np.arange(num_panel_right_fixed)*(params.pv_array.panel_span/params.pv_array.modules_per_span) # location of panels + # # rotation of connector + # x_panel = np.arange(num_panel_right_fixed)*(params.pv_array.panel_span/params.pv_array.modules_per_span) # location of panels - block_length = params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord - block_width = params.pv_array.block_chord_div_by_panel_chord*params.pv_array.panel_span/params.pv_array.modules_per_span/2 + # block_length = params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord + # block_width = params.pv_array.block_chord_div_by_panel_chord*params.pv_array.panel_span/params.pv_array.modules_per_span/2 - # To Do: check the angle, is this correct? - array_rotation = (params.fluid.wind_direction + 90.0) % 360.0 - array_rotation_rad = np.radians(array_rotation) + # # To Do: check the angle, is this correct? + # array_rotation = (params.fluid.wind_direction + 90.0) % 360.0 + # array_rotation_rad = np.radians(array_rotation) - for i in range(num_panel_right_fixed): + # for i in range(num_panel_right_fixed): - phi_i = phi[i] + # phi_i = phi[i] - K_i = 12*(R_reaction_torque[i])/((block_length)**3)/np.cos(array_rotation_rad+phi_i)/(np.sin(array_rotation_rad+phi_i)-np.sin(array_rotation_rad))/block_width + # K_i = 12*(R_reaction_torque[i])/((block_length)**3)/np.cos(array_rotation_rad+phi_i)/(np.sin(array_rotation_rad+phi_i)-np.sin(array_rotation_rad))/block_width - name_K = f"spring_stiffness_{panel_id:.0f}_{params.pv_array.modules_per_span+1-i:.0f}" - - setattr(self, name_K, K_i) + # name_K = f"spring_stiffness_{panel_id:.0f}_{params.pv_array.modules_per_span+1-i:.0f}" + # setattr(self, name_K, K_i) - # for left fixed part: - theo_matrix_left_fixed = np.zeros((num_panel_left_fixed+1, num_panel_left_fixed+1)) - theo_vector_left_fixed = np.zeros(num_panel_left_fixed+1) - theo_matrix_left_fixed[0, :] = 1.0 - theo_vector_left_fixed[0] = total_torque_left_fixed + # # for left fixed part: + # theo_matrix_left_fixed = np.zeros((num_panel_left_fixed+1, num_panel_left_fixed+1)) + # theo_vector_left_fixed = np.zeros(num_panel_left_fixed+1) + # theo_matrix_left_fixed[0, :] = 1.0 + # theo_vector_left_fixed[0] = total_torque_left_fixed - T_double_integral_left_fixed = [] - T_left_fixed = [] + # T_double_integral_left_fixed = [] + # T_left_fixed = [] - for i in range(num_panel_left_fixed): - name = f"double_integral_total_torque_panel_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" - T_double_integral_left_fixed.append(getattr(flow, name)) - name = f"total_torque_panel_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" - T_left_fixed.append(getattr(flow, name)) + # for i in range(num_panel_left_fixed): + # name = f"double_integral_total_torque_panel_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" + # T_double_integral_left_fixed.append(getattr(flow, name)) + # name = f"total_torque_panel_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" + # T_left_fixed.append(getattr(flow, name)) - for i in range(num_panel_left_fixed): - for j in range(i+1): - theo_matrix_left_fixed[i+1, j] = (i+1-j)*params.pv_array.panel_span/params.pv_array.modules_per_span/(Gp*Ipp) - theo_vector_left_fixed[i+1] += T_double_integral_left_fixed[j]/(Gp*Ipp) - for j in range(i): - theo_vector_left_fixed[i+1] += (i-j)*T_left_fixed[j]/(Gp*Ipp)*params.pv_array.panel_span/params.pv_array.modules_per_span + # for i in range(num_panel_left_fixed): + # for j in range(i+1): + # theo_matrix_left_fixed[i+1, j] = (i+1-j)*params.pv_array.panel_span/params.pv_array.modules_per_span/(Gp*Ipp) + # theo_vector_left_fixed[i+1] += T_double_integral_left_fixed[j]/(Gp*Ipp) + # for j in range(i): + # theo_vector_left_fixed[i+1] += (i-j)*T_left_fixed[j]/(Gp*Ipp)*params.pv_array.panel_span/params.pv_array.modules_per_span - for i in range(num_panel_left_fixed): - theo_matrix_left_fixed[i+1:, i+1:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt + # for i in range(num_panel_left_fixed): + # theo_matrix_left_fixed[i+1:, i+1:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt - R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_left_fixed), theo_vector_left_fixed) # this is the torque applied to tube + # R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_left_fixed), theo_vector_left_fixed) # this is the torque applied to tube - tube_rotate_matrix = np.zeros((num_panel_left_fixed, num_panel_left_fixed)) - for i in range(num_panel_left_fixed): - tube_rotate_matrix[i:, i:] += params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt + # tube_rotate_matrix = np.zeros((num_panel_left_fixed, num_panel_left_fixed)) + # for i in range(num_panel_left_fixed): + # tube_rotate_matrix[i:, i:] += params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt - phi = np.dot(tube_rotate_matrix, R_reaction_torque[1:]) + # phi = np.dot(tube_rotate_matrix, R_reaction_torque[1:]) - # rotation of connector - x_panel = np.arange(num_panel_left_fixed)*(params.pv_array.panel_span/params.pv_array.modules_per_span)+params.pv_array.panel_span/params.pv_array.modules_per_span # location of panles + # # rotation of connector + # x_panel = np.arange(num_panel_left_fixed)*(params.pv_array.panel_span/params.pv_array.modules_per_span)+params.pv_array.panel_span/params.pv_array.modules_per_span # location of panles - for i in range(num_panel_left_fixed): + # for i in range(num_panel_left_fixed): - phi_i = phi[i] + # phi_i = phi[i] - K_i = 12*(R_reaction_torque[i+1])/((block_length)**3)/np.cos(array_rotation_rad+phi_i)/(np.sin(array_rotation_rad+phi_i)-np.sin(array_rotation_rad))/block_width + # K_i = 12*(R_reaction_torque[i+1])/((block_length)**3)/np.cos(array_rotation_rad+phi_i)/(np.sin(array_rotation_rad+phi_i)-np.sin(array_rotation_rad))/block_width - name_K = f"spring_stiffness_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" + # name_K = f"spring_stiffness_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" - setattr(self, name_K, K_i) + # setattr(self, name_K, K_i) def update_a(self, u, u_old, v_old, a_old, dt, beta, ufl=True): # Update formula for acceleration @@ -482,7 +623,7 @@ def P_connector(u): # Robin boundary condition terms for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): - for i in range(params.pv_array.modules_per_span): + for i in range(params.pv_array.modules_per_span+1): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" K_springs = dolfinx.fem.Constant(domain.structure.msh, float(getattr(self, name_K))) self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector)*self.ds(domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]) diff --git a/pvade/structure/boundary_conditions.py b/pvade/structure/boundary_conditions.py index ebfe5f7..956b857 100644 --- a/pvade/structure/boundary_conditions.py +++ b/pvade/structure/boundary_conditions.py @@ -272,7 +272,7 @@ def build_structure_boundary_conditions(domain, params, functionspace): total_num_panels = params.pv_array.stream_rows * params.pv_array.span_rows for num_panel in range(total_num_panels): - for location in params.structure.bc_list: + for location in params.structure.bc_list: # it is empty location_panel = f"{location}_{num_panel}" # f"front_{num_panel}" , f"back_{num_panel}": # for location in [f"left_{num_panel}"]:# , f"right_{num_panel}": @@ -362,11 +362,11 @@ def connection_point_up_helper(nodes_to_pin_between): if params.structure.motor_connection == True: # The bottom surface of the center connector is fixed to represent the motor mount - motor_location = params.pv_array.modules_per_span // 2 + motor_location = params.pv_array.fixed_location # fixed location along the span, if fixed_location=5, it means the left boundary of the 6th connector is fixed for panel_id in range(total_num_panels): mount_facet = domain.structure.facet_tags.find( - domain.domain_markers[f"block_bottom_{panel_id:.0f}_{motor_location:.0f}"]["idx"] + domain.domain_markers[f"block_left_{panel_id:.0f}_{motor_location:.0f}"]["idx"] ) dofs_disp = dolfinx.fem.locate_dofs_topological( From 3f764ece8771b49d2482f0de98e64e0ac0179edd Mon Sep 17 00:00:00 2001 From: He Date: Tue, 16 Dec 2025 14:55:02 -0700 Subject: [PATCH 21/37] fix bugs in the input file --- input/duramat_case_study.yaml | 2 +- pvade/IO/input_schema.yaml | 4 ++-- pvade/fluid/FlowManager.py | 2 ++ pvade/geometry/MeshManager.py | 2 +- pvade/structure/ElasticityAnalysis.py | 4 ++++ pvade_main.py | 2 +- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/input/duramat_case_study.yaml b/input/duramat_case_study.yaml index bbb8277..1f40a19 100644 --- a/input/duramat_case_study.yaml +++ b/input/duramat_case_study.yaml @@ -68,4 +68,4 @@ structure: beta_relaxation: 0.5 elasticity_modulus_tube: 2.0e+11 poissons_ratio_tube: 0.3 - density_tube: 7800.0 \ No newline at end of file + rho_tube: 7800.0 \ No newline at end of file diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index f9f3bf2..18f1beb 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -224,7 +224,7 @@ properties: fixed_location: default: 5 type: "integer" - description: "If an integer between 0 and modules_per_span, indicates the fixed location + description: "If an integer between 0 and modules_per_span, indicates the fixed location" block_chord_div_by_panel_chord: default: 0.1 minimum: 0.01 @@ -695,7 +695,7 @@ properties: type: "number" description: "poissons ratio of the torque tube." units: "None" - density_tube: + rho_tube: default: 1.0 # minimum: 0.001 # maximum: 1000.0 diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index bf892f4..197a2d4 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -653,6 +653,8 @@ def compute_double_integral_panel_torques(self, domain, params): setattr(self, attr_name, total_torque_on_panel_array) + print('check total torque definiition', self.total_torque_panel_0) + coords_top = whole_top_submesh_function_space.tabulate_dof_coordinates() coords_bot = whole_bot_submesh_function_space.tabulate_dof_coordinates() diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index 11f54be..f35a7aa 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -747,7 +747,7 @@ def test_mesh_functionspace(self): print(f"Rank {self.rank} owns {num_nodes_owned_by_proc} nodes\n{coords}") - def test_submesh_transfer(self, params): + def test_submesh_transfer(self, params, domain, elasticity): P2 = ufl.VectorElement("Lagrange", self.msh.ufl_cell(), 2) # P2 = ufl.FiniteElement("Lagrange", self.fluid.msh.ufl_cell(), 1) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 24d344c..38fecea 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -110,6 +110,10 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): 0, params.pv_array.panel_span, params.pv_array.modules_per_span + 1 ) + # ?? double integral is not available until the second time step. Should make it be zero for the + # first time step then it will be updated once the first step is finished? but the assemble is out of + # the time loop. Should we solve fluid then structure? + total_torque_on_this_panel_name = f"total_torque_panel_{panel_id:.0f}" total_torque_on_this_panel = getattr(flow, total_torque_on_this_panel_name) diff --git a/pvade_main.py b/pvade_main.py index 6f262f5..2af05fe 100644 --- a/pvade_main.py +++ b/pvade_main.py @@ -17,7 +17,7 @@ from mpi4py import MPI """ -to run: python -u pvade_main.py --input_file=input/duramat_case_study.yaml --domain.l_char=3.0 --general.mesh_only=true --pv_array.torque_tube_radius=0.1 --pv_array.torque_tube_separation=0.4 --pv_array.stream_rows=2 --pv_array.tracker_angle=[-40,40] --pv_array.modules_per_span=10 +to run: python -u pvade_main.py --input_file=input/duramat_case_study.yaml --domain.l_char=3.0 --pv_array.torque_tube_radius=0.1 --pv_array.torque_tube_separation=0.2 --pv_array.stream_rows=2 --pv_array.tracker_angle=[20,20] --pv_array.modules_per_span=10 """ From 5bbb1dda6ce7ed0576d8e6e6617ba8c0fb1dde49 Mon Sep 17 00:00:00 2001 From: He Date: Fri, 19 Dec 2025 16:45:40 -0700 Subject: [PATCH 22/37] fix initialization of total torque --- pvade/fluid/FlowManager.py | 32 ++++++++++++++++++++++----- pvade/structure/ElasticityAnalysis.py | 26 ++++++++++++++++++---- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index 197a2d4..accb25c 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -165,6 +165,17 @@ def build_forms(self, domain, params): """ + # initialize total torque on each panel to zero + for panel_id in range( + int(params.pv_array.stream_rows * params.pv_array.span_rows) + ): + attr_name = f"total_torque_panel_{panel_id:.0f}" + setattr(self, attr_name, dolfinx.fem.Constant(domain.fluid.msh, 0.0)) + + for module_id in range(params.pv_array.modules_per_span+1): + attr_d_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{module_id:.0f}" + setattr(self, attr_d_name, dolfinx.fem.Constant(domain.fluid.msh, 0.0)) + # Define fluid properties if self.ndim == 2: self.dpdx = dolfinx.fem.Constant(domain.fluid.msh, (0.0, 0.0)) @@ -624,7 +635,8 @@ def compute_double_integral_panel_torques(self, domain, params): for panel_id in range( int(params.pv_array.stream_rows * params.pv_array.span_rows) - ): + ): + whole_top_surface_facet_index = [] # facet index of the whole panel top surface in a row whole_bot_surface_facet_index = [] # facet index of the whole panel bottom surface in a row @@ -651,7 +663,9 @@ def compute_double_integral_panel_torques(self, domain, params): attr_name = f"total_torque_panel_{panel_id:.0f}" - setattr(self, attr_name, total_torque_on_panel_array) + total_torque_constant = getattr(self, attr_name) + + total_torque_constant.value = total_torque_on_panel_array print('check total torque definiition', self.total_torque_panel_0) @@ -724,8 +738,12 @@ def compute_double_integral_panel_torques(self, domain, params): torque_double_integral = 0.0 + attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_0" - setattr(self, attr_name, torque_double_integral) + # setattr(self, attr_name, torque_double_integral) + + double_integral_total_torque_panel_constant = getattr(self, attr_name) + double_integral_total_torque_panel_constant.value = torque_double_integral single_integral_function_top_fluid = dolfinx.fem.Function(self.Q) single_integral_function_top_fluid.interpolate(single_integral_function_top) @@ -734,11 +752,13 @@ def compute_double_integral_panel_torques(self, domain, params): for connector_id in range(params.pv_array.modules_per_span): torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_top_fluid * ds_fluid( - domain.domain_markers[f"panel_top_{panel_id:.0f}_{params.panel_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord + domain.domain_markers[f"panel_top_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_bot_fluid * ds_fluid( - domain.domain_markers[f"panel_bot_{panel_id:.0f}_{params.panel_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord + domain.domain_markers[f"panel_bot_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{connector_id+1:.0f}" - setattr(self, attr_name, torque_double_integral) + + double_integral_total_torque_panel_constant = getattr(self, attr_name) + double_integral_total_torque_panel_constant.value = torque_double_integral # double_integral_total_torque_panel_0_0 is the double integral of first panel array at the most back connector (maximum y), # double_integral_total_torque_panel_0_10 is the double integral of first panel array at the most front connector (smallest y) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 38fecea..0e80a82 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -115,7 +115,7 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # the time loop. Should we solve fluid then structure? total_torque_on_this_panel_name = f"total_torque_panel_{panel_id:.0f}" - total_torque_on_this_panel = getattr(flow, total_torque_on_this_panel_name) + total_torque_on_this_panel = getattr(flow, total_torque_on_this_panel_name).value theo_matrix[params.pv_array.modules_per_span, :] = 1/Gp/Ipp theo_vector[params.pv_array.modules_per_span] = -total_torque_on_this_panel/Gp/Ipp @@ -129,7 +129,7 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # contribution from C0 to vector: T_double_integral_at_fixed_location_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{params.pv_array.fixed_location:.0f}" - T_double_integral_at_fixed_location = getattr(flow, T_double_integral_at_fixed_location_name) + T_double_integral_at_fixed_location = getattr(flow, T_double_integral_at_fixed_location_name).value theo_vector[:params.pv_array.modules_per_span] += -T_double_integral_at_fixed_location/Gp/Ipp # contribution of panels to matrix @@ -148,7 +148,7 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): T_double_integral_array = [] for i in range(params.pv_array.modules_per_span+1): name = f"double_integral_total_torque_panel_{panel_id:.0f}_{i:.0f}" - T_double_integral_array.append(getattr(flow, name)) + T_double_integral_array.append(getattr(flow, name).value) T_double_integral_array = np.array(T_double_integral_array) theo_vector[:params.pv_array.modules_per_span] += np.delete(T_double_integral_array, params.pv_array.fixed_location)/Gp/Ipp @@ -613,6 +613,24 @@ def P_connector(u): "dx", domain=domain.structure.msh, subdomain_data=domain.structure.cell_tags ) + # print(domain.domain_markers["modules"]) + # print(np.unique(domain.structure.cell_tags.values)) + + print(domain.domain_markers["modules"]["idx"]) + + print(dolfinx.fem.assemble_scalar(dolfinx.fem.form(dolfinx.fem.Constant(domain.structure.msh, 1.0) * dx_structure(167)))) + print(dolfinx.fem.assemble_scalar(dolfinx.fem.form(dolfinx.fem.Constant(domain.structure.msh, 1.0) * dx_structure(168)))) + + print(id(domain.structure.msh)) + print(id(self.V.mesh)) + print(id(domain.structure.cell_tags.mesh)) + + print(np.unique(domain.structure.cell_tags.values)) + print(domain.domain_markers["modules"]["idx"]) + print(domain.domain_markers["connectors"]["idx"]) + + exit() + # To Do: how to differentiate the connector part and the panel part, dx_connector and dx_panel self.res = ( m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * ufl.dx @@ -630,7 +648,7 @@ def P_connector(u): for i in range(params.pv_array.modules_per_span+1): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" K_springs = dolfinx.fem.Constant(domain.structure.msh, float(getattr(self, name_K))) - self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector)*self.ds(domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]) + self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector)*self.ds(domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]["idx"]) # self.a = dolfinx.fem.form(ufl.lhs(res)) # self.L = dolfinx.fem.form(ufl.rhs(res)) From 527e0820f4c765c292e3d6d40911e6d8a18fbb7d Mon Sep 17 00:00:00 2001 From: xinhe2205 <45882470+xinhe2205@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:18:47 -0700 Subject: [PATCH 23/37] fix the Assertion Error The variational form should include only ufl.dx or dx_structure. It cause assertion error if we use ufl.dx and dx_structure at the same time. --- pvade/structure/ElasticityAnalysis.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 0e80a82..e382e0c 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -613,28 +613,11 @@ def P_connector(u): "dx", domain=domain.structure.msh, subdomain_data=domain.structure.cell_tags ) - # print(domain.domain_markers["modules"]) - # print(np.unique(domain.structure.cell_tags.values)) - - print(domain.domain_markers["modules"]["idx"]) - - print(dolfinx.fem.assemble_scalar(dolfinx.fem.form(dolfinx.fem.Constant(domain.structure.msh, 1.0) * dx_structure(167)))) - print(dolfinx.fem.assemble_scalar(dolfinx.fem.form(dolfinx.fem.Constant(domain.structure.msh, 1.0) * dx_structure(168)))) - print(id(domain.structure.msh)) - print(id(self.V.mesh)) - print(id(domain.structure.cell_tags.mesh)) - - print(np.unique(domain.structure.cell_tags.values)) - print(domain.domain_markers["modules"]["idx"]) - print(domain.domain_markers["connectors"]["idx"]) - - exit() - # To Do: how to differentiate the connector part and the panel part, dx_connector and dx_panel self.res = ( - m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * ufl.dx - + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * ufl.dx + m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure + + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure(domain.domain_markers["modules"]["idx"]) + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure(domain.domain_markers["connectors"]["idx"]) - structure.rho * ufl.inner(self.f, self.u_) * dx_structure(domain.domain_markers["modules"]["idx"]) From 0681dd5cccb1b6f6768706f24f2b4fa373005182 Mon Sep 17 00:00:00 2001 From: xinhe2205 <45882470+xinhe2205@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:37:49 -0700 Subject: [PATCH 24/37] Initialize spring stiffness we are solving the structure first. For the first step, no stress is applied to the structure, so the rotation is zero from first time step, which return nan stiffness value. Initialize the spring stiffness to zero at the first time step (or phi=0). --- pvade/structure/ElasticityAnalysis.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index e382e0c..038ee3e 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -226,8 +226,11 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): else: tracker_angle_rad = np.radians(params.pv_array.tracker_angle) - - K = np.abs(12*(np.delete(R_reaction_torque, params.pv_array.fixed_location))/((params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord)**3)/np.cos(tracker_angle_rad+phi)/(np.sin(tracker_angle_rad+phi)-np.sin(tracker_angle_rad))/(params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord/2)) + if np.linalg.norm(phi) == 0: + print('yes 0') + K = np.zeros(params.pv_array.modules_per_span) + else: + K = np.abs(12*(np.delete(R_reaction_torque, params.pv_array.fixed_location))/((params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord)**3)/np.cos(tracker_angle_rad+phi)/(np.sin(tracker_angle_rad+phi)-np.sin(tracker_angle_rad))/(params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord/2)) # assume K is 0 at the fixed connector K = np.flip(np.insert(K, params.pv_array.fixed_location, 0)) From e5423e2e20b2b0869534aa063387f1efa6c3515f Mon Sep 17 00:00:00 2001 From: He Date: Wed, 7 Jan 2026 09:58:41 -0700 Subject: [PATCH 25/37] verified --- pvade/fluid/FlowManager.py | 98 +++++++++++++++++++-------- pvade/structure/ElasticityAnalysis.py | 5 -- 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index accb25c..6969054 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -625,55 +625,93 @@ def _all_interior_surfaces(x): # setattr(self, attr_name, total_torque) def compute_double_integral_panel_torques(self, domain, params): - torque_function_on_panels = dolfinx.fem.Function(self.Q) - - torque_function_on_panels.x.array[:] = self.spatial_X_coords.x.array[:]*self.traction[2].x.array[:]-self.spatial_Z_coords.x.array[:]*self.traction[0].x.array[:] ds_fluid = ufl.Measure( "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags ) - + for panel_id in range( int(params.pv_array.stream_rows * params.pv_array.span_rows) - ): - - whole_top_surface_facet_index = [] # facet index of the whole panel top surface in a row - whole_bot_surface_facet_index = [] # facet index of the whole panel bottom surface in a row + ): + whole_top_surface_facet_index = [] # facet index of the whole panel top surface in a row + whole_bot_surface_facet_index = [] # facet index of the whole panel bottom surface in a row for module_id in range(params.pv_array.modules_per_span): - whole_top_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() whole_bot_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() - whole_top_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_top_surface_facet_index)) - whole_bot_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_bot_surface_facet_index)) + whole_top_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_top_surface_facet_index, dtype=np.int32)) + whole_bot_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_bot_surface_facet_index, dtype=np.int32)) + + spatial_X_coords_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="X_coords_top") + spatial_X_coords_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="X_coords_bot") + spatial_Z_coords_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="Z_coords_top") + spatial_Z_coords_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="Z_coords_bot") + spatial_X_coords_top.interpolate(self.spatial_X_coords) + spatial_X_coords_bot.interpolate(self.spatial_X_coords) + spatial_Z_coords_top.interpolate(self.spatial_Z_coords) + spatial_Z_coords_bot.interpolate(self.spatial_Z_coords) + """ + the torque should be relative to central of tube, traction x times z (z=0 at center tube) + traction z times x, (x=0 at center tube) + for z, how we should put the tube center at zero? It is not minus elevation + """ + traction_z_array_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="traction_z_top") + traction_x_array_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="traction_x_top") + + traction_z_array_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="traction_z_bot") + traction_x_array_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="traction_x_bot") + + traction_z_array_top.interpolate(self.traction[2]) + traction_x_array_top.interpolate(self.traction[0]) + traction_z_array_bot.interpolate(self.traction[2]) + traction_x_array_bot.interpolate(self.traction[0]) + + # traction_z_array_top.x.array[:] = 10*np.sin(params.pv_array.tracker_angle[0]*np.pi/180) + # traction_z_array_bot.x.array[:] = 10*np.sin(params.pv_array.tracker_angle[0]*np.pi/180) + # traction_x_array_top.x.array[:] = 10*np.cos(params.pv_array.tracker_angle[0]*np.pi/180) + # traction_x_array_bot.x.array[:] = 10*np.cos(params.pv_array.tracker_angle[0]*np.pi/180) + + torque_function_on_panels_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1))) + torque_function_on_panels_top.x.array[:] = ( + (spatial_X_coords_top.x.array[:] - np.linspace( + 0.0, + params.pv_array.stream_spacing * (params.pv_array.stream_rows - 1), + params.pv_array.stream_rows, + )[panel_id]) * traction_z_array_top.x.array[:] + - (spatial_Z_coords_top.x.array[:] - params.pv_array.elevation) * traction_x_array_top.x.array[:] + ) + + torque_function_on_panels_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1))) + torque_function_on_panels_bot.x.array[:] = ( + (spatial_X_coords_bot.x.array[:] - np.linspace( + 0.0, + params.pv_array.stream_spacing * (params.pv_array.stream_rows - 1), + params.pv_array.stream_rows, + )[panel_id]) * traction_z_array_bot.x.array[:] + - (spatial_Z_coords_bot.x.array[:] - params.pv_array.elevation) * traction_x_array_bot.x.array[:] + ) dx_top_whole = ufl.Measure("dx", domain=whole_top_surf_submesh) dx_bot_whole = ufl.Measure("dx", domain=whole_bot_surf_submesh) + total_torque_on_panel_array = ( + dolfinx.fem.assemble_scalar(dolfinx.fem.form(torque_function_on_panels_top * dx_top_whole)) + + dolfinx.fem.assemble_scalar(dolfinx.fem.form(torque_function_on_panels_bot * dx_bot_whole)) + ) + whole_top_submesh_function_space = dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)) - whole_top_submesh_func = dolfinx.fem.Function(whole_top_submesh_function_space) - whole_top_submesh_func.interpolate()(torque_function_on_panels) whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)) - whole_bot_submesh_func = dolfinx.fem.Function(whole_bot_submesh_function_space) - whole_bot_submesh_func.interpolate()(torque_function_on_panels) - total_torque_on_panel_array = dolfinx.fem.assemble_scalar(dolfinx.fem.form(whole_top_submesh_func * dx_top_whole)) \ - + dolfinx.fem.assemble_scalar(dolfinx.fem.form(whole_bot_submesh_func * dx_bot_whole)) - attr_name = f"total_torque_panel_{panel_id:.0f}" - total_torque_constant = getattr(self, attr_name) total_torque_constant.value = total_torque_on_panel_array - print('check total torque definiition', self.total_torque_panel_0) - coords_top = whole_top_submesh_function_space.tabulate_dof_coordinates() coords_bot = whole_bot_submesh_function_space.tabulate_dof_coordinates() - top_integrated_func_along_y = np.zeros_like(whole_top_submesh_func.x.array[:]) - bot_integrated_func_along_y = np.zeros_like(whole_bot_submesh_func.x.array[:]) + top_integrated_func_along_y = np.zeros_like(torque_function_on_panels_top.x.array[:]) + bot_integrated_func_along_y = np.zeros_like(torque_function_on_panels_bot.x.array[:]) # in PVade, the front has the minimum y coordinates, if we integral from large y to small y, it is negative, # to keep it consistant as the theoretical model, we need to flip the sign of the integral @@ -687,7 +725,7 @@ def compute_double_integral_panel_torques(self, domain, params): ### integral of top surface: # Unique x-values (up to numerical tolerance) - ys_top = np.unique(np.round(top_coords_sorted[:, 0], decimals=6)) + ys_top = np.unique(np.round(top_coords_sorted[:, 1], decimals=6)) # get f(x) = integral of torque on top surface from left end to x (x is all unique x coordinates on top surface) @@ -703,7 +741,7 @@ def compute_double_integral_panel_torques(self, domain, params): submesh_top_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_top_surf_submesh, 2, marked_facet_top.astype(np.int32)) top_sub_function_space = dolfinx.fem.FunctionSpace(submesh_top_surface_part, ('CG', 1)) top_sub_func = dolfinx.fem.Function(top_sub_function_space) - top_sub_func.interpolate(torque_function_on_panels) + top_sub_func.interpolate(torque_function_on_panels_top) dx_top = ufl.Measure("dx", domain=submesh_top_surface_part) integrated_top = dolfinx.fem.assemble_scalar(dolfinx.fem.form(top_sub_func * dx_top)) @@ -724,7 +762,7 @@ def compute_double_integral_panel_torques(self, domain, params): submesh_bot_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_bot_surf_submesh, 2, marked_facet_bot.astype(np.int32)) bot_sub_function_space = dolfinx.fem.FunctionSpace(submesh_bot_surface_part, ('CG', 1)) bot_sub_func = dolfinx.fem.Function(bot_sub_function_space) - bot_sub_func.interpolate(torque_function_on_panels) + bot_sub_func.interpolate(torque_function_on_panels_bot) dx_bot = ufl.Measure("dx", domain=submesh_bot_surface_part) integrated_bot = dolfinx.fem.assemble_scalar(dolfinx.fem.form(bot_sub_func * dx_bot)) @@ -752,9 +790,9 @@ def compute_double_integral_panel_torques(self, domain, params): for connector_id in range(params.pv_array.modules_per_span): torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_top_fluid * ds_fluid( - domain.domain_markers[f"panel_top_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord + domain.domain_markers[f"panel_top_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/params.pv_array.panel_chord torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_bot_fluid * ds_fluid( - domain.domain_markers[f"panel_bot_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/self.pvarry.panel_chord + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/params.pv_array.panel_chord attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{connector_id+1:.0f}" double_integral_total_torque_panel_constant = getattr(self, attr_name) @@ -762,7 +800,7 @@ def compute_double_integral_panel_torques(self, domain, params): # double_integral_total_torque_panel_0_0 is the double integral of first panel array at the most back connector (maximum y), # double_integral_total_torque_panel_0_10 is the double integral of first panel array at the most front connector (smallest y) - + def _assemble_system(self, params): """Pre-assemble all LHS matrices and RHS vectors @@ -891,6 +929,8 @@ def solve(self, domain, params, current_time): self.mesh_vel.vector.array[:] = self.mesh_vel.vector.array / params.solver.dt self.mesh_vel.x.scatter_forward() + self.compute_double_integral_panel_torques(domain, params) + # Calculate the tentative velocity self._solver_step_1(params) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 038ee3e..dde7b95 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -244,12 +244,7 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" setattr(self, name_K, K[i]) - - - - - # num_panel_right_fixed = params.pv_array.modules_per_span // 2 # num_panel_left_fixed = params.pv_array.modules_per_span - num_panel_right_fixed From 25c1fd78718ecaf5789a5844705fdd246d4a9d29 Mon Sep 17 00:00:00 2001 From: He Date: Thu, 8 Jan 2026 14:01:01 -0700 Subject: [PATCH 26/37] add lift and drag force calculation --- pvade/fluid/FlowManager.py | 172 ++++++++++++++++++++++---- pvade/structure/ElasticityAnalysis.py | 3 +- pvade_main.py | 1 + 3 files changed, 149 insertions(+), 27 deletions(-) diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index 6969054..cdb97ce 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -623,6 +623,93 @@ def _all_interior_surfaces(x): # attr_name = f"total_torque_panel_{panel_id:.0f}" # setattr(self, attr_name, total_torque) + + # define integrated force forms + self.integrated_force_x_form = [] + self.integrated_force_y_form = [] + self.integrated_force_z_form = [] + + for panel_id in range( + int(params.pv_array.stream_rows * params.pv_array.span_rows) + ): + # for loc in ["top_0", "bottom_0", "left_0", "right_0"]: + # idx = domain.domain_markers[loc]["idx"] + # s = dolfinx.fem.assemble_scalar(dolfinx.fem.form(1.0*ds_fluid(idx))) + # print(f"loc = {loc}, idx = {idx}, s = {s}") + + self.integrated_force_x_form.append(0) + self.integrated_force_y_form.append(0) + self.integrated_force_z_form.append(0) + + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) + + if self.ndim == 3: + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] + ) + for module_id in range(params.pv_array.modules_per_span): + + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + ) + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + ) + + + self.integrated_force_x_form[-1] = dolfinx.fem.form( + self.integrated_force_x_form[-1] + ) + self.integrated_force_y_form[-1] = dolfinx.fem.form( + self.integrated_force_y_form[-1] + ) + self.integrated_force_z_form[-1] = dolfinx.fem.form( + self.integrated_force_z_form[-1] + ) def compute_double_integral_panel_torques(self, domain, params): @@ -635,14 +722,14 @@ def compute_double_integral_panel_torques(self, domain, params): ): whole_top_surface_facet_index = [] # facet index of the whole panel top surface in a row whole_bot_surface_facet_index = [] # facet index of the whole panel bottom surface in a row - + for module_id in range(params.pv_array.modules_per_span): whole_top_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() whole_bot_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() whole_top_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_top_surface_facet_index, dtype=np.int32)) whole_bot_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_bot_surface_facet_index, dtype=np.int32)) - + spatial_X_coords_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="X_coords_top") spatial_X_coords_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="X_coords_bot") spatial_Z_coords_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="Z_coords_top") @@ -655,23 +742,62 @@ def compute_double_integral_panel_torques(self, domain, params): the torque should be relative to central of tube, traction x times z (z=0 at center tube) + traction z times x, (x=0 at center tube) for z, how we should put the tube center at zero? It is not minus elevation """ - traction_z_array_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="traction_z_top") - traction_x_array_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="traction_x_top") - traction_z_array_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="traction_z_bot") - traction_x_array_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="traction_x_bot") + whole_top_submesh_function_space = dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)) + whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)) + + traction_z_array_top = dolfinx.fem.Function(whole_top_submesh_function_space, name="traction_z_top") + traction_x_array_top = dolfinx.fem.Function(whole_top_submesh_function_space, name="traction_x_top") + traction_z_array_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name="traction_z_bot") + traction_x_array_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name="traction_x_bot") - traction_z_array_top.interpolate(self.traction[2]) - traction_x_array_top.interpolate(self.traction[0]) - traction_z_array_bot.interpolate(self.traction[2]) - traction_x_array_bot.interpolate(self.traction[0]) + traction_z_fluid = dolfinx.fem.Function(self.Q, name="traction_z_fluid") + traction_x_fluid = dolfinx.fem.Function(self.Q, name="traction_x_fluid") + + # traction is expression on whole fluid mesh, we need to + # project it to a function defined on whole fluid mesh, then + # interpolate it to the top and bot submesh function space. + + u_inter_fluid = ufl.TrialFunction(self.Q) + v_inter_fluid = ufl.TestFunction(self.Q) + + a_inter_fluid = ufl.inner(u_inter_fluid, v_inter_fluid) * ufl.ds + L_inter_fluid_z = ufl.inner(self.traction[2], v_inter_fluid) * ufl.ds + L_inter_fluid_x = ufl.inner(self.traction[0], v_inter_fluid) * ufl.ds + + # Assemble and solve + A_inter_fluid = dolfinx.fem.petsc.assemble_matrix(dolfinx.fem.form(a_inter_fluid)) + A_inter_fluid.assemble() + + b_inter_fluid_z = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(L_inter_fluid_z)) + b_inter_fluid_x = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(L_inter_fluid_x)) + + solver = PETSc.KSP().create(traction_z_array_top.function_space.mesh.comm) + solver.setOperators(A_inter_fluid) + solver.setType("cg") + solver.getPC().setType("jacobi") + solver.setFromOptions() + solver.solve(b_inter_fluid_z, traction_z_fluid.vector) + traction_z_fluid.x.scatter_forward() + + solver = PETSc.KSP().create(traction_x_array_top.function_space.mesh.comm) + solver.setOperators(A_inter_fluid) + solver.setType("cg") + solver.getPC().setType("jacobi") + solver.setFromOptions() + solver.solve(b_inter_fluid_x, traction_x_fluid.vector) + traction_x_fluid.x.scatter_forward() + + traction_z_array_top.interpolate(traction_z_fluid) + traction_x_array_top.interpolate(traction_x_fluid) + traction_z_array_bot.interpolate(traction_z_fluid) + traction_x_array_bot.interpolate(traction_x_fluid) # traction_z_array_top.x.array[:] = 10*np.sin(params.pv_array.tracker_angle[0]*np.pi/180) # traction_z_array_bot.x.array[:] = 10*np.sin(params.pv_array.tracker_angle[0]*np.pi/180) # traction_x_array_top.x.array[:] = 10*np.cos(params.pv_array.tracker_angle[0]*np.pi/180) # traction_x_array_bot.x.array[:] = 10*np.cos(params.pv_array.tracker_angle[0]*np.pi/180) - - torque_function_on_panels_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1))) + torque_function_on_panels_top = dolfinx.fem.Function(whole_top_submesh_function_space) torque_function_on_panels_top.x.array[:] = ( (spatial_X_coords_top.x.array[:] - np.linspace( 0.0, @@ -681,7 +807,7 @@ def compute_double_integral_panel_torques(self, domain, params): - (spatial_Z_coords_top.x.array[:] - params.pv_array.elevation) * traction_x_array_top.x.array[:] ) - torque_function_on_panels_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1))) + torque_function_on_panels_bot = dolfinx.fem.Function(whole_bot_submesh_function_space) torque_function_on_panels_bot.x.array[:] = ( (spatial_X_coords_bot.x.array[:] - np.linspace( 0.0, @@ -690,7 +816,6 @@ def compute_double_integral_panel_torques(self, domain, params): )[panel_id]) * traction_z_array_bot.x.array[:] - (spatial_Z_coords_bot.x.array[:] - params.pv_array.elevation) * traction_x_array_bot.x.array[:] ) - dx_top_whole = ufl.Measure("dx", domain=whole_top_surf_submesh) dx_bot_whole = ufl.Measure("dx", domain=whole_bot_surf_submesh) @@ -698,10 +823,7 @@ def compute_double_integral_panel_torques(self, domain, params): dolfinx.fem.assemble_scalar(dolfinx.fem.form(torque_function_on_panels_top * dx_top_whole)) + dolfinx.fem.assemble_scalar(dolfinx.fem.form(torque_function_on_panels_bot * dx_bot_whole)) ) - - whole_top_submesh_function_space = dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)) - whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)) - + attr_name = f"total_torque_panel_{panel_id:.0f}" total_torque_constant = getattr(self, attr_name) @@ -715,7 +837,7 @@ def compute_double_integral_panel_torques(self, domain, params): # in PVade, the front has the minimum y coordinates, if we integral from large y to small y, it is negative, # to keep it consistant as the theoretical model, we need to flip the sign of the integral - + # Sort by y first, then z and z (group by y-levels) sort_idx_top = np.lexsort((coords_top[:, 0], -coords_top[:, 1])) # y is sorted, from large to small sort_idx_bot = np.lexsort((coords_bot[:, 0], -coords_bot[:, 1])) # y is sorted, from large to small @@ -728,7 +850,7 @@ def compute_double_integral_panel_torques(self, domain, params): ys_top = np.unique(np.round(top_coords_sorted[:, 1], decimals=6)) # get f(x) = integral of torque on top surface from left end to x (x is all unique x coordinates on top surface) - + indicator_top = 0 for y_value in ys_top: # Mask for points at this y-level y_mask_top = np.isclose(top_coords_sorted[:, 1], y_value, atol=1e-6) @@ -746,11 +868,11 @@ def compute_double_integral_panel_torques(self, domain, params): dx_top = ufl.Measure("dx", domain=submesh_top_surface_part) integrated_top = dolfinx.fem.assemble_scalar(dolfinx.fem.form(top_sub_func * dx_top)) top_integrated_func_along_y[sort_idx_top[y_mask_top]] = integrated_top - + indicator_top += 1 ### integral of bot surface: # Unique y-values (up to numerical tolerance) ys_bot = np.unique(np.round(bot_coords_sorted[:, 1], decimals=6)) - + indicator_bot = 0 for y_value in ys_bot: # Mask for points at this y-level @@ -768,6 +890,7 @@ def compute_double_integral_panel_torques(self, domain, params): integrated_bot = dolfinx.fem.assemble_scalar(dolfinx.fem.form(bot_sub_func * dx_bot)) bot_integrated_func_along_y[sort_idx_bot[y_mask_bot]] = integrated_bot + indicator_bot += 1 single_integral_function_top = dolfinx.fem.Function(whole_top_submesh_function_space, name='single_integral_function_top') single_integral_function_top.x.array[:] = top_integrated_func_along_y @@ -929,11 +1052,10 @@ def solve(self, domain, params, current_time): self.mesh_vel.vector.array[:] = self.mesh_vel.vector.array / params.solver.dt self.mesh_vel.x.scatter_forward() - self.compute_double_integral_panel_torques(domain, params) + # self.compute_double_integral_panel_torques(domain, params) # Calculate the tentative velocity self._solver_step_1(params) - # Calculate the change in pressure to enforce incompressibility self._solver_step_2(params) @@ -957,7 +1079,7 @@ def solve(self, domain, params, current_time): # Compute the pressure drop between the inlet and outlet if params.pv_array.stream_rows > 0: self.compute_pressure_drop_between_points(domain, params) - + # Update new -> old variables self.u_k2.x.array[:] = self.u_k1.x.array self.u_k1.x.array[:] = self.u_k.x.array diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index dde7b95..e585054 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -227,7 +227,6 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): tracker_angle_rad = np.radians(params.pv_array.tracker_angle) if np.linalg.norm(phi) == 0: - print('yes 0') K = np.zeros(params.pv_array.modules_per_span) else: K = np.abs(12*(np.delete(R_reaction_torque, params.pv_array.fixed_location))/((params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord)**3)/np.cos(tracker_angle_rad+phi)/(np.sin(tracker_angle_rad+phi)-np.sin(tracker_angle_rad))/(params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord/2)) @@ -244,7 +243,7 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" setattr(self, name_K, K[i]) - + # num_panel_right_fixed = params.pv_array.modules_per_span // 2 # num_panel_left_fixed = params.pv_array.modules_per_span - num_panel_right_fixed diff --git a/pvade_main.py b/pvade_main.py index 2af05fe..5e52d56 100644 --- a/pvade_main.py +++ b/pvade_main.py @@ -79,6 +79,7 @@ def main(input_file=None): for k in range(params.solver.t_steps): current_time = (k + 1) * params.solver.dt + if ( structural_analysis and (k + 1) % solve_structure_interval_n == 0 From a970d9dde5b93ac3d86a9fca85ae3a1a6ab7ed90 Mon Sep 17 00:00:00 2001 From: xinhe2205 Date: Thu, 22 Jan 2026 11:24:54 -0700 Subject: [PATCH 27/37] fix facet marker to make it compitile with previous geometry --- pvade/fluid/FlowManager.py | 447 +++++++++----- pvade/geometry/MeshManager.py | 25 +- pvade/geometry/panels3d/DomainCreation.py | 678 +++++++++++++++------- pvade/structure/ElasticityAnalysis.py | 320 ++++++---- pvade/structure/StructureMain.py | 11 +- pvade/structure/boundary_conditions.py | 14 +- pvade/tests/input/yaml/sim_params.yaml | 27 +- pvade/tests/test_fsi_mesh.py | 9 +- pvade_main.py | 2 +- 9 files changed, 1055 insertions(+), 478 deletions(-) diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index cdb97ce..f86311b 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -168,12 +168,14 @@ def build_forms(self, domain, params): # initialize total torque on each panel to zero for panel_id in range( int(params.pv_array.stream_rows * params.pv_array.span_rows) - ): + ): attr_name = f"total_torque_panel_{panel_id:.0f}" setattr(self, attr_name, dolfinx.fem.Constant(domain.fluid.msh, 0.0)) - for module_id in range(params.pv_array.modules_per_span+1): - attr_d_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{module_id:.0f}" + for module_id in range(params.pv_array.modules_per_span + 1): + attr_d_name = ( + f"double_integral_total_torque_panel_{panel_id:.0f}_{module_id:.0f}" + ) setattr(self, attr_d_name, dolfinx.fem.Constant(domain.fluid.msh, 0.0)) # Define fluid properties @@ -265,13 +267,12 @@ def __init__(self): def __call__(self, x): coords = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType) - coords[0] = x[0] return coords self.spatial_X_coords.interpolate(FillFunctionWithXCoords()) - + class FillFunctionWithYCoords: def __init__(self): pass @@ -279,7 +280,6 @@ def __init__(self): def __call__(self, x): coords = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType) - coords[0] = x[1] return coords @@ -293,9 +293,7 @@ def __init__(self): def __call__(self, x): coords = np.zeros((1, x.shape[1]), dtype=PETSc.ScalarType) - coords[0] = x[2] - return coords @@ -593,37 +591,37 @@ def _all_interior_surfaces(x): ds_fluid = ufl.Measure( "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags ) - - # def compute_total_torques_on_each_panel(self, domain, params): - - # ds_fluid = ufl.Measure( - # "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags - # ) - - # for panel_id in range( - # int(params.pv_array.stream_rows * params.pv_array.span_rows) - # ): - # total_torque = 0 - - # for module_id in range(params.pv_array.modules_per_span): - - # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( - # domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - # ) - # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( - # domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - # ) - # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( - # domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - # ) - # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( - # domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) - # ) - - # attr_name = f"total_torque_panel_{panel_id:.0f}" - - # setattr(self, attr_name, total_torque) - + + # def compute_total_torques_on_each_panel(self, domain, params): + + # ds_fluid = ufl.Measure( + # "ds", domain=domain.fluid.msh, subdomain_data=domain.fluid.facet_tags + # ) + + # for panel_id in range( + # int(params.pv_array.stream_rows * params.pv_array.span_rows) + # ): + # total_torque = 0 + + # for module_id in range(params.pv_array.modules_per_span): + + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( + # domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_X_coords, self.traction[2]) * ds_fluid( + # domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( + # domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + # total_torque += dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(self.spatial_Z_coords, self.traction[0]) * ds_fluid( + # domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"])) + # ) + + # attr_name = f"total_torque_panel_{panel_id:.0f}" + + # setattr(self, attr_name, total_torque) + # define integrated force forms self.integrated_force_x_form = [] self.integrated_force_y_form = [] @@ -642,64 +640,75 @@ def _all_interior_surfaces(x): self.integrated_force_z_form.append(0) self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] - ) + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] - ) + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] - ) + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] - ) + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] - ) + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] - ) - + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) + if self.ndim == 3: - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] - ) + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( + domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( + domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_front_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] + ) for module_id in range(params.pv_array.modules_per_span): - + self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] ) self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] ) self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] ) self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"] + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] ) - self.integrated_force_x_form[-1] = dolfinx.fem.form( self.integrated_force_x_form[-1] @@ -720,20 +729,56 @@ def compute_double_integral_panel_torques(self, domain, params): for panel_id in range( int(params.pv_array.stream_rows * params.pv_array.span_rows) ): - whole_top_surface_facet_index = [] # facet index of the whole panel top surface in a row - whole_bot_surface_facet_index = [] # facet index of the whole panel bottom surface in a row - - for module_id in range(params.pv_array.modules_per_span): - whole_top_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() - whole_bot_surface_facet_index += domain.fluid.facet_tags.find(domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"]["idx"]).tolist() + whole_top_surface_facet_index = ( + [] + ) # facet index of the whole panel top surface in a row + whole_bot_surface_facet_index = ( + [] + ) # facet index of the whole panel bottom surface in a row - whole_top_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_top_surface_facet_index, dtype=np.int32)) - whole_bot_surf_submesh, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(domain.fluid.msh, 2, np.array(whole_bot_surface_facet_index, dtype=np.int32)) + for module_id in range(params.pv_array.modules_per_span): + whole_top_surface_facet_index += domain.fluid.facet_tags.find( + domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"][ + "idx" + ] + ).tolist() + whole_bot_surface_facet_index += domain.fluid.facet_tags.find( + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] + ).tolist() + + whole_top_surf_submesh, entity_map, vertex_map, geom_map = ( + dolfinx.mesh.create_submesh( + domain.fluid.msh, + 2, + np.array(whole_top_surface_facet_index, dtype=np.int32), + ) + ) + whole_bot_surf_submesh, entity_map, vertex_map, geom_map = ( + dolfinx.mesh.create_submesh( + domain.fluid.msh, + 2, + np.array(whole_bot_surface_facet_index, dtype=np.int32), + ) + ) - spatial_X_coords_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="X_coords_top") - spatial_X_coords_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="X_coords_bot") - spatial_Z_coords_top = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)), name="Z_coords_top") - spatial_Z_coords_bot = dolfinx.fem.Function(dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)), name="Z_coords_bot") + spatial_X_coords_top = dolfinx.fem.Function( + dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ("CG", 1)), + name="X_coords_top", + ) + spatial_X_coords_bot = dolfinx.fem.Function( + dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ("CG", 1)), + name="X_coords_bot", + ) + spatial_Z_coords_top = dolfinx.fem.Function( + dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ("CG", 1)), + name="Z_coords_top", + ) + spatial_Z_coords_bot = dolfinx.fem.Function( + dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ("CG", 1)), + name="Z_coords_bot", + ) spatial_X_coords_top.interpolate(self.spatial_X_coords) spatial_X_coords_bot.interpolate(self.spatial_X_coords) spatial_Z_coords_top.interpolate(self.spatial_Z_coords) @@ -743,34 +788,52 @@ def compute_double_integral_panel_torques(self, domain, params): for z, how we should put the tube center at zero? It is not minus elevation """ - whole_top_submesh_function_space = dolfinx.fem.FunctionSpace(whole_top_surf_submesh, ('CG', 1)) - whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace(whole_bot_surf_submesh, ('CG', 1)) + whole_top_submesh_function_space = dolfinx.fem.FunctionSpace( + whole_top_surf_submesh, ("CG", 1) + ) + whole_bot_submesh_function_space = dolfinx.fem.FunctionSpace( + whole_bot_surf_submesh, ("CG", 1) + ) + + traction_z_array_top = dolfinx.fem.Function( + whole_top_submesh_function_space, name="traction_z_top" + ) + traction_x_array_top = dolfinx.fem.Function( + whole_top_submesh_function_space, name="traction_x_top" + ) + traction_z_array_bot = dolfinx.fem.Function( + whole_bot_submesh_function_space, name="traction_z_bot" + ) + traction_x_array_bot = dolfinx.fem.Function( + whole_bot_submesh_function_space, name="traction_x_bot" + ) - traction_z_array_top = dolfinx.fem.Function(whole_top_submesh_function_space, name="traction_z_top") - traction_x_array_top = dolfinx.fem.Function(whole_top_submesh_function_space, name="traction_x_top") - traction_z_array_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name="traction_z_bot") - traction_x_array_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name="traction_x_bot") - traction_z_fluid = dolfinx.fem.Function(self.Q, name="traction_z_fluid") traction_x_fluid = dolfinx.fem.Function(self.Q, name="traction_x_fluid") # traction is expression on whole fluid mesh, we need to # project it to a function defined on whole fluid mesh, then # interpolate it to the top and bot submesh function space. - + u_inter_fluid = ufl.TrialFunction(self.Q) v_inter_fluid = ufl.TestFunction(self.Q) - + a_inter_fluid = ufl.inner(u_inter_fluid, v_inter_fluid) * ufl.ds L_inter_fluid_z = ufl.inner(self.traction[2], v_inter_fluid) * ufl.ds L_inter_fluid_x = ufl.inner(self.traction[0], v_inter_fluid) * ufl.ds # Assemble and solve - A_inter_fluid = dolfinx.fem.petsc.assemble_matrix(dolfinx.fem.form(a_inter_fluid)) + A_inter_fluid = dolfinx.fem.petsc.assemble_matrix( + dolfinx.fem.form(a_inter_fluid) + ) A_inter_fluid.assemble() - - b_inter_fluid_z = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(L_inter_fluid_z)) - b_inter_fluid_x = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(L_inter_fluid_x)) + + b_inter_fluid_z = dolfinx.fem.petsc.assemble_vector( + dolfinx.fem.form(L_inter_fluid_z) + ) + b_inter_fluid_x = dolfinx.fem.petsc.assemble_vector( + dolfinx.fem.form(L_inter_fluid_x) + ) solver = PETSc.KSP().create(traction_z_array_top.function_space.mesh.comm) solver.setOperators(A_inter_fluid) @@ -797,33 +860,46 @@ def compute_double_integral_panel_torques(self, domain, params): # traction_z_array_bot.x.array[:] = 10*np.sin(params.pv_array.tracker_angle[0]*np.pi/180) # traction_x_array_top.x.array[:] = 10*np.cos(params.pv_array.tracker_angle[0]*np.pi/180) # traction_x_array_bot.x.array[:] = 10*np.cos(params.pv_array.tracker_angle[0]*np.pi/180) - torque_function_on_panels_top = dolfinx.fem.Function(whole_top_submesh_function_space) - torque_function_on_panels_top.x.array[:] = ( - (spatial_X_coords_top.x.array[:] - np.linspace( - 0.0, - params.pv_array.stream_spacing * (params.pv_array.stream_rows - 1), - params.pv_array.stream_rows, - )[panel_id]) * traction_z_array_top.x.array[:] - - (spatial_Z_coords_top.x.array[:] - params.pv_array.elevation) * traction_x_array_top.x.array[:] + torque_function_on_panels_top = dolfinx.fem.Function( + whole_top_submesh_function_space ) + torque_function_on_panels_top.x.array[:] = ( + spatial_X_coords_top.x.array[:] + - np.linspace( + 0.0, + params.pv_array.stream_spacing * (params.pv_array.stream_rows - 1), + params.pv_array.stream_rows, + )[panel_id] + ) * traction_z_array_top.x.array[:] - ( + spatial_Z_coords_top.x.array[:] - params.pv_array.elevation + ) * traction_x_array_top.x.array[ + : + ] - torque_function_on_panels_bot = dolfinx.fem.Function(whole_bot_submesh_function_space) - torque_function_on_panels_bot.x.array[:] = ( - (spatial_X_coords_bot.x.array[:] - np.linspace( - 0.0, - params.pv_array.stream_spacing * (params.pv_array.stream_rows - 1), - params.pv_array.stream_rows, - )[panel_id]) * traction_z_array_bot.x.array[:] - - (spatial_Z_coords_bot.x.array[:] - params.pv_array.elevation) * traction_x_array_bot.x.array[:] + torque_function_on_panels_bot = dolfinx.fem.Function( + whole_bot_submesh_function_space ) + torque_function_on_panels_bot.x.array[:] = ( + spatial_X_coords_bot.x.array[:] + - np.linspace( + 0.0, + params.pv_array.stream_spacing * (params.pv_array.stream_rows - 1), + params.pv_array.stream_rows, + )[panel_id] + ) * traction_z_array_bot.x.array[:] - ( + spatial_Z_coords_bot.x.array[:] - params.pv_array.elevation + ) * traction_x_array_bot.x.array[ + : + ] dx_top_whole = ufl.Measure("dx", domain=whole_top_surf_submesh) dx_bot_whole = ufl.Measure("dx", domain=whole_bot_surf_submesh) - total_torque_on_panel_array = ( - dolfinx.fem.assemble_scalar(dolfinx.fem.form(torque_function_on_panels_top * dx_top_whole)) - + dolfinx.fem.assemble_scalar(dolfinx.fem.form(torque_function_on_panels_bot * dx_bot_whole)) + total_torque_on_panel_array = dolfinx.fem.assemble_scalar( + dolfinx.fem.form(torque_function_on_panels_top * dx_top_whole) + ) + dolfinx.fem.assemble_scalar( + dolfinx.fem.form(torque_function_on_panels_bot * dx_bot_whole) ) - + attr_name = f"total_torque_panel_{panel_id:.0f}" total_torque_constant = getattr(self, attr_name) @@ -832,18 +908,30 @@ def compute_double_integral_panel_torques(self, domain, params): coords_top = whole_top_submesh_function_space.tabulate_dof_coordinates() coords_bot = whole_bot_submesh_function_space.tabulate_dof_coordinates() - top_integrated_func_along_y = np.zeros_like(torque_function_on_panels_top.x.array[:]) - bot_integrated_func_along_y = np.zeros_like(torque_function_on_panels_bot.x.array[:]) + top_integrated_func_along_y = np.zeros_like( + torque_function_on_panels_top.x.array[:] + ) + bot_integrated_func_along_y = np.zeros_like( + torque_function_on_panels_bot.x.array[:] + ) # in PVade, the front has the minimum y coordinates, if we integral from large y to small y, it is negative, # to keep it consistant as the theoretical model, we need to flip the sign of the integral # Sort by y first, then z and z (group by y-levels) - sort_idx_top = np.lexsort((coords_top[:, 0], -coords_top[:, 1])) # y is sorted, from large to small - sort_idx_bot = np.lexsort((coords_bot[:, 0], -coords_bot[:, 1])) # y is sorted, from large to small - - top_coords_sorted = coords_top[sort_idx_top] #sorted from largest y to smallest y - bot_coords_sorted = coords_bot[sort_idx_bot] #sorted from largest y to smallest y + sort_idx_top = np.lexsort( + (coords_top[:, 0], -coords_top[:, 1]) + ) # y is sorted, from large to small + sort_idx_bot = np.lexsort( + (coords_bot[:, 0], -coords_bot[:, 1]) + ) # y is sorted, from large to small + + top_coords_sorted = coords_top[ + sort_idx_top + ] # sorted from largest y to smallest y + bot_coords_sorted = coords_bot[ + sort_idx_bot + ] # sorted from largest y to smallest y ### integral of top surface: # Unique x-values (up to numerical tolerance) @@ -855,18 +943,35 @@ def compute_double_integral_panel_torques(self, domain, params): # Mask for points at this y-level y_mask_top = np.isclose(top_coords_sorted[:, 1], y_value, atol=1e-6) - facet_centers_top = dolfinx.mesh.compute_midpoints(whole_top_surf_submesh, 2, np.array(np.arange(whole_top_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) + facet_centers_top = dolfinx.mesh.compute_midpoints( + whole_top_surf_submesh, + 2, + np.array( + np.arange( + whole_top_surf_submesh.topology.index_map(2).size_local + ), + dtype=np.int32, + ), + ) # Identify cells where the midpoint y-coordinate is higher marked_facet_top = np.where(facet_centers_top[:, 1] >= y_value)[0] # Create a submesh from those cells - submesh_top_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_top_surf_submesh, 2, marked_facet_top.astype(np.int32)) - top_sub_function_space = dolfinx.fem.FunctionSpace(submesh_top_surface_part, ('CG', 1)) + submesh_top_surface_part, entity_map, vertex_map, geom_map = ( + dolfinx.mesh.create_submesh( + whole_top_surf_submesh, 2, marked_facet_top.astype(np.int32) + ) + ) + top_sub_function_space = dolfinx.fem.FunctionSpace( + submesh_top_surface_part, ("CG", 1) + ) top_sub_func = dolfinx.fem.Function(top_sub_function_space) top_sub_func.interpolate(torque_function_on_panels_top) dx_top = ufl.Measure("dx", domain=submesh_top_surface_part) - integrated_top = dolfinx.fem.assemble_scalar(dolfinx.fem.form(top_sub_func * dx_top)) + integrated_top = dolfinx.fem.assemble_scalar( + dolfinx.fem.form(top_sub_func * dx_top) + ) top_integrated_func_along_y[sort_idx_top[y_mask_top]] = integrated_top indicator_top += 1 ### integral of bot surface: @@ -877,29 +982,49 @@ def compute_double_integral_panel_torques(self, domain, params): # Mask for points at this y-level y_mask_bot = np.isclose(bot_coords_sorted[:, 1], y_value, atol=1e-6) - facet_centers_bot = dolfinx.mesh.compute_midpoints(whole_bot_surf_submesh, 2, np.array(np.arange(whole_bot_surf_submesh.topology.index_map(2).size_local), dtype=np.int32)) + facet_centers_bot = dolfinx.mesh.compute_midpoints( + whole_bot_surf_submesh, + 2, + np.array( + np.arange( + whole_bot_surf_submesh.topology.index_map(2).size_local + ), + dtype=np.int32, + ), + ) # Identify cells where the midpoint y-coordinate is less than 1 marked_facet_bot = np.where(facet_centers_bot[:, 1] >= y_value)[0] # Create a submesh from those cells - submesh_bot_surface_part, entity_map, vertex_map, geom_map = dolfinx.mesh.create_submesh(whole_bot_surf_submesh, 2, marked_facet_bot.astype(np.int32)) - bot_sub_function_space = dolfinx.fem.FunctionSpace(submesh_bot_surface_part, ('CG', 1)) + submesh_bot_surface_part, entity_map, vertex_map, geom_map = ( + dolfinx.mesh.create_submesh( + whole_bot_surf_submesh, 2, marked_facet_bot.astype(np.int32) + ) + ) + bot_sub_function_space = dolfinx.fem.FunctionSpace( + submesh_bot_surface_part, ("CG", 1) + ) bot_sub_func = dolfinx.fem.Function(bot_sub_function_space) bot_sub_func.interpolate(torque_function_on_panels_bot) dx_bot = ufl.Measure("dx", domain=submesh_bot_surface_part) - integrated_bot = dolfinx.fem.assemble_scalar(dolfinx.fem.form(bot_sub_func * dx_bot)) + integrated_bot = dolfinx.fem.assemble_scalar( + dolfinx.fem.form(bot_sub_func * dx_bot) + ) bot_integrated_func_along_y[sort_idx_bot[y_mask_bot]] = integrated_bot indicator_bot += 1 - single_integral_function_top = dolfinx.fem.Function(whole_top_submesh_function_space, name='single_integral_function_top') + single_integral_function_top = dolfinx.fem.Function( + whole_top_submesh_function_space, name="single_integral_function_top" + ) single_integral_function_top.x.array[:] = top_integrated_func_along_y - single_integral_function_bot = dolfinx.fem.Function(whole_bot_submesh_function_space, name='single_integral_function_bot') + single_integral_function_bot = dolfinx.fem.Function( + whole_bot_submesh_function_space, name="single_integral_function_bot" + ) single_integral_function_bot.x.array[:] = bot_integrated_func_along_y torque_double_integral = 0.0 - attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_0" # setattr(self, attr_name, torque_double_integral) @@ -912,16 +1037,40 @@ def compute_double_integral_panel_torques(self, domain, params): single_integral_function_bot_fluid.interpolate(single_integral_function_bot) for connector_id in range(params.pv_array.modules_per_span): - torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_top_fluid * ds_fluid( - domain.domain_markers[f"panel_top_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/params.pv_array.panel_chord - torque_double_integral += dolfinx.fem.assemble_scalar(dolfinx.fem.form(single_integral_function_bot_fluid * ds_fluid( - domain.domain_markers[f"panel_bottom_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}"]["idx"])))/params.pv_array.panel_chord + torque_double_integral += ( + dolfinx.fem.assemble_scalar( + dolfinx.fem.form( + single_integral_function_top_fluid + * ds_fluid( + domain.domain_markers[ + f"panel_top_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}" + ]["idx"] + ) + ) + ) + / params.pv_array.panel_chord + ) + torque_double_integral += ( + dolfinx.fem.assemble_scalar( + dolfinx.fem.form( + single_integral_function_bot_fluid + * ds_fluid( + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{params.pv_array.modules_per_span-1-connector_id:.0f}" + ]["idx"] + ) + ) + ) + / params.pv_array.panel_chord + ) attr_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{connector_id+1:.0f}" - + double_integral_total_torque_panel_constant = getattr(self, attr_name) - double_integral_total_torque_panel_constant.value = torque_double_integral + double_integral_total_torque_panel_constant.value = ( + torque_double_integral + ) - # double_integral_total_torque_panel_0_0 is the double integral of first panel array at the most back connector (maximum y), + # double_integral_total_torque_panel_0_0 is the double integral of first panel array at the most back connector (maximum y), # double_integral_total_torque_panel_0_10 is the double integral of first panel array at the most front connector (smallest y) def _assemble_system(self, params): @@ -1079,7 +1228,7 @@ def solve(self, domain, params, current_time): # Compute the pressure drop between the inlet and outlet if params.pv_array.stream_rows > 0: self.compute_pressure_drop_between_points(domain, params) - + # Update new -> old variables self.u_k2.x.array[:] = self.u_k1.x.array self.u_k1.x.array[:] = self.u_k.x.array diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index f35a7aa..f69e6aa 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -207,8 +207,6 @@ def build(self, params): gdim=self.ndim, ) - - self.msh.topology.create_connectivity(self.ndim, self.ndim - 1) # Specify names for the mesh elements @@ -285,13 +283,21 @@ def _create_submeshes_from_parent(self, params): # Get the idx associated with either "fluid" or "structure" # if structure includes modules and connectors - if sub_domain_name == "structure" and "structure" not in self.domain_markers: - marker_id = self.domain_markers["modules"]["idx"] + if ( + sub_domain_name == "structure" + and "structure" not in self.domain_markers + ): + marker_id = self.domain_markers["modules"]["idx"] # Find all cells where cell tag = marker_id - submesh_cells_modules = self.cell_tags.find(marker_id) - marker_id = self.domain_markers["connectors"]["idx"] - submesh_cells = np.hstack((self.cell_tags.find(marker_id), submesh_cells_modules)) - + if self.geometry.modeling_torque_tube: + submesh_cells_modules = self.cell_tags.find(marker_id) + marker_id = self.domain_markers["connectors"]["idx"] + submesh_cells = np.hstack( + (self.cell_tags.find(marker_id), submesh_cells_modules) + ) + else: + submesh_cells = self.cell_tags.find(marker_id) + else: marker_id = self.domain_markers[sub_domain_name]["idx"] # Find all cells where cell tag = marker_id @@ -369,17 +375,14 @@ def _transfer_mesh_tags_to_submeshes(self, params): all_cell_values = np.zeros(num_cells, dtype=np.int32) all_cell_values[self.cell_tags.indices] = self.cell_tags.values - sub_cell_map = sub_domain.msh.topology.index_map(self.ndim) sub_num_cells = sub_cell_map.size_local + sub_cell_map.num_ghosts sub_cell_values = np.empty(sub_num_cells, dtype=np.int32) - for k, entity in enumerate(sub_domain.entity_map): sub_cell_values[k] = all_cell_values[entity] - # sub_num_cells = sub_cell_map.size_local + sub_cell_map.num_ghosts sub_domain.cell_tags = dolfinx.mesh.meshtags( diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index fdd46af..9b7c1c1 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -152,7 +152,7 @@ def Rz(theta): # panel_tag_list includes all structure, panel+connector panel_tag_list = [] panel_ct = 0 - + # only include panels structure_panel_only_list = [] # only include connectors @@ -162,15 +162,15 @@ def Rz(theta): -half_span, half_span, params.pv_array.modules_per_span + 1 ) - transformed_com = {} # all panels + transformed_com = {} # all panels if ( params.pv_array.torque_tube_separation > 0.0 and params.pv_array.torque_tube_outer_radius > 0.0 ): - modeling_torque_tube = True + self.modeling_torque_tube = True else: - modeling_torque_tube = False + self.modeling_torque_tube = False vol_tags_modules = [] vol_tags_connectors = [] @@ -190,11 +190,11 @@ def Rz(theta): else: tracker_angle_rad = np.radians(params.pv_array.tracker_angle) - this_panel_tag_list = [] # for each row - this_panel_transformed_com = {} # for each row - - numpy_pt_list = [] # for each row - embedded_lines_tag_list = [] # for each row + this_panel_tag_list = [] # for each row + this_panel_transformed_com = {} # for each row + + numpy_pt_list = [] # for each row + embedded_lines_tag_list = [] # for each row for module_id in range(params.pv_array.modules_per_span): @@ -202,7 +202,7 @@ def Rz(theta): module_distances[module_id + 1] - module_distances[module_id] ) - if modeling_torque_tube: + if self.modeling_torque_tube: # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) this_module = self.gmsh_model.occ.addBox( -half_chord, @@ -212,17 +212,22 @@ def Rz(theta): module_span, params.pv_array.panel_thickness, ) - - panel_tag_list.append((self.ndim, this_module)) # all panels - this_panel_tag_list.append((self.ndim, this_module)) # this panel array/row + + panel_tag_list.append((self.ndim, this_module)) # all panels + this_panel_tag_list.append( + (self.ndim, this_module) + ) # this panel array/row structure_panel_only_list.append((self.ndim, this_module)) - + this_standoff = self.gmsh_model.occ.addBox( - -params.pv_array.block_chord_div_by_panel_chord * half_chord, + -params.pv_array.block_chord_div_by_panel_chord + * half_chord, module_distances[module_id], 0.0, - 2.0 * params.pv_array.block_chord_div_by_panel_chord * half_chord, + 2.0 + * params.pv_array.block_chord_div_by_panel_chord + * half_chord, params.pv_array.block_chord_div_by_panel_chord * half_chord, params.pv_array.torque_tube_separation, ) @@ -232,18 +237,39 @@ def Rz(theta): structure_connector_only_list.append((self.ndim, this_standoff)) # Add a bisecting line to the bottom of the connector in the spanwise direction - pt_1 = self.gmsh_model.occ.addPoint(0, module_distances[module_id], 0.0) - pt_2 = self.gmsh_model.occ.addPoint(0, module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0) - numpy_pt_list.append([0, module_distances[module_id], 0.0, 0, module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0]) # for this row + pt_1 = self.gmsh_model.occ.addPoint( + 0, module_distances[module_id], 0.0 + ) + pt_2 = self.gmsh_model.occ.addPoint( + 0, + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + 0.0, + ) + numpy_pt_list.append( + [ + 0, + module_distances[module_id], + 0.0, + 0, + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + 0.0, + ] + ) # for this row torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) torque_tube_tag = (1, torque_tube_id) - embedded_lines_tag_list.append(torque_tube_tag) # for this panel row + embedded_lines_tag_list.append( + torque_tube_tag + ) # for this panel row else: this_module = self.gmsh_model.occ.addBox( -half_chord, module_distances[module_id], - 0.0, + -half_thickness, params.pv_array.panel_chord, module_span, params.pv_array.panel_thickness, @@ -253,34 +279,70 @@ def Rz(theta): structure_panel_only_list.append((self.ndim, this_module)) - pt_1 = self.gmsh_model.occ.addPoint(0, module_distances[module_id], 0.0) - pt_2 = self.gmsh_model.occ.addPoint(0, module_distances[module_id+1], 0.0) - numpy_pt_list.append([0, module_distances[module_id], 0.0, 0, module_distances[module_id+1], 0.0]) + pt_1 = self.gmsh_model.occ.addPoint( + 0, module_distances[module_id], 0.0 + ) + pt_2 = self.gmsh_model.occ.addPoint( + 0, module_distances[module_id + 1], 0.0 + ) + numpy_pt_list.append( + [ + 0, + module_distances[module_id], + 0.0, + 0, + module_distances[module_id + 1], + 0.0, + ] + ) torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) torque_tube_tag = (1, torque_tube_id) - embedded_lines_tag_list.append(torque_tube_tag) # for this panel row - - if modeling_torque_tube: # add the last connector + embedded_lines_tag_list.append( + torque_tube_tag + ) # for this panel row + + if self.modeling_torque_tube: # add the last connector last_standoff = self.gmsh_model.occ.addBox( - -params.pv_array.block_chord_div_by_panel_chord * half_chord, - module_distances[module_id+1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, - 0.0, - 2.0 * params.pv_array.block_chord_div_by_panel_chord * half_chord, - params.pv_array.block_chord_div_by_panel_chord * half_chord, - params.pv_array.torque_tube_separation, - ) + -params.pv_array.block_chord_div_by_panel_chord * half_chord, + module_distances[module_id + 1] + - params.pv_array.block_chord_div_by_panel_chord * half_chord, + 0.0, + 2.0 + * params.pv_array.block_chord_div_by_panel_chord + * half_chord, + params.pv_array.block_chord_div_by_panel_chord * half_chord, + params.pv_array.torque_tube_separation, + ) this_panel_tag_list.append((self.ndim, last_standoff)) panel_tag_list.append((self.ndim, last_standoff)) structure_connector_only_list.append((self.ndim, last_standoff)) - pt_1 = self.gmsh_model.occ.addPoint(0, module_distances[module_id+1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0) - pt_2 = self.gmsh_model.occ.addPoint(0, module_distances[module_id+1], 0.0) - numpy_pt_list.append([0, module_distances[module_id+1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, 0.0, 0, module_distances[module_id+1], 0.0]) + pt_1 = self.gmsh_model.occ.addPoint( + 0, + module_distances[module_id + 1] + - params.pv_array.block_chord_div_by_panel_chord * half_chord, + 0.0, + ) + pt_2 = self.gmsh_model.occ.addPoint( + 0, module_distances[module_id + 1], 0.0 + ) + numpy_pt_list.append( + [ + 0, + module_distances[module_id + 1] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord, + 0.0, + 0, + module_distances[module_id + 1], + 0.0, + ] + ) torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) torque_tube_tag = (1, torque_tube_id) embedded_lines_tag_list.append(torque_tube_tag) - + """ this is not used as we are using block surface to simulate the fixed bc applied by motor """ # # Add lines in the streamwise direction to mimic sections of panel held rigid by motor # if params.pv_array.span_fixation_pts is not None: @@ -375,7 +437,7 @@ def Rz(theta): this_panel_tag_list, embedded_lines_tag_list ) - for panel_tag in this_panel_tag_list: # 3d cell domain + for panel_tag in this_panel_tag_list: # 3d cell domain self.gmsh_model.occ.synchronize() # Get the list of 2D surfaces (surfaces) that make up this panel @@ -383,14 +445,20 @@ def Rz(theta): [panel_tag], oriented=False ) - vol_com = self.gmsh_model.occ.getCenterOfMass(self.ndim, panel_tag[1]) - if np.isclose(vol_com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness / 2.0): + vol_com = self.gmsh_model.occ.getCenterOfMass( + self.ndim, panel_tag[1] + ) + if np.isclose( + vol_com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness / 2.0, + ): target_key = f"modules" - elif np.isclose(vol_com[2], params.pv_array.torque_tube_separation / 2.0): + elif np.isclose( + vol_com[2], params.pv_array.torque_tube_separation / 2.0 + ): target_key = f"connectors" - - - + if target_key is not None: if target_key in this_panel_transformed_com: this_panel_transformed_com[target_key].append(vol_com) @@ -407,187 +475,385 @@ def Rz(theta): surface_located_or_not = False # sturctures tagging - if ( - np.isclose(com[0], -half_chord) - ): + if np.isclose(com[0], -half_chord): target_key = f"panel_left_{panel_ct:.0f}" surface_located_or_not = True + - if ( - np.isclose(com[0], half_chord) - ): + if np.isclose(com[0], half_chord): target_key = f"panel_right_{panel_ct:.0f}" surface_located_or_not = True + if ( np.isclose(com[1], module_distances[0]) - and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness / 2.0, + ) ): target_key = f"panel_front_{panel_ct:.0f}" surface_located_or_not = True + if ( - np.isclose(com[1], module_distances[params.pv_array.modules_per_span]) - and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) + np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span], + ) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness / 2.0, + ) ): target_key = f"panel_back_{panel_ct:.0f}" surface_located_or_not = True - for module_id in range(params.pv_array.modules_per_span): + + if not self.modeling_torque_tube: if ( - com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord - and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord - and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation) + np.isclose( + com[2], params.pv_array.torque_tube_separation + ) ): - target_key = f"panel_bottom_{panel_ct:.0f}_{module_id:.0f}" + target_key = ( + f"panel_bottom_{panel_ct:.0f}" + ) surface_located_or_not = True - break + if ( - com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord - and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord - and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness) + np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness, + ) ): - target_key = f"panel_top_{panel_ct:.0f}_{module_id:.0f}" + target_key = f"panel_top_{panel_ct:.0f}" surface_located_or_not = True - break - # if ( - # np.isclose( - # com[2], - # params.pv_array.torque_tube_separation - # + params.pv_array.panel_thickness - # ) - # ): - # target_key = f"panel_top_{panel_ct:.0f}" - # surface_located_or_not = True - - # mark the last block in each row - if ( - np.isclose(com[0], params.pv_array.block_chord_div_by_panel_chord * half_chord) - and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - ): - target_key = f"block_right_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" - surface_located_or_not = True - - if ( - np.isclose(com[0], -params.pv_array.block_chord_div_by_panel_chord * half_chord) - and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - ): - target_key = f"block_left_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" - surface_located_or_not = True - - if ( - np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord) - ): - target_key = f"block_front_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" - surface_located_or_not = True - - if ( - np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span]) - ): - target_key = f"block_back_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" - surface_located_or_not = True - - if ( - np.isclose(com[2], 0.0) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - ): - target_key = f"block_bottom_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" - surface_located_or_not = True - - if ( - np.isclose(com[2], params.pv_array.torque_tube_separation) and modeling_torque_tube and np.isclose(com[1], module_distances[params.pv_array.modules_per_span] - params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - ): - target_key = f"interior_surface_{panel_ct:.0f}" # block/panel interface - surface_located_or_not = True - - # if not the most left/right panel boundary, it is panel/panel interface - if not surface_located_or_not: + else: for module_id in range(params.pv_array.modules_per_span): - - # sturctures tagging - if ( - np.isclose(com[1], module_distances[module_id]) - and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) + com[1] + >= module_distances[module_id] + + module_span / 2.0 + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + and com[1] + <= module_distances[module_id] + + module_span / 2.0 + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + and np.isclose(com[0], 0) + and np.isclose( + com[2], params.pv_array.torque_tube_separation + ) ): - target_key = f"interior_surface_{panel_ct:.0f}" + target_key = ( + f"panel_bottom_{panel_ct:.0f}_{module_id:.0f}" + ) surface_located_or_not = True break if ( - np.isclose(com[1], module_distances[module_id+1]) - and np.isclose(com[0], 0) and np.isclose(com[2], params.pv_array.torque_tube_separation + params.pv_array.panel_thickness/2.0) + com[1] + >= module_distances[module_id] + + module_span / 2.0 + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + and com[1] + <= module_distances[module_id] + + module_span / 2.0 + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness, + ) ): - target_key = f"interior_surface_{panel_ct:.0f}" + target_key = f"panel_top_{panel_ct:.0f}_{module_id:.0f}" surface_located_or_not = True break + # if ( + # np.isclose( + # com[2], + # params.pv_array.torque_tube_separation + # + params.pv_array.panel_thickness + # ) + + # ): + # target_key = f"panel_top_{panel_ct:.0f}" + # surface_located_or_not = True + + # mark the last block in each row + if ( + np.isclose( + com[0], + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"block_right_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True - # if ( - # np.isclose(com[2], params.pv_array.torque_tube_separation) - # and np.isclose(com[0], 0.0) and com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 - # and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 - # ): - # target_key = f"panel_bottom_{panel_ct:.0f}" - # surface_located_or_not = True - # break - - if ( - (np.isclose(com[2], params.pv_array.torque_tube_separation) - and np.isclose(com[0], 0.0) and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - and modeling_torque_tube) - ): - target_key = f"interior_surface_{panel_ct:.0f}" - surface_located_or_not = True - break - - if ( - np.isclose(com[0], params.pv_array.block_chord_div_by_panel_chord * half_chord) - and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - ): - target_key = f"block_right_{panel_ct:.0f}_{module_id:.0f}" - surface_located_or_not = True - break + if ( + np.isclose( + com[0], + -params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"block_left_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True - if ( - np.isclose(com[0], -params.pv_array.block_chord_div_by_panel_chord * half_chord) - and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - ): - target_key = f"block_left_{panel_ct:.0f}_{module_id:.0f}" - surface_located_or_not = True - break + if ( + np.isclose( + com[2], params.pv_array.torque_tube_separation / 2.0 + ) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + ): + target_key = f"block_front_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True - if ( - np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[module_id]) - ): - target_key = f"block_front_{panel_ct:.0f}_{module_id:.0f}" - surface_located_or_not = True - break - - if ( - np.isclose(com[2], params.pv_array.torque_tube_separation/2.0) and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord) - ): - target_key = f"block_back_{panel_ct:.0f}_{module_id:.0f}" - surface_located_or_not = True - break - - if ( - np.isclose(com[2], 0.0) and modeling_torque_tube and np.isclose(com[1], module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0) - ): - target_key = f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" - surface_located_or_not = True - break + if ( + np.isclose( + com[2], params.pv_array.torque_tube_separation / 2.0 + ) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span], + ) + ): + target_key = f"block_back_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose(com[2], 0.0) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"block_bottom_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose(com[2], params.pv_array.torque_tube_separation) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"interior_surface_{panel_ct:.0f}" # block/panel interface + surface_located_or_not = True + + # if not the most left/right panel boundary, it is panel/panel interface + if not surface_located_or_not: + for module_id in range(params.pv_array.modules_per_span): + + # sturctures tagging + + if ( + np.isclose(com[1], module_distances[module_id]) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness / 2.0, + ) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[1], module_distances[module_id + 1]) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation + + params.pv_array.panel_thickness / 2.0, + ) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + # if ( + # np.isclose(com[2], params.pv_array.torque_tube_separation) + # and np.isclose(com[0], 0.0) and com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 + # and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 + # ): + # target_key = f"panel_bottom_{panel_ct:.0f}" + # surface_located_or_not = True + # break + + if ( + np.isclose( + com[2], params.pv_array.torque_tube_separation + ) + and np.isclose(com[0], 0.0) + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + and self.modeling_torque_tube + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose( + com[0], + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = ( + f"block_right_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose( + com[0], + -params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = ( + f"block_left_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose( + com[2], + params.pv_array.torque_tube_separation / 2.0, + ) + and self.modeling_torque_tube + and np.isclose(com[1], module_distances[module_id]) + ): + target_key = ( + f"block_front_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose( + com[2], + params.pv_array.torque_tube_separation / 2.0, + ) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + ): + target_key = ( + f"block_back_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose(com[2], 0.0) + and self.modeling_torque_tube + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = ( + f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break if not surface_located_or_not: target_key = f"trash_{panel_ct:.0f}" - + print('facet in trash') + if target_key is not None: if target_key in this_panel_transformed_com: this_panel_transformed_com[target_key].append(com) else: this_panel_transformed_com[target_key] = [com] - - for key, val in this_panel_transformed_com.items(): # all facets for each panel row. + + # print(this_panel_transformed_com[f"trash_{panel_ct:.0f}"]) + + for ( + key, + val, + ) in ( + this_panel_transformed_com.items() + ): # all facets for each panel row. for row_num, com in enumerate(val): com_array = np.array(com) @@ -608,7 +874,7 @@ def Rz(theta): if key in transformed_com: transformed_com[key].append(com_array) else: - transformed_com[key] = [com_array] # including all rows + transformed_com[key] = [com_array] # including all rows # actually this is panel array count panel_ct += 1 @@ -678,7 +944,7 @@ def Rz(theta): ) else: self.numpy_pt_total_array = np.copy(numpy_pt_panel_array) - + # Fragment all panels from the overall domain self.gmsh_model.occ.fragment(domain_tag_list, panel_tag_list) @@ -690,7 +956,7 @@ def Rz(theta): # print(transformed_com) # exit() - + # for all panels # Loop over all the finalized surfaces after fragmentation and tag everything all_surf_tag_list = self.gmsh_model.occ.getEntities(self.ndim - 1) @@ -727,6 +993,7 @@ def Rz(theta): if np.allclose(np.array(com), target_com): located_this_surface = True if "trash" not in key: + print(key) self._add_to_domain_markers(key, [surf_id], "facet") if not located_this_surface: @@ -755,7 +1022,6 @@ def Rz(theta): ) if this_surf_bbox[2] > params.domain.z_max: raise ValueError(f"A panel extends past the z_max wall.") - # mark the panel and connector volumes all_vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) @@ -767,12 +1033,12 @@ def Rz(theta): located_this_volume = False for key, val in transformed_com.items(): - for target_com in val: - # print(target_com) - if np.allclose(np.array(com), target_com): - located_this_volume = True - if "trash" not in key: - self._add_to_domain_markers(key, [vol_id], "cell") + for target_com in val: + # print(target_com) + if np.allclose(np.array(com), target_com): + located_this_volume = True + if "trash" not in key: + self._add_to_domain_markers(key, [vol_id], "cell") # Mark the fluid volume # Volumes are the entities with dimension equal to the mesh dimension @@ -798,7 +1064,7 @@ def Rz(theta): structure_panel_only_list = [] structure_connector_only_list = [] -# + # # # self._add_to_domain_markers("structure", structure_vol_list, "cell") # for individual_vol_id in structure_vol_list: # self._add_to_domain_markers(f"structure_{individual_vol_id:03.0f}", [individual_vol_id], "cell") @@ -1474,7 +1740,7 @@ def set_length_scales(self, params, domain_markers): internal_surface_tags = [] for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): - + internal_surface_tags.extend( domain_markers[f"panel_left_{panel_id}"]["gmsh_tags"] ) @@ -1488,18 +1754,26 @@ def set_length_scales(self, params, domain_markers): domain_markers[f"panel_back_{panel_id}"]["gmsh_tags"] ) - for module_id in range(params.pv_array.modules_per_span): + if self.modeling_torque_tube: + for module_id in range(params.pv_array.modules_per_span): + internal_surface_tags.extend( + domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"][ + "gmsh_tags" + ] + ) + internal_surface_tags.extend( + domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"][ + "gmsh_tags" + ] + ) + else: internal_surface_tags.extend( - domain_markers[ - f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" - ]["gmsh_tags"] - ) + domain_markers[f"panel_bottom_{panel_id}"]["gmsh_tags"] + ) internal_surface_tags.extend( - domain_markers[ - f"panel_top_{panel_id:.0f}_{module_id:.0f}" - ]["gmsh_tags"] - ) - + domain_markers[f"panel_top_{panel_id}"]["gmsh_tags"] + ) + # print(internal_surface_tags) # print(len(internal_surface_tags)) # exit() diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index e585054..e6cc636 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -90,146 +90,245 @@ def build_boundary_conditions(self, domain, params): def calculate_K_for_Robin_BC(self, domain, flow, params): # shear modulus of tube - Gt = params.structure.elasticity_modulus_tube / (2 * (1 + params.structure.poissons_ratio_tube)) + Gt = params.structure.elasticity_modulus_tube / ( + 2 * (1 + params.structure.poissons_ratio_tube) + ) - Ipt = np.pi/2*(params.pv_array.torque_tube_outer_radius**4 - params.pv_array.torque_tube_inner_radius**4) + Ipt = ( + np.pi + / 2 + * ( + params.pv_array.torque_tube_outer_radius**4 + - params.pv_array.torque_tube_inner_radius**4 + ) + ) - Gp = params.structure.elasticity_modulus / (2 * (1 + params.structure.poissons_ratio)) + Gp = params.structure.elasticity_modulus / ( + 2 * (1 + params.structure.poissons_ratio) + ) - Ipp = 1/3.0*params.pv_array.panel_thickness * params.pv_array.panel_chord**3 + Ipp = 1 / 3.0 * params.pv_array.panel_thickness * params.pv_array.panel_chord**3 # total number of pv rows total_num_panels = params.pv_array.stream_rows * params.pv_array.span_rows for panel_id in range(total_num_panels): # construct the matrix, size: modules_per_span+1 by modules_per_span+1 - theo_matrix = np.zeros((params.pv_array.modules_per_span+1, params.pv_array.modules_per_span+1)) - theo_vector = np.zeros(params.pv_array.modules_per_span+1) + theo_matrix = np.zeros( + ( + params.pv_array.modules_per_span + 1, + params.pv_array.modules_per_span + 1, + ) + ) + theo_vector = np.zeros(params.pv_array.modules_per_span + 1) connector_locations = np.linspace( 0, params.pv_array.panel_span, params.pv_array.modules_per_span + 1 ) - # ?? double integral is not available until the second time step. Should make it be zero for the - # first time step then it will be updated once the first step is finished? but the assemble is out of + # ?? double integral is not available until the second time step. Should make it be zero for the + # first time step then it will be updated once the first step is finished? but the assemble is out of # the time loop. Should we solve fluid then structure? - + total_torque_on_this_panel_name = f"total_torque_panel_{panel_id:.0f}" - total_torque_on_this_panel = getattr(flow, total_torque_on_this_panel_name).value + total_torque_on_this_panel = getattr( + flow, total_torque_on_this_panel_name + ).value - theo_matrix[params.pv_array.modules_per_span, :] = 1/Gp/Ipp - theo_vector[params.pv_array.modules_per_span] = -total_torque_on_this_panel/Gp/Ipp + theo_matrix[params.pv_array.modules_per_span, :] = 1 / Gp / Ipp + theo_vector[params.pv_array.modules_per_span] = ( + -total_torque_on_this_panel / Gp / Ipp + ) # contribution from panel: - # contribution from C0 to matrix: + # contribution from C0 to matrix: - theo_matrix[:params.pv_array.modules_per_span, :params.pv_array.fixed_location] += connector_locations[params.pv_array.fixed_location]/(Gp*Ipp) - theo_matrix[:params.pv_array.modules_per_span, 1:params.pv_array.fixed_location] -= connector_locations[1:params.pv_array.fixed_location]/(Gp*Ipp) + theo_matrix[ + : params.pv_array.modules_per_span, : params.pv_array.fixed_location + ] += connector_locations[params.pv_array.fixed_location] / (Gp * Ipp) + theo_matrix[ + : params.pv_array.modules_per_span, 1 : params.pv_array.fixed_location + ] -= connector_locations[1 : params.pv_array.fixed_location] / (Gp * Ipp) - # contribution from C0 to vector: + # contribution from C0 to vector: T_double_integral_at_fixed_location_name = f"double_integral_total_torque_panel_{panel_id:.0f}_{params.pv_array.fixed_location:.0f}" - T_double_integral_at_fixed_location = getattr(flow, T_double_integral_at_fixed_location_name).value - theo_vector[:params.pv_array.modules_per_span] += -T_double_integral_at_fixed_location/Gp/Ipp - - # contribution of panels to matrix - theo_matrix[0, 0] += -connector_locations[0]/(Gp*Ipp) - theo_matrix[1, 0] += -connector_locations[1]/(Gp*Ipp) + T_double_integral_at_fixed_location = getattr( + flow, T_double_integral_at_fixed_location_name + ).value + theo_vector[: params.pv_array.modules_per_span] += ( + -T_double_integral_at_fixed_location / Gp / Ipp + ) + + # contribution of panels to matrix + theo_matrix[0, 0] += -connector_locations[0] / (Gp * Ipp) + theo_matrix[1, 0] += -connector_locations[1] / (Gp * Ipp) for i in range(2, params.pv_array.fixed_location): - theo_matrix[i, :i] += -connector_locations[i]/(Gp*Ipp) - theo_matrix[i, 1:i] += connector_locations[1:i]/(Gp*Ipp) + theo_matrix[i, :i] += -connector_locations[i] / (Gp * Ipp) + theo_matrix[i, 1:i] += connector_locations[1:i] / (Gp * Ipp) - for i in range(params.pv_array.fixed_location, params.pv_array.modules_per_span): - theo_matrix[i, :i+1] += -connector_locations[i+1]/(Gp*Ipp) - theo_matrix[i, 1:i+1] += connector_locations[1:i+1]/(Gp*Ipp) + for i in range( + params.pv_array.fixed_location, params.pv_array.modules_per_span + ): + theo_matrix[i, : i + 1] += -connector_locations[i + 1] / (Gp * Ipp) + theo_matrix[i, 1 : i + 1] += connector_locations[1 : i + 1] / (Gp * Ipp) # contribution of panels to vector T_double_integral_array = [] - for i in range(params.pv_array.modules_per_span+1): + for i in range(params.pv_array.modules_per_span + 1): name = f"double_integral_total_torque_panel_{panel_id:.0f}_{i:.0f}" T_double_integral_array.append(getattr(flow, name).value) T_double_integral_array = np.array(T_double_integral_array) - theo_vector[:params.pv_array.modules_per_span] += np.delete(T_double_integral_array, params.pv_array.fixed_location)/Gp/Ipp + theo_vector[: params.pv_array.modules_per_span] += ( + np.delete(T_double_integral_array, params.pv_array.fixed_location) + / Gp + / Ipp + ) # contribution from tube: - # contribution from C0 to matrix: - - theo_matrix[:params.pv_array.modules_per_span, params.pv_array.fixed_location:params.pv_array.modules_per_span+1] += -connector_locations[params.pv_array.fixed_location]/(Gt*Ipt) - theo_matrix[:params.pv_array.modules_per_span, 1:params.pv_array.fixed_location] += -connector_locations[1:params.pv_array.fixed_location]/(Gt*Ipt) - - # contribution from C0 to vector: - theo_vector[:params.pv_array.modules_per_span] += total_torque_on_this_panel*connector_locations[params.pv_array.fixed_location]/Gt/Ipt + # contribution from C0 to matrix: + + theo_matrix[ + : params.pv_array.modules_per_span, + params.pv_array.fixed_location : params.pv_array.modules_per_span + 1, + ] += -connector_locations[params.pv_array.fixed_location] / (Gt * Ipt) + theo_matrix[ + : params.pv_array.modules_per_span, 1 : params.pv_array.fixed_location + ] += -connector_locations[1 : params.pv_array.fixed_location] / (Gt * Ipt) + + # contribution from C0 to vector: + theo_vector[: params.pv_array.modules_per_span] += ( + total_torque_on_this_panel + * connector_locations[params.pv_array.fixed_location] + / Gt + / Ipt + ) - # contribution of tube to matrix - theo_matrix[0, 1:params.pv_array.modules_per_span+1] += connector_locations[0]/(Gt*Ipt) - theo_matrix[1, 1:params.pv_array.modules_per_span+1] += connector_locations[1]/(Gt*Ipt) + # contribution of tube to matrix + theo_matrix[ + 0, 1 : params.pv_array.modules_per_span + 1 + ] += connector_locations[0] / (Gt * Ipt) + theo_matrix[ + 1, 1 : params.pv_array.modules_per_span + 1 + ] += connector_locations[1] / (Gt * Ipt) for i in range(2, params.pv_array.fixed_location): - theo_matrix[i, i:params.pv_array.modules_per_span+1] += connector_locations[i]/(Gt*Ipt) - theo_matrix[i, 1:i] += connector_locations[1:i]/(Gt*Ipt) - - for i in range(params.pv_array.fixed_location, params.pv_array.modules_per_span): - theo_matrix[i, i+1:params.pv_array.modules_per_span+1] += connector_locations[i+1]/(Gt*Ipt) - theo_matrix[i, 1:i+1] += connector_locations[1:i+1]/(Gt*Ipt) + theo_matrix[ + i, i : params.pv_array.modules_per_span + 1 + ] += connector_locations[i] / (Gt * Ipt) + theo_matrix[i, 1:i] += connector_locations[1:i] / (Gt * Ipt) + + for i in range( + params.pv_array.fixed_location, params.pv_array.modules_per_span + ): + theo_matrix[ + i, i + 1 : params.pv_array.modules_per_span + 1 + ] += connector_locations[i + 1] / (Gt * Ipt) + theo_matrix[i, 1 : i + 1] += connector_locations[1 : i + 1] / (Gt * Ipt) # contribution of tube to vector - theo_vector[:params.pv_array.fixed_location] += -total_torque_on_this_panel/Gt/Ipt*connector_locations[:params.pv_array.fixed_location] - theo_vector[params.pv_array.fixed_location:params.pv_array.modules_per_span] += -total_torque_on_this_panel/Gt/Ipt*connector_locations[params.pv_array.fixed_location] + theo_vector[: params.pv_array.fixed_location] += ( + -total_torque_on_this_panel + / Gt + / Ipt + * connector_locations[: params.pv_array.fixed_location] + ) + theo_vector[ + params.pv_array.fixed_location : params.pv_array.modules_per_span + ] += ( + -total_torque_on_this_panel + / Gt + / Ipt + * connector_locations[params.pv_array.fixed_location] + ) + + R_reaction_torque = np.dot( + np.linalg.inv(theo_matrix), theo_vector + ) # this is the torque applied to tube - R_reaction_torque = np.dot(np.linalg.inv(theo_matrix), theo_vector) # this is the torque applied to tube - # rotation of tube at each connector location - tube_rotate_matrix = np.zeros((params.pv_array.modules_per_span, params.pv_array.modules_per_span+1)) + tube_rotate_matrix = np.zeros( + (params.pv_array.modules_per_span, params.pv_array.modules_per_span + 1) + ) tube_rotate_vector = np.zeros(params.pv_array.modules_per_span) # contribution from panel: - # contribution from C0 to matrix: + # contribution from C0 to matrix: - tube_rotate_matrix[:params.pv_array.modules_per_span, :params.pv_array.fixed_location] += connector_locations[params.pv_array.fixed_location]/(Gp*Ipp) - tube_rotate_matrix[:params.pv_array.modules_per_span, 1:params.pv_array.fixed_location] -= connector_locations[1:params.pv_array.fixed_location]/(Gp*Ipp) - - # contribution from C0 to vector: - tube_rotate_vector[:params.pv_array.modules_per_span] += T_double_integral_array[params.pv_array.fixed_location]/Gp/Ipp + tube_rotate_matrix[ + : params.pv_array.modules_per_span, : params.pv_array.fixed_location + ] += connector_locations[params.pv_array.fixed_location] / (Gp * Ipp) + tube_rotate_matrix[ + : params.pv_array.modules_per_span, 1 : params.pv_array.fixed_location + ] -= connector_locations[1 : params.pv_array.fixed_location] / (Gp * Ipp) + # contribution from C0 to vector: + tube_rotate_vector[: params.pv_array.modules_per_span] += ( + T_double_integral_array[params.pv_array.fixed_location] / Gp / Ipp + ) - # contribution of panels to matrix - tube_rotate_matrix[0, 0] += -connector_locations[0]/(Gp*Ipp) - tube_rotate_matrix[1, 0] += -connector_locations[1]/(Gp*Ipp) + # contribution of panels to matrix + tube_rotate_matrix[0, 0] += -connector_locations[0] / (Gp * Ipp) + tube_rotate_matrix[1, 0] += -connector_locations[1] / (Gp * Ipp) for i in range(2, params.pv_array.fixed_location): - tube_rotate_matrix[i, :i] += -connector_locations[i]/(Gp*Ipp) - tube_rotate_matrix[i, 1:i] += connector_locations[1:i]/(Gp*Ipp) - - for i in range(params.pv_array.fixed_location, params.pv_array.modules_per_span): - tube_rotate_matrix[i, :i+1] += -connector_locations[i+1]/(Gp*Ipp) - tube_rotate_matrix[i, 1:i+1] += connector_locations[1:i+1]/(Gp*Ipp) + tube_rotate_matrix[i, :i] += -connector_locations[i] / (Gp * Ipp) + tube_rotate_matrix[i, 1:i] += connector_locations[1:i] / (Gp * Ipp) + + for i in range( + params.pv_array.fixed_location, params.pv_array.modules_per_span + ): + tube_rotate_matrix[i, : i + 1] += -connector_locations[i + 1] / ( + Gp * Ipp + ) + tube_rotate_matrix[i, 1 : i + 1] += connector_locations[1 : i + 1] / ( + Gp * Ipp + ) # contribution of panels to vector - tube_rotate_vector[:params.pv_array.modules_per_span] += -np.delete(T_double_integral_array, params.pv_array.fixed_location)/Gp/Ipp - + tube_rotate_vector[: params.pv_array.modules_per_span] += ( + -np.delete(T_double_integral_array, params.pv_array.fixed_location) + / Gp + / Ipp + ) phi = np.dot(tube_rotate_matrix, R_reaction_torque) + tube_rotate_vector if isinstance(params.pv_array.tracker_angle, list): - if panel_id == 0: - assert ( - len(params.pv_array.tracker_angle) - == params.pv_array.stream_rows * params.pv_array.span_rows - ), f"Length of tracker angle list ({len(params.pv_array.tracker_angle)}) not equal to total number of PV tables ({params.pv_array.stream_rows * params.pv_array.span_rows})." - - tracker_angle_rad = np.radians( - params.pv_array.tracker_angle[panel_id] - ) + if panel_id == 0: + assert ( + len(params.pv_array.tracker_angle) + == params.pv_array.stream_rows * params.pv_array.span_rows + ), f"Length of tracker angle list ({len(params.pv_array.tracker_angle)}) not equal to total number of PV tables ({params.pv_array.stream_rows * params.pv_array.span_rows})." + + tracker_angle_rad = np.radians(params.pv_array.tracker_angle[panel_id]) else: tracker_angle_rad = np.radians(params.pv_array.tracker_angle) if np.linalg.norm(phi) == 0: K = np.zeros(params.pv_array.modules_per_span) else: - K = np.abs(12*(np.delete(R_reaction_torque, params.pv_array.fixed_location))/((params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord)**3)/np.cos(tracker_angle_rad+phi)/(np.sin(tracker_angle_rad+phi)-np.sin(tracker_angle_rad))/(params.pv_array.block_chord_div_by_panel_chord * params.pv_array.panel_chord/2)) + K = np.abs( + 12 + * (np.delete(R_reaction_torque, params.pv_array.fixed_location)) + / ( + ( + params.pv_array.block_chord_div_by_panel_chord + * params.pv_array.panel_chord + ) + ** 3 + ) + / np.cos(tracker_angle_rad + phi) + / (np.sin(tracker_angle_rad + phi) - np.sin(tracker_angle_rad)) + / ( + params.pv_array.block_chord_div_by_panel_chord + * params.pv_array.panel_chord + / 2 + ) + ) # assume K is 0 at the fixed connector K = np.flip(np.insert(K, params.pv_array.fixed_location, 0)) @@ -237,9 +336,9 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # K[0] is the stiffness at the most back connector (highest y) # K[10] is the stiffness at the most front connector (lowest y) # for block_bot_surface, it is numbered from lowest y to highest y, so flip K - + # if there are 10 modules per array, there are 11 connectors, K has shape of 10, the K at the fixed connector is not included. - for i in range(params.pv_array.modules_per_span+1): + for i in range(params.pv_array.modules_per_span + 1): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" setattr(self, name_K, K[i]) @@ -252,11 +351,11 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # for panel_id in range(total_num_panels): # total_torque_right_fixed = 0 # total_torque_left_fixed = 0 - + # for i in range(num_panel_left_fixed, params.pv_array.modules_per_span): # name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" # total_torque_right_fixed += getattr(flow, name) - + # for i in range(num_panel_left_fixed): # name = f"total_torque_panel_{panel_id:.0f}_{i:.0f}" # total_torque_left_fixed += getattr(flow, name) @@ -267,7 +366,7 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # theo_matrix_right_fixed[0, :] = 1.0 # theo_vector_right_fixed[0] = total_torque_right_fixed # theo_matrix_right_fixed[1:, :-1] = params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt - + # # as the double integral of torque along the span from front to back, left fixed part to right fixed part, it need to be flipped # T_double_integral_right_fixed = [] # T_right_fixed = [] @@ -283,19 +382,18 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # for j in range(i): # theo_matrix_right_fixed[i+1, :-1-j-1] += params.pv_array.panel_span/params.pv_array.modules_per_span/(Gt*Ipt) - # for i in range(num_panel_right_fixed): # for j in range(i+1): # theo_matrix_right_fixed[i+1, num_panel_right_fixed-j:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gp/Ipp # R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_right_fixed), theo_vector_right_fixed) - + # tube_rotate_matrix = np.ones((num_panel_right_fixed, num_panel_right_fixed))*params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) # for i in range(num_panel_right_fixed): # for j in range(i): # tube_rotate_matrix[i, :num_panel_right_fixed-1-j] += params.pv_array.panel_span/params.pv_array.modules_per_span*(1.0/Gt/Ipt) - + # # check the standalone code, why it need to be flipped. # phi = np.flip(np.dot(tube_rotate_matrix, R_reaction_torque[:-1])) @@ -340,14 +438,13 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # for j in range(i): # theo_vector_left_fixed[i+1] += (i-j)*T_left_fixed[j]/(Gp*Ipp)*params.pv_array.panel_span/params.pv_array.modules_per_span - # for i in range(num_panel_left_fixed): + # for i in range(num_panel_left_fixed): # theo_matrix_left_fixed[i+1:, i+1:] -= params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt # R_reaction_torque = np.dot(np.linalg.inv(theo_matrix_left_fixed), theo_vector_left_fixed) # this is the torque applied to tube - # tube_rotate_matrix = np.zeros((num_panel_left_fixed, num_panel_left_fixed)) - # for i in range(num_panel_left_fixed): + # for i in range(num_panel_left_fixed): # tube_rotate_matrix[i:, i:] += params.pv_array.panel_span/params.pv_array.modules_per_span/Gt/Ipt # phi = np.dot(tube_rotate_matrix, R_reaction_torque[1:]) @@ -355,7 +452,6 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # # rotation of connector # x_panel = np.arange(num_panel_left_fixed)*(params.pv_array.panel_span/params.pv_array.modules_per_span)+params.pv_array.panel_span/params.pv_array.modules_per_span # location of panles - # for i in range(num_panel_left_fixed): # phi_i = phi[i] @@ -363,7 +459,7 @@ def calculate_K_for_Robin_BC(self, domain, flow, params): # K_i = 12*(R_reaction_torque[i+1])/((block_length)**3)/np.cos(array_rotation_rad+phi_i)/(np.sin(array_rotation_rad+phi_i)-np.sin(array_rotation_rad))/block_width # name_K = f"spring_stiffness_{panel_id:.0f}_{num_panel_left_fixed-1-i:.0f}" - + # setattr(self, name_K, K_i) def update_a(self, u, u_old, v_old, a_old, dt, beta, ufl=True): @@ -491,7 +587,7 @@ def c(u, u_): def k_nominal(u, u_): return ufl.inner(P_(u), ufl.grad(u_)) - + def k_nominal_connector(u, u_): return ufl.inner(P_connector(u), ufl.grad(u_)) @@ -523,7 +619,7 @@ def S_(u): S_svk = structure.lame_lambda * ufl.tr(E) * I + 2.0 * structure.lame_mu * E return S_svk - + # The second Piola–Kirchhoff stress, S def S_connector(u): E = E_(u) @@ -532,7 +628,10 @@ def S_connector(u): # return lamda * ufl.tr(E) * I + 2.0 * mu * (E - ufl.tr(E) * I / 3.0) # TODO: Why does the above form give a better result and where does it come from? - S_svk = structure.lame_lambda_connector * ufl.tr(E) * I + 2.0 * structure.lame_mu_connector * E + S_svk = ( + structure.lame_lambda_connector * ufl.tr(E) * I + + 2.0 * structure.lame_mu_connector * E + ) return S_svk # The first Piola–Kirchhoff stress tensor, P = F*S @@ -541,7 +640,7 @@ def P_(u): S = S_(u) # return ufl.inv(F) * S return F * S - + def P_connector(u): F = F_(u) S = S_connector(u) @@ -602,34 +701,45 @@ def P_connector(u): F = ufl.grad(self.u) + ufl.Identity(len(self.u)) J = ufl.det(F) - self.z_unit_vector = dolfinx.fem.Constant(domain.structure.msh, [0.0,0.0,1.0]) # surface traction, N/m^2 - + self.z_unit_vector = dolfinx.fem.Constant( + domain.structure.msh, [0.0, 0.0, 1.0] + ) # surface traction, N/m^2 + self.calculate_K_for_Robin_BC(domain, flow, params) dx_structure = ufl.Measure( "dx", domain=domain.structure.msh, subdomain_data=domain.structure.cell_tags ) - # To Do: how to differentiate the connector part and the panel part, dx_connector and dx_panel self.res = ( m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure - + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure(domain.domain_markers["modules"]["idx"]) - + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure(domain.domain_markers["connectors"]["idx"]) - - structure.rho * ufl.inner(self.f, self.u_) * dx_structure(domain.domain_markers["modules"]["idx"]) - - structure.rho_connector * ufl.inner(self.f, self.u_) * dx_structure(domain.domain_markers["connectors"]["idx"]) + + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) + * dx_structure(domain.domain_markers["modules"]["idx"]) + + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) + * dx_structure(domain.domain_markers["connectors"]["idx"]) + - structure.rho + * ufl.inner(self.f, self.u_) + * dx_structure(domain.domain_markers["modules"]["idx"]) + - structure.rho_connector + * ufl.inner(self.f, self.u_) + * dx_structure(domain.domain_markers["connectors"]["idx"]) - ufl.dot(ufl.dot(self.stress_predicted * J * ufl.inv(F.T), n), self.u_) * self.ds ) # - Wext(self.u) # Robin boundary condition terms for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): - for i in range(params.pv_array.modules_per_span+1): + for i in range(params.pv_array.modules_per_span + 1): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" - K_springs = dolfinx.fem.Constant(domain.structure.msh, float(getattr(self, name_K))) - self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector)*self.ds(domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]["idx"]) - + K_springs = dolfinx.fem.Constant( + domain.structure.msh, float(getattr(self, name_K)) + ) + self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector) * self.ds( + domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]["idx"] + ) + # self.a = dolfinx.fem.form(ufl.lhs(res)) # self.L = dolfinx.fem.form(ufl.rhs(res)) diff --git a/pvade/structure/StructureMain.py b/pvade/structure/StructureMain.py index 8f81ea2..3ca6d2f 100644 --- a/pvade/structure/StructureMain.py +++ b/pvade/structure/StructureMain.py @@ -93,15 +93,20 @@ def __init__(self, domain, params): * self.poissons_ratio / ((1.0 + self.poissons_ratio) * (1.0 - 2.0 * self.poissons_ratio)) ) - + # we are assuming the connectors has same mechanical properties as the tubes self.E_connector = params.structure.elasticity_modulus_tube # 1.0e9 self.poissons_ratio_connector = params.structure.poissons_ratio_tube # 0.3 - self.lame_mu_connector = self.E_connector / (2.0 * (1.0 + self.poissons_ratio_connector)) + self.lame_mu_connector = self.E_connector / ( + 2.0 * (1.0 + self.poissons_ratio_connector) + ) self.lame_lambda_connector = ( self.E_connector * self.poissons_ratio_connector - / ((1.0 + self.poissons_ratio_connector) * (1.0 - 2.0 * self.poissons_ratio_connector)) + / ( + (1.0 + self.poissons_ratio_connector) + * (1.0 - 2.0 * self.poissons_ratio_connector) + ) ) if self.rank == 0: diff --git a/pvade/structure/boundary_conditions.py b/pvade/structure/boundary_conditions.py index 956b857..268458e 100644 --- a/pvade/structure/boundary_conditions.py +++ b/pvade/structure/boundary_conditions.py @@ -272,7 +272,7 @@ def build_structure_boundary_conditions(domain, params, functionspace): total_num_panels = params.pv_array.stream_rows * params.pv_array.span_rows for num_panel in range(total_num_panels): - for location in params.structure.bc_list: # it is empty + for location in params.structure.bc_list: # it is empty location_panel = f"{location}_{num_panel}" # f"front_{num_panel}" , f"back_{num_panel}": # for location in [f"left_{num_panel}"]:# , f"right_{num_panel}": @@ -362,13 +362,17 @@ def connection_point_up_helper(nodes_to_pin_between): if params.structure.motor_connection == True: # The bottom surface of the center connector is fixed to represent the motor mount - motor_location = params.pv_array.fixed_location # fixed location along the span, if fixed_location=5, it means the left boundary of the 6th connector is fixed + motor_location = ( + params.pv_array.fixed_location + ) # fixed location along the span, if fixed_location=5, it means the left boundary of the 6th connector is fixed for panel_id in range(total_num_panels): mount_facet = domain.structure.facet_tags.find( - domain.domain_markers[f"block_left_{panel_id:.0f}_{motor_location:.0f}"]["idx"] - ) - + domain.domain_markers[ + f"block_left_{panel_id:.0f}_{motor_location:.0f}" + ]["idx"] + ) + dofs_disp = dolfinx.fem.locate_dofs_topological( functionspace, 2, [mount_facet] ) diff --git a/pvade/tests/input/yaml/sim_params.yaml b/pvade/tests/input/yaml/sim_params.yaml index 2e63349..6427e3c 100644 --- a/pvade/tests/input/yaml/sim_params.yaml +++ b/pvade/tests/input/yaml/sim_params.yaml @@ -14,7 +14,7 @@ domain: z_max: 20 l_char: 20 pv_array: - stream_rows: 7 + stream_rows: 2 span_rows: 1 elevation: 1.45 # 1.5 - 0.5*0.1 = 1.4 stream_spacing: 7.0 @@ -22,6 +22,15 @@ pv_array: panel_span: 7.0 panel_thickness: 0.1 tracker_angle: -30.0 + # wind_direction: 15.0 + span_fixation_pts: [13.2] + # torque_tube_separation: 0.2 # gap between panel and tube center + # torque_tube_outer_radius: 0.1 # radius of the torque tube + # torque_tube_inner_radius: 0.09 # radius of the torque tube + # modules_per_span: 10 + # fixed_location: 5 # fixed location of the panel along the span (from 0 (fixed at left) to modules_per_span (fixed at right)) + # block_chord_div_by_panel_chord: 0.02 + solver: dt: 0.005 t_final: 0.05 @@ -43,3 +52,19 @@ fluid: bc_y_min: slip # slip noslip free bc_z_max: slip # slip noslip free bc_z_min: noslip # slip noslip free + wind_direction: 15.0 +structure: + dt : 0.01 + rho: 124.0 + poissons_ratio: 0.3 + elasticity_modulus: 4.0e+09 + body_force_x: 0.0 + body_force_y: 0.0 + body_force_z: 0.0 + bc_list: [] + motor_connection: true + tube_connection: true + beta_relaxation: 0.5 + elasticity_modulus_tube: 2.0e+11 + poissons_ratio_tube: 0.3 + rho_tube: 7800.0 \ No newline at end of file diff --git a/pvade/tests/test_fsi_mesh.py b/pvade/tests/test_fsi_mesh.py index eba906e..d722147 100644 --- a/pvade/tests/test_fsi_mesh.py +++ b/pvade/tests/test_fsi_mesh.py @@ -109,9 +109,16 @@ def test_meshing_3dpanels_rotations(wind_direction, num_stream_rows, num_span_ro params.domain.y_min = -30.0 params.domain.y_max = 30.0 + # x_min: -10 + # x_max: 50 + # y_min: -20 + # y_max: 27 + # z_min: 0 + # z_max: 20 + params.pv_array.stream_rows = num_stream_rows params.pv_array.span_rows = num_span_rows - params.pv_array.span_spacing = 15.0 + params.pv_array.span_spacing = 7 params.pv_array.tracker_angle = list( np.linspace(-52.0, 52.0, num_stream_rows * num_span_rows) ) diff --git a/pvade_main.py b/pvade_main.py index 5e52d56..6d759ec 100644 --- a/pvade_main.py +++ b/pvade_main.py @@ -44,7 +44,7 @@ def main(input_file=None): domain.read_mesh_files(params.general.input_mesh_dir, params) else: domain.build(params) - + exit() # If we only want to create the mesh, we can stop here if params.general.mesh_only: list_timings(params.comm, [TimingType.wall]) From 95c6dc060f46815bcfba96f6f1db89664754c2e3 Mon Sep 17 00:00:00 2001 From: xinhe2205 Date: Thu, 22 Jan 2026 11:57:22 -0700 Subject: [PATCH 28/37] fix facet marker for original geometry --- pvade/geometry/panels3d/DomainCreation.py | 4 ++-- pvade/tests/input/yaml/sim_params.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 9b7c1c1..8a345d7 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -269,7 +269,7 @@ def Rz(theta): this_module = self.gmsh_model.occ.addBox( -half_chord, module_distances[module_id], - -half_thickness, + 0.0, params.pv_array.panel_chord, module_span, params.pv_array.panel_thickness, @@ -993,7 +993,7 @@ def Rz(theta): if np.allclose(np.array(com), target_com): located_this_surface = True if "trash" not in key: - print(key) + # print(key) self._add_to_domain_markers(key, [surf_id], "facet") if not located_this_surface: diff --git a/pvade/tests/input/yaml/sim_params.yaml b/pvade/tests/input/yaml/sim_params.yaml index 6427e3c..268225e 100644 --- a/pvade/tests/input/yaml/sim_params.yaml +++ b/pvade/tests/input/yaml/sim_params.yaml @@ -14,8 +14,8 @@ domain: z_max: 20 l_char: 20 pv_array: - stream_rows: 2 - span_rows: 1 + stream_rows: 3 + span_rows: 2 elevation: 1.45 # 1.5 - 0.5*0.1 = 1.4 stream_spacing: 7.0 panel_chord: 2.0 @@ -52,7 +52,7 @@ fluid: bc_y_min: slip # slip noslip free bc_z_max: slip # slip noslip free bc_z_min: noslip # slip noslip free - wind_direction: 15.0 + wind_direction: 105.0 structure: dt : 0.01 rho: 124.0 From f0298eebd0303c9c708d938799cffb8cf9f71521 Mon Sep 17 00:00:00 2001 From: xinhe2205 Date: Mon, 2 Feb 2026 09:15:37 -0700 Subject: [PATCH 29/37] add back the BCs for case without Robin BC and move the structure down by half panel thickness --- input/duramat_case_study.yaml | 8 +- pvade/IO/input_schema.yaml | 2 +- pvade/fluid/FlowManager.py | 43 +- pvade/fluid/boundary_conditions.py | 38 +- pvade/geometry/MeshManager.py | 25 +- pvade/geometry/flag2d/DomainCreation.py | 8 +- pvade/geometry/panels3d/DomainCreation.py | 1243 ++++++++++++----- pvade/structure/ElasticityAnalysis.py | 75 +- pvade/structure/boundary_conditions.py | 88 +- .../input/mesh/panels3d/domain_markers.yaml | 696 ++------- pvade/tests/input/mesh/panels3d/fluid_mesh.h5 | Bin 365824 -> 204608 bytes .../tests/input/mesh/panels3d/fluid_mesh.xdmf | 18 +- .../mesh/panels3d/numpy_fixation_points.csv | 28 - .../input/mesh/panels3d/structure_mesh.h5 | Bin 93580 -> 39292 bytes .../input/mesh/panels3d/structure_mesh.xdmf | 18 +- pvade/tests/input/yaml/embedded_box.yaml | 4 +- pvade/tests/input/yaml/flag2d.yaml | 4 +- pvade/tests/input/yaml/sim_params.yaml | 33 +- pvade/tests/test_fsi_mesh.py | 21 +- pvade/tests/test_input_files.py | 2 +- pvade/tests/test_mesh_movement.py | 2 +- pvade/tests/test_solve.py | 20 +- pvade_main.py | 3 +- 23 files changed, 1249 insertions(+), 1130 deletions(-) diff --git a/input/duramat_case_study.yaml b/input/duramat_case_study.yaml index 1f40a19..b48d5b7 100644 --- a/input/duramat_case_study.yaml +++ b/input/duramat_case_study.yaml @@ -23,10 +23,10 @@ pv_array: elevation: 2.1 tracker_angle: 0.0 span_fixation_pts: [13.2] - torque_tube_separation: 0.2 # gap between panel and tube center - torque_tube_outer_radius: 0.1 # radius of the torque tube - torque_tube_inner_radius: 0.09 # radius of the torque tube - modules_per_span: 10 + # torque_tube_separation: 0.2 # gap between panel and tube center + # torque_tube_outer_radius: 0.1 # radius of the torque tube + # torque_tube_inner_radius: 0.09 # radius of the torque tube + modules_per_span: 1 fixed_location: 5 # fixed location of the panel along the span (from 0 (fixed at left) to modules_per_span (fixed at right)) block_chord_div_by_panel_chord: 0.02 diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index 18f1beb..1ec9468 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -650,7 +650,7 @@ properties: type: "boolean" description: "Controls whether or not a boundary condition is applied along the lines defined by the spanwise fixation points which run in the streamwise direction and divide the panel into multiple rectangles in the spanwise direction." bc_list: - default: [front,back] + default: []#[panel_front,panel_back] # type: "list" description: "location for clamped boundary conditions" dt: diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index f86311b..676b19d 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -651,12 +651,7 @@ def _all_interior_surfaces(x): self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] - ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] - ) + if self.ndim == 3: self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( @@ -677,6 +672,12 @@ def _all_interior_surfaces(x): self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) for module_id in range(params.pv_array.modules_per_span): self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( @@ -686,7 +687,7 @@ def _all_interior_surfaces(x): ) self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( domain.domain_markers[ - f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + f"panel_top_{panel_id:.0f}_{module_id:.0f}" ]["idx"] ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( @@ -696,20 +697,23 @@ def _all_interior_surfaces(x): ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( domain.domain_markers[ - f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" - ]["idx"] - ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[ - f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" - ]["idx"] - ) - self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[ - f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + f"panel_top_{panel_id:.0f}_{module_id:.0f}" ]["idx"] ) + if self.ndim == 3: + + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[ + f"panel_bottom_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] + ) + self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( + domain.domain_markers[ + f"panel_top_{panel_id:.0f}_{module_id:.0f}" + ]["idx"] + ) + self.integrated_force_x_form[-1] = dolfinx.fem.form( self.integrated_force_x_form[-1] ) @@ -1223,7 +1227,8 @@ def solve(self, domain, params, current_time): self.compute_lift_and_drag(params, current_time) # self.compute_panel_torques(domain, params) - self.compute_double_integral_panel_torques(domain, params) + if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + self.compute_double_integral_panel_torques(domain, params) # Compute the pressure drop between the inlet and outlet if params.pv_array.stream_rows > 0: diff --git a/pvade/fluid/boundary_conditions.py b/pvade/fluid/boundary_conditions.py index a68b153..3e6f5dd 100644 --- a/pvade/fluid/boundary_conditions.py +++ b/pvade/fluid/boundary_conditions.py @@ -585,27 +585,27 @@ def __call__(self, x): or params.general.geometry_module == "heliostats3d" or params.general.geometry_module == "flag2d" ): + for module_id in range(params.pv_array.modules_per_span): + for location in ( + f"panel_bottom_{panel_id}_{module_id}", + f"panel_top_{panel_id}_{module_id}", + f"panel_left_{panel_id}", + f"panel_right_{panel_id}", + # f"front_{panel_id}", # not valid in panels2d? + # f"back_{panel_id}", + ): + T0_pv_panel_scalar = dolfinx.fem.Constant( + domain.fluid.msh, PETSc.ScalarType(params.fluid.T0_panel) + ) - for location in ( - f"bottom_{panel_id}", - f"top_{panel_id}", - f"left_{panel_id}", - f"right_{panel_id}", - # f"front_{panel_id}", # not valid in panels2d? - # f"back_{panel_id}", - ): - T0_pv_panel_scalar = dolfinx.fem.Constant( - domain.fluid.msh, PETSc.ScalarType(params.fluid.T0_panel) - ) - - panel_sfc_dofs = get_facet_dofs_by_gmsh_tag( - domain, functionspace, location - ) - bc = dolfinx.fem.dirichletbc( - T0_pv_panel_scalar, panel_sfc_dofs, functionspace - ) + panel_sfc_dofs = get_facet_dofs_by_gmsh_tag( + domain, functionspace, location + ) + bc = dolfinx.fem.dirichletbc( + T0_pv_panel_scalar, panel_sfc_dofs, functionspace + ) - bcT.append(bc) + bcT.append(bc) # if params.general.debug_flag == True: # print('built temperature boundary conditions') diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index f69e6aa..0dc9895 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -128,6 +128,8 @@ def _get_domain_markers(self, params): def build(self, params): """This function call builds the geometry, marks the boundaries and creates a mesh using Gmsh.""" + + self.modeling_torque_tube = False domain_creation_module = ( f"pvade.geometry.{params.general.geometry_module}.DomainCreation" @@ -151,6 +153,8 @@ def build(self, params): and params.general.structural_analysis == True ): self.geometry.build_FSI(params) + self.modeling_torque_tube = self.geometry.modeling_torque_tube + elif ( ( params.general.geometry_module == "panels3d" @@ -160,8 +164,14 @@ def build(self, params): and params.general.structural_analysis == True ): self.geometry.build_structure(params) + self.modeling_torque_tube = self.geometry.modeling_torque_tube else: self.geometry.build_FSI(params) + + self.modeling_torque_tube = False + + + # Build the domain markers for each surface and cell if hasattr(self.geometry, "domain_markers"): # If the "build" process created domain markers, use those directly... @@ -186,6 +196,9 @@ def build(self, params): self.ndim = ( self.geometry.ndim ) # gmsh_model.get_dimension() # ?? should this be domain.ndim? + + self.modeling_torque_tube = self.comm.bcast(self.modeling_torque_tube, root=0) + # When finished, rank 0 needs to tell other ranks about how the domain_markers dictionary was created # and what values it holds. This is important now since the number of indices "idx" generated in the @@ -289,7 +302,7 @@ def _create_submeshes_from_parent(self, params): ): marker_id = self.domain_markers["modules"]["idx"] # Find all cells where cell tag = marker_id - if self.geometry.modeling_torque_tube: + if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": submesh_cells_modules = self.cell_tags.find(marker_id) marker_id = self.domain_markers["connectors"]["idx"] submesh_cells = np.hstack( @@ -592,6 +605,16 @@ def read_mesh_files(self, read_mesh_dir, params): and use it to solve the CFD/CSD problem """ + if ( + params.pv_array.torque_tube_separation > 0.0 + and params.pv_array.torque_tube_outer_radius > 0.0 + ): + self.modeling_torque_tube = True + else: + self.modeling_torque_tube = False + assert(params.pv_array.modules_per_span == 1), "When not modeling torque tube, modules_per_span must be 1." + + sub_domain_list = ["fluid", "structure"] for sub_domain_name in sub_domain_list: diff --git a/pvade/geometry/flag2d/DomainCreation.py b/pvade/geometry/flag2d/DomainCreation.py index 40eef9f..352ac0b 100644 --- a/pvade/geometry/flag2d/DomainCreation.py +++ b/pvade/geometry/flag2d/DomainCreation.py @@ -116,10 +116,10 @@ def build_FSI(self, params): elif np.allclose(com[1], params.domain.y_max): self._add_to_domain_markers("y_max", [surf_id], "facet") - self._add_to_domain_markers("left_0", [5], "facet") - self._add_to_domain_markers("bottom_0", [6], "facet") - self._add_to_domain_markers("right_0", [7], "facet") - self._add_to_domain_markers("top_0", [8], "facet") + self._add_to_domain_markers("panel_left_0", [5], "facet") + self._add_to_domain_markers("panel_bottom_0_0", [6], "facet") + self._add_to_domain_markers("panel_right_0", [7], "facet") + self._add_to_domain_markers("panel_top_0_0", [8], "facet") # Tag objects as either structure or fluid vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 8a345d7..c970322 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -66,7 +66,6 @@ def build_FSI(self, params): Returns: The function returns gmsh.model which contains the geometric description of the computational domain """ - def Rx(theta): rot_matrix = np.array( [ @@ -112,6 +111,16 @@ def Rz(theta): array_rotation = (params.fluid.wind_direction + 90.0) % 360.0 array_rotation_rad = np.radians(array_rotation) + if ( + params.pv_array.torque_tube_separation > 0.0 + and params.pv_array.torque_tube_outer_radius > 0.0 + ): + self.modeling_torque_tube = True + else: + self.modeling_torque_tube = False + assert(params.pv_array.modules_per_span == 1), "When not modeling torque tube, modules_per_span must be 1." + + # The centroid of each panel in the x-direction (these should start at x=0) x_centers = np.linspace( 0.0, @@ -164,14 +173,6 @@ def Rz(theta): transformed_com = {} # all panels - if ( - params.pv_array.torque_tube_separation > 0.0 - and params.pv_array.torque_tube_outer_radius > 0.0 - ): - self.modeling_torque_tube = True - else: - self.modeling_torque_tube = False - vol_tags_modules = [] vol_tags_connectors = [] # start to add panel @@ -202,12 +203,12 @@ def Rz(theta): module_distances[module_id + 1] - module_distances[module_id] ) - if self.modeling_torque_tube: + if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) this_module = self.gmsh_model.occ.addBox( -half_chord, module_distances[module_id], - params.pv_array.torque_tube_separation, + params.pv_array.torque_tube_separation-half_thickness, params.pv_array.panel_chord, module_span, params.pv_array.panel_thickness, @@ -224,7 +225,7 @@ def Rz(theta): -params.pv_array.block_chord_div_by_panel_chord * half_chord, module_distances[module_id], - 0.0, + -half_thickness, 2.0 * params.pv_array.block_chord_div_by_panel_chord * half_chord, @@ -238,25 +239,25 @@ def Rz(theta): # Add a bisecting line to the bottom of the connector in the spanwise direction pt_1 = self.gmsh_model.occ.addPoint( - 0, module_distances[module_id], 0.0 + 0, module_distances[module_id], -half_thickness ) pt_2 = self.gmsh_model.occ.addPoint( 0, module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord, - 0.0, + -half_thickness, ) numpy_pt_list.append( [ 0, module_distances[module_id], - 0.0, + -half_thickness, 0, module_distances[module_id] + params.pv_array.block_chord_div_by_panel_chord * half_chord, - 0.0, + -half_thickness, ] ) # for this row torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) @@ -269,7 +270,7 @@ def Rz(theta): this_module = self.gmsh_model.occ.addBox( -half_chord, module_distances[module_id], - 0.0, + -half_thickness, params.pv_array.panel_chord, module_span, params.pv_array.panel_thickness, @@ -280,19 +281,19 @@ def Rz(theta): structure_panel_only_list.append((self.ndim, this_module)) pt_1 = self.gmsh_model.occ.addPoint( - 0, module_distances[module_id], 0.0 + 0, module_distances[module_id], -half_thickness ) pt_2 = self.gmsh_model.occ.addPoint( - 0, module_distances[module_id + 1], 0.0 + 0, module_distances[module_id + 1], -half_thickness ) numpy_pt_list.append( [ 0, module_distances[module_id], - 0.0, + -half_thickness, 0, module_distances[module_id + 1], - 0.0, + -half_thickness, ] ) torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) @@ -301,12 +302,65 @@ def Rz(theta): torque_tube_tag ) # for this panel row - if self.modeling_torque_tube: # add the last connector + # Add lines in the streamwise direction to mimic sections of panel held rigid by motor + if params.pv_array.span_fixation_pts is not None: + if not isinstance(params.pv_array.span_fixation_pts, list): + num_fixation_pts = int( + np.floor( + params.pv_array.panel_span + / params.pv_array.span_fixation_pts + ) + ) + + fixation_pts_list = [] + + for k in range(1, num_fixation_pts + 1): + next_pt = k * params.pv_array.span_fixation_pts + + eps = 1e-9 + + if ( + next_pt > eps + and next_pt < params.pv_array.panel_span - eps + ): + + + fixation_pts_list.append(next_pt) + + else: + fixation_pts_list = params.pv_array.span_fixation_pts + + for fp in fixation_pts_list: + pt_1 = self.gmsh_model.occ.addPoint( + -half_chord, -half_span + fp, -half_thickness + ) + pt_2 = self.gmsh_model.occ.addPoint( + half_chord, -half_span + fp, -half_thickness + ) + + # FIXME: don't add the fixation points into the numpy tagging for now + numpy_pt_list.append( + [ + -half_chord, + -half_span + fp, + -half_thickness, + half_chord, + -half_span + fp, + -half_thickness, + ] + ) + + fixed_pt_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + fixed_pt_tag = (1, fixed_pt_id) + + embedded_lines_tag_list.append(fixed_pt_tag) + + if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": # add the last connector last_standoff = self.gmsh_model.occ.addBox( -params.pv_array.block_chord_div_by_panel_chord * half_chord, module_distances[module_id + 1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, - 0.0, + -half_thickness, 2.0 * params.pv_array.block_chord_div_by_panel_chord * half_chord, @@ -322,10 +376,10 @@ def Rz(theta): 0, module_distances[module_id + 1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, - 0.0, + -half_thickness, ) pt_2 = self.gmsh_model.occ.addPoint( - 0, module_distances[module_id + 1], 0.0 + 0, module_distances[module_id + 1], -half_thickness ) numpy_pt_list.append( [ @@ -333,10 +387,10 @@ def Rz(theta): module_distances[module_id + 1] - params.pv_array.block_chord_div_by_panel_chord * half_chord, - 0.0, + -half_thickness, 0, module_distances[module_id + 1], - 0.0, + -half_thickness, ] ) torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) @@ -450,12 +504,11 @@ def Rz(theta): ) if np.isclose( vol_com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness / 2.0, + params.pv_array.torque_tube_separation, ): target_key = f"modules" elif np.isclose( - vol_com[2], params.pv_array.torque_tube_separation / 2.0 + vol_com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness ): target_key = f"connectors" @@ -490,8 +543,7 @@ def Rz(theta): and np.isclose(com[0], 0) and np.isclose( com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness / 2.0, + params.pv_array.torque_tube_separation, ) ): target_key = f"panel_front_{panel_ct:.0f}" @@ -506,22 +558,21 @@ def Rz(theta): and np.isclose(com[0], 0) and np.isclose( com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness / 2.0, + params.pv_array.torque_tube_separation, ) ): target_key = f"panel_back_{panel_ct:.0f}" surface_located_or_not = True - if not self.modeling_torque_tube: + if (not self.modeling_torque_tube) or (params.general.geometry_modules != "panels3d"): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation + com[2], params.pv_array.torque_tube_separation-half_thickness ) ): target_key = ( - f"panel_bottom_{panel_ct:.0f}" + f"panel_bottom_{panel_ct:.0f}_0" ) surface_located_or_not = True @@ -529,11 +580,10 @@ def Rz(theta): if ( np.isclose( com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness, + half_thickness + params.pv_array.torque_tube_separation, ) ): - target_key = f"panel_top_{panel_ct:.0f}" + target_key = f"panel_top_{panel_ct:.0f}_0" surface_located_or_not = True else: @@ -551,7 +601,7 @@ def Rz(theta): * half_chord and np.isclose(com[0], 0) and np.isclose( - com[2], params.pv_array.torque_tube_separation + com[2], params.pv_array.torque_tube_separation-half_thickness ) ): target_key = ( @@ -575,7 +625,7 @@ def Rz(theta): and np.isclose( com[2], params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness, + + half_thickness, ) ): target_key = f"panel_top_{panel_ct:.0f}_{module_id:.0f}" @@ -600,6 +650,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -618,6 +669,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -631,9 +683,10 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation / 2.0 + com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -646,9 +699,10 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation / 2.0 + com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span], @@ -658,8 +712,9 @@ def Rz(theta): surface_located_or_not = True if ( - np.isclose(com[2], 0.0) + np.isclose(com[2], -half_thickness) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -672,8 +727,9 @@ def Rz(theta): surface_located_or_not = True if ( - np.isclose(com[2], params.pv_array.torque_tube_separation) + np.isclose(com[2], params.pv_array.torque_tube_separation - half_thickness) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -696,8 +752,7 @@ def Rz(theta): and np.isclose(com[0], 0) and np.isclose( com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness / 2.0, + params.pv_array.torque_tube_separation, ) ): target_key = f"interior_surface_{panel_ct:.0f}" @@ -709,8 +764,7 @@ def Rz(theta): and np.isclose(com[0], 0) and np.isclose( com[2], - params.pv_array.torque_tube_separation - + params.pv_array.panel_thickness / 2.0, + params.pv_array.torque_tube_separation, ) ): target_key = f"interior_surface_{panel_ct:.0f}" @@ -728,7 +782,7 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation + com[2], params.pv_array.torque_tube_separation - half_thickness ) and np.isclose(com[0], 0.0) and np.isclose( @@ -739,6 +793,7 @@ def Rz(theta): / 2.0, ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" ): target_key = f"interior_surface_{panel_ct:.0f}" surface_located_or_not = True @@ -751,6 +806,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -772,6 +828,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -789,9 +846,10 @@ def Rz(theta): if ( np.isclose( com[2], - params.pv_array.torque_tube_separation / 2.0, + params.pv_array.torque_tube_separation / 2.0 - half_thickness, ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose(com[1], module_distances[module_id]) ): target_key = ( @@ -803,9 +861,10 @@ def Rz(theta): if ( np.isclose( com[2], - params.pv_array.torque_tube_separation / 2.0, + params.pv_array.torque_tube_separation / 2.0 - half_thickness, ) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -820,8 +879,9 @@ def Rz(theta): break if ( - np.isclose(com[2], 0.0) + np.isclose(com[2], -half_thickness) and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -912,6 +972,8 @@ def Rz(theta): array_rotation_rad, ) + + # Now, apply the same transformations to the numpy representation # Rotate the panel by its tracking angle along the y-axis # (currently centered at (0.0, 0.0, 0.0)) @@ -949,6 +1011,8 @@ def Rz(theta): self.gmsh_model.occ.fragment(domain_tag_list, panel_tag_list) self.gmsh_model.occ.synchronize() + gmsh.write("pnales.brep") + # exit() self.numpy_pt_total_array = np.reshape( self.numpy_pt_total_array, (-1, int(2 * self.ndim)) @@ -995,6 +1059,8 @@ def Rz(theta): if "trash" not in key: # print(key) self._add_to_domain_markers(key, [surf_id], "facet") + # if "interior_surface" not in key: + # self._add_to_domain_markers("structure_fluid_interface", [surf_id], "facet") if not located_this_surface: print( @@ -1016,7 +1082,7 @@ def Rz(theta): raise ValueError(f"A panel extends past the y_min wall.") if this_surf_bbox[1] > params.domain.y_max: raise ValueError(f"A panel extends past the y_max wall.") - if this_surf_bbox[2] < 0.0: + if this_surf_bbox[2] < 0.0: #params.domain.z_min: raise ValueError( f"A panel extends past the z_min wall (ground level = 0.0)." ) @@ -1204,6 +1270,17 @@ def Rz(theta): array_rotation = (params.fluid.wind_direction + 90.0) % 360.0 array_rotation_rad = np.radians(array_rotation) + if ( + params.pv_array.torque_tube_separation > 0.0 + and params.pv_array.torque_tube_outer_radius > 0.0 + ): + self.modeling_torque_tube = True + else: + self.modeling_torque_tube = False + assert(params.pv_array.modules_per_span == 1), "When not modeling torque tube, modules_per_span must be 1." + + + # The centroid of each panel in the x-direction (these should start at x=0) x_centers = np.linspace( 0.0, @@ -1241,362 +1318,835 @@ def Rz(theta): domain_tag_list = [] # domain_tag_list.append(domain_tag) + # panel_tag_list includes all structure, panel+connector panel_tag_list = [] panel_ct = 0 - panel_id_y = -1 + # only include panels + structure_panel_only_list = [] + # only include connectors + structure_connector_only_list = [] + + module_distances = np.linspace( + -half_span, half_span, params.pv_array.modules_per_span + 1 + ) + + transformed_com = {} # all panels + + # panel_id_y = -1 - prev_surf_tag = [] - for k, yy in enumerate(y_centers): - panel_id_x = -1 - panel_id_y += 1 - for j, xx in enumerate(x_centers): - panel_id_x += 1 + # prev_surf_tag = [] + for panel_id_y, yy in enumerate(y_centers): + # panel_id_x = -1 + # panel_id_y += 1 + for panel_id_x, xx in enumerate(x_centers): + # panel_id_x += 1 # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) - panel_id = self.gmsh_model.occ.addBox( - -half_chord, - -half_span, - -half_thickness, - params.pv_array.panel_chord, - params.pv_array.panel_span, - params.pv_array.panel_thickness, - ) - panel_tag = (self.ndim, panel_id) - panel_tag_list.append(panel_tag) + this_panel_tag_list = [] # for each row + this_panel_transformed_com = {} # for each row - numpy_pt_list = [] - embedded_lines_tag_list = [] + numpy_pt_list = [] # for each row + embedded_lines_tag_list = [] # for each row - # Add a bisecting line to the bottom of the panel in the spanwise direction - pt_1 = self.gmsh_model.occ.addPoint(0, -half_span, -half_thickness) - pt_2 = self.gmsh_model.occ.addPoint(0, half_span, -half_thickness) + for module_id in range(params.pv_array.modules_per_span): - numpy_pt_list.append( - [0, -half_span, -half_thickness, 0, half_span, -half_thickness] - ) + module_span = ( + module_distances[module_id + 1] - module_distances[module_id] + ) - torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) - torque_tube_tag = (1, torque_tube_id) - embedded_lines_tag_list.append(torque_tube_tag) - - # Add lines in the streamwise direction to mimic sections of panel held rigid by motor - if params.pv_array.span_fixation_pts is not None: - if not isinstance(params.pv_array.span_fixation_pts, list): - num_fixation_pts = int( - np.floor( - params.pv_array.panel_span - / params.pv_array.span_fixation_pts - ) + if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": + # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) + this_module = self.gmsh_model.occ.addBox( + -half_chord, + module_distances[module_id], + params.pv_array.torque_tube_separation-half_thickness, + params.pv_array.panel_chord, + module_span, + params.pv_array.panel_thickness, ) - fixation_pts_list = [] - - for k in range(1, num_fixation_pts + 1): - next_pt = k * params.pv_array.span_fixation_pts + panel_tag_list.append((self.ndim, this_module)) # all panels + this_panel_tag_list.append( + (self.ndim, this_module) + ) # this panel array/row - eps = 1e-9 + structure_panel_only_list.append((self.ndim, this_module)) - if ( - next_pt > eps - and next_pt < params.pv_array.panel_span - eps - ): - fixation_pts_list.append(next_pt) + this_standoff = self.gmsh_model.occ.addBox( + -params.pv_array.block_chord_div_by_panel_chord + * half_chord, + module_distances[module_id], + -half_thickness, + 2.0 + * params.pv_array.block_chord_div_by_panel_chord + * half_chord, + params.pv_array.block_chord_div_by_panel_chord * half_chord, + params.pv_array.torque_tube_separation, + ) + panel_tag_list.append((self.ndim, this_standoff)) + this_panel_tag_list.append((self.ndim, this_standoff)) - else: - fixation_pts_list = params.pv_array.span_fixation_pts + structure_connector_only_list.append((self.ndim, this_standoff)) - for fp in fixation_pts_list: + # Add a bisecting line to the bottom of the connector in the spanwise direction pt_1 = self.gmsh_model.occ.addPoint( - -half_chord, -half_span + fp, -half_thickness + 0, module_distances[module_id], -half_thickness ) pt_2 = self.gmsh_model.occ.addPoint( - half_chord, -half_span + fp, -half_thickness + 0, + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + -half_thickness, ) - - # FIXME: don't add the fixation points into the numpy tagging for now numpy_pt_list.append( [ - -half_chord, - -half_span + fp, + 0, + module_distances[module_id], -half_thickness, - half_chord, - -half_span + fp, + 0, + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord, -half_thickness, ] - ) - - fixed_pt_id = self.gmsh_model.occ.addLine(pt_1, pt_2) - fixed_pt_tag = (1, fixed_pt_id) - - embedded_lines_tag_list.append(fixed_pt_tag) - - # Store the result of fragmentation, it holds all the small surfaces we need to tag - panel_frags = self.gmsh_model.occ.fragment( - [panel_tag], embedded_lines_tag_list - ) - - # Translate the panel by (x_center, y_center, elev) - self.gmsh_model.occ.translate( - [panel_tag], - xx, - yy, - params.pv_array.elevation, - ) - - # extract just the first entry, and remove the 3d entry in position 0 - panel_surfs = panel_frags[0] - panel_surfs.pop(0) - panel_surfs = [k[1] for k in panel_surfs] - - # TODO: USE THESE UNAMBIGUOUS NAMES IN A FUTURE REFACTOR - # self._add_to_domain_markers(f"x_min_{panel_ct:.0f}", [panel_surfs[0]], "facet") - # self._add_to_domain_markers(f"x_max_{panel_ct:.0f}", [panel_surfs[1]], "facet") - # self._add_to_domain_markers(f"y_min_{panel_ct:.0f}", [panel_surfs[2]], "facet") - # self._add_to_domain_markers(f"y_max_{panel_ct:.0f}", [panel_surfs[3]], "facet") - # self._add_to_domain_markers(f"z_min_{panel_ct:.0f}", panel_surfs[4:-1], "facet") - # self._add_to_domain_markers(f"z_max_{panel_ct:.0f}", [panel_surfs[-1]], "facet") - - # self._add_to_domain_markers(f"front_{panel_ct:.0f}", [panel_surfs[0]], "facet") - # self._add_to_domain_markers(f"back_{panel_ct:.0f}", [panel_surfs[1]], "facet") - # self._add_to_domain_markers(f"left_{panel_ct:.0f}", [panel_surfs[2]], "facet") - # self._add_to_domain_markers(f"right_{panel_ct:.0f}", [panel_surfs[3]], "facet") - # self._add_to_domain_markers(f"bottom_{panel_ct:.0f}", panel_surfs[4:-1], "facet") - # self._add_to_domain_markers(f"top_{panel_ct:.0f}", [panel_surfs[-1]], "facet") - - # self._add_to_domain_markers( - # f"front_{panel_ct:.0f}", [panel_surfs[1]], "facet" - # ) # should be bottom - # self._add_to_domain_markers( - # f"back_{panel_ct:.0f}", [panel_surfs[2]], "facet" - # ) - - # self._add_to_domain_markers( - # f"left_{panel_ct:.0f}", [panel_surfs[3]], "facet" - # ) # should be left - # self._add_to_domain_markers( - # f"right_{panel_ct:.0f}", [panel_surfs[4]], "facet" - # ) # correct 4 - - # self._add_to_domain_markers( - # f"bottom_{panel_ct:.0f}", panel_surfs[5:-1], "facet" - # ) - # self._add_to_domain_markers( - # f"top_{panel_ct:.0f}", [panel_surfs[-1]], "facet" - # ) # should be front - - top_coord = ( - params.domain.z_min - + (params.pv_array.elevation - params.domain.z_min) - + params.pv_array.panel_thickness / 2 - ) - print("top", top_coord) - bottom_coord = ( - params.domain.z_min - + (params.pv_array.elevation - params.domain.z_min) - - params.pv_array.panel_thickness / 2 - ) - print("bottom", bottom_coord) - left_coord = -params.pv_array.panel_chord / 2 + panel_id_x * ( - params.pv_array.stream_spacing - ) - print("left", left_coord) - right_coord = +params.pv_array.panel_chord / 2 + panel_id_x * ( - params.pv_array.stream_spacing - ) - print("right", right_coord) - - front_coord = -params.pv_array.panel_span / 2 + yy - print("front", front_coord) - back_coord = +params.pv_array.panel_span / 2 + yy - print("back", back_coord) - - surf_tag_list_total = self.gmsh_model.occ.getEntities(self.ndim - 1) - - surf_tag_list = [ - vector - for vector in surf_tag_list_total - if vector not in prev_surf_tag - ] - - prev_surf_tag = surf_tag_list_total - for surf_tag in surf_tag_list: - surf_id = surf_tag[1] - com = self.gmsh_model.occ.getCenterOfMass(self.ndim - 1, surf_id) - print(com) - # sturctures tagging - if np.isclose(com[2], bottom_coord): - self._add_to_domain_markers( - f"bottom_{panel_ct:.0f}", [surf_id], "facet" - ) - print("bottom found") - # self._add_to_domain_markers("x_min", [surf_id], "facet") + ) # for this row + torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + torque_tube_tag = (1, torque_tube_id) + embedded_lines_tag_list.append( + torque_tube_tag + ) # for this panel row - elif np.allclose(com[2], top_coord): - self._add_to_domain_markers( - f"top_{panel_ct:.0f}", [surf_id], "facet" + else: + this_module = self.gmsh_model.occ.addBox( + -half_chord, + module_distances[module_id], + -half_thickness, + params.pv_array.panel_chord, + module_span, + params.pv_array.panel_thickness, ) - print("top found") - # self._add_to_domain_markers("x_max", [surf_id], "facet") + panel_tag_list.append((self.ndim, this_module)) + this_panel_tag_list.append((self.ndim, this_module)) - elif np.allclose(com[0], left_coord): - self._add_to_domain_markers( - f"left_{panel_ct:.0f}", [surf_id], "facet" - ) - print("left found") - # self._add_to_domain_markers("y_min", [surf_id], "facet") + structure_panel_only_list.append((self.ndim, this_module)) - elif np.allclose(com[0], right_coord): - self._add_to_domain_markers( - f"right_{panel_ct:.0f}", [surf_id], "facet" + pt_1 = self.gmsh_model.occ.addPoint( + 0, module_distances[module_id], -half_thickness ) - print("right found") - - elif np.allclose(com[1], front_coord): - self._add_to_domain_markers( - f"front_{panel_ct:.0f}", [surf_id], "facet" + pt_2 = self.gmsh_model.occ.addPoint( + 0, module_distances[module_id + 1], -half_thickness ) - print("front found") - # self._add_to_domain_markers("y_min", [surf_id], "facet") - - elif np.allclose(com[1], back_coord): - self._add_to_domain_markers( - f"back_{panel_ct:.0f}", [surf_id], "facet" + numpy_pt_list.append( + [ + 0, + module_distances[module_id], + -half_thickness, + 0, + module_distances[module_id + 1], + -half_thickness, + ] ) - print("back found") - # self._add_to_domain_markers("y_max", [surf_id], "facet") + torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + torque_tube_tag = (1, torque_tube_id) + embedded_lines_tag_list.append( + torque_tube_tag + ) # for this panel row - panel_ct += 1 + # Add lines in the streamwise direction to mimic sections of panel held rigid by motor + if params.pv_array.span_fixation_pts is not None: + if not isinstance(params.pv_array.span_fixation_pts, list): + num_fixation_pts = int( + np.floor( + params.pv_array.panel_span + / params.pv_array.span_fixation_pts + ) + ) - # Rotate the panel by its tracking angle along the y-axis (currently centered at (0, 0, 0)) - self.gmsh_model.occ.rotate( - [panel_tag], 0, 0, 0, 0, 1, 0, tracker_angle_rad - ) + fixation_pts_list = [] - numpy_pt_panel_array = np.array(numpy_pt_list) - numpy_pt_panel_array = np.reshape(numpy_pt_panel_array, (-1, self.ndim)) + for k in range(1, num_fixation_pts + 1): + next_pt = k * params.pv_array.span_fixation_pts - numpy_pt_panel_array = np.dot( - numpy_pt_panel_array, Ry(tracker_angle_rad).T - ) + eps = 1e-9 - # if not hasattr(self, "numpy_pt_array"): - # numpy_pt_array = np.array(numpy_pt_list) - # else: - # numpy_pt_array = np.vcat(numpy_pt_array, np.array(numpy_pt_list)) + if ( + next_pt > eps + and next_pt < params.pv_array.panel_span - eps + ): + fixation_pts_list.append(next_pt) - numpy_pt_panel_array[:, 0] += xx - numpy_pt_panel_array[:, 1] += yy - numpy_pt_panel_array[:, 2] += params.pv_array.elevation + else: + fixation_pts_list = params.pv_array.span_fixation_pts - # Rotate the panel about the center of the full array as a proxy for changing wind direction (x_center, y_center, 0) - self.gmsh_model.occ.rotate( - [panel_tag], - x_center_of_mass, - y_center_of_mass, - 0, - 0, - 0, - 1, - array_rotation_rad, - ) + for fp in fixation_pts_list: + pt_1 = self.gmsh_model.occ.addPoint( + -half_chord, -half_span + fp, -half_thickness + ) + pt_2 = self.gmsh_model.occ.addPoint( + half_chord, -half_span + fp, -half_thickness + ) - numpy_pt_panel_array[:, 0] -= x_center_of_mass - numpy_pt_panel_array[:, 1] -= y_center_of_mass + # FIXME: don't add the fixation points into the numpy tagging for now + numpy_pt_list.append( + [ + -half_chord, + -half_span + fp, + -half_thickness, + half_chord, + -half_span + fp, + -half_thickness, + ] + ) - numpy_pt_panel_array = np.dot( - numpy_pt_panel_array, Rz(array_rotation_rad).T - ) + fixed_pt_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + fixed_pt_tag = (1, fixed_pt_id) - numpy_pt_panel_array[:, 0] += x_center_of_mass - numpy_pt_panel_array[:, 1] += y_center_of_mass + embedded_lines_tag_list.append(fixed_pt_tag) - if hasattr(self, "numpy_pt_total_array"): - self.numpy_pt_total_array = np.vstack( - (self.numpy_pt_total_array, numpy_pt_panel_array) + if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": # add the last connector + last_standoff = self.gmsh_model.occ.addBox( + -params.pv_array.block_chord_div_by_panel_chord * half_chord, + module_distances[module_id + 1] + - params.pv_array.block_chord_div_by_panel_chord * half_chord, + -half_thickness, + 2.0 + * params.pv_array.block_chord_div_by_panel_chord + * half_chord, + params.pv_array.block_chord_div_by_panel_chord * half_chord, + params.pv_array.torque_tube_separation, ) - else: - self.numpy_pt_total_array = np.copy(numpy_pt_panel_array) + this_panel_tag_list.append((self.ndim, last_standoff)) + panel_tag_list.append((self.ndim, last_standoff)) - # Check that this panel still exists in the confines of the domain - bbox = self.gmsh_model.occ.get_bounding_box(panel_tag[0], panel_tag[1]) + structure_connector_only_list.append((self.ndim, last_standoff)) - if bbox[0] < params.domain.x_min: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past x_min wall." - ) - if bbox[1] < params.domain.y_min: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past y_min wall." - ) - if bbox[3] > params.domain.x_max: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past x_max wall." + pt_1 = self.gmsh_model.occ.addPoint( + 0, + module_distances[module_id + 1] + - params.pv_array.block_chord_div_by_panel_chord * half_chord, + -half_thickness, ) - if bbox[4] > params.domain.y_max: - raise ValueError( - f"Panel with location (x, y) = ({xx}, {yy}) extends past y_max wall." + pt_2 = self.gmsh_model.occ.addPoint( + 0, module_distances[module_id + 1], -half_thickness ) - + numpy_pt_list.append( + [ + 0, + module_distances[module_id + 1] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord, + -half_thickness, + 0, + module_distances[module_id + 1], + -half_thickness, + ] + ) + torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + torque_tube_tag = (1, torque_tube_id) + embedded_lines_tag_list.append(torque_tube_tag) + + # Store the result of fragmentation, it holds all the small surfaces we need to tag + panel_frags = self.gmsh_model.occ.fragment( + this_panel_tag_list, embedded_lines_tag_list + ) + + for panel_tag in this_panel_tag_list: # 3d cell domain + self.gmsh_model.occ.synchronize() + + # Get the list of 2D surfaces (surfaces) that make up this panel + surf_tags_for_this_panel = self.gmsh_model.getBoundary( + [panel_tag], oriented=False + ) + + vol_com = self.gmsh_model.occ.getCenterOfMass( + self.ndim, panel_tag[1] + ) + if np.isclose( + vol_com[2], + params.pv_array.torque_tube_separation, + ): + target_key = f"modules" + elif np.isclose( + vol_com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + ): + target_key = f"connectors" + + if target_key is not None: + if target_key in this_panel_transformed_com: + this_panel_transformed_com[target_key].append(vol_com) + else: + this_panel_transformed_com[target_key] = [vol_com] + + for surf_tag in surf_tags_for_this_panel: + surf_dim = surf_tag[0] + surf_id = surf_tag[1] + com = self.gmsh_model.occ.getCenterOfMass(surf_dim, surf_id) + + target_key = None + + surface_located_or_not = False + + # sturctures tagging + if np.isclose(com[0], -half_chord): + target_key = f"panel_left_{panel_ct:.0f}" + surface_located_or_not = True + + + if np.isclose(com[0], half_chord): + target_key = f"panel_right_{panel_ct:.0f}" + surface_located_or_not = True + + + if ( + np.isclose(com[1], module_distances[0]) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation, + ) + ): + target_key = f"panel_front_{panel_ct:.0f}" + surface_located_or_not = True + + + if ( + np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span], + ) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation, + ) + ): + target_key = f"panel_back_{panel_ct:.0f}" + surface_located_or_not = True + + + if (not self.modeling_torque_tube) or (params.general.geometry_modules != "panels3d"): + if ( + np.isclose( + com[2], params.pv_array.torque_tube_separation-half_thickness + ) + ): + target_key = ( + f"panel_bottom_{panel_ct:.0f}_0" + ) + surface_located_or_not = True + + + if ( + np.isclose( + com[2], + half_thickness + params.pv_array.torque_tube_separation, + ) + ): + target_key = f"panel_top_{panel_ct:.0f}_0" + surface_located_or_not = True + + else: + for module_id in range(params.pv_array.modules_per_span): + if ( + com[1] + >= module_distances[module_id] + + module_span / 2.0 + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + and com[1] + <= module_distances[module_id] + + module_span / 2.0 + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + and np.isclose(com[0], 0) + and np.isclose( + com[2], params.pv_array.torque_tube_separation-half_thickness + ) + ): + target_key = ( + f"panel_bottom_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + com[1] + >= module_distances[module_id] + + module_span / 2.0 + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + and com[1] + <= module_distances[module_id] + + module_span / 2.0 + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation + + half_thickness, + ) + ): + target_key = f"panel_top_{panel_ct:.0f}_{module_id:.0f}" + surface_located_or_not = True + break + # if ( + # np.isclose( + # com[2], + # params.pv_array.torque_tube_separation + # + params.pv_array.panel_thickness + # ) + + # ): + # target_key = f"panel_top_{panel_ct:.0f}" + # surface_located_or_not = True + + # mark the last block in each row + if ( + np.isclose( + com[0], + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"block_right_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose( + com[0], + -params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"block_left_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose( + com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + ): + target_key = f"block_front_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose( + com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span], + ) + ): + target_key = f"block_back_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose(com[2], -half_thickness) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"block_bottom_{panel_ct:.0f}_{params.pv_array.modules_per_span:.0f}" + surface_located_or_not = True + + if ( + np.isclose(com[2], params.pv_array.torque_tube_separation - half_thickness) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[params.pv_array.modules_per_span] + - params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = f"interior_surface_{panel_ct:.0f}" # block/panel interface + surface_located_or_not = True + + # if not the most left/right panel boundary, it is panel/panel interface + if not surface_located_or_not: + for module_id in range(params.pv_array.modules_per_span): + + # sturctures tagging + + if ( + np.isclose(com[1], module_distances[module_id]) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation, + ) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose(com[1], module_distances[module_id + 1]) + and np.isclose(com[0], 0) + and np.isclose( + com[2], + params.pv_array.torque_tube_separation, + ) + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + # if ( + # np.isclose(com[2], params.pv_array.torque_tube_separation) + # and np.isclose(com[0], 0.0) and com[1] >= module_distances[module_id]+module_span/2.0-params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 + # and com[1] <= module_distances[module_id]+module_span/2.0+params.pv_array.block_chord_div_by_panel_chord * half_chord/2.0 + # ): + # target_key = f"panel_bottom_{panel_ct:.0f}" + # surface_located_or_not = True + # break + + if ( + np.isclose( + com[2], params.pv_array.torque_tube_separation - half_thickness + ) + and np.isclose(com[0], 0.0) + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): + target_key = f"interior_surface_{panel_ct:.0f}" + surface_located_or_not = True + break + + if ( + np.isclose( + com[0], + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = ( + f"block_right_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose( + com[0], + -params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = ( + f"block_left_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose( + com[2], + params.pv_array.torque_tube_separation / 2.0 - half_thickness, + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose(com[1], module_distances[module_id]) + ): + target_key = ( + f"block_front_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose( + com[2], + params.pv_array.torque_tube_separation / 2.0 - half_thickness, + ) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord, + ) + ): + target_key = ( + f"block_back_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if ( + np.isclose(com[2], -half_thickness) + and self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + and np.isclose( + com[1], + module_distances[module_id] + + params.pv_array.block_chord_div_by_panel_chord + * half_chord + / 2.0, + ) + ): + target_key = ( + f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" + ) + surface_located_or_not = True + break + + if not surface_located_or_not: + target_key = f"trash_{panel_ct:.0f}" + print('facet in trash') + + if target_key is not None: + if target_key in this_panel_transformed_com: + this_panel_transformed_com[target_key].append(com) + else: + this_panel_transformed_com[target_key] = [com] + + # print(this_panel_transformed_com[f"trash_{panel_ct:.0f}"]) + + for ( + key, + val, + ) in ( + this_panel_transformed_com.items() + ): # all facets for each panel row. + for row_num, com in enumerate(val): + com_array = np.array(com) + + com_array = np.dot(com_array, Ry(tracker_angle_rad).T) + + com_array[0] += xx + com_array[1] += yy + com_array[2] += params.pv_array.elevation + + com_array[0] -= x_center_of_mass + com_array[1] -= y_center_of_mass + + com_array = np.dot(com_array, Rz(array_rotation_rad).T) + + com_array[0] += x_center_of_mass + com_array[1] += y_center_of_mass + + if key in transformed_com: + transformed_com[key].append(com_array) + else: + transformed_com[key] = [com_array] # including all rows + + # actually this is panel array count + panel_ct += 1 + + # Rotate the panel by its tracking angle along the y-axis + # (currently centered at (0.0, 0.0, 0.0)) + self.gmsh_model.occ.rotate( + this_panel_tag_list, + 0.0, + 0.0, + 0.0, + 0, + 1, + 0, + tracker_angle_rad, + ) + + # Translate the panel by (x_center, y_center, elev) + self.gmsh_model.occ.translate( + this_panel_tag_list, + xx, + yy, + params.pv_array.elevation, + ) + + # Rotate the panel about the center of the full array as a proxy for changing wind direction (x_center, y_center, 0) + self.gmsh_model.occ.rotate( + this_panel_tag_list, + x_center_of_mass, + y_center_of_mass, + 0, + 0, + 0, + 1, + array_rotation_rad, + ) + + + + # Now, apply the same transformations to the numpy representation + # Rotate the panel by its tracking angle along the y-axis + # (currently centered at (0.0, 0.0, 0.0)) + numpy_pt_panel_array = np.array(numpy_pt_list) + numpy_pt_panel_array = np.reshape(numpy_pt_panel_array, (-1, self.ndim)) + + numpy_pt_panel_array = np.dot( + numpy_pt_panel_array, Ry(tracker_angle_rad).T + ) + + # Translate the panel by (x_center, y_center, elev) + numpy_pt_panel_array[:, 0] += xx + numpy_pt_panel_array[:, 1] += yy + numpy_pt_panel_array[:, 2] += params.pv_array.elevation + + # Rotate the panel about the center of the full array as a proxy for changing wind direction (x_center, y_center, 0) + numpy_pt_panel_array[:, 0] -= x_center_of_mass + numpy_pt_panel_array[:, 1] -= y_center_of_mass + + numpy_pt_panel_array = np.dot( + numpy_pt_panel_array, Rz(array_rotation_rad).T + ) + + numpy_pt_panel_array[:, 0] += x_center_of_mass + numpy_pt_panel_array[:, 1] += y_center_of_mass + + if hasattr(self, "numpy_pt_total_array"): + self.numpy_pt_total_array = np.vstack( + (self.numpy_pt_total_array, numpy_pt_panel_array) + ) + else: + self.numpy_pt_total_array = np.copy(numpy_pt_panel_array) + # Fragment all panels from the overall domain self.gmsh_model.occ.fragment(domain_tag_list, panel_tag_list) self.gmsh_model.occ.synchronize() + gmsh.write("pnales.brep") + # exit() self.numpy_pt_total_array = np.reshape( self.numpy_pt_total_array, (-1, int(2 * self.ndim)) ) - # import matplotlib.pyplot as plt - # for k in self.numpy_pt_total_array: - # plt.plot([k[0], k[3]], [k[1], k[4]]) - # plt.show() - - # # Surfaces are the entities with dimension equal to the mesh dimension -1 - # surf_tag_list = self.gmsh_model.occ.getEntities(self.ndim-1) - - # for surf_tag in surf_tag_list: - # surf_id = surf_tag[1] - # com = self.gmsh_model.occ.getCenterOfMass(self.ndim-1, surf_id) + # print(transformed_com) + # exit() - # #sturctures tagging - # if np.isclose(com[0], params.domain.x_min): - # self._add_to_domain_markers("x_min", [surf_id], "facet") + # for all panels + # Loop over all the finalized surfaces after fragmentation and tag everything + all_surf_tag_list = self.gmsh_model.occ.getEntities(self.ndim - 1) - # elif np.allclose(com[0], params.domain.x_max): - # self._add_to_domain_markers("x_max", [surf_id], "facet") + for surf_tag in all_surf_tag_list: + surf_id = surf_tag[1] + com = self.gmsh_model.occ.getCenterOfMass(self.ndim - 1, surf_id) - # elif np.allclose(com[1], params.domain.y_min): - # self._add_to_domain_markers("y_min", [surf_id], "facet") + located_this_surface = False - # elif np.allclose(com[1], params.domain.y_max): - # self._add_to_domain_markers("y_max", [surf_id], "facet") + for key, val in transformed_com.items(): + for target_com in val: + # print(target_com) + if np.allclose(np.array(com), target_com): + located_this_surface = True + if "trash" not in key: + # print(key) + self._add_to_domain_markers(key, [surf_id], "facet") + # if "interior_surface" not in key: + # self._add_to_domain_markers("structure_fluid_interface", [surf_id], "facet") - # elif np.allclose(com[2], params.domain.z_min): - # self._add_to_domain_markers("z_min", [surf_id], "facet") + if not located_this_surface: + print( + f"Warning: Surface {surf_tag} has not been added to domain markers" + ) - # elif np.allclose(com[2], params.domain.z_max): - # self._add_to_domain_markers("z_max", [surf_id], "facet") + # Since this is not one of the exterior walls, we should check if it extends + # past the boundaries x_min, x_max, ... + this_surf_bbox = self.gmsh_model.occ.get_bounding_box( + self.ndim - 1, surf_id + ) - # Volumes are the entities with dimension equal to the mesh dimension - vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) - structure_vol_list = [] - # fluid_vol_list = [] + # Test that the rotated point still exists in the box domain + if this_surf_bbox[0] < params.domain.x_min: + raise ValueError(f"A panel extends past the x_min wall.") + if this_surf_bbox[0] > params.domain.x_max: + raise ValueError(f"A panel extends past the x_max wall.") + if this_surf_bbox[1] < params.domain.y_min: + raise ValueError(f"A panel extends past the y_min wall.") + if this_surf_bbox[1] > params.domain.y_max: + raise ValueError(f"A panel extends past the y_max wall.") + if this_surf_bbox[2] < 0.0: + raise ValueError( + f"A panel extends past the z_min wall (ground level = 0.0)." + ) + if this_surf_bbox[2] > params.domain.z_max: + raise ValueError(f"A panel extends past the z_max wall.") + + # mark the panel and connector volumes + all_vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) - for vol_tag in vol_tag_list: + for vol_tag in all_vol_tag_list: vol_id = vol_tag[1] - structure_vol_list.append(vol_id) - # vol_id = vol_tag[1] + com = self.gmsh_model.occ.getCenterOfMass(self.ndim, vol_id) - # if vol_id <= params.pv_array.stream_rows * params.pv_array.span_rows: - # # Solid Cell + located_this_volume = False - # else: - # # Fluid Cell - # fluid_vol_list.append(vol_id) + for key, val in transformed_com.items(): + for target_com in val: + # print(target_com) + if np.allclose(np.array(com), target_com): + located_this_volume = True + if "trash" not in key: + self._add_to_domain_markers(key, [vol_id], "cell") - self._add_to_domain_markers("structure", structure_vol_list, "cell") - # self._add_to_domain_markers("fluid", fluid_vol_list, "cell") # Record all the data collected to domain_markers as physics groups with physical names # because we are creating domain_markers _within_ the build method, we don't need to @@ -1620,6 +2170,9 @@ def Rz(theta): ) self.gmsh_model.setPhysicalName(self.ndim - 1, data["idx"], key) + + + def set_length_scales_DEV(self, params, domain_markers): res_min = params.domain.l_char @@ -1754,7 +2307,7 @@ def set_length_scales(self, params, domain_markers): domain_markers[f"panel_back_{panel_id}"]["gmsh_tags"] ) - if self.modeling_torque_tube: + if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": for module_id in range(params.pv_array.modules_per_span): internal_surface_tags.extend( domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"][ @@ -1768,10 +2321,10 @@ def set_length_scales(self, params, domain_markers): ) else: internal_surface_tags.extend( - domain_markers[f"panel_bottom_{panel_id}"]["gmsh_tags"] + domain_markers[f"panel_bottom_{panel_id}_0"]["gmsh_tags"] ) internal_surface_tags.extend( - domain_markers[f"panel_top_{panel_id}"]["gmsh_tags"] + domain_markers[f"panel_top_{panel_id}_0"]["gmsh_tags"] ) # print(internal_surface_tags) diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index e6cc636..ef28fb1 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -704,41 +704,54 @@ def P_connector(u): self.z_unit_vector = dolfinx.fem.Constant( domain.structure.msh, [0.0, 0.0, 1.0] ) # surface traction, N/m^2 - - self.calculate_K_for_Robin_BC(domain, flow, params) + + if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + self.calculate_K_for_Robin_BC(domain, flow, params) dx_structure = ufl.Measure( "dx", domain=domain.structure.msh, subdomain_data=domain.structure.cell_tags ) - - # To Do: how to differentiate the connector part and the panel part, dx_connector and dx_panel - self.res = ( - m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure - + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure - + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) - * dx_structure(domain.domain_markers["modules"]["idx"]) - + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) - * dx_structure(domain.domain_markers["connectors"]["idx"]) - - structure.rho - * ufl.inner(self.f, self.u_) - * dx_structure(domain.domain_markers["modules"]["idx"]) - - structure.rho_connector - * ufl.inner(self.f, self.u_) - * dx_structure(domain.domain_markers["connectors"]["idx"]) - - ufl.dot(ufl.dot(self.stress_predicted * J * ufl.inv(F.T), n), self.u_) - * self.ds - ) # - Wext(self.u) - - # Robin boundary condition terms - for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): - for i in range(params.pv_array.modules_per_span + 1): - name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" - K_springs = dolfinx.fem.Constant( - domain.structure.msh, float(getattr(self, name_K)) - ) - self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector) * self.ds( - domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]["idx"] - ) + + if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + self.res = ( + m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure + + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure + + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) + * dx_structure(domain.domain_markers["modules"]["idx"]) + + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) + * dx_structure(domain.domain_markers["connectors"]["idx"]) + - structure.rho + * ufl.inner(self.f, self.u_) + * dx_structure(domain.domain_markers["modules"]["idx"]) + - structure.rho_connector + * ufl.inner(self.f, self.u_) + * dx_structure(domain.domain_markers["connectors"]["idx"]) + - ufl.dot(ufl.dot(self.stress_predicted * J * ufl.inv(F.T), n), self.u_) + * self.ds + ) # - Wext(self.u) + + # Robin boundary condition terms + for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): + for i in range(params.pv_array.modules_per_span + 1): + name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" + K_springs = dolfinx.fem.Constant( + domain.structure.msh, float(getattr(self, name_K)) + ) + self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector) * self.ds( + domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]["idx"] + ) + else: + self.res = ( + m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure + + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure + + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) + * dx_structure + - structure.rho + * ufl.inner(self.f, self.u_) + * dx_structure + - ufl.dot(ufl.dot(self.stress_predicted * J * ufl.inv(F.T), n), self.u_) + * self.ds + ) # - Wext(self.u) # self.a = dolfinx.fem.form(ufl.lhs(res)) # self.L = dolfinx.fem.form(ufl.rhs(res)) diff --git a/pvade/structure/boundary_conditions.py b/pvade/structure/boundary_conditions.py index 268458e..e87890c 100644 --- a/pvade/structure/boundary_conditions.py +++ b/pvade/structure/boundary_conditions.py @@ -344,39 +344,79 @@ def connection_point_up_helper(nodes_to_pin_between): return fn_handle - # # Start pinning along the lines expressed byt numpy_pt_total_array - # The center line of connectors bottom surface is fixed to remove rigid body motion - if params.structure.tube_connection == True: - # If making torque tube connections, pass only those pinning lines to the BC identification function - tube_nodes = domain.numpy_pt_total_array[:, :] + if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + # # Start pinning along the lines expressed byt numpy_pt_total_array + # The center line of connectors bottom surface is fixed to remove rigid body motion - facet_uppoint = dolfinx.mesh.locate_entities( - domain.structure.msh, 1, connection_point_up_helper(tube_nodes) - ) - dofs_disp = dolfinx.fem.locate_dofs_topological( - functionspace, 1, [facet_uppoint] - ) + if params.structure.tube_connection == True: + # If making torque tube connections, pass only those pinning lines to the BC identification function + tube_nodes = domain.numpy_pt_total_array[:, :] - bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) + facet_uppoint = dolfinx.mesh.locate_entities( + domain.structure.msh, 1, connection_point_up_helper(tube_nodes) + ) + dofs_disp = dolfinx.fem.locate_dofs_topological( + functionspace, 1, [facet_uppoint] + ) - if params.structure.motor_connection == True: - # The bottom surface of the center connector is fixed to represent the motor mount - motor_location = ( - params.pv_array.fixed_location - ) # fixed location along the span, if fixed_location=5, it means the left boundary of the 6th connector is fixed - for panel_id in range(total_num_panels): + bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) - mount_facet = domain.structure.facet_tags.find( - domain.domain_markers[ - f"block_left_{panel_id:.0f}_{motor_location:.0f}" - ]["idx"] - ) + if params.structure.motor_connection == True: + # The bottom surface of the center connector is fixed to represent the motor mount + motor_location = ( + params.pv_array.fixed_location + ) # fixed location along the span, if fixed_location=5, it means the left boundary of the 6th connector is fixed + for panel_id in range(total_num_panels): + + mount_facet = domain.structure.facet_tags.find( + domain.domain_markers[ + f"block_left_{panel_id:.0f}_{motor_location:.0f}" + ]["idx"] + ) + + dofs_disp = dolfinx.fem.locate_dofs_topological( + functionspace, 2, [mount_facet] + ) + + bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) + else: + # Start pinning along the lines expressed byt numpy_pt_total_array + # First, determine the total number of rows (number of lines to pin) + num_nodes = np.shape(domain.numpy_pt_total_array)[0] + # Determine how many pinning lines exist per each panel (e.g., 24 lines distributed on 8 panels means 3 lines per panel) + nodes_per_panel = int(num_nodes / total_num_panels) + + # The torque tube entry (oriented spanwise along the middle, divides panel into upstream and downstream rectangular halves) + # is always the first entry, e.g., [0, ..., ..., 3, ..., ..., 6, ..., ...], [0, 3, 6] are the torque tubes + tube_nodes_idx = np.arange(0, num_nodes, nodes_per_panel, dtype=np.int64) + + if params.structure.tube_connection == True: + # If making torque tube connections, pass only those pinning lines to the BC identification function + tube_nodes = domain.numpy_pt_total_array[tube_nodes_idx, :] + + facet_uppoint = dolfinx.mesh.locate_entities( + domain.structure.msh, 1, connection_point_up_helper(tube_nodes) + ) dofs_disp = dolfinx.fem.locate_dofs_topological( - functionspace, 2, [mount_facet] + functionspace, 1, [facet_uppoint] ) bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) + if params.structure.motor_connection == True: + # If making motor mount connections, pass only those pinning lines to the BC identification function + # this is done by making a copy of the numpy_pt_total_array with the torque tube lines *deleted* + # not done in place, so numpy_pt_total_array remains unaltered. + motor_nodes = np.delete(domain.numpy_pt_total_array, tube_nodes_idx, axis=0) + + facet_uppoint = dolfinx.mesh.locate_entities( + domain.structure.msh, 1, connection_point_up_helper(motor_nodes) + ) + dofs_disp = dolfinx.fem.locate_dofs_topological( + functionspace, 1, [facet_uppoint] + ) + + bc.append(dolfinx.fem.dirichletbc(zero_vec, dofs_disp, functionspace)) return bc diff --git a/pvade/tests/input/mesh/panels3d/domain_markers.yaml b/pvade/tests/input/mesh/panels3d/domain_markers.yaml index 2af5e53..50b75e6 100644 --- a/pvade/tests/input/mesh/panels3d/domain_markers.yaml +++ b/pvade/tests/input/mesh/panels3d/domain_markers.yaml @@ -1,110 +1,56 @@ -_current_idx: 135 -back_0: - entity: facet +_current_idx: 51 +fluid: + entity: cell gmsh_tags: - 8 - idx: 2 -back_1: - entity: facet - gmsh_tags: - - 17 - idx: 8 -back_10: - entity: facet - gmsh_tags: - - 98 - idx: 62 -back_11: - entity: facet - gmsh_tags: - - 107 - idx: 68 -back_12: - entity: facet - gmsh_tags: - - 116 - idx: 74 -back_13: - entity: facet - gmsh_tags: - - 125 - idx: 80 -back_14: - entity: facet - gmsh_tags: - - 134 - idx: 86 -back_15: - entity: facet - gmsh_tags: - - 143 - idx: 92 -back_16: - entity: facet - gmsh_tags: - - 152 - idx: 98 -back_17: - entity: facet - gmsh_tags: - - 161 - idx: 104 -back_18: - entity: facet - gmsh_tags: - - 170 - idx: 110 -back_19: - entity: facet - gmsh_tags: - - 179 - idx: 116 -back_2: - entity: facet - gmsh_tags: - - 26 - idx: 14 -back_20: - entity: facet + idx: 50 +modules: + entity: cell gmsh_tags: - - 188 - idx: 122 -back_3: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + idx: 49 +panel_back_0: entity: facet gmsh_tags: - - 35 - idx: 20 -back_4: + - 10 + idx: 4 +panel_back_1: entity: facet gmsh_tags: - - 44 - idx: 26 -back_5: + - 19 + idx: 10 +panel_back_2: entity: facet gmsh_tags: - - 53 - idx: 32 -back_6: + - 28 + idx: 16 +panel_back_3: entity: facet gmsh_tags: - - 62 - idx: 38 -back_7: + - 37 + idx: 22 +panel_back_4: entity: facet gmsh_tags: - - 71 - idx: 44 -back_8: + - 46 + idx: 28 +panel_back_5: entity: facet gmsh_tags: - - 80 - idx: 50 -back_9: + - 55 + idx: 34 +panel_back_6: entity: facet gmsh_tags: - - 89 - idx: 56 -bottom_0: + - 64 + idx: 40 +panel_bottom_0_0: entity: facet gmsh_tags: - 11 @@ -112,7 +58,7 @@ bottom_0: - 13 - 14 idx: 5 -bottom_1: +panel_bottom_1_0: entity: facet gmsh_tags: - 20 @@ -120,87 +66,7 @@ bottom_1: - 22 - 23 idx: 11 -bottom_10: - entity: facet - gmsh_tags: - - 101 - - 102 - - 103 - - 104 - idx: 65 -bottom_11: - entity: facet - gmsh_tags: - - 110 - - 111 - - 112 - - 113 - idx: 71 -bottom_12: - entity: facet - gmsh_tags: - - 119 - - 120 - - 121 - - 122 - idx: 77 -bottom_13: - entity: facet - gmsh_tags: - - 128 - - 129 - - 130 - - 131 - idx: 83 -bottom_14: - entity: facet - gmsh_tags: - - 137 - - 138 - - 139 - - 140 - idx: 89 -bottom_15: - entity: facet - gmsh_tags: - - 146 - - 147 - - 148 - - 149 - idx: 95 -bottom_16: - entity: facet - gmsh_tags: - - 155 - - 156 - - 157 - - 158 - idx: 101 -bottom_17: - entity: facet - gmsh_tags: - - 164 - - 165 - - 166 - - 167 - idx: 107 -bottom_18: - entity: facet - gmsh_tags: - - 173 - - 174 - - 175 - - 176 - idx: 113 -bottom_19: - entity: facet - gmsh_tags: - - 182 - - 183 - - 184 - - 185 - idx: 119 -bottom_2: +panel_bottom_2_0: entity: facet gmsh_tags: - 29 @@ -208,15 +74,7 @@ bottom_2: - 31 - 32 idx: 17 -bottom_20: - entity: facet - gmsh_tags: - - 191 - - 192 - - 193 - - 194 - idx: 125 -bottom_3: +panel_bottom_3_0: entity: facet gmsh_tags: - 38 @@ -224,7 +82,7 @@ bottom_3: - 40 - 41 idx: 23 -bottom_4: +panel_bottom_4_0: entity: facet gmsh_tags: - 47 @@ -232,7 +90,7 @@ bottom_4: - 49 - 50 idx: 29 -bottom_5: +panel_bottom_5_0: entity: facet gmsh_tags: - 56 @@ -240,7 +98,7 @@ bottom_5: - 58 - 59 idx: 35 -bottom_6: +panel_bottom_6_0: entity: facet gmsh_tags: - 65 @@ -248,507 +106,173 @@ bottom_6: - 67 - 68 idx: 41 -bottom_7: - entity: facet - gmsh_tags: - - 74 - - 75 - - 76 - - 77 - idx: 47 -bottom_8: - entity: facet - gmsh_tags: - - 83 - - 84 - - 85 - - 86 - idx: 53 -bottom_9: - entity: facet - gmsh_tags: - - 92 - - 93 - - 94 - - 95 - idx: 59 -fluid: - entity: cell - gmsh_tags: - - 22 - idx: 134 -front_0: - entity: facet - gmsh_tags: - - 7 - idx: 1 -front_1: - entity: facet - gmsh_tags: - - 16 - idx: 7 -front_10: - entity: facet - gmsh_tags: - - 97 - idx: 61 -front_11: - entity: facet - gmsh_tags: - - 106 - idx: 67 -front_12: - entity: facet - gmsh_tags: - - 115 - idx: 73 -front_13: - entity: facet - gmsh_tags: - - 124 - idx: 79 -front_14: - entity: facet - gmsh_tags: - - 133 - idx: 85 -front_15: - entity: facet - gmsh_tags: - - 142 - idx: 91 -front_16: - entity: facet - gmsh_tags: - - 151 - idx: 97 -front_17: - entity: facet - gmsh_tags: - - 160 - idx: 103 -front_18: - entity: facet - gmsh_tags: - - 169 - idx: 109 -front_19: - entity: facet - gmsh_tags: - - 178 - idx: 115 -front_2: - entity: facet - gmsh_tags: - - 25 - idx: 13 -front_20: - entity: facet - gmsh_tags: - - 187 - idx: 121 -front_3: - entity: facet - gmsh_tags: - - 34 - idx: 19 -front_4: - entity: facet - gmsh_tags: - - 43 - idx: 25 -front_5: - entity: facet - gmsh_tags: - - 52 - idx: 31 -front_6: - entity: facet - gmsh_tags: - - 61 - idx: 37 -front_7: - entity: facet - gmsh_tags: - - 70 - idx: 43 -front_8: - entity: facet - gmsh_tags: - - 79 - idx: 49 -front_9: - entity: facet - gmsh_tags: - - 88 - idx: 55 -left_0: +panel_front_0: entity: facet gmsh_tags: - 9 idx: 3 -left_1: +panel_front_1: entity: facet gmsh_tags: - 18 idx: 9 -left_10: - entity: facet - gmsh_tags: - - 99 - idx: 63 -left_11: - entity: facet - gmsh_tags: - - 108 - idx: 69 -left_12: - entity: facet - gmsh_tags: - - 117 - idx: 75 -left_13: - entity: facet - gmsh_tags: - - 126 - idx: 81 -left_14: - entity: facet - gmsh_tags: - - 135 - idx: 87 -left_15: - entity: facet - gmsh_tags: - - 144 - idx: 93 -left_16: - entity: facet - gmsh_tags: - - 153 - idx: 99 -left_17: - entity: facet - gmsh_tags: - - 162 - idx: 105 -left_18: - entity: facet - gmsh_tags: - - 171 - idx: 111 -left_19: - entity: facet - gmsh_tags: - - 180 - idx: 117 -left_2: +panel_front_2: entity: facet gmsh_tags: - 27 idx: 15 -left_20: - entity: facet - gmsh_tags: - - 189 - idx: 123 -left_3: +panel_front_3: entity: facet gmsh_tags: - 36 idx: 21 -left_4: +panel_front_4: entity: facet gmsh_tags: - 45 idx: 27 -left_5: +panel_front_5: entity: facet gmsh_tags: - 54 idx: 33 -left_6: +panel_front_6: entity: facet gmsh_tags: - 63 idx: 39 -left_7: - entity: facet - gmsh_tags: - - 72 - idx: 45 -left_8: - entity: facet - gmsh_tags: - - 81 - idx: 51 -left_9: - entity: facet - gmsh_tags: - - 90 - idx: 57 -right_0: - entity: facet - gmsh_tags: - - 10 - idx: 4 -right_1: - entity: facet - gmsh_tags: - - 19 - idx: 10 -right_10: +panel_left_0: entity: facet gmsh_tags: - - 100 - idx: 64 -right_11: - entity: facet - gmsh_tags: - - 109 - idx: 70 -right_12: - entity: facet - gmsh_tags: - - 118 - idx: 76 -right_13: - entity: facet - gmsh_tags: - - 127 - idx: 82 -right_14: - entity: facet - gmsh_tags: - - 136 - idx: 88 -right_15: - entity: facet - gmsh_tags: - - 145 - idx: 94 -right_16: + - 7 + idx: 1 +panel_left_1: entity: facet gmsh_tags: - - 154 - idx: 100 -right_17: + - 16 + idx: 7 +panel_left_2: entity: facet gmsh_tags: - - 163 - idx: 106 -right_18: + - 25 + idx: 13 +panel_left_3: entity: facet gmsh_tags: - - 172 - idx: 112 -right_19: + - 34 + idx: 19 +panel_left_4: entity: facet gmsh_tags: - - 181 - idx: 118 -right_2: + - 43 + idx: 25 +panel_left_5: entity: facet gmsh_tags: - - 28 - idx: 16 -right_20: + - 52 + idx: 31 +panel_left_6: entity: facet gmsh_tags: - - 190 - idx: 124 -right_3: + - 61 + idx: 37 +panel_right_0: entity: facet gmsh_tags: - - 37 - idx: 22 -right_4: + - 8 + idx: 2 +panel_right_1: entity: facet gmsh_tags: - - 46 - idx: 28 -right_5: + - 17 + idx: 8 +panel_right_2: entity: facet gmsh_tags: - - 55 - idx: 34 -right_6: + - 26 + idx: 14 +panel_right_3: entity: facet gmsh_tags: - - 64 - idx: 40 -right_7: + - 35 + idx: 20 +panel_right_4: entity: facet gmsh_tags: - - 73 - idx: 46 -right_8: + - 44 + idx: 26 +panel_right_5: entity: facet gmsh_tags: - - 82 - idx: 52 -right_9: + - 53 + idx: 32 +panel_right_6: entity: facet gmsh_tags: - - 91 - idx: 58 -structure: - entity: cell - gmsh_tags: - - 1 - - 2 - - 3 - - 4 - - 5 - - 6 - - 7 - - 8 - - 9 - - 10 - - 11 - - 12 - - 13 - - 14 - - 15 - - 16 - - 17 - - 18 - - 19 - - 20 - - 21 - idx: 133 -top_0: + - 62 + idx: 38 +panel_top_0_0: entity: facet gmsh_tags: - 15 idx: 6 -top_1: +panel_top_1_0: entity: facet gmsh_tags: - 24 idx: 12 -top_10: - entity: facet - gmsh_tags: - - 105 - idx: 66 -top_11: - entity: facet - gmsh_tags: - - 114 - idx: 72 -top_12: - entity: facet - gmsh_tags: - - 123 - idx: 78 -top_13: - entity: facet - gmsh_tags: - - 132 - idx: 84 -top_14: - entity: facet - gmsh_tags: - - 141 - idx: 90 -top_15: - entity: facet - gmsh_tags: - - 150 - idx: 96 -top_16: - entity: facet - gmsh_tags: - - 159 - idx: 102 -top_17: - entity: facet - gmsh_tags: - - 168 - idx: 108 -top_18: - entity: facet - gmsh_tags: - - 177 - idx: 114 -top_19: - entity: facet - gmsh_tags: - - 186 - idx: 120 -top_2: +panel_top_2_0: entity: facet gmsh_tags: - 33 idx: 18 -top_20: - entity: facet - gmsh_tags: - - 195 - idx: 126 -top_3: +panel_top_3_0: entity: facet gmsh_tags: - 42 idx: 24 -top_4: +panel_top_4_0: entity: facet gmsh_tags: - 51 idx: 30 -top_5: +panel_top_5_0: entity: facet gmsh_tags: - 60 idx: 36 -top_6: +panel_top_6_0: entity: facet gmsh_tags: - 69 idx: 42 -top_7: - entity: facet - gmsh_tags: - - 78 - idx: 48 -top_8: - entity: facet - gmsh_tags: - - 87 - idx: 54 -top_9: - entity: facet - gmsh_tags: - - 96 - idx: 60 x_max: entity: facet gmsh_tags: - - 6 - idx: 132 + - 75 + idx: 48 x_min: entity: facet gmsh_tags: - - 1 - idx: 127 + - 70 + idx: 43 y_max: entity: facet gmsh_tags: - - 4 - idx: 130 + - 73 + idx: 46 y_min: entity: facet gmsh_tags: - - 2 - idx: 128 + - 71 + idx: 44 z_max: entity: facet gmsh_tags: - - 3 - idx: 129 + - 72 + idx: 45 z_min: entity: facet gmsh_tags: - - 5 - idx: 131 + - 74 + idx: 47 diff --git a/pvade/tests/input/mesh/panels3d/fluid_mesh.h5 b/pvade/tests/input/mesh/panels3d/fluid_mesh.h5 index d382a0c5e2c4c7a7a8d796169063e0ed822352cb..4f3f15711349cb2528e6fb2bc4132274817a815e 100644 GIT binary patch literal 204608 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 z%0D{!gJOr=-no0<_Mo|;FLw>DvwX=DKD@nPZjX!}{kdQ6<{+H;%r9~1Z}Z2TF*nR_ z@7w$}Zx4w*xUxQ_fApZkn-lcl!{d9O;*byVlP}|@kKb_inEc3_JeGXy$ye#u_~=D9@8RXYq^pBDCtvL9UvZh+rGNMniu}Pl*(vqQ z54!yDhoAaKJo+6UT_2Zx@!<1EKOY|dy$5q%{Wl9=|CI5%%Lkl!HZk(%nEIPjBRk=n z3*_h_$NwWDFQ4K(ICOauw|uA%T*+tuYtd8Ar?UU_-I2ksoYQXK`-(2;KS2*bnL5>eA0ht`1~@j>Ek~;aQN~^PbshO^}G71kGp!RGrqjpGyLJl z>hWJ4;N@Xd_y^|H|Ln;JI{WziLx-0?^V+*mANG%lUH!RL?)qqW?&QToexvZ^k$(76 zenRlO=Jc-3PkeIXqn91=>2va>--pFcIe)zybU5LaC9O zaQKJEe|0VUz`Hp*IP*z<+`R{LtmGdXdG7^YJo3Xo{=%D2-Wh%JtWNTKTmMZ%xP+7w3_of=las0Yo6bE4;-jw=6yb& z!~NXXecji6|JMKidHoc=eQW&Y&HSF4zQpM}&&Eg3_>H&Y_Ag!g#QeeS1M4pCUO6p& z8K=JZrzhU$lb$;Ek9ztM*RS!Lzux!e(SEisj!z%6eZ?`%~OLh8v%KOaJ+)XZ_foC+pJt@2SbR9(?Qef{M#GPUqeD;Pz|B z`FQp*zIio2JrBlyk<-IWT;oP~0jDsG1eOfR3!G1Fj=8ukf z>iK?A>g%KHTdx;~XFcikd2pW1zjKb)aed==|LWLJ9j9;pHl0WgZZ<*_Epc1bu;c>KXugCr+Vt^3$8wWeVmc`QP25+t1r*F&^I1EJb84_Or4j+ zo)s&<>x#cLJay?=PwRbhc+Ladeihd@9evx6`ljPN$+xfY>6qu2r4MoIFYesvOC9~{ zPhRV(rw@7N!~U4YI1t#j}6-`10x7N8Xd|*U{I>8Hf3&-|LAdt}Y$>r00)L=c{M^=*Xu}x7UZReMs-@jK_M* zv(EH4*0}M-&8NQfWBlgRy5NgjA9&}B(|LLNS!>-mF^{0RN=3QJK3&)qQKYi&_yyyGX z8K*dl*E+Ml)um^B%mZD0zoyp3JX;68c{L7JJv#XG)$RG|xOv8-PgfoNn-}B67pE_e zj<~+?=!hG?JmXSFzpeAy%%?p3);~AA**Nq?r}JlZ@#xDp9{q^pw92*53KKj z^*yk@2iEt%1Mhd-Q9EtedBY9z{}=X67vvA*@!MP_!1+Jw$p7E|P8+VgVav>A`gYo| z-a@!C&0;gbf=`-QutRXMG0NXJCB>)@NXS23F00=hD_^%0DvT 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 literal 365824 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 diff --git a/pvade/tests/input/mesh/panels3d/fluid_mesh.xdmf b/pvade/tests/input/mesh/panels3d/fluid_mesh.xdmf index c60059d..ba06796 100644 --- a/pvade/tests/input/mesh/panels3d/fluid_mesh.xdmf +++ b/pvade/tests/input/mesh/panels3d/fluid_mesh.xdmf @@ -3,29 +3,29 @@ - - fluid_mesh.h5:/Mesh/fluid_mesh.xdmf/topology + + fluid_mesh.h5:/Mesh/fluid_mesh.xdmf/topology - fluid_mesh.h5:/Mesh/fluid_mesh.xdmf/geometry + fluid_mesh.h5:/Mesh/fluid_mesh.xdmf/geometry - - fluid_mesh.h5:/MeshTags/cell_tags/topology + + fluid_mesh.h5:/MeshTags/cell_tags/topology - fluid_mesh.h5:/MeshTags/cell_tags/Values + fluid_mesh.h5:/MeshTags/cell_tags/Values - - fluid_mesh.h5:/MeshTags/facet_tags/topology + + fluid_mesh.h5:/MeshTags/facet_tags/topology - fluid_mesh.h5:/MeshTags/facet_tags/Values + fluid_mesh.h5:/MeshTags/facet_tags/Values diff --git a/pvade/tests/input/mesh/panels3d/numpy_fixation_points.csv b/pvade/tests/input/mesh/panels3d/numpy_fixation_points.csv index 50ae95a..cc278ee 100644 --- a/pvade/tests/input/mesh/panels3d/numpy_fixation_points.csv +++ b/pvade/tests/input/mesh/panels3d/numpy_fixation_points.csv @@ -1,18 +1,4 @@ # start_x,start_y,start_z,end_x,end_y,end_z, -2.499999999999857891e-02,-1.050000000000000000e+01,1.456698729810778081e+00,2.499999999999857891e-02,-3.500000000000000000e+00,1.456698729810778081e+00 --8.410254037844389075e-01,-7.000000000000000000e+00,9.566987298107780813e-01,8.910254037844396180e-01,-7.000000000000000000e+00,1.956698729810778081e+00 -7.025000000000000355e+00,-1.050000000000000000e+01,1.456698729810778081e+00,7.025000000000000355e+00,-3.500000000000000000e+00,1.456698729810778081e+00 -6.158974596215561093e+00,-7.000000000000000000e+00,9.566987298107780813e-01,7.891025403784439618e+00,-7.000000000000000000e+00,1.956698729810778081e+00 -1.402500000000000036e+01,-1.050000000000000000e+01,1.456698729810778081e+00,1.402500000000000036e+01,-3.500000000000000000e+00,1.456698729810778081e+00 -1.315897459621556109e+01,-7.000000000000000000e+00,9.566987298107780813e-01,1.489102540378443962e+01,-7.000000000000000000e+00,1.956698729810778081e+00 -2.102499999999999858e+01,-1.050000000000000000e+01,1.456698729810778081e+00,2.102499999999999858e+01,-3.500000000000000000e+00,1.456698729810778081e+00 -2.015897459621556109e+01,-7.000000000000000000e+00,9.566987298107780813e-01,2.189102540378443962e+01,-7.000000000000000000e+00,1.956698729810778081e+00 -2.802499999999999858e+01,-1.050000000000000000e+01,1.456698729810778081e+00,2.802499999999999858e+01,-3.500000000000000000e+00,1.456698729810778081e+00 -2.715897459621556109e+01,-7.000000000000000000e+00,9.566987298107780813e-01,2.889102540378443962e+01,-7.000000000000000000e+00,1.956698729810778081e+00 -3.502499999999999858e+01,-1.050000000000000000e+01,1.456698729810778081e+00,3.502499999999999858e+01,-3.500000000000000000e+00,1.456698729810778081e+00 -3.415897459621556465e+01,-7.000000000000000000e+00,9.566987298107780813e-01,3.589102540378443962e+01,-7.000000000000000000e+00,1.956698729810778081e+00 -4.202499999999999858e+01,-1.050000000000000000e+01,1.456698729810778081e+00,4.202499999999999858e+01,-3.500000000000000000e+00,1.456698729810778081e+00 -4.115897459621556465e+01,-7.000000000000000000e+00,9.566987298107780813e-01,4.289102540378443962e+01,-7.000000000000000000e+00,1.956698729810778081e+00 2.499999999999857891e-02,-3.500000000000000000e+00,1.456698729810778081e+00,2.499999999999857891e-02,3.500000000000000000e+00,1.456698729810778081e+00 -8.410254037844389075e-01,0.000000000000000000e+00,9.566987298107780813e-01,8.910254037844396180e-01,0.000000000000000000e+00,1.956698729810778081e+00 7.025000000000000355e+00,-3.500000000000000000e+00,1.456698729810778081e+00,7.025000000000000355e+00,3.500000000000000000e+00,1.456698729810778081e+00 @@ -27,17 +13,3 @@ 3.415897459621556465e+01,0.000000000000000000e+00,9.566987298107780813e-01,3.589102540378443962e+01,0.000000000000000000e+00,1.956698729810778081e+00 4.202499999999999858e+01,-3.500000000000000000e+00,1.456698729810778081e+00,4.202499999999999858e+01,3.500000000000000000e+00,1.456698729810778081e+00 4.115897459621556465e+01,0.000000000000000000e+00,9.566987298107780813e-01,4.289102540378443962e+01,0.000000000000000000e+00,1.956698729810778081e+00 -2.499999999999857891e-02,3.500000000000000000e+00,1.456698729810778081e+00,2.499999999999857891e-02,1.050000000000000000e+01,1.456698729810778081e+00 --8.410254037844389075e-01,7.000000000000000000e+00,9.566987298107780813e-01,8.910254037844396180e-01,7.000000000000000000e+00,1.956698729810778081e+00 -7.025000000000000355e+00,3.500000000000000000e+00,1.456698729810778081e+00,7.025000000000000355e+00,1.050000000000000000e+01,1.456698729810778081e+00 -6.158974596215561093e+00,7.000000000000000000e+00,9.566987298107780813e-01,7.891025403784439618e+00,7.000000000000000000e+00,1.956698729810778081e+00 -1.402500000000000036e+01,3.500000000000000000e+00,1.456698729810778081e+00,1.402500000000000036e+01,1.050000000000000000e+01,1.456698729810778081e+00 -1.315897459621556109e+01,7.000000000000000000e+00,9.566987298107780813e-01,1.489102540378443962e+01,7.000000000000000000e+00,1.956698729810778081e+00 -2.102499999999999858e+01,3.500000000000000000e+00,1.456698729810778081e+00,2.102499999999999858e+01,1.050000000000000000e+01,1.456698729810778081e+00 -2.015897459621556109e+01,7.000000000000000000e+00,9.566987298107780813e-01,2.189102540378443962e+01,7.000000000000000000e+00,1.956698729810778081e+00 -2.802499999999999858e+01,3.500000000000000000e+00,1.456698729810778081e+00,2.802499999999999858e+01,1.050000000000000000e+01,1.456698729810778081e+00 -2.715897459621556109e+01,7.000000000000000000e+00,9.566987298107780813e-01,2.889102540378443962e+01,7.000000000000000000e+00,1.956698729810778081e+00 -3.502499999999999858e+01,3.500000000000000000e+00,1.456698729810778081e+00,3.502499999999999858e+01,1.050000000000000000e+01,1.456698729810778081e+00 -3.415897459621556465e+01,7.000000000000000000e+00,9.566987298107780813e-01,3.589102540378443962e+01,7.000000000000000000e+00,1.956698729810778081e+00 -4.202499999999999858e+01,3.500000000000000000e+00,1.456698729810778081e+00,4.202499999999999858e+01,1.050000000000000000e+01,1.456698729810778081e+00 -4.115897459621556465e+01,7.000000000000000000e+00,9.566987298107780813e-01,4.289102540378443962e+01,7.000000000000000000e+00,1.956698729810778081e+00 diff --git a/pvade/tests/input/mesh/panels3d/structure_mesh.h5 b/pvade/tests/input/mesh/panels3d/structure_mesh.h5 index d4acd84ea9145f2e28c034cac979823ccc34f80a..9c47e33932b30d58f602f084bb147cf96a179309 100644 GIT binary patch literal 39292 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 literal 93580 zcmeI*3Ebu9csKATi-?#Y1lhz`LhM0eNy0oKA@)Rw5X%sg1c^ivM5RoHHfnFBBC1lc zr6u;FRi>z_t+h*w4n^%d6YIR^|D50VIp_Rlj>+f!y!E!s>F3OKuHW^$mixM|`~II9 z>GM4E@I&vi^GR7kR-}Vk1e%K*L3~M|l3tbucfy`Od zg?p_z{lww(An*6^6HbSl&)WemqntOthi^9yaAM|iOY{4+D~$O&UdFszX}?_^m^Z)Y zWPYsn^fOL-{BdVI{Sa+OqCk?uW1iIlc$BS-uDM&D@I9bG(Iczq96T7Is+rK0E$n z_{KWk!ELr3cXGdcAK5+~YJI!W_uqkemC*$-z5^DQn3o%A1HSFH<3Ky+8}oMCoU`*1 zVPWAekxSyL@g98EAGqC`g@se6&A+kFo-e-NpL5i&Pd#4q!oq%6{Pn!wHLJdu8|xoW z6>T&h`G}Xt`7&XLe1eNhBDTM{xfVu$4;;V5A+H=cZ6$haI24df@y!|CJI)9i(-LhX)s*U3_-ykxyLpT8HCt)zmR>UE=c-2fzK* z$4?*BqfT{MAIUy%ee!mmYi1q3UtS~n_#Yk?7Sw;;;Jtq9+Ozu6>tn~aPVv90J_7#qIgV;TXuND1NO_;5V9y!d$R@RiBO zzxUUEwVmIs%U|96NlSKHh*yb{Mh9;kMhd1GI{0c_-*g`xK;db z7l|*wxc1rp_WX$3_S+|J@1K3QpLF}qkK}JZ`Pnb~4#%_4c=nrJU-kKxsaJpDy-)1= zNAG>4^Vd)PWf!OS-+t(C`_uIu&;A^e_4K;rv+w-mQy-o>@!9RyfyraPdOz(8`~JnZ zANB{IZvWu;hbE4A?Dz-7zAAFR$h$;#f9$vYq+35eyZ*rO*!2mn&-M+D$F9%f-z)KYUkOTR-64H$43k2hP87>*Du-<_y+&^LX;$Kz-JTR*z$vA^A?Bf}$`XP@<-zrM1oOWnQC^j<%|V^YWJ$h$_~CsJPf zt)8yqxbUs#fzkDEF}gXhKlH9o-FJ`Q-69_yc~s%%2>-#cKPD3Xkm&d)MTg^wZ!Xoj z7{2)vA5UENp2w5RUmwJkr~Ubr|(H z!137CuWs|9esgZ$&4GC|&(072qf;+C-FYzo_~OdLk2Hto6u)ut`QcfYy6=(t%%QmY zVZQjo`Qf)+9z6E0lij}9Cv#|j*v%;%kA2>po{@E$x7E>m4)NF@AKuzXbIA|S{IZ+l zhsTc{&hHViuPvT9c-Cj`&AUFZn``zpi3`WmXLb_r590U8Nb_la?Wg%N2XwriLp*kI z9-BN}pZ!%Y-YMa$du??4()&p7{ahVC{`SLuvFnGv>XZD>P8{{)vFqb=!aFna#7K6y zc-Bepc(?*+J1u@siF|70AI-r}56& z!0j)coxiy3FUWfMnQQC*i`btRX?@O(c{cCnoZbAITXuQO?ei1gc~O`C;H#U?&R;xz zW7ltX^~%Rzea<7kbA^w`51$?1`q}w8w_TUI+4*%H`lBEA&sM7BtB1dJ;h*xjdSZ<;W;OG&I?=|=hOWGw_fLpu20s@-f{6eFI@lGQfn$N#hF?9Yt;^2irQo*QXyo!|3yylao!kR zo$7N>!0BC|bEOXF&3SJ7Mag$|q;x#Uw`F%^`yfpH;k>>oZWq(caD?b|bh^B2#4;=Q}_+0Qoxx8L@2U2u8v@kn-l>b4)& z+x_ePu^)KdH++4yulDVAsaG75{U2f%pKQDRl?P8e>v>t?{dMGPBQJ>j%gDcr#ApB8 z;O~fp^P|5vcJcAw7hE6M^}&9di;KfIAM}4L{M_i~hQB#`ee6A#=E45U-*M!Z_nmX{ zcf7x^yz=0&zcM`Yua3?G|3LWaEar?uaSL@{u@A%!Hd3^m6S3LfGKfvveym<5OkBid} z=XKusb)MMQ#m~H&-#({!ZFerkb$*>s_`fBO^ULoO!Rh?)OekSqyobiXZ zKR`0IVbkjKDQliUc~)i*5&+sIJ$b(&n`b*UE;BSuHxW-B)GWp zSTFz2hxfL~w?}>~()o1mtoMSln`?IS&QCx1Ik))E6`nq}KfXA0`ywxY^J}i1FZ258 ztm9pg?~nXar1{jxb-|sle~P|7QXcWiPsHwg{B!gzohx?h==$yBy2P`;>TUbHxi_EY z%pCFO$1cxzl9#`^=Fbi{mu;5^uKu2HIG*_Y)yFQMIQZ=5-`xL4)?@zJKOX#TW&fAx z?ELV=`$GB2XI|FF-ur7FKN-7u{ZjNV7jBL}Rd#s$n_qKgUd<=_R}&A9{RhGEE{Tr6 zzU=(vXGoR+t+~TPZPn~#e7som~kGcC|>g>5S-~So=_lsxl@!I~Y@Xa;8 zJZ*kc1k?{SB}1Y zB!6>huFa+S)>r$WZ`QFx;_2^CQt#!1>o*~QA*ACJy%ZrX0n%m=%5nQ#5H4mjSGvL179-E=(tfo~H({^I8U{eIzU zv2Pdo!|W$NJowd%e~swQA71O%3{PC=R-M+Pe*Vo9hvbhhzWuQ;_Cf#qd^^W@&Jo-> z<7d9im3;W5c=jKUpE%Abzg^O=UuHipjl4o|=Tlwkb$-QR7r%LJ?|R%1;nnr%`HAV-MztIytcb9@YvzzkKKIXnKSjYoj;yA#Dnv9-geD?I)~1i zbJn=?<=lyb$If3J^0M=HzMLO+QvDrgo*zD`FZlXkE}ReN%G{dwJ<}g^zgx|}c@|F` zy7_eO#Ak=AgWY+-gNxVaa`)7=N6F?n*VaY1Uh8yz$*0fiX`Elz&CmYYH@vpX zi`RMR)?xkP=>uH9^tt`v_@sEfUhBjYPaZlyJbvuf3FkjAuRhJ|3qSF#OI&qq>E{Yv zJNvy|o|~<2H~M)&17(+$tCoMR(Ee9k*neXje$CGpI(8iLYkMU0lGya;3!OM^{%`+0 z@8_-c&!OAW&s*E=LE}<)`){v!Yl8oY9hmp?))pQxTkUacPCDsfXH0%z=?>F2tcyJIr9;`JQ6Usva8g7?`cjT2t_Z?i!~ervy zKJ2wlAAWJls7x# zsMC6nA6{SGbFugF{9ysR@3mZ~`t@yxrN-^GC=Ptw&lStK`meHN_{A#qXTP%E73((N z2kY8o-xsZGHt#;md21dP%|qo~l-E9v>%a71`0PF|Ea<~*pC@yu&*?+v)K~|U5n$y@7{I@wDvlS_gy-tmoCr4`0Mu7v))Ucuk=%$8|yr-SloR5 zlk;L8epz!qI}he_!z%M^uJmnoz4#kfop|x+_rQ5a-uKD-e_*mMbM=uAk6tToy1ZWdZokr>{Jg)VZ5He`n`&#qvElJ3s2&uxhDqJ51JPu6{Xlz9#2xwm*||I*U)v(JVfh zL+jNC^HAq*`J8@u-;3_B$Cta#+=oBif0KFJ<`vg_-kbKF%$xZ%hvrS**?pYMx%nKQ zugQ6_zdx?$xbr^dPxf&(&Saje&&%gm{GRjKdC<=dt7dWi@B3=`|5~%9~hsSpV|GL%;9Wa_{LS^ee88@Ts7XW$-XPUdsX zka!34pr0FZuc~wMJ_duBuKZRZp|>i^7~s3-T*V-8!c-+J|-&&ATcH@au~ zdF0&T>wnEpz907U#d&dWEca*mo|)bE$+??7r<42K{Fn#p8uv|nc{hyzo<4eByO%aU z|K_C6*JK`+`h41?pX2)~{1;B<#CcqrlkJ*s9?aY1{+aFb)OneCzD(|!**Todo4M-y zWcIl;nZwz>>BHpw>a%^HjWhWkGCPOr**LzpN9V}76K84OesSn{-lqPYb7?+DbBk|( zm+rUaK2Pp9bKB<#-ru8U&-LW{=j{DC*%$SgfA!QkTAt_GeVokmEPrzEsdMSy+eiN% zHSW))tLi>k{`=Q#{K-8z)6b2Qe@C62+sXYfyI;;rzHf~7WwZSp;r^cGd-Ci(Fu9kk z*Zk<)?EFm5wfegcv-2tbadJzdui$sEq|Cv}?7 z?pyU?`R^Cihvojy##jHO&*s^AF(>euIbZHi&G~Zwt+$_3ll9K_Q{U3h<>wcCv@gH$ z-=B5rQ^sfWf5-37`oNi~c8(a|tE1oc^@KTu-%}196VaU?ow?_(|d0-!ZpIKOdi%a6QzqpqVq3z;bId<{ngSQ`E+vO2=m&8>E98aDr zgqP#6uygbkkyk4_K7aMV`FH*+C5}2s>ktnQEbYsXLD^~-JO3LcFCM#f!^N{d^1<=>!}X1wzx}gc_Ty%$ zhs3il&4b%V_FE*5eTC!QAa?(~>9wNYuyFguzHjX9&u{P8Zyag=?3=k@7axuXmlq$8 z9lkR8`1k(WueS5Mb@{8CpFHi)ZjS6f-M*V6c7E*m?B>G$-z4?jG?I=7zkcj+{_N)J z=HZzOe0I{jm?wO?`RaJLO?>-jKFkgLHsP_GNBzdL-{ud_pC7yY=22dGRwl1J9lz~8 zAGeDC?IQ8z7uP=9-<}_F+kX4R?ftXw_LFYk`H}qXCqMgT-{E-n8P9&R>#IKBGWF^& zy!VM+|LDDsbpHCOzwF}l{@V}zZGXDHvYuX-eD{ zJ4Uz9_E%ha*bfVz9gfHUe&JcCb?XPb`-Z1q;=uVgZe9Eyko;Yj{*da$KRkX%MCub9 zUtiU)KKrE}Ja%=tmDp+i;?$@Jht$AM89X@4~@>g7+qX^ zJnK{kp8a7r2l}Q@_;~#6f9pqAJ@&WzbYysB^X#+!^Ve5)b*a1encnN?cTDP79eLNt z`$WoXztz)q92dUzJTSWcEk-v7_J`i}sr&BnyIbU=BabTF9N|AW_QyoR9}*q^r08%w z@y(?=7sEGy;^T?S-t%~J`RjwY^0Yrcb+Xgd(fQ4zI_!tM?BMW zV4dpJ4>%sX`qgbd)Nju1yE!nA=Gpnde{||)r#lbkA75N~_>tz&oZ>ewK0iF`QujSl zpE(p)Kg<_@I6wTh%Y(<>b+X$R`(zI754$;q$AV=#XBW@b+3(XUwR+uy`QV&$KQU~FLwRVSACNI*@>fmJa&D2PIzZV zo*2mv7tcEB9gkgK^hq7~>Yo?q_|)-?$P*$T75U`IKZ#VgdfDN4zaRe7BY!VaTzStb z{7;L|{)fd=r@SwWU0#0rVEy`~f2YOoDUnZ&{G&Pe>B0G(AN`Lb`Lp-jJuN(QY3|kr z$6pbIUG|xqW`(J1^?eAAEJw z+4+m7Z|wTbu3q{0tIv7Fcdqd9_~EnTTR%HL=eFxoH#@(sLx1$c{+Wvl(@*Eyx$bjH zKRyEH`1KHwd}7co_zTH z#g|{5_|6;I{&*KBpMKj%`=_7w|HAO?zkPdVaQ@=iPrP?mKKuEm;P%^ot_vJ@Pt#=GXk2Z+7!+zVXeKd2;@m=bXUtKAO6mFLv?R;dttVt5aR<*8Mlh z^Pb3mt$wiMvpcsh3*R}tAUb=WKX&KTIc|Tr^J=~P;T^yGGmo!d;)=(=?+3X3kr!{? z{c&;n;k?c}zs?i;y7-w_^V{bXukFsIxX!Qh3IDg`aen!IA~>BNo_z85_`^SU7 zt?d62ot+<^cwZ<#`OM4u*n5A?<0oS`uV0G(<-*PJr^*g*fAee3%&Yli|7zmlvHu`A z-X+oT*O#5YJgtlCys+zwc>KSX_;~Gazs-d?wf{Yr|DJgK?C(#5_dfnOxH$OY;M2vy zQxAVU`N^Jp^Sq(z?Rhu9c;?ewnp-^e;i(g^?c!Kx=P`F*Or1Tq=KDWm|9? z6~4K~m#6J;=VoL4KO6a#$Ztmafzj-j6pzl1Z~o1vxiqKj+t)M^!F#J*Y7*hZ#tfRk>B}}*ZC3e7m54r$ZtjZJxAZ2 zWB*z3{{2Vd{ycU(@x>$YoVW^X&rDpzu(E+TQ?m~f8hQ5$;9=0sQjKK zzgOvpnHzq1aKGmY-|r!E{_tA&`>MosZq;c$>gV4)aY+97;@cnlVjuLs&$n}o=N!SE zGk)gFT*-$|if8}v_=)43^7DJ3ewqEaG}7-ub3WCjUguXFcJZ6n_O8eMATGOmfzFQ4 zF1~v4%&U9C+&c&6)ZF8<)7=~V#cR9!0*@VT{@BeIo;g!b+xg>}Lp(Tt=g05uat@t0 z=d5w(%efN=kDb3dBP-}6K_C-$8` zKXJ?ryyvFv_ElVSBc43^qaMGv&VItxDZe=qS3PZ42cEe4?DyvBvw9lm*LCx=ul5bE z?egMv9=df{zj*op*Drl;e>grVUa!|W@x+sd&JT|tyLH0(&&#V%^ZLS1eCrZd9ovka zhg;uUd$4r|w$8xT8Q3}lTW4VF4E%4J0lyb>xBR{S%#Y^79I<2YQ_q@1_#K5W>(7B4 zW@C8yIgs?}p98u55@Y4kpYwkX(7Cl?#9;7TRX6|16w<=wF6t~ zz_uZ57dd_gO}sp(Ao$Iri_<)~I6LG69RKoLz~#ZCi{pEWJY9eD@WtWRxHyfA$IrU( z=y;7^A&!@e#K&V_Q9L+5_A6E#_MHnCAKvlc>S2fTZ`?j!Dfz6M6px*Ri+|<#+ZX;M zJ3qR)Xr4Ip?A9X=e&h7cOYi#e;a!*Y%PS6?WEW?b^hdwNZQOd{c#VrAA3mKQon1ZZ zgX63G%!7r$}-aD8dN#_{p= zuW^3t>f_gaYy2wd+chG0Dhc~L||?W_ISBXQvL-GlELY2Eq* zH)n7>cKvBwpGbUh=x|j>g52x9$F&!G2sT z>%VrSeY2m9^K0Bb!}&K(7l-UR;44!Xzw1<;)~jyz=IaAKUdLDWbt_Nn;xxZ;`@p|- ze0J+lKU|*1%}K|Rhg}@;t)FD?IPCIXFMV+@xHlSi|1|F2fYY1rzL5tX-g)8Tk^7`x z_g3re#}2ek=yc=h|eeU4;BR*X}@knvpk8pmCiwp0#aQ@=i zzt+W5N3XMWd~xJ44{&wCJ3ky>-#fni?0p~HB>O?KUq3kAJ#o{*>CG39oqyx4cO3W4 z&61aNkFblcUOYG+oMaad?!Ib2Ja+l4lfSr)yDx8^^@!iPxNy37jmrxcw{iaPTci$k z@oU_A;dtmGOhmi;H*b z@SRil%e0dhh*2c*5$u#`l3$XW8mhxaeQ?(zj3-a;_@@!t@DG+ zYdvr}{_V1^m67_QUu|z3A5R~}YaCx4>~QhhP8Xlw?Nhh=-t&xf4)92P=YX8YXBW5Q zxZmJ(esK3;v49r$>S zACS6MMeY{~zhl`CiSArE7jU|{gyX}_C;L3V<~f)6{KaP{+h2Zu&PDU!_C+7>l=Zi+ zuXm1}uJ8QX-uPnt4~;bUo;&8f?euv(>ktng&X3)E;n6!kTpaw?^$Q;lZtl$CVOhU9 zWWP)B)(C58#njOYn-l6_~Mx(xP0&$DoIa0lfAQ)3j!a(t zVyEjHo;Y;;yCu$jBkx}NVsvxD&hMz$?-%L(xtGi*JDiTMuW&kk$6?2}kMnr!_L-mf zboP!j&tLuKK^(d|dw%f5Wrx$%11H7Dv+wuMKG55IBsbbk9qii1yAXY=HNACvyy zHS#`@;^93oxVfRTo6iS@w-`yc-|*(MSwc-yV%QuW_H!qp>AM{ek6Qe9CrQU*Zj8Q z!PNs-mvz$F?E@(vySnIjq`d5;e0XquS4YQZ#}^0getB&6#XSN)rEqrl3p>6z@Yea? zD{)VaJh&v>z1Vy>KYVqxj<25PJudm>!G|9aJDfi|-1k_xcyxC0=d>?3hb$D<(K7aV* z!}ooG&WKvS%A71O?txcR` zBF$&xt7C`bHEv%U$2YIy(DBVLJ6v2i9)CJJY3{7A@zo3C7hE2Ge-OKS$-PCgn^*Tx z+wsJ~BiY3v;pX*esYjjGO|t6?p7pT9=}%1Dj`O78?D+7Gvo?GZUp?Ztz3XE?JMrX~ z_vyj=yu+Oz_dw(A_l(4I-uc1t>3EIvlMkPc|A$q-c+NljJX{>{o8LHo-w%J3I^gaL zc;6dv_XqnqiSx&i;)rWbo)x?OrnB2;I=eY)z2o4C^X%m7a|d4&`y(Ui?7j!E!#y|X z{2J%qxbGe65(h3`5pB))u&tFz-pC>e>^%qobGwq zapc9Pi|0OhUg~k5xG$a`+xa|X z`L%96&4-if=Km+D$A0mbpB*j^oL}Sk?D+cKd^ldugL=$4UhDFQgU%y{CRoxx&7hl>bUIY@P*kI>u+3qJUTw9Pk5ww>Vb>XxO&CKhx4O% zUU^$AdSYz4`LM)gcaCJodj#9WRT#Akukxd35Kc@fXLAN5{V~ z_NPRe|Hhp!IKBCOj_~{bf#b8o)$^j%1(y%bkB&z=_x#}MXNS|(rJt`z9s2j`=;rzC z=#86aJa+d+>mA43G+#V(Ee^foz||w}Yf`uP&IKILT#I{A{780u^E;0ZmscGAjf)Sr z9)9A<3&-p8#SWJjE+0F*2QEJRjj7Ll(f1KM zoZj~g{KAUYxHx^^;gRCV!;k%y$@8j6xOFs+FW#G~PC7qv;2po?u;YsZe{H>@J5%S2BhQU=@3!5&^!MSp*BZC4?t`|& zJ5J-`ygT{a=jv4_JDh%T{2CXB-SdK9^V#{y3+G36-|*YMF7>(p^|SHw!-Kmo^#`th z9fvVUiF{$XMKdP$^v zwe9fcyMN&P8W-Qa*nD{BfpDB@!;Z$^N(4l z`|jN6?nU-_I6pl19)9Ea?m_m(J3pTI{6CO--NUbl4#$VL-#lDAarlWxs;ha8|5NJX zhfk{GpTomvhdb9F49~g#P;_?pB+2fcq`N!A`Kyy(>-_QQAFlQAgVUSe z_uWSlhaa8aM`M3Yr1R3adkap-e@Fb;FQ~Zi=F!FR{S03mI6k}ki;mwoe?05;oPk?c zHWxt>c}^w?5MRnp=4D=;jWOKfL3>%{e}Qk{=$NF3!6a#xM8gmr8ek z!ap55zs5Tbe}4G3Rdfex5dF5*ypIv-Bl3kr}@!|aGc%Mz3mqdOg z(ms7Py8DLY$Br)!oIa0_Xa8H5559g*eQ~O!}&EX zK3w0#|3=osAO6kY>~!|;#P0d!pjsj*sU) ze1H7DAKChsg5%@C@#SN09iLrYct1#JwAD;8VZvN@wIA3snjnl>dQSx-(ejMB! znX|@yA0R&-KRnXB&5Og1FCP3SiQ98!KJhk$N3xqw-$U3x7Czn=%D;7d_`JM$zTb3S zI4PdGNasv_jdy;0>($4fW?kxn+b{Ok@!9R4yd=B0)(aO${I>JM*N@i4VekC-`k>y% z+YitD;j620>wtGWxcw4O{qXkdzTlD8EgosTct6X2e>?KKk&Qc7KcB;gw_oGp|Dxi! z|K(wyhpYEjiR1g8dl^o5-@@_Xo+IpV_i@|d&8Lg+dBvZON6N>J&+hrqx;pr^ovshx zPru-F`5PA>uj|;Dc>TSM&hGmk-Seq&e!j;wAHMWU%162JFG(Hn_0fH=gyYK(=MTqY z_dLXFyz}7WsRwS|od@3W;Nq#1&My8Bvi`0YUp;svyZDWZ@B6?u+l&+WUO?YA_zzPD zeY?WN**>`MV|4!FHXkl-`&s|x65qNS|3>O;UgP}qrTOq3l8=AiYjivkUtIHk`HF`p z4m-Yk7*4|ZiNh{$*TW9y-#A^I9aFda+I~(3e$Gkr)XmPH&L6&W_Qf38 zFL=+JeP-v^c*ij>c;mmkjGd4a2^ak}`<13z^(j*q7fJhIoV zjw@w<*gdb{c#V7RZOKm@>l07C?5&I2c5&FRoci^L?m0UT@A})1u74dze)g-R4)?X^ zkLOw28^?G5H@|Uw65sQ$`EWev6fTZ)jMsYCEe;;ZUwqFwxIQ)Bb-PF8;SZN@m-N4% zlkk3S!tp&%-EUV-JnL02o!xVt9gfEiC*^VfH*Q^U@!MG-f4A%>Y2VF-=K;Un>zva0(ecHFmS@cH!hC*uKA7Yk9zp?V{g6l^4I@fFTCTxua&-t zYYs_v_Yz*?bbW7sIDh*E=db>~Yrp8-|Hjn`r^_oJyZ*uP*zE%yAFu7Li)+0d2j1(R z=Vu=~PWv}c-x{|LIN9sKlh=OH^%<^yb~xR-;oFV={b<{~^G`_&12(({e0XIfmOfu@ z>CXu^*oOD^dGifzNer0xG%gl7ZJYkgPO?qwI{-)Wq|uvg$0>aH2p>N0H2jg*AtO1w z7xNy8<<}5f{ln)We!s+>jtLw1mmj_LXxewchnW4I=JXl{+s@O3(dSipPs77{ycY9e z$Tz%)*A>TN`Oj(nvV86uhCo*sy-!ZWA%@QbzPZ3HXTP~^t`F8U)IKlo;6Lpv-XM1wy;qFhZKn@5uPf&R2_Iq%xgQIN95AoD zjy|x%?=r%M&xaySN!Sp3czbp3q0Xy}-UB|!!H@6s=MlW1*W`3fgI%uad;>nb*u)%s z@dkOdfmE-7o>QL(&NOeZ4Ygf&^zIj2&j-8D*9>`(d*t)s zC8N#=YZ<`l&qy$Gzz>U2eEPI)__fim4@zD`&xiLk#BHyEy>BtX_Zz*v zhFH_D)(6`(w);pQ-u;@M6ZKDH`1|C;Q0MU4b1XLbe68T@L)~81uyOoXS#F?H8pV3JEc3q${J6*=g`oNjJb?=u)juLH(nSq{DQ9@ccD(R+HvLZ1x~dT+k;Y5dtbtbq(= z`K}!9#Un@W-YdL8+RH%>dtzS(+wj^PCOGzUx z^v4bIK^}5|htI=44DVayJrbLJ9Tjxw#q{geBjvWEw|b{-!{>Rn!LNPH0XeLD;B-Gn z(DG}jyI%t~?6v(K>^@KPeYQ4o;7@r?On=_oy6o4$ z_xUFIeB%Mk%jdz?uY(#G>C@QlNBZzSc#1du{P+1`BfIxNy=D01p!OQ*_Z``e4u4vl z9%Zl(FS?orOnmad0nE#1d~z_esbSc|>3tkHLrroHYwMTw4)UQfPJhN1ds?!QM~prU z@8Na7v6%UDbH4WJv@l8dZDt~jm4P$h$Bf?AG`)^TjA=`MPJe#D2pQh}I&K6Ev*A5$ zA94&YJtoKKc+RFVb+b>)J4`F1&v%SYwq2}24t1zya2t9#ypA1;RT(S?fvW*_h&=1||}-uOdr qhSwv - - structure_mesh.h5:/Mesh/structure_mesh.xdmf/topology + + structure_mesh.h5:/Mesh/structure_mesh.xdmf/topology - structure_mesh.h5:/Mesh/structure_mesh.xdmf/geometry + structure_mesh.h5:/Mesh/structure_mesh.xdmf/geometry - - structure_mesh.h5:/MeshTags/cell_tags/topology + + structure_mesh.h5:/MeshTags/cell_tags/topology - structure_mesh.h5:/MeshTags/cell_tags/Values + structure_mesh.h5:/MeshTags/cell_tags/Values - - structure_mesh.h5:/MeshTags/facet_tags/topology + + structure_mesh.h5:/MeshTags/facet_tags/topology - structure_mesh.h5:/MeshTags/facet_tags/Values + structure_mesh.h5:/MeshTags/facet_tags/Values diff --git a/pvade/tests/input/yaml/embedded_box.yaml b/pvade/tests/input/yaml/embedded_box.yaml index ad13c66..9550bb8 100644 --- a/pvade/tests/input/yaml/embedded_box.yaml +++ b/pvade/tests/input/yaml/embedded_box.yaml @@ -17,10 +17,10 @@ pv_array: span_rows: 1 stream_spacing: 1.0 span_spacing: 1.0 - elevation: 0.5 # 0.0 + elevation: 1.1 panel_chord: 1.2 panel_span: 1.1 - panel_thickness: 1.0 + panel_thickness: 0.8 tracker_angle: 0.0 diff --git a/pvade/tests/input/yaml/flag2d.yaml b/pvade/tests/input/yaml/flag2d.yaml index 0212422..e1a8eae 100644 --- a/pvade/tests/input/yaml/flag2d.yaml +++ b/pvade/tests/input/yaml/flag2d.yaml @@ -4,7 +4,7 @@ general: mesh_only: false structural_analysis: True fluid_analysis: True - input_mesh_dir: pvade/tests/input/mesh/flag2d + input_mesh_dir: #pvade/tests/input/mesh/flag2d domain: x_min: 0.0 x_max: 2.5 @@ -53,7 +53,7 @@ structure: body_force_x: 0 body_force_y: 0 body_force_z: 0 #100 - bc_list: ["left"] + bc_list: ["panel_left"] motor_connection: False tube_connection: False beta_relaxation: 0.005 diff --git a/pvade/tests/input/yaml/sim_params.yaml b/pvade/tests/input/yaml/sim_params.yaml index 268225e..7b6e4a6 100644 --- a/pvade/tests/input/yaml/sim_params.yaml +++ b/pvade/tests/input/yaml/sim_params.yaml @@ -14,23 +14,14 @@ domain: z_max: 20 l_char: 20 pv_array: - stream_rows: 3 - span_rows: 2 - elevation: 1.45 # 1.5 - 0.5*0.1 = 1.4 + stream_rows: 7 + span_rows: 1 + elevation: 1.5 stream_spacing: 7.0 panel_chord: 2.0 panel_span: 7.0 - panel_thickness: 0.1 + panel_thickness: 0.1 tracker_angle: -30.0 - # wind_direction: 15.0 - span_fixation_pts: [13.2] - # torque_tube_separation: 0.2 # gap between panel and tube center - # torque_tube_outer_radius: 0.1 # radius of the torque tube - # torque_tube_inner_radius: 0.09 # radius of the torque tube - # modules_per_span: 10 - # fixed_location: 5 # fixed location of the panel along the span (from 0 (fixed at left) to modules_per_span (fixed at right)) - # block_chord_div_by_panel_chord: 0.02 - solver: dt: 0.005 t_final: 0.05 @@ -52,19 +43,3 @@ fluid: bc_y_min: slip # slip noslip free bc_z_max: slip # slip noslip free bc_z_min: noslip # slip noslip free - wind_direction: 105.0 -structure: - dt : 0.01 - rho: 124.0 - poissons_ratio: 0.3 - elasticity_modulus: 4.0e+09 - body_force_x: 0.0 - body_force_y: 0.0 - body_force_z: 0.0 - bc_list: [] - motor_connection: true - tube_connection: true - beta_relaxation: 0.5 - elasticity_modulus_tube: 2.0e+11 - poissons_ratio_tube: 0.3 - rho_tube: 7800.0 \ No newline at end of file diff --git a/pvade/tests/test_fsi_mesh.py b/pvade/tests/test_fsi_mesh.py index d722147..0a9080c 100644 --- a/pvade/tests/test_fsi_mesh.py +++ b/pvade/tests/test_fsi_mesh.py @@ -109,16 +109,9 @@ def test_meshing_3dpanels_rotations(wind_direction, num_stream_rows, num_span_ro params.domain.y_min = -30.0 params.domain.y_max = 30.0 - # x_min: -10 - # x_max: 50 - # y_min: -20 - # y_max: 27 - # z_min: 0 - # z_max: 20 - params.pv_array.stream_rows = num_stream_rows params.pv_array.span_rows = num_span_rows - params.pv_array.span_spacing = 7 + params.pv_array.span_spacing = 15.0 params.pv_array.tracker_angle = list( np.linspace(-52.0, 52.0, num_stream_rows * num_span_rows) ) @@ -139,8 +132,12 @@ def test_meshing_3dpanels_rotations(wind_direction, num_stream_rows, num_span_ro # Arrays always start at xc = 0, but are centered in the y-direction # so now shift the mean to 0.0 + # yc -= np.mean(yc) + yc = yc.astype(float) yc -= np.mean(yc) + + counter = 0 for span_row in range(params.pv_array.span_rows): @@ -155,10 +152,10 @@ def test_meshing_3dpanels_rotations(wind_direction, num_stream_rows, num_span_ro # Create the 4 corners of this table corresponding to the *top* surface (+0.5*thickness) top_surface_corners = np.array( [ - [-0.5 * chord, -0.5 * span, thickness], - [0.5 * chord, -0.5 * span, thickness], - [0.5 * chord, 0.5 * span, thickness], - [-0.5 * chord, 0.5 * span, thickness], + [-0.5 * chord, -0.5 * span, 0.5*thickness], + [0.5 * chord, -0.5 * span, 0.5*thickness], + [0.5 * chord, 0.5 * span, 0.5*thickness], + [-0.5 * chord, 0.5 * span, 0.5*thickness], ] ) diff --git a/pvade/tests/test_input_files.py b/pvade/tests/test_input_files.py index 01c13c8..77af745 100644 --- a/pvade/tests/test_input_files.py +++ b/pvade/tests/test_input_files.py @@ -19,7 +19,7 @@ def launch_sim(test_file): tf = dt * 10 # ten timesteps command = ( - f"mpirun -n 8 python " + f"mpirun -n 4 python -u " + rootdir + "/pvade_main.py --input_file " + test_file["path_to_file"] diff --git a/pvade/tests/test_mesh_movement.py b/pvade/tests/test_mesh_movement.py index ec0798d..a1a746a 100644 --- a/pvade/tests/test_mesh_movement.py +++ b/pvade/tests/test_mesh_movement.py @@ -49,7 +49,7 @@ def test_calc_distance_to_panel_surface(): dx = params.domain.x_max - 0.5 * params.pv_array.panel_chord dy = params.domain.y_max - 0.5 * params.pv_array.panel_span - dz = params.pv_array.elevation + dz = params.domain.z_max - 0.5 * params.pv_array.panel_thickness truth_max_dist = np.sqrt(dx * dx + dy * dy + dz * dz) assert np.isclose(max_dist, truth_max_dist) diff --git a/pvade/tests/test_solve.py b/pvade/tests/test_solve.py index 77d5457..68dfde8 100644 --- a/pvade/tests/test_solve.py +++ b/pvade/tests/test_solve.py @@ -37,6 +37,18 @@ def test_flow_3dpanels(): domain = FSIDomain(params) domain.read_mesh_files(rootdir + "/pvade/tests/input/mesh/panels3d/", params) + """ + for this mesh: + stream_rows: 7 + span_rows: 1 + elevation: 1.5 + stream_spacing: 7.0 + panel_chord: 2.0 + panel_span: 7.0 + panel_thickness: 0.1 + tracker_angle: -30.0 + """ + print("fluid shape = ", np.shape(domain.fluid.msh.geometry.x)) print("struct shape = ", np.shape(domain.structure.msh.geometry.x)) @@ -71,8 +83,12 @@ def test_flow_3dpanels(): print("max_pressure = ", max_pressure) assert not np.any(np.isnan(flow.p_k.x.array)) - max_velocity_truth = 18.205784057651652 - max_pressure_truth = 56.175743310367395 + # max_velocity_truth = 18.205784057651652 + # max_pressure_truth = 56.175743310367395 + + max_velocity_truth = 18.613277617512917 + max_pressure_truth = 73.50109146276495 + assert np.isclose(max_velocity, max_velocity_truth, rtol=rtol) assert np.isclose(max_pressure, max_pressure_truth, rtol=rtol) diff --git a/pvade_main.py b/pvade_main.py index 6d759ec..04d489b 100644 --- a/pvade_main.py +++ b/pvade_main.py @@ -42,9 +42,10 @@ def main(input_file=None): domain = FSIDomain(params) if params.general.input_mesh_dir is not None: domain.read_mesh_files(params.general.input_mesh_dir, params) + else: domain.build(params) - exit() + # If we only want to create the mesh, we can stop here if params.general.mesh_only: list_timings(params.comm, [TimingType.wall]) From e19d7b60ac62ee608b98826668e82f612ebcbb8a Mon Sep 17 00:00:00 2001 From: xinhe2205 Date: Mon, 2 Feb 2026 09:45:46 -0700 Subject: [PATCH 30/37] black format --- pvade/fluid/FlowManager.py | 26 +- pvade/geometry/MeshManager.py | 17 +- pvade/geometry/panels3d/DomainCreation.py | 286 ++++++++++++---------- pvade/structure/ElasticityAnalysis.py | 34 ++- pvade/structure/boundary_conditions.py | 1 - pvade/tests/input/yaml/embedded_box.yaml | 4 +- pvade/tests/test_fsi_mesh.py | 10 +- pvade_main.py | 3 +- 8 files changed, 215 insertions(+), 166 deletions(-) diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index 676b19d..95c66d2 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -651,7 +651,6 @@ def _all_interior_surfaces(x): self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] ) - if self.ndim == 3: self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( @@ -673,11 +672,11 @@ def _all_interior_surfaces(x): domain.domain_markers[f"panel_back_{panel_id:.0f}"]["idx"] ) self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] - ) + domain.domain_markers[f"panel_left_{panel_id:.0f}"]["idx"] + ) self.integrated_force_z_form[-1] += self.traction[2] * ds_fluid( - domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] - ) + domain.domain_markers[f"panel_right_{panel_id:.0f}"]["idx"] + ) for module_id in range(params.pv_array.modules_per_span): self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( @@ -686,9 +685,9 @@ def _all_interior_surfaces(x): ]["idx"] ) self.integrated_force_x_form[-1] += self.traction[0] * ds_fluid( - domain.domain_markers[ - f"panel_top_{panel_id:.0f}_{module_id:.0f}" - ]["idx"] + domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"][ + "idx" + ] ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( domain.domain_markers[ @@ -696,9 +695,9 @@ def _all_interior_surfaces(x): ]["idx"] ) self.integrated_force_y_form[-1] += self.traction[1] * ds_fluid( - domain.domain_markers[ - f"panel_top_{panel_id:.0f}_{module_id:.0f}" - ]["idx"] + domain.domain_markers[f"panel_top_{panel_id:.0f}_{module_id:.0f}"][ + "idx" + ] ) if self.ndim == 3: @@ -1227,7 +1226,10 @@ def solve(self, domain, params, current_time): self.compute_lift_and_drag(params, current_time) # self.compute_panel_torques(domain, params) - if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + if ( + domain.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): self.compute_double_integral_panel_torques(domain, params) # Compute the pressure drop between the inlet and outlet diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index 0dc9895..a6d0205 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -128,7 +128,7 @@ def _get_domain_markers(self, params): def build(self, params): """This function call builds the geometry, marks the boundaries and creates a mesh using Gmsh.""" - + self.modeling_torque_tube = False domain_creation_module = ( @@ -170,8 +170,6 @@ def build(self, params): self.modeling_torque_tube = False - - # Build the domain markers for each surface and cell if hasattr(self.geometry, "domain_markers"): # If the "build" process created domain markers, use those directly... @@ -196,9 +194,8 @@ def build(self, params): self.ndim = ( self.geometry.ndim ) # gmsh_model.get_dimension() # ?? should this be domain.ndim? - - self.modeling_torque_tube = self.comm.bcast(self.modeling_torque_tube, root=0) + self.modeling_torque_tube = self.comm.bcast(self.modeling_torque_tube, root=0) # When finished, rank 0 needs to tell other ranks about how the domain_markers dictionary was created # and what values it holds. This is important now since the number of indices "idx" generated in the @@ -302,7 +299,10 @@ def _create_submeshes_from_parent(self, params): ): marker_id = self.domain_markers["modules"]["idx"] # Find all cells where cell tag = marker_id - if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": + if ( + self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): submesh_cells_modules = self.cell_tags.find(marker_id) marker_id = self.domain_markers["connectors"]["idx"] submesh_cells = np.hstack( @@ -612,8 +612,9 @@ def read_mesh_files(self, read_mesh_dir, params): self.modeling_torque_tube = True else: self.modeling_torque_tube = False - assert(params.pv_array.modules_per_span == 1), "When not modeling torque tube, modules_per_span must be 1." - + assert ( + params.pv_array.modules_per_span == 1 + ), "When not modeling torque tube, modules_per_span must be 1." sub_domain_list = ["fluid", "structure"] diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index c970322..40c87ec 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -66,6 +66,7 @@ def build_FSI(self, params): Returns: The function returns gmsh.model which contains the geometric description of the computational domain """ + def Rx(theta): rot_matrix = np.array( [ @@ -118,8 +119,9 @@ def Rz(theta): self.modeling_torque_tube = True else: self.modeling_torque_tube = False - assert(params.pv_array.modules_per_span == 1), "When not modeling torque tube, modules_per_span must be 1." - + assert ( + params.pv_array.modules_per_span == 1 + ), "When not modeling torque tube, modules_per_span must be 1." # The centroid of each panel in the x-direction (these should start at x=0) x_centers = np.linspace( @@ -203,12 +205,15 @@ def Rz(theta): module_distances[module_id + 1] - module_distances[module_id] ) - if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": + if ( + self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) this_module = self.gmsh_model.occ.addBox( -half_chord, module_distances[module_id], - params.pv_array.torque_tube_separation-half_thickness, + params.pv_array.torque_tube_separation - half_thickness, params.pv_array.panel_chord, module_span, params.pv_array.panel_thickness, @@ -323,7 +328,6 @@ def Rz(theta): next_pt > eps and next_pt < params.pv_array.panel_span - eps ): - fixation_pts_list.append(next_pt) @@ -355,7 +359,10 @@ def Rz(theta): embedded_lines_tag_list.append(fixed_pt_tag) - if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": # add the last connector + if ( + self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): # add the last connector last_standoff = self.gmsh_model.occ.addBox( -params.pv_array.block_chord_div_by_panel_chord * half_chord, module_distances[module_id + 1] @@ -508,7 +515,8 @@ def Rz(theta): ): target_key = f"modules" elif np.isclose( - vol_com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + vol_com[2], + params.pv_array.torque_tube_separation / 2.0 - half_thickness, ): target_key = f"connectors" @@ -531,12 +539,10 @@ def Rz(theta): if np.isclose(com[0], -half_chord): target_key = f"panel_left_{panel_ct:.0f}" surface_located_or_not = True - if np.isclose(com[0], half_chord): target_key = f"panel_right_{panel_ct:.0f}" surface_located_or_not = True - if ( np.isclose(com[1], module_distances[0]) @@ -548,7 +554,6 @@ def Rz(theta): ): target_key = f"panel_front_{panel_ct:.0f}" surface_located_or_not = True - if ( np.isclose( @@ -564,28 +569,23 @@ def Rz(theta): target_key = f"panel_back_{panel_ct:.0f}" surface_located_or_not = True - - if (not self.modeling_torque_tube) or (params.general.geometry_modules != "panels3d"): - if ( - np.isclose( - com[2], params.pv_array.torque_tube_separation-half_thickness - ) + if (not self.modeling_torque_tube) or ( + params.general.geometry_modules != "panels3d" + ): + if np.isclose( + com[2], + params.pv_array.torque_tube_separation - half_thickness, ): - target_key = ( - f"panel_bottom_{panel_ct:.0f}_0" - ) + target_key = f"panel_bottom_{panel_ct:.0f}_0" surface_located_or_not = True - - if ( - np.isclose( - com[2], - half_thickness + params.pv_array.torque_tube_separation, - ) + if np.isclose( + com[2], + half_thickness + params.pv_array.torque_tube_separation, ): target_key = f"panel_top_{panel_ct:.0f}_0" surface_located_or_not = True - + else: for module_id in range(params.pv_array.modules_per_span): if ( @@ -601,7 +601,9 @@ def Rz(theta): * half_chord and np.isclose(com[0], 0) and np.isclose( - com[2], params.pv_array.torque_tube_separation-half_thickness + com[2], + params.pv_array.torque_tube_separation + - half_thickness, ) ): target_key = ( @@ -628,7 +630,9 @@ def Rz(theta): + half_thickness, ) ): - target_key = f"panel_top_{panel_ct:.0f}_{module_id:.0f}" + target_key = ( + f"panel_top_{panel_ct:.0f}_{module_id:.0f}" + ) surface_located_or_not = True break # if ( @@ -683,7 +687,9 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + com[2], + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube and params.general.geometry_modules == "panels3d" @@ -699,7 +705,9 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + com[2], + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube and params.general.geometry_modules == "panels3d" @@ -727,7 +735,11 @@ def Rz(theta): surface_located_or_not = True if ( - np.isclose(com[2], params.pv_array.torque_tube_separation - half_thickness) + np.isclose( + com[2], + params.pv_array.torque_tube_separation + - half_thickness, + ) and self.modeling_torque_tube and params.general.geometry_modules == "panels3d" and np.isclose( @@ -743,7 +755,9 @@ def Rz(theta): # if not the most left/right panel boundary, it is panel/panel interface if not surface_located_or_not: - for module_id in range(params.pv_array.modules_per_span): + for module_id in range( + params.pv_array.modules_per_span + ): # sturctures tagging @@ -760,7 +774,9 @@ def Rz(theta): break if ( - np.isclose(com[1], module_distances[module_id + 1]) + np.isclose( + com[1], module_distances[module_id + 1] + ) and np.isclose(com[0], 0) and np.isclose( com[2], @@ -782,7 +798,9 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation - half_thickness + com[2], + params.pv_array.torque_tube_separation + - half_thickness, ) and np.isclose(com[0], 0.0) and np.isclose( @@ -793,7 +811,8 @@ def Rz(theta): / 2.0, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" ): target_key = f"interior_surface_{panel_ct:.0f}" surface_located_or_not = True @@ -806,7 +825,8 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -815,9 +835,7 @@ def Rz(theta): / 2.0, ) ): - target_key = ( - f"block_right_{panel_ct:.0f}_{module_id:.0f}" - ) + target_key = f"block_right_{panel_ct:.0f}_{module_id:.0f}" surface_located_or_not = True break @@ -828,7 +846,8 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -846,25 +865,29 @@ def Rz(theta): if ( np.isclose( com[2], - params.pv_array.torque_tube_separation / 2.0 - half_thickness, + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" - and np.isclose(com[1], module_distances[module_id]) - ): - target_key = ( - f"block_front_{panel_ct:.0f}_{module_id:.0f}" + and params.general.geometry_modules + == "panels3d" + and np.isclose( + com[1], module_distances[module_id] ) + ): + target_key = f"block_front_{panel_ct:.0f}_{module_id:.0f}" surface_located_or_not = True break if ( np.isclose( com[2], - params.pv_array.torque_tube_separation / 2.0 - half_thickness, + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -881,7 +904,8 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -890,22 +914,20 @@ def Rz(theta): / 2.0, ) ): - target_key = ( - f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" - ) + target_key = f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" surface_located_or_not = True break if not surface_located_or_not: target_key = f"trash_{panel_ct:.0f}" - print('facet in trash') + print("facet in trash") if target_key is not None: if target_key in this_panel_transformed_com: this_panel_transformed_com[target_key].append(com) else: this_panel_transformed_com[target_key] = [com] - + # print(this_panel_transformed_com[f"trash_{panel_ct:.0f}"]) for ( @@ -972,8 +994,6 @@ def Rz(theta): array_rotation_rad, ) - - # Now, apply the same transformations to the numpy representation # Rotate the panel by its tracking angle along the y-axis # (currently centered at (0.0, 0.0, 0.0)) @@ -1006,7 +1026,7 @@ def Rz(theta): ) else: self.numpy_pt_total_array = np.copy(numpy_pt_panel_array) - + # Fragment all panels from the overall domain self.gmsh_model.occ.fragment(domain_tag_list, panel_tag_list) @@ -1082,7 +1102,7 @@ def Rz(theta): raise ValueError(f"A panel extends past the y_min wall.") if this_surf_bbox[1] > params.domain.y_max: raise ValueError(f"A panel extends past the y_max wall.") - if this_surf_bbox[2] < 0.0: #params.domain.z_min: + if this_surf_bbox[2] < 0.0: # params.domain.z_min: raise ValueError( f"A panel extends past the z_min wall (ground level = 0.0)." ) @@ -1277,9 +1297,9 @@ def Rz(theta): self.modeling_torque_tube = True else: self.modeling_torque_tube = False - assert(params.pv_array.modules_per_span == 1), "When not modeling torque tube, modules_per_span must be 1." - - + assert ( + params.pv_array.modules_per_span == 1 + ), "When not modeling torque tube, modules_per_span must be 1." # The centroid of each panel in the x-direction (these should start at x=0) x_centers = np.linspace( @@ -1355,12 +1375,15 @@ def Rz(theta): module_distances[module_id + 1] - module_distances[module_id] ) - if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": + if ( + self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) this_module = self.gmsh_model.occ.addBox( -half_chord, module_distances[module_id], - params.pv_array.torque_tube_separation-half_thickness, + params.pv_array.torque_tube_separation - half_thickness, params.pv_array.panel_chord, module_span, params.pv_array.panel_thickness, @@ -1505,7 +1528,10 @@ def Rz(theta): embedded_lines_tag_list.append(fixed_pt_tag) - if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": # add the last connector + if ( + self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): # add the last connector last_standoff = self.gmsh_model.occ.addBox( -params.pv_array.block_chord_div_by_panel_chord * half_chord, module_distances[module_id + 1] @@ -1569,7 +1595,8 @@ def Rz(theta): ): target_key = f"modules" elif np.isclose( - vol_com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + vol_com[2], + params.pv_array.torque_tube_separation / 2.0 - half_thickness, ): target_key = f"connectors" @@ -1592,12 +1619,10 @@ def Rz(theta): if np.isclose(com[0], -half_chord): target_key = f"panel_left_{panel_ct:.0f}" surface_located_or_not = True - if np.isclose(com[0], half_chord): target_key = f"panel_right_{panel_ct:.0f}" surface_located_or_not = True - if ( np.isclose(com[1], module_distances[0]) @@ -1609,7 +1634,6 @@ def Rz(theta): ): target_key = f"panel_front_{panel_ct:.0f}" surface_located_or_not = True - if ( np.isclose( @@ -1625,28 +1649,23 @@ def Rz(theta): target_key = f"panel_back_{panel_ct:.0f}" surface_located_or_not = True - - if (not self.modeling_torque_tube) or (params.general.geometry_modules != "panels3d"): - if ( - np.isclose( - com[2], params.pv_array.torque_tube_separation-half_thickness - ) + if (not self.modeling_torque_tube) or ( + params.general.geometry_modules != "panels3d" + ): + if np.isclose( + com[2], + params.pv_array.torque_tube_separation - half_thickness, ): - target_key = ( - f"panel_bottom_{panel_ct:.0f}_0" - ) + target_key = f"panel_bottom_{panel_ct:.0f}_0" surface_located_or_not = True - - if ( - np.isclose( - com[2], - half_thickness + params.pv_array.torque_tube_separation, - ) + if np.isclose( + com[2], + half_thickness + params.pv_array.torque_tube_separation, ): target_key = f"panel_top_{panel_ct:.0f}_0" surface_located_or_not = True - + else: for module_id in range(params.pv_array.modules_per_span): if ( @@ -1662,7 +1681,9 @@ def Rz(theta): * half_chord and np.isclose(com[0], 0) and np.isclose( - com[2], params.pv_array.torque_tube_separation-half_thickness + com[2], + params.pv_array.torque_tube_separation + - half_thickness, ) ): target_key = ( @@ -1689,7 +1710,9 @@ def Rz(theta): + half_thickness, ) ): - target_key = f"panel_top_{panel_ct:.0f}_{module_id:.0f}" + target_key = ( + f"panel_top_{panel_ct:.0f}_{module_id:.0f}" + ) surface_located_or_not = True break # if ( @@ -1744,7 +1767,9 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + com[2], + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube and params.general.geometry_modules == "panels3d" @@ -1760,7 +1785,9 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation / 2.0 - half_thickness + com[2], + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube and params.general.geometry_modules == "panels3d" @@ -1788,7 +1815,11 @@ def Rz(theta): surface_located_or_not = True if ( - np.isclose(com[2], params.pv_array.torque_tube_separation - half_thickness) + np.isclose( + com[2], + params.pv_array.torque_tube_separation + - half_thickness, + ) and self.modeling_torque_tube and params.general.geometry_modules == "panels3d" and np.isclose( @@ -1804,7 +1835,9 @@ def Rz(theta): # if not the most left/right panel boundary, it is panel/panel interface if not surface_located_or_not: - for module_id in range(params.pv_array.modules_per_span): + for module_id in range( + params.pv_array.modules_per_span + ): # sturctures tagging @@ -1821,7 +1854,9 @@ def Rz(theta): break if ( - np.isclose(com[1], module_distances[module_id + 1]) + np.isclose( + com[1], module_distances[module_id + 1] + ) and np.isclose(com[0], 0) and np.isclose( com[2], @@ -1843,7 +1878,9 @@ def Rz(theta): if ( np.isclose( - com[2], params.pv_array.torque_tube_separation - half_thickness + com[2], + params.pv_array.torque_tube_separation + - half_thickness, ) and np.isclose(com[0], 0.0) and np.isclose( @@ -1854,7 +1891,8 @@ def Rz(theta): / 2.0, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" ): target_key = f"interior_surface_{panel_ct:.0f}" surface_located_or_not = True @@ -1867,7 +1905,8 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1876,9 +1915,7 @@ def Rz(theta): / 2.0, ) ): - target_key = ( - f"block_right_{panel_ct:.0f}_{module_id:.0f}" - ) + target_key = f"block_right_{panel_ct:.0f}_{module_id:.0f}" surface_located_or_not = True break @@ -1889,7 +1926,8 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1907,25 +1945,29 @@ def Rz(theta): if ( np.isclose( com[2], - params.pv_array.torque_tube_separation / 2.0 - half_thickness, + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" - and np.isclose(com[1], module_distances[module_id]) - ): - target_key = ( - f"block_front_{panel_ct:.0f}_{module_id:.0f}" + and params.general.geometry_modules + == "panels3d" + and np.isclose( + com[1], module_distances[module_id] ) + ): + target_key = f"block_front_{panel_ct:.0f}_{module_id:.0f}" surface_located_or_not = True break if ( np.isclose( com[2], - params.pv_array.torque_tube_separation / 2.0 - half_thickness, + params.pv_array.torque_tube_separation / 2.0 + - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1942,7 +1984,8 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_modules + == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1951,22 +1994,20 @@ def Rz(theta): / 2.0, ) ): - target_key = ( - f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" - ) + target_key = f"block_bottom_{panel_ct:.0f}_{module_id:.0f}" surface_located_or_not = True break if not surface_located_or_not: target_key = f"trash_{panel_ct:.0f}" - print('facet in trash') + print("facet in trash") if target_key is not None: if target_key in this_panel_transformed_com: this_panel_transformed_com[target_key].append(com) else: this_panel_transformed_com[target_key] = [com] - + # print(this_panel_transformed_com[f"trash_{panel_ct:.0f}"]) for ( @@ -2033,8 +2074,6 @@ def Rz(theta): array_rotation_rad, ) - - # Now, apply the same transformations to the numpy representation # Rotate the panel by its tracking angle along the y-axis # (currently centered at (0.0, 0.0, 0.0)) @@ -2067,7 +2106,7 @@ def Rz(theta): ) else: self.numpy_pt_total_array = np.copy(numpy_pt_panel_array) - + # Fragment all panels from the overall domain self.gmsh_model.occ.fragment(domain_tag_list, panel_tag_list) @@ -2104,9 +2143,9 @@ def Rz(theta): # self._add_to_domain_markers("structure_fluid_interface", [surf_id], "facet") if not located_this_surface: - print( - f"Warning: Surface {surf_tag} has not been added to domain markers" - ) + print( + f"Warning: Surface {surf_tag} has not been added to domain markers" + ) # Since this is not one of the exterior walls, we should check if it extends # past the boundaries x_min, x_max, ... @@ -2129,7 +2168,7 @@ def Rz(theta): ) if this_surf_bbox[2] > params.domain.z_max: raise ValueError(f"A panel extends past the z_max wall.") - + # mark the panel and connector volumes all_vol_tag_list = self.gmsh_model.occ.getEntities(self.ndim) @@ -2147,7 +2186,6 @@ def Rz(theta): if "trash" not in key: self._add_to_domain_markers(key, [vol_id], "cell") - # Record all the data collected to domain_markers as physics groups with physical names # because we are creating domain_markers _within_ the build method, we don't need to # call the separate, default mark_surfaces method from TemplateDomainCreation. @@ -2170,9 +2208,6 @@ def Rz(theta): ) self.gmsh_model.setPhysicalName(self.ndim - 1, data["idx"], key) - - - def set_length_scales_DEV(self, params, domain_markers): res_min = params.domain.l_char @@ -2307,7 +2342,10 @@ def set_length_scales(self, params, domain_markers): domain_markers[f"panel_back_{panel_id}"]["gmsh_tags"] ) - if self.modeling_torque_tube and params.general.geometry_modules == "panels3d": + if ( + self.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): for module_id in range(params.pv_array.modules_per_span): internal_surface_tags.extend( domain_markers[f"panel_bottom_{panel_id:.0f}_{module_id:.0f}"][ diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index ef28fb1..001e55a 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -704,21 +704,29 @@ def P_connector(u): self.z_unit_vector = dolfinx.fem.Constant( domain.structure.msh, [0.0, 0.0, 1.0] ) # surface traction, N/m^2 - - if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + + if ( + domain.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): self.calculate_K_for_Robin_BC(domain, flow, params) dx_structure = ufl.Measure( "dx", domain=domain.structure.msh, subdomain_data=domain.structure.cell_tags ) - - if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + + if ( + domain.modeling_torque_tube + and params.general.geometry_modules == "panels3d" + ): self.res = ( m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure(domain.domain_markers["modules"]["idx"]) - + k_nominal_connector(self.avg(self.u_old, self.u, self.alpha_f), self.u_) + + k_nominal_connector( + self.avg(self.u_old, self.u, self.alpha_f), self.u_ + ) * dx_structure(domain.domain_markers["connectors"]["idx"]) - structure.rho * ufl.inner(self.f, self.u_) @@ -731,14 +739,20 @@ def P_connector(u): ) # - Wext(self.u) # Robin boundary condition terms - for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): + for panel_id in range( + params.pv_array.stream_rows * params.pv_array.span_rows + ): for i in range(params.pv_array.modules_per_span + 1): name_K = f"spring_stiffness_{panel_id:.0f}_{i:.0f}" K_springs = dolfinx.fem.Constant( domain.structure.msh, float(getattr(self, name_K)) ) - self.res -= ufl.dot(K_springs * self.u_, self.z_unit_vector) * self.ds( - domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"]["idx"] + self.res -= ufl.dot( + K_springs * self.u_, self.z_unit_vector + ) * self.ds( + domain.domain_markers[f"block_bottom_{panel_id:.0f}_{i:.0f}"][ + "idx" + ] ) else: self.res = ( @@ -746,9 +760,7 @@ def P_connector(u): + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure + k_nominal(self.avg(self.u_old, self.u, self.alpha_f), self.u_) * dx_structure - - structure.rho - * ufl.inner(self.f, self.u_) - * dx_structure + - structure.rho * ufl.inner(self.f, self.u_) * dx_structure - ufl.dot(ufl.dot(self.stress_predicted * J * ufl.inv(F.T), n), self.u_) * self.ds ) # - Wext(self.u) diff --git a/pvade/structure/boundary_conditions.py b/pvade/structure/boundary_conditions.py index e87890c..4be84de 100644 --- a/pvade/structure/boundary_conditions.py +++ b/pvade/structure/boundary_conditions.py @@ -344,7 +344,6 @@ def connection_point_up_helper(nodes_to_pin_between): return fn_handle - if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": # # Start pinning along the lines expressed byt numpy_pt_total_array # The center line of connectors bottom surface is fixed to remove rigid body motion diff --git a/pvade/tests/input/yaml/embedded_box.yaml b/pvade/tests/input/yaml/embedded_box.yaml index 9550bb8..23c2f53 100644 --- a/pvade/tests/input/yaml/embedded_box.yaml +++ b/pvade/tests/input/yaml/embedded_box.yaml @@ -17,10 +17,10 @@ pv_array: span_rows: 1 stream_spacing: 1.0 span_spacing: 1.0 - elevation: 1.1 + elevation: 1.0 panel_chord: 1.2 panel_span: 1.1 - panel_thickness: 0.8 + panel_thickness: 1.0 tracker_angle: 0.0 diff --git a/pvade/tests/test_fsi_mesh.py b/pvade/tests/test_fsi_mesh.py index 0a9080c..24c8ebd 100644 --- a/pvade/tests/test_fsi_mesh.py +++ b/pvade/tests/test_fsi_mesh.py @@ -136,8 +136,6 @@ def test_meshing_3dpanels_rotations(wind_direction, num_stream_rows, num_span_ro yc = yc.astype(float) yc -= np.mean(yc) - - counter = 0 for span_row in range(params.pv_array.span_rows): @@ -152,10 +150,10 @@ def test_meshing_3dpanels_rotations(wind_direction, num_stream_rows, num_span_ro # Create the 4 corners of this table corresponding to the *top* surface (+0.5*thickness) top_surface_corners = np.array( [ - [-0.5 * chord, -0.5 * span, 0.5*thickness], - [0.5 * chord, -0.5 * span, 0.5*thickness], - [0.5 * chord, 0.5 * span, 0.5*thickness], - [-0.5 * chord, 0.5 * span, 0.5*thickness], + [-0.5 * chord, -0.5 * span, 0.5 * thickness], + [0.5 * chord, -0.5 * span, 0.5 * thickness], + [0.5 * chord, 0.5 * span, 0.5 * thickness], + [-0.5 * chord, 0.5 * span, 0.5 * thickness], ] ) diff --git a/pvade_main.py b/pvade_main.py index 04d489b..6b3d046 100644 --- a/pvade_main.py +++ b/pvade_main.py @@ -42,7 +42,7 @@ def main(input_file=None): domain = FSIDomain(params) if params.general.input_mesh_dir is not None: domain.read_mesh_files(params.general.input_mesh_dir, params) - + else: domain.build(params) @@ -80,7 +80,6 @@ def main(input_file=None): for k in range(params.solver.t_steps): current_time = (k + 1) * params.solver.dt - if ( structural_analysis and (k + 1) % solve_structure_interval_n == 0 From 05b4d730720e4164a3070cabf72d80b638017e20 Mon Sep 17 00:00:00 2001 From: arswalid Date: Tue, 3 Feb 2026 13:57:28 -0700 Subject: [PATCH 31/37] fix test mesh movement: - problem was 2 geometries had same volume center - fix was remove last tag that belongs to fluid - fix to test file to account for elevation and panel thickness --- pvade/IO/input_schema.yaml | 2 +- pvade/geometry/panels3d/DomainCreation.py | 31 ++++++++++++++--------- pvade/tests/input/yaml/embedded_box.yaml | 5 ++-- pvade/tests/test_mesh_movement.py | 13 +++++++--- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/pvade/IO/input_schema.yaml b/pvade/IO/input_schema.yaml index 1ec9468..b389509 100644 --- a/pvade/IO/input_schema.yaml +++ b/pvade/IO/input_schema.yaml @@ -154,7 +154,7 @@ properties: units: "meter" span_fixation_pts: default: 3.5 - minimum: 1.0 + minimum: 0.2 # maximum: 32.0 type: - "number" diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 40c87ec..6251531 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -169,6 +169,7 @@ def Rz(theta): # only include connectors structure_connector_only_list = [] + # if params.pv_array.modules_per_span > 1: module_distances = np.linspace( -half_span, half_span, params.pv_array.modules_per_span + 1 ) @@ -271,7 +272,7 @@ def Rz(theta): torque_tube_tag ) # for this panel row - else: + else: # this loop is to add panel only, no torque tube modeling this_module = self.gmsh_model.occ.addBox( -half_chord, module_distances[module_id], @@ -285,12 +286,14 @@ def Rz(theta): structure_panel_only_list.append((self.ndim, this_module)) + # Add a bisecting line to the bottom of the panel in the spanwise direction pt_1 = self.gmsh_model.occ.addPoint( 0, module_distances[module_id], -half_thickness ) pt_2 = self.gmsh_model.occ.addPoint( 0, module_distances[module_id + 1], -half_thickness ) + # numpy_pt_list for this row numpy_pt_list.append( [ 0, @@ -301,8 +304,11 @@ def Rz(theta): -half_thickness, ] ) - torque_tube_id = self.gmsh_model.occ.addLine(pt_1, pt_2) + torque_tube_id = self.gmsh_model.occ.addLine( + pt_1, pt_2 + ) # line simulating torque tube attached horizontally at the bottom of the module torque_tube_tag = (1, torque_tube_id) + # Store all the embedded lines that we need in the mesh embedded_lines_tag_list.append( torque_tube_tag ) # for this panel row @@ -1114,17 +1120,18 @@ def Rz(theta): for vol_tag in all_vol_tag_list: vol_id = vol_tag[1] - com = self.gmsh_model.occ.getCenterOfMass(self.ndim, vol_id) + if vol_id != all_vol_tag_list[-1][-1]: + com = self.gmsh_model.occ.getCenterOfMass(self.ndim, vol_id) - located_this_volume = False + located_this_volume = False - for key, val in transformed_com.items(): - for target_com in val: - # print(target_com) - if np.allclose(np.array(com), target_com): - located_this_volume = True - if "trash" not in key: - self._add_to_domain_markers(key, [vol_id], "cell") + for key, val in transformed_com.items(): + for target_com in val: + # print(target_com) + if np.allclose(np.array(com), target_com): + located_this_volume = True + if "trash" not in key: + self._add_to_domain_markers(key, [vol_id], "cell") # Mark the fluid volume # Volumes are the entities with dimension equal to the mesh dimension @@ -2357,7 +2364,7 @@ def set_length_scales(self, params, domain_markers): "gmsh_tags" ] ) - else: + else: # delete after testing internal_surface_tags.extend( domain_markers[f"panel_bottom_{panel_id}_0"]["gmsh_tags"] ) diff --git a/pvade/tests/input/yaml/embedded_box.yaml b/pvade/tests/input/yaml/embedded_box.yaml index 23c2f53..966a147 100644 --- a/pvade/tests/input/yaml/embedded_box.yaml +++ b/pvade/tests/input/yaml/embedded_box.yaml @@ -9,7 +9,7 @@ domain: x_max: 1.0 y_min: -1.0 y_max: 1.0 - z_min: 0.0 + z_min: 0 z_max: 2.0 l_char: 0.05 pv_array: @@ -17,10 +17,11 @@ pv_array: span_rows: 1 stream_spacing: 1.0 span_spacing: 1.0 - elevation: 1.0 + elevation: 1. panel_chord: 1.2 panel_span: 1.1 panel_thickness: 1.0 tracker_angle: 0.0 + # span_fixation_pts: null diff --git a/pvade/tests/test_mesh_movement.py b/pvade/tests/test_mesh_movement.py index a1a746a..f52daf5 100644 --- a/pvade/tests/test_mesh_movement.py +++ b/pvade/tests/test_mesh_movement.py @@ -49,7 +49,11 @@ def test_calc_distance_to_panel_surface(): dx = params.domain.x_max - 0.5 * params.pv_array.panel_chord dy = params.domain.y_max - 0.5 * params.pv_array.panel_span - dz = params.domain.z_max - 0.5 * params.pv_array.panel_thickness + dz = ( + params.domain.z_max + - 0.5 * params.pv_array.panel_thickness + - params.pv_array.elevation + ) truth_max_dist = np.sqrt(dx * dx + dy * dy + dz * dz) assert np.isclose(max_dist, truth_max_dist) @@ -113,7 +117,10 @@ def __init__(self, domain, x_shift, y_shift, z_shift): assert np.isclose( np.amin(structure_coords_before[:, 1]), -0.5 * params.pv_array.panel_span ) - assert np.isclose(np.amin(structure_coords_before[:, 2]), params.pv_array.elevation) + assert np.isclose( + np.amin(structure_coords_before[:, 2]), + params.pv_array.elevation - 0.5 * params.pv_array.panel_thickness, + ) assert np.isclose( np.amax(structure_coords_before[:, 0]), 0.5 * params.pv_array.panel_chord @@ -123,7 +130,7 @@ def __init__(self, domain, x_shift, y_shift, z_shift): ) assert np.isclose( np.amax(structure_coords_before[:, 2]), - params.pv_array.elevation + params.pv_array.panel_thickness, + params.pv_array.elevation + 0.5 * params.pv_array.panel_thickness, ) # Move the mesh by the amount prescribed in u_delta From ac1fac5d1efb215e297c06bfa6dc35e938f8de83 Mon Sep 17 00:00:00 2001 From: arswalid Date: Tue, 3 Feb 2026 14:32:04 -0700 Subject: [PATCH 32/37] reformatted files in examples - we should probably exclude those file from black testing and only check source files - black version 26 triggers new formatting errors not triggered by version 25 --- examples/cylinderflow.py | 1 - examples/poissoneq.py | 1 - .../generate_turbulent_inflow_h5_file.ipynb | 74 ++++++++++++------- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/examples/cylinderflow.py b/examples/cylinderflow.py index 8aae499..e1b9600 100644 --- a/examples/cylinderflow.py +++ b/examples/cylinderflow.py @@ -45,7 +45,6 @@ from dolfinx.io import VTXWriter, gmshio, XDMFFile from dolfinx.mesh import locate_entities_boundary - #################################################### # # # MESH PARAMETERS # diff --git a/examples/poissoneq.py b/examples/poissoneq.py index f296889..96b5572 100644 --- a/examples/poissoneq.py +++ b/examples/poissoneq.py @@ -10,7 +10,6 @@ from timeit import default_timer as timer from dolfinx.common import TimingType, list_timings - start = timer() elems = int(sys.argv[1]) # nelems # Create mesh and define function space diff --git a/examples/synthetic_turbulent_inflow/generate_turbulent_inflow_h5_file.ipynb b/examples/synthetic_turbulent_inflow/generate_turbulent_inflow_h5_file.ipynb index be39272..1a22d11 100644 --- a/examples/synthetic_turbulent_inflow/generate_turbulent_inflow_h5_file.ipynb +++ b/examples/synthetic_turbulent_inflow/generate_turbulent_inflow_h5_file.ipynb @@ -24,12 +24,18 @@ "import pandas as pd # need this to load our data from the csv files\n", "\n", "import sys\n", + "\n", "# NOTE: to run this, you will need to replace the path below #\n", - "sys.path.append('/Users/bstanisl/repos/pyconturb/pyconturb') # path to the pyconturb repository location on your local machine\n", + "sys.path.append(\n", + " \"/Users/bstanisl/repos/pyconturb/pyconturb\"\n", + ") # path to the pyconturb repository location on your local machine\n", "from pyconturb import gen_turb, gen_spat_grid # generate turbulence, useful helper\n", "from pyconturb.sig_models import iec_sig # IEC 61400-1 turbulence std dev\n", "from pyconturb.spectral_models import kaimal_spectrum # Kaimal spectrum\n", - "from pyconturb.wind_profiles import constant_profile, power_profile # wind-speed profile functions\n", + "from pyconturb.wind_profiles import (\n", + " constant_profile,\n", + " power_profile,\n", + ") # wind-speed profile functions\n", "\n", "from _nb_utils import plot_slice\n", "import h5py" @@ -217,11 +223,13 @@ "source": [ "# generate spatial gridpoints dataframe\n", "ny = 80\n", - "nz = 80 #40\n", + "nz = 80 # 40\n", "y = np.linspace(-10.0, 10.0, ny)\n", - "z = np.linspace(0.00001, 20.0, nz) \n", + "z = np.linspace(0.00001, 20.0, nz)\n", "\n", - "spat_df = gen_spat_grid(y, z) # if `comps` not passed in, assumes all 3 components are wanted\n", + "spat_df = gen_spat_grid(\n", + " y, z\n", + ") # if `comps` not passed in, assumes all 3 components are wanted\n", "spat_df.head() # look at the first few rows" ] }, @@ -233,9 +241,9 @@ "outputs": [], "source": [ "# generate time vector\n", - "t_final = 1.0 #10.0 [s]\n", - "dt = 0.01 # [s]\n", - "t_steps = int(t_final/dt)\n", + "t_final = 1.0 # 10.0 [s]\n", + "dt = 0.01 # [s]\n", + "t_steps = int(t_final / dt)\n", "time = np.linspace(0, t_final, t_steps)" ] }, @@ -483,16 +491,30 @@ "source": [ "# reshape to 3D array and visualize first timestep\n", "data = {}\n", - "data['u'] = turb_df.filter(regex='u').values.reshape(len(turb_df),y.size,z.size).transpose((0, 2, 1))\n", - "data['v'] = turb_df.filter(regex='v').values.reshape(len(turb_df),y.size,z.size).transpose((0, 2, 1))\n", - "data['w'] = turb_df.filter(regex='w').values.reshape(len(turb_df),y.size,z.size).transpose((0, 2, 1))\n", + "data[\"u\"] = (\n", + " turb_df.filter(regex=\"u\")\n", + " .values.reshape(len(turb_df), y.size, z.size)\n", + " .transpose((0, 2, 1))\n", + ")\n", + "data[\"v\"] = (\n", + " turb_df.filter(regex=\"v\")\n", + " .values.reshape(len(turb_df), y.size, z.size)\n", + " .transpose((0, 2, 1))\n", + ")\n", + "data[\"w\"] = (\n", + " turb_df.filter(regex=\"w\")\n", + " .values.reshape(len(turb_df), y.size, z.size)\n", + " .transpose((0, 2, 1))\n", + ")\n", "\n", "fig, ax = plt.subplots()\n", - "plt.imshow(data['u'][0,:,:], # imshow requires nz-ny slice\n", - " origin='lower', # smallest y-z in lower left, not upper left\n", - " extent=[y[0], y[-1], z[0], z[-1]], # lateral and vertical limits\n", - " interpolation='bilinear',\n", - " cmap='coolwarm') # image smoothing\n", + "plt.imshow(\n", + " data[\"u\"][0, :, :], # imshow requires nz-ny slice\n", + " origin=\"lower\", # smallest y-z in lower left, not upper left\n", + " extent=[y[0], y[-1], z[0], z[-1]], # lateral and vertical limits\n", + " interpolation=\"bilinear\",\n", + " cmap=\"coolwarm\",\n", + ") # image smoothing\n", "plt.colorbar()" ] }, @@ -507,24 +529,24 @@ "h5_filename = \"pct_turb_ny{}_nz{}_unconstrained_{}s_dt{}_uref{}.h5\".format(\n", " ny, nz, t_final, dt, u_ref\n", ")\n", - "with h5py.File(\"../../input/\"+h5_filename, \"w\") as fp:\n", + "with h5py.File(\"../../input/\" + h5_filename, \"w\") as fp:\n", " fp.create_dataset(\"time_index\", shape=(t_steps,))\n", " fp[\"time_index\"][:] = time\n", - " \n", + "\n", " fp.create_dataset(\"y_coordinates\", shape=(ny,))\n", " fp[\"y_coordinates\"][:] = y\n", - " \n", + "\n", " fp.create_dataset(\"z_coordinates\", shape=(nz,))\n", " fp[\"z_coordinates\"][:] = z\n", - " \n", + "\n", " fp.create_dataset(\"u\", shape=(t_steps, nz, ny))\n", - " fp[\"u\"][:] = data['u'][:]\n", - " \n", + " fp[\"u\"][:] = data[\"u\"][:]\n", + "\n", " fp.create_dataset(\"v\", shape=(t_steps, nz, ny))\n", - " fp[\"v\"][:] = data['v'][:]\n", - " \n", + " fp[\"v\"][:] = data[\"v\"][:]\n", + "\n", " fp.create_dataset(\"w\", shape=(t_steps, nz, ny))\n", - " fp[\"w\"][:] = data['w'][:]" + " fp[\"w\"][:] = data[\"w\"][:]" ] }, { @@ -551,7 +573,7 @@ "source": [ "# read h5 file\n", "# Note: Replace the path below with the path on your local machine to the .h5 file that you want to read\n", - "fname = '/Users/bstanisl/Documents/repos/PVade/input/pct_turb_ny80_nz80_unconstrained_1.0s_dt0.01_uref20.h5'\n", + "fname = \"/Users/bstanisl/Documents/repos/PVade/input/pct_turb_ny80_nz80_unconstrained_1.0s_dt0.01_uref20.h5\"\n", "\n", "# Open the file (read-only)\n", "with h5py.File(fname, \"r\") as f:\n", From 7b93766821dbdfb56ee7f28cd7ba9739dbcf287a Mon Sep 17 00:00:00 2001 From: arswalid Date: Tue, 3 Feb 2026 16:10:52 -0700 Subject: [PATCH 33/37] Fix to test all input - input files modified to incorporate panel_ in the naming scheme - addition of panel_ in domain creation for other geometries - addition of _0 for top and bottom surfaces when modeling torque tube is not available --- input/flag2d.yaml | 2 +- input/heated_panels2d.yaml | 2 +- input/inflow_input.yaml | 2 +- input/panels2d.yaml | 2 +- input/panels3d.yaml | 2 +- pvade/geometry/MeshManager.py | 2 +- pvade/geometry/heliostats3d/DomainCreation.py | 26 ++++++++++--------- pvade/geometry/panels2d/DomainCreation.py | 8 +++--- pvade/structure/boundary_conditions.py | 5 +++- test_all_inputs.py | 2 +- 10 files changed, 29 insertions(+), 24 deletions(-) diff --git a/input/flag2d.yaml b/input/flag2d.yaml index 561a698..307d36d 100644 --- a/input/flag2d.yaml +++ b/input/flag2d.yaml @@ -52,7 +52,7 @@ structure: body_force_x: 0 body_force_y: 0 body_force_z: 0 #100 - bc_list: ["left"] + bc_list: ["panel_left"] motor_connection: False tube_connection: False beta_relaxation: 0.005 diff --git a/input/heated_panels2d.yaml b/input/heated_panels2d.yaml index 468634f..dfc7c94 100644 --- a/input/heated_panels2d.yaml +++ b/input/heated_panels2d.yaml @@ -59,7 +59,7 @@ structure: body_force_x: 0 body_force_y: -1 body_force_z: 0 #100 - bc_list: ["left"] + bc_list: ["panel_left"] motor_connection: False tube_connection: False beta_relaxation: 0.005 diff --git a/input/inflow_input.yaml b/input/inflow_input.yaml index 539e87f..0646adf 100644 --- a/input/inflow_input.yaml +++ b/input/inflow_input.yaml @@ -56,7 +56,7 @@ structure: body_force_x: 0 body_force_y: 0 body_force_z: 0 #100 - bc_list: ["left"] + bc_list: ["panel_left"] motor_connection: False tube_connection: False beta_relaxation: 0.005 diff --git a/input/panels2d.yaml b/input/panels2d.yaml index 7482b21..57d80cd 100644 --- a/input/panels2d.yaml +++ b/input/panels2d.yaml @@ -46,7 +46,7 @@ structure: body_force_x: 0 body_force_y: -1 body_force_z: 0 #100 - bc_list: ["top"] + bc_list: [panel_top] motor_connection: False tube_connection: False beta_relaxation: 0.005 diff --git a/input/panels3d.yaml b/input/panels3d.yaml index a66e1c5..a6159f1 100644 --- a/input/panels3d.yaml +++ b/input/panels3d.yaml @@ -56,5 +56,5 @@ structure: body_force_x: 0 body_force_y: 0 body_force_z: -1 #100 - bc_list: [left ] + bc_list: [panel_left] tube_connection: False diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index a6d0205..990f6a5 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -141,7 +141,7 @@ def build(self, params): raise ValueError(f"Could not import {domain_creation_module}") self.geometry = dcm.DomainCreation(params) - + # Only rank 0 builds the geometry and meshes the domain if self.rank == 0: if ( diff --git a/pvade/geometry/heliostats3d/DomainCreation.py b/pvade/geometry/heliostats3d/DomainCreation.py index c7a804e..a4f38d9 100644 --- a/pvade/geometry/heliostats3d/DomainCreation.py +++ b/pvade/geometry/heliostats3d/DomainCreation.py @@ -83,6 +83,8 @@ def Rz(theta): return rot_matrix + self.modeling_torque_tube = False + # Compute and store some useful geometric quantities self.x_span = params.domain.x_max - params.domain.x_min self.y_span = params.domain.y_max - params.domain.y_min @@ -247,22 +249,22 @@ def Rz(theta): # self._add_to_domain_markers(f"z_max_{panel_ct:.0f}", [panel_surfs[-1]], "facet") self._add_to_domain_markers( - f"front_{panel_ct:.0f}", [panel_surfs[0]], "facet" + f"panel_front_{panel_ct:.0f}", [panel_surfs[0]], "facet" ) self._add_to_domain_markers( - f"back_{panel_ct:.0f}", [panel_surfs[1]], "facet" + f"panel_back_{panel_ct:.0f}", [panel_surfs[1]], "facet" ) self._add_to_domain_markers( - f"left_{panel_ct:.0f}", [panel_surfs[2]], "facet" + f"panel_left_{panel_ct:.0f}", [panel_surfs[2]], "facet" ) self._add_to_domain_markers( - f"right_{panel_ct:.0f}", [panel_surfs[3]], "facet" + f"panel_right_{panel_ct:.0f}", [panel_surfs[3]], "facet" ) self._add_to_domain_markers( - f"bottom_{panel_ct:.0f}", panel_surfs[4:-1], "facet" + f"panel_bottom_{panel_ct:.0f}_0", panel_surfs[4:-1], "facet" ) self._add_to_domain_markers( - f"top_{panel_ct:.0f}", [panel_surfs[-1]], "facet" + f"panel_top_{panel_ct:.0f}_0", [panel_surfs[-1]], "facet" ) # self._add_to_domain_markers(f"right_{panel_ct:.0f}", [panel_surfs[1]], "facet")#correct @@ -990,22 +992,22 @@ def set_length_scales(self, params, domain_markers): for panel_id in range(params.pv_array.stream_rows * params.pv_array.span_rows): internal_surface_tags.append( - domain_markers[f"bottom_{panel_id}"]["gmsh_tags"][0] + domain_markers[f"panel_bottom_{panel_id}_0"]["gmsh_tags"][0] ) internal_surface_tags.append( - domain_markers[f"top_{panel_id}"]["gmsh_tags"][0] + domain_markers[f"panel_top_{panel_id}_0"]["gmsh_tags"][0] ) internal_surface_tags.append( - domain_markers[f"left_{panel_id}"]["gmsh_tags"][0] + domain_markers[f"panel_left_{panel_id}"]["gmsh_tags"][0] ) internal_surface_tags.append( - domain_markers[f"right_{panel_id}"]["gmsh_tags"][0] + domain_markers[f"panel_right_{panel_id}"]["gmsh_tags"][0] ) internal_surface_tags.append( - domain_markers[f"front_{panel_id}"]["gmsh_tags"][0] + domain_markers[f"panel_front_{panel_id}"]["gmsh_tags"][0] ) internal_surface_tags.append( - domain_markers[f"back_{panel_id}"]["gmsh_tags"][0] + domain_markers[f"panel_back_{panel_id}"]["gmsh_tags"][0] ) min_dist = [] diff --git a/pvade/geometry/panels2d/DomainCreation.py b/pvade/geometry/panels2d/DomainCreation.py index 0df35ac..9c4a30b 100644 --- a/pvade/geometry/panels2d/DomainCreation.py +++ b/pvade/geometry/panels2d/DomainCreation.py @@ -184,22 +184,22 @@ def build_FSI(self, params): if np.isclose(com[0], x_min_panel): self._add_to_domain_markers( - f"left_{panel_id:.0f}", [surf_id], "facet" + f"panel_left_{panel_id:.0f}", [surf_id], "facet" ) elif np.isclose(com[0], x_max_panel): self._add_to_domain_markers( - f"right_{panel_id:.0f}", [surf_id], "facet" + f"panel_right_{panel_id:.0f}", [surf_id], "facet" ) elif np.isclose(com[1], y_min_panel): self._add_to_domain_markers( - f"bottom_{panel_id:.0f}", [100], "facet" + f"panel_bottom_{panel_id:.0f}_0", [surf_id], "facet" ) elif np.isclose(com[1], y_max_panel): self._add_to_domain_markers( - f"top_{panel_id:.0f}", [surf_id], "facet" + f"panel_top_{panel_id:.0f}_0", [surf_id], "facet" ) # Rotate the panel currently centered at diff --git a/pvade/structure/boundary_conditions.py b/pvade/structure/boundary_conditions.py index 4be84de..3adadc4 100644 --- a/pvade/structure/boundary_conditions.py +++ b/pvade/structure/boundary_conditions.py @@ -273,7 +273,10 @@ def build_structure_boundary_conditions(domain, params, functionspace): for num_panel in range(total_num_panels): for location in params.structure.bc_list: # it is empty - location_panel = f"{location}_{num_panel}" + if location == "panel_top" or location == "panel_bottom": + location_panel = f"{location}_{num_panel}_0" + else: + location_panel = f"{location}_{num_panel}" # f"front_{num_panel}" , f"back_{num_panel}": # for location in [f"left_{num_panel}"]:# , f"right_{num_panel}": # for location in f"left_{num_panel}": diff --git a/test_all_inputs.py b/test_all_inputs.py index 5c85e2f..2996610 100644 --- a/test_all_inputs.py +++ b/test_all_inputs.py @@ -4,7 +4,7 @@ @pytest.mark.parametrize("mesh_only", [True, False]) -@pytest.mark.parametrize("nprocs", [1]) +@pytest.mark.parametrize("nprocs", [4]) def test_pvade_run(input_file, mesh_only, nprocs): cmd = [ "mpirun", From 0ce0bd0d07c669bb67c0ee1a1299598ba5b7ffa7 Mon Sep 17 00:00:00 2001 From: arswalid Date: Tue, 3 Feb 2026 16:13:40 -0700 Subject: [PATCH 34/37] removing -n 4 that was used to test locally back to n 1 for gh runners --- test_all_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_all_inputs.py b/test_all_inputs.py index 2996610..5c85e2f 100644 --- a/test_all_inputs.py +++ b/test_all_inputs.py @@ -4,7 +4,7 @@ @pytest.mark.parametrize("mesh_only", [True, False]) -@pytest.mark.parametrize("nprocs", [4]) +@pytest.mark.parametrize("nprocs", [1]) def test_pvade_run(input_file, mesh_only, nprocs): cmd = [ "mpirun", From 02c23405b3a81e8dec1487b1362722f72eacdde8 Mon Sep 17 00:00:00 2001 From: arswalid Date: Tue, 3 Feb 2026 16:14:49 -0700 Subject: [PATCH 35/37] black formatting --- pvade/geometry/MeshManager.py | 2 +- pvade/geometry/heliostats3d/DomainCreation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index 990f6a5..a6d0205 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -141,7 +141,7 @@ def build(self, params): raise ValueError(f"Could not import {domain_creation_module}") self.geometry = dcm.DomainCreation(params) - + # Only rank 0 builds the geometry and meshes the domain if self.rank == 0: if ( diff --git a/pvade/geometry/heliostats3d/DomainCreation.py b/pvade/geometry/heliostats3d/DomainCreation.py index a4f38d9..3063d23 100644 --- a/pvade/geometry/heliostats3d/DomainCreation.py +++ b/pvade/geometry/heliostats3d/DomainCreation.py @@ -84,7 +84,7 @@ def Rz(theta): return rot_matrix self.modeling_torque_tube = False - + # Compute and store some useful geometric quantities self.x_span = params.domain.x_max - params.domain.x_min self.y_span = params.domain.y_max - params.domain.y_min From 264143b20c39c4a62b3cdfe3d4cdd486ef6c0460 Mon Sep 17 00:00:00 2001 From: arswalid Date: Wed, 4 Feb 2026 12:36:49 -0700 Subject: [PATCH 36/37] fixing misspelled modules/module --- input/duramat_case_study.yaml | 2 +- pvade/fluid/FlowManager.py | 2 +- pvade/geometry/MeshManager.py | 2 +- pvade/geometry/panels3d/DomainCreation.py | 62 +++++++++++------------ pvade/structure/ElasticityAnalysis.py | 4 +- pvade/structure/boundary_conditions.py | 2 +- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/input/duramat_case_study.yaml b/input/duramat_case_study.yaml index b48d5b7..646b0a1 100644 --- a/input/duramat_case_study.yaml +++ b/input/duramat_case_study.yaml @@ -27,7 +27,7 @@ pv_array: # torque_tube_outer_radius: 0.1 # radius of the torque tube # torque_tube_inner_radius: 0.09 # radius of the torque tube modules_per_span: 1 - fixed_location: 5 # fixed location of the panel along the span (from 0 (fixed at left) to modules_per_span (fixed at right)) + fixed_location: 1 # fixed location of the panel along the span (from 0 (fixed at left) to modules_per_span (fixed at right)) block_chord_div_by_panel_chord: 0.02 solver: diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index 95c66d2..4693b9a 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -1228,7 +1228,7 @@ def solve(self, domain, params, current_time): # self.compute_panel_torques(domain, params) if ( domain.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): self.compute_double_integral_panel_torques(domain, params) diff --git a/pvade/geometry/MeshManager.py b/pvade/geometry/MeshManager.py index a6d0205..1540807 100644 --- a/pvade/geometry/MeshManager.py +++ b/pvade/geometry/MeshManager.py @@ -301,7 +301,7 @@ def _create_submeshes_from_parent(self, params): # Find all cells where cell tag = marker_id if ( self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): submesh_cells_modules = self.cell_tags.find(marker_id) marker_id = self.domain_markers["connectors"]["idx"] diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 6251531..9edc71f 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -208,7 +208,7 @@ def Rz(theta): if ( self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) this_module = self.gmsh_model.occ.addBox( @@ -367,7 +367,7 @@ def Rz(theta): if ( self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): # add the last connector last_standoff = self.gmsh_model.occ.addBox( -params.pv_array.block_chord_div_by_panel_chord * half_chord, @@ -576,7 +576,7 @@ def Rz(theta): surface_located_or_not = True if (not self.modeling_torque_tube) or ( - params.general.geometry_modules != "panels3d" + params.general.geometry_module != "panels3d" ): if np.isclose( com[2], @@ -660,7 +660,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -679,7 +679,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module== "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -698,7 +698,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -716,7 +716,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span], @@ -728,7 +728,7 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -747,7 +747,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -817,7 +817,7 @@ def Rz(theta): / 2.0, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" ): target_key = f"interior_surface_{panel_ct:.0f}" @@ -831,7 +831,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -852,7 +852,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -875,7 +875,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -892,7 +892,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -910,7 +910,7 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -1384,7 +1384,7 @@ def Rz(theta): if ( self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): # Create an 0-tracking-degree panel centered at (x, y, z) = (0, 0, 0) this_module = self.gmsh_model.occ.addBox( @@ -1537,7 +1537,7 @@ def Rz(theta): if ( self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): # add the last connector last_standoff = self.gmsh_model.occ.addBox( -params.pv_array.block_chord_div_by_panel_chord * half_chord, @@ -1657,7 +1657,7 @@ def Rz(theta): surface_located_or_not = True if (not self.modeling_torque_tube) or ( - params.general.geometry_modules != "panels3d" + params.general.geometry_module != "panels3d" ): if np.isclose( com[2], @@ -1741,7 +1741,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -1760,7 +1760,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -1779,7 +1779,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -1797,7 +1797,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span], @@ -1809,7 +1809,7 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -1828,7 +1828,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -1898,7 +1898,7 @@ def Rz(theta): / 2.0, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" ): target_key = f"interior_surface_{panel_ct:.0f}" @@ -1912,7 +1912,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -1933,7 +1933,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -1956,7 +1956,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1973,7 +1973,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -1991,7 +1991,7 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_modules + and params.general.geometry_module == "panels3d" and np.isclose( com[1], @@ -2351,7 +2351,7 @@ def set_length_scales(self, params, domain_markers): if ( self.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): for module_id in range(params.pv_array.modules_per_span): internal_surface_tags.extend( diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index 001e55a..d10809c 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -707,7 +707,7 @@ def P_connector(u): if ( domain.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): self.calculate_K_for_Robin_BC(domain, flow, params) @@ -717,7 +717,7 @@ def P_connector(u): if ( domain.modeling_torque_tube - and params.general.geometry_modules == "panels3d" + and params.general.geometry_module == "panels3d" ): self.res = ( m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure diff --git a/pvade/structure/boundary_conditions.py b/pvade/structure/boundary_conditions.py index 3adadc4..57fc98d 100644 --- a/pvade/structure/boundary_conditions.py +++ b/pvade/structure/boundary_conditions.py @@ -347,7 +347,7 @@ def connection_point_up_helper(nodes_to_pin_between): return fn_handle - if domain.modeling_torque_tube and params.general.geometry_modules == "panels3d": + if domain.modeling_torque_tube and params.general.geometry_module == "panels3d": # # Start pinning along the lines expressed byt numpy_pt_total_array # The center line of connectors bottom surface is fixed to remove rigid body motion From 7ad7580510f1112ed8cfc2988f0ebb8bfcf6a441 Mon Sep 17 00:00:00 2001 From: arswalid Date: Wed, 4 Feb 2026 13:05:15 -0700 Subject: [PATCH 37/37] black formatted latest changes --- pvade/fluid/FlowManager.py | 5 +-- pvade/geometry/panels3d/DomainCreation.py | 38 ++++++++--------------- pvade/structure/ElasticityAnalysis.py | 10 ++---- 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/pvade/fluid/FlowManager.py b/pvade/fluid/FlowManager.py index 4693b9a..640e971 100644 --- a/pvade/fluid/FlowManager.py +++ b/pvade/fluid/FlowManager.py @@ -1226,10 +1226,7 @@ def solve(self, domain, params, current_time): self.compute_lift_and_drag(params, current_time) # self.compute_panel_torques(domain, params) - if ( - domain.modeling_torque_tube - and params.general.geometry_module == "panels3d" - ): + if domain.modeling_torque_tube and params.general.geometry_module == "panels3d": self.compute_double_integral_panel_torques(domain, params) # Compute the pressure drop between the inlet and outlet diff --git a/pvade/geometry/panels3d/DomainCreation.py b/pvade/geometry/panels3d/DomainCreation.py index 9edc71f..540870b 100644 --- a/pvade/geometry/panels3d/DomainCreation.py +++ b/pvade/geometry/panels3d/DomainCreation.py @@ -679,7 +679,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_module== "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[params.pv_array.modules_per_span] @@ -817,8 +817,7 @@ def Rz(theta): / 2.0, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" ): target_key = f"interior_surface_{panel_ct:.0f}" surface_located_or_not = True @@ -831,8 +830,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -852,8 +850,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -875,8 +872,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] ) @@ -892,8 +888,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -910,8 +905,7 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1898,8 +1892,7 @@ def Rz(theta): / 2.0, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" ): target_key = f"interior_surface_{panel_ct:.0f}" surface_located_or_not = True @@ -1912,8 +1905,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1933,8 +1925,7 @@ def Rz(theta): * half_chord, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1956,8 +1947,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] ) @@ -1973,8 +1963,7 @@ def Rz(theta): - half_thickness, ) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] @@ -1991,8 +1980,7 @@ def Rz(theta): if ( np.isclose(com[2], -half_thickness) and self.modeling_torque_tube - and params.general.geometry_module - == "panels3d" + and params.general.geometry_module == "panels3d" and np.isclose( com[1], module_distances[module_id] diff --git a/pvade/structure/ElasticityAnalysis.py b/pvade/structure/ElasticityAnalysis.py index d10809c..fe320d2 100644 --- a/pvade/structure/ElasticityAnalysis.py +++ b/pvade/structure/ElasticityAnalysis.py @@ -705,20 +705,14 @@ def P_connector(u): domain.structure.msh, [0.0, 0.0, 1.0] ) # surface traction, N/m^2 - if ( - domain.modeling_torque_tube - and params.general.geometry_module == "panels3d" - ): + if domain.modeling_torque_tube and params.general.geometry_module == "panels3d": self.calculate_K_for_Robin_BC(domain, flow, params) dx_structure = ufl.Measure( "dx", domain=domain.structure.msh, subdomain_data=domain.structure.cell_tags ) - if ( - domain.modeling_torque_tube - and params.general.geometry_module == "panels3d" - ): + if domain.modeling_torque_tube and params.general.geometry_module == "panels3d": self.res = ( m(self.avg(self.a_old, a_new, self.alpha_m), self.u_) * dx_structure + c(self.avg(self.v_old, v_new, self.alpha_f), self.u_) * dx_structure