JSBSim Flight Dynamics Model 1.3.0 (09 Apr 2026)
An Open Source Flight Dynamics and Control Software Library in C++
Loading...
Searching...
No Matches
FGGasCell.cpp
1/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2
3 Header: FGGasCell.h
4 Author: Anders Gidenstam
5 Date started: 01/21/2006
6
7 ----- Copyright (C) 2006 - 2013 Anders Gidenstam (anders(at)gidenstam.org) --
8
9 This program is free software; you can redistribute it and/or modify it under
10 the terms of the GNU Lesser General Public License as published by the Free
11 Software Foundation; either version 2 of the License, or (at your option) any
12 later version.
13
14 This program is distributed in the hope that it will be useful, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
17 details.
18
19 You should have received a copy of the GNU Lesser General Public License along
20 with this program; if not, write to the Free Software Foundation, Inc., 59
21 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22
23 Further information about the GNU Lesser General Public License can also be
24 found on the world wide web at http://www.gnu.org.
25
26FUNCTIONAL DESCRIPTION
27--------------------------------------------------------------------------------
28See header file.
29
30HISTORY
31--------------------------------------------------------------------------------
3201/21/2006 AG Created
33
34%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
35INCLUDES
36%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
37
38#include "FGFDMExec.h"
39#include "models/FGMassBalance.h"
40#include "FGGasCell.h"
41#include "input_output/FGXMLElement.h"
42#include "input_output/FGLog.h"
43
44using std::string;
45using std::max;
46
47namespace JSBSim {
48
49/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
50CLASS IMPLEMENTATION
51%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
52/* Constants. */
53const double FGGasCell::R = 3.4071; // [lbf ft/(mol Rankine)]
54const double FGGasCell::M_air = 0.0019186; // [slug/mol]
55const double FGGasCell::M_hydrogen = 0.00013841; // [slug/mol]
56const double FGGasCell::M_helium = 0.00027409; // [slug/mol]
57
58FGGasCell::FGGasCell(FGFDMExec* exec, Element* el, unsigned int num,
59 const struct Inputs& input)
60 : FGForce(exec), in(input)
61{
62 string token;
63 Element* element;
64
65 auto PropertyManager = exec->GetPropertyManager();
66 MassBalance = exec->GetMassBalance();
67
68 Buoyancy = MaxVolume = MaxOverpressure = Temperature = Pressure =
69 Contents = Volume = dVolumeIdeal = 0.0;
70 Xradius = Yradius = Zradius = Xwidth = Ywidth = Zwidth = 0.0;
71 ValveCoefficient = ValveOpen = 0.0;
72 CellNum = num;
73
74 // NOTE: In the local system X points north, Y points east and Z points down.
75 SetTransformType(FGForce::tLocalBody);
76
77 type = el->GetAttributeValue("type");
78 if (type == "HYDROGEN") Type = ttHYDROGEN;
79 else if (type == "HELIUM") Type = ttHELIUM;
80 else if (type == "AIR") Type = ttAIR;
81 else Type = ttUNKNOWN;
82
83 element = el->FindElement("location");
84 if (element) {
85 vXYZ = element->FindElementTripletConvertTo("IN");
86 } else {
87 XMLLogException err(el);
88 err << "\nFatal Error: No location found for this gas cell.\n";
89 throw err;
90 }
91 if ((el->FindElement("x_radius") || el->FindElement("x_width")) &&
92 (el->FindElement("y_radius") || el->FindElement("y_width")) &&
93 (el->FindElement("z_radius") || el->FindElement("z_width"))) {
94
95 if (el->FindElement("x_radius")) {
96 Xradius = el->FindElementValueAsNumberConvertTo("x_radius", "FT");
97 }
98 if (el->FindElement("y_radius")) {
99 Yradius = el->FindElementValueAsNumberConvertTo("y_radius", "FT");
100 }
101 if (el->FindElement("z_radius")) {
102 Zradius = el->FindElementValueAsNumberConvertTo("z_radius", "FT");
103 }
104
105 if (el->FindElement("x_width")) {
106 Xwidth = el->FindElementValueAsNumberConvertTo("x_width", "FT");
107 }
108 if (el->FindElement("y_width")) {
109 Ywidth = el->FindElementValueAsNumberConvertTo("y_width", "FT");
110 }
111 if (el->FindElement("z_width")) {
112 Zwidth = el->FindElementValueAsNumberConvertTo("z_width", "FT");
113 }
114
115 // The volume is a (potentially) extruded ellipsoid.
116 // However, currently only a few combinations of radius and width are
117 // fully supported.
118 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
119 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
120 // Ellipsoid volume.
121 MaxVolume = 4.0 * M_PI * Xradius * Yradius * Zradius / 3.0;
122 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
123 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
124 // Cylindrical volume.
125 MaxVolume = M_PI * Yradius * Zradius * Xwidth;
126 } else {
127 FGXMLLogging log(el, LogLevel::WARN);
128 log << "Unsupported gas cell shape.\n";
129 MaxVolume =
130 (4.0 * M_PI * Xradius * Yradius * Zradius / 3.0 +
131 M_PI * Yradius * Zradius * Xwidth +
132 M_PI * Xradius * Zradius * Ywidth +
133 M_PI * Xradius * Yradius * Zwidth +
134 2.0 * Xradius * Ywidth * Zwidth +
135 2.0 * Yradius * Xwidth * Zwidth +
136 2.0 * Zradius * Xwidth * Ywidth +
137 Xwidth * Ywidth * Zwidth);
138 }
139 } else {
140 XMLLogException err(el);
141 err << "\nGas cell shape must be given.\n";
142 throw err;
143 }
144 if (el->FindElement("max_overpressure")) {
145 MaxOverpressure = el->FindElementValueAsNumberConvertTo("max_overpressure",
146 "LBS/FT2");
147 }
148 if (el->FindElement("fullness")) {
149 const double Fullness = el->FindElementValueAsNumber("fullness");
150 if (0 <= Fullness) {
151 Volume = Fullness * MaxVolume;
152 } else {
153 FGXMLLogging log(el, LogLevel::WARN);
154 log << "Invalid initial gas cell fullness value.\n";
155 }
156 }
157 if (el->FindElement("valve_coefficient")) {
158 ValveCoefficient =
159 el->FindElementValueAsNumberConvertTo("valve_coefficient",
160 "FT4*SEC/SLUG");
161 ValveCoefficient = max(ValveCoefficient, 0.0);
162 }
163
164 // Initialize state
165 SetLocation(vXYZ);
166
167 if (Temperature == 0.0) {
168 Temperature = in.Temperature;
169 }
170 if (Pressure == 0.0) {
171 Pressure = in.Pressure;
172 }
173 if (Volume != 0.0) {
174 // Calculate initial gas content.
175 Contents = Pressure * Volume / (R * Temperature);
176
177 // Clip to max allowed value.
178 const double IdealPressure = Contents * R * Temperature / MaxVolume;
179 if (IdealPressure > Pressure + MaxOverpressure) {
180 Contents = (Pressure + MaxOverpressure) * MaxVolume / (R * Temperature);
181 Pressure = Pressure + MaxOverpressure;
182 } else {
183 Pressure = max(IdealPressure, Pressure);
184 }
185 } else {
186 // Calculate initial gas content.
187 Contents = Pressure * MaxVolume / (R * Temperature);
188 }
189
190 Volume = Contents * R * Temperature / Pressure;
191 Mass = Contents * M_gas();
192
193 // Bind relevant properties
194 string property_name, base_property_name;
195
196 base_property_name = CreateIndexedPropertyName("buoyant_forces/gas-cell", CellNum);
197
198 property_name = base_property_name + "/max_volume-ft3";
199 PropertyManager->Tie( property_name.c_str(), &MaxVolume);
200 PropertyManager->GetNode(property_name)->setAttribute( SGPropertyNode::WRITE, false );
201 property_name = base_property_name + "/temp-R";
202 PropertyManager->Tie( property_name.c_str(), &Temperature);
203 property_name = base_property_name + "/pressure-psf";
204 PropertyManager->Tie( property_name.c_str(), &Pressure);
205 property_name = base_property_name + "/volume-ft3";
206 PropertyManager->Tie( property_name.c_str(), &Volume);
207 property_name = base_property_name + "/buoyancy-lbs";
208 PropertyManager->Tie( property_name.c_str(), &Buoyancy);
209 property_name = base_property_name + "/contents-mol";
210 PropertyManager->Tie( property_name.c_str(), &Contents);
211 property_name = base_property_name + "/valve_open";
212 PropertyManager->Tie( property_name.c_str(), &ValveOpen);
213
214 Debug(0);
215
216 // Read heat transfer coefficients
217 if (Element* heat = el->FindElement("heat")) {
218 Element* function_element = heat->FindElement("function");
219 while (function_element) {
220 HeatTransferCoeff.push_back(new FGFunction(exec, function_element));
221 function_element = heat->FindNextElement("function");
222 }
223 }
224
225 // Load ballonets if there are any
226 if (Element* ballonet_element = el->FindElement("ballonet")) {
227 while (ballonet_element) {
228 Ballonet.push_back(new FGBallonet(exec,
229 ballonet_element,
230 Ballonet.size(),
231 this, in));
232 ballonet_element = el->FindNextElement("ballonet");
233 }
234 }
235
236}
237
238//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
239
240FGGasCell::~FGGasCell()
241{
242 unsigned int i;
243
244 for (i = 0; i < HeatTransferCoeff.size(); i++) delete HeatTransferCoeff[i];
245 HeatTransferCoeff.clear();
246
247 for (i = 0; i < Ballonet.size(); i++) delete Ballonet[i];
248 Ballonet.clear();
249
250 Debug(1);
251}
252
253//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
254
255void FGGasCell::Calculate(double dt)
256{
257 const double AirTemperature = in.Temperature; // [Rankine]
258 const double AirPressure = in.Pressure; // [lbs/ft^2]
259 const double AirDensity = in.Density; // [slug/ft^3]
260 const double g = in.gravity; // [lbs/slug]
261
262 const double OldTemperature = Temperature;
263 const double OldPressure = Pressure;
264 unsigned int i;
265 const size_t no_ballonets = Ballonet.size();
266
267 //-- Read ballonet state --
268 // NOTE: This model might need a more proper integration technique.
269 double BallonetsVolume = 0.0;
270 double BallonetsHeatFlow = 0.0;
271 for (i = 0; i < no_ballonets; i++) {
272 BallonetsVolume += Ballonet[i]->GetVolume();
273 BallonetsHeatFlow += Ballonet[i]->GetHeatFlow();
274 }
275
276 //-- Gas temperature --
277
278 if (!HeatTransferCoeff.empty()) {
279 // The model is based on the ideal gas law.
280 // However, it does look a bit fishy. Please verify.
281 // dT/dt = dU / (Cv n R)
282 double dU = 0.0;
283 for (i = 0; i < HeatTransferCoeff.size(); i++) {
284 dU += HeatTransferCoeff[i]->GetValue();
285 }
286 // Don't include dt when accounting for adiabatic expansion/contraction.
287 // The rate of adiabatic cooling looks about right: ~5.4 Rankine/1000ft.
288 if (Contents > 0) {
289 Temperature +=
290 (dU * dt - Pressure * dVolumeIdeal - BallonetsHeatFlow) /
291 (Cv_gas() * Contents * R);
292 } else {
293 Temperature = AirTemperature;
294 }
295 } else {
296 // No simulation of complex temperature changes.
297 // Note: Making the gas cell behave adiabatically might be a better
298 // option.
299 Temperature = AirTemperature;
300 }
301
302 //-- Pressure --
303 const double IdealPressure =
304 Contents * R * Temperature / (MaxVolume - BallonetsVolume);
305 if (IdealPressure > AirPressure + MaxOverpressure) {
306 Pressure = AirPressure + MaxOverpressure;
307 } else {
308 Pressure = max(IdealPressure, AirPressure);
309 }
310
311 //-- Manual valving --
312
313 // FIXME: Presently the effect of manual valving is computed using
314 // an ad hoc formula which might not be a good representation
315 // of reality.
316 if ((ValveCoefficient > 0.0) && (ValveOpen > 0.0)) {
317 // First compute the difference in pressure between the gas in the
318 // cell and the air above it.
319 // FixMe: CellHeight should depend on current volume.
320 const double CellHeight = 2 * Zradius + Zwidth; // [ft]
321 const double GasMass = Contents * M_gas(); // [slug]
322 const double GasVolume = Contents * R * Temperature / Pressure; // [ft^3]
323 const double GasDensity = GasMass / GasVolume;
324 const double DeltaPressure =
325 Pressure + CellHeight * g * (AirDensity - GasDensity) - AirPressure;
326 const double VolumeValved =
327 ValveOpen * ValveCoefficient * DeltaPressure * dt;
328 Contents =
329 max(1E-8, Contents - Pressure * VolumeValved / (R * Temperature));
330 }
331
332 //-- Update ballonets. --
333 // Doing that here should give them the opportunity to react to the
334 // new pressure.
335 BallonetsVolume = 0.0;
336 for (i = 0; i < no_ballonets; i++) {
337 Ballonet[i]->Calculate(dt);
338 BallonetsVolume += Ballonet[i]->GetVolume();
339 }
340
341 //-- Automatic safety valving. --
342 if (Contents * R * Temperature / (MaxVolume - BallonetsVolume) >
343 AirPressure + MaxOverpressure) {
344 // Gas is automatically valved. Valving capacity is assumed to be infinite.
345 // FIXME: This could/should be replaced by damage to the gas cell envelope.
346 Contents =
347 (AirPressure + MaxOverpressure) *
348 (MaxVolume - BallonetsVolume) / (R * Temperature);
349 }
350
351 //-- Volume --
352 Volume = Contents * R * Temperature / Pressure + BallonetsVolume;
353 dVolumeIdeal =
354 Contents * R * (Temperature / Pressure - OldTemperature / OldPressure);
355
356 //-- Current buoyancy --
357 // The buoyancy is computed using the atmospheres local density.
358 Buoyancy = Volume * AirDensity * g;
359
360 // Note: This is gross buoyancy. The weight of the gas itself and
361 // any ballonets is not deducted here as the effects of the gas mass
362 // is handled by FGMassBalance.
363 vFn = {0.0, 0.0, - Buoyancy};
364
365 // Compute the inertia of the gas cell.
366 // Consider the gas cell as a shape of uniform density.
367 // FIXME: If the cell isn't ellipsoid or cylindrical the inertia will
368 // be wrong.
369 gasCellJ.InitMatrix();
370 const double mass = Contents * M_gas();
371 double Ixx, Iyy, Izz;
372 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
373 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
374 // Ellipsoid volume.
375 Ixx = (1.0 / 5.0) * mass * (Yradius*Yradius + Zradius*Zradius);
376 Iyy = (1.0 / 5.0) * mass * (Xradius*Xradius + Zradius*Zradius);
377 Izz = (1.0 / 5.0) * mass * (Xradius*Xradius + Yradius*Yradius);
378 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
379 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
380 // Cylindrical volume (might not be valid with an elliptical cross-section).
381 Ixx = (1.0 / 2.0) * mass * Yradius * Zradius;
382 Iyy =
383 (1.0 / 4.0) * mass * Yradius * Zradius +
384 (1.0 / 12.0) * mass * Xwidth * Xwidth;
385 Izz =
386 (1.0 / 4.0) * mass * Yradius * Zradius +
387 (1.0 / 12.0) * mass * Xwidth * Xwidth;
388 } else {
389 // Not supported. Revert to pointmass model.
390 Ixx = Iyy = Izz = 0.0;
391 }
392 // The volume is symmetric, so Ixy = Ixz = Iyz = 0.
393 gasCellJ(1,1) = Ixx;
394 gasCellJ(2,2) = Iyy;
395 gasCellJ(3,3) = Izz;
396 Mass = mass;
397 // Transform the moments of inertia to the body frame.
398 gasCellJ += MassBalance->GetPointmassInertia(Mass, GetXYZ());
399
400 gasCellM.InitMatrix();
401 gasCellM(eX) +=
402 GetXYZ(eX) * Mass*slugtolb;
403 gasCellM(eY) +=
404 GetXYZ(eY) * Mass*slugtolb;
405 gasCellM(eZ) +=
406 GetXYZ(eZ) * Mass*slugtolb;
407
408 if (no_ballonets > 0) {
409 // Add the mass, moment and inertia of any ballonets.
410 for (i = 0; i < no_ballonets; i++) {
411 Mass += Ballonet[i]->GetMass();
412
413 // Add ballonet moments due to mass (in the structural frame).
414 gasCellM(eX) +=
415 Ballonet[i]->GetXYZ(eX) * Ballonet[i]->GetMass()*slugtolb;
416 gasCellM(eY) +=
417 Ballonet[i]->GetXYZ(eY) * Ballonet[i]->GetMass()*slugtolb;
418 gasCellM(eZ) +=
419 Ballonet[i]->GetXYZ(eZ) * Ballonet[i]->GetMass()*slugtolb;
420
421 gasCellJ += Ballonet[i]->GetInertia();
422 }
423 }
424}
425
426//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
427// The bitmasked value choices are as follows:
428// unset: In this case (the default) JSBSim would only print
429// out the normally expected messages, essentially echoing
430// the config files as they are read. If the environment
431// variable is not set, debug_lvl is set to 1 internally
432// 0: This requests JSBSim not to output any messages
433// whatsoever.
434// 1: This value explicity requests the normal JSBSim
435// startup messages
436// 2: This value asks for a message to be printed out when
437// a class is instantiated
438// 4: When this value is set, a message is displayed when a
439// FGModel object executes its Run() method
440// 8: When this value is set, various runtime state variables
441// are printed out periodically
442// 16: When set various parameters are sanity checked and
443// a message is printed out when they go out of bounds
444
445void FGGasCell::Debug(int from)
446{
447 if (debug_lvl <= 0) return;
448
449 if (debug_lvl & 1) { // Standard console startup message output
450 if (from == 0) { // Constructor
451 FGLogging log(LogLevel::DEBUG);
452 log << " Gas cell holds " << std::fixed << Contents << " mol " << type << "\n";
453 log << " Cell location (X, Y, Z) (in.): " << vXYZ(eX) << ", "
454 << vXYZ(eY) << ", " << vXYZ(eZ) << "\n";
455 log << " Maximum volume: " << MaxVolume << " ft3\n";
456 log << " Relief valve release pressure: " << MaxOverpressure
457 << " lbs/ft2\n";
458 log << " Manual valve coefficient: " << ValveCoefficient
459 << " ft4*sec/slug\n";
460 log << " Initial temperature: " << Temperature << " Rankine\n";
461 log << " Initial pressure: " << Pressure << " lbs/ft2\n";
462 log << " Initial volume: " << Volume << " ft3\n";
463 log << " Initial mass: " << GetMass() << " slug mass\n";
464 log << " Initial weight: " << GetMass()*slugtolb << " lbs force\n";
465 log << " Heat transfer: \n";
466 }
467 }
468 if (debug_lvl & 2 ) { // Instantiation/Destruction notification
469 FGLogging log(LogLevel::DEBUG);
470 if (from == 0) log << "Instantiated: FGGasCell\n";
471 if (from == 1) log << "Destroyed: FGGasCell\n";
472 }
473 if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
474 }
475 if (debug_lvl & 8 ) { // Runtime state variables
476 FGLogging log(LogLevel::DEBUG);
477 log << " " << type << " cell holds " << std::fixed << Contents << " mol\n";
478 log << " Temperature: " << Temperature << " Rankine\n";
479 log << " Pressure: " << Pressure << " lbs/ft2\n";
480 log << " Volume: " << Volume << " ft3\n";
481 log << " Mass: " << GetMass() << " slug mass\n";
482 log << " Weight: " << GetMass()*slugtolb << " lbs force\n";
483 }
484 if (debug_lvl & 16) { // Sanity checking
485 }
486 if (debug_lvl & 64) {
487 if (from == 0) { // Constructor
488 }
489 }
490}
491
492/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
493CLASS IMPLEMENTATION
494%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
495const double FGBallonet::R = 3.4071; // [lbs ft/(mol Rankine)]
496const double FGBallonet::M_air = 0.0019186; // [slug/mol]
497const double FGBallonet::Cv_air = 5.0/2.0; // [??]
498
499FGBallonet::FGBallonet(FGFDMExec* exec, Element* el, unsigned int num,
500 FGGasCell* parent, const struct FGGasCell::Inputs& input)
501 : in(input)
502{
503 string token;
504 Element* element;
505
506 auto PropertyManager = exec->GetPropertyManager();
507 MassBalance = exec->GetMassBalance();
508
509 MaxVolume = MaxOverpressure = Temperature = Pressure =
510 Contents = Volume = dVolumeIdeal = dU = 0.0;
511 Xradius = Yradius = Zradius = Xwidth = Ywidth = Zwidth = 0.0;
512 ValveCoefficient = ValveOpen = 0.0;
513 BlowerInput = NULL;
514 CellNum = num;
515 Parent = parent;
516
517 // NOTE: In the local system X points north, Y points east and Z points down.
518 element = el->FindElement("location");
519 if (element) {
520 vXYZ = element->FindElementTripletConvertTo("IN");
521 } else {
522 XMLLogException err(el);
523 err << "\nFatal Error: No location found for this ballonet.\n";
524 throw err;
525 }
526 if ((el->FindElement("x_radius") || el->FindElement("x_width")) &&
527 (el->FindElement("y_radius") || el->FindElement("y_width")) &&
528 (el->FindElement("z_radius") || el->FindElement("z_width"))) {
529
530 if (el->FindElement("x_radius")) {
531 Xradius = el->FindElementValueAsNumberConvertTo("x_radius", "FT");
532 }
533 if (el->FindElement("y_radius")) {
534 Yradius = el->FindElementValueAsNumberConvertTo("y_radius", "FT");
535 }
536 if (el->FindElement("z_radius")) {
537 Zradius = el->FindElementValueAsNumberConvertTo("z_radius", "FT");
538 }
539
540 if (el->FindElement("x_width")) {
541 Xwidth = el->FindElementValueAsNumberConvertTo("x_width", "FT");
542 }
543 if (el->FindElement("y_width")) {
544 Ywidth = el->FindElementValueAsNumberConvertTo("y_width", "FT");
545 }
546 if (el->FindElement("z_width")) {
547 Zwidth = el->FindElementValueAsNumberConvertTo("z_width", "FT");
548 }
549
550 // The volume is a (potentially) extruded ellipsoid.
551 // FIXME: However, currently only a few combinations of radius and
552 // width are fully supported.
553 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
554 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
555 // Ellipsoid volume.
556 MaxVolume = 4.0 * M_PI * Xradius * Yradius * Zradius / 3.0;
557 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
558 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
559 // Cylindrical volume.
560 MaxVolume = M_PI * Yradius * Zradius * Xwidth;
561 } else {
562 FGXMLLogging log(el, LogLevel::WARN);
563 log << "Unsupported ballonet shape.\n";
564 MaxVolume =
565 (4.0 * M_PI * Xradius * Yradius * Zradius / 3.0 +
566 M_PI * Yradius * Zradius * Xwidth +
567 M_PI * Xradius * Zradius * Ywidth +
568 M_PI * Xradius * Yradius * Zwidth +
569 2.0 * Xradius * Ywidth * Zwidth +
570 2.0 * Yradius * Xwidth * Zwidth +
571 2.0 * Zradius * Xwidth * Ywidth +
572 Xwidth * Ywidth * Zwidth);
573 }
574 } else {
575 XMLLogException err(el);
576 err << "\nFatal Error: Ballonet shape must be given.\n";
577 throw err;
578 }
579 if (el->FindElement("max_overpressure")) {
580 MaxOverpressure = el->FindElementValueAsNumberConvertTo("max_overpressure",
581 "LBS/FT2");
582 }
583 if (el->FindElement("fullness")) {
584 const double Fullness = el->FindElementValueAsNumber("fullness");
585 if (0 <= Fullness) {
586 Volume = Fullness * MaxVolume;
587 } else {
588 FGXMLLogging log(el, LogLevel::WARN);
589 log << "Invalid initial ballonet fullness value.\n";
590 }
591 }
592 if (el->FindElement("valve_coefficient")) {
593 ValveCoefficient =
594 el->FindElementValueAsNumberConvertTo("valve_coefficient",
595 "FT4*SEC/SLUG");
596 ValveCoefficient = max(ValveCoefficient, 0.0);
597 }
598
599 // Initialize state
600 if (Temperature == 0.0) {
601 Temperature = Parent->GetTemperature();
602 }
603 if (Pressure == 0.0) {
604 Pressure = Parent->GetPressure();
605 }
606 if (Volume != 0.0) {
607 // Calculate initial air content.
608 Contents = Pressure * Volume / (R * Temperature);
609
610 // Clip to max allowed value.
611 const double IdealPressure = Contents * R * Temperature / MaxVolume;
612 if (IdealPressure > Pressure + MaxOverpressure) {
613 Contents = (Pressure + MaxOverpressure) * MaxVolume / (R * Temperature);
614 Pressure = Pressure + MaxOverpressure;
615 } else {
616 Pressure = max(IdealPressure, Pressure);
617 }
618 } else {
619 // Calculate initial air content.
620 Contents = Pressure * MaxVolume / (R * Temperature);
621 }
622
623 Volume = Contents * R * Temperature / Pressure;
624
625 // Bind relevant properties
626 string property_name, base_property_name;
627 base_property_name = CreateIndexedPropertyName("buoyant_forces/gas-cell", Parent->GetIndex());
628 base_property_name = CreateIndexedPropertyName(base_property_name + "/ballonet", CellNum);
629
630 property_name = base_property_name + "/max_volume-ft3";
631 PropertyManager->Tie( property_name, &MaxVolume);
632 PropertyManager->GetNode(property_name)->setAttribute( SGPropertyNode::WRITE, false );
633
634 property_name = base_property_name + "/temp-R";
635 PropertyManager->Tie( property_name, &Temperature);
636
637 property_name = base_property_name + "/pressure-psf";
638 PropertyManager->Tie( property_name, &Pressure);
639
640 property_name = base_property_name + "/volume-ft3";
641 PropertyManager->Tie( property_name, &Volume);
642
643 property_name = base_property_name + "/contents-mol";
644 PropertyManager->Tie( property_name, &Contents);
645
646 property_name = base_property_name + "/valve_open";
647 PropertyManager->Tie( property_name, &ValveOpen);
648
649 Debug(0);
650
651 // Read heat transfer coefficients
652 if (Element* heat = el->FindElement("heat")) {
653 Element* function_element = heat->FindElement("function");
654 while (function_element) {
655 HeatTransferCoeff.push_back(new FGFunction(exec, function_element));
656 function_element = heat->FindNextElement("function");
657 }
658 }
659 // Read blower input function
660 if (Element* blower = el->FindElement("blower_input")) {
661 Element* function_element = blower->FindElement("function");
662 BlowerInput = new FGFunction(exec, function_element);
663 }
664}
665
666//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
667
668FGBallonet::~FGBallonet()
669{
670 unsigned int i;
671
672 for (i = 0; i < HeatTransferCoeff.size(); i++) delete HeatTransferCoeff[i];
673 HeatTransferCoeff.clear();
674
675 delete BlowerInput;
676 BlowerInput = NULL;
677
678 Debug(1);
679}
680
681//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
682
684{
685 const double ParentPressure = Parent->GetPressure(); // [lbs/ft^2]
686 const double AirPressure = in.Pressure; // [lbs/ft^2]
687
688 const double OldTemperature = Temperature;
689 const double OldPressure = Pressure;
690 unsigned int i;
691
692 //-- Gas temperature --
693
694 // The model is based on the ideal gas law.
695 // However, it does look a bit fishy. Please verify.
696 // dT/dt = dU / (Cv n R)
697 dU = 0.0;
698 for (i = 0; i < HeatTransferCoeff.size(); i++) {
699 dU += HeatTransferCoeff[i]->GetValue();
700 }
701 // dt is already accounted for in dVolumeIdeal.
702 if (Contents > 0) {
703 Temperature +=
704 (dU * dt - Pressure * dVolumeIdeal) / (Cv_air * Contents * R);
705 } else {
706 Temperature = Parent->GetTemperature();
707 }
708
709 //-- Pressure --
710 const double IdealPressure = Contents * R * Temperature / MaxVolume;
711 // The pressure is at least that of the parent gas cell.
712 Pressure = max(IdealPressure, ParentPressure);
713
714 //-- Blower input --
715 if (BlowerInput) {
716 const double AddedVolume = BlowerInput->GetValue() * dt;
717 if (AddedVolume > 0.0) {
718 Contents += Pressure * AddedVolume / (R * Temperature);
719 }
720 }
721
722 //-- Pressure relief and manual valving --
723 // FIXME: Presently the effect of valving is computed using
724 // an ad hoc formula which might not be a good representation
725 // of reality.
726 if ((ValveCoefficient > 0.0) &&
727 ((ValveOpen > 0.0) || (Pressure > AirPressure + MaxOverpressure))) {
728 const double DeltaPressure = Pressure - AirPressure;
729 const double VolumeValved =
730 ((Pressure > AirPressure + MaxOverpressure) ? 1.0 : ValveOpen) *
731 ValveCoefficient * DeltaPressure * dt;
732 // FIXME: Too small values of Contents sometimes leads to NaN.
733 // Currently the minimum is restricted to a safe value.
734 Contents =
735 max(1.0, Contents - Pressure * VolumeValved / (R * Temperature));
736 }
737
738 //-- Volume --
739 Volume = Contents * R * Temperature / Pressure;
740 dVolumeIdeal =
741 Contents * R * (Temperature / Pressure - OldTemperature / OldPressure);
742
743 // Compute the inertia of the ballonet.
744 // Consider the ballonet as a shape of uniform density.
745 // FIXME: If the ballonet isn't ellipsoid or cylindrical the inertia will
746 // be wrong.
747 ballonetJ.InitMatrix();
748 const double mass = Contents * M_air;
749 double Ixx, Iyy, Izz;
750 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
751 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
752 // Ellipsoid volume.
753 Ixx = (1.0 / 5.0) * mass * (Yradius*Yradius + Zradius*Zradius);
754 Iyy = (1.0 / 5.0) * mass * (Xradius*Xradius + Zradius*Zradius);
755 Izz = (1.0 / 5.0) * mass * (Xradius*Xradius + Yradius*Yradius);
756 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
757 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
758 // Cylindrical volume (might not be valid with an elliptical cross-section).
759 Ixx = (1.0 / 2.0) * mass * Yradius * Zradius;
760 Iyy =
761 (1.0 / 4.0) * mass * Yradius * Zradius +
762 (1.0 / 12.0) * mass * Xwidth * Xwidth;
763 Izz =
764 (1.0 / 4.0) * mass * Yradius * Zradius +
765 (1.0 / 12.0) * mass * Xwidth * Xwidth;
766 } else {
767 // Not supported. Revert to pointmass model.
768 Ixx = Iyy = Izz = 0.0;
769 }
770 // The volume is symmetric, so Ixy = Ixz = Iyz = 0.
771 ballonetJ(1,1) = Ixx;
772 ballonetJ(2,2) = Iyy;
773 ballonetJ(3,3) = Izz;
774 // Transform the moments of inertia to the body frame.
775 ballonetJ += MassBalance->GetPointmassInertia(GetMass(), GetXYZ());
776}
777
778//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
779// The bitmasked value choices are as follows:
780// unset: In this case (the default) JSBSim would only print
781// out the normally expected messages, essentially echoing
782// the config files as they are read. If the environment
783// variable is not set, debug_lvl is set to 1 internally
784// 0: This requests JSBSim not to output any messages
785// whatsoever.
786// 1: This value explicity requests the normal JSBSim
787// startup messages
788// 2: This value asks for a message to be printed out when
789// a class is instantiated
790// 4: When this value is set, a message is displayed when a
791// FGModel object executes its Run() method
792// 8: When this value is set, various runtime state variables
793// are printed out periodically
794// 16: When set various parameters are sanity checked and
795// a message is printed out when they go out of bounds
796
797void FGBallonet::Debug(int from)
798{
799 if (debug_lvl <= 0) return;
800
801 if (debug_lvl & 1) { // Standard console startup message output
802 if (from == 0) { // Constructor
803 FGLogging log(LogLevel::DEBUG);
804 log << " Ballonet holds " << std::fixed << Contents << " mol air\n";
805 log << " Location (X, Y, Z) (in.): " << vXYZ(eX) << ", "
806 << vXYZ(eY) << ", " << vXYZ(eZ) << "\n";
807 log << " Maximum volume: " << MaxVolume << " ft3\n";
808 log << " Relief valve release pressure: " << MaxOverpressure
809 << " lbs/ft2\n";
810 log << " Relief valve coefficient: " << ValveCoefficient
811 << " ft4*sec/slug\n";
812 log << " Initial temperature: " << Temperature << " Rankine\n";
813 log << " Initial pressure: " << Pressure << " lbs/ft2\n";
814 log << " Initial volume: " << Volume << " ft3\n";
815 log << " Initial mass: " << GetMass() << " slug mass\n";
816 log << " Initial weight: " << GetMass()*slugtolb
817 << " lbs force\n";
818 log << " Heat transfer: \n";
819 }
820 }
821 if (debug_lvl & 2 ) { // Instantiation/Destruction notification
822 FGLogging log(LogLevel::DEBUG);
823 if (from == 0) log << "Instantiated: FGBallonet\n";
824 if (from == 1) log << "Destroyed: FGBallonet\n";
825 }
826 if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
827 }
828 if (debug_lvl & 8 ) { // Runtime state variables
829 FGLogging log(LogLevel::DEBUG);
830 log << " Ballonet holds " << std::fixed << Contents << " mol air\n";
831 log << " Temperature: " << Temperature << " Rankine\n";
832 log << " Pressure: " << Pressure << " lbs/ft2\n";
833 log << " Volume: " << Volume << " ft3\n";
834 log << " Mass: " << GetMass() << " slug mass\n";
835 log << " Weight: " << GetMass()*slugtolb << " lbs force\n";
836 }
837 if (debug_lvl & 16) { // Sanity checking
838 }
839 if (debug_lvl & 64) {
840 if (from == 0) { // Constructor
841 }
842 }
843}
844}
Element * FindElement(const std::string &el="")
Searches for a specified element.
FGColumnVector3 FindElementTripletConvertTo(const std::string &target_units)
Composes a 3-element column vector for the supplied location or orientation.
std::string GetAttributeValue(const std::string &key)
Retrieves an attribute.
double FindElementValueAsNumberConvertTo(const std::string &el, const std::string &target_units)
Searches for the named element and converts and returns the data belonging to it.
Element * FindNextElement(const std::string &el="")
Searches for the next element as specified.
double FindElementValueAsNumber(const std::string &el="")
Searches for the named element and returns the data belonging to it as a number.
Models a ballonet inside a gas cell.
Definition FGGasCell.h:303
double GetMass(void) const
Get the current mass of the ballonets.
Definition FGGasCell.h:323
const FGColumnVector3 & GetXYZ(void) const
Get the center of gravity location of the ballonet.
Definition FGGasCell.h:316
void Calculate(double dt)
Runs the ballonet model; called by FGGasCell.
Encapsulates the JSBSim simulation executive.
Definition FGFDMExec.h:185
std::shared_ptr< FGMassBalance > GetMassBalance(void) const
Returns the FGAircraft pointer.
std::shared_ptr< FGPropertyManager > GetPropertyManager(void) const
Returns a pointer to the property manager object.
Definition FGFDMExec.h:422
Utility class that aids in the conversion of forces between coordinate systems and calculation of mom...
Definition FGForce.h:222
Represents a mathematical function.
Definition FGFunction.h:765
double GetValue(void) const override
Retrieves the value of the function object.
FGGasCell(FGFDMExec *exec, Element *el, unsigned int num, const struct Inputs &input)
Constructor.
Definition FGGasCell.cpp:58
double GetTemperature(void) const
Get the current gas temperature inside the gas cell.
Definition FGGasCell.h:220
double GetMass(void) const
Get the current mass of the gas cell (including any ballonets)
Definition FGGasCell.h:204
const FGColumnVector3 & GetXYZ(void) const
Get the center of gravity location of the gas cell (including any ballonets)
Definition FGGasCell.h:195
void Calculate(double dt)
Runs the gas cell model; called by BuoyantForces.
double GetPressure(void) const
Get the current gas pressure inside the gas cell.
Definition FGGasCell.h:224
static constexpr double slugtolb
Note that definition of lbtoslug by the inverse of slugtolb and not to a different constant you can a...
Definition FGJSBBase.h:314
void InitMatrix(void)
Initialize the matrix.
Main namespace for the JSBSim Flight Dynamics Model.
Definition FGFDMExec.cpp:71