def test_fix2euler(): """Test convertion from fix angles to Euler angles""" fix_angle = SimpleNamespace() fix_angle.x = 0 fix_angle.y = 0 fix_angle.z = 0 euler_angle = euler2fix(fix_angle) assert euler_angle.x == 0.0 assert euler_angle.y == 0.0 assert euler_angle.z == 0.0 fix_angle.x = 90 fix_angle.y = 90 fix_angle.z = 90 euler_angle = euler2fix(fix_angle) assert euler_angle.x == 90.0 assert euler_angle.y == -90.0 assert euler_angle.z == 90.0 # Test by doing both transformation fix_angle.x = 30.23 fix_angle.y = -85.52 fix_angle.z = -10.98 euler_angle = fix2euler(fix_angle) fix_angle2 = euler2fix(euler_angle) assert fix_angle == fix_angle2
def test_euler2fix(): """Test convertion from Euler angles to fix angles """ euler_angle = SimpleNamespace() euler_angle.x = 0 euler_angle.y = 0 euler_angle.z = 0 fix_angle = euler2fix(euler_angle) assert fix_angle.x == 0.0 assert fix_angle.y == 0.0 assert fix_angle.z == 0.0 euler_angle.x = 50 euler_angle.y = 32 euler_angle.z = 65 fix_angle = euler2fix(euler_angle) assert fix_angle.x == approx(49.24) assert fix_angle.y == approx(-33.39) assert fix_angle.z == approx(64.58) euler_angle.x = -12.5 euler_angle.y = 27 euler_angle.z = 93 fix_angle = euler2fix(euler_angle) assert fix_angle.x == approx(27.56) assert fix_angle.y == approx(11.12) assert fix_angle.z == approx(92.72) euler_angle.x = 90 euler_angle.y = 90 euler_angle.z = 90 fix_angle = euler2fix(euler_angle) assert fix_angle.x == 90.0 assert fix_angle.y == -90.0 assert fix_angle.z == 90.0
def convert_cpacs_to_sumo(cpacs_path, cpacs_out_path): """ Function to convert a CPACS file geometry into a SUMO file geometry. Function 'convert_cpacs_to_sumo' open an input cpacs file with TIXI handle and via two main loop, one for fuselage(s), one for wing(s) it convert every element (as much as possible) in the SUMO (.smx) format, which is also an xml file. Due to some differences between both format, some CPACS definition could lead to issues. The output sumo file is saved in the folder /ToolOutput Source: * CPACS documentation: https://www.cpacs.de/pages/documentation.html Args: cpacs_path (str): Path to the CPACS file Returns: sumo_output_path (str): Path to the SUMO file """ EMPTY_SMX = MODULE_DIR + '/files/sumo_empty.smx' tixi = cpsf.open_tixi(cpacs_path) sumo = cpsf.open_tixi(EMPTY_SMX) # Fuslage(s) ----- FUSELAGES_XPATH = '/cpacs/vehicles/aircraft/model/fuselages' if tixi.checkElement(FUSELAGES_XPATH): fus_cnt = tixi.getNamedChildrenCount(FUSELAGES_XPATH, 'fuselage') log.info(str(fus_cnt) + ' fuselage has been found.') else: fus_cnt = 0 log.warning('No fuselage has been found in this CPACS file!') for i_fus in range(fus_cnt): fus_xpath = FUSELAGES_XPATH + '/fuselage[' + str(i_fus + 1) + ']' fus_uid = tixi.getTextAttribute(fus_xpath, 'uID') fus_transf = Transformation() fus_transf.get_cpacs_transf(tixi, fus_xpath + '/transformation') # Create new body (SUMO) sumo.createElementAtIndex('/Assembly', 'BodySkeleton', i_fus + 1) body_xpath = '/Assembly/BodySkeleton[' + str(i_fus + 1) + ']' sumo.addTextAttribute(body_xpath, 'akimatg', 'false') sumo.addTextAttribute(body_xpath, 'name', fus_uid) body_tansf = Transformation() body_tansf.translation = fus_transf.translation # Convert angles body_tansf.rotation = euler2fix(fus_transf.rotation) # Add body rotation body_rot_str = str(math.radians(body_tansf.rotation.x)) + ' ' \ + str(math.radians(body_tansf.rotation.y)) + ' ' \ + str(math.radians(body_tansf.rotation.z)) sumo.addTextAttribute(body_xpath, 'rotation', body_rot_str) # Add body origin body_ori_str = str(body_tansf.translation.x) + ' ' \ + str(body_tansf.translation.y) + ' ' \ + str(body_tansf.translation.z) sumo.addTextAttribute(body_xpath, 'origin', body_ori_str) # Positionings if tixi.checkElement(fus_xpath + '/positionings'): pos_cnt = tixi.getNamedChildrenCount(fus_xpath + '/positionings', 'positioning') log.info(str(fus_cnt) + ' "Positionning" has been found : ') pos_x_list = [] pos_y_list = [] pos_z_list = [] from_sec_list = [] to_sec_list = [] for i_pos in range(pos_cnt): pos_xpath = fus_xpath + '/positionings/positioning[' \ + str(i_pos+1) + ']' length = tixi.getDoubleElement(pos_xpath + '/length') sweep_deg = tixi.getDoubleElement(pos_xpath + '/sweepAngle') sweep = math.radians(sweep_deg) dihedral_deg = tixi.getDoubleElement(pos_xpath + '/dihedralAngle') dihedral = math.radians(dihedral_deg) # Get the corresponding translation of each positionning pos_x_list.append(length * math.sin(sweep)) pos_y_list.append(length * math.cos(dihedral) * math.cos(sweep)) pos_z_list.append(length * math.sin(dihedral) * math.cos(sweep)) # Get which section are connected by the positionning if tixi.checkElement(pos_xpath + '/fromSectionUID'): from_sec = tixi.getTextElement(pos_xpath + '/fromSectionUID') else: from_sec = '' from_sec_list.append(from_sec) if tixi.checkElement(pos_xpath + '/toSectionUID'): to_sec = tixi.getTextElement(pos_xpath + '/toSectionUID') else: to_sec = '' to_sec_list.append(to_sec) # Re-loop though the positionning to re-order them for j_pos in range(pos_cnt): if from_sec_list[j_pos] == '': prev_pos_x = 0 prev_pos_y = 0 prev_pos_z = 0 elif from_sec_list[j_pos] == to_sec_list[j_pos - 1]: prev_pos_x = pos_x_list[j_pos - 1] prev_pos_y = pos_y_list[j_pos - 1] prev_pos_z = pos_z_list[j_pos - 1] else: index_prev = to_sec_list.index(from_sec_list[j_pos]) prev_pos_x = pos_x_list[index_prev] prev_pos_y = pos_y_list[index_prev] prev_pos_z = pos_z_list[index_prev] pos_x_list[j_pos] += prev_pos_x pos_y_list[j_pos] += prev_pos_y pos_z_list[j_pos] += prev_pos_z else: log.warning('No "positionings" have been found!') pos_cnt = 0 #Sections sec_cnt = tixi.getNamedChildrenCount(fus_xpath + '/sections', 'section') log.info(" -" + str(sec_cnt) + ' fuselage sections have been found') if pos_cnt == 0: pos_x_list = [0.0] * sec_cnt pos_y_list = [0.0] * sec_cnt pos_z_list = [0.0] * sec_cnt for i_sec in range(sec_cnt): sec_xpath = fus_xpath + '/sections/section[' + str(i_sec + 1) + ']' sec_uid = tixi.getTextAttribute(sec_xpath, 'uID') sec_transf = Transformation() sec_transf.get_cpacs_transf(tixi, sec_xpath + '/transformation') if (sec_transf.rotation.x or sec_transf.rotation.y or sec_transf.rotation.z): log.warning('Sections "' + sec_uid + '" is rotated, it is \ not possible to take that into acount in SUMO !') # Elements elem_cnt = tixi.getNamedChildrenCount(sec_xpath + '/elements', 'element') if elem_cnt > 1: log.warning("Sections " + sec_uid + " contains multiple \ element, it could be an issue for the conversion \ to SUMO!") for i_elem in range(elem_cnt): elem_xpath = sec_xpath + '/elements/element[' \ + str(i_elem + 1) + ']' elem_uid = tixi.getTextAttribute(elem_xpath, 'uID') elem_transf = Transformation() elem_transf.get_cpacs_transf(tixi, elem_xpath + '/transformation') if (elem_transf.rotation.x or elem_transf.rotation.y or elem_transf.rotation.z): log.warning('Element "' + elem_uid + '" is rotated, it \ is not possible to take that into acount in \ SUMO !') # Fuselage profiles prof_uid = tixi.getTextElement(elem_xpath + '/profileUID') prof_xpath = tixi.uIDGetXPath(prof_uid) prof_vect_x_str = tixi.getTextElement(prof_xpath + '/pointList/x') prof_vect_y_str = tixi.getTextElement(prof_xpath + '/pointList/y') prof_vect_z_str = tixi.getTextElement(prof_xpath + '/pointList/z') # Transform sting into list of float prof_vect_x = [] for i, item in enumerate(prof_vect_x_str.split(';')): if item: prof_vect_x.append(float(item)) prof_vect_y = [] for i, item in enumerate(prof_vect_y_str.split(';')): if item: prof_vect_y.append(float(item)) prof_vect_z = [] for i, item in enumerate(prof_vect_z_str.split(';')): if item: prof_vect_z.append(float(item)) prof_size_y = (max(prof_vect_y) - min(prof_vect_y)) / 2 prof_size_z = (max(prof_vect_z) - min(prof_vect_z)) / 2 prof_vect_y[:] = [y / prof_size_y for y in prof_vect_y] prof_vect_z[:] = [z / prof_size_z for z in prof_vect_z] prof_min_y = min(prof_vect_y) prof_max_y = max(prof_vect_y) prof_min_z = min(prof_vect_z) prof_max_z = max(prof_vect_z) prof_vect_y[:] = [y - 1 - prof_min_y for y in prof_vect_y] prof_vect_z[:] = [z - 1 - prof_min_z for z in prof_vect_z] # Could be a problem if they are less positionings than secions # TODO: solve that! pos_y_list[i_sec] += ( (1 + prof_min_y) * prof_size_y) * elem_transf.scale.y pos_z_list[i_sec] += ( (1 + prof_min_z) * prof_size_z) * elem_transf.scale.z # #To Plot a particular section # if i_sec==5: # plt.plot(prof_vect_z, prof_vect_y,'x') # plt.xlabel('y') # plt.ylabel('z') # plt.grid(True) # plt.show # Put value in SUMO format body_frm_center_x = ( elem_transf.translation.x \ + sec_transf.translation.x \ + pos_x_list[i_sec]) \ * fus_transf.scale.x body_frm_center_y = ( elem_transf.translation.y \ * sec_transf.scale.y \ + sec_transf.translation.y \ + pos_y_list[i_sec]) \ * fus_transf.scale.y body_frm_center_z = ( elem_transf.translation.z \ * sec_transf.scale.z \ + sec_transf.translation.z \ + pos_z_list[i_sec]) \ * fus_transf.scale.z body_frm_height = prof_size_z * 2 * elem_transf.scale.z \ * sec_transf.scale.z * fus_transf.scale.z if body_frm_height < 0.01: body_frm_height = 0.01 body_frm_width = prof_size_y * 2 * elem_transf.scale.y \ * sec_transf.scale.y * fus_transf.scale.y if body_frm_width < 0.01: body_frm_width = 0.01 # Convert the profile points in the SMX format prof_str = '' teta_list = [] teta_half = [] prof_vect_y_half = [] prof_vect_z_half = [] check_max = 0 check_min = 0 # Use polar angle to keep point in the correct order for i, item in enumerate(prof_vect_y): teta_list.append(math.atan2(prof_vect_z[i], prof_vect_y[i])) for t, teta in enumerate(teta_list): HALF_PI = math.pi / 2 EPSILON = 0.04 if abs(teta) <= HALF_PI - EPSILON: teta_half.append(teta) prof_vect_y_half.append(prof_vect_y[t]) prof_vect_z_half.append(prof_vect_z[t]) elif abs(teta) < HALF_PI + EPSILON: # Check if not the last element of the list if not t == len(teta_list) - 1: next_val = prof_vect_z[t + 1] # Check if it is better to keep next point if not abs(next_val) > abs(prof_vect_z[t]): if prof_vect_z[t] > 0 and not check_max: teta_half.append(teta) # Force y=0, to get symmetrical profile prof_vect_y_half.append(0) prof_vect_z_half.append(prof_vect_z[t]) check_max = 1 elif prof_vect_z[t] < 0 and not check_min: teta_half.append(teta) # Force y=0, to get symmetrical profile prof_vect_y_half.append(0) prof_vect_z_half.append(prof_vect_z[t]) check_min = 1 # Sort points by teta value, to fit the SUMO profile format teta_half, prof_vect_z_half, prof_vect_y_half = \ (list(t) for t in zip(*sorted(zip(teta_half, prof_vect_z_half, prof_vect_y_half)))) # Write profile as a string and add y=0 point at the begining # and at the end to ensure symmmetry if not check_min: prof_str += str(0) + ' ' + str(prof_vect_z_half[0]) + ' ' for i, item in enumerate(prof_vect_z_half): prof_str += str(round(prof_vect_y_half[i], 4)) + ' ' \ + str(round(prof_vect_z_half[i], 4)) + ' ' if not check_max: prof_str += str(0) + ' ' + str(prof_vect_z_half[i]) + ' ' # Write the SUMO file sumo.addTextElementAtIndex(body_xpath, 'BodyFrame', prof_str, i_sec + 1) frame_xpath = body_xpath + '/BodyFrame[' + str(i_sec + 1) + ']' body_center_str = str(body_frm_center_x) + ' ' + \ str(body_frm_center_y) + ' ' + \ str(body_frm_center_z) sumo.addTextAttribute(frame_xpath, 'center', body_center_str) sumo.addTextAttribute(frame_xpath, 'height', str(body_frm_height)) sumo.addTextAttribute(frame_xpath, 'width', str(body_frm_width)) sumo.addTextAttribute(frame_xpath, 'name', sec_uid) # To remove the default BodySkeleton if fus_cnt == 0: sumo.removeElement('/Assembly/BodySkeleton') else: sumo.removeElement('/Assembly/BodySkeleton[' + str(fus_cnt + 1) + ']') # Wing(s) ----- WINGS_XPATH = '/cpacs/vehicles/aircraft/model/wings' if tixi.checkElement(WINGS_XPATH): wing_cnt = tixi.getNamedChildrenCount(WINGS_XPATH, 'wing') log.info(str(wing_cnt) + ' wings has been found.') else: wing_cnt = 0 log.warning('No wings has been found in this CPACS file!') for i_wing in range(wing_cnt): wing_xpath = WINGS_XPATH + '/wing[' + str(i_wing + 1) + ']' wing_uid = tixi.getTextAttribute(wing_xpath, 'uID') wing_transf = Transformation() wing_transf.get_cpacs_transf(tixi, wing_xpath + '/transformation') # Create new wing (SUMO) sumo.createElementAtIndex('/Assembly', 'WingSkeleton', i_wing + 1) wg_sk_xpath = '/Assembly/WingSkeleton[' + str(i_wing + 1) + ']' sumo.addTextAttribute(wg_sk_xpath, 'akimatg', 'false') sumo.addTextAttribute(wg_sk_xpath, 'name', wing_uid) # Create a class for the transformation of the WingSkeleton wg_sk_tansf = Transformation() # Convert WingSkeleton rotation and add it to SUMO wg_sk_tansf.rotation = euler2fix(wing_transf.rotation) wg_sk_rot_str = str(math.radians(wg_sk_tansf.rotation.x)) + ' ' \ + str(math.radians(wg_sk_tansf.rotation.y)) + ' ' \ + str(math.radians(wg_sk_tansf.rotation.z)) sumo.addTextAttribute(wg_sk_xpath, 'rotation', wg_sk_rot_str) # Add WingSkeleton origin wg_sk_tansf.translation = wing_transf.translation wg_sk_ori_str = str(wg_sk_tansf.translation.x) + ' ' \ + str(wg_sk_tansf.translation.y) + ' ' \ + str(wg_sk_tansf.translation.z) sumo.addTextAttribute(wg_sk_xpath, 'origin', wg_sk_ori_str) if tixi.checkAttribute(wing_xpath, 'symmetry'): if tixi.getTextAttribute(wing_xpath, 'symmetry') == 'x-z-plane': sumo.addTextAttribute(wg_sk_xpath, 'flags', 'autosym,detectwinglet') else: sumo.addTextAttribute(wg_sk_xpath, 'flags', 'detectwinglet') # Positionings if tixi.checkElement(wing_xpath + '/positionings'): pos_cnt = tixi.getNamedChildrenCount(wing_xpath + '/positionings', 'positioning') log.info(str(wing_cnt) + ' "positionning" has been found : ') pos_x_list = [] pos_y_list = [] pos_z_list = [] from_sec_list = [] to_sec_list = [] for i_pos in range(pos_cnt): pos_xpath = wing_xpath + '/positionings/positioning[' \ + str(i_pos+1) + ']' length = tixi.getDoubleElement(pos_xpath + '/length') sweep_deg = tixi.getDoubleElement(pos_xpath + '/sweepAngle') sweep = math.radians(sweep_deg) dihedral_deg = tixi.getDoubleElement(pos_xpath + '/dihedralAngle') dihedral = math.radians(dihedral_deg) # Get the corresponding translation of each positionning pos_x_list.append(length * math.sin(sweep)) pos_y_list.append(length * math.cos(dihedral) * math.cos(sweep)) pos_z_list.append(length * math.sin(dihedral) * math.cos(sweep)) # Get which section are connected by the positionning if tixi.checkElement(pos_xpath + '/fromSectionUID'): from_sec = tixi.getTextElement(pos_xpath + '/fromSectionUID') else: from_sec = '' from_sec_list.append(from_sec) if tixi.checkElement(pos_xpath + '/toSectionUID'): to_sec = tixi.getTextElement(pos_xpath + '/toSectionUID') else: to_sec = '' to_sec_list.append(to_sec) # Re-loop though the positionning to re-order them for j_pos in range(pos_cnt): if from_sec_list[j_pos] == '': prev_pos_x = 0 prev_pos_y = 0 prev_pos_z = 0 elif from_sec_list[j_pos] == to_sec_list[j_pos - 1]: prev_pos_x = pos_x_list[j_pos - 1] prev_pos_y = pos_y_list[j_pos - 1] prev_pos_z = pos_z_list[j_pos - 1] else: index_prev = to_sec_list.index(from_sec_list[j_pos]) prev_pos_x = pos_x_list[index_prev] prev_pos_y = pos_y_list[index_prev] prev_pos_z = pos_z_list[index_prev] pos_x_list[j_pos] += prev_pos_x pos_y_list[j_pos] += prev_pos_y pos_z_list[j_pos] += prev_pos_z else: log.warning('No "positionings" have been found!') pos_cnt = 0 #Sections sec_cnt = tixi.getNamedChildrenCount(wing_xpath + '/sections', 'section') log.info(" -" + str(sec_cnt) + ' wing sections have been found') wing_sec_index = 1 if pos_cnt == 0: pos_x_list = [0.0] * sec_cnt pos_y_list = [0.0] * sec_cnt pos_z_list = [0.0] * sec_cnt for i_sec in reversed(range(sec_cnt)): sec_xpath = wing_xpath + '/sections/section[' + str(i_sec + 1) + ']' sec_uid = tixi.getTextAttribute(sec_xpath, 'uID') sec_transf = Transformation() sec_transf.get_cpacs_transf(tixi, sec_xpath + '/transformation') # Elements elem_cnt = tixi.getNamedChildrenCount(sec_xpath + '/elements', 'element') if elem_cnt > 1: log.warning("Sections " + sec_uid + " contains multiple \ element, it could be an issue for the conversion \ to SUMO!") for i_elem in range(elem_cnt): elem_xpath = sec_xpath + '/elements/element[' \ + str(i_elem + 1) + ']' elem_uid = tixi.getTextAttribute(elem_xpath, 'uID') elem_transf = Transformation() elem_transf.get_cpacs_transf(tixi, elem_xpath + '/transformation') # Wing profile (airfoil) prof_uid = tixi.getTextElement(elem_xpath + '/airfoilUID') prof_xpath = tixi.uIDGetXPath(prof_uid) try: tixi.checkElement(prof_xpath) except: log.error('No profile "' + prof_uid + '" has been found!') prof_vect_x_str = tixi.getTextElement(prof_xpath + '/pointList/x') prof_vect_y_str = tixi.getTextElement(prof_xpath + '/pointList/y') prof_vect_z_str = tixi.getTextElement(prof_xpath + '/pointList/z') # Transform airfoil points (string) into list of float prof_vect_x = [] for i, item in enumerate(prof_vect_x_str.split(';')): if item: prof_vect_x.append(float(item)) prof_vect_y = [] for i, item in enumerate(prof_vect_y_str.split(';')): if item: prof_vect_y.append(float(item)) prof_vect_z = [] for i, item in enumerate(prof_vect_z_str.split(';')): if item: prof_vect_z.append(float(item)) if sum(prof_vect_z[0:len(prof_vect_z)//2]) \ < sum(prof_vect_z[len(prof_vect_z)//2:-1]): log.info("Airfoil's points will be reversed.") tmp_vect_x = [] tmp_vect_y = [] tmp_vect_z = [] for i in range(len(prof_vect_x)): tmp_vect_x.append(prof_vect_x[len(prof_vect_x) - 1 - i]) tmp_vect_y.append(prof_vect_y[len(prof_vect_y) - 1 - i]) tmp_vect_z.append(prof_vect_z[len(prof_vect_z) - 1 - i]) prof_vect_x = tmp_vect_x prof_vect_y = tmp_vect_y prof_vect_z = tmp_vect_z # Apply scaling for i, item in enumerate(prof_vect_x): prof_vect_x[i] = item * elem_transf.scale.x \ * sec_transf.scale.x * wing_transf.scale.x for i, item in enumerate(prof_vect_y): prof_vect_y[i] = item * elem_transf.scale.y \ * sec_transf.scale.y * wing_transf.scale.y for i, item in enumerate(prof_vect_z): prof_vect_z[i] = item * elem_transf.scale.z \ * sec_transf.scale.z * wing_transf.scale.z # if (i_sec>8 and i_sec<=10): # plt.plot(prof_vect_x, prof_vect_z,'x') # plt.xlabel('x') # plt.ylabel('z') # plt.grid(True) # plt.show() prof_size_x = (max(prof_vect_x) - min(prof_vect_x)) prof_size_y = (max(prof_vect_y) - min(prof_vect_y)) prof_size_z = (max(prof_vect_z) - min(prof_vect_z)) if prof_size_y == 0: prof_vect_x[:] = [x / prof_size_x for x in prof_vect_x] prof_vect_z[:] = [z / prof_size_x for z in prof_vect_z] # Is it correct to divide by prof_size_x ???? wg_sec_chord = prof_size_x else: log.error("An airfoil profile is not define correctly") # SUMO variable for WingSection wg_sec_center_x = ( elem_transf.translation.x \ + sec_transf.translation.x \ + pos_x_list[i_sec]) \ * wing_transf.scale.x wg_sec_center_y = ( elem_transf.translation.y \ * sec_transf.scale.y \ + sec_transf.translation.y \ + pos_y_list[i_sec]) \ * wing_transf.scale.y wg_sec_center_z = ( elem_transf.translation.z \ * sec_transf.scale.z \ + sec_transf.translation.z \ + pos_z_list[i_sec]) \ * wing_transf.scale.z # Add roation from element and sections # Adding the two angles: Maybe not work in every case!!! add_rotation = SimpleNamespace() add_rotation.x = elem_transf.rotation.x + sec_transf.rotation.x add_rotation.y = elem_transf.rotation.y + sec_transf.rotation.y add_rotation.z = elem_transf.rotation.z + sec_transf.rotation.z # Get Section rotation for SUMO wg_sec_rot = euler2fix(add_rotation) wg_sec_dihed = math.radians(wg_sec_rot.x) wg_sec_twist = math.radians(wg_sec_rot.y) wg_sec_yaw = math.radians(wg_sec_rot.z) # Convert point list into string prof_str = '' # Airfoil points order : shoud be from TE (1 0) to LE (0 0) # then TE(1 0), but not reverse way. # to avoid double zero, not accepted by SUMO for i, item in (enumerate(prof_vect_x)): # if not (prof_vect_x[i] == prof_vect_x[i-1] or \ # round(prof_vect_z[i],4) == round(prof_vect_z[i-1],4)): if round(prof_vect_z[i], 4) != round( prof_vect_z[i - 1], 4): prof_str += str(round(prof_vect_x[i], 4)) + ' ' \ + str(round(prof_vect_z[i], 4)) + ' ' sumo.addTextElementAtIndex(wg_sk_xpath, 'WingSection', prof_str, wing_sec_index) wg_sec_xpath = wg_sk_xpath + '/WingSection[' \ + str(wing_sec_index) + ']' sumo.addTextAttribute(wg_sec_xpath, 'airfoil', prof_uid) sumo.addTextAttribute(wg_sec_xpath, 'name', sec_uid) wg_sec_center_str = str(wg_sec_center_x) + ' ' + \ str(wg_sec_center_y) + ' ' + \ str(wg_sec_center_z) sumo.addTextAttribute(wg_sec_xpath, 'center', wg_sec_center_str) sumo.addTextAttribute(wg_sec_xpath, 'chord', str(wg_sec_chord)) sumo.addTextAttribute(wg_sec_xpath, 'dihedral', str(wg_sec_dihed)) sumo.addTextAttribute(wg_sec_xpath, 'twist', str(wg_sec_twist)) sumo.addTextAttribute(wg_sec_xpath, 'yaw', str(wg_sec_yaw)) sumo.addTextAttribute(wg_sec_xpath, 'napprox', '-1') sumo.addTextAttribute(wg_sec_xpath, 'reversed', 'false') sumo.addTextAttribute(wg_sec_xpath, 'vbreak', 'false') wing_sec_index += 1 # Add Wing caps sumo.createElementAtIndex(wg_sk_xpath, "Cap", 1) sumo.addTextAttribute(wg_sk_xpath + '/Cap[1]', 'height', '0') sumo.addTextAttribute(wg_sk_xpath + '/Cap[1]', 'shape', 'LongCap') sumo.addTextAttribute(wg_sk_xpath + '/Cap[1]', 'side', 'south') sumo.createElementAtIndex(wg_sk_xpath, 'Cap', 2) sumo.addTextAttribute(wg_sk_xpath + '/Cap[2]', 'height', '0') sumo.addTextAttribute(wg_sk_xpath + '/Cap[2]', 'shape', 'LongCap') sumo.addTextAttribute(wg_sk_xpath + '/Cap[2]', 'side', 'north') # Save the SMX file wkdir = ceaf.get_wkdir_or_create_new(tixi) sumo_file_xpath = '/cpacs/toolspecific/CEASIOMpy/filesPath/sumoFilePath' sumo_dir = os.path.join(wkdir, 'SUMO') sumo_file_path = os.path.join(sumo_dir, 'ToolOutput.smx') if not os.path.isdir(sumo_dir): os.mkdir(sumo_dir) cpsf.create_branch(tixi, sumo_file_xpath) tixi.updateTextElement(sumo_file_xpath, sumo_file_path) cpsf.close_tixi(tixi, cpacs_out_path) cpsf.close_tixi(sumo, sumo_file_path)
def convert_cpacs_to_sumo(cpacs_path, cpacs_out_path): """Function to convert a CPACS file geometry into a SUMO file geometry. Function 'convert_cpacs_to_sumo' open an input cpacs file with TIXI handle and via two main loop, one for fuselage(s), one for wing(s) it convert every element (as much as possible) in the SUMO (.smx) format, which is also an xml file. Due to some differences between both format, some CPACS definition could lead to issues. The output sumo file is saved in the folder /ToolOutput Source: * CPACS documentation: https://www.cpacs.de/pages/documentation.html Args: cpacs_path (str): Path to the CPACS file Returns: sumo_output_path (str): Path to the SUMO file """ EMPTY_SMX = MODULE_DIR + "/files/sumo_empty.smx" tixi = open_tixi(cpacs_path) sumo = open_tixi(EMPTY_SMX) # Fuslage(s) --------------------------------------------------------------- if tixi.checkElement(FUSELAGES_XPATH): fus_cnt = tixi.getNamedChildrenCount(FUSELAGES_XPATH, "fuselage") log.info(str(fus_cnt) + " fuselage has been found.") else: fus_cnt = 0 log.warning("No fuselage has been found in this CPACS file!") for i_fus in range(fus_cnt): fus_xpath = FUSELAGES_XPATH + "/fuselage[" + str(i_fus + 1) + "]" fus_uid = tixi.getTextAttribute(fus_xpath, "uID") fus_transf = Transformation() fus_transf.get_cpacs_transf(tixi, fus_xpath) # Create new body (SUMO) sumo.createElementAtIndex("/Assembly", "BodySkeleton", i_fus + 1) body_xpath = "/Assembly/BodySkeleton[" + str(i_fus + 1) + "]" sumo.addTextAttribute(body_xpath, "akimatg", "false") sumo.addTextAttribute(body_xpath, "name", fus_uid) body_tansf = Transformation() body_tansf.translation = fus_transf.translation # Convert angles body_tansf.rotation = euler2fix(fus_transf.rotation) # Add body rotation body_rot_str = (str(math.radians(body_tansf.rotation.x)) + " " + str(math.radians(body_tansf.rotation.y)) + " " + str(math.radians(body_tansf.rotation.z))) sumo.addTextAttribute(body_xpath, "rotation", body_rot_str) # Add body origin body_ori_str = (str(body_tansf.translation.x) + " " + str(body_tansf.translation.y) + " " + str(body_tansf.translation.z)) sumo.addTextAttribute(body_xpath, "origin", body_ori_str) # Positionings if tixi.checkElement(fus_xpath + "/positionings"): pos_cnt = tixi.getNamedChildrenCount(fus_xpath + "/positionings", "positioning") log.info(str(fus_cnt) + ' "Positionning" has been found : ') pos_x_list = [] pos_y_list = [] pos_z_list = [] from_sec_list = [] to_sec_list = [] for i_pos in range(pos_cnt): pos_xpath = fus_xpath + "/positionings/positioning[" + str( i_pos + 1) + "]" length = tixi.getDoubleElement(pos_xpath + "/length") sweep_deg = tixi.getDoubleElement(pos_xpath + "/sweepAngle") sweep = math.radians(sweep_deg) dihedral_deg = tixi.getDoubleElement(pos_xpath + "/dihedralAngle") dihedral = math.radians(dihedral_deg) # Get the corresponding translation of each positionning pos_x_list.append(length * math.sin(sweep)) pos_y_list.append(length * math.cos(dihedral) * math.cos(sweep)) pos_z_list.append(length * math.sin(dihedral) * math.cos(sweep)) # Get which section are connected by the positionning if tixi.checkElement(pos_xpath + "/fromSectionUID"): from_sec = tixi.getTextElement(pos_xpath + "/fromSectionUID") else: from_sec = "" from_sec_list.append(from_sec) if tixi.checkElement(pos_xpath + "/toSectionUID"): to_sec = tixi.getTextElement(pos_xpath + "/toSectionUID") else: to_sec = "" to_sec_list.append(to_sec) # Re-loop though the positionning to re-order them for j_pos in range(pos_cnt): if from_sec_list[j_pos] == "": prev_pos_x = 0 prev_pos_y = 0 prev_pos_z = 0 elif from_sec_list[j_pos] == to_sec_list[j_pos - 1]: prev_pos_x = pos_x_list[j_pos - 1] prev_pos_y = pos_y_list[j_pos - 1] prev_pos_z = pos_z_list[j_pos - 1] else: index_prev = to_sec_list.index(from_sec_list[j_pos]) prev_pos_x = pos_x_list[index_prev] prev_pos_y = pos_y_list[index_prev] prev_pos_z = pos_z_list[index_prev] pos_x_list[j_pos] += prev_pos_x pos_y_list[j_pos] += prev_pos_y pos_z_list[j_pos] += prev_pos_z else: log.warning('No "positionings" have been found!') pos_cnt = 0 # Sections sec_cnt = tixi.getNamedChildrenCount(fus_xpath + "/sections", "section") log.info(" -" + str(sec_cnt) + " fuselage sections have been found") if pos_cnt == 0: pos_x_list = [0.0] * sec_cnt pos_y_list = [0.0] * sec_cnt pos_z_list = [0.0] * sec_cnt for i_sec in range(sec_cnt): sec_xpath = fus_xpath + "/sections/section[" + str(i_sec + 1) + "]" sec_uid = tixi.getTextAttribute(sec_xpath, "uID") sec_transf = Transformation() sec_transf.get_cpacs_transf(tixi, sec_xpath) if sec_transf.rotation.x or sec_transf.rotation.y or sec_transf.rotation.z: log.warning('Sections "' + sec_uid + '" is rotated, it is \ not possible to take that into acount in SUMO !') # Elements elem_cnt = tixi.getNamedChildrenCount(sec_xpath + "/elements", "element") if elem_cnt > 1: log.warning("Sections " + sec_uid + " contains multiple \ element, it could be an issue for the conversion \ to SUMO!") for i_elem in range(elem_cnt): elem_xpath = sec_xpath + "/elements/element[" + str(i_elem + 1) + "]" elem_uid = tixi.getTextAttribute(elem_xpath, "uID") elem_transf = Transformation() elem_transf.get_cpacs_transf(tixi, elem_xpath) if elem_transf.rotation.x or elem_transf.rotation.y or elem_transf.rotation.z: log.warning('Element "' + elem_uid + '" is rotated, it \ is not possible to take that into acount in \ SUMO !') # Fuselage profiles prof_uid = tixi.getTextElement(elem_xpath + "/profileUID") prof_vect_x, prof_vect_y, prof_vect_z = get_profile_coord( tixi, prof_uid) prof_size_y = (max(prof_vect_y) - min(prof_vect_y)) / 2 prof_size_z = (max(prof_vect_z) - min(prof_vect_z)) / 2 prof_vect_y[:] = [y / prof_size_y for y in prof_vect_y] prof_vect_z[:] = [z / prof_size_z for z in prof_vect_z] prof_min_y = min(prof_vect_y) prof_min_z = min(prof_vect_z) prof_vect_y[:] = [y - 1 - prof_min_y for y in prof_vect_y] prof_vect_z[:] = [z - 1 - prof_min_z for z in prof_vect_z] # Could be a problem if they are less positionings than secions # TODO: solve that! pos_y_list[i_sec] += ( (1 + prof_min_y) * prof_size_y) * elem_transf.scaling.y pos_z_list[i_sec] += ( (1 + prof_min_z) * prof_size_z) * elem_transf.scaling.z # #To Plot a particular section # if i_sec==5: # plt.plot(prof_vect_z, prof_vect_y,'x') # plt.xlabel('y') # plt.ylabel('z') # plt.grid(True) # plt.show # Put value in SUMO format body_frm_center_x = (elem_transf.translation.x + sec_transf.translation.x + pos_x_list[i_sec]) * fus_transf.scaling.x body_frm_center_y = ( elem_transf.translation.y * sec_transf.scaling.y + sec_transf.translation.y + pos_y_list[i_sec]) * fus_transf.scaling.y body_frm_center_z = ( elem_transf.translation.z * sec_transf.scaling.z + sec_transf.translation.z + pos_z_list[i_sec]) * fus_transf.scaling.z body_frm_height = (prof_size_z * 2 * elem_transf.scaling.z * sec_transf.scaling.z * fus_transf.scaling.z) if body_frm_height < 0.01: body_frm_height = 0.01 body_frm_width = (prof_size_y * 2 * elem_transf.scaling.y * sec_transf.scaling.y * fus_transf.scaling.y) if body_frm_width < 0.01: body_frm_width = 0.01 # Convert the profile points in the SMX format prof_str = "" teta_list = [] teta_half = [] prof_vect_y_half = [] prof_vect_z_half = [] check_max = 0 check_min = 0 # Use polar angle to keep point in the correct order for i, item in enumerate(prof_vect_y): teta_list.append(math.atan2(prof_vect_z[i], prof_vect_y[i])) for t, teta in enumerate(teta_list): HALF_PI = math.pi / 2 EPSILON = 0.04 if abs(teta) <= HALF_PI - EPSILON: teta_half.append(teta) prof_vect_y_half.append(prof_vect_y[t]) prof_vect_z_half.append(prof_vect_z[t]) elif abs(teta) < HALF_PI + EPSILON: # Check if not the last element of the list if not t == len(teta_list) - 1: next_val = prof_vect_z[t + 1] # Check if it is better to keep next point if not abs(next_val) > abs(prof_vect_z[t]): if prof_vect_z[t] > 0 and not check_max: teta_half.append(teta) # Force y=0, to get symmetrical profile prof_vect_y_half.append(0) prof_vect_z_half.append(prof_vect_z[t]) check_max = 1 elif prof_vect_z[t] < 0 and not check_min: teta_half.append(teta) # Force y=0, to get symmetrical profile prof_vect_y_half.append(0) prof_vect_z_half.append(prof_vect_z[t]) check_min = 1 # Sort points by teta value, to fit the SUMO profile format teta_half, prof_vect_z_half, prof_vect_y_half = ( list(t) for t in zip(*sorted( zip(teta_half, prof_vect_z_half, prof_vect_y_half)))) # Write profile as a string and add y=0 point at the begining # and at the end to ensure symmmetry if not check_min: prof_str += str(0) + " " + str(prof_vect_z_half[0]) + " " for i, item in enumerate(prof_vect_z_half): prof_str += (str(round(prof_vect_y_half[i], 4)) + " " + str(round(prof_vect_z_half[i], 4)) + " ") if not check_max: prof_str += str(0) + " " + str(prof_vect_z_half[i]) + " " # Write the SUMO file sumo.addTextElementAtIndex(body_xpath, "BodyFrame", prof_str, i_sec + 1) frame_xpath = body_xpath + "/BodyFrame[" + str(i_sec + 1) + "]" body_center_str = (str(body_frm_center_x) + " " + str(body_frm_center_y) + " " + str(body_frm_center_z)) sumo.addTextAttribute(frame_xpath, "center", body_center_str) sumo.addTextAttribute(frame_xpath, "height", str(body_frm_height)) sumo.addTextAttribute(frame_xpath, "width", str(body_frm_width)) sumo.addTextAttribute(frame_xpath, "name", sec_uid) # Fuselage symmetry (mirror copy) if tixi.checkAttribute(fus_xpath, "symmetry"): if tixi.getTextAttribute(fus_xpath, "symmetry") == "x-z-plane": sumo_mirror_copy(sumo, body_xpath, fus_uid, False) # To remove the default BodySkeleton if fus_cnt == 0: sumo.removeElement("/Assembly/BodySkeleton") else: sumo.removeElement("/Assembly/BodySkeleton[" + str(fus_cnt + 1) + "]") # Wing(s) ------------------------------------------------------------------ if tixi.checkElement(WINGS_XPATH): wing_cnt = tixi.getNamedChildrenCount(WINGS_XPATH, "wing") log.info(str(wing_cnt) + " wings has been found.") else: wing_cnt = 0 log.warning("No wings has been found in this CPACS file!") for i_wing in range(wing_cnt): wing_xpath = WINGS_XPATH + "/wing[" + str(i_wing + 1) + "]" wing_uid = tixi.getTextAttribute(wing_xpath, "uID") wing_transf = Transformation() wing_transf.get_cpacs_transf(tixi, wing_xpath) # Create new wing (SUMO) sumo.createElementAtIndex("/Assembly", "WingSkeleton", i_wing + 1) wg_sk_xpath = "/Assembly/WingSkeleton[" + str(i_wing + 1) + "]" sumo.addTextAttribute(wg_sk_xpath, "akimatg", "false") sumo.addTextAttribute(wg_sk_xpath, "name", wing_uid) # Create a class for the transformation of the WingSkeleton wg_sk_tansf = Transformation() # Convert WingSkeleton rotation and add it to SUMO wg_sk_tansf.rotation = euler2fix(wing_transf.rotation) wg_sk_rot_str = (str(math.radians(wg_sk_tansf.rotation.x)) + " " + str(math.radians(wg_sk_tansf.rotation.y)) + " " + str(math.radians(wg_sk_tansf.rotation.z))) sumo.addTextAttribute(wg_sk_xpath, "rotation", wg_sk_rot_str) # Add WingSkeleton origin wg_sk_tansf.translation = wing_transf.translation wg_sk_ori_str = (str(wg_sk_tansf.translation.x) + " " + str(wg_sk_tansf.translation.y) + " " + str(wg_sk_tansf.translation.z)) sumo.addTextAttribute(wg_sk_xpath, "origin", wg_sk_ori_str) if tixi.checkAttribute(wing_xpath, "symmetry"): if tixi.getTextAttribute(wing_xpath, "symmetry") == "x-z-plane": sumo.addTextAttribute(wg_sk_xpath, "flags", "autosym,detectwinglet") else: sumo.addTextAttribute(wg_sk_xpath, "flags", "detectwinglet") # Positionings if tixi.checkElement(wing_xpath + "/positionings"): pos_cnt = tixi.getNamedChildrenCount(wing_xpath + "/positionings", "positioning") log.info(str(wing_cnt) + ' "positionning" has been found : ') pos_x_list = [] pos_y_list = [] pos_z_list = [] from_sec_list = [] to_sec_list = [] for i_pos in range(pos_cnt): pos_xpath = wing_xpath + "/positionings/positioning[" + str( i_pos + 1) + "]" length = tixi.getDoubleElement(pos_xpath + "/length") sweep_deg = tixi.getDoubleElement(pos_xpath + "/sweepAngle") sweep = math.radians(sweep_deg) dihedral_deg = tixi.getDoubleElement(pos_xpath + "/dihedralAngle") dihedral = math.radians(dihedral_deg) # Get the corresponding translation of each positionning pos_x_list.append(length * math.sin(sweep)) pos_y_list.append(length * math.cos(dihedral) * math.cos(sweep)) pos_z_list.append(length * math.sin(dihedral) * math.cos(sweep)) # Get which section are connected by the positionning if tixi.checkElement(pos_xpath + "/fromSectionUID"): from_sec = tixi.getTextElement(pos_xpath + "/fromSectionUID") else: from_sec = "" from_sec_list.append(from_sec) if tixi.checkElement(pos_xpath + "/toSectionUID"): to_sec = tixi.getTextElement(pos_xpath + "/toSectionUID") else: to_sec = "" to_sec_list.append(to_sec) # Re-loop though the positionning to re-order them for j_pos in range(pos_cnt): if from_sec_list[j_pos] == "": prev_pos_x = 0 prev_pos_y = 0 prev_pos_z = 0 elif from_sec_list[j_pos] == to_sec_list[j_pos - 1]: prev_pos_x = pos_x_list[j_pos - 1] prev_pos_y = pos_y_list[j_pos - 1] prev_pos_z = pos_z_list[j_pos - 1] else: index_prev = to_sec_list.index(from_sec_list[j_pos]) prev_pos_x = pos_x_list[index_prev] prev_pos_y = pos_y_list[index_prev] prev_pos_z = pos_z_list[index_prev] pos_x_list[j_pos] += prev_pos_x pos_y_list[j_pos] += prev_pos_y pos_z_list[j_pos] += prev_pos_z else: log.warning('No "positionings" have been found!') pos_cnt = 0 # Sections sec_cnt = tixi.getNamedChildrenCount(wing_xpath + "/sections", "section") log.info(" -" + str(sec_cnt) + " wing sections have been found") wing_sec_index = 1 if pos_cnt == 0: pos_x_list = [0.0] * sec_cnt pos_y_list = [0.0] * sec_cnt pos_z_list = [0.0] * sec_cnt for i_sec in reversed(range(sec_cnt)): sec_xpath = wing_xpath + "/sections/section[" + str(i_sec + 1) + "]" sec_uid = tixi.getTextAttribute(sec_xpath, "uID") sec_transf = Transformation() sec_transf.get_cpacs_transf(tixi, sec_xpath) # Elements elem_cnt = tixi.getNamedChildrenCount(sec_xpath + "/elements", "element") if elem_cnt > 1: log.warning("Sections " + sec_uid + " contains multiple \ element, it could be an issue for the conversion \ to SUMO!") for i_elem in range(elem_cnt): elem_xpath = sec_xpath + "/elements/element[" + str(i_elem + 1) + "]" elem_uid = tixi.getTextAttribute(elem_xpath, "uID") elem_transf = Transformation() elem_transf.get_cpacs_transf(tixi, elem_xpath) # Get wing profile (airfoil) prof_uid = tixi.getTextElement(elem_xpath + "/airfoilUID") prof_vect_x, prof_vect_y, prof_vect_z = get_profile_coord( tixi, prof_uid) # Apply scaling for i, item in enumerate(prof_vect_x): prof_vect_x[i] = (item * elem_transf.scaling.x * sec_transf.scaling.x * wing_transf.scaling.x) for i, item in enumerate(prof_vect_y): prof_vect_y[i] = (item * elem_transf.scaling.y * sec_transf.scaling.y * wing_transf.scaling.y) for i, item in enumerate(prof_vect_z): prof_vect_z[i] = (item * elem_transf.scaling.z * sec_transf.scaling.z * wing_transf.scaling.z) # Plot setions (for tests) # if (i_sec>8 and i_sec<=10): # plt.plot(prof_vect_x, prof_vect_z,'x') # plt.xlabel('x') # plt.ylabel('z') # plt.grid(True) # plt.show() prof_size_x = max(prof_vect_x) - min(prof_vect_x) prof_size_y = max(prof_vect_y) - min(prof_vect_y) prof_size_z = max(prof_vect_z) - min(prof_vect_z) if prof_size_y == 0: prof_vect_x[:] = [x / prof_size_x for x in prof_vect_x] prof_vect_z[:] = [z / prof_size_x for z in prof_vect_z] # Is it correct to divide by prof_size_x ???? wg_sec_chord = prof_size_x else: log.error("An airfoil profile is not define correctly") # SUMO variable for WingSection wg_sec_center_x = (elem_transf.translation.x + sec_transf.translation.x + pos_x_list[i_sec]) * wing_transf.scaling.x wg_sec_center_y = ( elem_transf.translation.y * sec_transf.scaling.y + sec_transf.translation.y + pos_y_list[i_sec]) * wing_transf.scaling.y wg_sec_center_z = ( elem_transf.translation.z * sec_transf.scaling.z + sec_transf.translation.z + pos_z_list[i_sec]) * wing_transf.scaling.z # Add roation from element and sections # Adding the two angles: Maybe not work in every case!!! add_rotation = SimpleNamespace() add_rotation.x = elem_transf.rotation.x + sec_transf.rotation.x add_rotation.y = elem_transf.rotation.y + sec_transf.rotation.y add_rotation.z = elem_transf.rotation.z + sec_transf.rotation.z # Get Section rotation for SUMO wg_sec_rot = euler2fix(add_rotation) wg_sec_dihed = math.radians(wg_sec_rot.x) wg_sec_twist = math.radians(wg_sec_rot.y) wg_sec_yaw = math.radians(wg_sec_rot.z) # Convert point list into string prof_str = "" # Airfoil points order : shoud be from TE (1 0) to LE (0 0) # then TE(1 0), but not reverse way. # to avoid double zero, not accepted by SUMO for i, item in enumerate(prof_vect_x): # if not (prof_vect_x[i] == prof_vect_x[i-1] or \ # round(prof_vect_z[i],4) == round(prof_vect_z[i-1],4)): if round(prof_vect_z[i], 4) != round( prof_vect_z[i - 1], 4): prof_str += (str(round(prof_vect_x[i], 4)) + " " + str(round(prof_vect_z[i], 4)) + " ") sumo.addTextElementAtIndex(wg_sk_xpath, "WingSection", prof_str, wing_sec_index) wg_sec_xpath = wg_sk_xpath + "/WingSection[" + str( wing_sec_index) + "]" sumo.addTextAttribute(wg_sec_xpath, "airfoil", prof_uid) sumo.addTextAttribute(wg_sec_xpath, "name", sec_uid) wg_sec_center_str = (str(wg_sec_center_x) + " " + str(wg_sec_center_y) + " " + str(wg_sec_center_z)) sumo.addTextAttribute(wg_sec_xpath, "center", wg_sec_center_str) sumo.addTextAttribute(wg_sec_xpath, "chord", str(wg_sec_chord)) sumo.addTextAttribute(wg_sec_xpath, "dihedral", str(wg_sec_dihed)) sumo.addTextAttribute(wg_sec_xpath, "twist", str(wg_sec_twist)) sumo.addTextAttribute(wg_sec_xpath, "yaw", str(wg_sec_yaw)) sumo.addTextAttribute(wg_sec_xpath, "napprox", "-1") sumo.addTextAttribute(wg_sec_xpath, "reversed", "false") sumo.addTextAttribute(wg_sec_xpath, "vbreak", "false") wing_sec_index += 1 # Add Wing caps add_wing_cap(sumo, wg_sk_xpath) # Engyine pylon(s) --------------------------------------------------------- inc_pylon_xpath = "/cpacs/toolspecific/CEASIOMpy/engine/includePylon" include_pylon = get_value_or_default(tixi, inc_pylon_xpath, False) if tixi.checkElement(PYLONS_XPATH) and include_pylon: pylon_cnt = tixi.getNamedChildrenCount(PYLONS_XPATH, "enginePylon") log.info(str(pylon_cnt) + " pylons has been found.") else: pylon_cnt = 0 log.warning("No pylon has been found in this CPACS file!") for i_pylon in range(pylon_cnt): pylon_xpath = PYLONS_XPATH + "/enginePylon[" + str(i_pylon + 1) + "]" pylon_uid = tixi.getTextAttribute(pylon_xpath, "uID") pylon_transf = Transformation() pylon_transf.get_cpacs_transf(tixi, pylon_xpath) # Create new wing (SUMO) Pylons will be modeled as a wings sumo.createElementAtIndex("/Assembly", "WingSkeleton", i_pylon + 1) wg_sk_xpath = "/Assembly/WingSkeleton[" + str(i_pylon + 1) + "]" sumo.addTextAttribute(wg_sk_xpath, "akimatg", "false") sumo.addTextAttribute(wg_sk_xpath, "name", pylon_uid) # Create a class for the transformation of the WingSkeleton wg_sk_tansf = Transformation() # Convert WingSkeleton rotation and add it to SUMO wg_sk_tansf.rotation = euler2fix(pylon_transf.rotation) wg_sk_rot_str = sumo_str_format( math.radians(wg_sk_tansf.rotation.x), math.radians(wg_sk_tansf.rotation.y), math.radians(wg_sk_tansf.rotation.z), ) sumo.addTextAttribute(wg_sk_xpath, "rotation", wg_sk_rot_str) # Add WingSkeleton origin wg_sk_tansf.translation = pylon_transf.translation sumo.addTextAttribute( wg_sk_xpath, "origin", sumo_str_format(wg_sk_tansf.translation.x, wg_sk_tansf.translation.y, wg_sk_tansf.translation.z), ) sumo.addTextAttribute(wg_sk_xpath, "flags", "detectwinglet") # Positionings if tixi.checkElement(pylon_xpath + "/positionings"): pos_cnt = tixi.getNamedChildrenCount(pylon_xpath + "/positionings", "positioning") log.info(str(pylon_cnt) + ' "positionning" has been found : ') pos_x_list = [] pos_y_list = [] pos_z_list = [] from_sec_list = [] to_sec_list = [] for i_pos in range(pos_cnt): pos_xpath = pylon_xpath + "/positionings/positioning[" + str( i_pos + 1) + "]" length = tixi.getDoubleElement(pos_xpath + "/length") sweep_deg = tixi.getDoubleElement(pos_xpath + "/sweepAngle") sweep = math.radians(sweep_deg) dihedral_deg = tixi.getDoubleElement(pos_xpath + "/dihedralAngle") dihedral = math.radians(dihedral_deg) # Get the corresponding translation of each positionning pos_x_list.append(length * math.sin(sweep)) pos_y_list.append(length * math.cos(dihedral) * math.cos(sweep)) pos_z_list.append(length * math.sin(dihedral) * math.cos(sweep)) # Get which section are connected by the positionning if tixi.checkElement(pos_xpath + "/fromSectionUID"): from_sec = tixi.getTextElement(pos_xpath + "/fromSectionUID") else: from_sec = "" from_sec_list.append(from_sec) if tixi.checkElement(pos_xpath + "/toSectionUID"): to_sec = tixi.getTextElement(pos_xpath + "/toSectionUID") else: to_sec = "" to_sec_list.append(to_sec) # Re-loop though the positionning to re-order them for j_pos in range(pos_cnt): if from_sec_list[j_pos] == "": prev_pos_x = 0 prev_pos_y = 0 prev_pos_z = 0 elif from_sec_list[j_pos] == to_sec_list[j_pos - 1]: prev_pos_x = pos_x_list[j_pos - 1] prev_pos_y = pos_y_list[j_pos - 1] prev_pos_z = pos_z_list[j_pos - 1] else: index_prev = to_sec_list.index(from_sec_list[j_pos]) prev_pos_x = pos_x_list[index_prev] prev_pos_y = pos_y_list[index_prev] prev_pos_z = pos_z_list[index_prev] pos_x_list[j_pos] += prev_pos_x pos_y_list[j_pos] += prev_pos_y pos_z_list[j_pos] += prev_pos_z else: log.warning('No "positionings" have been found!') pos_cnt = 0 # Sections sec_cnt = tixi.getNamedChildrenCount(pylon_xpath + "/sections", "section") log.info(" -" + str(sec_cnt) + " wing sections have been found") wing_sec_index = 1 if pos_cnt == 0: pos_x_list = [0.0] * sec_cnt pos_y_list = [0.0] * sec_cnt pos_z_list = [0.0] * sec_cnt check_reversed_wing = [] for i_sec in range(sec_cnt): # for i_sec in reversed(range(sec_cnt)): sec_xpath = pylon_xpath + "/sections/section[" + str(i_sec + 1) + "]" sec_uid = tixi.getTextAttribute(sec_xpath, "uID") sec_transf = Transformation() sec_transf.get_cpacs_transf(tixi, sec_xpath) # Elements elem_cnt = tixi.getNamedChildrenCount(sec_xpath + "/elements", "element") if elem_cnt > 1: log.warning("Sections " + sec_uid + " contains multiple \ element, it could be an issue for the conversion \ to SUMO!") for i_elem in range(elem_cnt): elem_xpath = sec_xpath + "/elements/element[" + str(i_elem + 1) + "]" elem_uid = tixi.getTextAttribute(elem_xpath, "uID") elem_transf = Transformation() elem_transf.get_cpacs_transf(tixi, elem_xpath) # Get pylon profile (airfoil) prof_uid = tixi.getTextElement(elem_xpath + "/airfoilUID") prof_vect_x, prof_vect_y, prof_vect_z = get_profile_coord( tixi, prof_uid) # Apply scaling for i, item in enumerate(prof_vect_x): prof_vect_x[i] = (item * elem_transf.scaling.x * sec_transf.scaling.x * pylon_transf.scaling.x) for i, item in enumerate(prof_vect_y): prof_vect_y[i] = (item * elem_transf.scaling.y * sec_transf.scaling.y * pylon_transf.scaling.y) for i, item in enumerate(prof_vect_z): prof_vect_z[i] = (item * elem_transf.scaling.z * sec_transf.scaling.z * pylon_transf.scaling.z) prof_size_x = max(prof_vect_x) - min(prof_vect_x) prof_size_y = max(prof_vect_y) - min(prof_vect_y) prof_size_z = max(prof_vect_z) - min(prof_vect_z) if prof_size_y == 0: prof_vect_x[:] = [x / prof_size_x for x in prof_vect_x] prof_vect_z[:] = [z / prof_size_x for z in prof_vect_z] # Is it correct to divide by prof_size_x ???? wg_sec_chord = prof_size_x else: log.error("An airfoil profile is not define correctly") # SUMO variable for WingSection wg_sec_center_x = (elem_transf.translation.x + sec_transf.translation.x + pos_x_list[i_sec]) * pylon_transf.scaling.x wg_sec_center_y = ( elem_transf.translation.y * sec_transf.scaling.y + sec_transf.translation.y + pos_y_list[i_sec]) * pylon_transf.scaling.y wg_sec_center_z = ( elem_transf.translation.z * sec_transf.scaling.z + sec_transf.translation.z + pos_z_list[i_sec]) * pylon_transf.scaling.z check_reversed_wing.append(wg_sec_center_y) # Add roation from element and sections # Adding the two angles: Maybe not work in every case!!! add_rotation = SimpleNamespace() add_rotation.x = elem_transf.rotation.x + sec_transf.rotation.x add_rotation.y = elem_transf.rotation.y + sec_transf.rotation.y add_rotation.z = elem_transf.rotation.z + sec_transf.rotation.z # Get Section rotation for SUMO wg_sec_rot = euler2fix(add_rotation) wg_sec_dihed = math.radians(wg_sec_rot.x) wg_sec_twist = math.radians(wg_sec_rot.y) wg_sec_yaw = math.radians(wg_sec_rot.z) # Convert point list into string prof_str = "" # Airfoil points order : should be from TE (1 0) to LE (0 0) # then TE(1 0), but not reverse way. # to avoid double zero, not accepted by SUMO for i, item in enumerate(prof_vect_x): # if not (prof_vect_x[i] == prof_vect_x[i-1] or \ # round(prof_vect_z[i],4) == round(prof_vect_z[i-1],4)): if round(prof_vect_z[i], 4) != round( prof_vect_z[i - 1], 4): prof_str += (str(round(prof_vect_x[i], 4)) + " " + str(round(prof_vect_z[i], 4)) + " ") sumo.addTextElementAtIndex(wg_sk_xpath, "WingSection", prof_str, wing_sec_index) wg_sec_xpath = wg_sk_xpath + "/WingSection[" + str( wing_sec_index) + "]" sumo.addTextAttribute(wg_sec_xpath, "airfoil", prof_uid) sumo.addTextAttribute(wg_sec_xpath, "name", sec_uid) sumo.addTextAttribute( wg_sec_xpath, "center", sumo_str_format(wg_sec_center_x, wg_sec_center_y, wg_sec_center_z), ) sumo.addTextAttribute(wg_sec_xpath, "chord", str(wg_sec_chord)) sumo.addTextAttribute(wg_sec_xpath, "dihedral", str(wg_sec_dihed)) sumo.addTextAttribute(wg_sec_xpath, "twist", str(wg_sec_twist)) sumo.addTextAttribute(wg_sec_xpath, "yaw", str(wg_sec_yaw)) sumo.addTextAttribute(wg_sec_xpath, "napprox", "-1") sumo.addTextAttribute(wg_sec_xpath, "reversed", "false") sumo.addTextAttribute(wg_sec_xpath, "vbreak", "false") wing_sec_index += 1 # Check if the wing section order must be inverted with reversed attribute if check_reversed_wing[0] < check_reversed_wing[1]: log.info("Wing section order will be reversed.") for i_sec in range(sec_cnt): wg_sec_xpath = wg_sk_xpath + "/WingSection[" + str(i_sec + 1) + "]" sumo.removeAttribute(wg_sec_xpath, "reversed") sumo.addTextAttribute(wg_sec_xpath, "reversed", "true") # If symmetry, create a mirror copy of the Pylon if tixi.checkAttribute(pylon_xpath, "symmetry"): if tixi.getTextAttribute(pylon_xpath, "symmetry") == "x-z-plane": sumo_mirror_copy(sumo, wg_sk_xpath, pylon_uid, True) add_wing_cap(sumo, wg_sk_xpath) # Engine(s) ---------------------------------------------------------------- inc_engine_xpath = "/cpacs/toolspecific/CEASIOMpy/engine/includeEngine" include_engine = get_value_or_default(tixi, inc_engine_xpath, False) if tixi.checkElement(ENGINES_XPATH) and include_engine: engine_cnt = tixi.getNamedChildrenCount(ENGINES_XPATH, "engine") log.info(str(engine_cnt) + " engines has been found.") else: engine_cnt = 0 log.warning("No engine has been found in this CPACS file!") for i_engine in range(engine_cnt): engine_xpath = ENGINES_XPATH + "/engine[" + str(i_engine + 1) + "]" engine = Engine(tixi, engine_xpath) # Nacelle (sumo) xengtransl = engine.transf.translation.x yengtransl = engine.transf.translation.y zengtransl = engine.transf.translation.z engineparts = [ engine.nacelle.fancowl, engine.nacelle.corecowl, engine.nacelle.centercowl ] for engpart in engineparts: if not engpart.isengpart: log.info("This engine part is not define.") continue if engpart.iscone: xcontours = engpart.pointlist.xlist ycontours = engpart.pointlist.ylist xengtransl += engpart.xoffset ysectransl = 0 zsectransl = 0 else: xlist = engpart.section.pointlist.xlist ylist = engpart.section.pointlist.ylist xscaling = engpart.section.transf.scaling.x zscaling = engpart.section.transf.scaling.z # Why scaling z for point in y??? CPACS mystery... xlist = [i * xscaling for i in xlist] ylist = [i * zscaling for i in ylist] # Nacelle parts contour points # In CPACS nacelles are define as the revolution of section, in SUMO they have to # be define as a body composed of section + a lip at the inlet # Find upper part of the profile xminidx = xlist.index(min(xlist)) yavg1 = sum(ylist[0:xminidx]) / (xminidx) yavg2 = sum(ylist[xminidx:-1]) / (len(ylist) - xminidx) if yavg1 > yavg2: xcontours = xlist[0:xminidx] ycontours = ylist[0:xminidx] else: xcontours = xlist[xminidx:-1] ycontours = ylist[xminidx:-1] ysectransl = engpart.section.transf.translation.y zsectransl = engpart.section.transf.translation.z # # Plot # fig, ax = plt.subplots() # ax.plot(xlist, ylist,'x') # ax.plot(xcontours, ycontours,'or') # ax.set(xlabel='x', ylabel='y',title='Engine profile') # ax.grid() # plt.show() sumo.createElementAtIndex("/Assembly", "BodySkeleton", i_fus + 1) body_xpath = "/Assembly/BodySkeleton[" + str(i_fus + 1) + "]" sumo.addTextAttribute(body_xpath, "akimatg", "false") sumo.addTextAttribute(body_xpath, "name", engpart.uid) # Add body rotation and origin sumo.addTextAttribute(body_xpath, "rotation", sumo_str_format(0, 0, 0)) sumo.addTextAttribute( body_xpath, "origin", sumo_str_format(xengtransl + ysectransl, yengtransl, zengtransl), ) # Add section for i_sec in range(len(xcontours)): namesec = "section_" + str(i_sec + 1) # Only circle profiles prof_str = " 0 -1 0.7071 -0.7071 1 0 0.7071 0.7071 0 1" sumo.addTextElementAtIndex(body_xpath, "BodyFrame", prof_str, i_sec + 1) frame_xpath = body_xpath + "/BodyFrame[" + str(i_sec + 1) + "]" diam = (ycontours[i_sec] + zsectransl) * 2 if diam < 0.01: diam = 0.01 sumo.addTextAttribute(frame_xpath, "center", sumo_str_format(xcontours[i_sec], 0, 0)) sumo.addTextAttribute(frame_xpath, "height", str(diam)) sumo.addTextAttribute(frame_xpath, "width", str(diam)) sumo.addTextAttribute(frame_xpath, "name", namesec) # Nacelle/engine options sumo_add_nacelle_lip(sumo, body_xpath) if not engpart.iscone: sumo_add_engine_bc(sumo, "Engine", engpart.uid) if engine.sym: sumo.createElementAtIndex("/Assembly", "BodySkeleton", i_fus + 1) body_xpath = "/Assembly/BodySkeleton[" + str(i_fus + 1) + "]" sumo.addTextAttribute(body_xpath, "akimatg", "false") sumo.addTextAttribute(body_xpath, "name", engpart.uid + "_sym") # Add body rotation and origin sumo.addTextAttribute(body_xpath, "rotation", sumo_str_format(0, 0, 0)) sumo.addTextAttribute( body_xpath, "origin", sumo_str_format(xengtransl + ysectransl, -yengtransl, zengtransl), ) # Add section for i_sec in range(len(xcontours)): namesec = "section_" + str(i_sec + 1) # Only circle profiles prof_str = " 0 -1 0.7071 -0.7071 1 0 0.7071 0.7071 0 1" sumo.addTextElementAtIndex(body_xpath, "BodyFrame", prof_str, i_sec + 1) frame_xpath = body_xpath + "/BodyFrame[" + str(i_sec + 1) + "]" diam = (ycontours[i_sec] + zsectransl) * 2 if diam < 0.01: diam = 0.01 sumo.addTextAttribute( frame_xpath, "center", sumo_str_format(xcontours[i_sec], 0, 0)) sumo.addTextAttribute(frame_xpath, "height", str(diam)) sumo.addTextAttribute(frame_xpath, "width", str(diam)) sumo.addTextAttribute(frame_xpath, "name", namesec) # Nacelle/Enine options sumo_add_nacelle_lip(sumo, body_xpath) if not engpart.iscone: sumo_add_engine_bc(sumo, "Engine_sym", engpart.uid + "_sym") # Get results directory results_dir = get_results_directory("CPACS2SUMO") sumo_file_path = Path(results_dir, "ToolOutput.smx") create_branch(tixi, SUMOFILE_XPATH) tixi.updateTextElement(SUMOFILE_XPATH, str(sumo_file_path)) # Save CPACS and SMX file tixi.save(cpacs_out_path) sumo.save(str(sumo_file_path))