Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
10b08ee
Adding medium_transport example.
Czarified Sep 5, 2025
32e6935
Adding a couple function arguments.
Czarified Sep 19, 2025
b6273ef
Expanding the Medium Transport Example.
Czarified Sep 19, 2025
4275bfd
Functional 1.5g Taxi VMT
Czarified Apr 7, 2026
e0e8220
Some major work on the example script.
Czarified May 16, 2026
d9e765d
Set the cut_geometry to provide mean values for input stations.
Czarified May 16, 2026
4adae84
Adding new LoadCase class.
Czarified May 23, 2026
6df97ec
Adding the search and shell sizing routines.
Czarified May 23, 2026
6bbe301
Working on programmatic synthesis still.
Czarified May 25, 2026
9750a2e
Snapshot.
Czarified Jun 27, 2026
dda93a8
Functional Fuselage Synthesis!
Czarified Jun 27, 2026
d3e7cad
Debugging.
Czarified Jul 7, 2026
bcce798
MinorFrame Weight.
Czarified Jul 7, 2026
9efe19d
Adding skin min_gauge.
Czarified Jul 7, 2026
bdf2ad5
Cleaning up some docstrings and variable names.
Czarified Jul 25, 2026
e0385ad
Bumping version.
Czarified Jul 25, 2026
912ab7d
Forgot to comment out the mypy workflow on Ubuntu.
Czarified Jul 25, 2026
22e6769
Merge branch 'main' into fuselage_synthesis
Czarified Jul 25, 2026
00df3fc
Fixing some safety dependencies.
Czarified Aug 15, 2026
7e174b4
Merge branch 'fuselage_synthesis' of github.com:czarified/hyperstruct…
Czarified Aug 15, 2026
32eb036
Fixing black formatting.
Czarified Aug 15, 2026
d386b4d
Trying to stop black and isort from fighting.
Czarified Aug 21, 2026
865379b
Still trying to fix black and isort...
Czarified Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]
select = B,B9,C,D,DAR,E,F,N,RST,S,W
ignore = E203,E501,RST201,RST203,RST301,W503,N803,N806
ignore = E203,E501,RST201,RST203,RST301,W503,N803,N806,E741
max-line-length = 120
max-complexity = 13
docstring-convention = google
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
include:
- { python: "3.12", os: "ubuntu-latest", session: "pre-commit" }
# - { python: "3.12", os: "ubuntu-latest", session: "safety" }
- { python: "3.12", os: "ubuntu-latest", session: "mypy" }
# - { python: "3.12", os: "ubuntu-latest", session: "mypy" }
# - { python: "3.11", os: "ubuntu-latest", session: "mypy" }
# - { python: "3.10", os: "ubuntu-latest", session: "mypy" }
- { python: "3.12", os: "ubuntu-latest", session: "tests" }
Expand Down
7 changes: 7 additions & 0 deletions .safety-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Safety CLI policy configuration file
# For details, see: https://docs.safetycli.com

scanning-settings:
exclude:
- ".poetry/**"
- ".nox/**"
332 changes: 332 additions & 0 deletions examples/medium_transport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
"""This script builds components for an example Medium Transport aircraft.

All the values in this file are taken from publicly available data
on the Lockheed C-130J-30. However, instead of matching the fuselage shape
exactly, a rounded rectangle will be used.

Assumptions are made for material properties, and material data taken
from either SWEEP documentation or Matweb.

Unit system is lbf, in, s

References:
https://en.wikipedia.org/wiki/Lockheed_C-130_Hercules
https://man.fas.org/dod-101/sys/ac/c-130.htm
https://www.lockheedmartin.com/content/dam/lockheed-martin/aero/documents/sustainment/csc/service-news/sn-mag-v1-v10/V2N1.pdf
"""

import matplotlib.pyplot as plt
import numpy as np
from rich import print

from hyperstruct import LoadCase
from hyperstruct import Material
from hyperstruct import Station
from hyperstruct import composite_cg

# from hyperstruct.fuselage import Cover
# from hyperstruct.fuselage import ForcedCrippling
from hyperstruct.fuselage import Cover
from hyperstruct.fuselage import Fuselage
from hyperstruct.fuselage import Longeron
from hyperstruct.fuselage import MajorFrame
from hyperstruct.fuselage import MinorFrame

# from hyperstruct.fuselage import MinorFrame


# Some global variables for reference
FUSELAGE_LENGTH = 12 * (97 + 15) + 9
# Ignore the floor break transitions, since we're not matching the exact shape
# we'll turn this into a rounded rectangle of equivalent length and width.
FUSELAGE_DIAMETER = 12 * 14.17
CENTER_FUSE_Z_REF = 12 * 7 + 20
# A dict of FS locations where Major Frames exist and shapes change
_w, _d, _z, _r = [
FUSELAGE_DIAMETER,
FUSELAGE_DIAMETER,
CENTER_FUSE_Z_REF,
0.45 * FUSELAGE_DIAMETER,
]

