# RCAIDE/Library/Plots/Geometry/generate_3d_fuel_tank_points.py
#
#
# Created: Jul 2023, M. Clarke
# Modified: Aug 2025, S. Shekar
# ----------------------------------------------------------------------------------------------------------------------
# IMPORT
# ----------------------------------------------------------------------------------------------------------------------
import RCAIDE
from RCAIDE.Framework.Core import Data
from RCAIDE.Library.Methods.Powertrain.Sources.Fuel_Tanks.Non_Integral_Tank.compute_wing_non_integral_tank_volume import compute_non_dimensional_rib_coordinates
from RCAIDE.Library.Methods.Geometry.Airfoil import import_airfoil_geometry, compute_naca_4series
# python imports
import numpy as np
from scipy.interpolate import interp1d
from shapely import Polygon
import shapely.geometry as geom
# ----------------------------------------------------------------------------------------------------------------------
# generate_integral_wing_tank_points
# ----------------------------------------------------------------------------------------------------------------------
[docs]
def generate_integral_wing_tank_points(wing, n_points, segment_list,fuel_tank,plot_centerline = False):
"""
Generates 3D coordinate points that define a wing surface.
Parameters
----------
wing : Wing
RCAIDE wing data structure containing geometry information
n_points : int
Number of points used to discretize airfoil sections
dim : int
Number of wing segments
plot_centerline : bool, optional
Include the root centerline cap section in the output, default False
Returns
-------
G : Data
Data structure containing generated points with attributes:
- X, Y, Z : ndarray
Raw coordinate points
- PTS : ndarray
Combined coordinate array
- XA1, YA1, ZA1, XA2, YA2, ZA2 : ndarray
Leading edge surface points
- XB1, YB1, ZB1, XB2, YB2, ZB2 : ndarray
Trailing edge surface points
Notes
-----
Generates wing geometry by:
1. Creating airfoil sections at specified span positions
2. Applying twist, sweep, and dihedral
3. Scaling sections by local chord
4. Positioning in aircraft coordinate system
**Definitions**
'Leading Edge Sweep'
Angle between leading edge and y-axis
'Quarter Chord Sweep'
Angle between quarter chord line and y-axis
'Dihedral'
Upward angle of wing from horizontal
"""
# unpack
# obtain the geometry for each segment in a loop
symm = wing.xz_plane_symmetric
semispan = wing.spans.projected*0.5 * (2 - symm)
root_chord = wing.chords.root
segments = wing.segments
n_segments = len(segment_list)
origin = wing.origin
pts = np.zeros((n_segments+2,n_points, 3,1))
section_twist = np.zeros((n_segments+2,n_points, 3,3))
section_twist[:, :, 0, 0] = 1
section_twist[:, :, 1, 1] = 1
section_twist[:, :, 2, 2] = 1
translation = np.zeros((n_segments+2,n_points, 3,1))
translation[:, :, 0,:] = origin[0][0]
translation[:, :, 1,:] = origin[0][1]
translation[:, :, 2,:] = origin[0][2]
for i in range(n_segments+2):
if i == 0:
current_seg = segment_list[0]
fs = fuel_tank.segments_percent_chord_start[0]
rs = fuel_tank.segments_percent_chord_end[0]
elif i == n_segments + 1:
current_seg = segment_list[-1]
fs = fuel_tank.segments_percent_chord_start[-1]
rs = fuel_tank.segments_percent_chord_end[-1]
else:
current_seg = segment_list[i-1]
fs = fuel_tank.segments_percent_chord_start[i-1]
rs = fuel_tank.segments_percent_chord_end[i-1]
airfoil = wing.segments[current_seg].airfoil
if airfoil != None:
if type(airfoil) == RCAIDE.Library.Components.Airfoils.NACA_4_Series_Airfoil:
geometry = compute_naca_4series(airfoil.NACA_4_Series_code,n_points)
elif type(airfoil) == RCAIDE.Library.Components.Airfoils.Airfoil:
geometry = import_airfoil_geometry(airfoil.coordinate_file,n_points)
else:
t_c = str(int(wing.segments[current_seg].thickness_to_chord * 100)).zfill(4)
geometry = compute_naca_4series(t_c,n_points)
twist = wing.segments[current_seg].twist
is_end_cap = (i == 0 or i == n_segments + 1)
if wing.vertical:
# trim airfoil geometry to fuel tank segment start and end locations but setting points outside the segment to the segment start/end locations to maintain closed section
x_coordinates = geometry.x_coordinates
x_coordinates[x_coordinates <= fs] = fs
x_coordinates[x_coordinates >= rs] = rs
y_coordinates = geometry.y_coordinates
pts[i,:,1,0] = (y_coordinates + y_coordinates[::-1]) / 2 if is_end_cap else y_coordinates * wing.segments[current_seg].root_chord_percent * wing.chords.root
pts[i,:,2,0] = np.zeros_like(geometry.y_coordinates)
section_twist[i,:,0,0] = np.cos(twist)
section_twist[i,:,0,1] = -np.sin(twist)
section_twist[i,:,1,0] = np.sin(twist)
section_twist[i,:,1,1] = np.cos(twist)
else:
# trim airfoil geometry to fuel tank segment start and end locations but setting points outside the segment to the segment start/end locations to maintain closed section
x_coordinates = geometry.x_coordinates
x_coordinates[x_coordinates <= fs] = fs
x_coordinates[x_coordinates >= rs] = rs
y_coordinates = geometry.y_coordinates
pts[i,:,0,0] = x_coordinates * wing.segments[current_seg].root_chord_percent * wing.chords.root
pts[i,:,1,0] = np.zeros_like(geometry.y_coordinates)
pts[i,:,2,0] = (y_coordinates + y_coordinates[::-1]) / 2 if is_end_cap else y_coordinates * wing.segments[current_seg].root_chord_percent * wing.chords.root
section_twist[i,:,0,0] = np.cos(twist)
section_twist[i,:,0,2] = np.sin(twist)
section_twist[i,:,2,0] = -np.sin(twist)
section_twist[i,:,2,2] = np.cos(twist)
translation[i, :, 0,:] += segments[current_seg].origin[0][0]
translation[i, :, 1,:] += segments[current_seg].origin[0][1]
translation[i, :, 2,:] += segments[current_seg].origin[0][2]
mat = translation + np.matmul(section_twist ,pts)
if not plot_centerline:
mat = mat[1:, :, :, :]
# ---------------------------------------------------------------------------------------------
# create empty data structure for storing geometry
G = Data()
# store node points
G.X = mat[:,:,0,0]
G.Y = mat[:,:,1,0]
G.Z = mat[:,:,2,0]
G.PTS = mat[:,:,:,0]
# store points
G.XA1 = mat[:-1,:-1,0,0]
G.YA1 = mat[:-1,:-1,1,0]
G.ZA1 = mat[:-1,:-1,2,0]
G.XA2 = mat[:-1,1:,0,0]
G.YA2 = mat[:-1,1:,1,0]
G.ZA2 = mat[:-1,1:,2,0]
G.XB1 = mat[1:,:-1,0,0]
G.YB1 = mat[1:,:-1,1,0]
G.ZB1 = mat[1:,:-1,2,0]
G.XB2 = mat[1:,1:,0,0]
G.YB2 = mat[1:,1:,1,0]
G.ZB2 = mat[1:,1:,2,0]
return G
[docs]
def generate_integral_fuel_tank_points(fuselage,fuel_tank, segment_list, tessellation = 24):
"""
Generates 3D coordinate points that define a fuel_tank surface.
Parameters
----------
fuel_tank : fuel_tank
RCAIDE fuel_tank data structure containing geometry information
tessellation : int, optional
Number of points to use in circumferential discretization (default: 24)
Returns
-------
G : Data
Data structure containing generated points
- PTS : ndarray
Array of shape (num_segments, tessellation, 3) containing
x,y,z coordinates of surface points
Notes
-----
Points are generated by creating super-elliptical cross-sections at each segment
and positioning them according to segment locations.
**Major Assumptions**
* Cross-sections lie in y-z plane
* Segments are ordered from nose to tail
* Origin is at the nose of the fuel_tank
"""
tank_segs = fuselage.segments
num_tank_segs = len(segment_list)
if segment_list[0] == None or segment_list[1] == None:
raise Exception('Tank segments must be defined')
fuel_tank_points = np.zeros((num_tank_segs+2,tessellation ,3))
if num_tank_segs > 0:
# first segment
segment_start = tank_segs[segment_list[0]]
a = 1E-6
b = 1E-6
n = segment_start.curvature
theta = np.linspace(0,2*np.pi,tessellation)
tank_ypts = (abs((np.cos(theta)))**(2/n))*a * ((np.cos(theta)>0)*1 - (np.cos(theta)<0)*1)
tank_zpts = (abs((np.sin(theta)))**(2/n))*b * ((np.sin(theta)>0)*1 - (np.sin(theta)<0)*1)
fuel_tank_points[0,:,0] = segment_start.percent_x_location*fuselage.lengths.total + fuselage.origin[0][0]
fuel_tank_points[0,:,1] = tank_ypts + segment_start.percent_y_location*fuselage.lengths.total + fuselage.origin[0][1]
fuel_tank_points[0,:,2] = tank_zpts + segment_start.percent_z_location*fuselage.lengths.total + fuselage.origin[0][2]
for i in range(num_tank_segs):
segment = tank_segs[segment_list[i]]
a = segment.width/2
b = segment.height/2
n = segment.curvature
theta = np.linspace(0,2*np.pi,tessellation)
tank_ypts = (abs((np.cos(theta)))**(2/n))*a * ((np.cos(theta)>0)*1 - (np.cos(theta)<0)*1)
tank_zpts = (abs((np.sin(theta)))**(2/n))*b * ((np.sin(theta)>0)*1 - (np.sin(theta)<0)*1)
fuel_tank_points[i+1,:,0] = segment.percent_x_location*fuselage.lengths.total + fuselage.origin[0][0]
fuel_tank_points[i+1,:,1] = tank_ypts + segment.percent_y_location*fuselage.lengths.total + fuselage.origin[0][1]
fuel_tank_points[i+1,:,2] = tank_zpts + segment.percent_z_location*fuselage.lengths.total + fuselage.origin[0][2]
# last segment
segment_start = tank_segs[segment_list[-1]]
a = 1E-6
b = 1E-6
n = segment_start.curvature
theta = np.linspace(0,2*np.pi,tessellation)
tank_ypts = (abs((np.cos(theta)))**(2/n))*a * ((np.cos(theta)>0)*1 - (np.cos(theta)<0)*1)
tank_zpts = (abs((np.sin(theta)))**(2/n))*b * ((np.sin(theta)>0)*1 - (np.sin(theta)<0)*1)
fuel_tank_points[-1,:,0] = segment_start.percent_x_location*fuselage.lengths.total + fuselage.origin[0][0]
fuel_tank_points[-1,:,1] = tank_ypts + segment_start.percent_y_location*fuselage.lengths.total + fuselage.origin[0][1]
fuel_tank_points[-1,:,2] = tank_zpts + segment_start.percent_z_location*fuselage.lengths.total + fuselage.origin[0][2]
G = Data()
G.PTS = fuel_tank_points
return G
[docs]
def generate_non_integral_fuel_tank_points(fuel_tank, tessellation = 24):
"""
Generates 3D coordinate points that define a fuel_tank surface.
Parameters
----------
fuel_tank : fuel_tank
RCAIDE fuel_tank data structure containing geometry information
tessellation : int, optional
Number of points to use in circumferential discretization (default: 24)
Returns
-------
G : Data
Data structure containing generated points
- PTS : ndarray
Array of shape (num_segments, tessellation, 3) containing
x,y,z coordinates of surface points
Notes
-----
Points are generated by creating super-elliptical cross-sections at each segment
and positioning them according to segment locations.
**Major Assumptions**
* Cross-sections lie in y-z plane
* Segments are ordered from nose to tail
* Origin is at the nose of the fuel_tank
"""
N = 9
fuel_tank_points = np.zeros((2*N,tessellation ,3))
R = fuel_tank.diameters.external / 2
L_total = fuel_tank.lengths.external # tip-to-tip
L_cyl = L_total - 2 * R # cylinder-only
# front segments
front_angles = np.linspace(0, np.pi/2,N)
for i in range(len(front_angles)):
a = np.sin(front_angles[i]) * R
b = np.sin(front_angles[i]) * R
n = 2
theta = np.linspace(0,2*np.pi,tessellation)
tank_ypts = (abs((np.cos(theta)))**(2/n))*a * ((np.cos(theta)>0)*1 - (np.cos(theta)<0)*1)
tank_zpts = (abs((np.sin(theta)))**(2/n))*b * ((np.sin(theta)>0)*1 - (np.sin(theta)<0)*1)
fuel_tank_points[i,:,0] = R * (1 - np.cos(front_angles[i]))
fuel_tank_points[i,:,1] = tank_ypts
fuel_tank_points[i,:,2] = tank_zpts
# rear angles
rear_angles = np.linspace(np.pi/2,0,N)
for j in range(len(rear_angles)):
a = np.sin(rear_angles[j]) *R
b = np.sin(rear_angles[j]) *R
n = 2
theta = np.linspace(0,2*np.pi,tessellation)
tank_ypts = (abs((np.cos(theta)))**(2/n))*a * ((np.cos(theta)>0)*1 - (np.cos(theta)<0)*1)
tank_zpts = (abs((np.sin(theta)))**(2/n))*b * ((np.sin(theta)>0)*1 - (np.sin(theta)<0)*1)
fuel_tank_points[i+1+j,:,0] = R * np.cos(rear_angles[j]) + R + L_cyl
fuel_tank_points[i+1+j,:,1] = tank_ypts
fuel_tank_points[i+1+j,:,2] = tank_zpts
x_rotation = np.zeros(( 3, 3))
x_rotation[0,0] = 1
x_rotation[1,1] = np.cos(fuel_tank.orientation_euler_angles[0])
x_rotation[1,2] = -np.sin(fuel_tank.orientation_euler_angles[0])
x_rotation[2,1] = np.sin(fuel_tank.orientation_euler_angles[0])
x_rotation[2,2] = np.cos(fuel_tank.orientation_euler_angles[0])
y_rotation = np.zeros((3, 3))
y_rotation[0,0] = np.cos(fuel_tank.orientation_euler_angles[1])
y_rotation[0,2] = np.sin(fuel_tank.orientation_euler_angles[1])
y_rotation[1,1] = 1
y_rotation[2,0] = -np.sin(fuel_tank.orientation_euler_angles[1])
y_rotation[2,2] = np.cos(fuel_tank.orientation_euler_angles[1])
z_rotation = np.zeros(( 3, 3))
z_rotation[0,0] = np.cos(fuel_tank.orientation_euler_angles[2])
z_rotation[0,1] = -np.sin(fuel_tank.orientation_euler_angles[2])
z_rotation[1,0] = np.sin(fuel_tank.orientation_euler_angles[2])
z_rotation[1,1] = np.cos(fuel_tank.orientation_euler_angles[2])
z_rotation[2,2] = 1
R_total = z_rotation @ y_rotation @ x_rotation
fuel_tank_points = fuel_tank_points @ R_total.T
# translate to location on aircraft
if np.allclose(fuel_tank.orientation_euler_angles, [0., 0., np.pi/2]):
fuel_tank_points[:, :, 0] += fuel_tank.origin[0][0] + fuel_tank.diameters.external/2
fuel_tank_points[:, :, 1] += fuel_tank.origin[0][1] - L_total / 2
fuel_tank_points[:, :, 2] += fuel_tank.origin[0][2]
else:
fuel_tank_points[:, :, 0] += fuel_tank.origin[0][0]
fuel_tank_points[:, :, 1] += fuel_tank.origin[0][1]
fuel_tank_points[:, :, 2] += fuel_tank.origin[0][2]
if hasattr(fuel_tank, "wing_root_twist"):
# do one last rotation for root twist of wing
wing_root_rotation = np.zeros((3, 3))
wing_root_rotation[0,0] = np.cos(fuel_tank.wing_root_twist)
wing_root_rotation[0,2] = np.sin(fuel_tank.wing_root_twist)
wing_root_rotation[1,1] = 1
wing_root_rotation[2,0] = -np.sin(fuel_tank.wing_root_twist)
wing_root_rotation[2,2] = np.cos(fuel_tank.wing_root_twist)
fuel_tank_points = fuel_tank_points @ wing_root_rotation.T
G= Data()
G.PTS = fuel_tank_points
return G
[docs]
def transverse_tank_chord_bounds(fuel_tank, tessalation = 24):
"""
Returns aft tank root chord bounds used by aft BWB tank generators.
"""
return getattr(fuel_tank, "transverse_tank_chord_bounds", None)
[docs]
def generate_aft_integral_wing_tank_points(wing, n_points, segment_list, fuel_tank):
"""
Generates 3D points for a BWB aft conformal integral tank.
This mirrors the polygon-intersection logic used by
compute_bwb_aft_integral_prismatic_tank_volume so plotting geometry
matches volume geometry.
"""
if segment_list is None or len(segment_list) < 2:
raise ValueError("segment_list must contain [start_percent, end_percent].")
if any(val is None for val in [
segment_list[0],
segment_list[1],
fuel_tank.transverse_tank_segment_bound
]):
raise ValueError("Aft tank bounds and segment bound must be defined.")
root_chord = wing.chords.root
wing_span = wing.spans.projected
tank_start_percent = segment_list[0]
tank_end_percent = segment_list[1]
segments = wing.segments
seg_tags = list(segments.keys())
index = seg_tags.index(fuel_tank.transverse_tank_segment_bound)
seg_names = seg_tags[:index + 1]
num_tank_sections = len(seg_names)
wing_segment_origins = np.array([segments[tag].origin[0] for tag in seg_names])
x_tank_bounds = np.linspace(tank_start_percent * root_chord, tank_end_percent * root_chord, 2)
polygon_points = []
for seg_i, seg_name in enumerate(seg_names):
segment = segments[seg_name]
if seg_i == 0:
fuel_tank.wing_root_twist = segment.twist
if segment.airfoil is not None:
if type(segment.airfoil) == RCAIDE.Library.Components.Airfoils.NACA_4_Series_Airfoil:
geometry = compute_naca_4series(segment.airfoil.NACA_4_Series_code)
elif type(segment.airfoil) == RCAIDE.Library.Components.Airfoils.Airfoil:
geometry = import_airfoil_geometry(segment.airfoil.coordinate_file)
else:
geometry = compute_naca_4series('0012')
else:
geometry = compute_naca_4series('0012')
segment_chord = segment.root_chord_percent * root_chord
x_points_upper = segment_chord * geometry.x_upper_surface + wing_segment_origins[seg_i][0]
x_points_lower = segment_chord * geometry.x_lower_surface + wing_segment_origins[seg_i][0]
z_points_upper = segment_chord * geometry.y_upper_surface + wing_segment_origins[seg_i][2] - fuel_tank.wall_clearance
z_points_lower = segment_chord * geometry.y_lower_surface + wing_segment_origins[seg_i][2] + fuel_tank.wall_clearance
upper_fn = interp1d(x_points_upper, z_points_upper, kind='linear')
lower_fn = interp1d(x_points_lower, z_points_lower, kind='linear')
upper_raw = upper_fn(x_tank_bounds)
lower_raw = lower_fn(x_tank_bounds)
# Reflexed airfoils can have upper_z < lower_z near the trailing edge;
# clamp so the polygon is always non-self-intersecting.
upper_z = np.maximum(upper_raw, lower_raw)
lower_z = np.minimum(upper_raw, lower_raw)
polygon = [
(x_tank_bounds[0], upper_z[0]),
(x_tank_bounds[1], upper_z[1]),
(x_tank_bounds[1], lower_z[1]),
(x_tank_bounds[0], lower_z[0]),
(x_tank_bounds[0], upper_z[0]),
]
polygon_points.append(polygon)
tank_volumes = np.zeros(num_tank_sections - 1)
tank_lengths = np.zeros(num_tank_sections - 1)
intersection_polygons = []
for seg_i in range(1, num_tank_sections):
if seg_i == 1:
inner_polygon = Polygon(polygon_points[seg_i - 1]).buffer(0)
else:
inner_polygon = intersection_polygon
outer_polygon = Polygon(polygon_points[seg_i]).buffer(0)
intersection_polygon = inner_polygon.intersection(outer_polygon)
if intersection_polygon.is_empty:
tank_volumes[seg_i - 1] = 0.0
tank_lengths[seg_i - 1] = 0.0
intersection_polygons.append(None)
continue
if not isinstance(intersection_polygon, geom.Polygon):
polys = [g for g in getattr(intersection_polygon, 'geoms', []) if isinstance(g, geom.Polygon)]
intersection_polygon = max(polys, key=lambda g: g.area) if polys else None
if intersection_polygon is None:
tank_volumes[seg_i - 1] = 0.0
tank_lengths[seg_i - 1] = 0.0
intersection_polygons.append(None)
continue
area = intersection_polygon.area
y_curr = segments[seg_names[seg_i]].percent_span_location * wing_span
y_prev = segments[seg_names[seg_i - 1]].percent_span_location * wing_span
span_length = y_curr - y_prev
tank_volumes[seg_i - 1] = area * span_length
tank_lengths[seg_i - 1] = span_length
intersection_polygons.append(intersection_polygon)
if np.all(tank_volumes <= 0.0):
raise AttributeError("No valid intersection polygon found for aft tank geometry.")
max_idx = int(np.argmax(tank_volumes))
best_polygon = intersection_polygons[max_idx]
if best_polygon is None:
raise AttributeError("No valid intersection polygon found for aft tank geometry.")
polygon_for_plot = best_polygon
if polygon_for_plot.geom_type == 'MultiPolygon':
polygon_for_plot = max(polygon_for_plot.geoms, key=lambda g: g.area)
coords = list(polygon_for_plot.exterior.coords)
if np.allclose(coords[0], coords[-1]):
coords = coords[:-1]
if len(coords) != 4:
# Robust fallback for slightly over-resolved intersections.
rect = polygon_for_plot.minimum_rotated_rectangle
coords = list(rect.exterior.coords)[:-1]
coords_np = np.asarray(coords, dtype=float)
center = np.mean(coords_np, axis=0)
angles = np.arctan2(coords_np[:, 1] - center[1], coords_np[:, 0] - center[0])
coords_np = coords_np[np.argsort(angles)]
coords_np = np.vstack([coords_np, coords_np[0]])
span_length = float(tank_lengths[max_idx])
y0 = -0.5 * span_length
y1 = 0.5 * span_length
section_0 = np.column_stack((coords_np[:, 0], np.full(coords_np.shape[0], y0), coords_np[:, 1]))
section_1 = np.column_stack((coords_np[:, 0], np.full(coords_np.shape[0], y1), coords_np[:, 1]))
tank_points = np.stack((section_0, section_1), axis=0)
if hasattr(fuel_tank, "wing_root_twist"):
wing_root_rotation = np.zeros((3, 3))
wing_root_rotation[0, 0] = np.cos(fuel_tank.wing_root_twist)
wing_root_rotation[0, 2] = np.sin(fuel_tank.wing_root_twist)
wing_root_rotation[1, 1] = 1
wing_root_rotation[2, 0] = -np.sin(fuel_tank.wing_root_twist)
wing_root_rotation[2, 2] = np.cos(fuel_tank.wing_root_twist)
tank_points = tank_points @ wing_root_rotation.T
G = Data()
G.PTS = tank_points
return G