JSBSim Flight Dynamics Model  1.1.11 (13 Feb 2022)
An Open Source Flight Dynamics and Control Software Library in C++
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 
26 FUNCTIONAL DESCRIPTION
27 --------------------------------------------------------------------------------
28 See header file.
29 
30 HISTORY
31 --------------------------------------------------------------------------------
32 01/21/2006 AG Created
33 
34 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
35 INCLUDES
36 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
37 
38 #include "FGFDMExec.h"
39 #include "models/FGMassBalance.h"
40 #include "FGGasCell.h"
41 #include "input_output/FGXMLElement.h"
42 
43 using std::cerr;
44 using std::endl;
45 using std::cout;
46 using std::string;
47 using std::max;
48 
49 namespace JSBSim {
50 
51 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
52 CLASS IMPLEMENTATION
53 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
54 /* Constants. */
55 const double FGGasCell::R = 3.4071; // [lbf ft/(mol Rankine)]
56 const double FGGasCell::M_air = 0.0019186; // [slug/mol]
57 const double FGGasCell::M_hydrogen = 0.00013841; // [slug/mol]
58 const double FGGasCell::M_helium = 0.00027409; // [slug/mol]
59 
60 FGGasCell::FGGasCell(FGFDMExec* exec, Element* el, unsigned int num,
61  const struct Inputs& input)
62  : FGForce(exec), in(input)
63 {
64  string token;
65  Element* element;
66 
67  FGPropertyManager* PropertyManager = exec->GetPropertyManager();
68  MassBalance = exec->GetMassBalance();
69 
70  Buoyancy = MaxVolume = MaxOverpressure = Temperature = Pressure =
71  Contents = Volume = dVolumeIdeal = 0.0;
72  Xradius = Yradius = Zradius = Xwidth = Ywidth = Zwidth = 0.0;
73  ValveCoefficient = ValveOpen = 0.0;
74  CellNum = num;
75 
76  // NOTE: In the local system X points north, Y points east and Z points down.
77  SetTransformType(FGForce::tLocalBody);
78 
79  type = el->GetAttributeValue("type");
80  if (type == "HYDROGEN") Type = ttHYDROGEN;
81  else if (type == "HELIUM") Type = ttHELIUM;
82  else if (type == "AIR") Type = ttAIR;
83  else Type = ttUNKNOWN;
84 
85  element = el->FindElement("location");
86  if (element) {
87  vXYZ = element->FindElementTripletConvertTo("IN");
88  } else {
89  const string s("Fatal Error: No location found for this gas cell.");
90  cerr << el->ReadFrom() << endl << s << endl;
91  throw BaseException(s);
92  }
93  if ((el->FindElement("x_radius") || el->FindElement("x_width")) &&
94  (el->FindElement("y_radius") || el->FindElement("y_width")) &&
95  (el->FindElement("z_radius") || el->FindElement("z_width"))) {
96 
97  if (el->FindElement("x_radius")) {
98  Xradius = el->FindElementValueAsNumberConvertTo("x_radius", "FT");
99  }
100  if (el->FindElement("y_radius")) {
101  Yradius = el->FindElementValueAsNumberConvertTo("y_radius", "FT");
102  }
103  if (el->FindElement("z_radius")) {
104  Zradius = el->FindElementValueAsNumberConvertTo("z_radius", "FT");
105  }
106 
107  if (el->FindElement("x_width")) {
108  Xwidth = el->FindElementValueAsNumberConvertTo("x_width", "FT");
109  }
110  if (el->FindElement("y_width")) {
111  Ywidth = el->FindElementValueAsNumberConvertTo("y_width", "FT");
112  }
113  if (el->FindElement("z_width")) {
114  Zwidth = el->FindElementValueAsNumberConvertTo("z_width", "FT");
115  }
116 
117  // The volume is a (potentially) extruded ellipsoid.
118  // However, currently only a few combinations of radius and width are
119  // fully supported.
120  if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
121  (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
122  // Ellipsoid volume.
123  MaxVolume = 4.0 * M_PI * Xradius * Yradius * Zradius / 3.0;
124  } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
125  (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
126  // Cylindrical volume.
127  MaxVolume = M_PI * Yradius * Zradius * Xwidth;
128  } else {
129  cerr << "Warning: Unsupported gas cell shape." << endl;
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  const string s("Fatal Error: Gas cell shape must be given.");
142  cerr << el->ReadFrom() << endl << s << endl;
143  throw BaseException(s);
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  cerr << "Warning: Invalid initial gas cell fullness value." << endl;
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()->SetWritable( property_name, 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 
240 FGGasCell::~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 
255 void 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.size() > 0) {
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(0.0, 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 
445 void 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  cout << " Gas cell holds " << Contents << " mol " <<
452  type << endl;
453  cout << " Cell location (X, Y, Z) (in.): " << vXYZ(eX) << ", " <<
454  vXYZ(eY) << ", " << vXYZ(eZ) << endl;
455  cout << " Maximum volume: " << MaxVolume << " ft3" << endl;
456  cout << " Relief valve release pressure: " << MaxOverpressure <<
457  " lbs/ft2" << endl;
458  cout << " Manual valve coefficient: " << ValveCoefficient <<
459  " ft4*sec/slug" << endl;
460  cout << " Initial temperature: " << Temperature << " Rankine" <<
461  endl;
462  cout << " Initial pressure: " << Pressure << " lbs/ft2" << endl;
463  cout << " Initial volume: " << Volume << " ft3" << endl;
464  cout << " Initial mass: " << GetMass() << " slug mass" << endl;
465  cout << " Initial weight: " << GetMass()*slugtolb << " lbs force" <<
466  endl;
467  cout << " Heat transfer: " << endl;
468  }
469  }
470  if (debug_lvl & 2 ) { // Instantiation/Destruction notification
471  if (from == 0) cout << "Instantiated: FGGasCell" << endl;
472  if (from == 1) cout << "Destroyed: FGGasCell" << endl;
473  }
474  if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
475  }
476  if (debug_lvl & 8 ) { // Runtime state variables
477  cout << " " << type << " cell holds " << Contents << " mol " << endl;
478  cout << " Temperature: " << Temperature << " Rankine" << endl;
479  cout << " Pressure: " << Pressure << " lbs/ft2" << endl;
480  cout << " Volume: " << Volume << " ft3" << endl;
481  cout << " Mass: " << GetMass() << " slug mass" << endl;
482  cout << " Weight: " << GetMass()*slugtolb << " lbs force" << endl;
483  }
484  if (debug_lvl & 16) { // Sanity checking
485  }
486  if (debug_lvl & 64) {
487  if (from == 0) { // Constructor
488  }
489  }
490 }
491 
492 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
493 CLASS IMPLEMENTATION
494 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
495 const double FGBallonet::R = 3.4071; // [lbs ft/(mol Rankine)]
496 const double FGBallonet::M_air = 0.0019186; // [slug/mol]
497 const double FGBallonet::Cv_air = 5.0/2.0; // [??]
498 
499 FGBallonet::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  FGPropertyManager* 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  const string s("Fatal Error: No location found for this ballonet.");
523  cerr << el->ReadFrom() << endl << s << endl;
524  throw BaseException(s);
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  cerr << "Warning: Unsupported ballonet shape." << endl;
563  MaxVolume =
564  (4.0 * M_PI * Xradius * Yradius * Zradius / 3.0 +
565  M_PI * Yradius * Zradius * Xwidth +
566  M_PI * Xradius * Zradius * Ywidth +
567  M_PI * Xradius * Yradius * Zwidth +
568  2.0 * Xradius * Ywidth * Zwidth +
569  2.0 * Yradius * Xwidth * Zwidth +
570  2.0 * Zradius * Xwidth * Ywidth +
571  Xwidth * Ywidth * Zwidth);
572  }
573  } else {
574  const string s("Fatal Error: Ballonet shape must be given.");
575  cerr << el->ReadFrom() << endl << s << endl;
576  throw BaseException(s);
577  }
578  if (el->FindElement("max_overpressure")) {
579  MaxOverpressure = el->FindElementValueAsNumberConvertTo("max_overpressure",
580  "LBS/FT2");
581  }
582  if (el->FindElement("fullness")) {
583  const double Fullness = el->FindElementValueAsNumber("fullness");
584  if (0 <= Fullness) {
585  Volume = Fullness * MaxVolume;
586  } else {
587  cerr << "Warning: Invalid initial ballonet fullness value." << endl;
588  }
589  }
590  if (el->FindElement("valve_coefficient")) {
591  ValveCoefficient =
592  el->FindElementValueAsNumberConvertTo("valve_coefficient",
593  "FT4*SEC/SLUG");
594  ValveCoefficient = max(ValveCoefficient, 0.0);
595  }
596 
597  // Initialize state
598  if (Temperature == 0.0) {
599  Temperature = Parent->GetTemperature();
600  }
601  if (Pressure == 0.0) {
602  Pressure = Parent->GetPressure();
603  }
604  if (Volume != 0.0) {
605  // Calculate initial air content.
606  Contents = Pressure * Volume / (R * Temperature);
607 
608  // Clip to max allowed value.
609  const double IdealPressure = Contents * R * Temperature / MaxVolume;
610  if (IdealPressure > Pressure + MaxOverpressure) {
611  Contents = (Pressure + MaxOverpressure) * MaxVolume / (R * Temperature);
612  Pressure = Pressure + MaxOverpressure;
613  } else {
614  Pressure = max(IdealPressure, Pressure);
615  }
616  } else {
617  // Calculate initial air content.
618  Contents = Pressure * MaxVolume / (R * Temperature);
619  }
620 
621  Volume = Contents * R * Temperature / Pressure;
622 
623  // Bind relevant properties
624  string property_name, base_property_name;
625  base_property_name = CreateIndexedPropertyName("buoyant_forces/gas-cell", Parent->GetIndex());
626  base_property_name = CreateIndexedPropertyName(base_property_name + "/ballonet", CellNum);
627 
628  property_name = base_property_name + "/max_volume-ft3";
629  PropertyManager->Tie( property_name, &MaxVolume);
630  PropertyManager->GetNode()->SetWritable( property_name, false );
631 
632  property_name = base_property_name + "/temp-R";
633  PropertyManager->Tie( property_name, &Temperature);
634 
635  property_name = base_property_name + "/pressure-psf";
636  PropertyManager->Tie( property_name, &Pressure);
637 
638  property_name = base_property_name + "/volume-ft3";
639  PropertyManager->Tie( property_name, &Volume);
640 
641  property_name = base_property_name + "/contents-mol";
642  PropertyManager->Tie( property_name, &Contents);
643 
644  property_name = base_property_name + "/valve_open";
645  PropertyManager->Tie( property_name, &ValveOpen);
646 
647  Debug(0);
648 
649  // Read heat transfer coefficients
650  if (Element* heat = el->FindElement("heat")) {
651  Element* function_element = heat->FindElement("function");
652  while (function_element) {
653  HeatTransferCoeff.push_back(new FGFunction(exec, function_element));
654  function_element = heat->FindNextElement("function");
655  }
656  }
657  // Read blower input function
658  if (Element* blower = el->FindElement("blower_input")) {
659  Element* function_element = blower->FindElement("function");
660  BlowerInput = new FGFunction(exec, function_element);
661  }
662 }
663 
664 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
665 
666 FGBallonet::~FGBallonet()
667 {
668  unsigned int i;
669 
670  for (i = 0; i < HeatTransferCoeff.size(); i++) delete HeatTransferCoeff[i];
671  HeatTransferCoeff.clear();
672 
673  delete BlowerInput;
674  BlowerInput = NULL;
675 
676  Debug(1);
677 }
678 
679 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
680 
681 void FGBallonet::Calculate(double dt)
682 {
683  const double ParentPressure = Parent->GetPressure(); // [lbs/ft^2]
684  const double AirPressure = in.Pressure; // [lbs/ft^2]
685 
686  const double OldTemperature = Temperature;
687  const double OldPressure = Pressure;
688  unsigned int i;
689 
690  //-- Gas temperature --
691 
692  // The model is based on the ideal gas law.
693  // However, it does look a bit fishy. Please verify.
694  // dT/dt = dU / (Cv n R)
695  dU = 0.0;
696  for (i = 0; i < HeatTransferCoeff.size(); i++) {
697  dU += HeatTransferCoeff[i]->GetValue();
698  }
699  // dt is already accounted for in dVolumeIdeal.
700  if (Contents > 0) {
701  Temperature +=
702  (dU * dt - Pressure * dVolumeIdeal) / (Cv_air * Contents * R);
703  } else {
704  Temperature = Parent->GetTemperature();
705  }
706 
707  //-- Pressure --
708  const double IdealPressure = Contents * R * Temperature / MaxVolume;
709  // The pressure is at least that of the parent gas cell.
710  Pressure = max(IdealPressure, ParentPressure);
711 
712  //-- Blower input --
713  if (BlowerInput) {
714  const double AddedVolume = BlowerInput->GetValue() * dt;
715  if (AddedVolume > 0.0) {
716  Contents += Pressure * AddedVolume / (R * Temperature);
717  }
718  }
719 
720  //-- Pressure relief and manual valving --
721  // FIXME: Presently the effect of valving is computed using
722  // an ad hoc formula which might not be a good representation
723  // of reality.
724  if ((ValveCoefficient > 0.0) &&
725  ((ValveOpen > 0.0) || (Pressure > AirPressure + MaxOverpressure))) {
726  const double DeltaPressure = Pressure - AirPressure;
727  const double VolumeValved =
728  ((Pressure > AirPressure + MaxOverpressure) ? 1.0 : ValveOpen) *
729  ValveCoefficient * DeltaPressure * dt;
730  // FIXME: Too small values of Contents sometimes leads to NaN.
731  // Currently the minimum is restricted to a safe value.
732  Contents =
733  max(1.0, Contents - Pressure * VolumeValved / (R * Temperature));
734  }
735 
736  //-- Volume --
737  Volume = Contents * R * Temperature / Pressure;
738  dVolumeIdeal =
739  Contents * R * (Temperature / Pressure - OldTemperature / OldPressure);
740 
741  // Compute the inertia of the ballonet.
742  // Consider the ballonet as a shape of uniform density.
743  // FIXME: If the ballonet isn't ellipsoid or cylindrical the inertia will
744  // be wrong.
745  ballonetJ.InitMatrix();
746  const double mass = Contents * M_air;
747  double Ixx, Iyy, Izz;
748  if ((Xradius != 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
749  (Xwidth == 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
750  // Ellipsoid volume.
751  Ixx = (1.0 / 5.0) * mass * (Yradius*Yradius + Zradius*Zradius);
752  Iyy = (1.0 / 5.0) * mass * (Xradius*Xradius + Zradius*Zradius);
753  Izz = (1.0 / 5.0) * mass * (Xradius*Xradius + Yradius*Yradius);
754  } else if ((Xradius == 0.0) && (Yradius != 0.0) && (Zradius != 0.0) &&
755  (Xwidth != 0.0) && (Ywidth == 0.0) && (Zwidth == 0.0)) {
756  // Cylindrical volume (might not be valid with an elliptical cross-section).
757  Ixx = (1.0 / 2.0) * mass * Yradius * Zradius;
758  Iyy =
759  (1.0 / 4.0) * mass * Yradius * Zradius +
760  (1.0 / 12.0) * mass * Xwidth * Xwidth;
761  Izz =
762  (1.0 / 4.0) * mass * Yradius * Zradius +
763  (1.0 / 12.0) * mass * Xwidth * Xwidth;
764  } else {
765  // Not supported. Revert to pointmass model.
766  Ixx = Iyy = Izz = 0.0;
767  }
768  // The volume is symmetric, so Ixy = Ixz = Iyz = 0.
769  ballonetJ(1,1) = Ixx;
770  ballonetJ(2,2) = Iyy;
771  ballonetJ(3,3) = Izz;
772  // Transform the moments of inertia to the body frame.
773  ballonetJ += MassBalance->GetPointmassInertia(GetMass(), GetXYZ());
774 }
775 
776 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
777 // The bitmasked value choices are as follows:
778 // unset: In this case (the default) JSBSim would only print
779 // out the normally expected messages, essentially echoing
780 // the config files as they are read. If the environment
781 // variable is not set, debug_lvl is set to 1 internally
782 // 0: This requests JSBSim not to output any messages
783 // whatsoever.
784 // 1: This value explicity requests the normal JSBSim
785 // startup messages
786 // 2: This value asks for a message to be printed out when
787 // a class is instantiated
788 // 4: When this value is set, a message is displayed when a
789 // FGModel object executes its Run() method
790 // 8: When this value is set, various runtime state variables
791 // are printed out periodically
792 // 16: When set various parameters are sanity checked and
793 // a message is printed out when they go out of bounds
794 
795 void FGBallonet::Debug(int from)
796 {
797  if (debug_lvl <= 0) return;
798 
799  if (debug_lvl & 1) { // Standard console startup message output
800  if (from == 0) { // Constructor
801  cout << " Ballonet holds " << Contents << " mol air" << endl;
802  cout << " Location (X, Y, Z) (in.): " << vXYZ(eX) << ", " <<
803  vXYZ(eY) << ", " << vXYZ(eZ) << endl;
804  cout << " Maximum volume: " << MaxVolume << " ft3" << endl;
805  cout << " Relief valve release pressure: " << MaxOverpressure <<
806  " lbs/ft2" << endl;
807  cout << " Relief valve coefficient: " << ValveCoefficient <<
808  " ft4*sec/slug" << endl;
809  cout << " Initial temperature: " << Temperature << " Rankine" <<
810  endl;
811  cout << " Initial pressure: " << Pressure << " lbs/ft2" << endl;
812  cout << " Initial volume: " << Volume << " ft3" << endl;
813  cout << " Initial mass: " << GetMass() << " slug mass" << endl;
814  cout << " Initial weight: " << GetMass()*slugtolb <<
815  " lbs force" << endl;
816  cout << " Heat transfer: " << endl;
817  }
818  }
819  if (debug_lvl & 2 ) { // Instantiation/Destruction notification
820  if (from == 0) cout << "Instantiated: FGBallonet" << endl;
821  if (from == 1) cout << "Destroyed: FGBallonet" << endl;
822  }
823  if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
824  }
825  if (debug_lvl & 8 ) { // Runtime state variables
826  cout << " Ballonet holds " << Contents <<
827  " mol air" << endl;
828  cout << " Temperature: " << Temperature << " Rankine" << endl;
829  cout << " Pressure: " << Pressure << " lbs/ft2" << endl;
830  cout << " Volume: " << Volume << " ft3" << endl;
831  cout << " Mass: " << GetMass() << " slug mass" << endl;
832  cout << " Weight: " << GetMass()*slugtolb << " lbs force" << endl;
833  }
834  if (debug_lvl & 16) { // Sanity checking
835  }
836  if (debug_lvl & 64) {
837  if (from == 0) { // Constructor
838  }
839  }
840 }
841 }
JSBSim::FGPropertyNode::SetWritable
void SetWritable(const std::string &name, bool state=true)
Set the state of the write attribute for a property.
Definition: FGPropertyManager.cpp:285
JSBSim::FGFDMExec
Encapsulates the JSBSim simulation executive.
Definition: FGFDMExec.h:185
JSBSim::FGGasCell::Inputs
Definition: FGGasCell.h:169
JSBSim::FGGasCell::Calculate
void Calculate(double dt)
Runs the gas cell model; called by BuoyantForces.
Definition: FGGasCell.cpp:255
JSBSim::FGFunction
Represents a mathematical function.
Definition: FGFunction.h:752
JSBSim::Element::GetAttributeValue
std::string GetAttributeValue(const std::string &key)
Retrieves an attribute.
Definition: FGXMLElement.cpp:260
JSBSim::Element::FindElement
Element * FindElement(const std::string &el="")
Searches for a specified element.
Definition: FGXMLElement.cpp:389
JSBSim::Element::FindElementValueAsNumber
double FindElementValueAsNumber(const std::string &el="")
Searches for the named element and returns the data belonging to it as a number.
Definition: FGXMLElement.cpp:429
JSBSim::Element::FindElementValueAsNumberConvertTo
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.
Definition: FGXMLElement.cpp:480
JSBSim::FGFDMExec::GetPropertyManager
FGPropertyManager * GetPropertyManager(void)
Returns a pointer to the property manager object.
Definition: FGFDMExec.cpp:1121
JSBSim::FGBallonet::GetXYZ
const FGColumnVector3 & GetXYZ(void) const
Get the center of gravity location of the ballonet.
Definition: FGGasCell.h:316
JSBSim::BaseException
Definition: FGJSBBase.h:59
JSBSim::FGBallonet
Models a ballonet inside a gas cell.
Definition: FGGasCell.h:302
JSBSim::FGGasCell::GetTemperature
double GetTemperature(void) const
Get the current gas temperature inside the gas cell.
Definition: FGGasCell.h:220
JSBSim::FGGasCell::GetMass
double GetMass(void) const
Get the current mass of the gas cell (including any ballonets)
Definition: FGGasCell.h:204
JSBSim::FGJSBBase::slugtolb
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:366
JSBSim::FGForce
Utility class that aids in the conversion of forces between coordinate systems and calculation of mom...
Definition: FGForce.h:221
JSBSim::FGFDMExec::GetMassBalance
FGMassBalance * GetMassBalance(void)
Returns the FGAircraft pointer.
Definition: FGFDMExec.h:363
JSBSim::FGGasCell::FGGasCell
FGGasCell(FGFDMExec *exec, Element *el, unsigned int num, const struct Inputs &input)
Constructor.
Definition: FGGasCell.cpp:60
JSBSim::FGGasCell::GetXYZ
const FGColumnVector3 & GetXYZ(void) const
Get the center of gravity location of the gas cell (including any ballonets)
Definition: FGGasCell.h:195
JSBSim::FGGasCell::GetPressure
double GetPressure(void) const
Get the current gas pressure inside the gas cell.
Definition: FGGasCell.h:224
JSBSim::FGBallonet::GetMass
double GetMass(void) const
Get the current mass of the ballonets.
Definition: FGGasCell.h:323
JSBSim::FGBallonet::Calculate
void Calculate(double dt)
Runs the ballonet model; called by FGGasCell.
Definition: FGGasCell.cpp:681
JSBSim::FGFunction::GetValue
double GetValue(void) const override
Retrieves the value of the function object.
Definition: FGFunction.cpp:925
JSBSim::Element::ReadFrom
std::string ReadFrom(void) const
Return a string that contains a description of the location where the current XML element was read fr...
Definition: FGXMLElement.cpp:738
JSBSim::Element::FindNextElement
Element * FindNextElement(const std::string &el="")
Searches for the next element as specified.
Definition: FGXMLElement.cpp:407
JSBSim::FGMatrix33::InitMatrix
void InitMatrix(void)
Initialize the matrix.
Definition: FGMatrix33.cpp:259
JSBSim::FGMassBalance::GetPointmassInertia
FGMatrix33 GetPointmassInertia(double mass_sl, const FGColumnVector3 &r) const
Computes the inertia contribution of a pointmass.
Definition: FGMassBalance.h:159
JSBSim::FGPropertyManager
Definition: FGPropertyManager.h:373
JSBSim::Element
Definition: FGXMLElement.h:143
JSBSim::FGPropertyManager::Tie
void Tie(const std::string &name, T *pointer)
Tie a property to an external variable.
Definition: FGPropertyManager.h:449
JSBSim::Element::FindElementTripletConvertTo
FGColumnVector3 FindElementTripletConvertTo(const std::string &target_units)
Composes a 3-element column vector for the supplied location or orientation.
Definition: FGXMLElement.cpp:589