FS_DICT = {
93: [
"Nose Station, and NLG Bay Start",
12 * 2 + 10,
12 * 3 + 2,
0.5 * CENTER_FUSE_Z_REF,
13,
],
165: ["FWD Fuse, and NLG Bay End", _w, _d, _z, _r],
245: ["FWD Fuse Mate", _w, _d, _z, _r],
497: ["MLG Bay Start, Wing Fwd Spar", _w, _d, _z, _r],
620: ["MLG Bay End, Wing Aft Spar", _w, _d, _z, _r],
737: ["Empennage Mate", _w, 0.9 * _d, _z + 0.1 * _d, _r],
941: ["Ramp Cutout End", _w, 0.5 * _d, _z + 0.5 * _d, 0.5 * _r],
1071: [
"Vert and Horz Stabilizer Fwd Spar",
0.7 * _w,
0.3 * _d,
_z + 0.7 * _d,
0.3 * _r,
],
1200: ["Tail Station", 0.5 * _w, 0.2 * _d, _z + 0.7 * _d, 0.2 * _r],
}
#

# Build the stations geometry
stations = []
for fs, values in FS_DICT.items():
name, width, depth, z_ref, radius = values
stations.append(
Station(
orientation="FS",
name=name,
number=fs,
width=width,
depth=depth,
vertical_centroid=z_ref,
radius=radius,
)
)

# Plot the geometry
fig, axs = plt.subplots(
nrows=3,
ncols=3,
figsize=(9, 9),
layout="constrained",
gridspec_kw=dict(hspace=0, wspace=0),
sharex="col",
sharey="row",
subplot_kw=dict(box_aspect=1),
)
axs = axs.flatten()
square_side = 1.1 * _d + _z
xlim = (-square_side / 2, square_side / 2)
ylim = (0, square_side)
# print(f"x-axis width = {xlim[1]-xlim[0]:.2f}")
# print(f"y-axis depth = {ylim[1]-ylim[0]:.2f}")

for i, station in enumerate(stations):
station.show(display=False, axes=axs[i], xlim=xlim, ylim=ylim)

for ax in fig.get_axes():
ax.label_outer()

fig.suptitle(
"Station Diagrams for Generic Medium Transport", fontfamily="serif", fontsize=16
)
# plt.show()


# An initial estimate at weight distribution based on CG target
target_weight = 73000
point_weights = np.array(
[
# Weight , FS
[-1200, 93],
[-6500, 165],
[-9000, 245],
[-15000, 497],
[-16000, 620],
[-11000, 737],
[-7600, 941],
[-4200, 1071],
[-2500, 1200],
]
)
_w, _cg = composite_cg(point_weights)
print(f"Target Weight = {target_weight:d} [lbs]")
print(f" Total Weight = {_w:d} [lbs]")
print(f" CG = {_cg:.2f} [in]")


#
# Landing Gear Loads Calculation
# Taxi, WC=73k, 3.0g
#
FNZ0 = 3.0
XNGG = 165
XMGG = 620
XCG = _cg
DGW = target_weight
Rmg = FNZ0 * DGW * (XCG - XNGG) / (XMGG - XNGG)
Rng = FNZ0 * DGW - Rmg

# Sink Speed is 10 ft/s, convert to in/s
# SSPD = 12 * 10.0
# Shock Strut Stroke
# STKE = 26
# g = 386 # in/s2

# Assume no Airloads for now
PZN = 0 # Forebody lift
XCPN = 170 # Forebody Center of pressure
PZWB = 0 # Wing outer panel lift
XCPW = 620 # Coord of outer wing panel
PZBW = 0 # Body lift in presence of wing
XCPB = 600 # Coord of of body lift
PZH = 0 # Horz Tail Lift
XCPH = 1200 # Coord of hTail lift

# Vehicle Pitch Inertia, simplified. See pg 56 fo ADA002867
TIYY = np.sum(point_weights[:, 0] * (XCG - point_weights[:, 1]) ** 2)
print(f"Vehicle pitch inertia = {TIYY:.3e}")

print(f"\nLoads for {FNZ0}g Taxi:")
print(25 * "-")
print(f" Rmg = {Rmg:.3e} [lbs]")
print(f" Rng = {Rng:.3e} [lbs]")
print(f"FNZO = {FNZ0:.2f} [g]")
diff = FNZ0 * DGW - Rmg - Rng
print(
f"Balance of Vertical Forces: {FNZ0:.1f}*{DGW:.2e} - {Rmg:.2e} - {Rng:.2e} = {diff:.1f}\n"
)


#
# Turn the Stations and Loads into Frames
#

# A basic aluminum material
# 2024-T3, Sheet, A-basis
al2024 = Material(
rho=0.1,
E=10.5e6,
E_c=10.6e6,
nu=0.33,
F_tu=64e3,
F_ty=42.1e3,
F_cy=48.3e3,
F_su=41.0e3,
F_bru=10.04e3,
F_bry=89.0e3,
F_en=20.0e3,
db_r=116,
)


inertia_loads = FNZ0 * point_weights[:, 0].flatten()

