diff --git a/RealFuels/CryoCooler.cfg b/RealFuels/CryoCooler.cfg
new file mode 100644
index 00000000..e73e48e3
--- /dev/null
+++ b/RealFuels/CryoCooler.cfg
@@ -0,0 +1,63 @@
+// Cryocooler defaults for RealFuels tanks.
+// Refer to \RealismOverhaul\RO_RealFuels_Crycoolers.cfg for references and formulas.
+
+@PART:HAS[@MODULE[ModuleFuelTanks]:HAS[#type[Cryogenic]]]:AFTER[RealFuels]
+{
+ &rfAddCryocooler = true
+}
+
+@PART:HAS[@MODULE[ModuleFuelTanks]:HAS[#type[BalloonCryo]]]:AFTER[RealFuels]
+{
+ &rfAddCryocooler = true
+}
+
+@PART:HAS[@MODULE[ModuleFuelTanks]:HAS[#type[ServiceModule]]]:AFTER[RealFuels]
+{
+ &rfAddCryocooler = true
+}
+
+@PART:HAS[#rfAddCryocooler[?rue]]:NEEDS[!RealismOverhaul]:AFTER[RealFuels]
+{
+ @MODULE[ModuleFuelTanks]
+ {
+ maxCoolerInputKW = 10
+ coolerBaseMass = 0.01
+ coolerMassPerKWInput
+ {
+ key = 4 0.120
+ key = 20 0.060
+ key = 40 0.030
+ key = 65 0.025
+ key = 90 0.020
+ key = 150 0.015
+ key = 250 0.010
+ }
+ coolerBaseCost = 150
+ coolerCostPerKWInput
+ {
+ key = 4 9000
+ key = 20 4500
+ key = 40 2250
+ key = 65 1875
+ key = 90 1500
+ key = 150 1125
+ key = 250 750
+ }
+ cryoCoolerEfficiency
+ {
+ key = 0 0
+ key = 4 0.01
+ key = 20 0.08
+ key = 65 0.20
+ key = 90 0.25
+ key = 150 0.40
+ key = 250 0.50
+ }
+ }
+}
+
+// cleanup
+@PART:HAS[#rfAddCryocooler]:AFTER[RealFuels]
+{
+ !rfAddCryocooler = delete
+}
diff --git a/RealFuels/Localization/en-us.cfg b/RealFuels/Localization/en-us.cfg
index edc44872..31fb8a73 100644
--- a/RealFuels/Localization/en-us.cfg
+++ b/RealFuels/Localization/en-us.cfg
@@ -178,10 +178,13 @@ Localization
#RF_FuelTankRF_WallTemp = Wall Temp
#RF_FuelTankRF_HeatPenetration = Heat Penetration
#RF_FuelTankRF_BoiloffLoss = Boil-off Loss
- #RF_FuelTankRF_AnalyticCooling = Analytic Cooling
#RF_FuelTankRF_NoMLI = No MLI
#RF_FuelTankRF_Boiloffunit = kg/hr
#RF_FuelTankRF_kerbalismtips = boiloff product
+ #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input
+ #RF_FuelTankRF_CryoCoolerLift = Cooling Lift
+ #RF_FuelTankRF_CryoCoolerDraw = Power Draw
+ #RF_FuelTankRF_CryoCoolerCOP = Avg COP
#RF_TankDefineSelection_HighlyPressurized = Highly Pressurized
#RF_TankDefineSelection_NotHighlyPressurized = Not Highly Pressurized
diff --git a/RealFuels/Localization/pt-br.cfg b/RealFuels/Localization/pt-br.cfg
index e8f1b1f6..ef34580a 100644
--- a/RealFuels/Localization/pt-br.cfg
+++ b/RealFuels/Localization/pt-br.cfg
@@ -174,10 +174,13 @@ Localization
#RF_FuelTankRF_WallTemp = Temp. da Parede
#RF_FuelTankRF_HeatPenetration = Penetração de Calor
#RF_FuelTankRF_BoiloffLoss = Perda por Evaporação
- #RF_FuelTankRF_AnalyticCooling = Resfriamento Analítico
#RF_FuelTankRF_NoMLI = Sem MLI
#RF_FuelTankRF_Boiloffunit = kg/h
#RF_FuelTankRF_kerbalismtips = produto de evaporação
+ #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input
+ #RF_FuelTankRF_CryoCoolerLift = Cooling Lift
+ #RF_FuelTankRF_CryoCoolerDraw = Power Draw
+ #RF_FuelTankRF_CryoCoolerCOP = Avg COP
#RF_TankDefineSelection_HighlyPressurized = Altamente Pressurizado
#RF_TankDefineSelection_NotHighlyPressurized = Não Altamente Pressurizado
diff --git a/RealFuels/Localization/ru.cfg b/RealFuels/Localization/ru.cfg
index 0aaa0f07..4ab2f291 100644
--- a/RealFuels/Localization/ru.cfg
+++ b/RealFuels/Localization/ru.cfg
@@ -174,10 +174,13 @@ Localization
#RF_FuelTankRF_WallTemp = Температура стенок
#RF_FuelTankRF_HeatPenetration = Проникащее тепло
#RF_FuelTankRF_BoiloffLoss = Потери на испарение
- #RF_FuelTankRF_AnalyticCooling = Аналитическое охлаждение
#RF_FuelTankRF_NoMLI = Без ЭВТИ
#RF_FuelTankRF_Boiloffunit = кг/ч
#RF_FuelTankRF_kerbalismtips = резальтат испарения
+ #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input
+ #RF_FuelTankRF_CryoCoolerLift = Cooling Lift
+ #RF_FuelTankRF_CryoCoolerDraw = Power Draw
+ #RF_FuelTankRF_CryoCoolerCOP = Avg COP
#RF_TankDefineSelection_HighlyPressurized = Бак высокого давления
#RF_TankDefineSelection_NotHighlyPressurized = Бак нормальн. давления
diff --git a/RealFuels/Localization/zh-cn.cfg b/RealFuels/Localization/zh-cn.cfg
index c05e4a4c..13e32b3e 100644
--- a/RealFuels/Localization/zh-cn.cfg
+++ b/RealFuels/Localization/zh-cn.cfg
@@ -174,10 +174,13 @@ Localization
#RF_FuelTankRF_WallTemp = 壁面温度
#RF_FuelTankRF_HeatPenetration = 热渗透
#RF_FuelTankRF_BoiloffLoss = 蒸发损耗
- #RF_FuelTankRF_AnalyticCooling = 冷却分析
#RF_FuelTankRF_NoMLI = 无隔热层
#RF_FuelTankRF_Boiloffunit = 千克/时
#RF_FuelTankRF_kerbalismtips = 蒸发产出
+ #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input
+ #RF_FuelTankRF_CryoCoolerLift = Cooling Lift
+ #RF_FuelTankRF_CryoCoolerDraw = Power Draw
+ #RF_FuelTankRF_CryoCoolerCOP = Avg COP
#RF_TankDefineSelection_HighlyPressurized = 高压
#RF_TankDefineSelection_NotHighlyPressurized = 非高压
diff --git a/RealFuels/MFSSettings.cfg b/RealFuels/MFSSettings.cfg
index 74b112d0..ac865d4f 100644
--- a/RealFuels/MFSSettings.cfg
+++ b/RealFuels/MFSSettings.cfg
@@ -5,9 +5,7 @@ MFSSETTINGS
BatteryMultiplier = 1
basemassUseTotalVolume = True
-
- radiatorMinTempMult = 0.99
-
+
IgnoreFuelsForFill
{
IntakeAir = True
diff --git a/RealFuels/Resources/RealTankTypes.cfg b/RealFuels/Resources/RealTankTypes.cfg
index 41d3b480..6c295480 100644
--- a/RealFuels/Resources/RealTankTypes.cfg
+++ b/RealFuels/Resources/RealTankTypes.cfg
@@ -205,7 +205,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 180.65
- loss_rate = 0.00000000023
note = (lacks insulation)
}
TANK
@@ -226,7 +225,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 184.65
- loss_rate = 0.00000000025
note = (lacks insulation)
}
TANK
@@ -238,7 +236,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 169.45
- loss_rate = 0.00000000035
note = (lacks insulation)
}
TANK
@@ -250,7 +247,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 128.4
- loss_rate = 0.000000001
note = (lacks insulation)
}
TANK
@@ -262,7 +258,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 85.04
- loss_rate = 0.0000000055
note = (lacks insulation)
}
TANK
@@ -274,7 +269,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 200.15
- loss_rate = 0.00000000017
note = (lacks insulation)
}
TANK
@@ -331,7 +325,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -343,7 +336,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -355,7 +347,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -706,7 +697,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 180.65
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -727,7 +717,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 184.65
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -739,7 +728,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 169.45
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -751,7 +739,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 128.4
- loss_rate = 0.000000000016
note = (has insulation)
}
TANK
@@ -763,7 +750,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 85.04
- loss_rate = 0.000000000088
note = (has insulation)
}
TANK
@@ -775,7 +761,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 200.15
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -832,7 +817,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -844,7 +828,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -856,7 +839,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -1250,7 +1232,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 180.65
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -1272,7 +1253,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 184.65
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -1284,7 +1264,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 169.45
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -1296,7 +1275,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 128.4
- loss_rate = 0.000000000016
note = (has insulation, pressurized)
}
TANK
@@ -1308,7 +1286,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 85.04
- loss_rate = 0.000000000088
note = (has insulation, pressurized)
}
TANK
@@ -1320,7 +1297,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 200.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -1382,7 +1358,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -1394,7 +1369,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -1406,7 +1380,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -1927,7 +1900,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 180.65
- loss_rate = 0.00000000023
note = (lacks insulation)
}
TANK
@@ -1948,7 +1920,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 184.65
- loss_rate = 0.00000000025
note = (lacks insulation)
}
TANK
@@ -1960,7 +1931,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 169.45
- loss_rate = 0.00000000035
note = (lacks insulation)
}
TANK
@@ -1972,7 +1942,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 128.4
- loss_rate = 0.000000001
note = (lacks insulation)
}
TANK
@@ -1984,7 +1953,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 85.04
- loss_rate = 0.0000000055
note = (lacks insulation)
}
TANK
@@ -1996,7 +1964,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 200.15
- loss_rate = 0.00000000017
note = (lacks insulation)
}
TANK
@@ -2053,7 +2020,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -2065,7 +2031,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -2077,7 +2042,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -2478,7 +2442,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 180.65
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -2500,7 +2463,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 184.65
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -2512,7 +2474,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 169.45
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -2524,7 +2485,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 128.4
- loss_rate = 0.000000000016
note = (has insulation, pressurized)
}
TANK
@@ -2536,7 +2496,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 85.04
- loss_rate = 0.000000000088
note = (has insulation, pressurized)
isDewar = true
}
@@ -2549,7 +2508,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 200.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -2611,7 +2569,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -2623,7 +2580,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -2635,7 +2591,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation, pressurized)
}
TANK
@@ -3156,7 +3111,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 180.65
- loss_rate = 0.00000000023
note = (lacks insulation)
}
TANK
@@ -3177,7 +3131,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 184.65
- loss_rate = 0.00000000025
note = (lacks insulation)
}
TANK
@@ -3189,7 +3142,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 169.45
- loss_rate = 0.00000000035
note = (lacks insulation)
}
TANK
@@ -3201,7 +3153,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 128.4
- loss_rate = 0.000000001
note = (lacks insulation)
}
TANK
@@ -3213,7 +3164,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 85.04
- loss_rate = 0.0000000055
note = (lacks insulation)
}
TANK
@@ -3225,7 +3175,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 200.15
- loss_rate = 0.00000000017
note = (lacks insulation)
}
TANK
@@ -3282,7 +3231,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -3294,7 +3242,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -3306,7 +3253,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0.0000000001
note = (lacks insulation)
}
TANK
@@ -3657,7 +3603,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 180.65
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -3678,7 +3623,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 184.65
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -3690,7 +3634,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 169.45
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -3702,7 +3645,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 128.4
- loss_rate = 0.000000000016
note = (has insulation)
}
TANK
@@ -3714,7 +3656,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 85.04
- loss_rate = 0.000000000088
note = (has insulation)
}
TANK
@@ -3726,7 +3667,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 200.15
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -3783,7 +3723,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -3795,7 +3734,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation)
}
TANK
@@ -3807,7 +3745,6 @@ TANK_DEFINITION
amount = 0.0
maxAmount = 0.0
temperature = 216.15
- loss_rate = 0
note = (has insulation)
}
TANK
diff --git a/Source/RealFuels.csproj b/Source/RealFuels.csproj
index b3e002cf..4f09880e 100644
--- a/Source/RealFuels.csproj
+++ b/Source/RealFuels.csproj
@@ -112,11 +112,13 @@
+
+
@@ -131,6 +133,7 @@
+
diff --git a/Source/Tanks/BgBoiloffCache.cs b/Source/Tanks/BgBoiloffCache.cs
new file mode 100644
index 00000000..5c2f20b5
--- /dev/null
+++ b/Source/Tanks/BgBoiloffCache.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace RealFuels.Tanks
+{
+ internal sealed class BgBoiloffCache
+ {
+ internal readonly int MliLayers;
+ internal readonly BgTankEntry[] Tanks;
+ internal readonly double[] InternalTemps; // mutable, parallel to Tanks
+ internal readonly double[] FluxScratch; // mutable per-tick scratch for Q_kW pre-pass
+
+ internal readonly double CoolerInputKW; // (0 if no cooler installed)
+ internal readonly double CoolerFrac; // fraction-of-Carnot at CoolerLowestTempK
+ internal readonly double CoolerLowestTempK; // cold-side T of the cooler
+
+ internal BgBoiloffCache(int mliLayers, BgTankEntry[] tanks,
+ double coolerInputKW, double coolerFrac, double coolerLowestTempK)
+ {
+ MliLayers = mliLayers;
+ Tanks = tanks;
+ InternalTemps = new double[tanks.Length];
+ FluxScratch = new double[tanks.Length];
+ CoolerInputKW = coolerInputKW;
+ CoolerFrac = coolerFrac;
+ CoolerLowestTempK = coolerLowestTempK;
+ }
+
+ internal static BgBoiloffCache Build(string data, string coolerData, int mliLayers)
+ {
+ var tanks = new List();
+
+ foreach (string entry in data.Split(';'))
+ {
+ if (string.IsNullOrEmpty(entry)) continue;
+ string[] split = entry.Split(',');
+ if (split.Length != 7) continue;
+
+ string resourceName = split[0];
+ if (!double.TryParse(split[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double boilingPointK)) continue;
+ if (!double.TryParse(split[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double tankAreaM2)) continue;
+ if (!double.TryParse(split[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double conductWPerK)) continue;
+ if (!int.TryParse(split[4], out int isDewarInt)) continue;
+ if (!double.TryParse(split[5], NumberStyles.Float, CultureInfo.InvariantCulture, out double hsp)) continue;
+ if (!double.TryParse(split[6], NumberStyles.Float, CultureInfo.InvariantCulture, out double structThermalMassKJ)) continue;
+
+ PartResourceDefinition resDef = PartResourceLibrary.Instance.GetDefinition(resourceName);
+ if (resDef == null || resDef.density <= 0d) continue;
+ if (!MFSSettings.resourceVsps.TryGetValue(resourceName, out double vsp) || vsp <= 0) continue;
+
+ tanks.Add(new BgTankEntry
+ {
+ Name = resourceName,
+ Vsp = vsp,
+ Density = resDef.density,
+ BoilingPointK = boilingPointK,
+ TankAreaM2 = tankAreaM2,
+ ConductWPerK = conductWPerK,
+ IsDewar = isDewarInt != 0,
+ Hsp = hsp,
+ StructThermalMassKJ = structThermalMassKJ,
+ });
+ }
+
+ double coolerInputKW = 0d, coolerFrac = 0d, coolerLowestTempK = 0d;
+ if (!string.IsNullOrEmpty(coolerData))
+ {
+ string[] cSplit = coolerData.Split(',');
+ if (cSplit.Length == 3
+ && double.TryParse(cSplit[0], NumberStyles.Float, CultureInfo.InvariantCulture, out coolerInputKW)
+ && double.TryParse(cSplit[1], NumberStyles.Float, CultureInfo.InvariantCulture, out coolerFrac)
+ && double.TryParse(cSplit[2], NumberStyles.Float, CultureInfo.InvariantCulture, out coolerLowestTempK))
+ {
+ // ok
+ }
+ else
+ {
+ coolerInputKW = 0d; coolerFrac = 0d; coolerLowestTempK = 0d;
+ }
+ }
+
+ return new BgBoiloffCache(mliLayers, tanks.ToArray(), coolerInputKW, coolerFrac, coolerLowestTempK);
+ }
+
+ internal void InitTemps(ProtoPartModuleSnapshot proto_module)
+ {
+ // Seed InternalTemps from the persisted TANK nodes
+ var tempLookup = new Dictionary(StringComparer.Ordinal);
+ foreach (ConfigNode tankNode in proto_module.moduleValues.GetNodes("TANK"))
+ {
+ string tName = tankNode.GetValue("name");
+ string sVal = tankNode.GetValue("internalTemp");
+ if (tName != null && double.TryParse(sVal, NumberStyles.Float, CultureInfo.InvariantCulture, out double t))
+ tempLookup[tName] = t;
+ }
+
+ for (int i = 0; i < Tanks.Length; i++)
+ {
+ InternalTemps[i] = tempLookup.TryGetValue(Tanks[i].Name, out double v) && v > 0
+ ? v : Tanks[i].BoilingPointK;
+ }
+ }
+ }
+
+ internal struct BgTankEntry
+ {
+ internal string Name;
+ internal double Vsp; // kJ/t
+ internal double Density; // t/unit
+ internal double BoilingPointK;
+ internal double TankAreaM2; // per-tank surface area; used by MLI and Dewar formulas
+ internal double ConductWPerK; // wall conductance for non-MLI tanks; 0 for MLI/Dewar
+ internal bool IsDewar;
+ internal double Hsp; // specific heat capacity, kJ/(t·K)
+ internal double StructThermalMassKJ; // structural thermal mass contribution for this tank, kJ/K
+ }
+}
diff --git a/Source/Tanks/FuelTank.cs b/Source/Tanks/FuelTank.cs
index 3450b31e..ec16fe31 100644
--- a/Source/Tanks/FuelTank.cs
+++ b/Source/Tanks/FuelTank.cs
@@ -16,9 +16,6 @@ namespace RealFuels.Tanks
// of tank installed for this resource type. Tons per
// volume unit.
// temperature the part temperature at which this tank's contents start boiling
- // loss_rate How quickly this resource type bleeds out of the tank.
- // (TODO: instead of this unrealistic static loss_rate, all
- // resources should have vsp (heat of vaporization) added and optionally conduction)
//
public class FuelTank : IConfigNode
@@ -37,11 +34,8 @@ public class FuelTank : IConfigNode
public float mass = 0.0f;
[Persistent]
public float cost = 0.0f;
- // TODO Retaining for fallback purposes but should be deprecated
- [Persistent]
- public double loss_rate = 0.0;
-
- public double vsp;
+ public double hsp = 1000d; // specific heat capacity, kJ/(t·K), loaded from RESOURCE_DEFINITION
+ public double vsp; // heat of vapourization, kJ/t, loaded from RESOURCE_DEFINITION
public double resourceConductivity = 10;
@@ -62,6 +56,9 @@ public class FuelTank : IConfigNode
[Persistent]
public float temperature = 300.0f;
+ [Persistent]
+ public double internalTemp = -1d; // -1 = uninitialized; set on first OnStart
+
[Persistent]
public bool fillable = true;
[Persistent]
@@ -308,7 +305,7 @@ public void Load(ConfigNode node)
if (node.TryGetValue("boiloffProduct", ref boiloffRes))
boiloffProductResource = PartResourceLibrary.Instance.GetDefinition(boiloffRes);
- GetDensity();
+ Init();
}
public void Save(ConfigNode node)
@@ -384,14 +381,17 @@ internal FuelTank CreateCopy(ModuleFuelTanks toModule, ConfigNode overNode, bool
else
clone.amountExpression = clone.maxAmountExpression = null;
- clone.GetDensity();
+ clone.Init();
return clone;
}
- internal void GetDensity()
+ internal void Init()
{
PartResourceDefinition d = PartResourceLibrary.Instance.GetDefinition(name);
density = (d != null) ? d.density : 0;
+
+ if (!MFSSettings.resourceHsps.TryGetValue(name, out hsp) || hsp <= 0)
+ hsp = 1000d;
}
}
}
diff --git a/Source/Tanks/MFSSettings.cs b/Source/Tanks/MFSSettings.cs
index defc4f4c..3bad20c4 100644
--- a/Source/Tanks/MFSSettings.cs
+++ b/Source/Tanks/MFSSettings.cs
@@ -14,7 +14,6 @@ public class MFSSettings
public static bool partUtilizationTweakable = false;
public static string unitLabel = "u";
public static bool basemassUseTotalVolume = false;
- public static double radiatorMinTempMult = 0.99d;
// Move all possible tank upgrades into the preview list in OnStart
// It requires an external mod to be responsible for calling the Validate() method.
@@ -25,6 +24,7 @@ public class MFSSettings
public static readonly Dictionary resourceVsps = new Dictionary();
public static readonly Dictionary resourceConductivities = new Dictionary();
+ public static readonly Dictionary resourceHsps = new Dictionary();
private static readonly Dictionary overrides = new Dictionary();
@@ -72,8 +72,9 @@ public static void ModuleManagerPostLoad()
{
resourceVsps.Clear();
resourceConductivities.Clear();
+ resourceHsps.Clear();
- // fill vsps & conductivities
+ // fill vsps, conductivities & heat capacities
foreach (ConfigNode n in GameDatabase.Instance.GetConfigNodes("RESOURCE_DEFINITION"))
{
string nm = n.GetValue("name");
@@ -82,6 +83,8 @@ public static void ModuleManagerPostLoad()
resourceVsps[nm] = dtmp;
if (n.TryGetValue("conductivity", ref dtmp))
resourceConductivities[nm] = dtmp;
+ if (n.TryGetValue("hsp", ref dtmp))
+ resourceHsps[nm] = dtmp;
}
ConfigNode node = GameDatabase.Instance.GetConfigNodes("MFSSETTINGS").LastOrDefault();
@@ -95,7 +98,6 @@ public static void ModuleManagerPostLoad()
node.TryGetValue("partUtilizationTweakable", ref partUtilizationTweakable);
node.TryGetValue("unitLabel", ref unitLabel);
node.TryGetValue("basemassUseTotalVolume", ref basemassUseTotalVolume);
- node.TryGetValue("radiatorMinTempMult", ref radiatorMinTempMult);
node.TryGetValue("previewAllLockedTypes", ref previewAllLockedTypes);
ignoreFuelsForFill.Clear();
diff --git a/Source/Tanks/ModuleFuelTanksRF.cs b/Source/Tanks/ModuleFuelTanksRF.cs
index ffa9f6c9..a9af9cec 100644
--- a/Source/Tanks/ModuleFuelTanksRF.cs
+++ b/Source/Tanks/ModuleFuelTanksRF.cs
@@ -1,6 +1,9 @@
-using KSP.Localization;
+using KSP.Localization;
+using ROUtils;
using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.Profiling;
@@ -20,7 +23,8 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr
public float _numberOfAddedMLILayers = 0; // This is the number of layers added by the player.
public int numberOfAddedMLILayers => (int)_numberOfAddedMLILayers;
- public int totalMLILayers => numberOfMLILayers + numberOfAddedMLILayers;
+ [KSPField(isPersistant = true)]
+ public int totalMLILayers = 0;
[KSPField(isPersistant = true)]
protected double totalTankArea;
@@ -34,10 +38,24 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr
[KSPField(guiName = "#RF_FuelTankRF_BoiloffLoss", groupName = CryogenicGroupName)] // Boil-off Loss
public string sBoiloffLoss;
- [KSPField(guiName = "#RF_FuelTankRF_AnalyticCooling", groupName = CryogenicGroupName)] // Analytic Cooling
- public string sAnalyticCooling;
+ // Thermal data captured while loaded, used for processing boiloff BackgroundUpdate on unloaded vessels.
+ // Format per entry: "resourceName,boilingPointK,tankAreaM2,conductWPerK,isDewar"
+ // boilingPointK — boiling point of the propellant (K)
+ // tankAreaM2 — per-tank surface area for MLI/Dewar formulas
+ // conductWPerK — wall conductance W/K for non-MLI tanks (0 for MLI and Dewar)
+ // isDewar — 1 for Dewar tanks, 0 otherwise
+ [KSPField(isPersistant = true)]
+ public string bgBoiloffData = "";
- private double cooling = 0;
+ // UT of the last Kerbalism BackgroundUpdate tick; used to avoid double-applying boiloff
+ // when the vessel loads and SetAnalyticTemperature catches up for the unloaded period.
+ [KSPField(isPersistant = true)]
+ public double bgBoiloffLastUpdate = 0d;
+
+ // Cryocooler params captured at save time for unloaded BackgroundUpdate use.
+ // Format: "coolerInputKW,coolerFracAtLowestTemp,lowestTempK"
+ [KSPField(isPersistant = true)]
+ public string bgCoolerData = "";
[KSPField]
public int maxMLILayers = 10;
@@ -48,18 +66,61 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr
[KSPField]
public float MLIArealDensity = 0.000015f;
+ [KSPField]
+ public HermiteCurve cryoCoolerEfficiency = new HermiteCurve();
+
+ [KSPField]
+ public float maxCoolerInputKW = 0f;
+
+ [KSPField]
+ public float coolerBaseMass = 0f; // tonnes, fixed overhead (electronics, housing)
+
+ [KSPField]
+ public HermiteCurve coolerMassPerKWInput = new HermiteCurve(); // tonnes/kW, keyed to T_cold (K)
+
+ [KSPField]
+ public float coolerBaseCost = 0f;
+
+ [KSPField]
+ public HermiteCurve coolerCostPerKWInput = new HermiteCurve(); // funds/kW, keyed to T_cold (K)
+
+ [KSPField(isPersistant = true, guiActiveEditor = true,
+ guiName = "#RF_FuelTankRF_CryoCoolerInputPower", guiUnits = " kW", guiFormat = "F2",
+ groupName = CryogenicGroupName, groupDisplayName = CryogenicGroupName),
+ UI_FloatEdit(minValue = 0f, maxValue = 0f, incrementLarge = 1f, incrementSmall = 0.1f, incrementSlide = 0.01f, sigFigs = 2,
+ unit = " kW", scene = UI_Scene.Editor)]
+ public float coolerInputKW = 0f;
+
+ [KSPField(guiName = "#RF_FuelTankRF_CryoCoolerLift", groupName = CryogenicGroupName)]
+ public string sCoolerLift;
+
+ [KSPField(guiName = "#RF_FuelTankRF_CryoCoolerDraw", groupName = CryogenicGroupName)]
+ public string sCoolerDraw;
+
+ [KSPField(guiName = "#RF_FuelTankRF_CryoCoolerCOP", groupName = CryogenicGroupName)]
+ public string sCoolerCOP; // Coefficient of Performance, ratio of the useful cooling provided to the work (energy) required
+
private double analyticSkinTemp;
- private double analyticInternalTemp;
private readonly Dictionary boiloffProducts = new Dictionary();
public int numberOfMLILayers = 0; // base number of layers taken from TANK_DEFINITION configs
+ private double currentCoolerLiftKW;
+ private double currentCoolerDrawKW;
+ private double currentCoolerCOP;
+
private double boiloffMassT = 0d;
public double BoiloffMassRate => boiloffMassT;
private readonly List cryoTanks = new List(); // anything with maxAmount > 0 && vsp > 0
private readonly List lossInfo = new List();
private readonly List fluxInfo = new List();
+ private readonly Dictionary _perTankFlux = new Dictionary();
+ private readonly Dictionary _perTankLift = new Dictionary();
+
+ // Pre-parsed tank boiloff data for background processing.
+ private static readonly ConditionalWeakTable _bgCache
+ = new ConditionalWeakTable();
// for EngineIgnitor integration: store a public dictionary of all pressurized propellants
[NonSerialized]
@@ -69,8 +130,9 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr
double lowestTankTemperature = 300d;
- private static double ConductionFactors => RFSettings.Instance.globalConductionCompensation ? Math.Max(1.0d, PhysicsGlobals.ConductionFactor) : 1d;
public bool SupportsBoiloff => cryoTanks.Count > 0;
+ public bool SupportCryoCooler => maxCoolerInputKW > 0f;
+ public bool HasCryoCooler => coolerInputKW > 0f;
private bool IsProcedural => part.Modules.Contains("SSTUModularPart") || part.Modules.Contains("WingProcedural");
partial void OnLoadRF(ConfigNode _) { }
@@ -96,6 +158,52 @@ partial void OnAwakeRF()
}
}
+ partial void OnSaveRF(ConfigNode _)
+ {
+ if (!HighLogic.LoadedSceneIsFlight || !SupportsBoiloff)
+ return;
+
+ double structuralThermalMass = ComputeStructuralThermalMass();
+ var entries = new List(cryoTanks.Count);
+ foreach (var tank in cryoTanks)
+ {
+ if (tank.amount <= 0 || tank.vsp <= 0) continue;
+
+ double tankAreaM2 = tank.totalArea;
+ double conductWPerK = 0;
+ int isDewar = tank.isDewar ? 1 : 0;
+
+ if (!tank.isDewar && totalMLILayers == 0)
+ {
+ double wallF = tank.wallConduction > 0 ? tank.wallThickness / tank.wallConduction : 0;
+ double insulF = tank.insulationConduction > 0 ? tank.insulationThickness / tank.insulationConduction : 0;
+ double resF = tank.resourceConductivity > 0 ? 0.01 / tank.resourceConductivity : 0;
+ conductWPerK = tank.totalArea / Math.Max(double.Epsilon, wallF + insulF + resF);
+ }
+
+ entries.Add(string.Format(CultureInfo.InvariantCulture, "{0},{1:R},{2:R},{3:R},{4},{5:R},{6:R}",
+ tank.name, tank.temperature, tankAreaM2, conductWPerK, isDewar,
+ tank.hsp, structuralThermalMass * tank.tankRatio));
+ }
+
+ bgBoiloffData = entries.Count > 0 ? string.Join(";", entries) : "";
+
+ bgCoolerData = "";
+ if (HasCryoCooler)
+ {
+ CalculateLowestTankTemperature();
+ if (lowestTankTemperature > 0d && lowestTankTemperature < 300d)
+ {
+ double frac = cryoCoolerEfficiency.Evaluate(lowestTankTemperature);
+ if (frac > 0d)
+ {
+ bgCoolerData = string.Format(CultureInfo.InvariantCulture, "{0:R},{1:R},{2:R}",
+ coolerInputKW, frac, lowestTankTemperature);
+ }
+ }
+ }
+ }
+
partial void OnStartRF(StartState _)
{
if (HighLogic.LoadedSceneIsFlight)
@@ -103,7 +211,10 @@ partial void OnStartRF(StartState _)
foreach (var tank in tanksDict.Values)
{
- if (tank.maxAmount > 0 && (tank.vsp > 0 || tank.loss_rate > 0))
+ if (tank.internalTemp < 0)
+ tank.internalTemp = tank.vsp > 0 ? tank.temperature : (double.IsNaN(part.temperature) ? 300d : part.temperature);
+
+ if (tank.maxAmount > 0 && tank.vsp > 0)
cryoTanks.Add(tank);
}
CalculateTankArea();
@@ -112,55 +223,47 @@ partial void OnStartRF(StartState _)
{
Fields[nameof(_numberOfAddedMLILayers)].guiActiveEditor = maxMLILayers > 0;
_numberOfAddedMLILayers = Mathf.Clamp(_numberOfAddedMLILayers, 0, maxMLILayers);
+ totalMLILayers = numberOfMLILayers + numberOfAddedMLILayers;
((UI_FloatRange)Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor).maxValue = maxMLILayers;
Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor.onFieldChanged = delegate (BaseField field, object value)
{
+ totalMLILayers = numberOfMLILayers + numberOfAddedMLILayers;
massDirty = true;
CalculateMass();
};
+
+ Fields[nameof(coolerInputKW)].guiActiveEditor = SupportCryoCooler;
+ if (SupportCryoCooler)
+ {
+ coolerInputKW = Mathf.Clamp(coolerInputKW, 0f, maxCoolerInputKW);
+ ((UI_FloatEdit)Fields[nameof(coolerInputKW)].uiControlEditor).maxValue = maxCoolerInputKW;
+ Fields[nameof(coolerInputKW)].uiControlEditor.onFieldChanged = delegate (BaseField field, object value)
+ {
+ massDirty = true;
+ CalculateMass();
+ };
+ }
}
bool debugBoilActive = SupportsBoiloff && (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW);
Fields[nameof(sWallTemp)].guiActive = debugBoilActive;
Fields[nameof(sHeatPenetration)].guiActive = debugBoilActive;
Fields[nameof(sBoiloffLoss)].guiActive = debugBoilActive;
- Fields[nameof(sAnalyticCooling)].guiActive = debugBoilActive;
+
+ bool coolerFlightActive = HighLogic.LoadedSceneIsFlight && HasCryoCooler;
+ Fields[nameof(sCoolerLift)].guiActive = coolerFlightActive;
+ Fields[nameof(sCoolerDraw)].guiActive = coolerFlightActive;
+ Fields[nameof(sCoolerCOP)].guiActive = coolerFlightActive;
GameEvents.onPartResourceListChange.Add(OnPartResourceListChange);
GameEvents.onPartDestroyed.Add(OnPartDestroyed);
}
- private void CalculateInsulation()
- {
- Profiler.BeginSample("CalculateInsulation");
- // TODO tie this into insulation configuration GUI! Also, we should handle MLI separately and as part skin-internal conduction. (DONE)
- // Dewars and SOFI should be handled separately as part of the boiloff code on a per-tank basis (DONE)
- // Current SOFI configuration system should be left in place with players able to add to tanks that don't have it.
- if (totalMLILayers > 0 && totalVolume > 0 && !(double.IsNaN(part.temperature) || double.IsNaN(part.skinTemperature)))
- {
- double normalizationFactor = 1 / (PhysicsGlobals.SkinInternalConductionFactor * PhysicsGlobals.ConductionFactor * PhysicsGlobals.ThermalConvergenceFactor * 10 * 0.5);
- double tDelta = part.skinTemperature - part.temperature;
- if (tDelta == 0d)
- tDelta = 0.00000000001d;
- double insulationFactor = Math.Abs(GetMLITransferRate(part.skinTemperature, part.temperature) / tDelta) * 0.001;
- double condRecip = part.partInfo.partPrefab.skinInternalConductionMult == 0d ? double.MaxValue : (1d / part.partInfo.partPrefab.skinInternalConductionMult);
- part.heatConductivity = normalizationFactor * 1 / ((1 / insulationFactor) + condRecip);
- CalculateAnalyticInsulationFactor(insulationFactor);
- }
- Profiler.EndSample();
- }
-
- private void CalculateAnalyticInsulationFactor(double insulationFactor)
- {
- double tMassRecip = part.thermalMass == 0d ? 1d : 1d / part.thermalMass;
- part.analyticInternalInsulationFactor = _flightIntegrator is FlightIntegrator
- ? (1d / PhysicsGlobals.AnalyticLerpRateInternal) * (insulationFactor * totalTankArea * tMassRecip) * RFSettings.Instance.analyticInsulationMultiplier * part.partInfo.partPrefab.analyticInternalInsulationFactor
- : 0;
- }
-
partial void CalculateMassRF(ref double mass)
{
mass += MLIArealDensity * totalTankArea * totalMLILayers;
+ if (HasCryoCooler)
+ mass += coolerBaseMass + coolerMassPerKWInput.Evaluate(GetCoolerTargetTemp()) * coolerInputKW;
}
partial void GetModuleCostRF(ref double cost)
@@ -168,16 +271,24 @@ partial void GetModuleCostRF(ref double cost)
// Estimate material cost at 0.10764/m2 treating as Fund = $1000 (for RO purposes)
// Plus another 0.1 for installation
cost += MLIArealCost * totalTankArea * totalMLILayers;
+ if (HasCryoCooler)
+ cost += coolerBaseCost + coolerCostPerKWInput.Evaluate(GetCoolerTargetTemp()) * coolerInputKW;
}
partial void UpdateRF()
{
- if (HighLogic.LoadedSceneIsFlight && (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW) && SupportsBoiloff &&
- UIPartActionController.Instance.GetItem(part) != null)
+ if (!HighLogic.LoadedSceneIsFlight) return;
+ if (UIPartActionController.Instance.GetItem(part) == null) return;
+
+ if ((RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW) && SupportsBoiloff)
{
- string MLIText = totalMLILayers > 0 ? $"{GetMLITransferRate(part.skinTemperature, part.temperature):F2} W/m²" : Localizer.GetStringByTag("#RF_FuelTankRF_NoMLI"); // "No MLI"
- sWallTemp = $"{part.temperature:F2} ({MLIText} * {part.radiativeArea:F2} m²)"; //
- sAnalyticCooling = Utilities.FormatFlux(cooling);
+ sWallTemp = "";
+ foreach (var tank in cryoTanks)
+ sWallTemp += $"{tank.internalTemp:F2} | ";
+ if (!string.IsNullOrEmpty(sWallTemp))
+ sWallTemp = sWallTemp.Remove(sWallTemp.Length - 3);
+ string MLIText = totalMLILayers > 0 ? $"{GetMLITransferRate(part.skinTemperature, lowestTankTemperature):F2} W/m²" : Localizer.GetStringByTag("#RF_FuelTankRF_NoMLI"); // "No MLI"
+ sWallTemp += $" ({MLIText} * {part.radiativeArea:F2} m²)";
sHeatPenetration = "";
sBoiloffLoss = "";
@@ -191,43 +302,67 @@ partial void UpdateRF()
if (!string.IsNullOrEmpty(sHeatPenetration))
sHeatPenetration = sHeatPenetration.Remove(sHeatPenetration.Length - 3);
}
+
+ if (HasCryoCooler)
+ {
+ sCoolerLift = Utilities.FormatFlux(currentCoolerLiftKW);
+ sCoolerDraw = Utilities.FormatFlux(currentCoolerDrawKW);
+ sCoolerCOP = currentCoolerCOP > 0d ? currentCoolerCOP.ToString("F3") : "—";
+ }
}
public void FixedUpdate()
{
- //print ("[Real Fuels]" + Time.time.ToString ());
if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready)
{
- // MLI performance varies by temperature delta
- CalculateInsulation();
-
- if(!_flightIntegrator.isAnalytical && SupportsBoiloff)
- CalculateTankBoiloff(_flightIntegrator.timeSinceLastUpdate, _flightIntegrator.isAnalytical);
+ // For analytic case boiloff will run though SetAnalyticTemperature()
+ if (!_flightIntegrator.isAnalytical && SupportsBoiloff)
+ ProcessBoiloff(_flightIntegrator.timeSinceLastUpdate);
}
}
- private void HandleCooling(ref double cooling, double deltaTime, bool analyticalMode)
+ private double GetCoolerTargetTemp()
{
- cooling = 0;
- if (analyticalMode)
+ double lowest = 300d;
+ foreach (FuelTank tank in tanksDict.Values)
{
- if (part.thermalInternalFlux < 0)
- cooling = part.thermalInternalFlux;
- else if (part.thermalInternalFluxPrevious < 0)
- cooling = part.thermalInternalFluxPrevious;
-
- if (cooling < 0)
- {
- // in analytic mode, MFTRF interprets this as an attempt to cool the tanks
- // Questionable since the thermalInternalFlux is already tracking it??
- if (part.thermalMassReciprocal > 0d)
- analyticInternalTemp += cooling * part.thermalMassReciprocal * deltaTime;
- }
+ if (tank.amount > 0 && tank.vsp > 0 && tank.temperature < lowest)
+ lowest = tank.temperature;
}
+
+ return lowest;
}
- private double GetBoiloffTransferRate(double deltaTemp, double wettedArea, in FuelTank tank)
+ ///
+ /// Returns incoming heat flux in W for a single tank from the part skin,
+ /// based on tank type and whether the part has MLI.
+ ///
+ private double GetIncomingFlux(double skinTemp, FuelTank tank)
{
+ if (tank.isDewar)
+ return GetDewarTransferRate(skinTemp, tank.internalTemp, tank.totalArea);
+
+ if (totalMLILayers > 0)
+ return GetMLITransferRate(skinTemp, tank.internalTemp) * tank.totalArea;
+
+ return GetBoiloffTransferRate(skinTemp, tank.internalTemp, tank.totalArea, tank);
+ }
+
+ ///
+ /// Returns the structural thermal mass of the part (kJ/K), i.e. part.thermalMass
+ /// minus the thermal contribution of all resources using KSP's standard specific heat.
+ ///
+ private double ComputeStructuralThermalMass()
+ {
+ double resourceThermalMass = 0;
+ foreach (PartResource res in part.Resources)
+ resourceThermalMass += res.amount * res.info.density * PhysicsGlobals.StandardSpecificHeatCapacity;
+ return Math.Max(0, part.thermalMass - resourceThermalMass);
+ }
+
+ private double GetBoiloffTransferRate(double outerTemperature, double innerTemperature, double wettedArea, in FuelTank tank)
+ {
+ double deltaTemp = outerTemperature - innerTemperature;
double wallFactor = tank.wallConduction > 0 ? tank.wallThickness / tank.wallConduction : 0;
double insulationFactor = tank.insulationConduction > 0 ? tank.insulationThickness / tank.insulationConduction : 0;
double resourceFactor = tank.resourceConductivity > 0 ? 0.01 / tank.resourceConductivity : 0;
@@ -236,7 +371,7 @@ private double GetBoiloffTransferRate(double deltaTemp, double wettedArea, in Fu
return deltaTemp * wettedArea / divisor;
}
- private void CalculateTankBoiloff(double deltaTime, bool analyticalMode = false, double unclampedIntScalar = 0, double unclampedSkinScalar = 0)
+ private void ProcessBoiloff(double deltaTime, bool analyticalMode = false)
{
Profiler.BeginSample("CalculateTankBoiloff");
if (totalTankArea <= 0)
@@ -245,9 +380,11 @@ private void CalculateTankBoiloff(double deltaTime, bool analyticalMode = false,
CalculateTankArea();
}
- if (double.IsNaN(part.temperature))
+ double skinTemp = analyticalMode ? analyticSkinTemp : part.skinTemperature;
+
+ if (double.IsNaN(skinTemp))
{
- Debug.LogError($"RF: CalculateTankBoiloff found NaN part.temperature on {part}");
+ Debug.LogError($"RF: CalculateTankBoiloff found NaN skinTemperature on {part}");
Profiler.EndSample();
return;
}
@@ -255,118 +392,195 @@ private void CalculateTankBoiloff(double deltaTime, bool analyticalMode = false,
boiloffMassT = 0d;
lossInfo.Clear();
fluxInfo.Clear();
+ _perTankFlux.Clear();
+ _perTankLift.Clear();
bool hasCryoFuels = CalculateLowestTankTemperature();
- if (hasCryoFuels && MFSSettings.radiatorMinTempMult >= 0d)
- part.radiatorMax = lowestTankTemperature * MFSSettings.radiatorMinTempMult / part.maxTemp;
if (fueledByLaunchClamp)
{
if (hasCryoFuels)
{
- if (analyticalMode)
- analyticInternalTemp = lowestTankTemperature;
- else
- part.temperature = lowestTankTemperature;
- // part.skinTemperature or analyticSkinTemp ? Nah.
+ foreach (var tank in cryoTanks)
+ tank.internalTemp = tank.temperature;
}
fueledByLaunchClamp = false;
+ currentCoolerLiftKW = 0d;
+ currentCoolerDrawKW = 0d;
+ currentCoolerCOP = 0d;
Profiler.EndSample();
return;
}
- if (deltaTime > 0 && !CheatOptions.InfinitePropellant)
+ if (deltaTime <= 0 || CheatOptions.InfinitePropellant)
{
- //Debug.Log($"internalFlux = {part.thermalInternalFlux}, thermalInternalFluxPrevious = {part.thermalInternalFluxPrevious}, analytic internal flux = {previewInternalFluxAdjust}");
- HandleCooling(ref cooling, deltaTime, analyticalMode);
+ Profiler.EndSample();
+ return;
+ }
- foreach (var tank in cryoTanks)
- {
- double tankAmount = tank.amount;
- if (tankAmount > 0 && tank.vsp > 0)
- {
- double massLost = 0;
- double hotTemp = part.temperature;
+ // TODO: structuralThermalMass should be split up per-tank
+ // TODO2: KSP will internally still assign a part.thermalMass value that includes resources
+ double structuralThermalMass = ComputeStructuralThermalMass();
- // We might be in analytic mode, and have a target temperature = analyticInternalTemp/analyticSkinTemp, and "progress" towards it reprsented by the scalar params
- if (analyticalMode)
- {
- hotTemp = UtilMath.Lerp(part.temperature, analyticInternalTemp, Math.Min(1, unclampedIntScalar / 2));
- DebugLog($"[MFTRF] CalculateBoiloff.Analytic using adjusted temp {hotTemp:F1} from {part.temperature:F1} towards {analyticInternalTemp:F1} based on scalar {unclampedIntScalar:F2}");
- }
+ // Pre-pass: compute per-tank incoming flux and tally what the cryocooler could usefully lift.
+ // Only at-boiling cryo tanks are coolable; sub-boiling tanks are left to warm normally
+ // (no subcooling), and non-cryo tanks aren't cooled.
+ double totalCoolableKW = 0d;
+ foreach (FuelTank tank in tanksDict.Values)
+ {
+ double q = GetIncomingFlux(skinTemp, tank) * 0.001d;
+ _perTankFlux[tank] = q;
+ if (tank.vsp > 0 && tank.amount > 0 && q > 0 && tank.internalTemp >= tank.temperature)
+ totalCoolableKW += q;
+ }
- double deltaTemp = hotTemp - tank.temperature;
- double Q = 0;
- if (deltaTemp > 0)
- {
- double wettedArea = tank.totalArea; // disabled until proper wetted vs ullage conduction can be done (tank.amount / tank.maxAmount);
- Q = tank.isDewar ? GetDewarTransferRate(hotTemp, tank.temperature, tank.totalArea)
- : GetBoiloffTransferRate(deltaTemp, wettedArea, tank);
+ double totalLiftKW = 0d, totalInputKW = 0d;
+ if (hasCryoFuels)
+ ApplyCryocooling(deltaTime, skinTemp, totalCoolableKW, out totalLiftKW, out totalInputKW);
- Q *= 0.001d; // convert to kilowatts
- massLost = Q / tank.vsp;
+ double totalAbsorbedQ_kW = 0d;
+ foreach (FuelTank tank in tanksDict.Values)
+ {
+ double qIn = _perTankFlux.TryGetValue(tank, out double qv) ? qv : 0d;
+ double lift = _perTankLift.TryGetValue(tank, out double lv) ? lv : 0d;
+ totalAbsorbedQ_kW += CalculateBoiloffForTank(tank, qIn - lift, deltaTime, structuralThermalMass);
+ }
- lossInfo.Add(massLost * 1000 * 3600);
- fluxInfo.Add(Q);
- massLost *= deltaTime; // Frame scaling
- }
+ currentCoolerLiftKW = totalLiftKW;
+ currentCoolerDrawKW = totalInputKW;
+ currentCoolerCOP = totalInputKW > 0d ? totalLiftKW / totalInputKW : 0d;
+
+ // Skin energy balance:
+ // skin → tanks: -totalAbsorbedQ_kW (net flux absorbed by tanks, after any cooling)
+ // tanks → skin via cooler: +totalLiftKW (heat pumped out of tanks)
+ // EC → skin via cooler: +totalInputKW (electrical work turns into heat in the warm end)
+ // AddSkinThermalFlux takes kW and multiplies by TimeWarp.fixedDeltaTime internally,
+ // so passing the rate is correct here. Skipped in analytic mode.
+ if (!analyticalMode)
+ {
+ double skinFlux = -totalAbsorbedQ_kW + totalLiftKW + totalInputKW;
+ if (skinFlux != 0d)
+ part.AddSkinThermalFlux(skinFlux);
+ }
- double d = tank.density > 0 ? tank.density : 1;
- double lossAmount = massLost / d;
- lossAmount = Math.Min(lossAmount, tankAmount);
+ Profiler.EndSample();
+ }
- if (lossAmount > 0)
+ private void ApplyCryocooling(double deltaTime, double skinTemp, double totalCoolableKW, out double totalLiftKW, out double totalInputKW)
+ {
+ // Cryocooler allocation: COP = (T_cold / (T_hot - T_cold)) * fraction-of-Carnot curve
+ totalLiftKW = 0d;
+ totalInputKW = 0d;
+ if (HasCryoCooler && totalCoolableKW > 0d && skinTemp > lowestTankTemperature)
+ {
+ double carnot = lowestTankTemperature / (skinTemp - lowestTankTemperature);
+ double frac = cryoCoolerEfficiency.Evaluate(lowestTankTemperature);
+ double cop = Math.Max(0d, carnot * frac);
+ if (cop > 0d)
+ {
+ double maxLiftKW = coolerInputKW * cop;
+ double wantedLiftKW = Math.Min(maxLiftKW, totalCoolableKW);
+ double wantedInputKW = wantedLiftKW / cop;
+ double requestedEC = wantedInputKW * deltaTime;
+ double receivedEC = requestedEC > 0d
+ ? part.RequestResource("ElectricCharge", requestedEC, ResourceFlowMode.ALL_VESSEL)
+ : 0d;
+
+ double scale = requestedEC > 0d ? receivedEC / requestedEC : 0d;
+ if (scale < 0d) scale = 0d;
+ else if (scale > 1d) scale = 1d;
+
+ totalLiftKW = wantedLiftKW * scale;
+ totalInputKW = wantedInputKW * scale;
+
+ if (totalLiftKW > 0d)
+ {
+ double liftFrac = totalLiftKW / totalCoolableKW;
+ foreach (FuelTank tank in cryoTanks)
{
- // operate directly with the PartResource because FuelTank.amount isn't really meant for in-flight resource consumption
- tank.resource.amount -= lossAmount;
-
- // See if there is boiloff byproduct and see if any other parts want to accept it.
- if (tank.boiloffProductResource != null)
+ if (tank.amount > 0 && tank.vsp > 0
+ && tank.internalTemp >= tank.temperature
+ && _perTankFlux.TryGetValue(tank, out double q) && q > 0)
{
- double boiloffProductAmount = -(massLost / tank.boiloffProductResource.density);
- double retainedAmount = part.RequestResource(tank.boiloffProductResource.id, boiloffProductAmount, ResourceFlowMode.STAGE_PRIORITY_FLOW, Utilities.KerbalismFound);
- massLost -= retainedAmount * tank.boiloffProductResource.density;
-
- if (Utilities.KerbalismFound)
- {
- string rName = tank.boiloffProductResource.name;
- retainedAmount /= deltaTime;
- boiloffProducts[rName] = boiloffProducts.TryGetValue(rName, out double v) ? v + retainedAmount : retainedAmount;
- }
+ _perTankLift[tank] = q * liftFrac;
}
}
+ }
+ }
+ }
+ }
- boiloffMassT += massLost;
+ private double CalculateBoiloffForTank(FuelTank tank, double Q_kW, double deltaTime, double structuralThermalMass)
+ {
+ if (tank.totalArea <= 0 || tank.tankRatio <= 0)
+ return 0d;
- // subtract heat from boiloff
- // subtracting heat in analytic mode is tricky: Analytic flux handling is 'cheaty' and tricky to predict.
+ double thermalMass = structuralThermalMass * tank.tankRatio
+ + tank.amount * tank.density * tank.hsp;
+ thermalMass = Math.Max(thermalMass, 1.0);
- if (Q > 0)
- {
- double heatLost = -Q;
- if (!analyticalMode)
- part.AddThermalFlux(heatLost);
- else
- {
- analyticInternalTemp += heatLost * part.thermalMassReciprocal * deltaTime;
- DebugLog($"{part.name} deltaTime = {deltaTime:F2}s, heat lost = {heatLost:F4}, thermalMassReciprocal = {part.thermalMassReciprocal:F6}");
- }
- }
- }
- else if (tankAmount > 0 && tank.loss_rate > 0)
+ if (tank.vsp <= 0 || tank.internalTemp < tank.temperature)
+ {
+ if (tank.vsp <= 0)
+ {
+ // Non-cryo: clamp internalTemp at skinTemp to prevent overshoot.
+ // At high warp the large deltaTime can push internalTemp past skinTemp in a
+ // single step; the resulting oscillation with asymmetric flux handling bleeds
+ // energy from the skin each cycle, eventually driving it to 0 K (or infinity).
+ double skinTemp = part.skinTemperature;
+ double prevTemp = tank.internalTemp;
+ double newTemp = prevTemp + Q_kW * deltaTime / thermalMass;
+ newTemp = Q_kW >= 0 ? Math.Min(newTemp, skinTemp) : Math.Max(newTemp, skinTemp);
+ tank.internalTemp = newTemp;
+ return (newTemp - prevTemp) * thermalMass / deltaTime;
+ }
+ else
+ {
+ // Sub-boiling cryo: heat toward boiling point
+ tank.internalTemp += Q_kW * deltaTime / thermalMass;
+ if (tank.internalTemp > tank.temperature)
+ tank.internalTemp = tank.temperature;
+ return Q_kW > 0 ? Q_kW : 0d;
+ }
+ }
+ else
+ {
+ // At or above boiling point: phase transition holds temperature, all flux → boiloff
+ tank.internalTemp = tank.temperature;
+
+ double tankAmount = tank.amount;
+ if (tankAmount <= 0 || Q_kW <= 0) return 0d;
+
+ double massLost = Q_kW / tank.vsp * deltaTime;
+
+ lossInfo.Add(Q_kW / tank.vsp * 1000d * 3600d); // kg/hr for display
+ fluxInfo.Add(Q_kW);
+
+ double d = tank.density > 0 ? tank.density : 1;
+ double lossAmount = Math.Min(massLost / d, tankAmount);
+
+ if (lossAmount > 0)
+ {
+ tank.resource.amount -= lossAmount;
+
+ if (tank.boiloffProductResource != null)
{
- double deltaTemp = part.temperature - tank.temperature;
- if (deltaTemp > 0)
+ double boiloffProductAmount = -(massLost / tank.boiloffProductResource.density);
+ double retainedAmount = part.RequestResource(tank.boiloffProductResource.id, boiloffProductAmount, ResourceFlowMode.STAGE_PRIORITY_FLOW, Utilities.KerbalismFound);
+ massLost -= retainedAmount * tank.boiloffProductResource.density;
+
+ if (Utilities.KerbalismFound)
{
- double lossAmount = tank.maxAmount * tank.loss_rate * deltaTemp * deltaTime;
- lossAmount = Math.Min(lossAmount, tankAmount);
- tank.resource.amount -= lossAmount;
- boiloffMassT += lossAmount * tank.density;
+ string rName = tank.boiloffProductResource.name;
+ retainedAmount /= deltaTime;
+ boiloffProducts[rName] = boiloffProducts.TryGetValue(rName, out double v) ? v + retainedAmount : retainedAmount;
}
}
}
+
+ boiloffMassT += massLost;
+ return Q_kW;
}
- Profiler.EndSample();
}
partial void UpdateTankTypeRF(TankDefinition def)
@@ -385,6 +599,7 @@ partial void UpdateTankTypeRF(TankDefinition def)
_numberOfAddedMLILayers = Mathf.Clamp(_numberOfAddedMLILayers, 0, maxMLILayers);
((UI_FloatRange)Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor).maxValue = maxMLILayers;
}
+ totalMLILayers = numberOfMLILayers + numberOfAddedMLILayers;
InitUtilization();
@@ -407,11 +622,10 @@ private void UpdateEngineIgnitor(TankDefinition def)
public void OnResourceMaxChanged(BaseEventDetails _) => CalculateTankArea();
private void OnPartResourceListChange(Part p)
{
- if (p == part)
+ if (p == part)
CalculateTankArea();
}
- // This is how you update drag cubes, we shouldn't be the service for this, but left-over code.
private void UpdateDragCubes()
{
DragCube dragCube = DragCubeSystem.Instance.RenderProceduralDragCube(part);
@@ -423,15 +637,8 @@ private void UpdateDragCubes()
public void CalculateTankArea()
{
- // TODO: Codify a more accurate tank area calculator.
- // Thought: cube YN/YP can be used to find the part diameter / circumference... X or Z finds the length
- // Also should try to determine if tank has a common bulkhead - and adjust heat flux into individual tanks accordingly
SetTankAreaInfo(volume);
- // This allows a rough guess as to individual tank surface area based on ratio of tank volume to total volume but it breaks down at very small fractions
- // So use greater of spherical calculation and tank ratio of total area.
- // if for any reason our totalTankArea is still 0 (no drag cubes available yet or analytic temp routines executed first)
- // then we're going to be defaulting to spherical calculation
double areaSpherical = SphericalAreaFromVolume(totalVolume);
double areaPartsSpherical = CalculateTankAreaFromSphericalSubTanks();
@@ -464,30 +671,35 @@ private bool CalculateLowestTankTemperature()
}
#region IAnalyticTemperatureModifier
- // Analytic Interface
public void SetAnalyticTemperature(FlightIntegrator fi, double analyticTemp, double predictedInternalTemp, double predictedSkinTemp)
{
analyticSkinTemp = predictedSkinTemp;
- analyticInternalTemp = predictedInternalTemp;
if (SupportsBoiloff)
{
- DebugLog($"{part.name} Analytic Temp = {analyticTemp:F2}, Analytic Internal = {predictedInternalTemp:F2}, Analytic Skin = {predictedSkinTemp:F2}");
- double lerpScalarInt = PhysicsGlobals.AnalyticLerpRateInternal * fi.timeSinceLastUpdate;
- double lerpScalarSkin = PhysicsGlobals.AnalyticLerpRateSkin * fi.timeSinceLastUpdate;
- double skinScalar = lerpScalarSkin * part.analyticSkinInsulationFactor;
- double intScalar = lerpScalarInt * part.analyticInternalInsulationFactor;
- // A value of 1.0 (unclamped) indicates the time that has passed == the expected time to equalize temperatures
- // For values <= 1-ish, we may consider trying to scale the temp progress down by accounting for boiloff.
- // Alternatively, just adjust the analytic output using the boiloff calculation anyway.
+ DebugLog($"{part.name} Analytic Temp = {analyticTemp:F2}, Analytic Skin = {predictedSkinTemp:F2}");
if (fi.timeSinceLastUpdate < double.MaxValue)
- CalculateTankBoiloff(fi.timeSinceLastUpdate, fi.isAnalytical, intScalar, skinScalar);
+ {
+ double remainingTime;
+ if (bgBoiloffLastUpdate > 0d)
+ {
+ remainingTime = Math.Max(0d, Planetarium.GetUniversalTime() - bgBoiloffLastUpdate);
+ bgBoiloffLastUpdate = 0d;
+ }
+ else
+ {
+ remainingTime = fi.timeSinceLastUpdate;
+ }
+
+ if (remainingTime > 0d)
+ ProcessBoiloff(remainingTime, fi.isAnalytical);
+ }
else if (CalculateLowestTankTemperature())
{
- // Vessel is freshly spawned and has cryogenic tanks, set temperatures appropriately
- analyticSkinTemp = lowestTankTemperature;
- analyticInternalTemp = lowestTankTemperature;
+ // Vessel is freshly spawned with cryogenic tanks — initialise per-tank state only
+ foreach (var tank in cryoTanks)
+ tank.internalTemp = tank.temperature;
}
}
}
@@ -498,42 +710,21 @@ public double GetSkinTemperature(out bool lerp)
return Math.Max(analyticSkinTemp, PhysicsGlobals.SpaceTemperature);
}
+ // Return skin temp as the internal temp: the part structure has no meaningful insulation
+ // from the skin surface, so it equilibrates with it. Propellant thermal state is managed
+ // independently via per-tank internalTemp and must not inflate part.thermalMass here.
public double GetInternalTemperature(out bool lerp)
{
lerp = false;
- return Math.Max(analyticInternalTemp, PhysicsGlobals.SpaceTemperature);
+ return Math.Max(analyticSkinTemp, PhysicsGlobals.SpaceTemperature);
}
#endregion
#region Analytic Preview Interface
-
- // We don't really implement this interface anymore.
- // Boiloff should not be a significant portion of the flux generation that it will actively keep
- // the vessel cool and will change the steady-state temperature that is being calculated/previewed here.
- // Diff-Eq problem: this calculates the steady-state temp, of which boiloff result is an input.
- // However, boiloff as a resource can be consumed, so the amount of time to target this steady state changes.
- //
- // Normally called every FixedUpdate by FlightIntegrator in Analytic Mode
- // May be called outside of Analytic Mode if part.temp/part.skinTemp were out of bounds
public void AnalyticInfo(FlightIntegrator fi, double sunAndBodyIn, double backgroundRadiation, double radArea, double absEmissRatio, double internalFlux, double convCoeff, double ambientTemp, double maxPartTemp)
{
if (!fi.isAnalytical)
Debug.Log($"[MFTRF] AnalyticInfo called in non-analytic mode for {vessel}. dT: {fi.timeSinceLastUpdate:F2}s");
- /*
- if (TimeWarp.CurrentRate == 1)
- DebugLog("AnalyticInfo being called with: sunAndBodyIn = " + sunAndBodyIn.ToString()
- + ", backgroundRadiation = " + backgroundRadiation.ToString()
- + ", radArea = "+ radArea.ToString()
- + ", absEmissRatio = " + absEmissRatio.ToString()
- + ", internalFlux = " + internalFlux.ToString()
- + ", convCoeff = " + convCoeff.ToString()
- + ", ambientTemp = " + ambientTemp.ToString()
- + ", maxPartTemp = " + maxPartTemp.ToString()
- );
- //float deltaTime = (float)(Planetarium.GetUniversalTime() - vessel.lastUT);
- //if (this.supportsBoiloff)
- // CalculateTankBoiloff(TimeWarp.fixedDeltaTime, true);
- */
}
public double InternalFluxAdjust() => 0;
@@ -551,28 +742,30 @@ void DebugLog(string msg)
///
/// Transfer rate through multilayer insulation in watts/m2 via radiation, conduction and convection (conduction through gas in the layers).
- /// Default hot and cold values of 300 / 70. Can be called in real time substituting skin temp and internal temp for hot and cold.
+ /// Can be called in real time substituting skin temp and internal temp for hot and cold.
///
- private double GetMLITransferRate(double outerTemperature = 300, double innerTemperature = 70)
- {
- //
- double QrCoefficient = 0.0000000004944; // typical MLI radiation flux coefficient
- double QcCoefficient = 0.0000000895; // typical MLI conductive flux coefficient. Possible tech upgrade target based on spacing mechanism between layers?
- //double QvCoefficient = 3.65; // 14.600; // 14600; // not even sure how this is right: convective contribution will be MURDEROUS.
- double emissivity = 0.03; // typical reflective mylar emissivity...?
- double layerDensity = 10.055; //14.99813f; // 8.51f; // layer density (layers/cm)
+ private double GetMLITransferRate(double outerTemperature, double innerTemperature)
+ => GetMLITransferRate(outerTemperature, innerTemperature, totalMLILayers, vessel.staticPressurekPa);
- double radiation = (QrCoefficient * emissivity * (Math.Pow(outerTemperature, 4.67) - Math.Pow(innerTemperature, 4.67))) / totalMLILayers;
- double conduction = ((QcCoefficient * Math.Pow(layerDensity, 2.63) * ((outerTemperature + innerTemperature) / 2)) / (totalMLILayers + 1)) * (outerTemperature - innerTemperature);
- double convection = RFSettings.Instance.QvCoefficient * ((vessel.staticPressurekPa * 7.500616851) * (Math.Pow(outerTemperature, 0.52) - Math.Pow(innerTemperature, 0.52))) / totalMLILayers;
- return radiation + conduction + convection;
+ private static double GetMLITransferRate(double outerTemp, double innerTemp, int mliLayers, double pressureKPa = 0)
+ {
+ const double QrCoefficient = 0.0000000004944; // typical MLI radiation flux coefficient
+ const double QcCoefficient = 0.0000000895; // typical MLI conductive flux coefficient
+ const double emissivity = 0.03; // typical reflective mylar emissivity
+ const double layerDensity = 10.055; // layer density (layers/cm)
+
+ double radiation = QrCoefficient * emissivity * (Math.Pow(outerTemp, 4.67) - Math.Pow(innerTemp, 4.67)) / mliLayers;
+ double conduction = QcCoefficient * Math.Pow(layerDensity, 2.63) * ((outerTemp + innerTemp) / 2) / (mliLayers + 1) * (outerTemp - innerTemp);
+ double result = radiation + conduction;
+ if (pressureKPa > 0)
+ result += RFSettings.Instance.QvCoefficient * (pressureKPa * 7.500616851) * (Math.Pow(outerTemp, 0.52) - Math.Pow(innerTemp, 0.52)) / mliLayers;
+ return result;
}
///
- /// Transfer rate through Dewar walls
- /// This is simplified down to basic radiation formula using corrected emissivity values for concentric walls for sake of performance
+ /// Transfer rate through Dewar walls via radiation across the vacuum gap.
///
- private double GetDewarTransferRate(double hot, double cold, double area)
+ private static double GetDewarTransferRate(double hot, double cold, double area)
{
// TODO Just radiation now; need to calculate conduction through piping/lid, etc
double emissivity = 0.005074871897; // corrected and rounded value for concentric surfaces, actual emissivity of each surface is assumed to be 0.01 for silvered or aluminized coating
@@ -582,13 +775,168 @@ private double GetDewarTransferRate(double hot, double cold, double area)
#endregion
#region Kerbalism
+
///
- /// Called by Kerbalism every frame. Uses their resource system when Kerbalism is installed.
+ /// Called by Kerbalism for unloaded (background) vessels via reflection.
+ /// For each cryo propellant, computes boiloff directly from the Kerbalism vessel temperature
+ /// using the appropriate heat-transfer formula (MLI, conduction, or Dewar radiation).
///
- public virtual string ResourceUpdate(Dictionary availableResources, List> resourceChangeRequest)
+ public static string BackgroundUpdate(
+ Vessel vessel,
+ ProtoPartSnapshot proto_part,
+ ProtoPartModuleSnapshot proto_module,
+ PartModule partModule,
+ Part part,
+ Dictionary availableResources,
+ List> resourceChangeRequest,
+ double elapsed_s)
{
- //resourceChangeRequest.Clear();
+ bool hasGeometry = KerbalismInterface.TryGetThermalData(vessel, out double vesselTemp, out _);
+ if (!hasGeometry) return string.Empty;
+
+ if (!_bgCache.TryGetValue(proto_module, out BgBoiloffCache cache))
+ {
+ string data = proto_module.moduleValues.GetValue(nameof(bgBoiloffData));
+ if (string.IsNullOrEmpty(data)) return string.Empty;
+
+ string coolerData = proto_module.moduleValues.GetValue(nameof(bgCoolerData)) ?? "";
+ int.TryParse(proto_module.moduleValues.GetValue(nameof(totalMLILayers)), out int mliLayers);
+ cache = BgBoiloffCache.Build(data, coolerData, mliLayers);
+ cache.InitTemps(proto_module);
+
+ _bgCache.Remove(proto_module);
+ _bgCache.Add(proto_module, cache);
+ }
+
+ double totalCoolableKW = ProcessBackgroundHeatLeakage(vesselTemp, cache);
+ double liftFrac = ProcessBackgroundCryocooling(availableResources, resourceChangeRequest, elapsed_s, vesselTemp, cache, totalCoolableKW);
+ bool anyBoiloff = ApplyBackgroundTankFlux(availableResources, resourceChangeRequest, elapsed_s, cache, liftFrac);
+ PersistBackgroundTankTemps(proto_module, cache);
+
+ proto_module.moduleValues.SetValue("bgBoiloffLastUpdate",
+ Planetarium.GetUniversalTime().ToString(CultureInfo.InvariantCulture));
+
+ return anyBoiloff ? Localizer.GetStringByTag("#RF_FuelTankRF_kerbalismtips") : string.Empty;
+ }
+
+ ///
+ /// compute per-tank Q_kW and tally coolable demand
+ ///
+ ///
+ ///
+ ///
+ private static double ProcessBackgroundHeatLeakage(double vesselTemp, BgBoiloffCache cache)
+ {
+ double totalCoolableKW = 0d;
+ for (int i = 0; i < cache.Tanks.Length; i++)
+ {
+ BgTankEntry entry = cache.Tanks[i];
+ double internalTemp = cache.InternalTemps[i];
+ double q;
+ if (entry.IsDewar)
+ q = GetDewarTransferRate(vesselTemp, internalTemp, entry.TankAreaM2) * 0.001;
+ else if (cache.MliLayers > 0 && entry.TankAreaM2 > 0)
+ q = GetMLITransferRate(vesselTemp, internalTemp, cache.MliLayers) * entry.TankAreaM2 * 0.001;
+ else if (entry.ConductWPerK > 0)
+ q = entry.ConductWPerK * (vesselTemp - internalTemp) * 0.001;
+ else
+ q = 0d;
+
+ cache.FluxScratch[i] = q;
+ if (q > 0 && internalTemp >= entry.BoilingPointK)
+ totalCoolableKW += q;
+ }
+
+ return totalCoolableKW;
+ }
+
+ private static double ProcessBackgroundCryocooling(Dictionary availableResources, List> resourceChangeRequest, double elapsed_s, double vesselTemp, BgBoiloffCache cache, double totalCoolableKW)
+ {
+ double liftFrac = 0d;
+ if (cache.CoolerInputKW > 0d && cache.CoolerFrac > 0d && totalCoolableKW > 0d
+ && vesselTemp > cache.CoolerLowestTempK)
+ {
+ double carnot = cache.CoolerLowestTempK / (vesselTemp - cache.CoolerLowestTempK);
+ double cop = carnot * cache.CoolerFrac;
+ if (cop > 0d)
+ {
+ double maxLiftKW = cache.CoolerInputKW * cop;
+ double wantedLiftKW = Math.Min(maxLiftKW, totalCoolableKW);
+ double wantedInputKW = wantedLiftKW / cop;
+ double ecAvail = availableResources.TryGetValue("ElectricCharge", out double ev) ? ev : 0d;
+ double requestedEC = wantedInputKW * elapsed_s;
+ double scale = requestedEC > 0d ? Math.Min(1d, ecAvail / requestedEC) : 0d;
+ if (scale < 0d) scale = 0d;
+ double actualInputKW = wantedInputKW * scale;
+ double actualLiftKW = wantedLiftKW * scale;
+ if (actualInputKW > 0d)
+ resourceChangeRequest.Add(new KeyValuePair("ElectricCharge", -actualInputKW));
+ if (actualLiftKW > 0d)
+ liftFrac = actualLiftKW / totalCoolableKW;
+ }
+ }
+
+ return liftFrac;
+ }
+
+ private static bool ApplyBackgroundTankFlux(Dictionary availableResources, List> resourceChangeRequest, double elapsed_s, BgBoiloffCache cache, double liftFrac)
+ {
+ bool anyRequest = false;
+ for (int i = 0; i < cache.Tanks.Length; i++)
+ {
+ BgTankEntry entry = cache.Tanks[i];
+ double internalTemp = cache.InternalTemps[i];
+ double Q_kW = cache.FluxScratch[i];
+
+ if (Q_kW > 0d && internalTemp >= entry.BoilingPointK && liftFrac > 0d)
+ Q_kW -= Q_kW * liftFrac;
+
+ if (Q_kW == 0d) continue;
+
+ if (internalTemp < entry.BoilingPointK)
+ {
+ // Sub-boiling: advance internalTemp through thermal mass
+ double amount = availableResources.TryGetValue(entry.Name, out double a) ? a : 0d;
+ double thermalMass = entry.StructThermalMassKJ + amount * entry.Density * entry.Hsp;
+ thermalMass = Math.Max(thermalMass, 1.0);
+ cache.InternalTemps[i] = Math.Min(internalTemp + Q_kW * elapsed_s / thermalMass, entry.BoilingPointK);
+ }
+ else if (Q_kW > 0)
+ {
+ // At boiling point: residual flux drives boiloff
+ double rateKgS = Q_kW / entry.Vsp;
+ resourceChangeRequest.Add(new KeyValuePair(entry.Name, -rateKgS / entry.Density));
+ anyRequest = true;
+ }
+ }
+
+ return anyRequest;
+ }
+
+ private static void PersistBackgroundTankTemps(ProtoPartModuleSnapshot proto_module, BgBoiloffCache cache)
+ {
+ foreach (ConfigNode tankNode in proto_module.moduleValues.GetNodes("TANK"))
+ {
+ string tName = tankNode.GetValue("name");
+ if (tName == null) continue;
+ for (int i = 0; i < cache.Tanks.Length; i++)
+ {
+ if (cache.Tanks[i].Name == tName)
+ {
+ var sTemp = cache.InternalTemps[i].ToString("R", CultureInfo.InvariantCulture);
+ tankNode.SetValue("internalTemp", sTemp, true);
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Called by Kerbalism every frame for loaded vessels. Uses their resource system when Kerbalism is installed.
+ ///
+ public virtual string ResourceUpdate(Dictionary availableResources, List> resourceChangeRequest)
+ {
foreach (var resourceRequest in boiloffProducts)
{
var definition = PartResourceLibrary.Instance.GetDefinition(resourceRequest.Key);
@@ -601,6 +949,7 @@ public virtual string ResourceUpdate(Dictionary availableResourc
return Localizer.GetStringByTag("#RF_FuelTankRF_kerbalismtips"); // "boiloff product"
}
+
#endregion
#region Tank Dimensions
diff --git a/Source/Utilities/KerbalismInterface.cs b/Source/Utilities/KerbalismInterface.cs
new file mode 100644
index 00000000..a2f8d1f3
--- /dev/null
+++ b/Source/Utilities/KerbalismInterface.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Reflection;
+using UnityEngine;
+
+namespace RealFuels
+{
+ public static class KerbalismInterface
+ {
+ private static bool _initialized;
+ private static Func _getVesselData;
+ private static Func