symcad.parts.composite.CrossFormAirfoils
1#!/usr/bin/env python3 2# Copyright (C) 2022, Will Hedgecock 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 3 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17from __future__ import annotations 18from PyFreeCAD.FreeCAD import FreeCAD, Part 19from typing import Dict, Optional, Tuple, Union 20from sympy import Expr, Symbol, sqrt, sin, cos 21from . import CompositeShape 22import math 23 24class CrossFormAirfoils(CompositeShape): 25 """Model representing a set of cross-form parameteric airfoils. 26 27 By default, the airfoils are oriented in the following configuration: 28 29  30 31 The `geometry` of this shape includes the following parameters: 32 33 - `max_thickness`: Maximum thickness (in `% of length`) of each airfoil 34 - `chord_length`: Length (in `m`) of the chord of each airfoil 35 - `span`: Width (in `m`) of each airfoil 36 - `separation_radius`: Radius (in `m`) of separation between each airfoil 37 - `curvature_tilt`: Amount of tilt (in `deg`) of each airfoil 38 39 Note that the above dimensions should be interpreted as if the airfoils are unrotated. In other 40 words, any shape rotation takes place *after* the airfoil dimensions have been specified. 41 """ 42 43 # Constructor ---------------------------------------------------------------------------------- 44 45 def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None: 46 """Initializes a cross-form parametric airfoil object. 47 48 Parameters 49 ---------- 50 identifier : `str` 51 Unique identifying name for the object. 52 material_density_kg_m3 : `float`, optional, default=1.0 53 Uniform material density in `kg/m^3` to be used in mass property calculations. 54 """ 55 super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3) 56 setattr(self.geometry, 'max_thickness', Symbol(self.name + '_max_thickness')) 57 setattr(self.geometry, 'chord_length', Symbol(self.name + '_chord_length')) 58 setattr(self.geometry, 'span', Symbol(self.name + '_span')) 59 setattr(self.geometry, 'separation_radius', Symbol(self.name + '_separation_radius')) 60 setattr(self.geometry, 'curvature_tilt', Symbol(self.name + '_curvature_tilt')) 61 62 63 # CAD generation function ---------------------------------------------------------------------- 64 65 @staticmethod 66 def z_val(x: float, max_thickness: float, chord_length: float) -> float: 67 return 5.0 * max_thickness * chord_length * ( 68 (0.2969 * x**0.5) - (0.1260 * x) - (0.3516 * x**2) + (0.2843 * x**3) - (0.1036 * x**4)) 69 70 @staticmethod 71 def __create_cad__(params: Dict[str, float], _fully_displace: bool) -> Part.Solid: 72 """Scripted CAD generation method for `CrossFormAirfoils`.""" 73 doc = FreeCAD.newDocument('Temp') 74 max_thickness_percent = params['max_thickness'] 75 chord_length_mm = 1000.0 * params['chord_length'] 76 span_mm = 1000.0 * params['span'] 77 separation_radius_mm = 1000.0 * params['separation_radius'] 78 curvature_tilt = params['curvature_tilt'] 79 x, points_upper, points_lower = 0.0, [], [] 80 while x <= 1.009: 81 points_upper.append(FreeCAD.Vector(x * chord_length_mm, 82 CrossFormAirfoils.z_val(x, max_thickness_percent, 83 chord_length_mm))) 84 points_lower.append(FreeCAD.Vector(x * chord_length_mm, 85 -CrossFormAirfoils.z_val(x, max_thickness_percent, 86 chord_length_mm))) 87 x += 0.01 88 bodies = [] 89 for i in range(4): 90 body = doc.addObject('PartDesign::Body','Airfoil' + str(i)) 91 sketch = doc.addObject('Sketcher::SketchObject', 'Sketch' + str(i)) 92 sketch.Support = doc.XZ_Plane 93 sketch.MapMode = 'FlatFace' 94 body.addObject(sketch) 95 sketch.addGeometry(Part.BSplineCurve(points_upper, None, None, 96 False, 3, None, False), False) 97 sketch.addGeometry(Part.BSplineCurve(points_lower, None, None, 98 False, 3, None, False), False) 99 pad = doc.addObject('PartDesign::Pad', 'Pad' + str(i)) 100 body.addObject(pad) 101 pad.Length = int(span_mm) 102 pad.Profile = sketch 103 placement_vector = FreeCAD.Vector(0, -separation_radius_mm if i == 0 else 104 (separation_radius_mm if i == 2 else 0), 105 -separation_radius_mm if i == 1 else 106 (separation_radius_mm if i == 3 else 0)) 107 body.Placement = FreeCAD.Placement(placement_vector, 108 FreeCAD.Rotation(-curvature_tilt if i == 2 else 109 (curvature_tilt if i == 0 else 0), 110 -curvature_tilt if i == 1 else 111 (curvature_tilt if i == 3 else 0), 112 i * 90.0)) 113 bodies.append(body) 114 fusion = doc.addObject('Part::MultiFuse', 'Fusion') 115 fusion.Shapes = bodies 116 doc.recompute() 117 airfoils = fusion.Shape 118 FreeCAD.closeDocument(doc.Name) 119 return airfoils 120 121 122 # Geometry setter ------------------------------------------------------------------------------ 123 124 def set_geometry(self, *, max_thickness_percent: Union[float, None], 125 chord_length_m: Union[float, None], 126 span_m: Union[float, None], 127 separation_radius_m: Union[float, None], 128 curvature_tilt_deg: Union[float, None]) -> CrossFormAirfoils: 129 """Sets the physical geometry of the current `CrossFormAirfoils` object. 130 131 See the `CrossFormAirfoils` class documentation for a description of each geometric 132 parameter. 133 """ 134 self.geometry.set(max_thickness=max_thickness_percent, 135 chord_length=chord_length_m, 136 span=span_m, 137 separation_radius=separation_radius_m, 138 curvature_tilt=curvature_tilt_deg) 139 return self 140 141 def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]: 142 parameter_bounds = { 143 'max_thickness': (0.01, 0.90), 144 'chord_length': (0.1, 2.0), 145 'span': (0.0, 2.0), 146 'separation_radius': (0.0, 1.5), 147 'curvature_tilt': (0.0, 45.0) 148 } 149 return parameter_bounds.get(parameter, (0.0, 0.0)) 150 151 152 # Geometric properties ------------------------------------------------------------------------- 153 154 @property 155 def material_volume(self) -> Union[float, Expr]: 156 return self.displaced_volume 157 158 @property 159 def displaced_volume(self) -> Union[float, Expr]: 160 x, points_upper = 0.0, [] 161 while x <= 1.009: 162 points_upper.append((x * self.geometry.chord_length, 163 CrossFormAirfoils.z_val(x, self.geometry.max_thickness, 164 self.geometry.chord_length))) 165 x += 0.01 166 area = 0.0 167 for i in range(1, len(points_upper) - 1): 168 area += points_upper[i][1] 169 area = points_upper[0][1] + (2.0 * area) + points_upper[len(points_upper)-1][1] 170 h = (points_upper[len(points_upper)-1][0] - points_upper[0][0]) / (len(points_upper) - 1) 171 return 4.0 * h * area * self.geometry.span 172 173 @property 174 def surface_area(self) -> Union[float, Expr]: 175 x, points_upper = 0.0, [] 176 while x <= 1.009: 177 points_upper.append((x * self.geometry.chord_length, 178 CrossFormAirfoils.z_val(x, self.geometry.max_thickness, 179 self.geometry.chord_length))) 180 x += 0.01 181 area = 0.0 182 for i in range(1, len(points_upper)): 183 x_diff = (points_upper[i][0] - points_upper[i-1][0]) 184 y_diff = (points_upper[i][1] - points_upper[i-1][1]) 185 area += sqrt(x_diff**2 + y_diff**2) 186 area = (2.0 * area) * self.geometry.span 187 return 4.0 * area 188 189 @property 190 def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr], 191 Union[float, Expr], 192 Union[float, Expr]]: 193 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 194 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 195 return ((0.41 * cosine * self.geometry.chord_length) + (0.5 * sine * self.geometry.span), 196 0.5 * self.unoriented_width, 197 0.5 * self.unoriented_height) 198 199 @property 200 def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr], 201 Union[float, Expr], 202 Union[float, Expr]]: 203 return self.unoriented_center_of_gravity 204 205 @property 206 def unoriented_length(self) -> Union[float, Expr]: 207 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 208 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 209 return (cosine * self.geometry.chord_length) + (sine * self.geometry.span) 210 211 @property 212 def unoriented_width(self) -> Union[float, Expr]: 213 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 214 return (2.0 * cosine * self.geometry.span) + (2.0 * self.geometry.separation_radius) 215 216 @property 217 def unoriented_height(self) -> Union[float, Expr]: 218 return self.unoriented_width 219 220 @property 221 def oriented_length(self) -> Union[float, Expr]: 222 # TODO: Implement this 223 return 0 224 225 @property 226 def oriented_width(self) -> Union[float, Expr]: 227 # TODO: Implement this 228 return 0 229 230 @property 231 def oriented_height(self) -> Union[float, Expr]: 232 # TODO: Implement this 233 return 0
25class CrossFormAirfoils(CompositeShape): 26 """Model representing a set of cross-form parameteric airfoils. 27 28 By default, the airfoils are oriented in the following configuration: 29 30  31 32 The `geometry` of this shape includes the following parameters: 33 34 - `max_thickness`: Maximum thickness (in `% of length`) of each airfoil 35 - `chord_length`: Length (in `m`) of the chord of each airfoil 36 - `span`: Width (in `m`) of each airfoil 37 - `separation_radius`: Radius (in `m`) of separation between each airfoil 38 - `curvature_tilt`: Amount of tilt (in `deg`) of each airfoil 39 40 Note that the above dimensions should be interpreted as if the airfoils are unrotated. In other 41 words, any shape rotation takes place *after* the airfoil dimensions have been specified. 42 """ 43 44 # Constructor ---------------------------------------------------------------------------------- 45 46 def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None: 47 """Initializes a cross-form parametric airfoil object. 48 49 Parameters 50 ---------- 51 identifier : `str` 52 Unique identifying name for the object. 53 material_density_kg_m3 : `float`, optional, default=1.0 54 Uniform material density in `kg/m^3` to be used in mass property calculations. 55 """ 56 super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3) 57 setattr(self.geometry, 'max_thickness', Symbol(self.name + '_max_thickness')) 58 setattr(self.geometry, 'chord_length', Symbol(self.name + '_chord_length')) 59 setattr(self.geometry, 'span', Symbol(self.name + '_span')) 60 setattr(self.geometry, 'separation_radius', Symbol(self.name + '_separation_radius')) 61 setattr(self.geometry, 'curvature_tilt', Symbol(self.name + '_curvature_tilt')) 62 63 64 # CAD generation function ---------------------------------------------------------------------- 65 66 @staticmethod 67 def z_val(x: float, max_thickness: float, chord_length: float) -> float: 68 return 5.0 * max_thickness * chord_length * ( 69 (0.2969 * x**0.5) - (0.1260 * x) - (0.3516 * x**2) + (0.2843 * x**3) - (0.1036 * x**4)) 70 71 @staticmethod 72 def __create_cad__(params: Dict[str, float], _fully_displace: bool) -> Part.Solid: 73 """Scripted CAD generation method for `CrossFormAirfoils`.""" 74 doc = FreeCAD.newDocument('Temp') 75 max_thickness_percent = params['max_thickness'] 76 chord_length_mm = 1000.0 * params['chord_length'] 77 span_mm = 1000.0 * params['span'] 78 separation_radius_mm = 1000.0 * params['separation_radius'] 79 curvature_tilt = params['curvature_tilt'] 80 x, points_upper, points_lower = 0.0, [], [] 81 while x <= 1.009: 82 points_upper.append(FreeCAD.Vector(x * chord_length_mm, 83 CrossFormAirfoils.z_val(x, max_thickness_percent, 84 chord_length_mm))) 85 points_lower.append(FreeCAD.Vector(x * chord_length_mm, 86 -CrossFormAirfoils.z_val(x, max_thickness_percent, 87 chord_length_mm))) 88 x += 0.01 89 bodies = [] 90 for i in range(4): 91 body = doc.addObject('PartDesign::Body','Airfoil' + str(i)) 92 sketch = doc.addObject('Sketcher::SketchObject', 'Sketch' + str(i)) 93 sketch.Support = doc.XZ_Plane 94 sketch.MapMode = 'FlatFace' 95 body.addObject(sketch) 96 sketch.addGeometry(Part.BSplineCurve(points_upper, None, None, 97 False, 3, None, False), False) 98 sketch.addGeometry(Part.BSplineCurve(points_lower, None, None, 99 False, 3, None, False), False) 100 pad = doc.addObject('PartDesign::Pad', 'Pad' + str(i)) 101 body.addObject(pad) 102 pad.Length = int(span_mm) 103 pad.Profile = sketch 104 placement_vector = FreeCAD.Vector(0, -separation_radius_mm if i == 0 else 105 (separation_radius_mm if i == 2 else 0), 106 -separation_radius_mm if i == 1 else 107 (separation_radius_mm if i == 3 else 0)) 108 body.Placement = FreeCAD.Placement(placement_vector, 109 FreeCAD.Rotation(-curvature_tilt if i == 2 else 110 (curvature_tilt if i == 0 else 0), 111 -curvature_tilt if i == 1 else 112 (curvature_tilt if i == 3 else 0), 113 i * 90.0)) 114 bodies.append(body) 115 fusion = doc.addObject('Part::MultiFuse', 'Fusion') 116 fusion.Shapes = bodies 117 doc.recompute() 118 airfoils = fusion.Shape 119 FreeCAD.closeDocument(doc.Name) 120 return airfoils 121 122 123 # Geometry setter ------------------------------------------------------------------------------ 124 125 def set_geometry(self, *, max_thickness_percent: Union[float, None], 126 chord_length_m: Union[float, None], 127 span_m: Union[float, None], 128 separation_radius_m: Union[float, None], 129 curvature_tilt_deg: Union[float, None]) -> CrossFormAirfoils: 130 """Sets the physical geometry of the current `CrossFormAirfoils` object. 131 132 See the `CrossFormAirfoils` class documentation for a description of each geometric 133 parameter. 134 """ 135 self.geometry.set(max_thickness=max_thickness_percent, 136 chord_length=chord_length_m, 137 span=span_m, 138 separation_radius=separation_radius_m, 139 curvature_tilt=curvature_tilt_deg) 140 return self 141 142 def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]: 143 parameter_bounds = { 144 'max_thickness': (0.01, 0.90), 145 'chord_length': (0.1, 2.0), 146 'span': (0.0, 2.0), 147 'separation_radius': (0.0, 1.5), 148 'curvature_tilt': (0.0, 45.0) 149 } 150 return parameter_bounds.get(parameter, (0.0, 0.0)) 151 152 153 # Geometric properties ------------------------------------------------------------------------- 154 155 @property 156 def material_volume(self) -> Union[float, Expr]: 157 return self.displaced_volume 158 159 @property 160 def displaced_volume(self) -> Union[float, Expr]: 161 x, points_upper = 0.0, [] 162 while x <= 1.009: 163 points_upper.append((x * self.geometry.chord_length, 164 CrossFormAirfoils.z_val(x, self.geometry.max_thickness, 165 self.geometry.chord_length))) 166 x += 0.01 167 area = 0.0 168 for i in range(1, len(points_upper) - 1): 169 area += points_upper[i][1] 170 area = points_upper[0][1] + (2.0 * area) + points_upper[len(points_upper)-1][1] 171 h = (points_upper[len(points_upper)-1][0] - points_upper[0][0]) / (len(points_upper) - 1) 172 return 4.0 * h * area * self.geometry.span 173 174 @property 175 def surface_area(self) -> Union[float, Expr]: 176 x, points_upper = 0.0, [] 177 while x <= 1.009: 178 points_upper.append((x * self.geometry.chord_length, 179 CrossFormAirfoils.z_val(x, self.geometry.max_thickness, 180 self.geometry.chord_length))) 181 x += 0.01 182 area = 0.0 183 for i in range(1, len(points_upper)): 184 x_diff = (points_upper[i][0] - points_upper[i-1][0]) 185 y_diff = (points_upper[i][1] - points_upper[i-1][1]) 186 area += sqrt(x_diff**2 + y_diff**2) 187 area = (2.0 * area) * self.geometry.span 188 return 4.0 * area 189 190 @property 191 def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr], 192 Union[float, Expr], 193 Union[float, Expr]]: 194 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 195 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 196 return ((0.41 * cosine * self.geometry.chord_length) + (0.5 * sine * self.geometry.span), 197 0.5 * self.unoriented_width, 198 0.5 * self.unoriented_height) 199 200 @property 201 def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr], 202 Union[float, Expr], 203 Union[float, Expr]]: 204 return self.unoriented_center_of_gravity 205 206 @property 207 def unoriented_length(self) -> Union[float, Expr]: 208 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 209 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 210 return (cosine * self.geometry.chord_length) + (sine * self.geometry.span) 211 212 @property 213 def unoriented_width(self) -> Union[float, Expr]: 214 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 215 return (2.0 * cosine * self.geometry.span) + (2.0 * self.geometry.separation_radius) 216 217 @property 218 def unoriented_height(self) -> Union[float, Expr]: 219 return self.unoriented_width 220 221 @property 222 def oriented_length(self) -> Union[float, Expr]: 223 # TODO: Implement this 224 return 0 225 226 @property 227 def oriented_width(self) -> Union[float, Expr]: 228 # TODO: Implement this 229 return 0 230 231 @property 232 def oriented_height(self) -> Union[float, Expr]: 233 # TODO: Implement this 234 return 0
Model representing a set of cross-form parameteric airfoils.
By default, the airfoils are oriented in the following configuration:

