diff --git a/.github/workflows/cpp-python-build.yml b/.github/workflows/cpp-python-build.yml
index 5a49357630..f73308950d 100644
--- a/.github/workflows/cpp-python-build.yml
+++ b/.github/workflows/cpp-python-build.yml
@@ -668,7 +668,7 @@ jobs:
find . -type f -name *.exe -exec cp {} dist/. \;
find . -type f -name *.deb -exec cp {} dist/. \;
- name: Rolling release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -724,7 +724,7 @@ jobs:
export MSVC_VERSION=`cat MSVC_version.txt`
echo "MSVC_VERSION=$MSVC_VERSION" >> $GITHUB_ENV
- name: GitHub release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
diff --git a/JSBSim.vcxproj.filters b/JSBSim.vcxproj.filters
index b274b95ceb..4fc92c21c9 100644
--- a/JSBSim.vcxproj.filters
+++ b/JSBSim.vcxproj.filters
@@ -205,6 +205,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/JSBSimForUnreal.vcxproj b/JSBSimForUnreal.vcxproj
index 17181f6734..8d75d3b473 100644
--- a/JSBSimForUnreal.vcxproj
+++ b/JSBSimForUnreal.vcxproj
@@ -214,6 +214,7 @@
+
diff --git a/JSBSimForUnreal.vcxproj.filters b/JSBSimForUnreal.vcxproj.filters
index 727c71aee9..6715f9ef36 100644
--- a/JSBSimForUnreal.vcxproj.filters
+++ b/JSBSimForUnreal.vcxproj.filters
@@ -212,6 +212,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/README.md b/README.md
index 4047af35ea..30e342fb0e 100644
--- a/README.md
+++ b/README.md
@@ -222,6 +222,14 @@ We are also on Facebook:
# Legal Notice
+
+
+
+
+
+
+
+
The JSBSim library is open source and is licensed under the [LGPL 2.1 license](https://opensource.org/licenses/LGPL-2.1). The license is included in the source code file [COPYING](https://github.com/JSBSim-Team/jsbsim/blob/master/COPYING).
The Unreal Engine Reference Application for JSBSim is open source and is licensed under the [MIT license](https://opensource.org/licenses/MIT). The license is included in the source code file [UnrealEngine/LICENSE.txt](https://github.com/JSBSim-Team/jsbsim/blob/master/UnrealEngine/LICENSE.txt).
diff --git a/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Private/JSBSimMovementComponent.cpp b/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Private/JSBSimMovementComponent.cpp
index a3755907c4..4988c6dcdc 100644
--- a/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Private/JSBSimMovementComponent.cpp
+++ b/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Private/JSBSimMovementComponent.cpp
@@ -35,6 +35,7 @@
#include "models/FGMassBalance.h"
#include "models/propulsion/FGThruster.h"
#include "models/propulsion/FGPiston.h"
+#include "models/propulsion/FGPistonDiesel.h"
#include "models/propulsion/FGTurbine.h"
#include "models/propulsion/FGTurboProp.h"
#include "models/propulsion/FGTank.h"
@@ -356,7 +357,7 @@ void UJSBSimMovementComponent::TickComponent(float DeltaTime, ELevelTick TickTyp
FTransform ENUTransform = GeoReferencingSystem->GetTangentTransformAtECEFLocation(AircraftState.ECEFLocation);
FRotator LocalUERotation(AircraftState.LocalEulerAngles);
LocalUERotation.Yaw -= 90.0; // JSBSim heading is aero heading (0 at north). We have to remove 90 because in UE, 0 is pointing east.
- if (GeoReferencingSystem->PlanetShape == EPlanetShape::FlatPlanet) //Fix for Flat Planet bug
+ if (GeoReferencingSystem->PlanetShape == EPlanetShape::FlatPlanet) //Fix for Flat Planet bug
{
LocalUERotation.Yaw -= 180.0;
ECEFForwardHorizontal.Y *= -1.0;
@@ -1080,6 +1081,15 @@ void UJSBSimMovementComponent::GetEnginesStates()
EngineStates[i].Magnetos = (EMagnetosMode)PistonEngine->GetMagnetos();
break;
}
+ case JSBSim::FGEngine::etPistonDiesel:
+ {
+ std::shared_ptr DieselEngine =
+ std::static_pointer_cast(Engine);
+ EngineStates[i].FuelRack = DieselEngine->getFuelRack();
+ EngineStates[i].CoolantTemp = DieselEngine->getCoolantTemp_degF();
+ EngineStates[i].GlowPlugOn = DieselEngine->getGlowPlugOn();
+ break;
+ }
case JSBSim::FGEngine::etTurbine:
{
// TODO
diff --git a/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Public/FDMTypes.h b/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Public/FDMTypes.h
index d845ae9ba3..480f480354 100644
--- a/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Public/FDMTypes.h
+++ b/UnrealEngine/Plugins/JSBSimFlightDynamicsModel/Source/JSBSimFlightDynamicsModel/Public/FDMTypes.h
@@ -41,7 +41,7 @@ struct FTank
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
double TemperatureCelcius = 0;
- // Possible Other Functions - Fill, Drain...
+ // Possible Other Functions - Fill, Drain...
FString GetDebugMessage()
{
@@ -67,7 +67,7 @@ struct FGear
double NormalizedPosition = 1;
/*
- * Doesn't exist in JSBSim, but need to be set in Editor in case you want to do separate gear animations
+ * Doesn't exist in JSBSim, but need to be set in Editor in case you want to do separate gear animations
*/
UPROPERTY(BlueprintReadOnly, EditAnywhere)
bool IsFrontBogey = false;
@@ -104,12 +104,12 @@ struct FGear
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
FVector Force = FVector(FVector::ZeroVector);
- // Possible Other Functions - Steering, Compression...
+ // Possible Other Functions - Steering, Compression...
FString GetDebugMessage()
{
FString DebugMessage;
- FString UpDownState("I");
+ FString UpDownState("I");
if (NormalizedPosition == 0) UpDownState = FString("U");
if (NormalizedPosition == 1) UpDownState = FString("D");
DebugMessage += FString::Printf(TEXT(" NormPosition %.2f [%s] WOW %d RollLinVel %.1f Force %.1f"), NormalizedPosition, *UpDownState, HasWeightOnWheel, WheelRollLinearVelocityMetersPerSec, Force.Length()) + LINE_TERMINATOR;
@@ -123,6 +123,7 @@ enum class EEngineType : uint8 {
Unknown,
Rocket,
Piston,
+ PistonDiesel,
Turbine,
Turboprop,
Electric
@@ -151,11 +152,11 @@ struct FEngineCommand
/* Normalized [0..1] value expected */
UPROPERTY(BlueprintReadWrite, EditAnywhere)
double Throttle = 0.0f;
-
+
/* Normalized [0..1] value expected */
UPROPERTY(BlueprintReadWrite, EditAnywhere)
double Mixture = 0.0f;
-
+
UPROPERTY(BlueprintReadWrite, EditAnywhere)
bool Starter = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
@@ -176,13 +177,13 @@ struct FEngineCommand
bool Injection = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
int32 Ignition = 0;
-
+
// Turbine & TurboPropeller Engine Commands
UPROPERTY(BlueprintReadWrite, EditAnywhere)
bool Reverse = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
bool CutOff = false;
-
+
// TurboPropeller Engine Commands
UPROPERTY(BlueprintReadWrite, EditAnywhere)
bool GeneratorPower = false;
@@ -222,7 +223,7 @@ struct FEngineState
double Thrust = 0;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
double EngineRPM = 0;
-
+
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
double N1 = 0; // Turbine
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
@@ -236,7 +237,7 @@ struct FEngineState
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
bool CutOff = false; // Turbine + TurboProp
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
- int Ignition = 0; // Turbine +
+ int Ignition = 0; // Turbine +
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
bool GeneratorPower = false; // TurboProp
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
@@ -244,7 +245,7 @@ struct FEngineState
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
EMagnetosMode Magnetos = EMagnetosMode::Off; // Piston
-
+
FString GetDebugMessage()
{
FString DebugMessage;
@@ -254,12 +255,12 @@ struct FEngineState
{
DebugMessage += FString::Printf(TEXT(" N1 %.2f N2 %.2f CutOff %d Augmentation %d Reversed %d Injection %d Ignition %d"), N1, N2, CutOff, Augmentation, Reversed, Injection, Ignition) + LINE_TERMINATOR;
}
-
+
if (EngineType == EEngineType::Piston)
{
DebugMessage += FString::Printf(TEXT(" Magnetos %s "), *UEnum::GetValueAsName(Magnetos).ToString()) + LINE_TERMINATOR;
}
-
+
return DebugMessage;
}
};
@@ -387,22 +388,22 @@ struct FAircraftState
double AltitudeRateFtps = 0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Speed")
double StallWarning = 0;
-
-
+
+
// Transformation
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Transformation")
FVector ECEFLocation = FVector::ZeroVector;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Transformation")
double Latitude = 0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Transformation")
- double Longitude = 0;
+ double Longitude = 0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Transformation")
FRotator LocalEulerAngles = FRotator::ZeroRotator;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Transformation")
FVector EulerRates = FVector::ZeroVector;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Transformation")
FVector UEForwardHorizontal = FVector::ZeroVector;
-
+
// Misc
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Misc")
bool Crashed = false;
@@ -448,7 +449,7 @@ enum class ETurbType:uint8
Milspec UMETA(DisplayName = "Milspec turbulence model (Dryden spectrum)"),
//Similar to ttMilspec, this model also uses the Dryden spectrum. The main difference lies in how the transfer functions are implemented based on the specifications in the military document. It helps in simulating realistic turbulence under similar conditions as the Milspec model.
Tustin UMETA(DisplayName = "Tustin turbulence model (Dryden spectrum)") ,
-
+
};
/*
* Wind State
@@ -495,8 +496,8 @@ struct JSBSIMFLIGHTDYNAMICSMODEL_API FSimpleWindState
static const FSimpleWindState StandardNorthZephyr;
//South wind, a wind speed that people perceive as relatively strong.
static const FSimpleWindState StandardSouthZephyr;
-
-
-
+
+
+
};
diff --git a/aircraft/gazaile/.gitkeep b/aircraft/gazaile/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/aircraft/gazaile/Engines/GazAile_Elec_Engine.xml b/aircraft/gazaile/Engines/GazAile_Elec_Engine.xml
new file mode 100644
index 0000000000..06ccc1dbf7
--- /dev/null
+++ b/aircraft/gazaile/Engines/GazAile_Elec_Engine.xml
@@ -0,0 +1,4 @@
+
+
+ 41000
+
diff --git a/aircraft/gazaile/Engines/GazAile_Engine.xml b/aircraft/gazaile/Engines/GazAile_Engine.xml
new file mode 100644
index 0000000000..490e32e860
--- /dev/null
+++ b/aircraft/gazaile/Engines/GazAile_Engine.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ 83.0
+ 52
+ 5500
+ 5000
+ 2500
+ 1700
+ 4
+ 2.953
+ 3.0315
+ 4
+ 22
+ 1.0
+ 30.0
+ 2000
+ IDI
+ 20000
+ 9000
+
\ No newline at end of file
diff --git a/aircraft/gazaile/Engines/GazAile_Prop.xml b/aircraft/gazaile/Engines/GazAile_Prop.xml
new file mode 100644
index 0000000000..3086ce54d5
--- /dev/null
+++ b/aircraft/gazaile/Engines/GazAile_Prop.xml
@@ -0,0 +1,79 @@
+
+
+
+
+ 1.20
+ 61.0
+ 2
+ 1.88
+ 1.00
+ 1.30
+
+
+
+ 0.0 0.0991
+ 0.1 0.0946
+ 0.2 0.0890
+ 0.3 0.0824
+ 0.4 0.0748
+ 0.5 0.0662
+ 0.6 0.0565
+ 0.7 0.0457
+ 0.8 0.0340
+ 0.9 0.0212
+ 1.0 0.0073
+ 1.1 -0.0071
+ 1.2 -0.0212
+
+
+
+
+
+ 0.0 0.0419
+ 0.1 0.0485
+ 0.2 0.0528
+ 0.3 0.0550
+ 0.4 0.0550
+ 0.5 0.0528
+ 0.6 0.0485
+ 0.7 0.0419
+ 0.8 0.0332
+ 0.9 0.0222
+ 1.0 0.0091
+ 1.1 0.0011
+ 1.2 -0.0069
+
+
+
+
+
+
+ 0.85 1.0
+ 1.05 0.8
+
+
+
+
+
+
+ 0.85 1.0
+ 1.05 1.8
+ 2.00 1.4
+
+
+
+
\ No newline at end of file
diff --git a/aircraft/gazaile/gazaile.xml b/aircraft/gazaile/gazaile.xml
new file mode 100644
index 0000000000..74c2e5d496
--- /dev/null
+++ b/aircraft/gazaile/gazaile.xml
@@ -0,0 +1,1040 @@
+
+
+
+
+
+ Aeromatic v 0.96
+ 2026-04-02
+ $Revision: 1.20 $
+ Models a GazAile_ULM. Standalone JSBSim file (no FlightGear dependencies).
+
+
+
+
+
+
+ 87.73
+ 26.90
+ 3.00
+ 3.26
+ 14.43
+ 11.81
+ 7.75
+ 9.19
+
+ 74.81
+ 0.00
+ 0.00
+
+
+ 28.15
+ -18.00
+ 45.00
+
+
+ 0
+ 0
+ 0
+
+
+
+ 248
+ 199
+ 419
+ 606
+
+ 74.81
+ 0.00
+ -5.41
+
+
+ 166.5
+
+ 74.81
+ 0.00
+ -5.41
+
+
+
+
+
+
+
+
+ 28.15
+ 0.00
+ -25.99
+
+ 0.80
+ 0.50
+ 0.02
+ 330.75
+ 165.38
+ 5.00
+ NONE
+ 0
+
+
+
+
+ 77.80
+ -29.06
+ -25.99
+
+ 0.80
+ 0.50
+ 0.02
+ 1102.50
+ 551.25
+ 0
+ LEFT
+ 0
+
+
+
+
+ 77.80
+ 29.06
+ -25.99
+
+ 0.80
+ 0.50
+ 0.02
+ 1102.50
+ 551.25
+ 0
+ RIGHT
+ 0
+
+
+
+
+ 74.81
+ -13.45
+ -5.41
+
+ 1.00
+ 1.00
+ 1102.50
+ 1102.50
+
+
+
+
+ 74.81
+ 13.45
+ -5.41
+
+ 1.00
+ 1.00
+ 1102.50
+ 1102.50
+
+
+
+
+
+
+
+
+ 36.00
+ 0.00
+ 0.00
+
+
+ 0.00
+ 0.00
+ 0.00
+
+ 0
+
+ 1
+
+ 36.00
+ 0.00
+ 0.00
+
+
+ 0.00
+ 0.00
+ 0.00
+
+
+
+
+
+
+ 74.81
+ 0.00
+ -5.41
+
+ 51
+ 51
+ 0.85
+
+
+
+
+
+
+
+ guidance/roll-angle-limit
+ guidance/roll-rate-limit
+
+
+
+
+
+
+
+
+
+ fcs/elevator-cmd-norm
+ fcs/pitch-trim-cmd-norm
+ ap/elevator_cmd
+
+ -1
+ 1
+
+
+
+
+ fcs/pitch-trim-sum
+
+ -0.35
+ 0.35
+
+
+
+
+
+ fcs/elevator-pos-rad
+
+ -0.35
+ 0.35
+
+
+ -1
+ 1
+
+
+
+
+
+
+
+
+
+ fcs/aileron-cmd-norm
+ fcs/roll-trim-cmd-norm
+ ap/aileron_cmd
+
+ -1
+ 1
+
+
+
+
+ fcs/roll-trim-sum
+
+ -0.35
+ 0.35
+
+
+
+
+
+ fcs/roll-trim-sum
+
+ -0.35
+ 0.35
+
+
+
+
+
+ fcs/left-aileron-pos-rad
+
+ -0.35
+ 0.35
+
+
+ -1
+ 1
+
+
+
+
+
+ fcs/right-aileron-pos-rad
+
+ -0.35
+ 0.35
+
+
+ -1
+ 1
+
+
+
+
+
+
+
+
+
+ fcs/rudder-cmd-norm
+ fcs/yaw-trim-cmd-norm
+
+ -1
+ 1
+
+
+
+
+ fcs/rudder-command-sum
+
+ -0.35
+ 0.35
+
+
+
+
+
+ fcs/rudder-pos-rad
+
+ -0.35
+ 0.35
+
+
+ -1
+ 1
+
+
+
+
+
+
+
+
+ fcs/flap-cmd-norm
+
+
+ 0
+
+
+
+ 15
+
+
+
+ 30
+
+
+
+
+
+
+
+ fcs/flap-pos-deg
+
+ 0
+ 30
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ fcs/speedbrake-cmd-norm
+
+
+ 0
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lift due to alpha
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+
+ aero/alpha-rad
+
+ -0.20 -0.750
+ 0.00 0.250
+ 0.23 1.400
+ 0.60 0.710
+
+
+
+
+
+
+ Delta Lift due to flaps
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ fcs/flap-pos-deg
+ 0.01333
+
+
+
+
+ Delta Lift due to speedbrake
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ fcs/speedbrake-pos-norm
+ 0
+
+
+
+
+ Lift due to Elevator Deflection
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ fcs/elevator-pos-rad
+ 0.2
+
+
+
+
+
+
+
+
+ Drag at zero lift
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+
+ aero/alpha-rad
+
+ -1.57 1.019
+ -0.26 0.024
+ 0.00 0.019
+ 0.26 0.024
+ 1.57 1.019
+
+
+
+
+
+
+ Induced drag
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ aero/cl-squared
+ 0.04
+
+
+
+
+ Drag due to mach
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+
+ velocities/mach
+
+ 0.00 0.000
+ 0.7 0.000
+ 1.10 0.023
+ 1.80 0.015
+
+
+
+
+
+
+ Drag due to flaps
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ fcs/flap-pos-deg
+ 0.00100
+
+
+
+
+ Drag due to speedbrakes
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ fcs/speedbrake-pos-norm
+ 0.028
+
+
+
+
+ Drag due to sideslip
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+
+ aero/beta-rad
+
+ -1.57 1.230
+ -0.26 0.050
+ 0.00 0.000
+ 0.26 0.050
+ 1.57 1.230
+
+
+
+
+
+
+ Drag due to Elevator Deflection
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ fcs/elevator-pos-norm
+ 0.04
+
+
+
+
+
+
+
+
+ Side force due to beta
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ aero/beta-rad
+ -1
+
+
+
+
+
+
+
+
+ Roll moment due to beta
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/beta-rad
+ -0.1
+
+
+
+
+ Roll moment due to roll rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/bi2vel
+ velocities/p-aero-rad_sec
+ -0.4
+
+
+
+
+ Roll moment due to yaw rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/bi2vel
+ velocities/r-aero-rad_sec
+ 0.15
+
+
+
+
+ Roll moment due to aileron
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ fcs/left-aileron-pos-rad
+ 0.17
+
+
+
+
+ Roll moment due to rudder
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ fcs/rudder-pos-rad
+ 0.01
+
+
+
+
+
+
+
+
+ Pitch moment due to alpha
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/cbarw-ft
+ aero/alpha-rad
+ -0.5
+
+
+
+
+ Pitch moment due to elevator
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/cbarw-ft
+ fcs/elevator-pos-rad
+
+ velocities/mach
+
+ 0.0 -1.100
+ 2.0 -0.275
+
+
+
+
+
+
+ Pitch moment due to pitch rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/cbarw-ft
+ aero/ci2vel
+ velocities/q-aero-rad_sec
+ -12
+
+
+
+
+ Pitch moment due to alpha rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/cbarw-ft
+ aero/ci2vel
+ aero/alphadot-rad_sec
+ -7
+
+
+
+
+
+
+
+
+ Yaw moment due to beta
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/beta-rad
+ 0.12
+
+
+
+
+ Yaw moment due to yaw rate
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ aero/bi2vel
+ velocities/r-aero-rad_sec
+ -0.15
+
+
+
+
+ Yaw moment due to rudder
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ fcs/rudder-pos-rad
+ -0.1
+
+
+
+
+ Adverse yaw
+
+ aero/qbar-psf
+ metrics/Sw-sqft
+ metrics/bw-ft
+ fcs/left-aileron-pos-rad
+ -0.01
+
+
+
+
+
+
+
+
+
+
+
+
+ ice/propeller
+
+
+
+
+
+ ice/propeller
+
+ 0 1
+ 1 0
+
+
+
+
+
+
+
+
+
+
+
+ propulsion/engine/prop-ice-factor
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ice/wing
+ ice/elevator
+ ice/stabilizer
+ ice/fuselage
+ ice/windshield
+ ice/propeller
+
+
+
+
+
+ ice/wing
+ 35.86
+
+
+
+
+
+
+ ice/elevator
+ 8.12
+
+
+
+
+
+
+ ice/stabilizer
+ 3.44
+
+
+
+
+
+
+ ice/fuselage
+ 6.08
+
+
+
+
+
+
+ ice/windshield
+ 1.96
+
+
+
+
+
+
+ ice/propeller
+ 0.49
+
+
+
+
+ inertia/pointmass-weight-lbs[17]
+ inertia/pointmass-weight-lbs[18]
+ inertia/pointmass-weight-lbs[19]
+ inertia/pointmass-weight-lbs[20]
+ inertia/pointmass-weight-lbs[21]
+ inertia/pointmass-weight-lbs[22]
+
+
+
+
+ ice/total-ice-lbs
+ 0.04
+
+
+
+ 0.0
+ 1.0
+
+
+
+
+
+
+
+
+
+
+
+ /instrumentation/airspeed-indicator/indicated-speed-kt
+
+ /sim/model/c172p/securing/pitot-cover-visible
+
+ /systems/pitot/icing
+
+ /controls/anti-ice/pitot-heat
+
+ /systems/electrical/outputs/pitot-heat
+
+
+
+
+
+
+
+ /instrumentation/airspeed-indicator/indicated-speed-kt
+ fcs/flap-pos-deg
+
+ 0 10 30
+ 0 0 0 0
+ 47 38 38 40
+ 49 40 40 42
+ 53 45 47 50
+ 55 49 50 52
+ 56 50 51 54
+ 62 60 60 61
+ 70 70 70 70
+ 80 81 81 80
+ 89 90 90 90
+ 98 100 100 100
+ 107 110 109 109
+ 117 120 120 120
+ 126 130 130 130
+ 135 140 140 140
+ 145 150 150 150
+ 154 160 160 160
+
+
+
+
+
+
+
+
+
+
+ /sim/model/c172p/securing/pitot-cover-visible EQ 1
+ /systems/pitot/icing GT 0.03
+
+
+
+
+
+
+
+
+ /controls/anti-ice/pitot-heat EQ 1
+ /systems/electrical/outputs/pitot-heat GT 20
+
+
+
+
+ /systems/pitot/heating
+
+
+ 0
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ /systems/pitot/heat-applied-norm LT 0.3
+
+
+
+
+
+
+
+
+
+
+
+
+ /systems/static[1]/serviceable
+ /systems/static-selected-source
+ /systems/static-selected-source-norm
+
+ /position/altitude-ft
+ /velocities/airspeed-kt
+
+ /systems/static[0]/pressure-inhg
+ /systems/static[1]/pressure-inhg
+
+
+
+
+
+
+
+ 1
+
+ 0.1
+
+
+ /position/altitude-ft
+ /velocities/airspeed-kt
+
+ 0 40 60 80 100 120 140 160
+ 0 0 10 20 -10 -30 -60 -90 -120
+ 2000 0 10 20 -10 -30 -60 -90 -130
+ 4000 0 10 20 -10 -30 -70 -100 -140
+ 6000 0 15 30 -10 -40 -70 -110 -140
+ 8000 0 15 30 -10 -40 -80 -110 -150
+ 10000 0 15 30 -10 -40 -80 -120 -160
+ 12000 0 15 30 -10 -40 -80 -120 -170
+ 14000 0 15 30 -10 -50 -90 -120 -180
+
+
+
+ /position/altitude-ft
+ 1
+
+
+
+
+ /systems/static[1]/pressure-inhg
+
+
+
+
+
+
+ /systems/static-selected-source
+
+
+ 0
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+ /systems/static[0]/pressure-inhg
+
+ /systems/static-selected-source-norm
+
+ /systems/static[1]/pressure-inhg-venturi
+ /systems/static[0]/pressure-inhg
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/aircraft/gazaile/gazaileap.xml b/aircraft/gazaile/gazaileap.xml
new file mode 100644
index 0000000000..029e05aa0b
--- /dev/null
+++ b/aircraft/gazaile/gazaileap.xml
@@ -0,0 +1,318 @@
+
+
+
+
+
+
+ ap/attitude_hold
+ ap/altitude_hold
+ ap/heading_hold
+ ap/altitude_setpoint
+ ap/heading_setpoint
+ ap/aileron_cmd
+ ap/elevator_cmd
+ ap/airspeed_setpoint
+ ap/airspeed_hold
+ ap/throttle-cmd-norm
+
+
+
+ ap/hdg-roll-err-c1
+ ap/roll-pid-kp
+ ap/roll-pid-ki
+ ap/roll-pid-kd
+
+
+
+
+
+
+
+
+ attitude/phi-rad
+ 0.5
+ 2
+ 0.05
+
+ 12
+ -3.1416
+ 3.1416
+
+ 0.001
+
+
+
+
+
+ ap/attitude_hold == 1
+
+
+
+
+ attitude/phi-rad
+ ap/roll-pid-kp
+ ap/roll-pid-ki
+ ap/roll-pid-kd
+ fcs/wing-leveler-ap-on-off
+
+
+
+
+
+ ap/attitude_hold == 1
+
+
+
+
+
+
+
+
+
+ attitude/heading-true-rad
+ 57.3
+
+
+
+ -fcs/heading-true-degrees
+ ap/heading_setpoint
+
+
+
+
+
+ fcs/heading-error lt -180
+
+
+ fcs/heading-error gt 180
+
+
+
+
+ fcs/heading-error-bias-switch
+ fcs/heading-error
+
+ -30
+ 30
+
+
+
+
+ fcs/heading-corrected
+ 0.01745
+
+
+
+ fcs/heading-command
+ ap/hdg-roll-err-c1
+
+
+
+ fcs/heading-roll-error-lag
+ -attitude/phi-rad
+
+
+
+
+
+ ap/heading_hold == 1
+
+
+
+
+ fcs/heading-roll-error-switch
+ 6.0
+ 0.13
+ 6.0
+
+
+
+
+
+ ap/heading_hold == 1
+ gear/unit[2]/WOW == 0
+
+
+ ap/attitude_hold == 1
+ gear/unit[2]/WOW == 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ap/altitude_setpoint
+ -position/h-agl-ft
+
+ -100
+ 100
+
+
+
+
+
+ fcs/altitude-error
+ 1
+
+
+
+
+ fcs/alt-error-lag
+
+ position/h-sl-ft
+
+ 0.0 0.06
+ 1000.0 0.055
+ 2000.0 0.050
+ 3000.0 0.048
+ 4000.0 0.046
+ 5000.0 0.043
+ 6000.0 0.039
+ 7000.0 0.035
+ 8000.0 0.030
+ 9000.0 0.026
+ 10000.0 0.022
+ 11000.0 0.018
+ 12000.0 0.014
+
+
+
+
+
+
+ fcs/hdot-command
+ -velocities/h-dot-fps
+
+
+
+
+
+
+ ap/altitude_hold == 1
+
+
+
+
+
+ fcs/elevator-pos-deg
+ 46.0
+
+
+
+
+
+ fcs/ap-alt-hold-switch
+ 0.01
+ 0.00015
+ 0.0003
+ fcs/windup-trigger
+ -1.0
+ 1.0
+
+
+
+
+ fcs/altitude-hold-pid
+ -1.0
+
+
+
+
+
diff --git a/aircraft/gazaile/reset00.xml b/aircraft/gazaile/reset00.xml
new file mode 100644
index 0000000000..de7adc1066
--- /dev/null
+++ b/aircraft/gazaile/reset00.xml
@@ -0,0 +1,18 @@
+
+
+
+ 0.0
+ 0.0
+ 0.0
+ -95.163839
+ 29.593978
+ 0.0
+ 0.0
+ 0.0
+ 5.305
+ 10.0
+ 0.0
+
diff --git a/aircraft/gazaile/thumbnail.jpg b/aircraft/gazaile/thumbnail.jpg
new file mode 100644
index 0000000000..b2adc1e4af
Binary files /dev/null and b/aircraft/gazaile/thumbnail.jpg differ
diff --git a/examples/FlightGear.cxx b/examples/FlightGear.cxx
index 241f8af4df..b295df34e4 100644
--- a/examples/FlightGear.cxx
+++ b/examples/FlightGear.cxx
@@ -853,6 +853,28 @@ bool FGJSBsim::copy_from_JSBsim()
node->setDoubleValue("rpm", eng->getRPM());
} // end FGPiston code block
break;
+ case FGEngine::etPistonDiesel:
+ {
+ FGPistonDiesel* eng = (FGPistonDiesel*)Propulsion->GetEngine(i);
+ node->setDoubleValue("egt-degf", eng->getExhaustGasTemp_degF());
+ node->setDoubleValue("oil-temperature-degf", eng->getOilTemp_degF());
+ node->setDoubleValue("oil-pressure-psi", eng->getOilPressure_psi());
+ node->setDoubleValue("mp-osi", eng->getManifoldPressure_inHg());
+ node->setDoubleValue("mp-inhg", eng->getManifoldPressure_inHg());
+ node->setDoubleValue("cht-degf", eng->getCylinderHeadTemp_degF());
+ node->setDoubleValue("rpm", eng->getRPM());
+ node->setDoubleValue("coolant-temperature-degf", eng->getCoolantTemperature_degF());
+ node->setDoubleValue("fuel-rack", eng->getFuelRack());
+ node->setDoubleValue("afr", eng->getAFR());
+ node->setDoubleValue("glow-plug-on", eng->getGlowPlugOn());
+ node->setDoubleValue("boost-pa", eng->getBoostPa());
+ node->setDoubleValue("power-hp", eng->getHP());
+ node->setDoubleValue("combustion-efficiency", eng->getCombustionEfficiency());
+ node->setDoubleValue("ve-reduced", eng->getVolumetricEfficiencyReduced());
+ node->setDoubleValue("indicated-hp", eng->getIndicatedHorsePower());
+ node->setDoubleValue("torque-si", eng->getTorque_SI());
+ } // end FGPistonDiesel code block
+ break; // end FGPistonDiesel code block
case FGEngine::etRocket:
{ // FGRocket code block
// FGRocket* eng = (FGRocket*)Propulsion->GetEngine(i);
diff --git a/python/jsbsim.pyx.in b/python/jsbsim.pyx.in
index 4895bf8d58..7b5df86f9e 100644
--- a/python/jsbsim.pyx.in
+++ b/python/jsbsim.pyx.in
@@ -94,7 +94,7 @@ cdef _convertToNumpyVec(const c_FGColumnVector3& v):
return numpy.matrix([v.Entry(1), v.Entry(2), v.Entry(3)]).T
-class LogLevel(enum.Enum):
+class LogLevel(enum.IntEnum):
"""Enumeration of log levels."""
BULK = 0 # For frequent messages
DEBUG = 1 # Less frequent debug type messages
@@ -105,7 +105,7 @@ class LogLevel(enum.Enum):
STDOUT = 6 # Standard output - unconditionally displayed.
-class LogFormat(enum.Enum):
+class LogFormat(enum.IntEnum):
"""Enumeration of log formats."""
RESET = 0
RED = 1
@@ -172,10 +172,17 @@ def _inherit_docstring_from_super_class(cls):
@_inherit_docstring_from_super_class
class DefaultLogger(FGLogger):
"""Default logger: print messages to stdout without formatting."""
+ def __init__(self, min_level: LogLevel = LogLevel.BULK):
+ self.min_level = min_level
+ def set_min_level(self, level: LogLevel) -> None:
+ """Set minimum log level for filtering."""
+ self.min_level = level
def file_location(self, filename: str, line: int) -> None:
- print(f"\nIn file {filename}: line {line}")
+ if int(self.log_level) >= int(self.min_level):
+ print(f"\nIn file {filename}: line {line}")
def message(self, message: str) -> None:
- print(f"{message}", end='')
+ if int(self.log_level) >= int(self.min_level):
+ print(f"{message}", end='')
# Reset the logger before Python shutdown to prevent crashes.
@@ -230,7 +237,7 @@ cdef class FGPropagate:
return _convertToNumpyVec(deref(self.thisptr).GetUVW())
-class Attribute(enum.Enum):
+class Attribute(enum.IntEnum):
NO_ATTR = 0
READ = 1
WRITE = 2
@@ -517,7 +524,7 @@ cdef class FGAircraft:
return _convertToNumpyVec(deref(self.thisptr).GetXYZrp())
-class eTemperature(enum.Enum):
+class eTemperature(enum.IntEnum):
eNoTempUnit = 0
eFahrenheit = 1
eCelsius = 2
@@ -525,7 +532,7 @@ class eTemperature(enum.Enum):
eKelvin = 4
-class ePressure(enum.Enum):
+class ePressure(enum.IntEnum):
eNoPressUnit= 0
ePSF = 1
eMillibars = 2
diff --git a/python/pyxstubgen.py b/python/pyxstubgen.py
index 99eb7cb62f..9f51982a3a 100644
--- a/python/pyxstubgen.py
+++ b/python/pyxstubgen.py
@@ -321,6 +321,8 @@ def funcdef(self, tree: Tree) -> None:
"python__string",
):
pvalue = self.visit(value)
+ elif value.data == "python__getattr":
+ pvalue = '.'.join(self.visit(value))
else:
pvalue = get_constant(value)
parameters.append(f"{pname}: {ptype} = {pvalue}")
diff --git a/scripts/gazaile_elevation_test.xml b/scripts/gazaile_elevation_test.xml
new file mode 100644
index 0000000000..fc9fbd86cb
--- /dev/null
+++ b/scripts/gazaile_elevation_test.xml
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+ simulation/notify-time-trigger
+ simulation/engine-notify-trigger
+ simulation/run_id
+ fcs/left-brake-cmd-norm
+ fcs/right-brake-cmd-norm
+ fcs/center-brake-cmd-norm
+
+
+ Start the engine
+ simulation/sim-time-sec >= 0.25
+
+
+
+
+
+
+ simulation/run_id
+ ap/hdg-roll-err-c1
+ position/terrain-elevation-asl-ft
+ position/h-agl-ft
+ position/h-sl-ft
+ metrics/terrain-radius
+
+
+
+
+ Release brakes and get rolling with flaps at 10 degrees.
+ simulation/sim-time-sec >= 2.0
+
+
+
+
+
+
+
+ Set Autopilot for 400 ft and rotate at 49 keas.
+
+ velocities/vc-kts >= 49
+
+
+
+
+
+ Set Autopilot for 6000 ft after a five second delay.
+
+ velocities/vc-kts >= 49
+ 5.0
+
+
+
+
+ Reduce throttle to 75% at 1000 ft and remove flaps
+
+
+ position/h-agl-ft >= 1000
+
+
+
+
+
+
+
+ Remove flaps at 50 ft and set heading to 100; acquire heading.
+
+
+ position/h-agl-ft >= 1000
+
+
+
+
+
+
+
+ Output engine state every second
+
+ simulation/sim-time-sec
+ propulsion/engine[0]/rpm
+ propulsion/engine[0]/set-running
+ propulsion/engine[0]/cranking
+ propulsion/engine[0]/fuel-rack
+ propulsion/engine[0]/fuel-flow-rate-pps
+ propulsion/engine[0]/power-hp
+ propulsion/engine[0]/indicated-hp
+ propulsion/engine[0]/thrust-lbs
+ propulsion/engine[0]/coolant-temperature-degK
+ propulsion/engine[0]/egt-degF
+
+ simulation/sim-time-sec >= simulation/engine-notify-trigger
+
+
+
+
+ Output flight state at 1 minute intervals
+
+ velocities/vc-kts
+ position/h-sl-ft
+ position/lat-geod-deg
+ position/geod-alt-ft
+
+ simulation/sim-time-sec >= simulation/notify-time-trigger
+
+
+
+
+
+ Terminate this run when the run ID is greater than a specified number
+
+
+ simulation/run_id >= 20
+
+
+
+
+
+
+
+ This event is very critical with regard to when the various properties
+ below are set. The terrain elevation is set in the initial conditions (IC)
+ BEFORE the reset is performed. Since the altitude is initially set as an
+ altitude above ground level (AGL), it needs not to be repeated here as
+ JSBSim will update the altitude above sea level (ASL) to keep the AGL in
+ the IC at its original value.
+
+
+ simulation/sim-time-sec ge 600.0
+
+
+
+
+
+
+
+
+
+ simulation/run_id
+ 500.0
+
+
+
+
+
+ simulation/run_id
+ position/terrain-elevation-asl-ft
+ position/h-agl-ft
+ position/h-sl-ft
+ metrics/terrain-radius
+
+
+
+
+
diff --git a/src/FGJSBBase.h b/src/FGJSBBase.h
index 63deba4204..e9137d6ef1 100644
--- a/src/FGJSBBase.h
+++ b/src/FGJSBBase.h
@@ -314,6 +314,7 @@ class JSBSIM_API FGJSBBase {
static constexpr double lbtoslug = 1.0/slugtolb;
static constexpr double kgtolb = 2.20462;
static constexpr double kgtoslug = 0.06852168;
+ static constexpr double intom = 0.0254;
static const std::string needed_cfg_version;
static const std::string JSBSim_version;
diff --git a/src/models/FGPropulsion.cpp b/src/models/FGPropulsion.cpp
index 3bcbc50797..6c316b01ba 100644
--- a/src/models/FGPropulsion.cpp
+++ b/src/models/FGPropulsion.cpp
@@ -55,6 +55,7 @@ INCLUDES
#include "models/propulsion/FGRocket.h"
#include "models/propulsion/FGTurbine.h"
#include "models/propulsion/FGPiston.h"
+#include "models/propulsion/FGPistonDiesel.h"
#include "models/propulsion/FGElectric.h"
#include "models/propulsion/FGTurboProp.h"
#include "models/propulsion/FGTank.h"
@@ -406,6 +407,9 @@ bool FGPropulsion::Load(Element* el)
if (engine_element->FindElement("piston_engine")) {
Element *element = engine_element->FindElement("piston_engine");
Engines.push_back(make_shared(FDMExec, element, numEngines, in));
+ } else if (engine_element->FindElement("diesel_engine")) {
+ Element *element = engine_element->FindElement("diesel_engine");
+ Engines.push_back(make_shared(FDMExec, element, numEngines, in));
} else if (engine_element->FindElement("turbine_engine")) {
Element *element = engine_element->FindElement("turbine_engine");
Engines.push_back(make_shared(FDMExec, element, numEngines, in));
@@ -821,10 +825,12 @@ void FGPropulsion::SetFuelFreeze(bool f)
void FGPropulsion::bind(void)
{
bool HavePistonEngine = false;
+ bool HavePistonDieselEngine = false;
bool HaveTurboEngine = false;
for (const auto& engine: Engines) {
if (!HavePistonEngine && engine->GetType() == FGEngine::etPiston) HavePistonEngine = true;
+ if (!HavePistonDieselEngine && engine->GetType() == FGEngine::etPistonDiesel) HavePistonDieselEngine = true;
if (!HaveTurboEngine && engine->GetType() == FGEngine::etTurbine) HaveTurboEngine = true;
if (!HaveTurboEngine && engine->GetType() == FGEngine::etTurboprop) HaveTurboEngine = true;
}
@@ -836,6 +842,10 @@ void FGPropulsion::bind(void)
PropertyManager->Tie("propulsion/cutoff_cmd", this, &FGPropulsion::GetCutoff, &FGPropulsion::SetCutoff);
}
+ if (HavePistonDieselEngine) {
+ PropertyManager->Tie("propulsion/starter_cmd", this, &FGPropulsion::GetStarter, &FGPropulsion::SetStarter);
+ }
+
if (HavePistonEngine) {
PropertyManager->Tie("propulsion/starter_cmd", this, &FGPropulsion::GetStarter, &FGPropulsion::SetStarter);
PropertyManager->Tie("propulsion/magneto_cmd", this,
diff --git a/src/models/propulsion/CMakeLists.txt b/src/models/propulsion/CMakeLists.txt
index f01d772102..6824c7e716 100644
--- a/src/models/propulsion/CMakeLists.txt
+++ b/src/models/propulsion/CMakeLists.txt
@@ -3,6 +3,7 @@ set(SOURCES FGElectric.cpp
FGForce.cpp
FGNozzle.cpp
FGPiston.cpp
+ FGPistonDiesel.cpp
FGPropeller.cpp
FGRocket.cpp
FGTank.cpp
@@ -18,6 +19,7 @@ set(HEADERS FGElectric.h
FGForce.h
FGNozzle.h
FGPiston.h
+ FGPistonDiesel.h
FGPropeller.h
FGRocket.h
FGTank.h
diff --git a/src/models/propulsion/FGEngine.h b/src/models/propulsion/FGEngine.h
index d081ee49ce..f25a97122f 100644
--- a/src/models/propulsion/FGEngine.h
+++ b/src/models/propulsion/FGEngine.h
@@ -66,13 +66,13 @@ CLASS DOCUMENTATION
/** Base class for all engines.
This base class contains methods and members common to all engines, such as
- logic to drain fuel from the appropriate tank, etc.
+ logic to drain fuel from the appropriate tank, etc.
Configuration File Format:
@code
{integer}
- ... optional more feed tank index numbers ...
+ ... optional more feed tank index numbers ...
{number}
@@ -92,7 +92,7 @@ CLASS DOCUMENTATION
Not all thruster types can be matched with a given engine type. See the class
documentation for engine and thruster classes.
-
+
@author Jon S. Berndt
*/
@@ -133,7 +133,7 @@ class FGEngine : public FGModelFunctions
FGEngine(int engine_number, struct Inputs& input);
~FGEngine() override;
- enum EngineType {etUnknown, etRocket, etPiston, etTurbine, etTurboprop, etElectric};
+ enum EngineType {etUnknown, etRocket, etPiston, etPistonDiesel, etTurbine, etTurboprop, etElectric};
EngineType GetType(void) const { return Type; }
virtual const std::string& GetName(void) const { return Name; }
@@ -171,7 +171,7 @@ class FGEngine : public FGModelFunctions
virtual void Calculate(void) = 0;
virtual double GetThrust(void) const;
-
+
/** The fuel need is calculated based on power levels and flow rate for that
power level. It is also turned from a rate into an actual amount (pounds)
by multiplying it by the delta T and the rate.
diff --git a/src/models/propulsion/FGPistonDiesel.cpp b/src/models/propulsion/FGPistonDiesel.cpp
new file mode 100644
index 0000000000..c04e2be21f
--- /dev/null
+++ b/src/models/propulsion/FGPistonDiesel.cpp
@@ -0,0 +1,1385 @@
+/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+ Module: FGPistonDiesel.cpp
+ Author: Jon S. Berndt, JSBSim framework
+ Dave Luff, Piston engine model
+ Ronald Jensen, Piston engine model
+ Marc Traverso, Diesel Piston engine model
+ Date started: 09/12/2000
+ Purpose: This module models a piston diesel engine
+
+ ------------- Copyright (C) 2000 Jon S. Berndt (jon@jsbsim.org) --------------
+
+ This program is free software; you can redistribute it and/or modify it under
+ the terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 2 of the License, or (at your option) any later
+ version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ details.
+
+ You should have received a copy of the GNU Lesser General Public License along with
+ this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ Further information about the GNU Lesser General Public License can also be found on
+ the world wide web at http://www.gnu.org.
+
+FUNCTIONAL DESCRIPTION
+--------------------------------------------------------------------------------
+
+This class descends from the FGEngine class and models a diesel piston engine
+based on parameters given in the engine config file for this class.
+
+HISTORY
+--------------------------------------------------------------------------------
+09/12/2000 JSB Created
+14/04/2026 MTR Derived FGPiston to create the FGPistonDiesel class implementation
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+INCLUDES
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
+
+#include
+#include
+#include
+#include
+
+#include "FGFDMExec.h"
+#include "FGPistonDiesel.h"
+#include "FGPropeller.h"
+#include "input_output/FGXMLElement.h"
+
+namespace JSBSim {
+
+/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+CLASS IMPLEMENTATION
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
+
+FGPistonDiesel::FGPistonDiesel(FGFDMExec* exec, Element* el, int engine_number, struct Inputs& input)
+ : FGEngine(engine_number, input)
+{
+ Load(exec, el);
+
+ Element* table_element = nullptr;
+ auto PropertyManager = exec->GetPropertyManager();
+
+ Type = etPistonDiesel;
+
+ // ===== DEFAULTS =====
+ // Loosely based on a small automotive diesel (~1.4 L, 4-cylinder).
+
+ // Geometry
+ Cycles = 4;
+ Displacement = 360;
+ Bore = 2.95;
+ Stroke = 3.03;
+ Cylinders = 4;
+ CylinderHeadMass = 12; // kg
+ CompressionRatio = 22;
+
+ // Performance
+ MaxHP = 50;
+ MaxRPM = 5000;
+ IdleRPM = 800;
+ RatedRPM = 0.0;
+ PeakTorqueRPM = -1; // negative = derive during init
+
+ // Volumetric efficiency
+ volumetric_efficiency = 0.85;
+ VE_peak = -1; // negative = derive during init
+
+ // Fuel
+ injection_type = InjectionType::DI;
+ AFR_smoke_limit = 20.0;
+ calorific_value_fuel = 42.8e6; // J/kg (EN 590 diesel)
+ Cp_fuel = 1850.0; // J/(kg*K)
+ BSFC_best = -1; // negative = derive during init
+
+ // Intake
+ Z_airbox = -999;
+ Ram_Air_Factor = 1;
+ PeakMeanPistonSpeed_fps = 100;
+
+ // Friction (Chen-Flynn)
+ FMEPDynamic = 18400; // Pa/(m/s)
+ FMEPStatic = 46500; // Pa
+ StaticFriction_HP = 1.5;
+
+ // Cooling
+ Cooling_Factor = -1;
+ coolant_heat_fraction = 0.30;
+
+ // Starter
+ StarterGain = 1.0;
+ StarterTorque = -1.0; // negative = derive during init
+ StarterRPM = -1.0; // negative = derive during init
+
+ // Governor
+ governor_droop = 0.06;
+ governor_tau = 0.08; // seconds
+ IdleFuelRack = 0.12;
+ idle_governor_gain = 0.005;
+
+ // Cold start
+ cold_enrichment_max = 1.20;
+ cold_enrichment_threshold = 40.0; // deg C
+ GlowPlugThreshold_degK = 278.0;
+ GlowPlugOn = false;
+
+ // Turbocharger (default: naturally aspirated)
+ Boosted = false;
+ BoostMaxPressure_Pa = 0.0;
+ BoostThreshold_RPM = 0.0;
+ TurboLag_tau = 0.5; // seconds
+ BoostLossFactor = 0.0;
+
+ // Oil system
+ Design_Oil_Temp = 358; // K
+ Oil_Viscosity_Index = 0.25;
+ Oil_Press_Relief_Valve = 60; // psi
+ Oil_Press_RPM_Max = 0.0; // set after MaxRPM is read
+
+ // Calibration (derived during init)
+ m_fuel_max_per_cycle = -1;
+ eta_indicated_calibrated = -1;
+
+ // Internal state
+ FuelRack = 0.0;
+ m_fuel_per_cycle = 0.0;
+ CurrentBoost_Pa = 0.0;
+
+ // Outputs
+ rho_air = 0.0;
+ volumetric_efficiency_reduced = 0.0;
+ m_dot_air = 0.0;
+ v_dot_air = 0.0;
+ m_dot_fuel = 0.0;
+ HP = 0.0;
+ combustion_efficiency = 0.0;
+ ExhaustGasTemp_degK = 0.0;
+ EGT_degC = 0.0;
+ CylinderHeadTemp_degK = 0.0;
+ OilPressure_psi = 0.0;
+ OilTemp_degK = 0.0;
+ MeanPistonSpeed_fps = 0.0;
+ CoolantTemperature_degK = 0.0;
+ FuelFlow_pps = 0.0;
+ MAP = 101325.0; // Pa - ambient, no throttle
+
+ ResetToIC();
+
+ // ===== READ CONFIGURATION =====
+
+ if (el->FindElement("displacement"))
+ Displacement = el->FindElementValueAsNumberConvertTo("displacement", "IN3");
+ if (el->FindElement("maxhp"))
+ MaxHP = el->FindElementValueAsNumberConvertTo("maxhp", "HP");
+ if (el->FindElement("static-friction"))
+ StaticFriction_HP = el->FindElementValueAsNumberConvertTo("static-friction", "HP");
+ if (el->FindElement("cycles"))
+ Cycles = el->FindElementValueAsNumber("cycles");
+ if (el->FindElement("idlerpm"))
+ IdleRPM = el->FindElementValueAsNumber("idlerpm");
+ if (el->FindElement("maxrpm"))
+ MaxRPM = el->FindElementValueAsNumber("maxrpm");
+ if (el->FindElement("rated-rpm"))
+ RatedRPM = el->FindElementValueAsNumber("rated-rpm");
+ if (el->FindElement("peak-torque-rpm"))
+ PeakTorqueRPM = el->FindElementValueAsNumber("peak-torque-rpm");
+ if (el->FindElement("bsfc"))
+ BSFC_best = el->FindElementValueAsNumberConvertTo("bsfc", "LBS/HP*HR");
+ if (el->FindElement("volumetric-efficiency"))
+ volumetric_efficiency = el->FindElementValueAsNumber("volumetric-efficiency");
+ if (el->FindElement("compression-ratio"))
+ CompressionRatio = el->FindElementValueAsNumber("compression-ratio");
+ if (el->FindElement("bore"))
+ Bore = el->FindElementValueAsNumberConvertTo("bore", "IN");
+ if (el->FindElement("stroke"))
+ Stroke = el->FindElementValueAsNumberConvertTo("stroke", "IN");
+ if (el->FindElement("cylinders"))
+ Cylinders = el->FindElementValueAsNumber("cylinders");
+ if (el->FindElement("cylinder-head-mass"))
+ CylinderHeadMass = el->FindElementValueAsNumberConvertTo("cylinder-head-mass", "KG");
+ if (el->FindElement("air-intake-impedance-factor"))
+ Z_airbox = el->FindElementValueAsNumber("air-intake-impedance-factor");
+ if (el->FindElement("ram-air-factor"))
+ Ram_Air_Factor = el->FindElementValueAsNumber("ram-air-factor");
+ if (el->FindElement("cooling-factor"))
+ Cooling_Factor = el->FindElementValueAsNumber("cooling-factor");
+ if (el->FindElement("starter-rpm"))
+ StarterRPM = el->FindElementValueAsNumber("starter-rpm");
+ if (el->FindElement("starter-torque"))
+ StarterTorque = el->FindElementValueAsNumber("starter-torque");
+ if (el->FindElement("dynamic-fmep"))
+ FMEPDynamic = el->FindElementValueAsNumberConvertTo("dynamic-fmep", "PA");
+ if (el->FindElement("static-fmep"))
+ FMEPStatic = el->FindElementValueAsNumberConvertTo("static-fmep", "PA");
+ if (el->FindElement("peak-piston-speed"))
+ PeakMeanPistonSpeed_fps = el->FindElementValueAsNumber("peak-piston-speed");
+
+ // Diesel-specific XML parameters
+ if (el->FindElement("calorific-value-fuel"))
+ calorific_value_fuel = el->FindElementValueAsNumber("calorific-value-fuel");
+ if (el->FindElement("bsfc-best"))
+ BSFC_best = el->FindElementValueAsNumber("bsfc-best");
+ if (el->FindElement("ve-peak"))
+ VE_peak = el->FindElementValueAsNumber("ve-peak");
+ if (el->FindElement("injection-type")) {
+ std::string itype = el->FindElementValue("injection-type");
+ injection_type = (itype == "DI") ? InjectionType::DI : InjectionType::IDI;
+ }
+ if (el->FindElement("afr-smoke-limit"))
+ AFR_smoke_limit = el->FindElementValueAsNumber("afr-smoke-limit");
+ if (el->FindElement("governor-droop"))
+ governor_droop = el->FindElementValueAsNumber("governor-droop");
+ if (el->FindElement("governor-tau"))
+ governor_tau = el->FindElementValueAsNumber("governor-tau");
+ if (el->FindElement("idle-fuel-rack"))
+ IdleFuelRack = el->FindElementValueAsNumber("idle-fuel-rack");
+ if (el->FindElement("idle-governor-gain"))
+ idle_governor_gain = el->FindElementValueAsNumber("idle-governor-gain");
+ if (el->FindElement("cold-enrichment-max"))
+ cold_enrichment_max = el->FindElementValueAsNumber("cold-enrichment-max");
+ if (el->FindElement("cold-enrichment-threshold"))
+ cold_enrichment_threshold = el->FindElementValueAsNumber("cold-enrichment-threshold");
+ if (el->FindElement("glow-plug-threshold"))
+ GlowPlugThreshold_degK = el->FindElementValueAsNumberConvertTo("glow-plug-threshold", "DEGK");
+ if (el->FindElement("coolant-heat-fraction"))
+ coolant_heat_fraction = el->FindElementValueAsNumber("coolant-heat-fraction");
+
+ // Turbocharging
+ if (el->FindElement("boosted")) {
+ Boosted = (el->FindElementValue("boosted") == "true");
+ if (el->FindElement("boost-max-pressure"))
+ BoostMaxPressure_Pa = el->FindElementValueAsNumberConvertTo("boost-max-pressure", "PA");
+ if (el->FindElement("boost-threshold-rpm"))
+ BoostThreshold_RPM = el->FindElementValueAsNumber("boost-threshold-rpm");
+ if (el->FindElement("turbo-lag"))
+ TurboLag_tau = el->FindElementValueAsNumber("turbo-lag");
+ if (el->FindElement("boost-loss-factor"))
+ BoostLossFactor = el->FindElementValueAsNumber("boost-loss-factor");
+ }
+
+ // Oil system
+ Oil_Press_RPM_Max = MaxRPM * 0.75;
+ if (el->FindElement("oil-pressure-relief-valve-psi"))
+ Oil_Press_Relief_Valve = el->FindElementValueAsNumberConvertTo("oil-pressure-relief-valve-psi", "PSI");
+ if (el->FindElement("design-oil-temp-degK"))
+ Design_Oil_Temp = el->FindElementValueAsNumberConvertTo("design-oil-temp-degK", "DEGK");
+ if (el->FindElement("oil-pressure-rpm-max"))
+ Oil_Press_RPM_Max = el->FindElementValueAsNumber("oil-pressure-rpm-max");
+ if (el->FindElement("oil-viscosity-index"))
+ Oil_Viscosity_Index = el->FindElementValueAsNumber("oil-viscosity-index");
+
+ // Lookup tables
+ while ((table_element = el->FindNextElement("table")) != nullptr) {
+ std::string name = table_element->GetAttributeValue("name");
+ try {
+ if (name == "COMBUSTION") {
+ Lookup_Combustion_Efficiency = std::make_unique(PropertyManager, table_element);
+ } else {
+ FGLogging log(LogLevel::ERROR);
+ log << "Unknown table type: " << name << " in piston engine definition.\n";
+ }
+ } catch (std::string& str) {
+ XMLLogException err(table_element);
+ Lookup_Combustion_Efficiency.reset();
+ err << "Error loading piston engine table:" + name + ". " + str << "\n";
+ throw err;
+ }
+ }
+
+ // ===== POST-READ VALIDATION =====
+
+ volumetric_efficiency_reduced = volumetric_efficiency;
+
+ if (MaxHP <= 0 || MaxRPM <= 0 || Displacement <= 0) {
+ throw BaseException(
+ "FGPistonDiesel: displacement, maxhp, and maxrpm are required.");
+ }
+
+ // ===== DERIVED DEFAULTS =====
+
+ if (StarterRPM < 0.0)
+ StarterRPM = 2 * IdleRPM;
+ if (StarterTorque < 0.0)
+ StarterTorque = MaxHP * 0.4;
+
+ displacement_SI = Displacement * in3tom3;
+ // AKA 2 * (RPM/60) * (Stroke/12)
+ RatedMeanPistonSpeed_fps = (MaxRPM * Stroke) / 360.0;
+
+ if (Oil_Press_RPM_Max <= 0.0)
+ Oil_Press_RPM_Max = MaxRPM * 0.75;
+
+ if (RatedRPM <= 0.0)
+ RatedRPM = MaxRPM * 0.85;
+
+ if (PeakTorqueRPM < 0.0)
+ PeakTorqueRPM = RatedRPM * 0.55;
+
+ // ===== DERIVE BSFC IF NOT PROVIDED =====
+
+ if (BSFC_best < 0.0) {
+ double eta_brake_best = 0.10
+ + 0.35 * (1.0 - 1.0 / std::pow(CompressionRatio, 0.35));
+
+ if (injection_type == InjectionType::IDI) {
+ eta_brake_best *= 0.92;
+ }
+
+ if (displacement_SI < 0.002) {
+ double size_penalty = 1.0
+ - 0.03 * (2.0 - displacement_SI * 1000.0);
+ eta_brake_best *= size_penalty;
+ }
+
+ BSFC_best = 3.6e9 / (eta_brake_best * calorific_value_fuel);
+
+ if (Cooling_Factor < 0.0)
+ Cooling_Factor = 5.0 * MaxHP;
+
+ FGLogging log(LogLevel::INFO);
+ log << "FGPistonDiesel: BSFC_best estimated at "
+ << BSFC_best << " g/(kW*hr)"
+ << " (eta_brake = " << eta_brake_best << ").\n";
+
+ if (BSFC_best < 190.0 || BSFC_best > 320.0) {
+ log << "WARNING: BSFC outside typical range (190-320), clamping.\n";
+ BSFC_best = Constrain(190.0, BSFC_best, 320.0);
+ }
+ }
+
+ // ===== DERIVE FUEL CALIBRATION =====
+
+ double MaxHP_kW = MaxHP * 0.7457;
+ double fuel_flow_rated_kgs = (BSFC_best * MaxHP_kW) / 3.6e6;
+ double injections_per_sec = (RatedRPM / 60.0) * Cylinders / (Cycles / 2.0);
+ m_fuel_max_per_cycle = fuel_flow_rated_kgs / injections_per_sec;
+
+ // ===== DERIVE VE_PEAK IF NOT PROVIDED =====
+
+ if (VE_peak < 0.0) {
+ double m_dot_air_rated = fuel_flow_rated_kgs * AFR_smoke_limit;
+ double rho_sl = standard_pressure / (R_air * 288.15);
+ double swept_rate_rated = (displacement_SI * (RatedRPM / 60.0))
+ / (Cycles / 2.0);
+
+ double VE_at_rated = m_dot_air_rated / (swept_rate_rated * rho_sl);
+
+ double rpm_norm_rated = RatedRPM / MaxRPM;
+ double curve_at_rated = 1.0 - 0.35 * std::pow(rpm_norm_rated - 0.42, 2);
+ if (rpm_norm_rated > 0.75) {
+ curve_at_rated -= 0.15 * std::pow(rpm_norm_rated - 0.75, 1.5);
+ }
+
+ if (curve_at_rated > 0.1) {
+ VE_peak = VE_at_rated / curve_at_rated;
+ } else {
+ VE_peak = VE_at_rated;
+ }
+
+ FGLogging log(LogLevel::INFO);
+ log << "FGPistonDiesel: VE_peak estimated at " << VE_peak << ".\n";
+
+ if (VE_peak < 0.70 || VE_peak > 0.98) {
+ log << "WARNING: VE_peak outside typical range (0.70-0.98), clamping.\n";
+ VE_peak = Constrain(0.70, VE_peak, 0.98);
+ }
+ }
+
+ // ===== DERIVE INDICATED EFFICIENCY =====
+
+ double V_cylinder = displacement_SI / Cylinders;
+ double MPS_rated = Stroke * intom * RatedRPM / 30.0;
+ double FMEP_rated = FMEPStatic + FMEPDynamic * MPS_rated;
+
+ double MaxTorque_Nm = (MaxHP * 745.7)
+ / (RatedRPM * 2.0 * M_PI / 60.0);
+ double BMEP_rated = (MaxTorque_Nm * 2.0 * M_PI * (Cycles / 2.0))
+ / displacement_SI;
+ double IMEP_rated = BMEP_rated + FMEP_rated;
+
+ eta_indicated_calibrated = (IMEP_rated * V_cylinder)
+ / (m_fuel_max_per_cycle * calorific_value_fuel);
+
+ // Default combustion efficiency table if not provided
+ if (!Lookup_Combustion_Efficiency) {
+ // First column is fuel rack (normalised load), second is combustion efficiency
+ Lookup_Combustion_Efficiency = std::make_unique(8);
+ *Lookup_Combustion_Efficiency << 0.00 << 0.75; // idle: poor atomization
+ *Lookup_Combustion_Efficiency << 0.10 << 0.85; // light load
+ *Lookup_Combustion_Efficiency << 0.20 << 0.92;
+ *Lookup_Combustion_Efficiency << 0.40 << 0.96;
+ *Lookup_Combustion_Efficiency << 0.60 << 0.98; // mid load: best
+ *Lookup_Combustion_Efficiency << 0.80 << 0.97; // high load: approaching smoke
+ *Lookup_Combustion_Efficiency << 0.95 << 0.93; // near smoke limit
+ *Lookup_Combustion_Efficiency << 1.00 << 0.88; // at smoke limit
+ }
+
+ // ===== BIND PROPERTIES =====
+
+ std::string base_property_name = CreateIndexedPropertyName("propulsion/engine", EngineNumber);
+
+ auto bind = [&](const char* suffix, auto* var_or_getter) {
+ std::string prop = base_property_name + suffix;
+ PropertyManager->Tie(prop, var_or_getter);
+ };
+ auto bind_method = [&](const char* suffix, auto method) {
+ std::string prop = base_property_name + suffix;
+ PropertyManager->Tie(prop, this, method);
+ };
+
+ bind("/power-hp", &HP);
+ bind("/friction-hp", &StaticFriction_HP);
+ bind("/starter-norm", &StarterGain);
+ bind("/volumetric-efficiency", &volumetric_efficiency);
+ bind("/air-intake-impedance-factor", &Z_airbox);
+ bind("/ram-air-factor", &Ram_Air_Factor);
+ bind("/cooling-factor", &Cooling_Factor);
+ bind("/fuel-rack", &FuelRack);
+ // Note: "/fuel-flow-rate-pps" is already bound by FGEngine::Load()
+ // via GetFuelFlowRate(). Binding it again here causes a double-tie error.
+ bind("/bsfc", &BSFC_best);
+ bind("/map-pa", &MAP);
+ bind("/glow-plug-on", &GlowPlugOn);
+ bind("/combustion-efficiency", &combustion_efficiency);
+ bind("/m-dot-air-kgs", &m_dot_air);
+ bind("/ve-reduced", &volumetric_efficiency_reduced);
+ bind("/cranking", &Cranking);
+ bind("/rpm", &RPM);
+ bind("/indicated-hp", &IndicatedHorsePower);
+ bind("/torque-si", &Torque_SI);
+
+ bind_method("/coolant-temperature-degF", &FGPistonDiesel::getCoolantTemperature_degF);
+ bind_method("/cht-degF", &FGPistonDiesel::getCylinderHeadTemp_degF);
+ bind_method("/oil-temperature-degF", &FGPistonDiesel::getOilTemp_degF);
+ bind_method("/oil-pressure-psi", &FGPistonDiesel::getOilPressure_psi);
+ bind_method("/egt-degF", &FGPistonDiesel::getExhaustGasTemp_degF);
+ bind_method("/AFR", &FGPistonDiesel::getAFR);
+
+ if (Boosted) {
+ bind("/boost-pa", &CurrentBoost_Pa);
+ if (BoostLossFactor > 0.0) {
+ bind("/boostloss-factor", &BoostLossFactor);
+ bind("/boostloss-hp", &BoostLossHP);
+ }
+ }
+
+ // Turbocharger sanity checks
+ if (Boosted) {
+ if (BoostMaxPressure_Pa <= 0.0) {
+ FGLogging log(LogLevel::WARN);
+ log << "FGPistonDiesel: boosted=true but boost-max-pressure "
+ << "not set or zero. Defaulting to 1.5 bar absolute.\n";
+ BoostMaxPressure_Pa = 150000.0;
+ }
+ if (BoostThreshold_RPM <= 0.0) {
+ BoostThreshold_RPM = IdleRPM * 1.5;
+ FGLogging log(LogLevel::INFO);
+ log << "FGPistonDiesel: boost-threshold-rpm not set, "
+ << "estimating " << BoostThreshold_RPM << " RPM.\n";
+ }
+ if (TurboLag_tau <= 0.0) {
+ TurboLag_tau = 0.5;
+ }
+ }
+
+ Debug(0);
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+FGPistonDiesel::~FGPistonDiesel()
+{
+ Debug(1);
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+void FGPistonDiesel::ResetToIC()
+{
+ FGEngine::ResetToIC();
+
+ // Intake pressure - no throttle, MAP is always ambient
+ MAP = in.Pressure * psftopa;
+
+ // Temperatures - everything starts at ambient
+ double airTemperature_degK = RankineToKelvin(in.Temperature);
+ OilTemp_degK = airTemperature_degK;
+ CylinderHeadTemp_degK = airTemperature_degK;
+ CoolantTemperature_degK = airTemperature_degK;
+ ExhaustGasTemp_degK = airTemperature_degK;
+ EGT_degC = ExhaustGasTemp_degK - 273.15;
+
+ // Engine state
+ Thruster->SetRPM(0.0);
+ RPM = 0.0;
+ HP = 0.0;
+ Torque_SI = 0.0;
+ IndicatedHorsePower = 0.0;
+ Running = false;
+ Cranking = false;
+
+ // Fuel system
+ FuelRack = 0.0;
+ m_fuel_per_cycle = 0.0;
+ m_dot_fuel = 0.0;
+ FuelFlow_pps = 0.0;
+ FuelFlowRate = 0.0;
+
+ // Air flow
+ m_dot_air = 0.0;
+ v_dot_air = 0.0;
+ rho_air = 0.0;
+ volumetric_efficiency_reduced = volumetric_efficiency;
+ combustion_efficiency = 0.0;
+ MeanPistonSpeed_fps = 0.0;
+ equivalence_ratio = 0.0;
+
+ // Oil
+ OilPressure_psi = 0.0;
+
+ // Turbo
+ CurrentBoost_Pa = 0.0;
+ BoostLossHP = 0.0;
+
+ // Glow plug off
+ GlowPlugOn = false;
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+void FGPistonDiesel::Calculate()
+{
+ p_amb = in.Pressure * psftopa;
+ double p = in.TotalPressure * psftopa;
+ p_ram = (p - p_amb) * Ram_Air_Factor + p_amb;
+ T_amb = RankineToKelvin(in.Temperature);
+
+ RunPreFunctions();
+
+ // The thruster controls the engine RPM because it encapsulates
+ // the gear ratio and other transmission variables.
+ RPM = Thruster->GetEngineRPM();
+
+ // AKA 2 * (RPM/60) * (Stroke/12)
+ MeanPistonSpeed_fps = (RPM * Stroke) / 360.0;
+
+ IAS = in.Vc;
+
+ doEngineStartup();
+
+ MAP = p_amb;
+ if (Boosted) doTurbo();
+ doAirFlow();
+ doFuelRack();
+ doFuelFlow();
+
+ doEnginePower();
+ if (Running && IndicatedHorsePower < 0.1250) Running = false;
+
+ doEGT();
+ doCHT();
+ doCoolantTemperature();
+ doOilTemperature();
+ doOilPressure();
+
+ if (Thruster->GetType() == FGThruster::ttPropeller) {
+ auto* prop = static_cast(Thruster);
+ prop->SetAdvance(in.PropAdvance[EngineNumber]);
+ prop->SetFeather(in.PropFeather[EngineNumber]);
+ }
+
+ LoadThrusterInputs();
+
+ // Filter out negative power when the propeller is not rotating.
+ double power = HP * hptoftlbssec;
+ if (RPM <= 0.1) power = std::max(power, 0.0);
+ Thruster->Calculate(power);
+
+ RunPostFunctions();
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+double FGPistonDiesel::CalcFuelNeed()
+{
+ FuelExpended = FuelFlowRate * in.TotalDeltaT;
+ if (!Starved) FuelUsedLbs += FuelExpended;
+ return FuelExpended;
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+int FGPistonDiesel::InitRunning()
+{
+ // No mixture control on diesel - set throttle to idle.
+ in.ThrottleCmd[EngineNumber] = 0.0;
+ in.ThrottlePos[EngineNumber] = 0.0;
+
+ Thruster->SetRPM(2.0 * IdleRPM / Thruster->GetGearRatio());
+ Running = true;
+ Cranking = false;
+
+ // Initialize temperatures to warm engine conditions
+ // rather than ambient - the engine is already running.
+ CoolantTemperature_degK = 273.15 + 80.0; // 80 deg C - normal operating temp
+ OilTemp_degK = 273.15 + 75.0; // 75 deg C - warm oil
+ CylinderHeadTemp_degK = 273.15 + 90.0; // 90 deg C - warm CHT
+
+ // Governor will settle fuel rack to idle on first frame.
+ FuelRack = IdleFuelRack;
+
+ // No boost at idle.
+ CurrentBoost_Pa = 0.0;
+
+ return 1;
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Start or stop the engine.
+ */
+
+void FGPistonDiesel::doEngineStartup()
+{
+ // Check parameters that may alter the operating state of the engine
+ // (fuel, starter motor, etc.).
+ // Neglects battery voltage, master-on switch, etc. for now.
+
+ // We will 'run' with any fuel flow. If there is not enough fuel to
+ // make power it will show in doEnginePower.
+ bool fuel = (FuelFlowRate > 0.0);
+
+ // Cranking follows the starter command, but the overrunning clutch disengages
+ // the starter automatically once the engine is self-sustaining.
+ Cranking = Starter && !Running;
+
+ // Cut the engine *power* - the engine will continue to spin
+ // depending on prop Ixx and freestream velocity.
+ if (Running) {
+ if (!fuel) Running = false;
+ if (RPM < IdleRPM * 0.5) Running = false;
+ } else {
+ if (fuel && RPM > IdleRPM * 0.5) {
+ // This allows in-air start when windmilling.
+ Running = true;
+ }
+ }
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+void FGPistonDiesel::doTurbo()
+{
+ // Simple exhaust-driven turbo model.
+ // Boost is a function of RPM and load - more exhaust energy
+ // means more turbine power means more boost.
+
+ double target_boost = 0.0;
+
+ if (Running && RPM > BoostThreshold_RPM) {
+ // Boost builds proportionally to RPM above threshold,
+ // scaled by fuel rack (more fuel = more exhaust energy).
+ double rpm_factor = (RPM - BoostThreshold_RPM)
+ / (MaxRPM - BoostThreshold_RPM);
+ rpm_factor = Constrain(0.0, rpm_factor, 1.0);
+
+ target_boost = BoostMaxPressure_Pa * rpm_factor * FuelRack;
+ }
+
+ // Turbo lag - first-order lag on spool-up and spool-down.
+ double dt = in.TotalDeltaT;
+ CurrentBoost_Pa += (target_boost - CurrentBoost_Pa)
+ * (1.0 - std::exp(-dt / TurboLag_tau));
+
+ // Apply boost to intake pressure.
+ MAP = p_amb + CurrentBoost_Pa;
+
+ // Boost power loss - turbine extracts energy from exhaust.
+ if (BoostLossFactor > 0.0) {
+ BoostLossHP = HP * BoostLossFactor * (CurrentBoost_Pa / BoostMaxPressure_Pa);
+ }
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the air flow through the engine.
+ * Also calculates ambient air density (used in CHT calculation).
+ *
+ * Inputs: p_amb, R_air, T_amb, MAP, Displacement, RPM, volumetric_efficiency
+ *
+ * Outputs: rho_air, m_dot_air, volumetric_efficiency_reduced
+ */
+
+void FGPistonDiesel::doAirFlow()
+{
+ // Diesel: no throttle plate. Intake is always at full pressure.
+ // MAP = ambient pressure (naturally aspirated) or boost pressure (turbo).
+
+ double intake_pressure = MAP;
+ double intake_temp = T_amb;
+
+ // Air density at ambient and intake conditions
+ rho_air = p_amb / (R_air * T_amb);
+ double rho_intake = intake_pressure / (R_air * intake_temp);
+
+ // Swept volume flow rate (4-stroke: intake every other rev)
+ double swept_volume = (displacement_SI * (RPM / 60.0)) / (Cycles / 2.0);
+
+ // Volumetric efficiency: peaks around 42 % of max RPM for a generic NA diesel.
+ double rpm_norm = RPM / MaxRPM;
+ double ve_base = VE_peak * (1.0 - 0.35 * std::pow(rpm_norm - 0.42, 2));
+
+ // High RPM falloff steeper than gasoline due to heavier valve train
+ // and swirl requirements.
+ if (rpm_norm > 0.75) {
+ ve_base -= 0.15 * std::pow(rpm_norm - 0.75, 1.5);
+ }
+
+ // Airbox impedance correction
+ if (Z_airbox > 0) {
+ double airbox_loss = Z_airbox * m_dot_air;
+ ve_base *= std::max(0.5, 1.0 - airbox_loss);
+ }
+
+ volumetric_efficiency_reduced = ve_base;
+
+ // Residual gas correction - minimal for diesel but not zero.
+ double residual_fraction = 0.03;
+ if (Boosted) {
+ // Positive scavenging: boost > exhaust, less residual.
+ residual_fraction = std::max(0.01, 0.03 * (p_amb / intake_pressure));
+ }
+ volumetric_efficiency_reduced *= (1.0 - residual_fraction);
+
+ // Clamp VE to sane range
+ volumetric_efficiency_reduced = Constrain(0.0, volumetric_efficiency_reduced, 1.0);
+
+ // Volume and mass flow
+ v_dot_air = swept_volume * volumetric_efficiency_reduced;
+ m_dot_air = v_dot_air * rho_intake;
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the fuel rack position based on governor and driver demand.
+ *
+ * Models a mechanical centrifugal governor typical of Bosch VE / Lucas DPC
+ * rotary distributor injection pumps.
+ *
+ * Inputs: ThrottlePos, RPM, m_dot_air, CoolantTemperature_degK, Running, Cranking
+ *
+ * Outputs: FuelRack, m_fuel_per_cycle, m_dot_fuel, FuelFlow_pps
+ */
+
+void FGPistonDiesel::doFuelRack()
+{
+ double dt = in.TotalDeltaT;
+
+ // === FUEL CUTOFF ===
+ // Electromagnetic shutoff solenoid - no fuel when engine is off.
+ if (!Running && !Cranking) {
+ // Solenoid cuts fuel — rack target is zero, but decay through
+ // governor lag so that a rapid Running toggle doesn't reset
+ // the rack to zero and prevent restart.
+ double dt = in.TotalDeltaT;
+ if (dt > 0.0) {
+ FuelRack *= std::exp(-dt / governor_tau);
+ } else {
+ FuelRack = 0.0;
+ } m_fuel_per_cycle = 0.0;
+ m_dot_fuel = 0.0;
+ FuelFlow_pps = 0.0;
+ return;
+ }
+
+ // === DRIVER DEMAND ===
+ // Throttle lever moves the control lever on the injection pump.
+ double throttle_cmd = in.ThrottlePos[EngineNumber];
+
+ // Demanded RPM: pedal position maps to a target RPM range.
+ double RPM_demanded = IdleRPM + throttle_cmd * (MaxRPM - IdleRPM);
+
+ // === CENTRIFUGAL GOVERNOR ===
+ // Compares actual RPM to demanded RPM and adjusts fuel delivery.
+ // Modeled as a proportional controller with droop.
+ double rpm_error = RPM_demanded - RPM;
+ double governor_gain = 1.0 / (governor_droop * MaxRPM);
+ double rack_governor = 0.5 + rpm_error * governor_gain;
+
+ // === IDLE GOVERNOR ===
+ // Below idle RPM, governor forces fuel up to maintain idle.
+ double idle_rack;
+ if (RPM < IdleRPM) {
+ double idle_error = IdleRPM - RPM;
+ idle_rack = IdleFuelRack + idle_error * idle_governor_gain;
+ } else {
+ idle_rack = IdleFuelRack;
+ }
+
+ // === MAX RPM GOVERNOR (CUTOFF) ===
+ // Flyweights hit a stiffer spring - fuel drops rapidly.
+ double max_rpm_rack = 1.0;
+ if (RPM > MaxRPM * 0.95) {
+ double overspeed = (RPM - MaxRPM * 0.95) / (MaxRPM * 0.10);
+ max_rpm_rack = 1.0 - overspeed * 2.0;
+ }
+
+ // === CONTROL LEVER LIMIT ===
+ // Driver's pedal sets the ceiling on fuel delivery.
+ double lever_max_rack = 0.10 + throttle_cmd * 0.90;
+
+ // === COMBINE ===
+ double rack_raw = rack_governor;
+ rack_raw = std::min(rack_raw, lever_max_rack); // driver demand ceiling
+ rack_raw = std::min(rack_raw, max_rpm_rack); // overspeed protection
+ rack_raw = std::max(rack_raw, idle_rack); // idle governor floor - ALWAYS LAST
+
+ double injections_per_sec = (RPM / 60.0) * Cylinders / (Cycles / 2.0);
+
+ // === SMOKE LIMIT ===
+ // Cannot inject more fuel than the available air can burn.
+ if (!Cranking) {
+ double m_air_per_cycle = 0.0;
+ if (injections_per_sec > 0.0) {
+ m_air_per_cycle = m_dot_air / injections_per_sec;
+ }
+ double m_fuel_smoke = m_air_per_cycle / AFR_smoke_limit;
+ double rack_smoke_limit = 1.0;
+ if (m_fuel_max_per_cycle > 0.0) {
+ rack_smoke_limit = m_fuel_smoke / m_fuel_max_per_cycle;
+ }
+ rack_raw = std::min(rack_raw, rack_smoke_limit);
+ }
+
+// === COLD START ENRICHMENT ===
+ // Thermo-wax element allows extra fuel when engine is cold.
+ double CoolantTemp_degC = CoolantTemperature_degK - 273.15;
+ double cold_enrichment = 1.0;
+ if (CoolantTemp_degC < cold_enrichment_threshold) {
+ cold_enrichment = 1.0 + (cold_enrichment_max - 1.0)
+ * (cold_enrichment_threshold - CoolantTemp_degC)
+ / cold_enrichment_threshold;
+ }
+ // Cold enrichment raises smoke limit and lever limit only
+ double rack_ceiling = std::min(lever_max_rack * cold_enrichment, 1.0);
+ if (rack_raw <= lever_max_rack || rack_raw > rack_ceiling) {
+ rack_raw = std::min(rack_raw, rack_ceiling);
+ }
+
+ // === CRANKING ENRICHMENT ===
+ // During cranking, governor isn't spinning fast enough to regulate.
+ if (Cranking && !Running) {
+ rack_raw = std::max(rack_raw, IdleFuelRack * 1.5);
+ }
+
+ // === GOVERNOR DYNAMIC RESPONSE ===
+ // Mechanical governor has inertia - first-order lag.
+ if (dt > 0.0 && !Cranking) {
+ FuelRack += (rack_raw - FuelRack) * (1.0 - std::exp(-dt / governor_tau));
+ } else {
+ FuelRack = rack_raw;
+ }
+
+ // Final clamp
+ FuelRack = Constrain(0.0, FuelRack, 1.0);
+
+ // === FUEL QUANTITY ===
+ m_fuel_per_cycle = FuelRack * m_fuel_max_per_cycle;
+
+ // === FUEL FLOW RATE ===
+ m_dot_fuel = m_fuel_per_cycle * injections_per_sec; // kg/s
+ FuelFlow_pps = m_dot_fuel * kgtolb; // lb/s
+ FuelFlow_gph = m_dot_fuel * 3600.0 / 1000.0; // g/h
+ FuelFlowRate = FuelFlow_pps; // for CalcFuelNeed
+
+ // === STARVED CHECK ===
+ if (Starved) {
+ FuelRack = 0.0;
+ m_fuel_per_cycle = 0.0;
+ m_dot_fuel = 0.0;
+ FuelFlow_pps = 0.0;
+ FuelFlowRate = 0.0;
+ }
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the fuel flow into the engine.
+ *
+ * Inputs: m_dot_air, m_dot_fuel, FuelRack
+ *
+ * Outputs: equivalence_ratio, combustion_efficiency
+ */
+
+void FGPistonDiesel::doFuelFlow()
+{
+ // Air/Fuel ratio
+ if (m_dot_fuel > 0.0) {
+ double AFR = m_dot_air / m_dot_fuel;
+ // Equivalence ratio: actual FAR / stoichiometric FAR
+ // Stoichiometric AFR for diesel is approximately 14.5
+ equivalence_ratio = 14.5 / AFR;
+ } else {
+ equivalence_ratio = 0.0;
+ }
+
+ // Combustion efficiency lookup from fuel rack position.
+ if (Lookup_Combustion_Efficiency) {
+ combustion_efficiency = Lookup_Combustion_Efficiency->GetValue(FuelRack);
+ } else {
+ combustion_efficiency = 0.98;
+ }
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the engine power output.
+ *
+ * Inputs: m_fuel_per_cycle, combustion_efficiency, RPM,
+ * eta_indicated_calibrated, FuelRack, displacement_SI,
+ * Cylinders, Cycles
+ *
+ * Outputs: HP, Torque_SI, IndicatedHorsePower
+ */
+
+void FGPistonDiesel::doEnginePower()
+{
+ if (!Running && !Cranking) {
+ HP = 0.0;
+ Torque_SI = 0.0;
+ IndicatedHorsePower = 0.0;
+ return;
+ }
+
+ double V_cylinder = displacement_SI / Cylinders;
+
+ // === INDICATED EFFICIENCY WITH CORRECTIONS ===
+
+ // RPM correction: efficiency drops at very low and very high RPM.
+ double rpm_ratio = RPM / RatedRPM;
+ double eta_rpm_correction;
+ if (rpm_ratio < 0.4) {
+ // Low RPM: heat losses dominate, poor spray atomization
+ eta_rpm_correction = 0.92 + 0.2 * rpm_ratio;
+ } else if (rpm_ratio < 0.85) {
+ // Mid range: best efficiency
+ eta_rpm_correction = 1.0;
+ } else {
+ // High RPM: less time for combustion
+ eta_rpm_correction = 1.0 - 0.4 * (rpm_ratio - 0.85);
+ }
+
+ // Load correction: diesel is most efficient at 60-80% load.
+ double load_ratio = FuelRack;
+ double eta_load_correction;
+ if (load_ratio < 0.1) {
+ eta_load_correction = 0.70;
+ } else if (load_ratio < 0.3) {
+ eta_load_correction = 0.70 + (load_ratio - 0.1) * 1.5;
+ } else if (load_ratio < 0.8) {
+ eta_load_correction = 1.0;
+ } else {
+ eta_load_correction = 1.0 - 0.1 * (load_ratio - 0.8);
+ }
+
+ double eta_current = eta_indicated_calibrated
+ * eta_rpm_correction
+ * eta_load_correction;
+
+ // === IMEP FROM FUEL ENERGY ===
+ double IMEP = (eta_current * m_fuel_per_cycle * calorific_value_fuel
+ * combustion_efficiency) / V_cylinder;
+
+ // === FRICTION LOSSES ===
+ double MPS = Stroke * intom * RPM / 30.0;
+ double FMEP_local = FMEPStatic + FMEPDynamic * MPS;
+
+ // === BMEP AND TORQUE ===
+ double BMEP = IMEP - FMEP_local;
+
+ Torque_SI = (BMEP * displacement_SI) / (2.0 * M_PI * (Cycles / 2.0));
+
+ // === POWER ===
+ double omega = RPM * 2.0 * M_PI / 60.0;
+ HP = Torque_SI * omega / 745.7;
+
+ // === INDICATED POWER (for stall detection) ===
+ double Torque_indicated = (IMEP * displacement_SI)
+ / (2.0 * M_PI * (Cycles / 2.0));
+ IndicatedHorsePower = Torque_indicated * omega / 745.7;
+
+ // === STATIC FRICTION ===
+ // Additional parasitic loss from accessories (alternator, water pump, etc.)
+ // Not applied during cranking: at standstill the starter torque is tiny
+ // (~0.004 HP) and the 1 HP accessory drag would clamp net HP to zero,
+ // preventing the engine from ever starting. FMEP already models the
+ // mechanical cranking resistance.
+ if (Running || !Cranking) {
+ HP -= StaticFriction_HP;
+ }
+
+ // === BOOST LOSS ===
+ if (Boosted && BoostLossHP > 0.0) {
+ HP -= BoostLossHP;
+ }
+
+ // === STARTER MOTOR ===
+ // A diesel fires by compression ignition, so combustion contributes during
+ // cranking. Add starter torque on top of the combustion model so both
+ // sources help spin the engine up to the Running transition RPM.
+ // Clamp rpm to 1.0 so the starter produces nonzero power even at standstill.
+ if (Cranking) {
+ double rpm = RPM < 1.0 ? 1.0 : RPM;
+ double k_torque = (rpm < StarterRPM) ? 1.0 - rpm / StarterRPM : 0.0;
+ HP += StarterTorque * k_torque * StarterGain * rpm / 5252.0;
+ }
+
+ // Engine can produce negative power (being motored). Leave it negative
+ // so the propeller/thruster sees the drag correctly.
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the exhaust gas temperature.
+ *
+ * Inputs: equivalence_ratio, m_dot_fuel, calorific_value_fuel,
+ * Cp_air, m_dot_air, T_amb
+ *
+ * Outputs: ExhaustGasTemp_degK, EGT_degC
+ */
+
+void FGPistonDiesel::doEGT()
+{
+ if (Running && m_dot_air > 0.0 && m_dot_fuel > 0.0) {
+ // Exhaust enthalpy: fuel energy not converted to work and not
+ // lost to coolant goes out the exhaust.
+ double exhaust_fraction = 1.0 - eta_indicated_calibrated - coolant_heat_fraction;
+ exhaust_fraction = Constrain(0.15, exhaust_fraction, 0.50);
+
+ double enthalpy_exhaust = m_dot_fuel * calorific_value_fuel
+ * combustion_efficiency * exhaust_fraction;
+
+ // Heat capacity of exhaust gas - mostly air since diesel
+ // always runs lean with large excess air.
+ double heat_capacity_exhaust = Cp_air * (m_dot_air + m_dot_fuel);
+
+ double delta_T_exhaust = enthalpy_exhaust / heat_capacity_exhaust;
+ double target_EGT = T_amb + delta_T_exhaust;
+
+ // EGT doesn't change instantly - exhaust manifold has thermal mass.
+ double egt_tau = 2.0; // seconds
+ double dt = in.TotalDeltaT;
+ if (dt > 0.0) {
+ ExhaustGasTemp_degK += (target_EGT - ExhaustGasTemp_degK)
+ * (1.0 - std::exp(-dt / egt_tau));
+ }
+ } else {
+ // Engine not running - cool down toward ambient.
+ double dEGTdt = (T_amb - ExhaustGasTemp_degK) / 100.0;
+ ExhaustGasTemp_degK += dEGTdt * in.TotalDeltaT;
+ }
+
+ EGT_degC = ExhaustGasTemp_degK - 273.15;
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the cylinder head temperature.
+ *
+ * Inputs: T_amb, IAS, rho_air, m_dot_fuel, calorific_value_fuel,
+ * combustion_efficiency, RPM, MaxRPM, Displacement, Cylinders
+ *
+ * Outputs: CylinderHeadTemp_degK
+ */
+
+void FGPistonDiesel::doCHT()
+{
+ double dt = in.TotalDeltaT;
+ if (dt <= 0.0) return;
+
+ double CpCylinderHead = 800.0; // J/(kg*K) - cast iron/aluminum
+ double MassCylinderHead = CylinderHeadMass * Cylinders;
+ double HeatCapacityCylinderHead = CpCylinderHead * MassCylinderHead;
+
+ // === HEAT INPUT FROM COMBUSTION ===
+ // Cylinder head absorbs ~12 % of fuel energy directly.
+ double dqdt_from_combustion =
+ m_dot_fuel * calorific_value_fuel * combustion_efficiency * 0.12;
+
+ // === HEAT REJECTION TO COOLANT ===
+ // Primary cooling path on a liquid-cooled diesel.
+ double head_to_coolant_coeff = 45.0; // W/K
+
+ // Coolant flow scales with RPM (water pump is crankshaft-driven).
+ double pump_flow;
+ if (RPM < 1.0) {
+ pump_flow = 0.0;
+ } else if (RPM < IdleRPM) {
+ pump_flow = RPM / IdleRPM;
+ } else {
+ pump_flow = 1.0;
+ }
+
+ double dqdt_to_coolant = head_to_coolant_coeff * pump_flow
+ * (CylinderHeadTemp_degK - CoolantTemperature_degK);
+
+ // === HEAT REJECTION TO AIR (SURFACE RADIATION/CONVECTION) ===
+ double arbitrary_area = Displacement / 360.0;
+ double h_free = -95.0; // free convection coefficient
+ double temperature_difference_amb = CylinderHeadTemp_degK - T_amb;
+ double dqdt_to_air = h_free * temperature_difference_amb * arbitrary_area;
+
+ // Ram air effect on external surfaces - minor on liquid-cooled.
+ double v_apparent = IAS * Cooling_Factor * 0.2;
+ double dqdt_ram = -2.0 * arbitrary_area * v_apparent * temperature_difference_amb;
+
+ // === NET HEAT BALANCE ===
+ double dqdt_net = dqdt_from_combustion - dqdt_to_coolant + dqdt_to_air + dqdt_ram;
+
+ CylinderHeadTemp_degK += (dqdt_net / HeatCapacityCylinderHead) * dt;
+
+ // Clamp to physical limits.
+ CylinderHeadTemp_degK = Constrain(T_amb, CylinderHeadTemp_degK, 273.15 + 350.0);
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate coolant temperature.
+ *
+ * Liquid cooling model for diesel engine. Heat input from combustion,
+ * heat rejection through radiator scaled by airspeed.
+ * Thermostat controls coolant flow through radiator.
+ *
+ * Inputs: m_dot_fuel, calorific_value_fuel, combustion_efficiency,
+ * T_amb, IAS, RPM, coolant_heat_fraction
+ *
+ * Outputs: CoolantTemperature_degK
+ */
+
+void FGPistonDiesel::doCoolantTemperature()
+{
+ double dt = in.TotalDeltaT;
+ if (dt <= 0.0) return;
+
+ double Q_fuel_total = m_dot_fuel * calorific_value_fuel * combustion_efficiency;
+ double Q_coolant_in = Q_fuel_total * coolant_heat_fraction; // watts
+
+ // === THERMOSTAT ===
+ // Wax thermostat: closed below 83 deg C, fully open above 89 deg C.
+ double coolant_degC = CoolantTemperature_degK - 273.15;
+ double thermostat;
+ if (coolant_degC < 83.0) {
+ thermostat = 0.05; // small bypass flow even when closed
+ } else if (coolant_degC > 89.0) {
+ thermostat = 1.0;
+ } else {
+ thermostat = 0.05 + 0.95 * (coolant_degC - 83.0) / 6.0;
+ }
+
+ // === RADIATOR HEAT REJECTION ===
+ double delta_T = CoolantTemperature_degK - T_amb;
+ double airflow_factor = 0.8 + IAS * 0.015;
+
+ double Q_radiator = thermostat * Cooling_Factor * delta_T * airflow_factor;
+
+ // === ENGINE BLOCK RADIATION ===
+ double Q_block = 0.008 * delta_T;
+
+ // === WATER PUMP ===
+ // Coolant only circulates when engine is turning.
+ double pump_factor;
+ if (RPM < 1.0) {
+ pump_factor = 0.0;
+ } else if (RPM < IdleRPM) {
+ pump_factor = RPM / IdleRPM;
+ } else {
+ pump_factor = 1.0;
+ }
+
+ Q_radiator *= pump_factor;
+
+ // === NET HEAT AND TEMPERATURE CHANGE ===
+ double thermal_mass = 5.5 * 3500.0 + CylinderHeadMass * Cylinders * 500.0;
+
+ double Q_net = Q_coolant_in - Q_radiator - Q_block;
+ double dT = (Q_net * dt) / thermal_mass;
+
+ CoolantTemperature_degK += dT;
+ CoolantTemperature_degK = Constrain(T_amb, CoolantTemperature_degK, 273.15 + 130.0);
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the oil temperature.
+ *
+ * Inputs: CylinderHeadTemp_degK, T_amb, OilPressure_psi
+ *
+ * Outputs: OilTemp_degK
+ */
+
+void FGPistonDiesel::doOilTemperature()
+{
+ double dt = in.TotalDeltaT;
+ if (dt <= 0.0) return;
+
+ // Oil target temperature: when running, oil sits above coolant temp
+ // by a load-dependent margin; when stopped, oil cools toward ambient.
+ double oil_target;
+ if (Running) {
+ double load_offset = 10.0 + HP / MaxHP * 15.0; // 10-25 deg C above coolant
+ oil_target = CoolantTemperature_degK + load_offset;
+ } else {
+ oil_target = T_amb;
+ }
+
+ // Oil thermal mass time constant ~150 seconds for automotive diesel.
+ constexpr double oil_tau = 150.0;
+
+ OilTemp_degK += (oil_target - OilTemp_degK)
+ * (1.0 - std::exp(-dt / oil_tau));
+
+ OilTemp_degK = Constrain(T_amb, OilTemp_degK, 273.15 + 160.0);
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+/**
+ * Calculate the oil pressure.
+ *
+ * Inputs: RPM, MaxRPM, OilTemp_degK
+ *
+ * Outputs: OilPressure_psi
+ */
+
+void FGPistonDiesel::doOilPressure()
+{
+ // Oil pump is mechanically driven - pressure proportional to RPM
+ // up to the relief valve setting.
+ double rpm_ratio = (Oil_Press_RPM_Max > 0.0)
+ ? RPM / Oil_Press_RPM_Max
+ : 0.0;
+
+ double base_pressure = Oil_Press_Relief_Valve * rpm_ratio;
+
+ // Viscosity correction: cold oil is thicker (higher pressure),
+ // hot oil is thinner (lower pressure).
+ double temp_ratio = (Design_Oil_Temp > 0.0)
+ ? Design_Oil_Temp / OilTemp_degK
+ : 1.0;
+ double viscosity_factor = std::pow(temp_ratio, Oil_Viscosity_Index);
+
+ OilPressure_psi = base_pressure * viscosity_factor;
+
+ // Relief valve limits maximum pressure.
+ OilPressure_psi = std::min(OilPressure_psi, Oil_Press_Relief_Valve);
+
+ // No pressure when engine isn't turning.
+ if (RPM < 1.0) {
+ OilPressure_psi = 0.0;
+ }
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+std::string FGPistonDiesel::GetEngineLabels(const std::string& delimiter)
+{
+ std::ostringstream buf;
+
+ buf << Name << " Power Available (engine " << EngineNumber << " in ft-lbs/sec)" << delimiter
+ << Name << " HP (engine " << EngineNumber << ")" << delimiter
+ << Name << " equivalent ratio (engine " << EngineNumber << ")" << delimiter
+ << Name << " MAP (engine " << EngineNumber << " in inHg)" << delimiter
+ << Thruster->GetThrusterLabels(EngineNumber, delimiter);
+
+ return buf.str();
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+std::string FGPistonDiesel::GetEngineValues(const std::string& delimiter)
+{
+ std::ostringstream buf;
+
+ buf << (HP * hptoftlbssec) << delimiter
+ << HP << delimiter
+ << equivalence_ratio << delimiter
+ << (MAP / inhgtopa) << delimiter
+ << Thruster->GetThrusterValues(EngineNumber, delimiter);
+
+ return buf.str();
+}
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+//
+// The bitmasked value choices are as follows:
+// unset: In this case (the default) JSBSim would only print
+// out the normally expected messages, essentially echoing
+// the config files as they are read. If the environment
+// variable is not set, debug_lvl is set to 1 internally
+// 0: This requests JSBSim not to output any messages
+// whatsoever.
+// 1: This value explicitly requests the normal JSBSim
+// startup messages
+// 2: This value asks for a message to be printed out when
+// a class is instantiated
+// 4: When this value is set, a message is displayed when a
+// FGModel object executes its Run() method
+// 8: When this value is set, various runtime state variables
+// are printed out periodically
+// 16: When set various parameters are sanity checked and
+// a message is printed out when they go out of bounds
+
+void FGPistonDiesel::Debug(int from)
+{
+ if (debug_lvl <= 0) return;
+
+ if (debug_lvl & 1) { // Standard console startup message output
+ if (from == 0) { // Constructor
+ FGLogging log(LogLevel::DEBUG);
+ log << "\n Engine Name: " << Name << "\n";
+ log << " Displacement: " << Displacement << "\n";
+ log << " Bore: " << Bore << "\n";
+ log << " Stroke: " << Stroke << "\n";
+ log << " Cylinders: " << Cylinders << "\n";
+ log << " Cylinder Head Mass: " << CylinderHeadMass << "\n";
+ log << " Compression Ratio: " << CompressionRatio << "\n";
+ log << " MaxHP: " << MaxHP << "\n";
+ log << " Cycles: " << Cycles << "\n";
+ log << " IdleRPM: " << IdleRPM << "\n";
+ log << " MaxRPM: " << MaxRPM << "\n";
+ log << " Volumetric Efficiency: " << volumetric_efficiency << "\n";
+ log << " PeakMeanPistonSpeed_fps: " << PeakMeanPistonSpeed_fps << "\n";
+ log << " Intake Impedance Factor: " << Z_airbox << "\n";
+ log << " Dynamic FMEP Factor: " << FMEPDynamic << "\n";
+ log << " Static FMEP Factor: " << FMEPStatic << "\n";
+ log << " Starter Motor Torque: " << StarterTorque << "\n";
+ log << " Starter Motor RPM: " << StarterRPM << "\n";
+ log << "\n";
+ log << " Combustion Efficiency table:\n";
+ Lookup_Combustion_Efficiency->Print();
+ log << "\n";
+ }
+ }
+ if (debug_lvl & 2) { // Instantiation/Destruction notification
+ FGLogging log(LogLevel::DEBUG);
+ if (from == 0) log << "Instantiated: FGPistonDiesel\n";
+ if (from == 1) log << "Destroyed: FGPistonDiesel\n";
+ }
+ if (debug_lvl & 4) { // Run() method entry print for FGModel-derived objects
+ }
+ if (debug_lvl & 8) { // Runtime state variables
+ }
+ if (debug_lvl & 16) { // Sanity checking
+ }
+ if (debug_lvl & 64) {
+ if (from == 0) { // Constructor
+ }
+ }
+}
+
+} // namespace JSBSim
\ No newline at end of file
diff --git a/src/models/propulsion/FGPistonDiesel.h b/src/models/propulsion/FGPistonDiesel.h
new file mode 100644
index 0000000000..c0f022e1fa
--- /dev/null
+++ b/src/models/propulsion/FGPistonDiesel.h
@@ -0,0 +1,425 @@
+/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+ Header: FGPistonDiesel.h
+ Author: Marc Traverso
+ Date started: 09/12/2000
+
+ ------------- Copyright (C) 2000 Jon S. Berndt (jon@jsbsim.org) --------------
+
+ This program is free software; you can redistribute it and/or modify it under
+ the terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 2 of the License, or (at your option) any later
+ version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ details.
+
+ You should have received a copy of the GNU Lesser General Public License along with
+ this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ Further information about the GNU Lesser General Public License can also be found on
+ the world wide web at http://www.gnu.org.
+
+HISTORY
+--------------------------------------------------------------------------------
+14/04/2026 MTR Derived FGPiston to create the FGPistonDiesel class definition
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+SENTRY
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
+
+#ifndef FGPISTONDIESEL_H
+#define FGPISTONDIESEL_H
+
+/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+INCLUDES
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
+
+#include "FGEngine.h"
+#include "math/FGTable.h"
+
+#include
+#include
+
+/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+FORWARD DECLARATIONS
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
+
+namespace JSBSim {
+
+/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+CLASS DOCUMENTATION
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
+
+/** Models a compression-ignition (diesel) piston engine with optional
+ turbocharging, mechanical governor fuel-rack control, and liquid cooling.
+
+ The model implements the thermodynamic cycle of a four-stroke diesel engine
+ from first principles: volumetric air flow through an unthrottled intake,
+ fuel injection metered by a centrifugal-governor-controlled fuel rack,
+ indicated mean effective pressure derived from fuel energy and indicated
+ efficiency, friction losses via Chen-Flynn correlation, and a turbocharger
+ modeled as a first-order-lag exhaust-driven compressor.
+
+ Thermal sub-models cover exhaust gas temperature, cylinder head temperature,
+ liquid coolant temperature (with wax-element thermostat), oil temperature,
+ and oil pressure. Cold-start behavior is influenced by glow-plug state and
+ coolant-temperature-dependent enrichment.
+
+ Most parameters are derived automatically from a small set of headline
+ values (displacement, rated power, rated RPM, compression ratio). Any
+ derived value can be overridden explicitly in the configuration file.
+
+Configuration File Format:
+
+@code
+
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {DI | IDI}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {true | false}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+ {number}
+
+@endcode
+
+Definition of the diesel engine configuration file parameters:
+
+Basic parameters:
+- \b displacement - swept volume of all cylinders. Used to compute mass air
+ flow, which drives power and cooling.
+- \b maxhp - rated brake power at \b maxrpm. Determines BSFC and starter
+ torque when those are not specified explicitly.
+- \b maxrpm - absolute maximum engine speed. The overspeed governor cuts
+ fuel above 95 % of this value.
+- \b idlerpm - target idle speed maintained by the idle governor spring.
+ The engine stalls if speed falls below 80 % of this value.
+- \b cycles - designate a 2- or 4-stroke engine (only 4-stroke is currently
+ supported).
+- \b bore - cylinder bore (currently unused; reserved for future heat-transfer
+ refinement).
+- \b stroke - piston stroke. Determines mean piston speed, which controls
+ the high-RPM fall-off of volumetric efficiency and the Chen-Flynn
+ friction model.
+- \b cylinders - number of cylinders. Scales cylinder head thermal mass.
+- \b compression-ratio - geometric compression ratio. Affects the
+ automatic BSFC estimate and altitude lapse of volumetric efficiency.
+- \b rated-rpm - engine speed at which \b maxhp is produced. Defaults to
+ 85 % of \b maxrpm.
+- \b peak-torque-rpm - RPM at which peak torque occurs. Defaults to
+ 55 % of \b rated-rpm.
+- \b static-friction - parasitic power required to turn a non-firing engine.
+ Models accessory drag and windmilling resistance. Choose a small
+ percentage of \b maxhp.
+- \b starter-torque - zero-RPM torque of the starter motor in lb-ft.
+ Defaults to 40 % of \b maxhp.
+- \b starter-rpm - unloaded peak RPM of the starter motor.
+
+Advanced parameters:
+- \b bsfc-best - best brake-specific fuel consumption in g/(kW*h). If
+ omitted, it is estimated from compression ratio, injection type,
+ and displacement using an empirical correlation.
+- \b volumetric-efficiency - nominal volumetric efficiency. Turbocharged
+ engines may require values above 1. Typical range is 0.80-0.90.
+- \b ve-peak - peak volumetric efficiency used by the RPM-dependent VE
+ curve. If omitted, it is back-calculated from rated airflow.
+- \b air-intake-impedance-factor - pressure drop across the intake system.
+ Increasing this reduces manifold pressure. Also a run-time property.
+- \b ram-air-factor - multiplier on dynamic-pressure ram-air recovery.
+ Also a run-time property.
+- \b dynamic-fmep - Chen-Flynn friction coefficient proportional to mean
+ piston speed, in Pa/(m/s).
+- \b static-fmep - Chen-Flynn friction constant term, in Pa.
+- \b peak-piston-speed - mean piston speed at which intake begins to choke.
+- \b injection-type - DI (direct injection) or IDI (indirect injection).
+ IDI applies a 0.92 penalty to estimated brake efficiency.
+- \b afr-smoke-limit - minimum air/fuel ratio below which visible smoke
+ is produced. The fuel rack is clamped to respect this limit.
+- \b calorific-value-fuel - lower heating value of fuel in J/kg. Defaults
+ to 42.8 MJ/kg (EN 590 diesel).
+
+Governor parameters:
+- \b governor-droop - proportional droop of the centrifugal governor
+ (dimensionless, typically 0.04-0.08).
+- \b governor-tau - time constant of the mechanical governor response, in
+ seconds.
+- \b idle-fuel-rack - normalised fuel-rack position that sustains idle.
+- \b idle-governor-gain - additional proportional gain of the idle-speed
+ governor, in rack-units per RPM.
+
+Cold-start parameters:
+- \b cold-enrichment-max - maximum fuel enrichment multiplier when the
+ engine is cold.
+- \b cold-enrichment-threshold - coolant temperature in degrees C below which
+ enrichment begins.
+- \b glow-plug-threshold - ambient temperature in K below which the glow
+ plug must be energised for reliable starting.
+
+Cooling parameters:
+- \b cylinder-head-mass - nominal mass per cylinder head in kg. A larger
+ value slows temperature transients.
+- \b cooling-factor - overall effectiveness of the cooling installation.
+ Also a run-time property.
+- \b coolant-heat-fraction - fraction of fuel energy rejected to the
+ coolant circuit (typically 0.25-0.35).
+
+Turbocharger parameters:
+- \b boosted - set to "true" to enable the exhaust-driven turbocharger
+ model.
+- \b boost-max-pressure - maximum compressor outlet pressure in Pa
+ (absolute). Defaults to 1.5 bar if boosted is true.
+- \b boost-threshold-rpm - engine RPM below which the turbocharger
+ produces no meaningful boost. Defaults to 1.5 * \b idlerpm.
+- \b turbo-lag - spool-up/spool-down time constant in seconds.
+- \b boost-loss-factor - fraction of shaft power consumed by driving
+ the compressor. Zero for a free-running turbocharger.
+
+Oil system parameters:
+- \b oil-pressure-relief-valve-psi - relief-valve crack pressure.
+- \b design-oil-temp-degK - design-point oil temperature used for
+ viscosity normalisation.
+- \b oil-pressure-rpm-max - engine RPM at which the oil pump reaches
+ full rated flow. Defaults to 75 % of \b maxrpm.
+- \b oil-viscosity-index - exponent controlling the sensitivity of oil
+ pressure to temperature.
+
+ @author Jon S. Berndt (Engine framework code and framework-related mods)
+ @author Dave Luff (engine operational code)
+ @author David Megginson (initial porting and additional code)
+ @author Ron Jensen (additional engine code)
+ @author Marc Traverso (diesel adaptation)
+ @see Taylor, Charles Fayette, "The Internal Combustion Engine in Theory and Practice"
+ */
+
+/*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+CLASS DECLARATION
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
+
+class FGPistonDiesel : public FGEngine
+{
+public:
+ /// Constructor
+ FGPistonDiesel(FGFDMExec* exec, Element* el, int engine_number, struct Inputs& input);
+ /// Destructor
+ ~FGPistonDiesel();
+
+ std::string GetEngineLabels(const std::string& delimiter);
+ std::string GetEngineValues(const std::string& delimiter);
+
+ void Calculate();
+ double GetPowerAvailable() const { return HP * hptoftlbssec; }
+ double CalcFuelNeed();
+
+ void ResetToIC();
+
+ double GetEGT() const { return EGT_degC; }
+
+ double getExhaustGasTemp_degF() const { return KelvinToFahrenheit(ExhaustGasTemp_degK); }
+ double getManifoldPressure_inHg() const { return MAP / inhgtopa; }
+ double getCylinderHeadTemp_degF() const { return KelvinToFahrenheit(CylinderHeadTemp_degK); }
+ double getOilPressure_psi() const { return OilPressure_psi; }
+ double getOilTemp_degF() const { return KelvinToFahrenheit(OilTemp_degK); }
+ double getRPM() const { return RPM; }
+ double getAFR() const { return m_dot_fuel > 0.0 ? m_dot_air / m_dot_fuel : INFINITY; }
+ double getCoolantTemperature_degF() const { return KelvinToFahrenheit(CoolantTemperature_degK); }
+ double getBoostPa() const { return CurrentBoost_Pa; }
+ double getFuelRack() const { return FuelRack; }
+ double getHP() const { return HP; }
+ double getTorque_SI() const { return Torque_SI; }
+ double getVolumetricEfficiencyReduced() const { return volumetric_efficiency_reduced; }
+ double getCombustionEfficiency() const { return combustion_efficiency; }
+ double getIndicatedHorsePower() const { return IndicatedHorsePower; }
+
+ bool getGlowPlugOn() const { return GlowPlugOn; }
+
+private:
+ // --- Simulation step functions ---
+ void doEngineStartup();
+ void doTurbo();
+ void doAirFlow();
+ void doFuelRack();
+ void doFuelFlow();
+ void doEnginePower();
+ void doEGT();
+ void doCHT();
+ void doOilPressure();
+ void doOilTemperature();
+ void doCoolantTemperature();
+
+ int InitRunning();
+
+ // --- Physical constants ---
+ static constexpr double R_air = 287.3; // J/(kg*K) - gas constant for air
+ static constexpr double Cp_air = 1005.0; // J/(kg*K) - specific heat of air (const. pressure)
+ static constexpr double standard_pressure = 101320.73; // Pa
+
+ // --- Lookup tables ---
+ std::unique_ptr Lookup_Combustion_Efficiency;
+
+ // --- Configuration (read from XML or derived at init) ---
+
+ // Geometry
+ double Displacement = 0.0; // cubic inches
+ double displacement_SI = 0.0; // cubic metres
+ double Bore = 0.0; // inches
+ double Stroke = 0.0; // inches
+ double Cylinders = 0.0; // count
+ double CylinderHeadMass = 0.0; // kg
+ double CompressionRatio = 0.0;
+ double Cycles = 0.0; // strokes per power cycle
+
+ // Performance
+ double MaxHP = 0.0; // horsepower
+ double IdleRPM = 0.0; // rev/min
+ double MaxRPM = 0.0; // rev/min
+ double RatedRPM = 0.0; // rev/min
+ double PeakTorqueRPM = 0.0;
+
+ // Friction
+ double StaticFriction_HP = 0.0; // HP - accessory drag
+ double FMEPDynamic = 0.0; // Pa/(m/s) - Chen-Flynn speed term
+ double FMEPStatic = 0.0; // Pa - Chen-Flynn constant term
+
+ // Intake
+ double Z_airbox = 0.0; // intake impedance factor
+ double PeakMeanPistonSpeed_fps = 0.0; // ft/s - intake choking threshold
+ double RatedMeanPistonSpeed_fps = 0.0; // ft/s - derived from MaxRPM and stroke
+ double Ram_Air_Factor = 0.0;
+
+ // Starter
+ double StarterTorque = 0.0; // lb*ft peak torque
+ double StarterRPM = 0.0; // peak unloaded RPM
+ double StarterGain = 0.0; // normalised starter command
+
+ // Turbocharger
+ bool Boosted = false;
+ double BoostMaxPressure_Pa = 0.0; // maximum absolute boost pressure
+ double BoostThreshold_RPM = 0.0; // RPM below which turbo is inactive
+ double TurboLag_tau = 0.0; // spool-up time constant (seconds)
+ double BoostLossFactor = 0.0; // power extraction fraction
+
+ // Fuel system
+ double BSFC_best = 0.0; // g/(kW*h)
+ double VE_peak = 0.0;
+ double eta_indicated_calibrated = 0.0;
+ double calorific_value_fuel = 0.0; // J/kg
+ double Cp_fuel = 0.0; // J/(kg*K)
+ double AFR_smoke_limit = 0.0;
+ double m_fuel_max_per_cycle = 0.0; // kg per injection event
+
+ enum class InjectionType { DI, IDI };
+ InjectionType injection_type = InjectionType::IDI;
+
+ // Governor
+ double governor_droop = 0.06;
+ double governor_tau = 0.08; // seconds
+ double IdleFuelRack = 0.12;
+ double idle_governor_gain = 0.005; // rack/RPM
+
+ // Cold start
+ double cold_enrichment_max = 0.0;
+ double cold_enrichment_threshold = 0.0; // deg C
+ bool GlowPlugOn = false;
+ double GlowPlugThreshold_degK = 0.0;
+
+ // Liquid cooling
+ double coolant_heat_fraction = 0.0;
+
+ // Oil system
+ double Oil_Press_Relief_Valve = 0.0; // psi
+ double Oil_Press_RPM_Max = 0.0;
+ double Design_Oil_Temp = 0.0; // K
+ double Oil_Viscosity_Index = 0.0;
+
+ // --- Run-time state ---
+
+ int crank_counter = 0;
+
+ // Ambient / inputs
+ double p_amb = 0.0; // Pa
+ double p_ram = 0.0; // Pa
+ double T_amb = 0.0; // K
+ double RPM = 0.0; // rev/min
+ double IAS = 0.0; // knots
+ double Cooling_Factor = 0.0;
+
+ // Intake
+ double MAP = 0.0; // Pa - manifold/intake pressure
+ double intake_pressure_Pa = 0.0;
+
+ // Fuel
+ double FuelRack = 0.0; // normalised 0-1
+ double m_fuel_per_cycle = 0.0; // kg per injection event
+ double equivalence_ratio = 0.0;
+
+ // Computed outputs
+ double IndicatedHorsePower = 0.0;
+ double PMEP = 0.0;
+ double FMEP = 0.0;
+ double FuelFlow_pps = 0.0;
+ double rho_air = 0.0;
+ double volumetric_efficiency = 0.0;
+ double volumetric_efficiency_reduced = 0.0;
+ double m_dot_air = 0.0;
+ double v_dot_air = 0.0;
+ double m_dot_fuel = 0.0;
+ double HP = 0.0;
+ double BoostLossHP = 0.0;
+ double combustion_efficiency = 0.0;
+ double ExhaustGasTemp_degK = 0.0;
+ double EGT_degC = 0.0;
+ double CylinderHeadTemp_degK = 0.0;
+ double OilPressure_psi = 0.0;
+ double OilTemp_degK = 0.0;
+ double MeanPistonSpeed_fps = 0.0;
+ double CoolantTemperature_degK = 0.0;
+ double Torque_SI = 0.0;
+ double CurrentBoost_Pa = 0.0;
+
+ void Debug(int from);
+};
+
+} // namespace JSBSim
+
+//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+#endif
\ No newline at end of file
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 49eca821e0..96e4d08ef1 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -15,6 +15,7 @@ set(PYTHON_TESTS ResetOutputFiles
CheckOutputRate
TestAccelerometer
CheckDebugLvl
+ TestPythonDefaultLoggerFiltering
TestTurbulenceRandomSeed
TestCosineGust
TestTurbulenceGustStop
diff --git a/tests/TestPythonDefaultLoggerFiltering.py b/tests/TestPythonDefaultLoggerFiltering.py
new file mode 100644
index 0000000000..e1aa0dc120
--- /dev/null
+++ b/tests/TestPythonDefaultLoggerFiltering.py
@@ -0,0 +1,76 @@
+# TestPythonDefaultLoggerFiltering.py
+#
+# Test that the default logger is filtering messages according to the log level.
+#
+# Copyright (c) 2015-2026 Sean McLeod
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, see
+#
+
+from typing import final
+from JSBSim_utils import (ExecuteUntil, JSBSimTestCase, RunTest)
+from jsbsim import (DefaultLogger, LogLevel, set_logger)
+import io
+import sys
+
+class CapturedDefaultLogger(DefaultLogger):
+ def __init__(self):
+ super().__init__()
+ self.buffer = io.StringIO()
+
+ def file_location(self, filename: str, line: int) -> None:
+ original_stdout = sys.stdout
+ try:
+ sys.stdout = self.buffer
+ super().file_location(filename, line)
+ finally:
+ sys.stdout = original_stdout
+
+ def message(self, message: str) -> None:
+ original_stdout = sys.stdout
+ try:
+ sys.stdout = self.buffer
+ super().message(message)
+ finally:
+ sys.stdout = original_stdout
+
+class TestPythonDefaultLoggerFiltering(JSBSimTestCase):
+
+ def test_logoutput_fatal(self):
+ """Confirm that no log output occurs for LogLevel.FATAL"""
+ logger = CapturedDefaultLogger()
+ logger.set_min_level(LogLevel.FATAL)
+ set_logger(logger)
+
+ fdm = self.create_fdm()
+ fdm.load_script(self.sandbox.path_to_jsbsim_file('scripts', 'ball_orbit.xml'))
+ fdm.run_ic()
+ ExecuteUntil(fdm, 1000.)
+
+ self.assertEqual(logger.buffer.getvalue(), "")
+
+ def test_logoutput_bulk(self):
+ """Confirm that log output occurs for LogLevel.BULK"""
+ logger = CapturedDefaultLogger()
+ logger.set_min_level(LogLevel.BULK)
+ set_logger(logger)
+
+ fdm = self.create_fdm()
+ fdm.load_script(self.sandbox.path_to_jsbsim_file('scripts', 'ball_orbit.xml'))
+ fdm.run_ic()
+ ExecuteUntil(fdm, 1000.)
+
+ self.assertNotEqual(logger.buffer.getvalue(), "")
+
+RunTest(TestPythonDefaultLoggerFiltering)