gear_loads = [
# y, z, V, H, M
np.array(
[ # FS 165
[20.0, 17.0, Rng / 2, 0.0, 0.0], # NLG LH Mount
[-20.0, 17.0, Rng / 2, 0.0, 0.0], # NLG RH Mount
]
),
np.array(
[ # FS 620
[FUSELAGE_DIAMETER / 2, 47.0, Rmg / 2, 0.0, 0.0], # MLG LH Mount
[-FUSELAGE_DIAMETER / 2, 47.0, Rmg / 2, 0.0, 0.0], # MLG RH Mount
]
),
]

frames = {}
for station in stations:
if station.number == 165:
load = gear_loads[0]
elif station.number == 620:
load = gear_loads[1]
else:
load = np.zeros((2, 5))

frames[station.number] = MajorFrame(
material=al2024,
fs_loc=station.number,
loads=load,
geom=station,
fd=6.0,
)

nlg_frame = frames.pop(165)
mlg_frame = frames.pop(620)
# Don't need all the frames since only 2 of them have loads
frames = (nlg_frame, mlg_frame)
fig1, ax1 = nlg_frame.show()
fig2, ax2 = mlg_frame.show()


#
# Compile the frames and stations into a fuselage
#
w_fus = np.column_stack(
(point_weights[:, 1], FNZ0 * point_weights[:, 0], np.zeros((len(point_weights),)))
)
w_fc = np.zeros(3)
p_air = np.zeros(3)
ext_loads = [arr[:, 2].sum() for arr in gear_loads]
p_ext = np.column_stack(
(np.array([[165], [620]]), np.transpose(ext_loads), np.zeros((2,)))
)
print("Fuselage Frame Weights:")
print(25 * "-")
print(w_fus)
print(11 * " " + f"{np.sum(w_fus[:, 1]):.2f}")
print("\nFuselage Frame Loads:")
print(25 * "-")
print(p_ext)
print(17 * " " + f"{np.sum(p_ext[:, 1]):.2f}")

cover_model = Cover(material=al2024, milled=False, L=30, D=20, R=0, RC=25)
long_model = Longeron(material=al2024, b=2.0, t_s=0.1, k=0.8)
frame_model = MinorFrame(material=al2024, c=4.0, b=3.0, construction="longeron")

fuse = Fuselage(
stations=stations,
major_frames=frames,
construction="longeron",
cover_model=cover_model,
long_model=long_model,
frame_model=frame_model,
)
loads = fuse.net_loads(w_fus, w_fc, p_air, p_ext)
fig, (ax1, ax2) = fuse.vmt_diagram(w_fus, w_fc, p_air, p_ext)
_ = fig.suptitle(f"{FNZ0:.1f}g Taxi, WC=73kip, xCG={XCG:.0f}[in]")

with np.printoptions(precision=3):
print(" FS , P , M_ext , V , M_int")
print(loads)
print("\n\n")

x, v, m = fuse.lookup_loads(x=400, loads=loads)
print(f"FS{x}: V={v / 1000:.1f}[kip], M={m:.2e}[in-lbs]")

_ = ax1.plot(x, v, marker="^", color="k")
_ = ax2.plot(x, m, marker="^", color="k", label="Analysis Point")
_ = ax2.legend()

# plt.show()


#
# S I Z I N G
#

lc = LoadCase(
fuse_loads=loads, lcid=31, name="3g Taxi", mach=0.1, altitude=0.0, cg_x=XCG
)

# Major Frames
print("Major Frame Sizing:")
for frame in frames:
frame.synthesis()
print(f" FS {frame.fs_loc}: {frame.weight:.1f}[lbf]")

results = fuse.synthesis(loadcase=lc)

print(results)

x = [float(x[0]) for x in results]
y = [float(x[1].weight) for x in results]
fig, ax = plt.subplots(figsize=(7, 4))
_ = ax.bar(x, y, width=100, color="k")
_ = ax.set_xlabel("Fuselage Station, $FS$, [in]")
_ = ax.set_ylabel("Weight, $W$, [lbs]")
_ = fig.suptitle(f"{FNZ0:.1f}g Taxi, xCG={XCG:.0f}[in]")


plt.show()
7 changes: 3 additions & 4 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import nox


try:
from nox_poetry import Session
from nox_poetry import session
Expand All @@ -29,7 +28,7 @@
nox.options.sessions = (
"pre-commit",
"safety",
"mypy",
# "mypy",
"tests",
"typeguard",
"xdoctest",
Expand Down Expand Up @@ -118,7 +117,7 @@ def precommit(session: Session) -> None:
"run",
"--all-files",
"--hook-stage=manual",
"--show-diff-on-failure",
# "--show-diff-on-failure",
]
session.install(
"black",
Expand All @@ -128,7 +127,7 @@ def precommit(session: Session) -> None:
"flake8-bugbear",
"flake8-docstrings",
"flake8-rst-docstrings",
"isort",
"isort[toml]",
"pep8-naming",
"pre-commit",
"pre-commit-hooks",
Expand Down
Loading
Loading