1 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4 Author: Anders Gidenstam
5 Date started: 01/21/2006
7 ----- Copyright (C) 2006 - 2011 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/FGMassBalance.h"
40 #include "FGGasCell.h"
41 #include "input_output/FGXMLElement.h"
53 static const char *IdSrc = "$Id: FGGasCell.cpp,v 1.15 2011/08/06 13:47:59 jberndt Exp $";
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, const struct Inputs& input)
66 : FGForce(exec), in(input)
71 PropertyManager = exec->GetPropertyManager();
72 MassBalance = exec->GetMassBalance();
74 gasCellJ = FGMatrix33();
75 gasCellM = FGColumnVector3();
77 Buoyancy = MaxVolume = MaxOverpressure = Temperature = Pressure =
78 Contents = Volume = dVolumeIdeal = 0.0;
79 Xradius = Yradius = Zradius = Xwidth = Ywidth = Zwidth = 0.0;
80 ValveCoefficient = ValveOpen = 0.0;
83 // NOTE: In the local system X points north, Y points east and Z points down.
84 SetTransformType(FGForce::tLocalBody);
86 type = el->GetAttributeValue("type");
87 if (type == "HYDROGEN") Type = ttHYDROGEN;
88 else if (type == "HELIUM") Type = ttHELIUM;
89 else if (type == "AIR") Type = ttAIR;
90 else Type = ttUNKNOWN;
92 element = el->FindElement("location");
94 vXYZ = element->FindElementTripletConvertTo("IN");
96 cerr << "Fatal Error: No location found for this gas cell." << endl;
99 if ((el->FindElement("x_radius") || el->FindElement("x_width")) &&
100 (el->FindElement("y_radius") || el->FindElement("y_width")) &&
101 (el->FindElement("z_radius") || el->FindElement("z_width"))) {
103 if (el->FindElement("x_radius")) {
104 Xradius = el->FindElementValueAsNumberConvertTo("x_radius", "FT");
106 if (el->FindElement("y_radius")) {
107 Yradius = el->FindElementValueAsNumberConvertTo("y_radius", "FT");
109 if (el->FindElement("z_radius")) {
110 Zradius = el->FindElementValueAsNumberConvertTo("z_radius", "FT");
113 if (el->FindElement("x_width")) {
114 Xwidth = el->FindElementValueAsNumberConvertTo("x_width", "FT");
116 if (el->FindElement("y_width")) {
117 Ywidth = el->FindElementValueAsNumberConvertTo("y_width", "FT");
119 if (el->FindElement("z_width")) {
120 Zwidth = el->FindElementValueAsNumberConvertTo("z_width", "FT");
123 // The volume is a (potentially) extruded ellipsoid.
124 // However, currently only a few combinations of radius and width are
126 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
127 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
129 MaxVolume = 4.0 * M_PI * Xradius * Yradius * Zradius / 3.0;
130 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
131 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
132 // Cylindrical volume.
133 MaxVolume = M_PI * Yradius * Zradius * Xwidth;
135 cerr << "Warning: Unsupported gas cell shape." << endl;
137 (4.0 * M_PI * Xradius * Yradius * Zradius / 3.0 +
138 M_PI * Yradius * Zradius * Xwidth +
139 M_PI * Xradius * Zradius * Ywidth +
140 M_PI * Xradius * Yradius * Zwidth +
141 2.0 * Xradius * Ywidth * Zwidth +
142 2.0 * Yradius * Xwidth * Zwidth +
143 2.0 * Zradius * Xwidth * Ywidth +
144 Xwidth * Ywidth * Zwidth);
147 cerr << "Fatal Error: Gas cell shape must be given." << endl;
150 if (el->FindElement("max_overpressure")) {
151 MaxOverpressure = el->FindElementValueAsNumberConvertTo("max_overpressure",
154 if (el->FindElement("fullness")) {
155 const double Fullness = el->FindElementValueAsNumber("fullness");
157 Volume = Fullness * MaxVolume;
159 cerr << "Warning: Invalid initial gas cell fullness value." << endl;
162 if (el->FindElement("valve_coefficient")) {
164 el->FindElementValueAsNumberConvertTo("valve_coefficient",
166 ValveCoefficient = max(ValveCoefficient, 0.0);
172 if (Temperature == 0.0) {
173 Temperature = in.Temperature;
175 if (Pressure == 0.0) {
176 Pressure = in.Pressure;
179 // Calculate initial gas content.
180 Contents = Pressure * Volume / (R * Temperature);
182 // Clip to max allowed value.
183 const double IdealPressure = Contents * R * Temperature / MaxVolume;
184 if (IdealPressure > Pressure + MaxOverpressure) {
185 Contents = (Pressure + MaxOverpressure) * MaxVolume / (R * Temperature);
186 Pressure = Pressure + MaxOverpressure;
188 Pressure = max(IdealPressure, Pressure);
191 // Calculate initial gas content.
192 Contents = Pressure * MaxVolume / (R * Temperature);
195 Volume = Contents * R * Temperature / Pressure;
196 Mass = Contents * M_gas();
198 // Bind relevant properties
199 string property_name, base_property_name;
201 base_property_name = CreateIndexedPropertyName("buoyant_forces/gas-cell", CellNum);
203 property_name = base_property_name + "/max_volume-ft3";
204 PropertyManager->Tie( property_name.c_str(), &MaxVolume, false );
205 PropertyManager->SetWritable( property_name, false );
206 property_name = base_property_name + "/temp-R";
207 PropertyManager->Tie( property_name.c_str(), &Temperature, false );
208 property_name = base_property_name + "/pressure-psf";
209 PropertyManager->Tie( property_name.c_str(), &Pressure, false );
210 property_name = base_property_name + "/volume-ft3";
211 PropertyManager->Tie( property_name.c_str(), &Volume, false );
212 property_name = base_property_name + "/buoyancy-lbs";
213 PropertyManager->Tie( property_name.c_str(), &Buoyancy, false );
214 property_name = base_property_name + "/contents-mol";
215 PropertyManager->Tie( property_name.c_str(), &Contents, false );
216 property_name = base_property_name + "/valve_open";
217 PropertyManager->Tie( property_name.c_str(), &ValveOpen, false );
221 // Read heat transfer coefficients
222 if (Element* heat = el->FindElement("heat")) {
223 Element* function_element = heat->FindElement("function");
224 while (function_element) {
225 HeatTransferCoeff.push_back(new FGFunction(PropertyManager,
227 function_element = heat->FindNextElement("function");
231 // Load ballonets if there are any
232 if (Element* ballonet_element = el->FindElement("ballonet")) {
233 while (ballonet_element) {
234 Ballonet.push_back(new FGBallonet(exec,
238 ballonet_element = el->FindNextElement("ballonet");
244 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
246 FGGasCell::~FGGasCell()
250 for (i = 0; i < HeatTransferCoeff.size(); i++) delete HeatTransferCoeff[i];
251 HeatTransferCoeff.clear();
253 for (i = 0; i < Ballonet.size(); i++) delete Ballonet[i];
259 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
261 void FGGasCell::Calculate(double dt)
263 const double AirTemperature = in.Temperature; // [Rankine]
264 const double AirPressure = in.Pressure; // [lbs/ft^2]
265 const double AirDensity = in.Density; // [slug/ft^3]
266 const double g = in.gravity; // [lbs/slug]
268 const double OldTemperature = Temperature;
269 const double OldPressure = Pressure;
271 const unsigned int no_ballonets = Ballonet.size();
273 //-- Read ballonet state --
274 // NOTE: This model might need a more proper integration technique.
275 double BallonetsVolume = 0.0;
276 double BallonetsHeatFlow = 0.0;
277 for (i = 0; i < no_ballonets; i++) {
278 BallonetsVolume += Ballonet[i]->GetVolume();
279 BallonetsHeatFlow += Ballonet[i]->GetHeatFlow();
282 //-- Gas temperature --
284 if (HeatTransferCoeff.size() > 0) {
285 // The model is based on the ideal gas law.
286 // However, it does look a bit fishy. Please verify.
287 // dT/dt = dU / (Cv n R)
289 for (i = 0; i < HeatTransferCoeff.size(); i++) {
290 dU += HeatTransferCoeff[i]->GetValue();
292 // Don't include dt when accounting for adiabatic expansion/contraction.
293 // The rate of adiabatic cooling looks about right: ~5.4 Rankine/1000ft.
296 (dU * dt - Pressure * dVolumeIdeal - BallonetsHeatFlow) /
297 (Cv_gas() * Contents * R);
299 Temperature = AirTemperature;
302 // No simulation of complex temperature changes.
303 // Note: Making the gas cell behave adiabatically might be a better
305 Temperature = AirTemperature;
309 const double IdealPressure =
310 Contents * R * Temperature / (MaxVolume - BallonetsVolume);
311 if (IdealPressure > AirPressure + MaxOverpressure) {
312 Pressure = AirPressure + MaxOverpressure;
314 Pressure = max(IdealPressure, AirPressure);
317 //-- Manual valving --
319 // FIXME: Presently the effect of manual valving is computed using
320 // an ad hoc formula which might not be a good representation
322 if ((ValveCoefficient > 0.0) && (ValveOpen > 0.0)) {
323 // First compute the difference in pressure between the gas in the
324 // cell and the air above it.
325 // FixMe: CellHeight should depend on current volume.
326 const double CellHeight = 2 * Zradius + Zwidth; // [ft]
327 const double GasMass = Contents * M_gas(); // [slug]
328 const double GasVolume = Contents * R * Temperature / Pressure; // [ft³]
329 const double GasDensity = GasMass / GasVolume;
330 const double DeltaPressure =
331 Pressure + CellHeight * g * (AirDensity - GasDensity) - AirPressure;
332 const double VolumeValved =
333 ValveOpen * ValveCoefficient * DeltaPressure * dt;
335 max(0.0, Contents - Pressure * VolumeValved / (R * Temperature));
338 //-- Update ballonets. --
339 // Doing that here should give them the opportunity to react to the
341 BallonetsVolume = 0.0;
342 for (i = 0; i < no_ballonets; i++) {
343 Ballonet[i]->Calculate(dt);
344 BallonetsVolume += Ballonet[i]->GetVolume();
347 //-- Automatic safety valving. --
348 if (Contents * R * Temperature / (MaxVolume - BallonetsVolume) >
349 AirPressure + MaxOverpressure) {
350 // Gas is automatically valved. Valving capacity is assumed to be infinite.
351 // FIXME: This could/should be replaced by damage to the gas cell envelope.
353 (AirPressure + MaxOverpressure) *
354 (MaxVolume - BallonetsVolume) / (R * Temperature);
358 Volume = Contents * R * Temperature / Pressure + BallonetsVolume;
360 Contents * R * (Temperature / Pressure - OldTemperature / OldPressure);
362 //-- Current buoyancy --
363 // The buoyancy is computed using the atmospheres local density.
364 Buoyancy = Volume * AirDensity * g;
366 // Note: This is gross buoyancy. The weight of the gas itself and
367 // any ballonets is not deducted here as the effects of the gas mass
368 // is handled by FGMassBalance.
369 vFn.InitMatrix(0.0, 0.0, - Buoyancy);
371 // Compute the inertia of the gas cell.
372 // Consider the gas cell as a shape of uniform density.
373 // FIXME: If the cell isn't ellipsoid or cylindrical the inertia will
375 gasCellJ = FGMatrix33();
376 const double mass = Contents * M_gas();
377 double Ixx, Iyy, Izz;
378 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
379 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
381 Ixx = (1.0 / 5.0) * mass * (Yradius*Yradius + Zradius*Zradius);
382 Iyy = (1.0 / 5.0) * mass * (Xradius*Xradius + Zradius*Zradius);
383 Izz = (1.0 / 5.0) * mass * (Xradius*Xradius + Yradius*Yradius);
384 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
385 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
386 // Cylindrical volume (might not be valid with an elliptical cross-section).
387 Ixx = (1.0 / 2.0) * mass * Yradius * Zradius;
389 (1.0 / 4.0) * mass * Yradius * Zradius +
390 (1.0 / 12.0) * mass * Xwidth * Xwidth;
392 (1.0 / 4.0) * mass * Yradius * Zradius +
393 (1.0 / 12.0) * mass * Xwidth * Xwidth;
395 // Not supported. Revert to pointmass model.
396 Ixx = Iyy = Izz = 0.0;
398 // The volume is symmetric, so Ixy = Ixz = Iyz = 0.
403 // Transform the moments of inertia to the body frame.
404 gasCellJ += MassBalance->GetPointmassInertia(Mass, GetXYZ());
406 gasCellM.InitMatrix();
408 GetXYZ(eX) * Mass*slugtolb;
410 GetXYZ(eY) * Mass*slugtolb;
412 GetXYZ(eZ) * Mass*slugtolb;
414 if (no_ballonets > 0) {
415 // Add the mass, moment and inertia of any ballonets.
416 for (i = 0; i < no_ballonets; i++) {
417 Mass += Ballonet[i]->GetMass();
419 // Add ballonet moments due to mass (in the structural frame).
421 Ballonet[i]->GetXYZ(eX) * Ballonet[i]->GetMass()*slugtolb;
423 Ballonet[i]->GetXYZ(eY) * Ballonet[i]->GetMass()*slugtolb;
425 Ballonet[i]->GetXYZ(eZ) * Ballonet[i]->GetMass()*slugtolb;
427 gasCellJ += Ballonet[i]->GetInertia();
432 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
433 // The bitmasked value choices are as follows:
434 // unset: In this case (the default) JSBSim would only print
435 // out the normally expected messages, essentially echoing
436 // the config files as they are read. If the environment
437 // variable is not set, debug_lvl is set to 1 internally
438 // 0: This requests JSBSim not to output any messages
440 // 1: This value explicity requests the normal JSBSim
442 // 2: This value asks for a message to be printed out when
443 // a class is instantiated
444 // 4: When this value is set, a message is displayed when a
445 // FGModel object executes its Run() method
446 // 8: When this value is set, various runtime state variables
447 // are printed out periodically
448 // 16: When set various parameters are sanity checked and
449 // a message is printed out when they go out of bounds
451 void FGGasCell::Debug(int from)
453 if (debug_lvl <= 0) return;
455 if (debug_lvl & 1) { // Standard console startup message output
456 if (from == 0) { // Constructor
457 cout << " Gas cell holds " << Contents << " mol " <<
459 cout << " Cell location (X, Y, Z) (in.): " << vXYZ(eX) << ", " <<
460 vXYZ(eY) << ", " << vXYZ(eZ) << endl;
461 cout << " Maximum volume: " << MaxVolume << " ft3" << endl;
462 cout << " Relief valve release pressure: " << MaxOverpressure <<
464 cout << " Manual valve coefficient: " << ValveCoefficient <<
465 " ft4*sec/slug" << endl;
466 cout << " Initial temperature: " << Temperature << " Rankine" <<
468 cout << " Initial pressure: " << Pressure << " lbs/ft2" << endl;
469 cout << " Initial volume: " << Volume << " ft3" << endl;
470 cout << " Initial mass: " << GetMass() << " slug mass" << endl;
471 cout << " Initial weight: " << GetMass()*lbtoslug << " lbs force" <<
473 cout << " Heat transfer: " << endl;
476 if (debug_lvl & 2 ) { // Instantiation/Destruction notification
477 if (from == 0) cout << "Instantiated: FGGasCell" << endl;
478 if (from == 1) cout << "Destroyed: FGGasCell" << endl;
480 if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
482 if (debug_lvl & 8 ) { // Runtime state variables
483 cout << " " << type << " cell holds " << Contents << " mol " << endl;
484 cout << " Temperature: " << Temperature << " Rankine" << endl;
485 cout << " Pressure: " << Pressure << " lbs/ft2" << endl;
486 cout << " Volume: " << Volume << " ft3" << endl;
487 cout << " Mass: " << GetMass() << " slug mass" << endl;
488 cout << " Weight: " << GetMass()*lbtoslug << " lbs force" << endl;
490 if (debug_lvl & 16) { // Sanity checking
492 if (debug_lvl & 64) {
493 if (from == 0) { // Constructor
494 cout << IdSrc << endl;
495 cout << IdHdr << endl;
500 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
502 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
503 const double FGBallonet::R = 3.4071; // [lbs ft/(mol Rankine)]
504 const double FGBallonet::M_air = 0.0019186; // [slug/mol]
505 const double FGBallonet::Cv_air = 5.0/2.0; // [??]
507 FGBallonet::FGBallonet(FGFDMExec* exec, Element* el, int num, FGGasCell* parent, const struct FGGasCell::Inputs& input)
513 PropertyManager = exec->GetPropertyManager();
514 MassBalance = exec->GetMassBalance();
516 ballonetJ = FGMatrix33();
518 MaxVolume = MaxOverpressure = Temperature = Pressure =
519 Contents = Volume = dVolumeIdeal = dU = 0.0;
520 Xradius = Yradius = Zradius = Xwidth = Ywidth = Zwidth = 0.0;
521 ValveCoefficient = ValveOpen = 0.0;
526 // NOTE: In the local system X points north, Y points east and Z points down.
527 element = el->FindElement("location");
529 vXYZ = element->FindElementTripletConvertTo("IN");
531 cerr << "Fatal Error: No location found for this ballonet." << endl;
534 if ((el->FindElement("x_radius") || el->FindElement("x_width")) &&
535 (el->FindElement("y_radius") || el->FindElement("y_width")) &&
536 (el->FindElement("z_radius") || el->FindElement("z_width"))) {
538 if (el->FindElement("x_radius")) {
539 Xradius = el->FindElementValueAsNumberConvertTo("x_radius", "FT");
541 if (el->FindElement("y_radius")) {
542 Yradius = el->FindElementValueAsNumberConvertTo("y_radius", "FT");
544 if (el->FindElement("z_radius")) {
545 Zradius = el->FindElementValueAsNumberConvertTo("z_radius", "FT");
548 if (el->FindElement("x_width")) {
549 Xwidth = el->FindElementValueAsNumberConvertTo("x_width", "FT");
551 if (el->FindElement("y_width")) {
552 Ywidth = el->FindElementValueAsNumberConvertTo("y_width", "FT");
554 if (el->FindElement("z_width")) {
555 Zwidth = el->FindElementValueAsNumberConvertTo("z_width", "FT");
558 // The volume is a (potentially) extruded ellipsoid.
559 // FIXME: However, currently only a few combinations of radius and
560 // width are fully supported.
561 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
562 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
564 MaxVolume = 4.0 * M_PI * Xradius * Yradius * Zradius / 3.0;
565 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
566 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
567 // Cylindrical volume.
568 MaxVolume = M_PI * Yradius * Zradius * Xwidth;
570 cerr << "Warning: Unsupported ballonet shape." << endl;
572 (4.0 * M_PI * Xradius * Yradius * Zradius / 3.0 +
573 M_PI * Yradius * Zradius * Xwidth +
574 M_PI * Xradius * Zradius * Ywidth +
575 M_PI * Xradius * Yradius * Zwidth +
576 2.0 * Xradius * Ywidth * Zwidth +
577 2.0 * Yradius * Xwidth * Zwidth +
578 2.0 * Zradius * Xwidth * Ywidth +
579 Xwidth * Ywidth * Zwidth);
582 cerr << "Fatal Error: Ballonet shape must be given." << endl;
585 if (el->FindElement("max_overpressure")) {
586 MaxOverpressure = el->FindElementValueAsNumberConvertTo("max_overpressure",
589 if (el->FindElement("fullness")) {
590 const double Fullness = el->FindElementValueAsNumber("fullness");
592 Volume = Fullness * MaxVolume;
594 cerr << "Warning: Invalid initial ballonet fullness value." << endl;
597 if (el->FindElement("valve_coefficient")) {
599 el->FindElementValueAsNumberConvertTo("valve_coefficient",
601 ValveCoefficient = max(ValveCoefficient, 0.0);
605 if (Temperature == 0.0) {
606 Temperature = Parent->GetTemperature();
608 if (Pressure == 0.0) {
609 Pressure = Parent->GetPressure();
612 // Calculate initial air content.
613 Contents = Pressure * Volume / (R * Temperature);
615 // Clip to max allowed value.
616 const double IdealPressure = Contents * R * Temperature / MaxVolume;
617 if (IdealPressure > Pressure + MaxOverpressure) {
618 Contents = (Pressure + MaxOverpressure) * MaxVolume / (R * Temperature);
619 Pressure = Pressure + MaxOverpressure;
621 Pressure = max(IdealPressure, Pressure);
624 // Calculate initial air content.
625 Contents = Pressure * MaxVolume / (R * Temperature);
628 Volume = Contents * R * Temperature / Pressure;
630 // Bind relevant properties
631 string property_name, base_property_name;
632 base_property_name = CreateIndexedPropertyName("buoyant_forces/gas-cell", Parent->GetIndex());
633 base_property_name = CreateIndexedPropertyName(base_property_name + "/ballonet", CellNum);
635 property_name = base_property_name + "/max_volume-ft3";
636 PropertyManager->Tie( property_name, &MaxVolume, false );
637 PropertyManager->SetWritable( property_name, false );
639 property_name = base_property_name + "/temp-R";
640 PropertyManager->Tie( property_name, &Temperature, false );
642 property_name = base_property_name + "/pressure-psf";
643 PropertyManager->Tie( property_name, &Pressure, false );
645 property_name = base_property_name + "/volume-ft3";
646 PropertyManager->Tie( property_name, &Volume, false );
648 property_name = base_property_name + "/contents-mol";
649 PropertyManager->Tie( property_name, &Contents, false );
651 property_name = base_property_name + "/valve_open";
652 PropertyManager->Tie( property_name, &ValveOpen, false );
656 // Read heat transfer coefficients
657 if (Element* heat = el->FindElement("heat")) {
658 Element* function_element = heat->FindElement("function");
659 while (function_element) {
660 HeatTransferCoeff.push_back(new FGFunction(PropertyManager,
662 function_element = heat->FindNextElement("function");
665 // Read blower input function
666 if (Element* blower = el->FindElement("blower_input")) {
667 Element* function_element = blower->FindElement("function");
668 BlowerInput = new FGFunction(PropertyManager,
673 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
675 FGBallonet::~FGBallonet()
679 for (i = 0; i < HeatTransferCoeff.size(); i++) delete HeatTransferCoeff[i];
680 HeatTransferCoeff.clear();
688 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
690 void FGBallonet::Calculate(double dt)
692 const double ParentPressure = Parent->GetPressure(); // [lbs/ft^2]
693 const double AirPressure = in.Pressure; // [lbs/ft^2]
695 const double OldTemperature = Temperature;
696 const double OldPressure = Pressure;
699 //-- Gas temperature --
701 // The model is based on the ideal gas law.
702 // However, it does look a bit fishy. Please verify.
703 // dT/dt = dU / (Cv n R)
705 for (i = 0; i < HeatTransferCoeff.size(); i++) {
706 dU += HeatTransferCoeff[i]->GetValue();
708 // dt is already accounted for in dVolumeIdeal.
711 (dU * dt - Pressure * dVolumeIdeal) / (Cv_air * Contents * R);
713 Temperature = Parent->GetTemperature();
717 const double IdealPressure = Contents * R * Temperature / MaxVolume;
718 // The pressure is at least that of the parent gas cell.
719 Pressure = max(IdealPressure, ParentPressure);
723 const double AddedVolume = BlowerInput->GetValue() * dt;
724 if (AddedVolume > 0.0) {
725 Contents += Pressure * AddedVolume / (R * Temperature);
729 //-- Pressure relief and manual valving --
730 // FIXME: Presently the effect of valving is computed using
731 // an ad hoc formula which might not be a good representation
733 if ((ValveCoefficient > 0.0) &&
734 ((ValveOpen > 0.0) || (Pressure > AirPressure + MaxOverpressure))) {
735 const double DeltaPressure = Pressure - AirPressure;
736 const double VolumeValved =
737 ((Pressure > AirPressure + MaxOverpressure) ? 1.0 : ValveOpen) *
738 ValveCoefficient * DeltaPressure * dt;
739 // FIXME: Too small values of Contents sometimes leads to NaN.
740 // Currently the minimum is restricted to a safe value.
742 max(1.0, Contents - Pressure * VolumeValved / (R * Temperature));
746 Volume = Contents * R * Temperature / Pressure;
748 Contents * R * (Temperature / Pressure - OldTemperature / OldPressure);
750 // Compute the inertia of the ballonet.
751 // Consider the ballonet as a shape of uniform density.
752 // FIXME: If the ballonet isn't ellipsoid or cylindrical the inertia will
754 ballonetJ = FGMatrix33();
755 const double mass = Contents * M_air;
756 double Ixx, Iyy, Izz;
757 if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
758 (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
760 Ixx = (1.0 / 5.0) * mass * (Yradius*Yradius + Zradius*Zradius);
761 Iyy = (1.0 / 5.0) * mass * (Xradius*Xradius + Zradius*Zradius);
762 Izz = (1.0 / 5.0) * mass * (Xradius*Xradius + Yradius*Yradius);
763 } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
764 (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
765 // Cylindrical volume (might not be valid with an elliptical cross-section).
766 Ixx = (1.0 / 2.0) * mass * Yradius * Zradius;
768 (1.0 / 4.0) * mass * Yradius * Zradius +
769 (1.0 / 12.0) * mass * Xwidth * Xwidth;
771 (1.0 / 4.0) * mass * Yradius * Zradius +
772 (1.0 / 12.0) * mass * Xwidth * Xwidth;
774 // Not supported. Revert to pointmass model.
775 Ixx = Iyy = Izz = 0.0;
777 // The volume is symmetric, so Ixy = Ixz = Iyz = 0.
778 ballonetJ(1,1) = Ixx;
779 ballonetJ(2,2) = Iyy;
780 ballonetJ(3,3) = Izz;
781 // Transform the moments of inertia to the body frame.
782 ballonetJ += MassBalance->GetPointmassInertia(GetMass(), GetXYZ());
785 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
786 // The bitmasked value choices are as follows:
787 // unset: In this case (the default) JSBSim would only print
788 // out the normally expected messages, essentially echoing
789 // the config files as they are read. If the environment
790 // variable is not set, debug_lvl is set to 1 internally
791 // 0: This requests JSBSim not to output any messages
793 // 1: This value explicity requests the normal JSBSim
795 // 2: This value asks for a message to be printed out when
796 // a class is instantiated
797 // 4: When this value is set, a message is displayed when a
798 // FGModel object executes its Run() method
799 // 8: When this value is set, various runtime state variables
800 // are printed out periodically
801 // 16: When set various parameters are sanity checked and
802 // a message is printed out when they go out of bounds
804 void FGBallonet::Debug(int from)
806 if (debug_lvl <= 0) return;
808 if (debug_lvl & 1) { // Standard console startup message output
809 if (from == 0) { // Constructor
810 cout << " Ballonet holds " << Contents << " mol air" << endl;
811 cout << " Location (X, Y, Z) (in.): " << vXYZ(eX) << ", " <<
812 vXYZ(eY) << ", " << vXYZ(eZ) << endl;
813 cout << " Maximum volume: " << MaxVolume << " ft3" << endl;
814 cout << " Relief valve release pressure: " << MaxOverpressure <<
816 cout << " Relief valve coefficient: " << ValveCoefficient <<
817 " ft4*sec/slug" << endl;
818 cout << " Initial temperature: " << Temperature << " Rankine" <<
820 cout << " Initial pressure: " << Pressure << " lbs/ft2" << endl;
821 cout << " Initial volume: " << Volume << " ft3" << endl;
822 cout << " Initial mass: " << GetMass() << " slug mass" << endl;
823 cout << " Initial weight: " << GetMass()*lbtoslug <<
824 " lbs force" << endl;
825 cout << " Heat transfer: " << endl;
828 if (debug_lvl & 2 ) { // Instantiation/Destruction notification
829 if (from == 0) cout << "Instantiated: FGBallonet" << endl;
830 if (from == 1) cout << "Destroyed: FGBallonet" << endl;
832 if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
834 if (debug_lvl & 8 ) { // Runtime state variables
835 cout << " Ballonet holds " << Contents <<
837 cout << " Temperature: " << Temperature << " Rankine" << endl;
838 cout << " Pressure: " << Pressure << " lbs/ft2" << endl;
839 cout << " Volume: " << Volume << " ft3" << endl;
840 cout << " Mass: " << GetMass() << " slug mass" << endl;
841 cout << " Weight: " << GetMass()*lbtoslug << " lbs force" << endl;
843 if (debug_lvl & 16) { // Sanity checking
845 if (debug_lvl & 64) {
846 if (from == 0) { // Constructor
847 cout << IdSrc << endl;
848 cout << IdHdr << endl;