Instantly share code, notes, and snippets.
Created
December 7, 2023 13:30
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save fredvol/3a6267854bce52acd9ea9de98f05d297 to your computer and use it in GitHub Desktop.
model for aerosandbox
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# MODEL FILE FOR QUICK 3D POLAR CALCULATION | |
# 17/07/2023 - frd | |
# %% | |
import json | |
import os | |
import re | |
from typing import List, Union | |
import aerosandbox as asb | |
import aerosandbox.numpy as np | |
import pandas as pd | |
from matplotlib import pyplot as plt | |
from scipy.linalg import expm, norm | |
from copy import deepcopy | |
from skspatial.objects import Line | |
import plotly.graph_objects as go | |
import plotly.express as px | |
# %% | |
glider_model_path = os.path.join( | |
"ref_files", "Enzo3_M_production_weightCost_state.json" | |
) | |
# %% FUNCTIONS | |
def distance_between_2pts(point1, point2): | |
# Convert input points to NumPy arrays for vector operations | |
point1 = np.array(point1) | |
point2 = np.array(point2) | |
# Calculate the difference between the two points | |
difference = point2 - point1 | |
# Calculate the Euclidean distance | |
distance = np.linalg.norm(difference) | |
return distance | |
def convert_float_list(mstr): | |
""" | |
Convert a comma-separated string of numbers to a list of floats. | |
Used to convert a list of coord of 3D point to a python list. | |
Parameters: | |
mstr (str): A string containing comma-separated numerical values. | |
Returns: | |
list of float: A list containing the numerical values as floats. | |
Example: | |
>>> convert_float_list("1.5, 2.3, 3.7") | |
[1.5, 2.3, 3.7] | |
Note: | |
- If the input `mstr` is not a string, the function will raise a TypeError. | |
- The function does not perform extensive error-checking on the input string. | |
- The input string must contain numerical values separated by commas, with no spaces. | |
- Any invalid or improperly formatted values may result in unexpected behavior. | |
""" | |
if type(mstr) is str: | |
return [float(i) for i in mstr.split(",")] | |
def unit_vector(vector): | |
"""Returns the unit vector of the vector.""" | |
return vector / np.linalg.norm(vector) | |
def vector_between2pts(point1, point2, unitize=True): | |
# Convert the input points to NumPy arrays | |
point1 = np.array(point1) | |
point2 = np.array(point2) | |
# Calculate the vector between the two points | |
vector = point2 - point1 | |
if unitize: | |
vector = unit_vector(vector) | |
return vector | |
def translate_point(point, vector): | |
# Convert the input point and vector to NumPy arrays | |
point = np.array(point) | |
vector = np.array(vector) | |
# Translate the point by adding the vector | |
translated_point = point + vector | |
return translated_point | |
def convert_mm_to_m(my_list): | |
return [i / 1000 for i in my_list] | |
def find_point0_TE(original_point, angle_degrees, distance): | |
"""0nly valid for root chord, TODO use the referenciel of each Xsecs | |
Args: | |
original_point (List): _description_ | |
angle_degrees (float): _description_ | |
distance (float): _description_ | |
Returns: | |
List of float : coorddinate | |
""" | |
# Convert input point to a NumPy array | |
original_point = np.array(original_point) | |
# Convert angle from degrees to radians | |
angle_radians = np.radians(angle_degrees) | |
# Calculate the new point's coordinates | |
new_x = original_point[0] + distance * np.cos(angle_radians) | |
new_z = original_point[1] + distance * np.sin(angle_radians) | |
# Keep the original Z coordinate | |
new_y = original_point[2] | |
return [new_x, new_y, new_z] | |
def angle_between(v1, v2=(0, 1, 0)): | |
"""Returns the angle in radians between vectors 'v1' and 'v2':: | |
v2, default is Y axis | |
>>> angle_between((1, 0, 0), (0, 1, 0)) | |
1.5707963267948966 | |
>>> angle_between((1, 0, 0), (1, 0, 0)) | |
0.0 | |
>>> angle_between((1, 0, 0), (-1, 0, 0)) | |
3.141592653589793 | |
""" | |
v1_u = unit_vector(v1) | |
v2_u = unit_vector(v2) | |
return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)) | |
def rotate_pt(orign_pt, theta_deg, axis=[0, -1, 0]): | |
""" | |
Rotate a 3D point around an arbitrary axis. | |
The function rotates a 3D point `origin_pt` by an angle `theta_deg` (in degrees) | |
around an arbitrary axis defined by `axis`. | |
Parameters: | |
origin_pt (list or numpy.array): A list or numpy array representing the 3D point to rotate | |
in the format [x, y, z]. | |
theta_deg (float): The angle of rotation in degrees. | |
axis (list or numpy.array, optional): A list or numpy array representing the axis of rotation. | |
The default axis is Y , [0, -1, 0]. | |
Returns: | |
numpy.array: A numpy array representing the rotated 3D point. | |
Example: | |
>>> rotate_pt(point=[1.0, 0.0, 0.0] , angle= 90,axis=[0, 0, 1] ) | |
[0 , 1 , 0] | |
Note: | |
- The `axis` should be a unit vector; otherwise, the function will normalize it. | |
- The function uses the Rodrigues' rotation formula to perform the rotation. | |
""" | |
theta = np.radians(theta_deg) | |
M0 = expm(np.cross(np.eye(3), axis / norm(axis) * theta)) | |
return np.dot(M0, orign_pt) | |
def convert_sentence_to_dict(sentence, multiplicator_factor=1): | |
""" | |
Convert a sentence with key-value pairs into a Python dictionary ( generated chatGPT). | |
The function takes a sentence containing key-value pairs and converts it into a Python | |
dictionary where keys are strings and values are lists of three floating-point numbers. | |
The sentence must have the following format for each key-value pair: | |
'key:[x, y, z]', where 'key' is a string, and 'x', 'y', 'z' are floating-point numbers. | |
Parameters: | |
sentence (str): A sentence containing key-value pairs in the specified format. | |
multiplicator_factor (float) : value to scale de data from mm to m. | |
Returns: | |
dict: A Python dictionary where keys are strings and values are lists of three floating-point numbers. | |
Note: | |
- The function assumes the provided sentence follows the specified format for each key-value pair. | |
- The function does not perform extensive error-checking on the input sentence. | |
- Any invalid or improperly formatted key-value pairs may result in unexpected behavior. | |
""" | |
# Regular expression pattern to match key-value pairs | |
pattern = r"(\w+):\[(\-*\d+\.\d+),(\-*\d+\.\d+),(\-*\d+\.\d+)\]" | |
# Find all key-value pairs in the sentence | |
matches = re.findall(pattern, sentence) | |
# Create the dictionary | |
result_dict = { | |
key: [ | |
float(x) * multiplicator_factor, | |
float(y) * multiplicator_factor, | |
float(z) * multiplicator_factor, | |
] | |
for key, x, y, z in matches | |
} | |
return result_dict | |
def extract_profile(s): | |
"""convert profile coord as list space seaprated to numpy N2x array. | |
the profile is scaled to 0,1 coord. | |
Args: | |
s (str): string input | |
Returns: | |
np.ndarray: Scaled profile coords | |
Example: | |
>>> extract_profile("2068.0,0.0 2034.9,5.7 1971.6,16.7 1895.3,29.9 1813.0,44.1") | |
array([[1. , 0. ], | |
[0.9839942 , 0.00275629], | |
[0.95338491, 0.00807544], | |
[0.91648936, 0.01445841], | |
[0.87669246, 0.02132495]]) | |
""" | |
x = [] | |
y = [] | |
for ligne in s.split(" "): | |
xi, yi = ligne.split(",") | |
x.append(float(xi)) | |
y.append(float(yi)) | |
ratio_scale = 1 / max(x) | |
xs = np.array(x) * ratio_scale | |
ys = np.array(y) * ratio_scale | |
return np.array(list(zip(xs, ys))) | |
def weighted_barycentric_average(points, weights): | |
""" | |
Calculate the weighted barycentric average of a list of 3D points. ( generated chatGPT) | |
The function computes the weighted barycentric average of the given 3D points | |
using the provided weights. | |
Parameters: | |
points (list of lists): A list containing 3D points represented as lists of three | |
floating-point numbers [x, y, z]. | |
weights (list of float): A list of weights corresponding to each point in the `points` list. | |
Returns: | |
list: A list containing the x, y, and z coordinates of the weighted barycentric average. | |
Raises: | |
ValueError: If the number of points and weights are not the same | |
Example: | |
>>> points = [ | |
... [83.4, 320.1, -7871.3], | |
... [83.4, 320.1, -7871.3], | |
... [83.4, 320.1, -7871.3], | |
... [627.8, 5017.5, -3097.9], | |
... [627.8, 5017.5, -3097.9], | |
... ] | |
>>> weights = [0.009547, 0.006893, 0.009810, 0.000106, 0.000109] | |
>>> result = weighted_barycentric_average(points, weights) | |
>>> print(result) | |
[83.40001397960151, 320.1000336941283, -7871.299988324128] | |
Note: | |
The function assumes that the number of points and weights is the same, | |
""" | |
if len(points) != len(weights): | |
raise ValueError("Number of points and weights must be the same.") | |
weighted_sum = [0.0, 0.0, 0.0] | |
total_weight = 0.0 | |
for i, point in enumerate(points): | |
weight = weights[i] | |
total_weight += weight | |
for j in range(3): | |
weighted_sum[j] += point[j] * weight | |
barycentric_average = [coord_sum / total_weight for coord_sum in weighted_sum] | |
return barycentric_average | |
class ParagliderData: | |
def __init__(self, glider_model_path): | |
self.source_path = glider_model_path | |
self.glider_mass = 5 # kg | |
with open(self.source_path) as f: | |
# data = json.load(f) # ,object_hook=Generic.from_dict) | |
# Parse JSON into an object with attributes corresponding to dict keys. | |
self.original_data = json.loads( | |
f.read().replace( | |
"/*Already listed*/", "" | |
) # , object_hook=lambda d: SimpleNamespace(**d) | |
) | |
self.compute_general() | |
self.compute_ribs() | |
self.compute_lines() | |
print(" end importing data from Json file") | |
def compute_general(self): | |
# % Import general | |
interesting_fields = [ | |
"Cells", | |
"FlatSpan", | |
"HarnessDragArea", | |
"Name", | |
"RiserLength", | |
"RiserWebs", | |
"RootChord", | |
"RootThickness", | |
"ShortName", | |
"Size", | |
"Span", | |
"VertDist", | |
"Area", | |
"HasCentreCell", | |
"HasCentreRib", | |
"AspectRatio", | |
"PilotLoad", | |
"PilotVerticalCentreOfGravity", | |
"HarnessDragArea", | |
] | |
general_data = dict( | |
(k, self.original_data[k]) | |
for k in interesting_fields | |
if k in self.original_data | |
) | |
general_data["RisersDict"] = convert_sentence_to_dict( | |
general_data["RiserWebs"], multiplicator_factor=0.001 | |
) | |
# convert to numeric | |
for c in ["PilotLoad", "HarnessDragArea"]: | |
general_data[c] = float(general_data[c]) | |
# convert mm to m | |
for c in [ | |
"FlatSpan", | |
"RiserLength", | |
"RootChord", | |
"Span", | |
"VertDist", | |
"PilotVerticalCentreOfGravity", | |
]: | |
general_data[c] = float(general_data[c]) / 1000 | |
self.general = general_data | |
print(" General data computed") | |
def compute_ribs(self): | |
if hasattr(self, "general"): | |
# % WORK ON RIBS | |
ribs = [ | |
item["Children"] | |
for item in self.original_data["Children"] | |
if "FoilRibs" in item["$Type"] | |
][0] | |
df_ribs_full = pd.json_normalize(ribs) | |
df_ribs = df_ribs_full[ | |
[ | |
"Name", | |
"ChordAngle", | |
"Chord", | |
"LEPoint", | |
"TEPoint", | |
"Profile2D", | |
"Tabs", | |
] | |
] | |
df_ribs.loc[:, "ChordAngle"] = pd.to_numeric(df_ribs["ChordAngle"]) | |
df_ribs.loc[:, "Chord"] = pd.to_numeric(df_ribs["Chord"]) | |
df_ribs.loc[:, "Chord"] /= 1000 # mm to m conversion | |
df_ribs.loc[:, "LEPoint"] = df_ribs["LEPoint"].apply(convert_float_list) | |
df_ribs.loc[:, "LEPoint"] = df_ribs["LEPoint"].apply(convert_mm_to_m) | |
df_ribs.loc[:, "TEPoint"] = df_ribs["TEPoint"].apply(convert_float_list) | |
df_ribs.loc[:, "TEPoint"] = df_ribs["TEPoint"].apply(convert_mm_to_m) | |
if self.general["HasCentreCell"]: | |
# add a virtual center rib | |
Rib0 = deepcopy(df_ribs.loc[0]) | |
le_point0 = list(Rib0["LEPoint"]) | |
le_point0[0] = 0 | |
le_point0[1] = 0 | |
te_point0 = list(Rib0["TEPoint"]) | |
te_point0[0] = Rib0["TEPoint"][0] | |
te_point0[1] = 0 | |
Rib0["TEPoint"] = te_point0 | |
df_ribs.loc[-1] = Rib0 | |
df_ribs.index = df_ribs.index + 1 | |
df_ribs = df_ribs.sort_index() | |
# create local vector | |
# TODO comapre to Grashopper | |
# % simplify tabs | |
def extract_tabs(tabs): | |
tabs_return = [] | |
if len(tabs) > 0: | |
for tab_dict in tabs: | |
if tab_dict["LoadBearing"]: | |
tabs_return.append( | |
dict( | |
(k, tab_dict[k]) | |
for k in ["Type", "PcntChord", "PositionBottom3D"] | |
if k in tab_dict | |
) | |
) | |
return tabs_return | |
df_ribs.loc[:, "Tabs"] = df_ribs["Tabs"].apply(extract_tabs) | |
self.ribs_df = df_ribs | |
print(" Lines data computed") | |
else: | |
print(" impossible general data not computed") | |
def compute_lines(self): | |
# % WORK ON Lines | |
self.line_splice_ratio = 0.5 | |
lines = [ | |
item["Children"] | |
for item in self.original_data["Children"] | |
if "SuspensionLines" in item["$Type"] | |
][0] | |
df_lines_full = pd.json_normalize(lines) | |
df_lines_na_include = df_lines_full[ | |
[ | |
"Name", | |
"Diameter", | |
"AdjustedLength", | |
"Material", | |
"Riser", | |
"@Point0", | |
"@Point1", | |
] | |
].copy() # here the SettingWithCopyWarning could be triger | |
df_lines = df_lines_na_include.dropna() | |
df_lines.loc[:, "Diameter"] = pd.to_numeric(df_lines["Diameter"]) | |
df_lines.loc[:, "AdjustedLength"] = pd.to_numeric(df_lines["AdjustedLength"]) | |
df_lines.loc[:, "AdjustedLength"] /= 1000 | |
df_lines.loc[:, "SpliceLength"] = 0.100 * 2 | |
df_lines.loc[:, "cx"] = 1 | |
df_lines.loc[:, "Scx"] = ( | |
df_lines["Diameter"] / 1000 * df_lines["cx"] * df_lines["AdjustedLength"] | |
) | |
df_lines.loc[:, "Scx_splice"] = ( | |
df_lines["Diameter"] | |
/ 1000 | |
* df_lines["cx"] | |
* self.line_splice_ratio | |
* df_lines["SpliceLength"] | |
) | |
df_lines.loc[:, "Scx_tot"] = df_lines["Scx_splice"] + df_lines["Scx"] | |
total_line_drag = df_lines["Scx_tot"].sum() * 2 | |
# compute barycenter | |
df_lines.loc[:, "@Point0"] = df_lines["@Point0"].apply(convert_float_list) | |
df_lines.loc[:, "@Point1"] = df_lines["@Point1"].apply(convert_float_list) | |
df_lines.loc[:, "@Point0"] = df_lines["@Point0"].apply(convert_mm_to_m) | |
df_lines.loc[:, "@Point1"] = df_lines["@Point1"].apply(convert_mm_to_m) | |
line_application_point_one_side = weighted_barycentric_average( | |
list(df_lines["@Point0"]), list(df_lines["Scx_tot"]) | |
) | |
line_application_point = line_application_point_one_side | |
line_application_point[1] = 0 # remove y component | |
self.lines_scx = total_line_drag | |
self.lines_line_application_point = line_application_point | |
self.lines_df = df_lines | |
def generate_wing(self): | |
# % Create Wing | |
mainwing_xsecs = [] | |
for i, row in self.ribs_df.iterrows(): | |
xsec = asb.WingXSec( | |
xyz_le=row["LEPoint"], | |
chord=row["Chord"], | |
twist=row["ChordAngle"], | |
airfoil=asb.Airfoil( | |
row["Name"], coordinates=extract_profile(row["Profile2D"]) | |
), | |
) | |
mainwing_xsecs.append(xsec) | |
mainwing = asb.Wing( | |
name="Main", | |
xsecs=mainwing_xsecs, | |
symmetric=True, | |
is_main_wing=True, | |
is_horizontal_stabilizer=False, | |
is_vertical_stabilizer=False, | |
is_fuselage=False, | |
) | |
return mainwing | |
# %% | |
t = ParagliderData(glider_model_path) | |
# %% | |
class Paraglider(asb.Airplane): | |
def __init__(self, glider_model_path, derotate=True): | |
# PArse json file | |
self.data = ParagliderData(glider_model_path) | |
# Call the constructor of the base class 'aerosandbox.Airplane' | |
super().__init__( | |
name=self.data.general["ShortName"], | |
xyz_ref=[0, 0, 0], | |
wings=[self.data.generate_wing()], | |
) | |
self.data.general["original_angle"] = self.main_ChordAngle() | |
self.data.general["hasbeenDerotated"] = False | |
if derotate and self.main_ChordAngle() != 0: | |
self.derotate_all() | |
self._compute_main_pts() | |
# self.data.[elt.twist for elt in glider.wings[0].xsecs] | |
self._store_twist_origin() | |
def draw_skeleton(self): | |
df = self.data.ribs_df | |
df[["X_LE", "Y_LE", "Z_LE"]] = pd.DataFrame( | |
df["LEPoint"].tolist(), index=df.index | |
) | |
df[["X_TE", "Y_TE", "Z_TE"]] = pd.DataFrame( | |
df["TEPoint"].tolist(), index=df.index | |
) | |
fig = go.Figure() | |
fig.add_trace( | |
go.Scatter3d( | |
x=df["X_LE"], | |
y=df["Y_LE"], | |
z=df["Z_LE"], | |
mode="markers", | |
marker=dict(color="blue", size=8), | |
name="LEPoint", | |
) | |
) | |
fig.add_trace( | |
go.Scatter3d( | |
x=df["X_TE"], | |
y=df["Y_TE"], | |
z=df["Z_TE"], | |
mode="markers", | |
marker=dict(color="red", size=8), | |
name="TEPoint", | |
) | |
) | |
# Draw lines connecting LEPoint to TEPoint | |
for i in range(len(df)): | |
fig.add_trace( | |
go.Scatter3d( | |
x=[df["X_LE"][i], df["X_TE"][i]], | |
y=[df["Y_LE"][i], df["Y_TE"][i]], | |
z=[df["Z_LE"][i], df["Z_TE"][i]], | |
mode="lines", | |
line=dict(color="black", width=2), | |
showlegend=False, | |
) | |
) | |
# Setting same scale for all three axes | |
fig.update_layout( | |
scene=dict(aspectmode="cube", aspectratio=dict(x=1, y=1, z=1)) | |
) | |
fig.show() | |
def draw_full(self, plot_lines=True): | |
plotly_fig = self.draw(backend="plotly") | |
# add line | |
if plot_lines: | |
df_line = self.data.lines_df | |
for i, row in df_line.iterrows(): | |
plotly_fig.add_trace( | |
go.Scatter3d( | |
x=[row["@Point0"][0], row["@Point1"][0]], | |
y=[row["@Point0"][1], row["@Point1"][1]], | |
z=[row["@Point0"][2], row["@Point1"][2]], | |
mode="lines", | |
name="lines", | |
) | |
) | |
# ref line_application pt | |
plotly_fig.add_trace( | |
go.Scatter3d( | |
x=[self.data.lines_line_application_point[0]], | |
y=[self.data.lines_line_application_point[1]], | |
z=[self.data.lines_line_application_point[2]], | |
mode="markers", | |
marker=dict( | |
size=6, | |
), | |
) | |
) | |
# add pilot | |
plotly_fig.add_trace( | |
go.Scatter3d( | |
x=[self.data.pilot_pt[0]], | |
y=[self.data.pilot_pt[1]], | |
z=[self.data.pilot_pt[2]], | |
mode="markers", | |
marker=dict( | |
size=6, | |
), | |
) | |
) | |
plotly_fig.show() | |
def get_twist(self): | |
return [elt.twist for elt in self.wings[0].xsecs] | |
def _store_twist_origin(self): | |
self.data.origin_twist = self.get_twist() | |
def reset_twist(self): | |
self.modify_twist_airplane(self.data.origin_twist) | |
def modify_twist_airplane(self, twists: list): | |
""" | |
modify the twist without moving the LE point , just the angle | |
Args: | |
twists (list): The list of new twists in degger , abosolute to x axis. | |
Returns: | |
modify in place | |
""" | |
assert len(self.wings[0].xsecs) == len( | |
twists | |
), f"twists {len(twists)} isn't the same size as the number of sections of the airplane {len(self.wings[0].xsecs)}" | |
for i in range(len(twists)): | |
self.wings[0].xsecs[i].twist = twists[i] | |
def _compute_quasi_vertical_vect(self): | |
pt_center_aero = self.wings[0].aerodynamic_center() | |
pt_B = self.data.general["RisersDict"]["B"] | |
pt_B[1] = 0 | |
return vector_between2pts(pt_center_aero, pt_B, unitize=True) | |
def _compute_main_pts(self): | |
""" | |
Compute key points for the glider: Carabiner point, the Pilot point, and Center of Gravity (CG) point. | |
The following input are coming from Ozcad source file: | |
- RiserLength [m] | |
- PilotVerticalCentreOfGravity [m] | |
- HarnessDragArea [m2] | |
- PilotLoad [kg] | |
The following input are hard coded: glider_mass ( from ParagliderData.__init__ ) | |
Note: | |
- pt_B : center point between the Top B risers pt ( with y=0) | |
- quasi_vertical_vect : vector from the aerodynamic_center of the wing to pt_B | |
- Carabiner Point (carabiner_pt): The middle point of the main harness carabiner. extending quasi_vertical_vect from pt_B by the risers length | |
- Pilot Point (pilot_pt): the pilot's cg. Extending quasi_vertical_vect from carabiner_pt by the PilotVerticalCentreOfGravity | |
- Center of Gravity (cg): the combined center of gravity of the glider and the pilot. | |
""" | |
quasi_vertical_vect = self._compute_quasi_vertical_vect() | |
# carabiner pt | |
translate_vect_top_bottom_riser = ( | |
quasi_vertical_vect * self.data.general["RiserLength"] | |
) | |
pt_B = self.data.general["RisersDict"]["B"] | |
pt_B[1] = 0 | |
self.data.carabiner_pt = translate_point(pt_B, translate_vect_top_bottom_riser) | |
# Pilot pt | |
translate_vect_carabiner_pilot = ( | |
quasi_vertical_vect * self.data.general["PilotVerticalCentreOfGravity"] | |
) | |
pilot_pt = translate_point( | |
self.data.carabiner_pt, translate_vect_carabiner_pilot | |
) | |
self.data.pilot_pt = pilot_pt | |
self.data.pilot_scx = self.data.general["HarnessDragArea"] | |
# cg pt | |
pt_center_aero = self.wings[0].aerodynamic_center() | |
pilot_mass_kg = self.data.general["PilotLoad"] | |
cg_pt = weighted_barycentric_average( | |
[pt_center_aero, self.data.pilot_pt], [self.data.glider_mass, pilot_mass_kg] | |
) | |
self.data.cg = cg_pt | |
def compute_triming(self): | |
cg_pt = np.array(self.data.cg) | |
LE_pt = self.wings[0].xsecs[0].xyz_le | |
twist0 = -self.wings[0].xsecs[0].twist | |
chord0 = self.wings[0].xsecs[0].chord | |
TE_pt = find_point0_TE(LE_pt, twist0, chord0) | |
Chord_line = Line.from_points(LE_pt, TE_pt) | |
projected_point = Chord_line.project_point(cg_pt) | |
ratio = distance_between_2pts(LE_pt, projected_point) / chord0 | |
if projected_point[0] < LE_pt[0]: | |
ratio = -ratio | |
return ratio | |
def main_ChordAngle(self): | |
return self.data.ribs_df["ChordAngle"][0] | |
def derotate_all(self): | |
if not self.data.general["hasbeenDerotated"]: | |
self._derotate_wing() | |
self._derotate_lines() | |
self._compute_main_pts() # TODO group in one derotate | |
self.data.general["hasbeenDerotated"] = True | |
else: | |
print("Impossible : Glider already derotated") | |
def _derotate_lines(self): | |
# derotate the appplication point | |
angle_derotate = self.data.general["original_angle"] | |
print(f"The aplication lines point will be derotate by {angle_derotate} deg ") | |
self.data.lines_line_application_point = rotate_pt( | |
self.data.lines_line_application_point, angle_derotate | |
) | |
# derotate_each lines | |
self.data.lines_df["@Point0"] = self.data.lines_df["@Point0"].apply( | |
rotate_pt, theta_deg=angle_derotate | |
) | |
self.data.lines_df["@Point1"] = self.data.lines_df["@Point1"].apply( | |
rotate_pt, theta_deg=angle_derotate | |
) | |
# derotate riser webs | |
for riser, riser_pt in self.data.general["RisersDict"].items(): | |
self.data.general["RisersDict"][riser] = rotate_pt(riser_pt, angle_derotate) | |
def _derotate_wing(self, angle_target=0): | |
"""Rotate the glider around y axis to bring the main chord to colinear with vector x. | |
Positive , is pitch down | |
Args: | |
angle_derotate (flaot): angle in degrees | |
""" | |
assert angle_target == 0, "angle_target != 0 not implemented" | |
angle_derotate = angle_target - self.data.general["original_angle"] | |
print(f"The glider will be derotate by {angle_derotate} deg ") | |
self.data.ribs_df["LEPoint_derot"] = self.data.ribs_df["LEPoint"].apply( | |
rotate_pt, theta_deg=-angle_derotate | |
) | |
# df_yaxis = pd.DataFrame( | |
# [ | |
# {"y_axis": self.wings[0]._compute_frame_of_WingXSec(i)[1]} | |
# for i in range(len(self.wings[0].xsecs)) | |
# ] | |
# ) | |
# self.data.ribs_df = pd.concat([self.data.ribs_df, df_yaxis], axis=1) | |
# self.data.ribs_df["y_angle_rad"] = self.data.ribs_df["y_axis"].apply( | |
# angle_between | |
# ) | |
# self.data.ribs_df["y_angle_deg"] = np.degrees(self.data.ribs_df["y_angle_rad"]) | |
# self.data.ribs_df["correction_twist"] = np.cos( | |
# self.data.ribs_df["y_angle_rad"] | |
# ) * np.radians(angle_derotate) | |
derotate_wing_xsecs = [] | |
for i, row in self.data.ribs_df.iterrows(): | |
xsec = asb.WingXSec( | |
xyz_le=row["LEPoint_derot"], | |
chord=row["Chord"], | |
twist=0, | |
airfoil=asb.Airfoil( | |
row["Name"], coordinates=extract_profile(row["Profile2D"]) | |
), | |
) | |
derotate_wing_xsecs.append(xsec) | |
derotate_wing = asb.Wing( | |
name="Main", | |
xsecs=derotate_wing_xsecs, | |
symmetric=True, | |
is_main_wing=True, | |
is_horizontal_stabilizer=False, | |
is_vertical_stabilizer=False, | |
is_fuselage=False, | |
) | |
self.wings[0] = derotate_wing | |
# %% TEMP PLOT | |
# def draw_skeleton(self): | |
# df = self.data.ribs_df | |
# # Extracting X, Y, and Z coordinates for LEPoint | |
# df[["X_LE", "Y_LE", "Z_LE"]] = pd.DataFrame(df["LEPoint"].tolist(), index=df.index) | |
# # Extracting X, Y, and Z coordinates for TEPoint | |
# df[["X_TE", "Y_TE", "Z_TE"]] = pd.DataFrame(df["TEPoint"].tolist(), index=df.index) | |
# # Plotting with isometric scaling | |
# fig = go.Figure() | |
# # Scatter plot for LEPoint | |
# fig.add_trace( | |
# go.Scatter3d( | |
# x=df["X_LE"], | |
# y=df["Y_LE"], | |
# z=df["Z_LE"], | |
# mode="markers", | |
# marker=dict(color="blue", size=8), | |
# name="LEPoint", | |
# ) | |
# ) | |
# # Scatter plot for TEPoint | |
# fig.add_trace( | |
# go.Scatter3d( | |
# x=df["X_TE"], | |
# y=df["Y_TE"], | |
# z=df["Z_TE"], | |
# mode="markers", | |
# marker=dict(color="red", size=8), | |
# name="TEPoint", | |
# ) | |
# ) | |
# # Draw lines connecting LEPoint to TEPoint | |
# for i in range(len(df)): | |
# fig.add_trace( | |
# go.Scatter3d( | |
# x=[df["X_LE"][i], df["X_TE"][i]], | |
# y=[df["Y_LE"][i], df["Y_TE"][i]], | |
# z=[df["Z_LE"][i], df["Z_TE"][i]], | |
# mode="lines", | |
# line=dict(color="black", width=2), | |
# showlegend=False, | |
# ) | |
# ) | |
# # Setting same scale for all three axes | |
# fig.update_layout(scene=dict(aspectmode="cube", aspectratio=dict(x=1, y=1, z=1))) | |
# fig.show() | |
# %% IMPORT GLIDERS | |
if __name__ == "__main__": | |
para = Paraglider(glider_model_path, derotate=False) | |
# para.draw_full() | |
# %% | |
foldername = "Enzo3" | |
tab_to_load = os.path.join(os.getcwd(), "xflr5_file", foldername, "tabs_Enzo3.xlsx") | |
speed_range = 100 # mm | |
df_tab = pd.read_excel(tab_to_load) | |
df_tab.interpolate(limit_direction="both", inplace=True) | |
tab_to_consider = ["A_average", "B"] | |
df_tab["distance_tab_pct"] = df_tab[tab_to_consider].diff(axis=1)[ | |
tab_to_consider[-1] | |
] | |
df_tab["distance_tab_mm"] = (df_tab["distance_tab_pct"] / 100) * df_tab["chord"] | |
df_tab["speed_travel"] = df_tab["system"] * speed_range | |
df_tab["angle_speed_move"] = np.degrees( | |
np.arctan2(df_tab["speed_travel"], df_tab["distance_tab_mm"]) | |
) | |
if para.data.general["HasCentreCell"]: | |
first_row = df_tab.iloc[[0]].copy() | |
df_tab = pd.concat([first_row, df_tab], ignore_index=True) | |
df_tab["angle_tot"] = df_tab["angle_speed_move"] - para.data.origin_twist | |
ax_speed = df_tab["angle_speed_move"].plot() | |
ax_speed.set_title("angle_change (deg)") | |
# % | |
para.modify_twist_airplane(df_tab["angle_tot"]) | |
# %% | |
vlm_TS = asb.VortexLatticeMethod( | |
chordwise_resolution=8, | |
spanwise_resolution=1, | |
airplane=para, | |
op_point=asb.OperatingPoint( | |
velocity=12, # m/s | |
alpha=3, # degree | |
), | |
) | |
vlm_enzo_TS_result = vlm_TS.run() | |
# %% | |
ctr_s = asb.ControlSurface( | |
"flapo", symmetric=True, deflection=10, hinge_point=0.76, trailing_edge=True | |
) | |
w = para.wings[0] | |
for i in range(10): | |
w.xsecs[i].control_surfaces.append(ctr_s) | |
para_d = para.with_control_deflections({"flapo": 20}) | |
# %% | |
vlm_TS_d = asb.VortexLatticeMethod( | |
chordwise_resolution=8, | |
spanwise_resolution=1, | |
airplane=para_d, | |
op_point=asb.OperatingPoint( | |
velocity=12, # m/s | |
alpha=3, # degree | |
), | |
) | |
vlm_enzo_TS_result_d = vlm_TS_d.run() | |
# %% | |
# cg_pt = np.array(para.data.cg) | |
# LE_pt = para.wings[0].xsecs[0].xyz_le | |
# twist0 = -para.wings[0].xsecs[0].twist | |
# chord0 = para.wings[0].xsecs[0].chord | |
# TE_pt = find_point0_TE(LE_pt, twist0, chord0) | |
# Chord_line = Line.from_points(LE_pt, TE_pt) | |
# projected_point = Chord_line.project_point(cg_pt) | |
# ratio = distance_between_2pts(LE_pt, projected_point) / chord0 | |
# if projected_point[0] < LE_pt[0]: | |
# ratio = -ratio | |
# # %% | |
# angle_radians = -np.radians() | |
# line_direction = np.array([np.cos(angle_radians), 0, np.sin(angle_radians)]) | |
# # Calculate the vector from the line's point to the given point | |
# point_to_line_point = cg_pt - np.array(LE_pt) | |
# # Calculate the projection of the point onto the line | |
# projection = ( | |
# np.array(LE_pt) + np.dot(point_to_line_point, line_direction) * line_direction | |
# ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment