# ! /usr/bin/python2
# -*- coding: utf-8; -*-
#
# (c) 2013 booya (http://booya.at)
#
# This file is part of the OpenGlider project.
#
# OpenGlider is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# OpenGlider is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OpenGlider. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
import copy
import math
import numpy
from openglider.glider.in_out import IMPORT_GEOMETRY, EXPORT_3D
from openglider.glider.shape import Shape
from openglider.mesh import Mesh
from openglider.utils import consistent_value
from openglider.utils.distribution import Distribution
from openglider.vector.functions import norm, rotation_2d
from openglider.vector.projection import flatten_list
[docs]class Glider(object):
cell_naming_scheme = "c{cell_no}"
rib_naming_scheme = "r{rib_no}"
def __init__(self, cells=None, lineset=None):
self.cells = cells or []
self.lineset = lineset
def __json__(self):
new = self.copy()
ribs = new.ribs[:]
# de-reference Ribs not to store too much data
for cell in new.cells:
cell.rib1 = ribs.index(cell.rib1)
cell.rib2 = ribs.index(cell.rib2)
return {"cells": new.cells,
"ribs": ribs,
"lineset": self.lineset
}
@classmethod
def __from_json__(cls, cells, ribs, lineset):
for cell in cells:
if isinstance(cell.rib1, int):
cell.rib1 = ribs[cell.rib1]
if isinstance(cell.rib2, int):
cell.rib2 = ribs[cell.rib2]
return cls(cells, lineset=lineset)
def __repr__(self):
return """
{}
Span: {}
A/R: {}
Cells: {}
""".format(super(Glider, self).__repr__(),
self.span,
self.aspect_ratio,
len(self.cells))
@classmethod
[docs] def import_geometry(cls, path, filetype=None):
if not filetype:
filetype = path.split(".")[-1]
glider = cls()
IMPORT_GEOMETRY[filetype](path, glider=glider)
return glider
[docs] def export_3d(self, path="", *args, **kwargs):
filetype = path.split(".")[-1]
return EXPORT_3D[filetype](self, path, *args, **kwargs)
[docs] def rename_parts(self):
for rib_no, rib in enumerate(self.ribs):
rib.name = self.rib_naming_scheme.format(rib=rib, rib_no=rib_no+1)
rib.rename_parts()
for cell_no, cell in enumerate(self.cells):
cell.name = self.cell_naming_scheme.format(cell=cell, cell_no=cell_no+1)
cell.rename_parts()
[docs] def get_panel_groups(self):
panels = {}
for cell in self.cells:
for panel in cell.panels:
panels.setdefault(panel.material_code, [])
panels[panel.material_code] += panel
return panels
[docs] def get_mesh(self, midribs=0):
mesh = sum([Mesh.from_rib(rib) for rib in self.ribs], Mesh())
for cell in self.cells:
for panel in cell.panels:
mesh += panel.get_mesh(cell, midribs)
for diagonal in cell.diagonals:
mesh += diagonal.get_mesh(cell)
mesh += self.lineset.get_mesh()
mesh += self.get_mesh_panels()
mesh += self.get_mesh_hull(midribs)
return mesh
[docs] def get_mesh_panels(self, num_midribs=0):
mesh = Mesh(name="panels")
for cell in self.cells:
for panel in cell.panels:
mesh += panel.get_mesh(cell, num_midribs)
return mesh
[docs] def get_mesh_hull(self, num_midribs=0, ballooning=True):
ribs = self.return_ribs(num=num_midribs, ballooning=ballooning)
num = len(ribs)
numpoints = len(ribs[0]) # points per rib
polygons = []
boundary = {
"ribs": [],
"trailing_edge": []
}
for i in range(num-1): # because we use i+1 below
boundary["trailing_edge"].append(i*numpoints)
if not i % (num_midribs+1):
boundary["ribs"] += [i*numpoints+k for k in range(numpoints-1)]
for k in range(numpoints - 1): # same reason as above
kplus = (k+1) % (numpoints-1)
polygons.append([
i * numpoints + k,
i * numpoints + kplus,
(i + 1) * numpoints + kplus,
(i + 1) * numpoints + k
])
return Mesh.from_indexed(numpy.concatenate(ribs), {"hull": polygons}, boundary)
[docs] def return_ribs(self, num=0, ballooning=True):
"""
Get a list of rib-curves
:param num: number of midribs per cell
:param ballooning: calculate ballooned cells
:return: nested list of ribs [[[x,y,z],p2,p3...],rib2,rib3,..]
"""
num += 1
if not self.cells:
return numpy.array([])
#will hold all the points
ribs = []
for cell in self.cells:
for y in range(num):
ribs.append(cell.midrib(y * 1. / num, ballooning=ballooning).data)
ribs.append(self.cells[-1].midrib(1.).data)
return ribs
[docs] def apply_mean_ribs(self, num_mean=8):
"""
Calculate Mean ribs
:param num_mean:
:return:
"""
ribs = [cell.mean_rib(num_mean) for cell in self.cells]
if self.has_center_cell:
ribs.insert(0, ribs[1])
else:
ribs.insert(0, ribs[0])
for i in range(len(self.ribs))[:-1]:
self.ribs[i].profile_2d = (ribs[i] + ribs[i+1]) * 0.5
[docs] def return_average_ribs(self, num=0, num_average=8):
glider = self.copy()
glider.apply_mean_ribs(num_average)
glider.close_rib()
return glider.return_ribs(num, ballooning=False)
[docs] def close_rib(self, rib=-1):
self.ribs[rib].profile_2d *= 0.
[docs] def get_midrib(self, y=0):
k = y % 1
i = int(y - k)
if i == len(self.cells) and k == 0: # Stabi-rib
i -= 1
k = 1
return self.cells[i].midrib(k)
[docs] def get_point(self, y=0, x=-1):
"""
Get a point on the glider
:param y: span-wise argument (0, cell_no)
:param x: chord-wise argument (-1, 1)
:return: point
"""
rib = self.get_midrib(y)
rib_no = int(y)
dy = y - rib_no
if rib_no == len(self.ribs)-1:
rib_no -= 1
dy = 1
left_rib = self.ribs[rib_no]
right_rib = self.ribs[rib_no+1]
ik_l = left_rib.profile_2d(x)
ik_r = right_rib.profile_2d(x)
ik = ik_l + dy * (ik_r - ik_l)
return rib[ik]
[docs] def mirror(self, cutmidrib=True):
if self.has_center_cell and cutmidrib: # Cut midrib
self.cells = self.cells[1:]
for rib in self.ribs:
rib.mirror()
for cell in self.cells:
cell.mirror(mirror_ribs=False)
self.cells = self.cells[::-1]
[docs] def copy(self):
return copy.deepcopy(self)
[docs] def copy_complete(self):
"""Returns a mirrored and combined copy of the glider, ready for export/view"""
other = self.copy()
other2 = self.copy()
other2.mirror()
other2.cells[-1].rib2 = other.cells[0].rib1
other2.cells = other2.cells + other.cells
# lineset
for p in other2.lineset.attachment_points:
p.get_position()
for node in [node for node in other2.lineset.nodes]:
if node.type != 2:
node.vec *= numpy.array([1, -1, 1])
if all(node.force):
node.force *= numpy.array([1, -1, 1])
other2.lineset.lines += other.lineset.lines
other2.lineset.sort_lines()
other2.lineset.recalc()
# rename
return other2
[docs] def scale(self, faktor):
for rib in self.ribs:
rib.pos *= faktor
rib.chord *= faktor
# todo: scale lines,
@property
def shape_simple(self, cut_center=True):
"""
Simple (rectangular) shape representation for spline inputs
"""
last_pos = numpy.array([0, 0]) # y,z
front = []
back = []
x = 0
for rib in self.ribs:
width = norm(rib.pos[1:] - last_pos)
last_pos = rib.pos[1:]
x += width * (rib.pos[1] > 0) # x-value
if x == 0:
last_pos = numpy.array([0., 0.])
y_front = -rib.pos[0] + rib.chord * rib.startpos
y_back = -rib.pos[0] + rib.chord * (rib.startpos - 1)
front.append([x, y_front])
back.append([x, y_back])
return Shape(front, back)
@property
def shape_flattened(self):
"""
Projected Shape of the glider (as it would lie on the ground - flattened)
"""
rot = rotation_2d(numpy.pi / 2)
front, back = flatten_list(self.get_spanwise(0), self.get_spanwise(1))
return Shape([rot.dot(p) for p in front], [rot.dot(p) for p in back])
# delete ?
@property
def arc(self):
return [rib.pos[1:] for rib in self.ribs]
@property
def ribs(self):
ribs = []
for cell in self.cells:
for rib in cell.ribs:
if rib not in ribs:
ribs.append(rib)
return ribs
@property
def profile_numpoints(self):
return consistent_value(self.ribs, 'profile_2d.numpoints')
@profile_numpoints.setter
def profile_numpoints(self, numpoints):
self.profile_x_values = Distribution.from_nose_cos_distribution(numpoints, 0.3)
@property
def profile_x_values(self):
return self.ribs[0].profile_2d.x_values
# return consistent_value(self.ribs, 'profile_2d.x_values')
@profile_x_values.setter
def profile_x_values(self, xvalues):
for rib in self.ribs:
rib.profile_2d.x_values = xvalues
@property
def span(self):
span = sum([cell.span for cell in self.cells])
if self.has_center_cell:
return 2 * span - self.cells[0].span
else:
return 2 * span
@span.setter
def span(self, span):
faktor = span / self.span
self.scale(faktor)
@property
def area(self):
area = 0.
if len(self.ribs) == 0:
return 0
front = self.get_spanwise(0)
back = self.get_spanwise(1)
front[0][1] = 0 # Get only half a midrib, if there is...
back[0][1] = 0
for i in range(len(front) - 1):
area += norm(numpy.cross(front[i] - front[i + 1], back[i + 1] - front[i + 1]))
area += norm(numpy.cross(back[i] - back[i + 1], back[i] - front[i]))
# By this we get twice the area of half the glider :)
# http://en.wikipedia.org/wiki/Triangle#Using_vectors
return area
@area.setter
def area(self, area):
faktor = area / self.area
self.scale(math.sqrt(faktor))
@property
def projected_area(self):
complete = self.copy_complete()
return sum(cell.projected_area for cell in complete.cells)
@property
def aspect_ratio(self):
return self.span ** 2 / self.area
@aspect_ratio.setter
def aspect_ratio(self, aspect_ratio):
area_backup = self.area
factor = self.aspect_ratio / aspect_ratio
for rib in self.ribs:
rib.chord *= factor
self.area = area_backup
[docs] def get_spanwise(self, x=None):
"""
Return a list of points for a x_value
"""
if x == 0:
return copy.deepcopy([rib.pos for rib in self.ribs]) # This is much faster
else:
return [rib.align([x, 0, 0]) for rib in self.ribs]
@property
def attachment_points(self):
points = []
for line in self.lineset.lowest_lines:
points += self.lineset.get_upper_influence_nodes(line)
return points
@property
def has_center_cell(self):
return self.ribs[0].pos[1] != 0
@property
def glide(self):
return consistent_value(self.ribs, 'glide')
@glide.setter
def glide(self, glide):
for rib in self.ribs:
rib.glide = glide