The geometry of this shape includes the following parameters:
max_thickness: Maximum thickness (in% of length) of each airfoilchord_length: Length (inm) of the chord of each airfoilspan: Width (inm) of each airfoilseparation_radius: Radius (inm) of separation between each airfoilcurvature_tilt: Amount of tilt (indeg) of each airfoil
Note that the above dimensions should be interpreted as if the airfoils are unrotated. In other words, any shape rotation takes place after the airfoil dimensions have been specified.
46 def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None: 47 """Initializes a cross-form parametric airfoil object. 48 49 Parameters 50 ---------- 51 identifier : `str` 52 Unique identifying name for the object. 53 material_density_kg_m3 : `float`, optional, default=1.0 54 Uniform material density in `kg/m^3` to be used in mass property calculations. 55 """ 56 super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3) 57 setattr(self.geometry, 'max_thickness', Symbol(self.name + '_max_thickness')) 58 setattr(self.geometry, 'chord_length', Symbol(self.name + '_chord_length')) 59 setattr(self.geometry, 'span', Symbol(self.name + '_span')) 60 setattr(self.geometry, 'separation_radius', Symbol(self.name + '_separation_radius')) 61 setattr(self.geometry, 'curvature_tilt', Symbol(self.name + '_curvature_tilt'))
Initializes a cross-form parametric airfoil object.
Parameters
- identifier (
str): Unique identifying name for the object. - material_density_kg_m3 (
float, optional, default=1.0): Uniform material density inkg/m^3to be used in mass property calculations.
125 def set_geometry(self, *, max_thickness_percent: Union[float, None], 126 chord_length_m: Union[float, None], 127 span_m: Union[float, None], 128 separation_radius_m: Union[float, None], 129 curvature_tilt_deg: Union[float, None]) -> CrossFormAirfoils: 130 """Sets the physical geometry of the current `CrossFormAirfoils` object. 131 132 See the `CrossFormAirfoils` class documentation for a description of each geometric 133 parameter. 134 """ 135 self.geometry.set(max_thickness=max_thickness_percent, 136 chord_length=chord_length_m, 137 span=span_m, 138 separation_radius=separation_radius_m, 139 curvature_tilt=curvature_tilt_deg) 140 return self
Sets the physical geometry of the current CrossFormAirfoils object.
See the CrossFormAirfoils class documentation for a description of each geometric
parameter.
142 def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]: 143 parameter_bounds = { 144 'max_thickness': (0.01, 0.90), 145 'chord_length': (0.1, 2.0), 146 'span': (0.0, 2.0), 147 'separation_radius': (0.0, 1.5), 148 'curvature_tilt': (0.0, 45.0) 149 } 150 return parameter_bounds.get(parameter, (0.0, 0.0))
Abstract method that must be overridden by a concrete SymPart class to return the
minimum and maximum expected bounds for a given geometric parameter.
Parameters
- parameter (
str): Name of the geometric parameter for which to return the minimum and maximum bounds.
Returns
Tuple[float, float]: Minimum and maximum bounds for the specified geometricparameter.
Material volume (in m^3) of the SymPart (read-only).
Displaced volume (in m^3) of the SymPart (read-only).
Surface/wetted area (in m^2) of the SymPart (read-only).
Center of gravity (in m) of the unoriented SymPart (read-only).
Center of buoyancy (in m) of the unoriented SymPart (read-only).
X-axis length (in m) of the bounding box of the unoriented SymPart (read-only).
Y-axis width (in m) of the bounding box of the unoriented SymPart (read-only).
Z-axis height (in m) of the bounding box of the unoriented SymPart (read-only).
X-axis length (in m) of the bounding box of the oriented SymPart (read-only).
Y-axis length (in m) of the bounding box of the oriented SymPart (read-only).
Z-axis length (in m) of the bounding box of the oriented SymPart (read-only).
Inherited Members
- symcad.core.SymPart.SymPart
- name
- geometry
- attachment_points
- attachments
- connection_ports
- connections
- static_origin
- static_placement
- orientation
- material_density
- current_states
- is_exposed
- clone
- set_placement
- set_orientation
- set_state
- set_unexposed
- set_material_density
- add_attachment_point
- add_connection_port
- attach
- connect
- get_cad_physical_properties
- export
- get_valid_states
- mass
- oriented_center_of_gravity
- oriented_center_of_buoyancy