symcad.parts.composite.PlanarAirfoils
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 PlanarAirfoils(CompositeShape): 25 """Model representing a set of planar 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 planar 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 `PlanarAirfoils`.""" 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 PlanarAirfoils.z_val(x, max_thickness_percent, 83 chord_length_mm))) 84 points_lower.append(FreeCAD.Vector(x * chord_length_mm, 85 -PlanarAirfoils.z_val(x, max_thickness_percent, 86 chord_length_mm))) 87 x += 0.01 88 bodies = [] 89 for i in range(2): 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 = \ 104 FreeCAD.Vector(0, -separation_radius_mm if i == 0 else separation_radius_mm, 0) 105 body.Placement = \ 106 FreeCAD.Placement(placement_vector, 107 FreeCAD.Rotation(-curvature_tilt if i == 1 else curvature_tilt, 108 0, i * 180.0)) 109 bodies.append(body) 110 fusion = doc.addObject('Part::MultiFuse', 'Fusion') 111 fusion.Shapes = bodies 112 doc.recompute() 113 airfoils = fusion.Shape 114 FreeCAD.closeDocument(doc.Name) 115 return airfoils 116 117 118 # Geometry setter ------------------------------------------------------------------------------ 119 120 def set_geometry(self, *, max_thickness_percent: Union[float, None], 121 chord_length_m: Union[float, None], 122 span_m: Union[float, None], 123 separation_radius_m: Union[float, None], 124 curvature_tilt_deg: Union[float, None]) -> PlanarAirfoils: 125 """Sets the physical geometry of the current `PlanarAirfoils` object. 126 127 See the `PlanarAirfoils` class documentation for a description of each geometric 128 parameter. 129 """ 130 self.geometry.set(max_thickness=max_thickness_percent, 131 chord_length=chord_length_m, 132 span=span_m, 133 separation_radius=separation_radius_m, 134 curvature_tilt=curvature_tilt_deg) 135 return self 136 137 def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]: 138 parameter_bounds = { 139 'max_thickness': (0.01, 0.90), 140 'chord_length': (0.1, 2.0), 141 'span': (0.0, 2.0), 142 'separation_radius': (0.0, 1.5), 143 'curvature_tilt': (0.0, 45.0) 144 } 145 return parameter_bounds.get(parameter, (0.0, 0.0)) 146 147 148 # Geometric properties ------------------------------------------------------------------------- 149 150 @property 151 def material_volume(self) -> Union[float, Expr]: 152 return self.displaced_volume 153 154 @property 155 def displaced_volume(self) -> Union[float, Expr]: 156 x, points_upper = 0.0, [] 157 while x <= 1.009: 158 points_upper.append((x * self.geometry.chord_length, 159 PlanarAirfoils.z_val(x, self.geometry.max_thickness, 160 self.geometry.chord_length))) 161 x += 0.01 162 area = 0.0 163 for i in range(1, len(points_upper) - 1): 164 area += points_upper[i][1] 165 area = points_upper[0][1] + (2.0 * area) + points_upper[len(points_upper)-1][1] 166 h = (points_upper[len(points_upper)-1][0] - points_upper[0][0]) / (len(points_upper) - 1) 167 return 2.0 * h * area * self.geometry.span 168 169 @property 170 def surface_area(self) -> Union[float, Expr]: 171 x, points_upper = 0.0, [] 172 while x <= 1.009: 173 points_upper.append((x * self.geometry.chord_length, 174 PlanarAirfoils.z_val(x, self.geometry.max_thickness, 175 self.geometry.chord_length))) 176 x += 0.01 177 area = 0.0 178 for i in range(1, len(points_upper)): 179 x_diff = (points_upper[i][0] - points_upper[i-1][0]) 180 y_diff = (points_upper[i][1] - points_upper[i-1][1]) 181 area += sqrt(x_diff**2 + y_diff**2) 182 area = (2.0 * area) * self.geometry.span 183 return 2.0 * area 184 185 @property 186 def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr], 187 Union[float, Expr], 188 Union[float, Expr]]: 189 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 190 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 191 return ((0.41 * cosine * self.geometry.chord_length) + (0.5 * sine * self.geometry.span), 192 0.5 * self.unoriented_width, 193 0.5 * self.unoriented_height) 194 195 @property 196 def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr], 197 Union[float, Expr], 198 Union[float, Expr]]: 199 return self.unoriented_center_of_gravity 200 201 @property 202 def unoriented_length(self) -> Union[float, Expr]: 203 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 204 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 205 return (cosine * self.geometry.chord_length) + (sine * self.geometry.span) 206 207 @property 208 def unoriented_width(self) -> Union[float, Expr]: 209 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 210 return (2.0 * cosine * self.geometry.span) + (2.0 * self.geometry.separation_radius) 211 212 @property 213 def unoriented_height(self) -> Union[float, Expr]: 214 return self.geometry.max_thickness * self.geometry.chord_length 215 216 @property 217 def oriented_length(self) -> Union[float, Expr]: 218 # TODO: Implement this 219 return 0 220 221 @property 222 def oriented_width(self) -> Union[float, Expr]: 223 # TODO: Implement this 224 return 0 225 226 @property 227 def oriented_height(self) -> Union[float, Expr]: 228 # TODO: Implement this 229 return 0
25class PlanarAirfoils(CompositeShape): 26 """Model representing a set of planar 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 planar 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 `PlanarAirfoils`.""" 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 PlanarAirfoils.z_val(x, max_thickness_percent, 84 chord_length_mm))) 85 points_lower.append(FreeCAD.Vector(x * chord_length_mm, 86 -PlanarAirfoils.z_val(x, max_thickness_percent, 87 chord_length_mm))) 88 x += 0.01 89 bodies = [] 90 for i in range(2): 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 = \ 105 FreeCAD.Vector(0, -separation_radius_mm if i == 0 else separation_radius_mm, 0) 106 body.Placement = \ 107 FreeCAD.Placement(placement_vector, 108 FreeCAD.Rotation(-curvature_tilt if i == 1 else curvature_tilt, 109 0, i * 180.0)) 110 bodies.append(body) 111 fusion = doc.addObject('Part::MultiFuse', 'Fusion') 112 fusion.Shapes = bodies 113 doc.recompute() 114 airfoils = fusion.Shape 115 FreeCAD.closeDocument(doc.Name) 116 return airfoils 117 118 119 # Geometry setter ------------------------------------------------------------------------------ 120 121 def set_geometry(self, *, max_thickness_percent: Union[float, None], 122 chord_length_m: Union[float, None], 123 span_m: Union[float, None], 124 separation_radius_m: Union[float, None], 125 curvature_tilt_deg: Union[float, None]) -> PlanarAirfoils: 126 """Sets the physical geometry of the current `PlanarAirfoils` object. 127 128 See the `PlanarAirfoils` class documentation for a description of each geometric 129 parameter. 130 """ 131 self.geometry.set(max_thickness=max_thickness_percent, 132 chord_length=chord_length_m, 133 span=span_m, 134 separation_radius=separation_radius_m, 135 curvature_tilt=curvature_tilt_deg) 136 return self 137 138 def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]: 139 parameter_bounds = { 140 'max_thickness': (0.01, 0.90), 141 'chord_length': (0.1, 2.0), 142 'span': (0.0, 2.0), 143 'separation_radius': (0.0, 1.5), 144 'curvature_tilt': (0.0, 45.0) 145 } 146 return parameter_bounds.get(parameter, (0.0, 0.0)) 147 148 149 # Geometric properties ------------------------------------------------------------------------- 150 151 @property 152 def material_volume(self) -> Union[float, Expr]: 153 return self.displaced_volume 154 155 @property 156 def displaced_volume(self) -> Union[float, Expr]: 157 x, points_upper = 0.0, [] 158 while x <= 1.009: 159 points_upper.append((x * self.geometry.chord_length, 160 PlanarAirfoils.z_val(x, self.geometry.max_thickness, 161 self.geometry.chord_length))) 162 x += 0.01 163 area = 0.0 164 for i in range(1, len(points_upper) - 1): 165 area += points_upper[i][1] 166 area = points_upper[0][1] + (2.0 * area) + points_upper[len(points_upper)-1][1] 167 h = (points_upper[len(points_upper)-1][0] - points_upper[0][0]) / (len(points_upper) - 1) 168 return 2.0 * h * area * self.geometry.span 169 170 @property 171 def surface_area(self) -> Union[float, Expr]: 172 x, points_upper = 0.0, [] 173 while x <= 1.009: 174 points_upper.append((x * self.geometry.chord_length, 175 PlanarAirfoils.z_val(x, self.geometry.max_thickness, 176 self.geometry.chord_length))) 177 x += 0.01 178 area = 0.0 179 for i in range(1, len(points_upper)): 180 x_diff = (points_upper[i][0] - points_upper[i-1][0]) 181 y_diff = (points_upper[i][1] - points_upper[i-1][1]) 182 area += sqrt(x_diff**2 + y_diff**2) 183 area = (2.0 * area) * self.geometry.span 184 return 2.0 * area 185 186 @property 187 def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr], 188 Union[float, Expr], 189 Union[float, Expr]]: 190 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 191 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 192 return ((0.41 * cosine * self.geometry.chord_length) + (0.5 * sine * self.geometry.span), 193 0.5 * self.unoriented_width, 194 0.5 * self.unoriented_height) 195 196 @property 197 def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr], 198 Union[float, Expr], 199 Union[float, Expr]]: 200 return self.unoriented_center_of_gravity 201 202 @property 203 def unoriented_length(self) -> Union[float, Expr]: 204 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 205 sine = sin(self.geometry.curvature_tilt * math.pi / 180.0) 206 return (cosine * self.geometry.chord_length) + (sine * self.geometry.span) 207 208 @property 209 def unoriented_width(self) -> Union[float, Expr]: 210 cosine = cos(self.geometry.curvature_tilt * math.pi / 180.0) 211 return (2.0 * cosine * self.geometry.span) + (2.0 * self.geometry.separation_radius) 212 213 @property 214 def unoriented_height(self) -> Union[float, Expr]: 215 return self.geometry.max_thickness * self.geometry.chord_length 216 217 @property 218 def oriented_length(self) -> Union[float, Expr]: 219 # TODO: Implement this 220 return 0 221 222 @property 223 def oriented_width(self) -> Union[float, Expr]: 224 # TODO: Implement this 225 return 0 226 227 @property 228 def oriented_height(self) -> Union[float, Expr]: 229 # TODO: Implement this 230 return 0
Model representing a set of planar 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 planar 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 planar 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.
121 def set_geometry(self, *, max_thickness_percent: Union[float, None], 122 chord_length_m: Union[float, None], 123 span_m: Union[float, None], 124 separation_radius_m: Union[float, None], 125 curvature_tilt_deg: Union[float, None]) -> PlanarAirfoils: 126 """Sets the physical geometry of the current `PlanarAirfoils` object. 127 128 See the `PlanarAirfoils` class documentation for a description of each geometric 129 parameter. 130 """ 131 self.geometry.set(max_thickness=max_thickness_percent, 132 chord_length=chord_length_m, 133 span=span_m, 134 separation_radius=separation_radius_m, 135 curvature_tilt=curvature_tilt_deg) 136 return self
Sets the physical geometry of the current PlanarAirfoils object.
See the PlanarAirfoils class documentation for a description of each geometric
parameter.
138 def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]: 139 parameter_bounds = { 140 'max_thickness': (0.01, 0.90), 141 'chord_length': (0.1, 2.0), 142 'span': (0.0, 2.0), 143 'separation_radius': (0.0, 1.5), 144 'curvature_tilt': (0.0, 45.0) 145 } 146 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