Source code for openglider.lines.lineset

import re
import numpy as np
import copy
import logging
from openglider.lines import SagMatrix

from openglider.lines.functions import proj_force
from openglider.lines.elements import Node
from openglider.mesh import Mesh
from openglider.vector.functions import norm, normalize
from openglider.utils.table import Table

logger = logging.getLogger(__name__)


[docs] class LineSet(object): """ Set of different lines """ calculate_sag = True knots_table = [ # lower_line_type, upper_line_type, upper_line_count, first_line_correction, last_line_correction ["liros.ltc65", "liros.ltc65", 2, 2.0, 2.0] ] def __init__(self, lines, v_inf=None): if v_inf is not None: v_inf = np.array(v_inf) self.v_inf = v_inf self.lines = lines or [] for line in lines: line.lineset = self self.mat = None self.glider = None def __repr__(self): return """ {} Lines: {} Length: {} """.format(super(LineSet, self).__repr__(), len(self.lines), self.total_length) @property def lowest_lines(self): return [line for line in self.lines if line.lower_node.type == 0] @property def uppermost_lines(self): return [line for line in self.lines if line.upper_node.type == 2] @property def nodes(self): nodes = set() for line in self.lines: nodes.add(line.upper_node) nodes.add(line.lower_node) return nodes
[docs] def scale(self, factor): for p in self.lower_attachment_points: p.vec = np.array([p.vec[0] * factor, p.vec[1], p.vec[2] * factor]) for line in self.lines: if "riser" not in line.name: if line.target_length: line.target_length *= factor if line.init_length: line.init_length *= factor line.force = None for node in self.nodes: if node.type == 2: # upper att-node node.force *= factor**2 self.recalc() return self
@property def attachment_points(self): return [n for n in self.nodes if n.type == 2] @property def lower_attachment_points(self): return [n for n in self.nodes if n.type == 0]
[docs] def get_main_attachment_point(self): main_attachment_point = None for ap in self.lower_attachment_points: if ap.name.upper() == "MAIN": main_attachment_point = ap if main_attachment_point is None: raise RuntimeError("No 'main' attachment point") return main_attachment_point
@property def floors(self): """ floors: number of line-levels """ def recursive_count_floors(node): if node.type == 2: return 0 lines = self.get_upper_connected_lines(node) nodes = [line.upper_node for line in lines] depths = [recursive_count_floors(node) for node in nodes] return max(depths) + 1 return {n: recursive_count_floors(n) for n in self.lower_attachment_points}
[docs] def get_lines_by_floor( self, target_floor: int = 0, node: Node = None, en_style=True ): """ starting from node: walk up "target_floor" floors and return all the lines. when en_style is True the uppermost lines are added in case there is no such floor (see EN 926.1 for details) """ node = node or self.get_main_attachment_point() def recursive_level(node: Node, current_level: int): lines = self.get_upper_connected_lines(node) nodes = [line.upper_node for line in lines] if not lines and en_style: return self.get_lower_connected_lines(node) elif current_level == target_floor: return lines else: line_list = [] for line in lines: line_list += recursive_level(line.upper_node, current_level + 1) return line_list return recursive_level(node, 0)
[docs] def get_floor_strength(self, node: Node = None): strength_list = [] node = node or self.get_main_attachment_point() for i in range(self.floors[node]): lines = self.get_lines_by_floor(i, node, en_style=True) strength = 0 for line in lines: if line.type.min_break_load is None: logger.warning(f"no min_break_load set for {line.type.name}") else: strength += line.type.min_break_load strength_list.append(strength) return strength_list
[docs] def get_mesh(self, numpoints=10, main_lines_only=False): if main_lines_only: lines = self.get_upper_lines(self.get_main_attachment_point()) else: lines = self.lines return sum([line.get_mesh(numpoints) for line in lines], Mesh())
[docs] def get_upper_line_mesh(self, numpoints=1, breaks=False): mesh = Mesh() for line in self.uppermost_lines: if not breaks: # TODO: is there a better solution??? if "BR" in line.upper_node.name: continue mesh += line.get_mesh(numpoints) return mesh
[docs] def recalc(self, calculate_sag=True, iterations=1): """ Recalculate Lineset Geometry. if LineSet.calculate_sag = True, drag induced sag will be calculated :return: self """ # for att in self.lower_attachment_points: # for line in self.get_upper_connected_lines(att): # for node in self.get_upper_influence_nodes(line): # node.get_position() # TODO: recalc always for reproducibility if iterations > 1: for line in self.lines: line.force = None for point in self.attachment_points: point.get_position() self.calculate_sag = calculate_sag for i in range(iterations): self._calc_geo() if self.calculate_sag: self._calc_sag() else: self.calc_forces(self.lowest_lines) for line in self.lines: line.sag_par_1 = line.sag_par_2 = None return self
def _calc_geo(self, start=None): if start is None: start = self.lowest_lines for line in start: logger.debug(f"upper line: {line.number}") if line.upper_node.type == 1: # no gallery line lower_point = line.lower_node.vec tangential = self.get_tangential_comp(line, lower_point) line.upper_node.vec = lower_point + tangential * line.init_length self._calc_geo(self.get_upper_connected_lines(line.upper_node)) def _calc_sag(self, start=None): if start is None: start = self.lowest_lines # 0 every line calculates its parameters self.mat = SagMatrix(len(self.lines)) # calculate projections for n in self.nodes: n.calc_proj_vec(self.v_inf) self.calc_forces(start) for line in start: self._calc_matrix_entries(line) self.mat.solve_system() for l in self.lines: l.sag_par_1, l.sag_par_2 = self.mat.get_sag_parameters(l.number) # -----CALCULATE SAG-----# def _calc_matrix_entries(self, line): up = self.get_upper_connected_lines(line.upper_node) if line.lower_node.type == 0: self.mat.insert_type_0_lower(line) else: lo = self.get_lower_connected_lines(line.lower_node) self.mat.insert_type_1_lower(line, lo[0]) if line.upper_node.type == 1: self.mat.insert_type_1_upper(line, up) else: self.mat.insert_type_2_upper(line) for u in up: self._calc_matrix_entries(u)
[docs] def calc_forces(self, start_lines): for line_lower in start_lines: upper_node = line_lower.upper_node vec = line_lower.diff_vector if line_lower.upper_node.type != 2: # not a gallery line # recursive force-calculation # setting the force from top to down lines_upper = self.get_upper_connected_lines(upper_node) self.calc_forces(lines_upper) force = np.zeros(3) for line in lines_upper: if line.force is None: logger.warning(f"error line force not set: {line}") else: force += line.force * line.diff_vector # vec = line_lower.upper_node.vec - line_lower.lower_node.vec line_lower.force = norm(np.dot(force, normalize(vec))) else: force = line_lower.upper_node.force force_projected = proj_force(force, normalize(vec)) if force_projected is None: line_lower.force = 10 else: line_lower.force = norm(force_projected)
[docs] def get_upper_connected_lines(self, node): return [line for line in self.lines if line.lower_node is node]
[docs] def get_upper_lines(self, node): """ recursive upper lines for node :param node: :return: """ lines = self.get_upper_connected_lines(node) for line in lines[:]: # copy to not mess up the loop lines += self.get_upper_lines(line.upper_node) return lines
[docs] def get_lower_connected_lines(self, node): return [line for line in self.lines if line.upper_node is node]
[docs] def get_connected_lines(self, node): return self.get_upper_connected_lines(node) + self.get_lower_connected_lines( node )
[docs] def get_drag(self): """ Get Total drag of the lineset :return: Center of Pressure, Drag (1/2*cw*A*v^2) """ drag_total = 0.0 center = np.array([0.0, 0.0, 0.0]) for line in self.lines: drag_total += line.drag_total center += line.get_line_point(0.5) * line.drag_total center /= drag_total return center, drag_total
[docs] def get_weight(self): weight = 0 for line in self.lines: weight += line.get_weight() return weight
[docs] def get_normalized_drag(self): """get the line drag normalized by the velocity ** 2 / 2""" return self.get_drag()[1] / norm(self.v_inf) ** 2 * 2
# -----CALCULATE GEO-----#
[docs] def get_tangential_comp(self, line, pos_vec): # upper_lines = self.get_upper_connected_lines(line.upper_node) # first we try to use already computed forces # and shift the upper node by residual force # we have to make sure to not overcompensate the residual force if line.has_geo and line.force is not None: r = self.get_residual_force(line.upper_node) s = line.get_correction_influence(r) for con_line in self.get_connected_lines(line.upper_node): s += con_line.get_correction_influence(r) # the additional factor is needed for stability. A better approach would be to # compute the compensation factor s with a system of linear equation. The movement # of the upper node has impact on the compensation of residual force # of the lower node (and the other way). return normalize(line.diff_vector + r / s * 0.5) # if norm(r) == 0: # return line.diff_vector # z = r / np.linalg.norm(r) # v0 = line.upper_node.vec - line.lower_node.vec # s = norm(r) * (norm(v0) / line.force - v0.dot(z)) # return normalize(v0 + s * z * 0.1) else: # if there are no computed forces available, use all the uppermost forces to compute # the direction of the line tangent = np.array([0.0, 0.0, 0.0]) upper_node = self.get_upper_influence_nodes(line) for node in upper_node: tangent += node.calc_force_infl(pos_vec) return normalize(tangent)
[docs] def get_upper_influence_nodes(self, line=None, node=None): """ get the points that have influence on the line and are connected to the wing """ if line is not None: node = line.upper_node if node is None: raise ValueError("Must either provide a node or line") if node.type == 2: return [node] else: upper_lines = self.get_upper_connected_lines(node) result = [] for upper_line in upper_lines: result += self.get_upper_influence_nodes(line=upper_line) return result
[docs] def iterate_target_length(self, steps=10, pre_load=50): """ iterative method to satisfy the target length """ self.recalc() for i in range(steps): for l in self.lines: if l.target_length is not None: diff = l.get_stretched_length(pre_load) - l.target_length l.init_length -= diff # l.init_length = l.target_length * l.init_length / l.get_stretched_length(pre_load) self.recalc()
def _set_line_indices(self): for i, line in enumerate(self.lines): line.number = i @property def total_length(self): length = 0 for line in self.lines: length += line.get_stretched_length() return length
[docs] def sort_lines(self, lines=None): if lines is None: lines = self.lines lines_new = lines[:] def sort_key(line): nodes = self.get_upper_influence_nodes(line) val_x = 0 val_rib_pos = 0 for node in nodes: if hasattr(node, "rib_pos"): val_rib_pos += node.rib_pos else: val_rib_pos += 1000 * node.vec[0] val_x += node.vec[1] return (10 * val_rib_pos + val_x) / len(nodes) lines_new.sort(key=sort_key) return lines_new
[docs] def create_tree(self, start_node=None): """ Create a tree of lines :return: [(line, [(upper_line1, []),...]),(...)] """ if start_node is None: start_node = self.lower_attachment_points lines = [] for node in start_node: lines += self.get_upper_connected_lines(node) else: lines = self.get_upper_connected_lines(start_node) return [ (line, self.create_tree(line.upper_node)) for line in self.sort_lines(lines) ]
def _get_lines_table(self, callback, start_node=None): line_tree = self.create_tree(start_node=start_node) table = Table() floors = max(self.floors.values()) columns_per_line = len(callback(line_tree[0][0])) def insert_block(line, upper, row, column): values = callback(line) column_0 = column - columns_per_line for index, value in enumerate(values): table[row, column_0 + index] = value if upper: for line, line_upper in upper: row = insert_block(line, line_upper, row, column - columns_per_line) else: # Insert a top node name = line.upper_node.name if not name: name = "XXX" table.set_value(column_0 - 1, row, name) # table.set_value(column+2+floors, row, name) row += 1 return row row = 1 for line, upper in line_tree: row = insert_block(line, upper, row, floors * (columns_per_line) + 2) return table node_group_rex = re.compile(r"[^A-Za-z]*([A-Za-z]*)[^A-Za-z]*")
[docs] def rename_lines(self): floors = max(self.floors.values()) upper_nodes = [] for node in self.attachment_points: upper_nodes += self.get_upper_influence_nodes(node=node) lines = [] for node in upper_nodes: lines += self.get_lower_connected_lines(node) for floor in range(floors): lines_grouped = {} for line in lines: line_groups = set() for node in self.get_upper_influence_nodes(line): node_group = self.node_group_rex.match(node.name) if node_group: line_groups.add(node_group.group(1)) line_groups_list = list(line_groups) line_groups_list.sort() line_group_name = "".join(line_groups_list) lines_grouped.setdefault(line_group_name, []) lines_grouped[line_group_name].append(line) for name, group in lines_grouped.items(): group_sorted = self.sort_lines(group) for i, line in enumerate(group_sorted): if floor > 0: line.name = f"{floor}_{name}{i+1}" else: line.name = f"{name}{i+1}" lines_new = set() for line in lines: for lower_line in self.get_lower_connected_lines(line.lower_node): lines_new.add(lower_line) lines = list(lines_new) return self
[docs] def get_line_length(self, line): length = line.get_stretched_length() # seam correction length += line.type.seam_correction # loop correction lower_lines = self.get_lower_connected_lines(line.lower_node) if len(lower_lines) == 0: return length lower_line = lower_lines[0] # Todo: Reinforce upper_lines = self.sort_lines(self.get_upper_connected_lines(line.lower_node)) index = upper_lines.index(line) total_lines = len(upper_lines) for data in self.knots_table: name1 = data[0] name2 = data[1] count = data[2] min_value = data[3] max_value = data[4] if ( name1 == lower_line.type.name and name2 == line.type.name and count == total_lines ): shortening = min_value + index * (max_value - min_value) / ( total_lines - 1 ) length -= data[3] + (data[4] - shortening) return length logger.warning( f"no shortening values for: {lower_line.type.name} / {line.type.name} ({total_lines})" ) return length
[docs] def get_table(self): length_table = self._get_lines_table( lambda line: [round(self.get_line_length(line) * 1000)] ) names_table = self._get_lines_table( lambda line: [line.name, line.type.name, line.color] ) def get_checklength(line, upper_lines): line_length = line.get_stretched_length() if not len(upper_lines): return [line_length] else: lengths = [] for upper in upper_lines: lengths += get_checklength(*upper) return [length + line_length for length in lengths] checklength_values = [] for line, upper_line in self.create_tree(): checklength_values += get_checklength(line, upper_line) checklength_table = Table() for index, length in enumerate(checklength_values): checklength_table[index + 1, 0] = round(1000 * length) length_table.append_right(checklength_table) length_table.append_right(names_table) return length_table
[docs] def get_force_table(self): def get_line_force(line): percentage = "" if line.type.min_break_load: percentage = "{}%".format( round(100 * line.force / line.type.min_break_load, 1) ) return [line.type.name, line.force, percentage] return self._get_lines_table(get_line_force)
[docs] def get_table_2(self): return self._get_lines_table( lambda line: [ line.name, line.type.name, round(line.get_stretched_length() * 1000), ] )
[docs] def get_table_sorted_lengths(self): table = Table() table[0, 0] = "Name" table[0, 0] = "Length [mm]" lines = list(self.lines) lines.sort(key=lambda line: line.name) for i, line in enumerate(lines): table[i + 1, 0] = line.name table[i + 1, 1] = line.type.name table[i + 1, 2] = round(line.get_stretched_length() * 1000) return table
[docs] def get_upper_connected_force(self, node): """ get the sum of the forces of all upper-connected lines """ force = np.array([0.0, 0.0, 0.0]) for line in self.get_upper_connected_lines(node): force += line.force * line.diff_vector return force
[docs] def get_residual_force(self, node): """ compute the residual force in a node to due simplified computation of lines """ residual_force = np.zeros(3) upper_lines = self.get_upper_connected_lines(node) lower_lines = self.get_lower_connected_lines(node) for line in upper_lines: residual_force += line.force * line.diff_vector for line in lower_lines: residual_force -= line.force * line.diff_vector return residual_force
[docs] def copy(self): return copy.deepcopy(self)
def __json__(self): new = self.copy() nodes = list(new.nodes) for line in new.lines: line.upper_node = nodes.index(line.upper_node) line.lower_node = nodes.index(line.lower_node) return {"lines": new.lines, "nodes": nodes, "v_inf": self.v_inf.tolist()} @classmethod def __from_json__(cls, lines, nodes, v_inf): for line in lines: if isinstance(line.upper_node, int): line.upper_node = nodes[line.upper_node] if isinstance(line.lower_node, int): line.lower_node = nodes[line.lower_node] obj = cls(lines, v_inf) for line in obj.lines: line.lineset = obj return obj def __getitem__(self, name): if isinstance(name, list): return [self[n] for n in name] for line in self.lines: if name == line.name: return line raise KeyError(name)