Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved cabin sizing documentation #101

Merged
merged 4 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ makedocs(
"atmos/atmos.md"
],
"Structures" => Any["structures/wing.md",
"structures/fuselage.md"
"structures/fuselage.md",
"structures/cabin_sizing.md"
],
"Cryogenic tanks" => Any["cryo_tank/cryotank.md",
"cryo_tank/fueltanks.md"
Expand Down
1,009 changes: 1,009 additions & 0 deletions docs/src/assets/cabin_layout.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions docs/src/structures/cabin_sizing.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good detailed pass. Two suggestions:

  • a very high level summary as the introductory paragraph would be ideal. Something that answers very quickly: "tasopt includes these assumptions by default, additional models can include y (e.g., doubledecking), you can calculate/optimize x if you specify blah".
  • On a more nitpicky stylistic note, the gen'l structure for each documentation page is: 1. high level summary, 2. conceptual/theoretical breakdown with figures and/or references (with links) to the relevant fxns 3. list of fxns as a list at the end. adhering to this i think would be helpful in keeping things consistent for readers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I just made these changes.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# [Cabin sizing](@id cabin_sizing)
When creating an aircraft input file, the fuselage geometry has to be specified (i.e., the pressure vessel start and end locations, cylindrical portion start and end locations and radius, guesses for the wing and tail). The user is given the option to resize the fuselage if, for example, these parameters are not known or if the radius is to be optimized. This can be accomplished via [`structures.update_fuse_for_pax!()`](@ref).

## Theory
!!! details "📖 Theory - Cabin sizing"
The TASOPT input file requires the user to specify a radius and general fuselage layout, including the start and end of the pressure vessel, and the start and end of the cylindrical portions. Whether this provided layout is used or not depends on the flag `calculate_cabin_length` in the `Fuselage.Geometry` input field: this flag is `False` by default, implying that the fuselage length and layout does not get recalculated for the input radius. However, if the flag is set to `True`, the function [`structures.update_fuse_for_pax!()`](@ref) is used to update the fuselage length and layout to accommodate the desired number of passengers.

To resize the fuselage, information about the number of passengers and seat properties are needed. The number of passengers used the resize the fuselage is set in the input file as `exit_limit` in `Mission`; this is assumed to be the maximum number of people that could fit in the cabin in an all-economy layout. The goal then is to determine the minimum cabin length corresponding to the specified radius and seat properties: seat pitch, width and height. Two key functions are used for this purpose:

- [`structures.find_floor_angles()`](@ref): This function aims to determine the angular placement of the cabin floor inside the fuselage cross-section. If the cabin has a single deck, this is the angle that maximizes the cabin width. If the aircraft is a double-decker, the main (lower) cabin could be at any angle; the main-cabin angle must then be specified so that the upper-cabin angle can be calculated from the cabin height. The double-decker geometry problem is more involved and is described in the subsequent section.

- [`structures.find_cabin_width()`](@ref) If the fuselage has a single deck, the angular position of the cabin floor is then used by the following function to calculate the maximum width corresponding to that section. For example, if the seat height is 0, the angular placement would be 0 and the maximum width of a single bubble fuselage is ``2R_{fuse}``. When the seat height is greater than 0, the effective cabin width is the distance between the top or bottom of the seats in the cabin, whichever is smallest.

Once the geometric properties of the floor and cabin width have been determined, the seats are placed in the cabin so as to determine the total cabin length. A 10 ft offset from the front of the cabin is assumed. First, the number of seats abreast is determined by [`structures.findSeatsAbreast()`](@ref). This function assumes an aisle width of 20 inches and a fuselage offset of 6 inches, as seats do not start at the edge of the fuselage. Once the number of seats abreast is known, the number of rows is calculated from the exit-limit number of passengers and the cabin length is calculated from the seat pitch, taking emergency exits into account.

When the cabin length has been determined, the [`structures.update_fuse_for_pax!()`](@ref) proceeds to update the position of all the structural elements that have to be specified by the user, such as the cylinder and pressure vessel ends, horizontal and vertical tail placements, etc. To do this, the relative distances between these elements specified by the user are maintained. For example, the distance from the end of the fuselage to the horizontal and vertical tails is the same in the resized aircraft as in the user-defined parameters.

!!! details "📖 Theory - Double-decker aircraft"
When the cabin has two decks, determining the best layout is more involved as the position of the lower floor and the passenger split between the lower and upper cabins is not known in advance. In TASOPT, this is solved as an optimization problem: the optimization variables are the number of passengers in the lower cabin and the lower-cabin floor angle. The geometry of the cabin is shown in the figure below.

![cabin](../assets/cabin_layout.svg)

The objective function to be minimized is the cabin length, which is the maximum of the lengths of the upper and lower cabins. An optimal design is expected to be one in which both cabins have similar lengths. Constraints are applied on the minimum cargo bay height, ``d_{cargo}`` (by default 1.626 m), and on the minimum height of the upper cabin, ``d_{cabin}`` (by default 2 m). The objective function is [`structures.find_double_decker_cabin_length()`](@ref), which returns the maximum of the lengths of either cabins (which is used as the objective function), and the number of seats abreast in the main cabin for a later check (ignored in the optimization process). The optimization is done using the `NLopt.jl` optimization suite via [`structures.optimize_double_decker_cabin()`](@ref).

Since there are only two optimization variables, this function uses the `GN_AGS` global optimizer. Once the minimum cabin length has been determined, the fuselage and component layouts get updated in [`structures.update_fuse_for_pax!()`](@ref) as in the single deck case.

!!! details "📖 Theory - Fuselage radius from seats abreast"
In some circumstances, it may be of interest to calculate the minimum fuselage radius that results in a given number of seats abreast. If multiple fuselage radii are being tested, it is likely that the ones providing best performance are those with the narrowest cabin for a given number of seats abreast (and therefore cabin length).

In TASOPT, this problem is solved as an optimization problem: the goal is to find the minimum fuselage radius such that the seats abreast in the main cabin are exactly equal to the desired number. This is done with `structures.find_minimum_radius_for_seats_per_row()`(@ref).

In this function, two optimizers are used. First, the global optimizer `GN_DIRECT` is used to find the approximate location of the solution with up to 500 function evaluations. This optimizer cannot handle equality constraints directly; instead, the constraint is introduced with a penalty function. The objective function is simply
```math
f = R_{fuse}+10^3 \Delta s,
```
where ``\Delta s`` is the difference between the desired and actual seats per row for a given radius. The function that computes this difference for a given radius is [`structures.check_seats_per_row_diff()`](@ref).
```

Once the approximate solution has been found, a local optimizer `LN_NELDERMEAD` is used to polish off the solution at a lower computational cost. Finally, the solution is checked to verify it meets the equality constraint.

## Functions

```@docs
TASOPT.structures.update_fuse_for_pax!
TASOPT.structures.find_floor_angles
TASOPT.structures.find_cabin_width
TASOPT.structures.findSeatsAbreast
TASOPT.structures.find_double_decker_cabin_length
TASOPT.structures.optimize_double_decker_cabin
TASOPT.structures.find_minimum_radius_for_seats_per_row
TASOPT.structures.check_seats_per_row_diff
```
3 changes: 2 additions & 1 deletion src/IO/read_input.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ if !ac_type_fixed
end

maxpax = readmis("max_payload_in_pax_equivalent") #This represents the maximum aircraft payload in equivalent number of pax
#This may exceed the seatable capacity to account for belly cargo
pax = readmis("pax")
exitlimit = readmis("exit_limit")
exitlimit = readmis("exit_limit") #Maximum number of pax that could fit in cabin in an all-economy layout
despax = pax[1] #Design number of passengers
if despax > maxpax
error("Design mission has higher payload weight than maximum aircraft payload!")
Expand Down
25 changes: 22 additions & 3 deletions src/structures/size_cabin.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ length.
**Inputs:**
- `pax::Float64`: design number of passengers
- `cabin_width::Float64`: width of cabin (m).
- `seat_pitch::Float64`: longitudinal distance between seats (m).
- `seat_width::Float64`: width of one seat (m).
- `aisle_halfwidth::Float64`: half the width of an aisle (m).
- `fuse_offset::Float64`: distance from outside of fuselage to edge of closest window seat (m).
Expand All @@ -21,7 +22,7 @@ length.
function place_cabin_seats(pax, cabin_width, seat_pitch = 30.0*in_to_m,
seat_width = 19.0*in_to_m, aisle_halfwidth = 10.0*in_to_m, fuse_offset = 6.0*in_to_m)

cabin_offset = 10 * ft_to_m #Distance to the front and back of seats
cabin_offset = 10 * ft_to_m #Distance to the front of seats
#TODO the hardcoded 10 ft is not elegant
Copy link
Collaborator

@argonaut22 argonaut22 Nov 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. off-the-cuff thought: perhaps it could be a default value for a field within the Cabin struct


seats_per_row = findSeatsAbreast(cabin_width, seat_width, aisle_halfwidth, fuse_offset)
Expand Down Expand Up @@ -49,6 +50,22 @@ function place_cabin_seats(pax, cabin_width, seat_pitch = 30.0*in_to_m,
return lcabin, xseats, seats_per_row
end # function place_cabin_seats

"""
findSeatsAbreast(cabin_width,
seat_width = 19.0*in_to_m, aisle_halfwidth = 10.0*in_to_m, fuse_offset = 6.0*in_to_m)

Function to find the number of seats abreast that can fit in a given cabin width.

!!! details "🔃 Inputs and Outputs"
**Inputs:**
- `cabin_width::Float64`: width of cabin (m).
- `seat_width::Float64`: width of one seat (m).
- `aisle_halfwidth::Float64`: half the width of an aisle (m).
- `fuse_offset::Float64`: distance from outside of fuselage to edge of closest window seat (m).

**Outputs:**
- `seats_per_row::Float64`: number of seats per row.
"""
function findSeatsAbreast(cabin_width,
seat_width = 19.0*in_to_m, aisle_halfwidth = 10.0*in_to_m, fuse_offset = 6.0*in_to_m)

Expand Down Expand Up @@ -202,6 +219,7 @@ passengers on each deck.

**Outputs:**
- `maxl::Float64`: required cabin length (m)
- `pax_per_row_main::Float64`: number of seats abreast in lower cabin
"""
function find_double_decker_cabin_length(x::Vector{Float64}, fuse)
seat_pitch = fuse.cabin.seat_pitch
Expand Down Expand Up @@ -282,15 +300,16 @@ end
"""
optimize_double_decker_cabin(fuse)

This function can be used to optimize the passenger distribution across two decks in a double decker aircraft.
If the cross-section is circular, it also optimizes the deck layouts.
This function can be used to optimize the deck layouts and passenger distribution in a double decker aircraft.

!!! details "🔃 Inputs and Outputs"
**Inputs:**
- `parg::Vector{Float64}`: vector with aircraft geometric and mass parameters
- `fuse::Fuselage`: structure with fuselage parameters

**Outputs:**
- `xopt::Vector{Float64}`: vector with optimization results
- `seats_per_row_main::Float64`: number of seats abreast in lower cabin
"""
function optimize_double_decker_cabin(fuse)
dRfuse = fuse.layout.bubble_lower_downward_shift
Expand Down
11 changes: 4 additions & 7 deletions src/structures/update_fuse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,13 @@ This function calculates the minimum radius required to have a desired number of

!!! details "🔃 Inputs and Outputs"
**Inputs:**
- `seats_per_row::Float64`: number of seats per row in main cabin (lower deck if double decker)
- `seats_per_row::Int64`: number of seats per row in main cabin (lower deck if double decker)
- `ac_base::aircraft`: aircraft object

**Outputs:**
- `R::Float64`: minimum radius for desired number of seats per row (m)
"""
function find_minimum_radius_for_seats_per_row(seats_per_row, ac_base)
function find_minimum_radius_for_seats_per_row(seats_per_row::Int64, ac_base)
ac = deepcopy(ac_base) #Copy input ac to avoid modifying it
obj(x, grad) = x[1] + 1e3 * abs(check_seats_per_row_diff(seats_per_row, x, ac)) #Objective function is the radius plus a big penalty if constraint is not met

Expand All @@ -173,7 +173,7 @@ function find_minimum_radius_for_seats_per_row(seats_per_row, ac_base)
(minf,xopt,ret) = NLopt.optimize(opt, initial_x) #Solve optimization problem

#Next, use local optimizer to polish off optimum
opt = Opt(:LN_NELDERMEAD, length(initial_x)) #Use a
opt = Opt(:LN_NELDERMEAD, length(initial_x)) #Use a local optimizer
opt.lower_bounds = [0.0]
opt.upper_bounds = [5.0]
opt.min_objective = obj
Expand Down Expand Up @@ -217,7 +217,4 @@ function check_seats_per_row_diff(seats_per_row, x, ac)
catch
return 1.0
end
end

# [fuselage.layout.x_end_cylinder, parg[igxwbox], fuselage.layout.x_pressure_shell_aft, fuselage.layout.x_cone_end,
# fuselage.APU.x, fuselage.layout.x_end, fuselage.HPE_sys.x, parg[igxhbox], parg[igxvbox],parg[igxeng],parg[igdxcabin]]
end
85 changes: 43 additions & 42 deletions test/unit_test_structures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,10 @@ fuselage.layout.x_cone_end = fuselage.layout.x_cone_end * 0.52484
pari = zeros(iitotal)
pari[iinftanks] = 1

# parg_orig = deepcopy(parg)
# deleteat!(parg_orig, parg_orig .== 0)

#Update fuel tank length and check changes
parg[iglftank] = 5.0
TASOPT.update_fuse!(fuselage, pari, parg)

# parg_check = [43.40480000000001, 6.096, 35.175200000000004, 36.699200000000005, 41.27120000000001, 14.52032532088372, 16.04432532088372, 40.50920000000001, 39.137600000000006, 42.18560000000001, 21.660776608000003, 5.0, 23.4696, 1.5239999999999991, 18.716634144]
update_fuse_out = [fuselage.layout.x_end_cylinder,
fuselage.layout.x_pressure_shell_aft,
fuselage.layout.x_cone_end,
Expand Down Expand Up @@ -295,42 +291,47 @@ parg[igxeng]]
update_fuse_out_test = [29.5656, 31.0896, 18.716634144, 36.57600000000001, 37.79520000000001, 9.82323826413696, 34.89960000000001, 33.528000000000006, 14.52032532088372]
@test all(isapprox.(update_fuse_out, update_fuse_out_test))

# #Test cabin resizing
# parg = zeros(igtotal)
# fuselage.layout.x_start_cylinder = 6.096
# fuselage.layout.x_end_cylinder = 29.5656
# fuselage.layout.x_pressure_shell_aft = 31.0896
# parg[igdxcabin] = 23.4696
# parg[igdxeng2wbox] = 1.5239999999999991
# fuselage.APU.r = [36.576, 0.0, 0.0]
# fuselage.layout.x_end = 37.7952
# fuselage.layout.x_cone_end = 35.6616
# parg[igxhbox ] = 34.8996
# parg[igxvbox ] = 33.528
# parg[igxwbox] = 16.04432532088372
# parg[igxeng] = parg[igxwbox] - parg[igdxeng2wbox]
# fuselage.layout.x_cone_end = fuselage.layout.x_cone_end * 0.52484

# parg[igseatpitch] = 0.762
# parg[igseatwidth] = 0.4826
# parg[igaislehalfwidth] = 0.254
# parg[igWpaymax] = 219964.5779
# fuselage.layout.radius

# pari = zeros(iitotal)
# pari[iidoubledeck] = 0

# parm = zeros(imtotal)
# parm[imWperpax,1] = 956.36773

# fuse_tank = TASOPT.fuselage_tank()

# TASOPT.update_fuse_for_pax!(pari, parg, fuselage, fuse_tank)

# parg_check = [47.091600000000014, 6.096, 38.86200000000001, 40.38600000000001, 44.95800000000001, 18.460895740194808, 19.98489574019481, 44.19600000000001, 42.82440000000001, 45.87240000000001, 23.595756720000004, 1.9558, 219964.5779, 32.76600000000001, 1.5239999999999991, 0.762, 0.4826, 0.254]
# parg_nz = deepcopy(parg)
# deleteat!(parg_nz, parg_nz .== 0)
# for (i,item) in enumerate(parg_nz) #For every nonzero element in parg
# @test parg_nz[i] ≈ parg_check[i]
# end
#Test cabin resizing
parg = zeros(igtotal)
fuselage.layout.x_start_cylinder = 6.096
fuselage.layout.x_end_cylinder = 29.5656
fuselage.layout.x_pressure_shell_aft = 31.0896
fuselage.layout.l_cabin_cylinder = 23.4696
parg[igdxeng2wbox] = 1.5239999999999991
fuselage.APU.r = [36.576, 0.0, 0.0]
fuselage.layout.x_end = 37.7952
fuselage.layout.x_cone_end = 35.6616
parg[igxhbox ] = 34.8996
parg[igxvbox ] = 33.528
parg[igxwbox] = 16.04432532088372
parg[igxeng] = parg[igxwbox] - parg[igdxeng2wbox]
fuselage.layout.x_cone_end = fuselage.layout.x_cone_end * 0.52484

parg[igseatpitch] = 0.762
parg[igseatwidth] = 0.4826
parg[igaislehalfwidth] = 0.254
parg[igWpaymax] = 219964.5779
fuselage.layout.cross_section.radius = 2.5 #Change radius to 2.5 m

pari = zeros(iitotal)
pari[iidoubledeck] = 0

fuse_tank = TASOPT.fuselage_tank()

TASOPT.update_fuse_for_pax!(pari, parg, fuselage, fuse_tank)

parg_check = [14.584924835954398, 16.108924835954397, 35.05200000000001, 33.680400000000006, 219964.5779, 1.5239999999999991, 0.762, 0.4826, 0.254]
parg_nz = deepcopy(parg)
deleteat!(parg_nz, parg_nz .== 0)
for (i,item) in enumerate(parg_nz) #For every nonzero element in parg
@test parg_nz[i] ≈ parg_check[i]
end

@test fuselage.layout.x_pressure_shell_aft ≈ 31.24200000000001
@test fuselage.layout.x_cone_end ≈ 18.86903414400001
@test fuselage.layout.x_end ≈ 37.94760000000001

#Test minimum radius calculation
Rmin = TASOPT.structures.find_minimum_radius_for_seats_per_row(5, ac_test)
@test Rmin ≈ 1.7113052179793784
end
Loading