diff --git a/docs/make.jl b/docs/make.jl index 12e344e1..44a2a6f2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -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" diff --git a/docs/src/assets/cabin_layout.svg b/docs/src/assets/cabin_layout.svg new file mode 100644 index 00000000..6ea823c0 --- /dev/null +++ b/docs/src/assets/cabin_layout.svg @@ -0,0 +1,1009 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 𝜃 + + 1 + + 𝜃 + + 2 + + + 𝑑 + + 𝑓𝑙𝑜𝑜𝑟𝑠 + + + + 𝑠𝑒𝑎𝑡 + + + + + + 𝑑 + + 𝑐𝑎𝑟𝑔𝑜 + + + 𝑑 + + 𝑐𝑎𝑏𝑖𝑛 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/structures/cabin_sizing.md b/docs/src/structures/cabin_sizing.md new file mode 100644 index 00000000..347b7575 --- /dev/null +++ b/docs/src/structures/cabin_sizing.md @@ -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 +``` \ No newline at end of file diff --git a/src/IO/read_input.jl b/src/IO/read_input.jl index cd5ad477..0fa03697 100644 --- a/src/IO/read_input.jl +++ b/src/IO/read_input.jl @@ -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!") diff --git a/src/structures/size_cabin.jl b/src/structures/size_cabin.jl index 5dcc48aa..03a78291 100644 --- a/src/structures/size_cabin.jl +++ b/src/structures/size_cabin.jl @@ -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). @@ -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 seats_per_row = findSeatsAbreast(cabin_width, seat_width, aisle_halfwidth, fuse_offset) @@ -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) @@ -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 @@ -282,8 +300,8 @@ 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 @@ -291,6 +309,7 @@ If the cross-section is circular, it also optimizes the deck layouts. **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 diff --git a/src/structures/update_fuse.jl b/src/structures/update_fuse.jl index 3a61b599..79aa4963 100644 --- a/src/structures/update_fuse.jl +++ b/src/structures/update_fuse.jl @@ -75,6 +75,7 @@ function update_fuse_for_pax!(pari, parg, fuse, fuse_tank) seat_width = fuse.cabin.seat_width aisle_halfwidth = fuse.cabin.aisle_halfwidth h_seat = fuse.cabin.seat_height + d_floor = fuse.cabin.floor_distance Rfuse = fuse.layout.radius dRfuse = fuse.layout.bubble_lower_downward_shift @@ -87,6 +88,9 @@ function update_fuse_for_pax!(pari, parg, fuse, fuse_tank) lcyl, _ = find_double_decker_cabin_length(xopt, fuse) #Total length is maximum of the two + #Store angles + fuse.cabin.floor_angle_main = xopt[2] + fuse.cabin.floor_angle_top = find_floor_angles(true, Rfuse, dRfuse, θ1 = xopt[2], h_seat = h_seat, d_floor = d_floor)[2] else θ = find_floor_angles(false, Rfuse, dRfuse, h_seat = h_seat) #Find the floor angle paxsize = fuse.cabin.exit_limit #maximum number of passengers @@ -145,13 +149,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 @@ -173,7 +177,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 @@ -217,7 +221,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]] \ No newline at end of file +end \ No newline at end of file diff --git a/test/unit_test_structures.jl b/test/unit_test_structures.jl index b65492ed..25f2cba7 100644 --- a/test/unit_test_structures.jl +++ b/test/unit_test_structures.jl @@ -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, @@ -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 \ No newline at end of file