From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:05:10 +0000 (-0800) Subject: Atmospherics/Temperature HeatContainers (#39997) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=8313a4e3105d69a496261cddcffcc6894a053bbe;p=space-station-14.git Atmospherics/Temperature HeatContainers (#39997) * Initial HeatContainer logic * comment fixes * Comment changes + ChangeHeatCapacity * highly intelligent specimen * n-body full heat exchange methods * extract to partials * highly intelligent specimen * fixes + ChangeHeatCapacityKeepTemperature * Divide and merge methods * even divide * different merge signature * forgot one little thing * address review * missing docs * addr review * oops * review --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainer.cs b/Content.Shared/Temperature/HeatContainer/HeatContainer.cs new file mode 100644 index 0000000000..c88d11b15a --- /dev/null +++ b/Content.Shared/Temperature/HeatContainer/HeatContainer.cs @@ -0,0 +1,66 @@ +using Content.Shared.Atmos; +using Content.Shared.Atmos.EntitySystems; +using Robust.Shared.Serialization; + +namespace Content.Shared.Temperature.HeatContainer; + +/// +/// A general-purpose container for heat energy. +/// Any object that contains, stores, or transfers heat should use a +/// instead of implementing its own system. +/// This allows for consistent heat transfer mechanics across different objects and systems. +/// +[Serializable, NetSerializable, DataDefinition] +[Access(typeof(HeatContainerHelpers), typeof(SharedAtmosphereSystem))] +public partial struct HeatContainer : IRobustCloneable +{ + /// + /// The heat capacity of this container in Joules per Kelvin. + /// This determines how much energy is required to change the temperature of the container. + /// Higher values mean the container can absorb or release more heat energy + /// without a significant change in temperature. + /// + [DataField] + public float HeatCapacity = 4000f; // about 1kg of water + + /// + /// The current temperature of the container in Kelvin. + /// + [DataField] + public float Temperature = Atmospherics.T20C; // room temperature + + /// + /// The current temperature of the container in Celsius. + /// Ideal if you just need to read the temperature for UI. + /// Do not perform computations in Celsius/set this value, use Kelvin instead. + /// + [ViewVariables] + public float TemperatureC => TemperatureHelpers.KelvinToCelsius(Temperature); + + /// + /// The current thermal energy of the container in Joules. + /// + [ViewVariables] + public float InternalEnergy => Temperature * HeatCapacity; + + public HeatContainer(float heatCapacity, float temperature) + { + HeatCapacity = heatCapacity; + Temperature = temperature; + } + + /// + /// Copy constructor for implementing ICloneable. + /// + /// The HeatContainer to copy. + private HeatContainer(HeatContainer c) + { + HeatCapacity = c.HeatCapacity; + Temperature = c.Temperature; + } + + public HeatContainer Clone() + { + return new HeatContainer(this); + } +} diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Conduct.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Conduct.cs new file mode 100644 index 0000000000..deb705b8e2 --- /dev/null +++ b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Conduct.cs @@ -0,0 +1,155 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Temperature.HeatContainer; + +public static partial class HeatContainerHelpers +{ + /// + /// Conducts heat between a and some body with a different temperature, + /// given some constant thermal conductance g and a small time delta. + /// + /// The to conduct heat to. + /// The temperature of the second object that we are conducting heat with, in kelvin. + /// + /// The amount of time that the heat is allowed to conduct, in seconds. + /// This value should be small such that deltaTime << C / g where C is the heat capacity of the container. + /// If you need to simulate a larger time step split it into several smaller ones. + /// + /// The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies. + /// The amount of heat in joules that was added to the heat container. + /// A positive value indicates heat transfer from a hot body to a cold heat container c. + /// + /// This performs a single step using the Euler method for solving the Fourier heat equation + /// \frac{dQ}{dt} = g \Delta T. + /// If we need more precision in the future consider using a higher order integration scheme. + /// If we need support for larger time steps in the future consider adding a method to split the time delta into several + /// integration steps with adaptive step size. + /// + [PublicAPI] + public static float ConductHeat(this HeatContainer c, float temp, float deltaTime, float g) + { + var dQ = c.ConductHeatQuery(temp, deltaTime, g); + c.AddHeat(dQ); + return dQ; + } + + /// + /// Conducts heat between two s, + /// given some constant thermal conductance g and a small time delta. + /// + /// The first to conduct heat to. + /// The second to conduct heat to. + /// + /// The amount of time that the heat is allowed to conduct, in seconds. + /// This value should be small such that deltaTime << C / g where C is the heat capacity of the containers. + /// If you need to simulate a larger time step split it into several smaller ones. + /// + /// The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies. + /// The amount of heat in joules that is exchanged between the bodies. + /// A positive value indicates heat transfer from a hot cB to a cold cA. + /// + /// This performs a single step using the Euler method for solving the Fourier heat equation + /// \frac{dQ}{dt} = g \Delta T. + /// If we need more precision in the future consider using a higher order integration scheme. + /// If we need support for larger time steps in the future consider adding a method to split the time delta into several + /// integration steps with adaptive step size. + /// + [PublicAPI] + public static float ConductHeat(this HeatContainer cA, HeatContainer cB, float deltaTime, float g) + { + var dQ = ConductHeatQuery(cA, cB.Temperature, deltaTime, g); + cA.AddHeat(dQ); + cB.AddHeat(-dQ); + return dQ; + } + + /// + /// Calculates the amount of heat that would be conducted between a and some body with a different temperature, + /// given some constant thermal conductance g and a small time delta. + /// + /// The to conduct heat to. + /// The temperature of the second object that we are conducting heat with, in kelvin. + /// + /// The amount of time that the heat is allowed to conduct, in seconds. + /// This value should be small such that deltaTime << C / g where C is the heat capacity of the container. + /// If you need to simulate a larger time step split it into several smaller ones. + /// + /// The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies. + /// The amount of heat in joules that would be exchanged between the bodies. + /// A positive value indicates heat transfer from a hot body to a cold heat container c. + /// + /// This performs a single step using the Euler method for solving the Fourier heat equation + /// \frac{dQ}{dt} = g \Delta T. + /// If we need more precision in the future consider using a higher order integration scheme. + /// If we need support for larger time steps in the future consider adding a method to split the time delta into several + /// integration steps with adaptive step size. + /// + [PublicAPI] + public static float ConductHeatQuery(this HeatContainer c, float temp, float deltaTime, float g) + { + var dQ = g * (temp - c.Temperature) * deltaTime; + var dQMax = Math.Abs(ConductHeatToTempQuery(c, temp)); + + // Clamp the transferred heat amount in case we are overshooting the equilibrium temperature because our time step was too large. + return Math.Clamp(dQ, -dQMax, dQMax); + } + + /// + /// Calculates the amount of heat that would be conducted between two s, + /// given some conductivity constant k and a time delta. Does not modify the containers. + /// + /// The first to conduct heat to. + /// The second to conduct heat to. + /// + /// The amount of time that the heat is allowed to conduct, in seconds. + /// This value should be small such that deltaTime << C / g where C is the heat capacity of the container. + /// If you need to simulate a larger time step split it into several smaller ones. + /// + /// The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies. + /// The amount of heat in joules that would be exchanged between the bodies. + /// A positive value indicates heat transfer from a hot c2 to a cold c1. + /// + /// This performs a single step using the Euler method for solving the Fourier heat equation + /// \frac{dQ}{dt} = g \Delta T. + /// If we need more precision in the future consider using a higher order integration scheme. + /// If we need support for larger time steps in the future consider adding a method to split the time delta into several + /// integration steps with adaptive step size. + /// + [PublicAPI] + public static float ConductHeatQuery(this HeatContainer c1, HeatContainer c2, float deltaTime, float g) + { + return ConductHeatQuery(c1, c2.Temperature, deltaTime, g); + } + + /// + /// Changes the temperature of a to a target temperature by + /// adding or removing the necessary amount of heat. + /// + /// The to change the temperature of. + /// The desired temperature to reach. + /// The amount of heat in joules that was transferred to or from the + /// to reach the target temperature. + /// A positive value indicates heat must be added to the container to reach the target temperature. + [PublicAPI] + public static float ConductHeatToTemp(this HeatContainer c, float targetTemp) + { + var dQ = ConductHeatToTempQuery(c, targetTemp); + c.Temperature = targetTemp; + return dQ; + } + + /// + /// Determines the amount of heat that must be transferred to or from a + /// to reach a target temperature. Does not modify the heat container. + /// + /// The to query. + /// The desired temperature to reach. + /// The amount of heat in joules that must be transferred to or from the + /// to reach the target temperature. + /// A positive value indicates heat must be added to the container to reach the target temperature. + [PublicAPI] + public static float ConductHeatToTempQuery(this HeatContainer c, float targetTemp) + { + return (targetTemp - c.Temperature) * c.HeatCapacity; + } +} diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Divide.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Divide.cs new file mode 100644 index 0000000000..f1e7206683 --- /dev/null +++ b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Divide.cs @@ -0,0 +1,55 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Temperature.HeatContainer; + +public static partial class HeatContainerHelpers +{ + /// + /// Splits a into two. + /// + /// The to split. This will be modified to contain the remaining heat capacity. + /// The fraction of the heat capacity to move to the new container. Clamped between 0 and 1. + /// A new containing the specified fraction of the original container's heat capacity and the same temperature. + [PublicAPI] + public static HeatContainer Split(this ref HeatContainer c, float fraction = 0.5f) + { + fraction = Math.Clamp(fraction, 0f, 1f); + var newHeatCapacity = c.HeatCapacity * fraction; + + var newContainer = new HeatContainer + { + HeatCapacity = newHeatCapacity, + Temperature = c.Temperature, + }; + + c.HeatCapacity -= newHeatCapacity; + + return newContainer; + } + + /// + /// Divides a source into a specified number of equal parts. + /// + /// The input to split. + /// The number of s + /// to split the source into. + /// Thrown when attempting to divide the source container by zero. + /// An array of s equally split from the source . + [PublicAPI] + public static HeatContainer[] Divide(this HeatContainer c, uint num) + { + if (num == 0) + throw new ArgumentException("Cannot divide by zero.", nameof(num)); + + var fraction = 1f / num; + var cFrac = c.Split(fraction); + var containers = new HeatContainer[num]; + + for (var i = 0; i < num; i++) + { + containers[i] = cFrac; + } + + return containers; + } +} diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Exchange.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Exchange.cs new file mode 100644 index 0000000000..6b7eb125dc --- /dev/null +++ b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Exchange.cs @@ -0,0 +1,214 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Temperature.HeatContainer; + +public static partial class HeatContainerHelpers +{ + #region 2-Body Exchange + + /// + /// Determines the amount of heat energy that must be transferred between two heat containers + /// to bring them into thermal equilibrium. + /// Does not modify the containers. + /// + /// The first to exchange heat. + /// The second to exchange heat with. + /// The amount of heat in joules that is needed + /// to bring the containers to thermal equilibrium. + /// A positive value indicates heat transfer from a hot cA to a cold cB. + [PublicAPI] + public static float EquilibriumHeatQuery(this HeatContainer cA, HeatContainer cB) + { + /* + The solution is derived from the following facts: + 1. Let Q be the amount of heat energy transferred from cA to cB. + 2. T_A > T_B, so heat will flow from cA to cB. + 3. The energy lost by T_A is equal to Q = C_A * (T_A_initial - T_A_final) + 4. The energy gained by T_B is equal to Q = C_B * (T_B_final - T_B_initial) + 5. Energy is conserved. So T_A_final and T_B_final can be expressed as: + T_A_final = T_A_initial - Q / C_A + T_B_final = T_B_initial + Q / C_B + 6. At thermal equilibrium, T_A_final = T_B_final. + 7. Solve for Q. + */ + return (cA.Temperature - cB.Temperature) * + (cA.HeatCapacity * cB.HeatCapacity / (cA.HeatCapacity + cB.HeatCapacity)); + } + + /// + /// Determines the resulting temperature if two heat containers are brought into thermal equilibrium. + /// Does not modify the containers. + /// + /// The first to exchange heat. + /// The second to exchange heat with. + /// The resulting equilibrium temperature both containers will be at. + [PublicAPI] + public static float EquilibriumTemperatureQuery(this HeatContainer cA, HeatContainer cB) + { + // Insert the above solution for Q into T_A_final = T_A_initial - Q / C_A and rearrange the result. + return (cA.HeatCapacity * cA.Temperature - cB.HeatCapacity * cB.Temperature) / (cA.HeatCapacity + cB.HeatCapacity); + } + + /// + /// Brings two s into thermal equilibrium by exchanging heat. + /// + /// The first to exchange heat. + /// The second to exchange heat with. + [PublicAPI] + public static void Equilibrate(this HeatContainer cA, HeatContainer cB) + { + var tFinal = EquilibriumTemperatureQuery(cA, cB); + cA.Temperature = tFinal; + cB.Temperature = tFinal; + } + + /// + /// Brings two s into thermal equilibrium by exchanging heat. + /// + /// The first to exchange heat. + /// The second to exchange heat with. + /// The amount of heat in joules that was transferred from container A to B. + [PublicAPI] + public static void Equilibrate(this HeatContainer cA, HeatContainer cB, out float dQ) + { + var tInitialA = cA.Temperature; + var tFinal = EquilibriumTemperatureQuery(cA, cB); + cA.Temperature = tFinal; + cB.Temperature = tFinal; + dQ = (tInitialA - tFinal) / cA.HeatCapacity; + } + + #endregion + + #region N-Body Exchange + + /// + /// Brings an array of s into thermal equilibrium by exchanging heat. + /// + /// The array of s to bring into thermal equilibrium. + [PublicAPI] + public static void Equilibrate(this HeatContainer[] cN) + { + var tF = cN.EquilibriumTemperatureQuery(); + for (var i = 0; i < cN.Length; i++) + { + cN[i].Temperature = tF; + } + } + + /// + /// Brings a into thermal equilibrium + /// with an array of other s by exchanging heat. + /// + /// The first to bring into thermal equilibrium. + /// The array of s to bring into thermal equilibrium. + [PublicAPI] + public static void Equilibrate(this HeatContainer cA, HeatContainer[] cN) + { + var tF = cA.EquilibriumTemperatureQuery(cN); + + cA.Temperature = tF; + for (var i = 0; i < cN.Length; i++) + { + cN[i].Temperature = tF; + } + } + + /// + /// Determines the final temperature of an array of s + /// when they are brought into thermal equilibrium. Does not modify the containers. + /// + /// The array of s to bring into thermal equilibrium. + /// The temperature of all s involved after reaching thermal equilibrium. + [PublicAPI] + public static float EquilibriumTemperatureQuery(this HeatContainer[] cN) + { + /* + The solution is derived via the following: + + 1. In thermal equilibrium all bodies have the same temperature T_f. + + 2. Heat exchange for each body is defined by the equation \Delta Q_n = C_n \Delta T_n = C_n (T_f - T_n) + where C_n is the heat capacity and \Delta T_n the change in temperature of the n-th body. + + 3. Heat energy must be conserved, so the sum of all heat changes must equal zero. + Therefore, \sum_{n=1}^{N} Q_n = 0. + + 4. Substitute and expand. + \sum_{n=1}^{N} C_n (T_f - T_n) = 0. + + 5. Unroll and expand. + C_1(T_f - T_1) + C_2(T_f - T_2) + ... + C_n(T_f - T_n) = 0 + C_1 T_f - C_1 T_1 + C_2 T_f - C_2 T_2 + ... + C_n T_f - C_n T_n = 0 + + 6. Group like terms. + T_f(C_1 + C_2 + ... + C_n) - (C_1 T_1 + C_2 T_2 + ... + C_n T_n) = 0 + + 7. Solve. + T_f(C_1 + C_2 + ... + C_n) = (C_1 T_1 + C_2 T_2 + ... + C_n T_n) + T_f = \frac{C_1 T_1 + C_2 T_2 + ... + C_n T_n}{C_1 + C_2 + ... + C_n} + + 8. Summation. + T_f = \frac{\sum(C_n T_n)}{\sum(C_n)} + */ + + var numerator = 0f; + var denominator = 0f; + + foreach (var c in cN) + { + numerator += c.HeatCapacity * c.Temperature; + denominator += c.HeatCapacity; + } + + return numerator / denominator; + } + + /// + /// Determines the final temperature of an array of s + /// when they are brought into thermal equilibrium. Does not modify the containers. + /// + /// The array of s to bring into thermal equilibrium. + /// The amount of heat in joules that was added to each container + /// to reach thermal equilibrium. + /// The temperature of all s involved after reaching thermal equilibrium. + [PublicAPI] + public static float EquilibriumTemperatureQuery(this HeatContainer[] cN, out float[] dQ) + { + /* + For finding the total heat exchanged during the equalization between a group of bodies + take the difference of the internal energy before and after the exchange. + + dQ = C * (T_f - T_i) for each container + */ + + var tF = cN.EquilibriumTemperatureQuery(); + dQ = new float[cN.Length]; + + for (var i = 0; i < cN.Length; i++) + { + dQ[i] = cN[i].HeatCapacity * (tF - cN[i].Temperature); + } + + return tF; + } + + /// + /// Determines the final temperature of a when it is brought into thermal equilibrium + /// with an array of other s. Does not modify the containers. + /// + /// The first to bring into thermal equilibrium. + /// The array of s to bring into thermal equilibrium. + /// The temperature of all s involved after reaching thermal equilibrium. + [PublicAPI] + public static float EquilibriumTemperatureQuery(this HeatContainer cA, HeatContainer[] cN) + { + var cAll = new HeatContainer[cN.Length + 1]; + cAll[0] = cA; + cN.CopyTo(cAll, 1); + + return cAll.EquilibriumTemperatureQuery(); + } + + #endregion +} diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Merge.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Merge.cs new file mode 100644 index 0000000000..d9f2cdd3e7 --- /dev/null +++ b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Merge.cs @@ -0,0 +1,66 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Temperature.HeatContainer; + +public static partial class HeatContainerHelpers +{ + /// + /// Merges two heat containers into one, conserving total internal energy. + /// + /// The first to merge. This will be modified to contain the merged result. + /// The second to merge. + [PublicAPI] + public static void Merge(this ref HeatContainer cA, HeatContainer cB) + { + var merged = new HeatContainer + { + HeatCapacity = cA.HeatCapacity + cB.HeatCapacity, + Temperature = (cA.InternalEnergy + cB.InternalEnergy) / (cA.HeatCapacity + cB.HeatCapacity) + }; + + cA = merged; + } + + + /// + /// Merges an array of s into a single heat container, conserving total internal energy. + /// + /// The first to merge. + /// This will be modified to contain the merged result. + /// The array of s to merge. + [PublicAPI] + public static void Merge(this ref HeatContainer cA, HeatContainer[] cN) + { + var cAcN = new HeatContainer[cN.Length + 1]; + cAcN[0] = cA; + cN.CopyTo(cAcN, 1); + + cA = cAcN.Merge(); + } + + /// + /// Merges an array of s into a single heat container, conserving total internal energy. + /// + /// The array of s to merge. + /// A new representing the merged result. + [PublicAPI] + public static HeatContainer Merge(this HeatContainer[] cN) + { + var totalHeatCapacity = 0f; + var totalEnergy = 0f; + + foreach (var c in cN) + { + totalHeatCapacity += c.HeatCapacity; + totalEnergy += c.InternalEnergy; + } + + var result = new HeatContainer + { + HeatCapacity = totalHeatCapacity, + Temperature = totalEnergy / totalHeatCapacity, + }; + + return result; + } +} diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.cs new file mode 100644 index 0000000000..39078a810e --- /dev/null +++ b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.cs @@ -0,0 +1,52 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Temperature.HeatContainer; + +/// +/// Class containing helper methods for working with s. +/// Use these classes instead of implementing your own heat transfer logic. +/// +public static partial class HeatContainerHelpers +{ + /// + /// Adds or removes heat energy from the container. + /// Positive values add heat, negative values remove heat. + /// The temperature can never become lower than 0K even if more heat is removed. + /// + /// The to add or remove energy. + /// The energy in joules to add or remove. + [PublicAPI] + public static void AddHeat(this HeatContainer c, float dQ) + { + c.Temperature = c.AddHeatQuery(dQ); + } + + /// + /// Calculates the resulting temperature of the container after adding or removing heat energy. + /// Positive values add heat, negative values remove heat. This method doesn't change the container's state. + /// The temperature can never become lower than 0K even if more heat is removed. + /// + /// The to query. + /// The energy in joules to add or remove. + /// The resulting temperature in kelvin after the heat change. + [PublicAPI] + public static float AddHeatQuery(this HeatContainer c, float dQ) + { + // Don't allow the temperature to go below the absolute minimum. + return Math.Max(0f, c.Temperature + dQ / c.HeatCapacity); + } + + /// + /// Sets the heat capacity of a without altering its thermal energy. + /// Adjusts the temperature accordingly to maintain the same internal energy. + /// + /// The to modify. + /// The new heat capacity to set. + [PublicAPI] + public static void SetHeatCapacity(this HeatContainer c, float newHeatCapacity) + { + var currentEnergy = c.InternalEnergy; + c.HeatCapacity = newHeatCapacity; + c.Temperature = currentEnergy / c.HeatCapacity; + } +}