1 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4 Author: Anders Gidenstam
5 Date started: 01/21/2006
7 ----- Copyright (C) 2006 - 2008 Anders Gidenstam (anders(at)gidenstam.org) --
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 Software
11 Foundation; either version 2 of the License, or (at your option) any later
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
19 You should have received a copy of the GNU Lesser General Public License along with
20 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
21 Place - Suite 330, Boston, MA 02111-1307, USA.
23 Further information about the GNU Lesser General Public License can also be found on
24 the world wide web at http://www.gnu.org.
26 FUNCTIONAL DESCRIPTION
27 --------------------------------------------------------------------------------
31 --------------------------------------------------------------------------------
34 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
36 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
38 #include <FGFDMExec.h>
39 #include <models/FGAuxiliary.h>
40 #include <models/FGAtmosphere.h>
41 #include <models/FGInertial.h>
42 #include <models/FGMassBalance.h>
43 #include "FGGasCell.h"
45 #if !defined ( sgi ) || defined( __GNUC__ ) && (_COMPILER_VERSION < 740)
53 static const char *IdSrc = "$Id$";
54 static const char *IdHdr = ID_GASCELL;
56 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
58 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
60 const double FGGasCell::R = 3.4071; // [lbs ft/(mol Rankine)]
61 const double FGGasCell::M_air = 0.0019186; // [slug/mol]
62 const double FGGasCell::M_hydrogen = 0.00013841; // [slug/mol]
63 const double FGGasCell::M_helium = 0.00027409; // [slug/mol]
65 FGGasCell::FGGasCell(FGFDMExec* exec, Element* el, int num) : FGForce(exec)
70 Auxiliary = exec->GetAuxiliary();
71 Atmosphere = exec->GetAtmosphere();
72 PropertyManager = exec->GetPropertyManager();
73 Inertial = exec->GetInertial();
74 MassBalance = exec->GetMassBalance();
76 gasCellJ = FGMatrix33();
77 gasCellM = FGColumnVector3();
79 Buoyancy = MaxVolume = MaxOverpressure = Temperature = Pressure =
80 Contents = Volume = dVolumeIdeal = 0.0;
81 Xradius = Yradius = Zradius = Xwidth = Ywidth = Zwidth = 0.0;
82 ValveCoefficient = ValveOpen = 0.0;
85 // NOTE: In the local system X points north, Y points east and Z points down.
86 SetTransformType(FGForce::tLocalBody);
88 type = el->GetAttributeValue("type");
89 if (type == "HYDROGEN") Type = ttHYDROGEN;
90 else if (type == "HELIUM") Type = ttHELIUM;
91 else if (type == "AIR") Type = ttAIR;
92 else Type = ttUNKNOWN;
94 element = el->FindElement("location");
96 vXYZ = element->FindElementTripletConvertTo("IN");
98 cerr << "Fatal Error: No location found for this gas cell." << endl;
101 if ((el->FindElement("x_radius") || el->FindElement("x_width")) &&
102 (el->FindElement("y_radius") || el->FindElement("y_width")) &&
103 (el->FindElement("z_radius") || el->FindElement("z_width"))) {
105 if (el->FindElement("x_radius")) {
106 Xradius = el->FindElementValueAsNumberConvertTo("x_radius", "FT");
108 if (el->FindElement("y_radius")) {
109 Yradius = el->FindElementValueAsNumberConvertTo("y_radius", "FT");
111 if (el->FindElement("z_radius")) {
112 Zradius = el->FindElementValueAsNumberConvertTo("z_radius", "FT");
115 if (el->FindElement("x_width")) {
116 Xwidth = el->FindElementValueAsNumberConvertTo("x_width", "FT");
118 if (el->FindElement("y_width")) {
119 Ywidth = el->FindElementValueAsNumberConvertTo("y_width", "FT");
121 if (el->FindElement("z_width")) {
122 Zwidth = el->FindElementValueAsNumberConvertTo("z_width", "FT");
125 // The volume is a (potentially) extruded ellipsoid.
126 // However, currently only a few combinations of radius and width are
128 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
129 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
131 MaxVolume = 4.0 * M_PI * Xradius * Yradius * Zradius / 3.0;
132 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
133 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
134 // Cylindrical volume.
135 MaxVolume = M_PI * Yradius * Zradius * Xwidth;
137 cerr << "Warning: Unsupported gas cell shape." << endl;
139 (4.0 * M_PI * Xradius * Yradius * Zradius / 3.0 +
140 M_PI * Yradius * Zradius * Xwidth +
141 M_PI * Xradius * Zradius * Ywidth +
142 M_PI * Xradius * Yradius * Zwidth +
143 2.0 * Xradius * Ywidth * Zwidth +
144 2.0 * Yradius * Xwidth * Zwidth +
145 2.0 * Zradius * Xwidth * Ywidth +
146 Xwidth * Ywidth * Zwidth);
149 cerr << "Fatal Error: Gas cell shape must be given." << endl;
152 if (el->FindElement("max_overpressure")) {
153 MaxOverpressure = el->FindElementValueAsNumberConvertTo("max_overpressure",
156 if (el->FindElement("fullness")) {
157 const double Fullness = el->FindElementValueAsNumber("fullness");
159 Volume = Fullness * MaxVolume;
161 cerr << "Warning: Invalid initial gas cell fullness value." << endl;
164 if (el->FindElement("valve_coefficient")) {
166 el->FindElementValueAsNumberConvertTo("valve_coefficient",
168 ValveCoefficient = max(ValveCoefficient, 0.0);
174 if (Temperature == 0.0) {
175 Temperature = Atmosphere->GetTemperature();
177 if (Pressure == 0.0) {
178 Pressure = Atmosphere->GetPressure();
181 // Calculate initial gas content.
182 Contents = Pressure * Volume / (R * Temperature);
184 // Clip to max allowed value.
185 const double IdealPressure = Contents * R * Temperature / MaxVolume;
186 if (IdealPressure > Pressure + MaxOverpressure) {
187 Contents = (Pressure + MaxOverpressure) * MaxVolume / (R * Temperature);
188 Pressure = Pressure + MaxOverpressure;
190 Pressure = max(IdealPressure, Pressure);
193 // Calculate initial gas content.
194 Contents = Pressure * MaxVolume / (R * Temperature);
197 Volume = Contents * R * Temperature / Pressure;
198 Mass = Contents * M_gas();
200 // Bind relevant properties
201 char property_name[80];
202 snprintf(property_name, 80, "buoyant_forces/gas-cell[%d]/max_volume-ft3",
204 PropertyManager->Tie( property_name, &MaxVolume );
205 PropertyManager->SetWritable( property_name, false );
206 snprintf(property_name, 80, "buoyant_forces/gas-cell[%d]/temp-R",
208 PropertyManager->Tie( property_name, &Temperature );
209 snprintf(property_name, 80, "buoyant_forces/gas-cell[%d]/pressure-psf",
211 PropertyManager->Tie( property_name, &Pressure );
212 snprintf(property_name, 80, "buoyant_forces/gas-cell[%d]/volume-ft3",
214 PropertyManager->Tie( property_name, &Volume );
215 snprintf(property_name, 80, "buoyant_forces/gas-cell[%d]/buoyancy-lbs",
217 PropertyManager->Tie( property_name, &Buoyancy );
218 snprintf(property_name, 80, "buoyant_forces/gas-cell[%d]/contents-mol",
220 PropertyManager->Tie( property_name, &Contents );
221 snprintf(property_name, 80, "buoyant_forces/gas-cell[%d]/valve_open",
223 PropertyManager->Tie( property_name, &ValveOpen );
227 // Read heat transfer coefficients
228 if (Element* heat = el->FindElement("heat")) {
229 Element* function_element = heat->FindElement("function");
230 while (function_element) {
231 HeatTransferCoeff.push_back(new FGFunction(PropertyManager,
233 function_element = heat->FindNextElement("function");
237 // Load ballonets if there are any
238 if (Element* ballonet_element = el->FindElement("ballonet")) {
239 while (ballonet_element) {
240 Ballonet.push_back(new FGBallonet(exec,
244 ballonet_element = el->FindNextElement("ballonet");
250 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
252 FGGasCell::~FGGasCell()
256 for (i = 0; i < HeatTransferCoeff.size(); i++) delete HeatTransferCoeff[i];
257 HeatTransferCoeff.clear();
259 for (i = 0; i < Ballonet.size(); i++) delete Ballonet[i];
265 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
267 void FGGasCell::Calculate(double dt)
269 const double AirTemperature = Atmosphere->GetTemperature(); // [Rankine]
270 const double AirPressure = Atmosphere->GetPressure(); // [lbs/ft²]
271 const double AirDensity = Atmosphere->GetDensity(); // [slug/ft³]
272 const double g = Inertial->gravity(); // [lbs/slug]
274 const double OldTemperature = Temperature;
275 const double OldPressure = Pressure;
277 const unsigned int no_ballonets = Ballonet.size();
279 //-- Read ballonet state --
280 // NOTE: This model might need a more proper integration technique.
281 double BallonetsVolume = 0.0;
282 double BallonetsHeatFlow = 0.0;
283 for (i = 0; i < no_ballonets; i++) {
284 BallonetsVolume += Ballonet[i]->GetVolume();
285 BallonetsHeatFlow += Ballonet[i]->GetHeatFlow();
288 //-- Gas temperature --
290 if (HeatTransferCoeff.size() > 0) {
291 // The model is based on the ideal gas law.
292 // However, it does look a bit fishy. Please verify.
293 // dT/dt = dU / (Cv n R)
295 for (i = 0; i < HeatTransferCoeff.size(); i++) {
296 dU += HeatTransferCoeff[i]->GetValue();
298 // Don't include dt when accounting for adiabatic expansion/contraction.
299 // The rate of adiabatic cooling looks about right: ~5.4 Rankine/1000ft.
302 (dU * dt - Pressure * dVolumeIdeal - BallonetsHeatFlow) /
303 (Cv_gas() * Contents * R);
305 Temperature = AirTemperature;
308 // No simulation of complex temperature changes.
309 // Note: Making the gas cell behave adiabatically might be a better
311 Temperature = AirTemperature;
315 const double IdealPressure =
316 Contents * R * Temperature / (MaxVolume - BallonetsVolume);
317 if (IdealPressure > AirPressure + MaxOverpressure) {
318 Pressure = AirPressure + MaxOverpressure;
320 Pressure = max(IdealPressure, AirPressure);
323 //-- Manual valving --
325 // FIXME: Presently the effect of manual valving is computed using
326 // an ad hoc formula which might not be a good representation
328 if ((ValveCoefficient > 0.0) && (ValveOpen > 0.0)) {
329 // First compute the difference in pressure between the gas in the
330 // cell and the air above it.
331 // FixMe: CellHeight should depend on current volume.
332 const double CellHeight = 2 * Zradius + Zwidth; // [ft]
333 const double GasMass = Contents * M_gas(); // [slug]
334 const double GasVolume = Contents * R * Temperature / Pressure; // [ft³]
335 const double GasDensity = GasMass / GasVolume;
336 const double DeltaPressure =
337 Pressure + CellHeight * g * (AirDensity - GasDensity) - AirPressure;
338 const double VolumeValved =
339 ValveOpen * ValveCoefficient * DeltaPressure * dt;
341 max(0.0, Contents - Pressure * VolumeValved / (R * Temperature));
344 //-- Update ballonets. --
345 // Doing that here should give them the opportunity to react to the
347 BallonetsVolume = 0.0;
348 for (i = 0; i < no_ballonets; i++) {
349 Ballonet[i]->Calculate(dt);
350 BallonetsVolume += Ballonet[i]->GetVolume();
353 //-- Automatic safety valving. --
354 if (Contents * R * Temperature / (MaxVolume - BallonetsVolume) >
355 AirPressure + MaxOverpressure) {
356 // Gas is automatically valved. Valving capacity is assumed to be infinite.
357 // FIXME: This could/should be replaced by damage to the gas cell envelope.
359 (AirPressure + MaxOverpressure) *
360 (MaxVolume - BallonetsVolume) / (R * Temperature);
364 Volume = Contents * R * Temperature / Pressure + BallonetsVolume;
366 Contents * R * (Temperature / Pressure - OldTemperature / OldPressure);
368 //-- Current buoyancy --
369 // The buoyancy is computed using the atmospheres local density.
370 Buoyancy = Volume * AirDensity * g;
372 // Note: This is gross buoyancy. The weight of the gas itself and
373 // any ballonets is not deducted here as the effects of the gas mass
374 // is handled by FGMassBalance.
375 vFn.InitMatrix(0.0, 0.0, - Buoyancy);
377 // Compute the inertia of the gas cell.
378 // Consider the gas cell as a shape of uniform density.
379 // FIXME: If the cell isn't ellipsoid or cylindrical the inertia will
381 gasCellJ = FGMatrix33();
382 const double mass = Contents * M_gas();
383 double Ixx, Iyy, Izz;
384 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
385 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
387 Ixx = (1.0 / 5.0) * mass * (Yradius*Yradius + Zradius*Zradius);
388 Iyy = (1.0 / 5.0) * mass * (Xradius*Xradius + Zradius*Zradius);
389 Izz = (1.0 / 5.0) * mass * (Xradius*Xradius + Yradius*Yradius);
390 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
391 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
392 // Cylindrical volume (might not be valid with an elliptical cross-section).
393 Ixx = (1.0 / 2.0) * mass * Yradius * Zradius;
395 (1.0 / 4.0) * mass * Yradius * Zradius +
396 (1.0 / 12.0) * mass * Xwidth * Xwidth;
398 (1.0 / 4.0) * mass * Yradius * Zradius +
399 (1.0 / 12.0) * mass * Xwidth * Xwidth;
401 // Not supported. Revert to pointmass model.
402 Ixx = Iyy = Izz = 0.0;
404 // The volume is symmetric, so Ixy = Ixz = Iyz = 0.
409 gasCellM.InitMatrix();
411 GetXYZ(eX) * Mass*slugtolb;
413 GetXYZ(eY) * Mass*slugtolb;
415 GetXYZ(eZ) * Mass*slugtolb;
417 if (no_ballonets > 0) {
418 // Add the mass, moment and inertia of any ballonets.
419 const FGColumnVector3 p = MassBalance->StructuralToBody( GetXYZ() );
421 for (i = 0; i < no_ballonets; i++) {
422 Mass += Ballonet[i]->GetMass();
424 // Add ballonet moments.
426 Ballonet[i]->GetXYZ(eX) * Ballonet[i]->GetMass()*slugtolb;
428 Ballonet[i]->GetXYZ(eY) * Ballonet[i]->GetMass()*slugtolb;
430 Ballonet[i]->GetXYZ(eZ) * Ballonet[i]->GetMass()*slugtolb;
432 // Moments of inertia must be converted to the gas cell frame here.
434 MassBalance->StructuralToBody( Ballonet[i]->GetXYZ() ) - p;
435 // Body basis is in FT.
436 const double mass = Ballonet[i]->GetMass();
437 gasCellJ += Ballonet[i]->GetInertia() +
438 FGMatrix33( 0, - mass*v(1)*v(2), - mass*v(1)*v(3),
439 - mass*v(2)*v(1), 0, - mass*v(2)*v(3),
440 - mass*v(3)*v(1), - mass*v(3)*v(2), 0 );
445 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
446 // The bitmasked value choices are as follows:
447 // unset: In this case (the default) JSBSim would only print
448 // out the normally expected messages, essentially echoing
449 // the config files as they are read. If the environment
450 // variable is not set, debug_lvl is set to 1 internally
451 // 0: This requests JSBSim not to output any messages
453 // 1: This value explicity requests the normal JSBSim
455 // 2: This value asks for a message to be printed out when
456 // a class is instantiated
457 // 4: When this value is set, a message is displayed when a
458 // FGModel object executes its Run() method
459 // 8: When this value is set, various runtime state variables
460 // are printed out periodically
461 // 16: When set various parameters are sanity checked and
462 // a message is printed out when they go out of bounds
464 void FGGasCell::Debug(int from)
466 if (debug_lvl <= 0) return;
468 if (debug_lvl & 1) { // Standard console startup message output
469 if (from == 0) { // Constructor
470 cout << " Gas cell holds " << Contents << " mol " <<
472 cout << " Cell location (X, Y, Z) (in.): " << vXYZ(eX) << ", " <<
473 vXYZ(eY) << ", " << vXYZ(eZ) << endl;
474 cout << " Maximum volume: " << MaxVolume << " ft3" << endl;
475 cout << " Relief valve release pressure: " << MaxOverpressure <<
477 cout << " Manual valve coefficient: " << ValveCoefficient <<
478 " ft4*sec/slug" << endl;
479 cout << " Initial temperature: " << Temperature << " Rankine" <<
481 cout << " Initial pressure: " << Pressure << " lbs/ft2" << endl;
482 cout << " Initial volume: " << Volume << " ft3" << endl;
483 cout << " Initial mass: " << GetMass() << " slug mass" << endl;
484 cout << " Initial weight: " << GetMass()*lbtoslug << " lbs force" <<
486 cout << " Heat transfer: " << endl;
489 if (debug_lvl & 2 ) { // Instantiation/Destruction notification
490 if (from == 0) cout << "Instantiated: FGGasCell" << endl;
491 if (from == 1) cout << "Destroyed: FGGasCell" << endl;
493 if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
495 if (debug_lvl & 8 ) { // Runtime state variables
496 cout << " " << type << " cell holds " << Contents << " mol " << endl;
497 cout << " Temperature: " << Temperature << " Rankine" << endl;
498 cout << " Pressure: " << Pressure << " lbs/ft2" << endl;
499 cout << " Volume: " << Volume << " ft3" << endl;
500 cout << " Mass: " << GetMass() << " slug mass" << endl;
501 cout << " Weight: " << GetMass()*lbtoslug << " lbs force" << endl;
503 if (debug_lvl & 16) { // Sanity checking
505 if (debug_lvl & 64) {
506 if (from == 0) { // Constructor
507 cout << IdSrc << endl;
508 cout << IdHdr << endl;
513 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
515 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
516 const double FGBallonet::R = 3.4071; // [lbs ft/(mol Rankine)]
517 const double FGBallonet::M_air = 0.0019186; // [slug/mol]
518 const double FGBallonet::Cv_air = 5.0/2.0; // [??]
520 FGBallonet::FGBallonet(FGFDMExec* exec, Element* el, int num, FGGasCell* parent)
525 Auxiliary = exec->GetAuxiliary();
526 Atmosphere = exec->GetAtmosphere();
527 PropertyManager = exec->GetPropertyManager();
528 Inertial = exec->GetInertial();
530 ballonetJ = FGMatrix33();
532 MaxVolume = MaxOverpressure = Temperature = Pressure =
533 Contents = Volume = dVolumeIdeal = dU = 0.0;
534 Xradius = Yradius = Zradius = Xwidth = Ywidth = Zwidth = 0.0;
535 ValveCoefficient = ValveOpen = 0.0;
540 // NOTE: In the local system X points north, Y points east and Z points down.
541 element = el->FindElement("location");
543 vXYZ = element->FindElementTripletConvertTo("IN");
545 cerr << "Fatal Error: No location found for this ballonet." << endl;
548 if ((el->FindElement("x_radius") || el->FindElement("x_width")) &&
549 (el->FindElement("y_radius") || el->FindElement("y_width")) &&
550 (el->FindElement("z_radius") || el->FindElement("z_width"))) {
552 if (el->FindElement("x_radius")) {
553 Xradius = el->FindElementValueAsNumberConvertTo("x_radius", "FT");
555 if (el->FindElement("y_radius")) {
556 Yradius = el->FindElementValueAsNumberConvertTo("y_radius", "FT");
558 if (el->FindElement("z_radius")) {
559 Zradius = el->FindElementValueAsNumberConvertTo("z_radius", "FT");
562 if (el->FindElement("x_width")) {
563 Xwidth = el->FindElementValueAsNumberConvertTo("x_width", "FT");
565 if (el->FindElement("y_width")) {
566 Ywidth = el->FindElementValueAsNumberConvertTo("y_width", "FT");
568 if (el->FindElement("z_width")) {
569 Zwidth = el->FindElementValueAsNumberConvertTo("z_width", "FT");
572 // The volume is a (potentially) extruded ellipsoid.
573 // FIXME: However, currently only a few combinations of radius and
574 // width are fully supported.
575 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
576 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
578 MaxVolume = 4.0 * M_PI * Xradius * Yradius * Zradius / 3.0;
579 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
580 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
581 // Cylindrical volume.
582 MaxVolume = M_PI * Yradius * Zradius * Xwidth;
584 cerr << "Warning: Unsupported ballonet shape." << endl;
586 (4.0 * M_PI * Xradius * Yradius * Zradius / 3.0 +
587 M_PI * Yradius * Zradius * Xwidth +
588 M_PI * Xradius * Zradius * Ywidth +
589 M_PI * Xradius * Yradius * Zwidth +
590 2.0 * Xradius * Ywidth * Zwidth +
591 2.0 * Yradius * Xwidth * Zwidth +
592 2.0 * Zradius * Xwidth * Ywidth +
593 Xwidth * Ywidth * Zwidth);
596 cerr << "Fatal Error: Ballonet shape must be given." << endl;
599 if (el->FindElement("max_overpressure")) {
600 MaxOverpressure = el->FindElementValueAsNumberConvertTo("max_overpressure",
603 if (el->FindElement("fullness")) {
604 const double Fullness = el->FindElementValueAsNumber("fullness");
606 Volume = Fullness * MaxVolume;
608 cerr << "Warning: Invalid initial ballonet fullness value." << endl;
611 if (el->FindElement("valve_coefficient")) {
613 el->FindElementValueAsNumberConvertTo("valve_coefficient",
615 ValveCoefficient = max(ValveCoefficient, 0.0);
619 if (Temperature == 0.0) {
620 Temperature = Parent->GetTemperature();
622 if (Pressure == 0.0) {
623 Pressure = Parent->GetPressure();
626 // Calculate initial air content.
627 Contents = Pressure * Volume / (R * Temperature);
629 // Clip to max allowed value.
630 const double IdealPressure = Contents * R * Temperature / MaxVolume;
631 if (IdealPressure > Pressure + MaxOverpressure) {
632 Contents = (Pressure + MaxOverpressure) * MaxVolume / (R * Temperature);
633 Pressure = Pressure + MaxOverpressure;
635 Pressure = max(IdealPressure, Pressure);
638 // Calculate initial air content.
639 Contents = Pressure * MaxVolume / (R * Temperature);
642 Volume = Contents * R * Temperature / Pressure;
644 // Bind relevant properties
645 char property_name[80];
646 snprintf(property_name, 80,
647 "buoyant_forces/gas-cell[%d]/ballonet[%d]/max_volume-ft3",
650 PropertyManager->Tie( property_name, &MaxVolume );
651 PropertyManager->SetWritable( property_name, false );
652 snprintf(property_name, 80,
653 "buoyant_forces/gas-cell[%d]/ballonet[%d]/temp-R",
656 PropertyManager->Tie( property_name, &Temperature );
657 snprintf(property_name, 80,
658 "buoyant_forces/gas-cell[%d]/ballonet[%d]/pressure-psf",
661 PropertyManager->Tie( property_name, &Pressure );
662 snprintf(property_name, 80,
663 "buoyant_forces/gas-cell[%d]/ballonet[%d]/volume-ft3",
666 PropertyManager->Tie( property_name, &Volume );
667 snprintf(property_name, 80,
668 "buoyant_forces/gas-cell[%d]/ballonet[%d]/contents-mol",
671 PropertyManager->Tie( property_name, &Contents );
672 snprintf(property_name, 80,
673 "buoyant_forces/gas-cell[%d]/ballonet[%d]/valve_open",
676 PropertyManager->Tie( property_name, &ValveOpen );
680 // Read heat transfer coefficients
681 if (Element* heat = el->FindElement("heat")) {
682 Element* function_element = heat->FindElement("function");
683 while (function_element) {
684 HeatTransferCoeff.push_back(new FGFunction(PropertyManager,
686 function_element = heat->FindNextElement("function");
689 // Read blower input function
690 if (Element* blower = el->FindElement("blower_input")) {
691 Element* function_element = blower->FindElement("function");
692 BlowerInput = new FGFunction(PropertyManager,
697 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
699 FGBallonet::~FGBallonet()
703 for (i = 0; i < HeatTransferCoeff.size(); i++) delete HeatTransferCoeff[i];
704 HeatTransferCoeff.clear();
712 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
714 void FGBallonet::Calculate(double dt)
716 const double ParentPressure = Parent->GetPressure(); // [lbs/ft²]
717 const double AirPressure = Atmosphere->GetPressure(); // [lbs/ft²]
719 const double OldTemperature = Temperature;
720 const double OldPressure = Pressure;
723 //-- Gas temperature --
725 // The model is based on the ideal gas law.
726 // However, it does look a bit fishy. Please verify.
727 // dT/dt = dU / (Cv n R)
729 for (i = 0; i < HeatTransferCoeff.size(); i++) {
730 dU += HeatTransferCoeff[i]->GetValue();
732 // dt is already accounted for in dVolumeIdeal.
735 (dU * dt - Pressure * dVolumeIdeal) / (Cv_air * Contents * R);
737 Temperature = Parent->GetTemperature();
741 const double IdealPressure = Contents * R * Temperature / MaxVolume;
742 // The pressure is at least that of the parent gas cell.
743 Pressure = max(IdealPressure, ParentPressure);
747 const double AddedVolume = BlowerInput->GetValue() * dt;
748 if (AddedVolume > 0.0) {
749 Contents += Pressure * AddedVolume / (R * Temperature);
753 //-- Pressure relief and manual valving --
754 // FIXME: Presently the effect of valving is computed using
755 // an ad hoc formula which might not be a good representation
757 if ((ValveCoefficient > 0.0) &&
758 ((ValveOpen > 0.0) || (Pressure > AirPressure + MaxOverpressure))) {
759 const double DeltaPressure = Pressure - AirPressure;
760 const double VolumeValved =
761 ((Pressure > AirPressure + MaxOverpressure) ? 1.0 : ValveOpen) *
762 ValveCoefficient * DeltaPressure * dt;
763 // FIXME: Too small values of Contents sometimes leads to NaN.
764 // Currently the minimum is restricted to a safe value.
766 max(1.0, Contents - Pressure * VolumeValved / (R * Temperature));
770 Volume = Contents * R * Temperature / Pressure;
772 Contents * R * (Temperature / Pressure - OldTemperature / OldPressure);
774 // Compute the inertia of the ballonet.
775 // Consider the ballonet as a shape of uniform density.
776 // FIXME: If the ballonet isn't ellipsoid or cylindrical the inertia will
778 ballonetJ = FGMatrix33();
779 const double mass = Contents * M_air;
780 double Ixx, Iyy, Izz;
781 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
782 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
784 Ixx = (1.0 / 5.0) * mass * (Yradius*Yradius + Zradius*Zradius);
785 Iyy = (1.0 / 5.0) * mass * (Xradius*Xradius + Zradius*Zradius);
786 Izz = (1.0 / 5.0) * mass * (Xradius*Xradius + Yradius*Yradius);
787 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
788 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
789 // Cylindrical volume (might not be valid with an elliptical cross-section).
790 Ixx = (1.0 / 2.0) * mass * Yradius * Zradius;
792 (1.0 / 4.0) * mass * Yradius * Zradius +
793 (1.0 / 12.0) * mass * Xwidth * Xwidth;
795 (1.0 / 4.0) * mass * Yradius * Zradius +
796 (1.0 / 12.0) * mass * Xwidth * Xwidth;
798 // Not supported. Revert to pointmass model.
799 Ixx = Iyy = Izz = 0.0;
801 // The volume is symmetric, so Ixy = Ixz = Iyz = 0.
802 ballonetJ(1,1) = Ixx;
803 ballonetJ(2,2) = Iyy;
804 ballonetJ(3,3) = Izz;
807 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
808 // The bitmasked value choices are as follows:
809 // unset: In this case (the default) JSBSim would only print
810 // out the normally expected messages, essentially echoing
811 // the config files as they are read. If the environment
812 // variable is not set, debug_lvl is set to 1 internally
813 // 0: This requests JSBSim not to output any messages
815 // 1: This value explicity requests the normal JSBSim
817 // 2: This value asks for a message to be printed out when
818 // a class is instantiated
819 // 4: When this value is set, a message is displayed when a
820 // FGModel object executes its Run() method
821 // 8: When this value is set, various runtime state variables
822 // are printed out periodically
823 // 16: When set various parameters are sanity checked and
824 // a message is printed out when they go out of bounds
826 void FGBallonet::Debug(int from)
828 if (debug_lvl <= 0) return;
830 if (debug_lvl & 1) { // Standard console startup message output
831 if (from == 0) { // Constructor
832 cout << " Ballonet holds " << Contents << " mol air" << endl;
833 cout << " Location (X, Y, Z) (in.): " << vXYZ(eX) << ", " <<
834 vXYZ(eY) << ", " << vXYZ(eZ) << endl;
835 cout << " Maximum volume: " << MaxVolume << " ft3" << endl;
836 cout << " Relief valve release pressure: " << MaxOverpressure <<
838 cout << " Relief valve coefficient: " << ValveCoefficient <<
839 " ft4*sec/slug" << endl;
840 cout << " Initial temperature: " << Temperature << " Rankine" <<
842 cout << " Initial pressure: " << Pressure << " lbs/ft2" << endl;
843 cout << " Initial volume: " << Volume << " ft3" << endl;
844 cout << " Initial mass: " << GetMass() << " slug mass" << endl;
845 cout << " Initial weight: " << GetMass()*lbtoslug <<
846 " lbs force" << endl;
847 cout << " Heat transfer: " << endl;
850 if (debug_lvl & 2 ) { // Instantiation/Destruction notification
851 if (from == 0) cout << "Instantiated: FGBallonet" << endl;
852 if (from == 1) cout << "Destroyed: FGBallonet" << endl;
854 if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
856 if (debug_lvl & 8 ) { // Runtime state variables
857 cout << " Ballonet holds " << Contents <<
859 cout << " Temperature: " << Temperature << " Rankine" << endl;
860 cout << " Pressure: " << Pressure << " lbs/ft2" << endl;
861 cout << " Volume: " << Volume << " ft3" << endl;
862 cout << " Mass: " << GetMass() << " slug mass" << endl;
863 cout << " Weight: " << GetMass()*lbtoslug << " lbs force" << endl;
865 if (debug_lvl & 16) { // Sanity checking
867 if (debug_lvl & 64) {
868 if (from == 0) { // Constructor
869 cout << IdSrc << endl;
870 cout << IdHdr << endl;