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