# RCAIDE/Library/Methods/Performance/generate_cruise_drag_buildup_table.py
#
#
# Created: Feb 2026, S. Shekar
# ----------------------------------------------------------------------------------------------------------------------
# IMPORT
# ----------------------------------------------------------------------------------------------------------------------
# RCAIDE imports
from RCAIDE.Library.Plots.Common import set_axes, plot_style
from RCAIDE.Library.Plots import *
# Pacakge imports
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.patches import Patch
import os,sys
import pandas as pd
# ----------------------------------------------------------------------
# Calculate vehicle Payload Range Diagram
# ----------------------------------------------------------------------
[docs]
def generate_cruise_drag_buildup_table(mission = None, cruise_segment_tag = "cruise", save_filepath = None,
width = 11, height = 7):
if mission == None:
raise AssertionError('Mission not specifed!')
mission.tag = "cruise_drag_buildup"
results = mission.evaluate()
drag = results.segments[cruise_segment_tag].conditions.aerodynamics.coefficients.drag
eps = 1e-12
def _format_tag(tag):
return tag.replace("_", " ").title()
# --- unpack settings/geometry
vehicle = mission.segments[cruise_segment_tag].analyses.vehicle
settings = mission.segments[cruise_segment_tag].analyses.aerodynamics.settings
# --- parasite subcomponents from available keys (excluding totals and aggregates)
aggregate_keys = {"total", "nacelles", "fuselages", "wings", "booms"}
parasite_sub = []
for key in drag.parasite.keys():
if key in aggregate_keys:
continue
item = drag.parasite[key]
arr = np.asarray(item)
if arr.ndim == 2:
val = float(np.mean(arr[:, 0]))
elif arr.ndim == 1:
val = float(np.mean(arr))
elif hasattr(item, "total"):
arr = np.asarray(item.total)
if arr.ndim == 2:
arr = arr[:, 0]
val = float(np.mean(arr))
else:
val = float(np.mean(arr))
val = val * (1 - settings.drag_reduction_factors.parasite_drag)
if abs(val) > eps:
parasite_sub.append((key, val))
cd_parasite_total = float(np.mean(drag.parasite.total[:, 0]))
# --- totals (mean over cruise nodes)
cd_total = float(np.mean(drag.total[:, 0]))
cd_induced_total = float(np.mean(drag.induced.total[:, 0]))
cd_comp_total = float(np.mean(drag.compressible.total[:, 0]))
cd_misc_total = float(np.mean(drag.miscellaneous.total[:, 0]))
cd_form_total = float(np.mean(drag.form.total[:, 0]))
cd_cool_total = float(np.mean(drag.cooling.total[:, 0]))
item_vis = drag.induced["viscous"]
arr_vis = np.asarray(item_vis)
if arr_vis.ndim == 2:
val_vis = float(np.mean(arr_vis[:, 0]))
elif arr_vis.ndim == 1:
val_vis = float(np.mean(arr_vis))
elif hasattr(item_vis, "total"):
arr_vis = np.asarray(item_vis.total)
if arr_vis.ndim == 2:
arr_vis = arr_vis[:, 0]
val_vis = float(np.mean(arr_vis))
else:
val_vis = float(np.mean(arr_vis))
item_inv = drag.induced["inviscid"]
arr_inv = np.asarray(item_inv)
if arr_inv.ndim == 2:
val_inv = float(np.mean(arr_inv[:, 0]))
elif arr_inv.ndim == 1:
val_inv = float(np.mean(arr_inv))
elif hasattr(item_inv, "total"):
arr_inv = np.asarray(item_inv.total)
if arr_inv.ndim == 2:
arr_inv = arr_inv[:, 0]
val_inv = float(np.mean(arr_inv))
else:
val_inv = float(np.mean(arr_inv))
induced_sub = [
("viscous", val_vis),
("inviscid", val_inv),
]
induced_sub = [(name, val) for name, val in induced_sub if abs(val) > eps]
trim_factor = settings.trim_drag_correction_factor if hasattr(settings, 'trim_drag_correction_factor') else 1.0
categories_raw = [
("parasite", parasite_sub, cd_parasite_total),
("induced", induced_sub, cd_induced_total),
("compressible", [("total", cd_comp_total)], cd_comp_total),
("miscellaneous", [("total", cd_misc_total)], cd_misc_total),
("form", [("total", cd_form_total)], cd_form_total),
("cooling", [("total", cd_cool_total)], cd_cool_total),
("TOTAL", [("total", cd_total)], cd_total),
]
categories = []
for cat, subs, tot in categories_raw:
filtered_subs = [(name, val) for name, val in subs if abs(val) > eps]
if abs(tot) > eps or cat == "TOTAL":
categories.append((cat, filtered_subs, tot))
# -------------------------
# Terminal print
# -------------------------
print("\nCruise Drag Buildup (mean over segment)")
print("-" * 72)
for cat, subs, tot in categories:
print(f"{cat:14s} total = {tot: .6e}")
for (name, val) in subs:
if name != "total":
print(f" - {name:28s} {val: .6e}")
print("-" * 72)
cd_buildup_sum = trim_factor * (cd_parasite_total + cd_induced_total + cd_comp_total + cd_misc_total + cd_form_total + cd_cool_total)
if abs(cd_total - cd_buildup_sum) > 1e-6:
print(f" Note: aero solver total ({cd_total: .6e}) differs from buildup sum ({cd_buildup_sum: .6e}) by {abs(cd_total - cd_buildup_sum):.6e}")
# -------------------------
# DataFrame for Excel
# -------------------------
rows = []
for cat, subs, tot in categories:
for (name, val) in subs:
if name == "total":
continue
rows.append({"category": cat, "component": name, "CD": float(val)})
rows.append({"category": cat, "component": "total", "CD": float(tot)})
df = pd.DataFrame(rows)
# -------------------------
# Plot (stacked bars, hatched subcomponents)
# -------------------------
ps = plot_style()
plt.rcParams.update({
"axes.labelsize": ps.axis_font_size + 4,
"xtick.labelsize": ps.axis_font_size + 4,
"ytick.labelsize": ps.axis_font_size + 4,
"axes.titlesize": ps.title_font_size,
})
fig, ax = plt.subplots(figsize=(width, height))
hatch_list = ["///", "\\\\\\", "xx", "..", "++", "--", "oo", "**", "||", "//"]
cat_colors = plt.cm.tab10(np.linspace(0, 1, max(len(categories), 1)))
cat_to_idx = {cat: i for i, (cat, _, _) in enumerate(categories)}
for i, (cat, subs, tot) in enumerate(categories):
bottom = 0.0
base_color = cat_colors[i]
# stack subcomponents (if any), each with a different hatch
if len(subs) > 1 or (len(subs) == 1 and subs[0][0] != "total"):
for j, (name, val) in enumerate(subs):
ax.barh(i, val, height=0.7, left=bottom,
color=base_color, alpha=0.65,
hatch=hatch_list[j % len(hatch_list)], edgecolor="k")
bottom += val
else:
bar_val = subs[0][1] if len(subs) > 0 else tot
ax.barh(i, bar_val, height=0.7, color=base_color, alpha=0.65,
hatch=hatch_list[0], edgecolor="k")
# outline to the category total
ax.barh(i, tot, height=0.7, fill=False, edgecolor=base_color, linewidth=2.0)
ax.set_yticks(np.arange(len(categories)))
ax.set_yticklabels([c[0].capitalize() for c in categories])
ax.set_xlabel(r"c$_D$")
ax.set_title("Cruise Drag Buildup")
parasite_color = cat_colors[cat_to_idx["parasite"]] if "parasite" in cat_to_idx else "0.85"
induced_color = cat_colors[cat_to_idx["induced"]] if "induced" in cat_to_idx else "0.85"
parasite_handles = [
Patch(
facecolor=parasite_color,
edgecolor="k",
alpha=0.65,
hatch=hatch_list[i % len(hatch_list)],
label=_format_tag(name),
)
for i, (name, val) in enumerate(parasite_sub) if abs(val) > eps
]
induced_handles = [
Patch(
facecolor=induced_color,
edgecolor="k",
alpha=0.65,
hatch=hatch_list[i % len(hatch_list)],
label=_format_tag(name),
)
for i, (name, val) in enumerate(induced_sub) if abs(val) > eps
]
if parasite_handles:
leg1 = ax.legend(
handles=parasite_handles,
title="Parasite subcomponents",
loc="lower right",
bbox_to_anchor=(0.99, 0.02),
fontsize=14,
)
ax.add_artist(leg1)
if induced_handles:
ax.legend(
handles=induced_handles,
title="Induced subcomponents",
loc="lower right",
bbox_to_anchor=(0.99, 0.42),
fontsize=14,
)
set_axes(ax)
plt.tight_layout()
fig.tight_layout()
# -------------------------
# Pie chart (major component contribution to total drag)
# -------------------------
pie_entries = [
("Parasite", cd_parasite_total),
("Induced", cd_induced_total),
("Compressible", cd_comp_total),
("Miscellaneous", cd_misc_total),
("Form", cd_form_total),
("Cooling", cd_cool_total),
]
pie_entries = [(label, val) for (label, val) in pie_entries if abs(val) > eps]
pie_labels = [x[0] for x in pie_entries]
pie_values = [x[1] for x in pie_entries]
pie_colors = [cat_colors[cat_to_idx[label.lower()]] for label in pie_labels if label.lower() in cat_to_idx]
if len(pie_colors) != len(pie_labels):
pie_colors = plt.cm.tab10(np.linspace(0, 1, len(pie_labels)))
fig_pie, ax_pie = plt.subplots(figsize=(7, 6))
ax_pie.pie(
pie_values,
labels=pie_labels,
autopct="%1.1f%%",
startangle=90,
colors=pie_colors,
pctdistance=0.78,
labeldistance=1.06,
wedgeprops={"edgecolor": "white", "linewidth": 1.0},
textprops={"fontsize": 12},
)
# ax_pie.set_title("Main Drag Component Contribution (%)")
ax_pie.axis("equal")
fig_pie.tight_layout()
# -------------------------
# Separate parasite zoom figure
# -------------------------
fig_parasite_zoom = None
if len(parasite_sub) > 0:
fig_parasite_zoom, ax_pz = plt.subplots(figsize=(6, 4.8))
parasite_zoom_data = [(n, v) for (n, v) in parasite_sub if abs(v) > eps]
x_pz = np.arange(len(parasite_zoom_data))
for i, (name, val) in enumerate(parasite_zoom_data):
ax_pz.barh(
i, val,
color=parasite_color,
alpha=0.75,
edgecolor="k",
hatch=hatch_list[i % len(hatch_list)],
)
ax_pz.set_yticks(x_pz)
nice_labels = [_format_tag(n) for n, _ in parasite_zoom_data]
ax_pz.set_yticklabels(nice_labels, fontsize=13)
ax_pz.set_xlabel(r"c$_D$")
ax_pz.set_title("Parasite Drag Subcomponents")
ax_pz.grid(True, axis="x", linestyle="--", alpha=0.4)
zoom_handles = [
Patch(
facecolor=parasite_color,
edgecolor="k",
alpha=0.75,
hatch=hatch_list[i % len(hatch_list)],
label=nice_labels[i],
)
for i in range(len(parasite_zoom_data))
]
ax_pz.legend(handles=zoom_handles, title="Parasite subcomponents", loc="upper right", fontsize=14)
set_axes(ax_pz)
fig_parasite_zoom.tight_layout()
# -------------------------
# Save Excel + image (if save_filepath provided)
# -------------------------
if save_filepath is not None:
if os.path.isdir(save_filepath):
base_path = os.path.join(save_filepath, "cruise_drag_buildup")
else:
base_path, _ = os.path.splitext(save_filepath)
excel_path = f"{base_path}.xlsx"
img_path = f"{base_path}.png"
pie_img_path = f"{base_path}_pie.png"
parasite_zoom_img_path = f"{base_path}_parasite_zoom.png"
os.makedirs(os.path.dirname(excel_path) or ".", exist_ok=True)
df.to_excel(excel_path, index=False)
fig.savefig(img_path, dpi=200)
fig_pie.savefig(pie_img_path, dpi=200)
if fig_parasite_zoom is not None:
fig_parasite_zoom.savefig(parasite_zoom_img_path, dpi=200)
return df