HeatContainer tests, bugfixes and API cleanup (#43690)

tests and fixes
This commit is contained in:
slarticodefast
2026-04-24 05:54:57 +02:00
committed by GitHub
parent ebe5d47cc6
commit 5a7540edb2
7 changed files with 755 additions and 136 deletions
@@ -9,7 +9,7 @@ namespace Content.Shared.Temperature.HeatContainer;
/// </summary>
[Serializable, NetSerializable, DataDefinition]
[Access(typeof(HeatContainerHelpers), typeof(SharedAtmosphereSystem))]
public partial struct HeatContainer : IRobustCloneable<HeatContainer>, IHeatContainer
public partial struct HeatContainer : IHeatContainer
{
/// <inheritdoc/>
[DataField]
@@ -29,22 +29,15 @@ public partial struct HeatContainer : IRobustCloneable<HeatContainer>, IHeatCont
public HeatContainer(float heatCapacity, float temperature)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(heatCapacity);
ArgumentOutOfRangeException.ThrowIfNegative(temperature);
HeatCapacity = heatCapacity;
Temperature = temperature;
}
/// <summary>
/// Copy constructor for implementing ICloneable.
/// </summary>
/// <param name="c">The HeatContainer to copy.</param>
private HeatContainer(HeatContainer c)
public HeatContainer(float heatCapacity)
{
HeatCapacity = c.HeatCapacity;
Temperature = c.Temperature;
}
public HeatContainer Clone()
{
return new HeatContainer(this);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(heatCapacity);
HeatCapacity = heatCapacity;
}
}
@@ -1,70 +0,0 @@
using JetBrains.Annotations;
namespace Content.Shared.Temperature.HeatContainer;
public static partial class HeatContainerHelpers
{
/// <summary>
/// Splits a <see cref="IHeatContainer"/> into two.
/// </summary>
/// <param name="c">The <see cref="IHeatContainer"/> to split. This will be modified to contain the remaining heat capacity.</param>
/// <param name="cSplit">A <see cref="IHeatContainer"/> that will be modified to contain
/// the specified fraction of the original container's heat capacity and the same temperature.</param>
/// <param name="fraction">The fraction of the heat capacity to move to the new container. Clamped between 0 and 1.</param>
[PublicAPI]
public static void Split<T1, T2>(ref T1 c, ref T2 cSplit, float fraction = 0.5f)
where T1 : IHeatContainer
where T2 : IHeatContainer
{
fraction = Math.Clamp(fraction, 0f, 1f);
var newHeatCapacity = c.HeatCapacity * fraction;
cSplit.HeatCapacity = newHeatCapacity;
cSplit.Temperature = c.Temperature;
c.HeatCapacity -= newHeatCapacity;
}
/// <summary>
/// Splits a <see cref="IHeatContainer"/> into two,
/// modifying the original container to contain the specified fraction of the original heat capacity and the same temperature.
/// </summary>
/// <param name="c">A <see cref="IHeatContainer"/> that will be modified to contain
/// the specified fraction of the original container's heat capacity and the same temperature.</param>
/// <param name="fraction">The fraction of the heat capacity to move to the new container. Clamped between 0 and 1.</param>
/// <remarks>This discards the leftover fraction. Be very careful with using this as you may void heat unintentionally.</remarks>
[PublicAPI]
public static void Split<T>(ref T c, float fraction = 0.5f)
where T : IHeatContainer
{
fraction = Math.Clamp(fraction, 0f, 1f);
var newHeatCapacity = c.HeatCapacity * fraction;
c.HeatCapacity = newHeatCapacity;
}
/// <summary>
/// Divides a source <see cref="IHeatContainer"/> into a specified number of equal parts.
/// </summary>
/// <param name="c">The input <see cref="IHeatContainer"/> to split.</param>
/// <param name="dividedArray">An array of <see cref="IHeatContainer"/>s equally split from the source <see cref="IHeatContainer"/>.
/// This will be written to. This must be the same length as num.</param>
/// <param name="num">The number of <see cref="IHeatContainer"/>s
/// to split the source <see cref="IHeatContainer"/> into.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when attempting to divide the source container by zero.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of the divided array does not match the specified number of divisions.</exception>
[PublicAPI]
public static void Divide<T>(this T c, T[] dividedArray, int num)
where T : struct, IHeatContainer // if we allowed classes you'd just have an array reffing the same obj
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(num);
ArgumentOutOfRangeException.ThrowIfNotEqual(dividedArray.Length, num);
var fraction = 1f / num;
Split(ref c, fraction);
for (var i = 0; i < num; i++)
{
dividedArray[i] = c;
}
}
}
@@ -16,15 +16,11 @@ public static partial class HeatContainerHelpers
/// <returns>The amount of transferred heat in joules that is needed
/// to bring the containers to thermal equilibrium.</returns>
/// <example>A positive value indicates heat transfer from a hot cA to a cold cB.</example>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the combined heat capacity of both containers is zero or negative.</exception>
[PublicAPI]
public static float EquilibriumHeatQuery<T1, T2>(ref T1 cA, ref T2 cB)
where T1 : IHeatContainer
where T2 : IHeatContainer
{
var cTotal = cA.HeatCapacity + cB.HeatCapacity;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(cTotal);
/*
The solution is derived from the following facts:
1. Let Q be the amount of heat energy transferred from cA to cB.
@@ -37,6 +33,7 @@ public static partial class HeatContainerHelpers
6. At thermal equilibrium, T_A_final = T_B_final.
7. Solve for Q.
*/
var cTotal = cA.HeatCapacity + cB.HeatCapacity;
return (cA.Temperature - cB.Temperature) *
(cA.HeatCapacity * cB.HeatCapacity / cTotal);
}
@@ -48,14 +45,12 @@ public static partial class HeatContainerHelpers
/// <param name="cA">The first <see cref="IHeatContainer"/> to exchange heat.</param>
/// <param name="cB">The second <see cref="IHeatContainer"/> to exchange heat with.</param>
/// <returns>The resulting equilibrium temperature both containers will be at.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the combined heat capacity of both containers is zero or negative.</exception>
[PublicAPI]
public static float EquilibriumTemperatureQuery<T1, T2>(ref T1 cA, ref T2 cB)
where T1 : IHeatContainer
where T2 : IHeatContainer
{
var cTotal = cA.HeatCapacity + cB.HeatCapacity;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(cTotal);
// 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) / cTotal;
}
@@ -91,7 +86,7 @@ public static partial class HeatContainerHelpers
cA.Temperature = tFinal;
cB.Temperature = tFinal;
// Guarded against div/0 in EquilibriumTemperatureQuery: totalHeatCapacity > 0.
dQ = (tInitialA - tFinal) / cA.HeatCapacity;
dQ = (tInitialA - tFinal) * cA.HeatCapacity;
}
#endregion
@@ -103,7 +98,7 @@ public static partial class HeatContainerHelpers
/// </summary>
/// <param name="cN">The array of <see cref="IHeatContainer"/>s to bring into thermal equilibrium.</param>
[PublicAPI]
public static void Equilibrate<T>(this T[] cN) where T : IHeatContainer
public static void Equilibrate<T>(T[] cN) where T : IHeatContainer
{
var tF = EquilibriumTemperatureQuery(cN);
for (var i = 0; i < cN.Length; i++)
@@ -140,7 +135,7 @@ public static partial class HeatContainerHelpers
/// <returns>The temperature of all <see cref="IHeatContainer"/>s involved after reaching thermal equilibrium.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the combined heat capacity of all containers is zero or negative.</exception>
[PublicAPI]
public static float EquilibriumTemperatureQuery<T>(this T[] cN) where T : IHeatContainer
public static float EquilibriumTemperatureQuery<T>(T[] cN) where T : IHeatContainer
{
/*
The solution is derived via the following:
@@ -194,7 +189,7 @@ public static partial class HeatContainerHelpers
/// to reach thermal equilibrium.</param>
/// <returns>The temperature of all <see cref="IHeatContainer"/>s involved after reaching thermal equilibrium.</returns>
[PublicAPI]
public static float EquilibriumTemperatureQuery<T>(this T[] cN, out float[] dQ) where T : IHeatContainer
public static float EquilibriumTemperatureQuery<T>(T[] cN, out float[] dQ) where T : IHeatContainer
{
/*
For finding the total heat exchanged during the equalization between a group of bodies
@@ -203,7 +198,7 @@ public static partial class HeatContainerHelpers
dQ = C * (T_f - T_i) for each container
*/
var tF = cN.EquilibriumTemperatureQuery();
var tF = EquilibriumTemperatureQuery(cN);
dQ = new float[cN.Length];
for (var i = 0; i < cN.Length; i++)
@@ -230,7 +225,7 @@ public static partial class HeatContainerHelpers
cAll[0] = cA;
cN.CopyTo(cAll, 1);
return cAll.EquilibriumTemperatureQuery();
return EquilibriumTemperatureQuery(cAll);
}
#endregion
@@ -5,64 +5,65 @@ namespace Content.Shared.Temperature.HeatContainer;
public static partial class HeatContainerHelpers
{
/// <summary>
/// Merges two heat containers into one, conserving total internal energy.
/// Merges one heat container into another.
/// </summary>
/// <param name="cA">The first <see cref="IHeatContainer"/> to merge. This will be modified to contain the merged result.</param>
/// <param name="cB">The second <see cref="IHeatContainer"/> to merge.</param>
/// <param name="cB">The second <see cref="IHeatContainer"/> to merge. This will remain unmodified.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the combined heat capacity of both containers is zero or negative.</exception>
[PublicAPI]
public static void Merge<T1, T2>(ref T1 cA, ref T2 cB)
public static void MergeInto<T1, T2>(ref T1 cA, ref T2 cB)
where T1 : IHeatContainer
where T2 : IHeatContainer
{
var combinedHeatCapacity = cA.HeatCapacity + cB.HeatCapacity;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(combinedHeatCapacity);
cA.HeatCapacity = combinedHeatCapacity;
cA.Temperature = (cA.InternalEnergy + cB.InternalEnergy) / combinedHeatCapacity;
cA.HeatCapacity = combinedHeatCapacity;
}
/// <summary>
/// Merges an array of <see cref="IHeatContainer"/>s into a single heat container, conserving total internal energy.
/// Merges an array of <see cref="IHeatContainer"/>s into a single heat container.
/// This means you combine N+1 containers into 1.
/// </summary>
/// <param name="cA">The first <see cref="IHeatContainer"/> to merge.
/// This will be modified to contain the merged result.</param>
/// <param name="cN">The array of <see cref="IHeatContainer"/>s to merge.</param>
/// <param name="cA">The first <see cref="IHeatContainer"/> to merge. This will be modified to contain the merged result.</param>
/// <param name="cN">The array of <see cref="IHeatContainer"/>s to merge. These will remain unmodified.</param>
[PublicAPI]
public static void Merge<T1, T2>(ref T1 cA, T2[] cN)
public static void MergeInto<T1, T2>(ref T1 cA, T2[] cN)
where T1 : IHeatContainer
where T2 : IHeatContainer
{
// merge the first array and then merge the result with cA to avoid alloc
var temp = new HeatContainer();
cN.Merge(ref temp);
Merge(ref cA, ref temp);
var totalEnergy = cA.InternalEnergy;
var totalHeatCapacity = cA.HeatCapacity;
for (var i = 0; i < cN.Length; i++)
{
totalEnergy += cN[i].InternalEnergy;
totalHeatCapacity += cN[i].HeatCapacity;
}
cA.Temperature = totalEnergy / totalHeatCapacity;
cA.HeatCapacity = totalHeatCapacity;
}
/// <summary>
/// Merges an array of <see cref="IHeatContainer"/>s into a single heat container, conserving total internal energy.
/// Merges an array of <see cref="IHeatContainer"/>s into a single new output heat container.
/// This means you combine N containers into 1.
/// </summary>
/// <param name="cN">The array of <see cref="IHeatContainer"/>s to merge.</param>
/// <param name="result">The modified <see cref="IHeatContainer"/> containing the merged result.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the combined heat capacity of all containers is zero or negative.</exception>
/// <param name="cA">The <see cref="IHeatContainer"/> to write the result to.</param>
/// <param name="cN">The array of <see cref="IHeatContainer"/>s to merge. These will remain unmodified.</param>
[PublicAPI]
public static void Merge<T1, T2>(this T1[] cN, ref T2 result)
public static void MergeAndCopy<T1, T2>(ref T1 cA, T2[] cN)
where T1 : IHeatContainer
where T2 : IHeatContainer
{
var totalHeatCapacity = 0f;
var totalEnergy = 0f;
foreach (var c in cN)
var totalHeatCapacity = 0f;
for (var i = 0; i < cN.Length; i++)
{
totalHeatCapacity += c.HeatCapacity;
totalEnergy += c.InternalEnergy;
totalEnergy += cN[i].InternalEnergy;
totalHeatCapacity += cN[i].HeatCapacity;
}
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalHeatCapacity);
result.HeatCapacity = totalHeatCapacity;
result.Temperature = totalEnergy / totalHeatCapacity;
cA.Temperature = totalEnergy / totalHeatCapacity;
cA.HeatCapacity = totalHeatCapacity;
}
}
@@ -0,0 +1,90 @@
using JetBrains.Annotations;
namespace Content.Shared.Temperature.HeatContainer;
public static partial class HeatContainerHelpers
{
/// <summary>
/// Splits a <see cref="IHeatContainer"/> into two, modifying the original container
/// to contain the remaining fraction of the original heat capacity and the same temperature.
/// </summary>
/// <param name="c">The <see cref="IHeatContainer"/> to split. This will be modified to contain the remaining heat capacity.</param>
/// <param name="cSplit">A <see cref="IHeatContainer"/> that will be modified to contain
/// the specified fraction of the original container's heat capacity and the same temperature. Any previous value will be overwritten.</param>
/// <param name="fraction">The fraction of the heat capacity to move to the new container. Clamped between 0 and 1.</param>
[PublicAPI]
public static void SplitFrom<T1, T2>(ref T1 c, ref T2 cSplit, float fraction = 0.5f)
where T1 : IHeatContainer
where T2 : IHeatContainer
{
fraction = Math.Clamp(fraction, 0f, 1f);
var newHeatCapacity = c.HeatCapacity * fraction;
cSplit.HeatCapacity = newHeatCapacity;
cSplit.Temperature = c.Temperature;
c.HeatCapacity -= newHeatCapacity;
}
/// <summary>
/// Splits a <see cref="IHeatContainer"/> into two, modifying the original container
/// to contain the remaining fraction of the original heat capacity and the same temperature,
/// while discarding the rest.
/// </summary>
/// <param name="c">The <see cref="IHeatContainer"/> to split off. This will be modified to contain the remaining heat capacity.</param>
/// <param name="fraction">The fraction of the heat capacity to remove from the original container. Clamped between 0 and 1.</param>
/// <remarks>This discards the leftover fraction. Be very careful with using this as you may void heat unintentionally.</remarks>
[PublicAPI]
public static void SplitFrom<T>(ref T c, float fraction = 0.5f)
where T : IHeatContainer
{
fraction = Math.Clamp(fraction, 0f, 1f);
var newHeatCapacity = c.HeatCapacity * fraction;
c.HeatCapacity -= newHeatCapacity;
}
/// <summary>
/// Splits a source <see cref="IHeatContainer"/> into a specified number of equal parts.
/// This means you will get N + 1 equal parts where N is the length of the given array.
/// </summary>
/// <param name="c">The input <see cref="IHeatContainer"/> to split. It will be modified such that it is equal to each entry in <paramref name="dividedArray"/>.</param>
/// <param name="dividedArray">An array of <see cref="IHeatContainer"/>s equally split from the source.<see cref="IHeatContainer"/>.
/// This will be written to.</param>
[PublicAPI]
public static void SplitFrom<T1, T2>(ref T1 c, T2[] dividedArray)
where T1 : IHeatContainer
where T2 : struct, IHeatContainer // if we allowed classes you'd just have an array reffing the same obj
{
var num = dividedArray.Length + 1;
for (var i = 0; i < dividedArray.Length; i++)
{
dividedArray[i].Temperature = c.Temperature;
dividedArray[i].HeatCapacity = c.HeatCapacity / num;
}
c.HeatCapacity /= num;
}
/// <summary>
/// Splits a source <see cref="IHeatContainer"/> into a specified number of equal parts, keeping the source unmodified.
/// This means you will get N equal parts where N is the length of the given array.
/// </summary>
/// <param name="c">The input <see cref="IHeatContainer"/> to split into equal parts. This container is not modified, so make sure to discard it to avoid breaking energy conservation.</param>
/// <param name="dividedArray">An array of <see cref="IHeatContainer"/>s the source will be split into.
/// This will be written to.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when attempting to divide the source container by zero.</exception>
[PublicAPI]
public static void SplitAndCopy<T1, T2>(ref T1 c, T2[] dividedArray)
where T1 : IHeatContainer
where T2 : struct, IHeatContainer // if we allowed classes you'd just have an array reffing the same obj
{
var num = dividedArray.Length;
ArgumentOutOfRangeException.ThrowIfZero(num);
for (var i = 0; i < num; i++)
{
dividedArray[i].Temperature = c.Temperature;
dividedArray[i].HeatCapacity = c.HeatCapacity / num;
}
}
}
@@ -13,8 +13,8 @@ public static partial class HeatContainerHelpers
/// Positive values add heat, negative values remove heat.
/// The temperature can never become lower than 0K even if more heat is removed.
/// </summary>
/// <param name="c">The <see cref="IHeatContainer"/> to add or remove energy.</param>
/// <param name="dQ">The energy in joules to add or remove.</param>
/// <param name="c">The <see cref="IHeatContainer"/> to add heat to or remove heat from.</param>
/// <param name="dQ">The amount of energy in joules to add or remove.</param>
[PublicAPI]
public static void AddHeat<T>(ref T c, float dQ) where T : IHeatContainer
{
@@ -27,13 +27,11 @@ public static partial class HeatContainerHelpers
/// The temperature can never become lower than 0K even if more heat is removed.
/// </summary>
/// <param name="c">The <see cref="IHeatContainer"/> to query.</param>
/// <param name="dQ">The energy in joules to add or remove.</param>
/// <param name="dQ">The amount of energy in joules to add or remove.</param>
/// <returns>The resulting temperature in kelvin after the heat change.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the heat capacity of the container is zero or negative.</exception>
[PublicAPI]
public static float AddHeatQuery<T>(ref T c, float dQ) where T : IHeatContainer
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(c.HeatCapacity);
// Don't allow the temperature to go below the absolute minimum.
return Math.Max(0f, c.Temperature + dQ / c.HeatCapacity);
}
@@ -0,0 +1,612 @@
using System.Linq;
using Content.Shared.Temperature.HeatContainer;
using NUnit.Framework;
namespace Content.Tests.Shared.Temperature;
[TestFixture, TestOf(typeof(HeatContainer))]
[Parallelizable(ParallelScope.All)]
public sealed class HeatContainerTest
{
#region HeatContainerHelpers
[Test]
public void AddHeatTest()
{
// T = 100 K
// C = 1000 J/K
var c = new HeatContainer(1000f, 100f);
var originalEnergy = c.InternalEnergy;
// Check initial values.
Assert.That(c.Temperature, Is.EqualTo(100f));
Assert.That(c.HeatCapacity, Is.EqualTo(1000f));
// Add 5000 J, the temperature should rise by 5 K.
HeatContainerHelpers.AddHeat(ref c, 5000);
Assert.That(c.Temperature, Is.EqualTo(105f).Within(1).Ulps);
Assert.That(c.HeatCapacity, Is.EqualTo(1000f));
Assert.That(c.InternalEnergy, Is.EqualTo(originalEnergy + 5000).Within(1).Ulps);
// Subtract 15000 J, the temperature should lower by 15 K.
HeatContainerHelpers.AddHeat(ref c, -15000);
Assert.That(c.Temperature, Is.EqualTo(90f).Within(1).Ulps);
Assert.That(c.HeatCapacity, Is.EqualTo(1000f));
Assert.That(c.InternalEnergy, Is.EqualTo(originalEnergy + 5000 - 15000).Within(1).Ulps);
// Check that we cannot go below 0 K.
HeatContainerHelpers.AddHeat(ref c, -200000f);
Assert.That(c.Temperature, Is.Zero);
Assert.That(c.HeatCapacity, Is.EqualTo(1000f));
Assert.That(c.InternalEnergy, Is.Zero);
}
[Test]
public void AddHeatQueryTest()
{
// T = 100 K
// C = 1000 J/K
var c = new HeatContainer(1000f, 100f);
// Check initial values.
Assert.That(c.Temperature, Is.EqualTo(100f));
Assert.That(c.HeatCapacity, Is.EqualTo(1000f));
// Add 5000 J, the temperature should rise by 5 K from the original value.
Assert.That(HeatContainerHelpers.AddHeatQuery(ref c, 5000), Is.EqualTo(105f).Within(1).Ulps);
// Subtract 15000 J, the temperature should lower by 15 K from the original value.
Assert.That(HeatContainerHelpers.AddHeatQuery(ref c, -15000), Is.EqualTo(85f).Within(1).Ulps);
// Check that we cannot go below 0 K.
Assert.That(HeatContainerHelpers.AddHeatQuery(ref c, -200000f), Is.Zero);
// The original container should be unchanged.
Assert.That(c.Temperature, Is.EqualTo(100f));
Assert.That(c.HeatCapacity, Is.EqualTo(1000f));
}
[Test]
public void SetHeatCapacityTest()
{
// T = 300 K
// C = 1000 J/K
var c = new HeatContainer(1000f, 300f);
var originalEnergy = c.InternalEnergy;
// We triple the heat capacity, resulting in the temeperature to become one third of the original.
HeatContainerHelpers.SetHeatCapacity(ref c, 3000f);
// The original container should be unchanged.
Assert.That(c.Temperature, Is.EqualTo(100f).Within(1).Ulps);
Assert.That(c.HeatCapacity, Is.EqualTo(3000f));
// The total energy is conserved.
Assert.That(c.InternalEnergy, Is.EqualTo(originalEnergy).Within(1).Ulps);
}
#endregion
#region Divide
[Test]
public void SplitTest()
{
// T = 42 K
// C = 3000 J/K
var c1 = new HeatContainer(3000f, 42f);
var c2 = new HeatContainer();
var totalEnergy = c1.InternalEnergy;
// Split equally.
HeatContainerHelpers.SplitFrom(ref c1, ref c2, fraction: 0.5f);
// The heat capacity should be split equally.
// The temperature should be the same.
// The total energy should be conserved.
Assert.That(c1.Temperature, Is.EqualTo(42f));
Assert.That(c1.HeatCapacity, Is.EqualTo(1500f).Within(1).Ulps);
Assert.That(c2.Temperature, Is.EqualTo(42f));
Assert.That(c2.HeatCapacity, Is.EqualTo(1500f).Within(1).Ulps);
Assert.That(c1.InternalEnergy + c2.InternalEnergy, Is.EqualTo(totalEnergy).Within(1).Ulps);
// Reset the first container.
c1 = new HeatContainer(3000f, 42f);
// Split into 2/3 + 1/3.
HeatContainerHelpers.SplitFrom(ref c1, ref c2, fraction: 1f / 3);
// The heat capacity should be split according to the fraction.
// The temperature should be the same.
// The total energy should be conserved.
Assert.That(c1.Temperature, Is.EqualTo(42f));
Assert.That(c1.HeatCapacity, Is.EqualTo(2000f).Within(1).Ulps);
Assert.That(c2.Temperature, Is.EqualTo(42f));
Assert.That(c2.HeatCapacity, Is.EqualTo(1000f).Within(1).Ulps);
Assert.That(c1.InternalEnergy + c2.InternalEnergy, Is.EqualTo(totalEnergy).Within(1).Ulps);
}
[Test]
public void SplitDiscardTest()
{
// T = 42 K
// C = 3000 J/K
var c1 = new HeatContainer(3000f, 42f);
// Split equally.
HeatContainerHelpers.SplitFrom(ref c1, fraction: 0.5f);
// The heat capacity should be split equally, the temperature should be the same.
Assert.That(c1.Temperature, Is.EqualTo(42f));
Assert.That(c1.HeatCapacity, Is.EqualTo(1500f).Within(1).Ulps);
// Reset the container.
c1 = new HeatContainer(3000f, 42f);
// Split into 1/3 + 2/3.
HeatContainerHelpers.SplitFrom(ref c1, fraction: 1f / 3);
// The heat capacity should be split according to the fraction, the temperature should be the same.
Assert.That(c1.Temperature, Is.EqualTo(42f));
Assert.That(c1.HeatCapacity, Is.EqualTo(2000f).Within(1).Ulps);
}
[Test]
public void SplitArrayTest()
{
// T = 42 K
// C = 1000 J/K
const int n = 4;
var c1 = new HeatContainer(1000f, 42f);
var cA = new HeatContainer[n];
// Split into n + 1 equal parts.
HeatContainerHelpers.SplitFrom(ref c1, cA);
for (var i = 0; i < n; i++)
{
// The temperature should be the same as the initial one.
// The heat capacities should be equally split.
Assert.That(cA[i].Temperature, Is.EqualTo(42f));
Assert.That(cA[i].HeatCapacity, Is.EqualTo(1000f / (n + 1)).Within(1).Ulps);
}
// Check that the initital container is the same as the output containers.
Assert.That(c1.Temperature, Is.EqualTo(42f));
Assert.That(c1.HeatCapacity, Is.EqualTo(1000f / (n + 1)).Within(1).Ulps);
}
[Test]
public void SplitAndCopyTest()
{
// T = 42 K
// C = 1000 J/K
const int n = 5;
var c1 = new HeatContainer(1000f, 42f);
var cA = new HeatContainer[n];
// Divide into n equal parts.
HeatContainerHelpers.SplitAndCopy(ref c1, cA);
for (var i = 0; i < n; i++)
{
// The temperature should be the same as the initial one.
// The heat capacities should be equally split.
Assert.That(cA[i].Temperature, Is.EqualTo(42f));
Assert.That(cA[i].HeatCapacity, Is.EqualTo(1000f / n).Within(1).Ulps);
}
// Check that the initital container is unmodified.
Assert.That(c1.Temperature, Is.EqualTo(42f));
Assert.That(c1.HeatCapacity, Is.EqualTo(1000f));
}
#endregion
#region Merge
[Test]
public void Merge2Test()
{
// T = 42 K
// C = 5000 J/K
var c1 = new HeatContainer(5000f, 42f);
var energy1 = c1.InternalEnergy;
// T = 100 K
// C = 5000 J/K
var c2 = new HeatContainer(5000f, 100f);
var energy2 = c2.InternalEnergy;
// Merge 2 containers of the same capacity and different temperatures.
HeatContainerHelpers.MergeInto(ref c1, ref c2);
// The temperature should be the average of the two initial ones.
// The total heat capacity should be the sum of the two initial capacities.
// The total energy should be conserved.
Assert.That(c1.Temperature, Is.EqualTo((42f + 100f) / 2).Within(1).Ulps);
Assert.That(c1.HeatCapacity, Is.EqualTo(10000f).Within(1).Ulps);
Assert.That(c1.InternalEnergy, Is.EqualTo(energy1 + energy2).Within(1).Ulps);
// The second container should remain unchanged.
Assert.That(c2.Temperature, Is.EqualTo(100));
Assert.That(c2.HeatCapacity, Is.EqualTo(5000f));
// T = 100 K
// C = 750 J/K
// E = 75000 J
c1 = new HeatContainer(750f, 100f);
energy1 = c1.InternalEnergy;
// T = 300 K
// C = 250 J/K
// E = 75000 J
c2 = new HeatContainer(250f, 300f);
energy2 = c2.InternalEnergy;
// Merge 2 containers with different temperature and capacity.
HeatContainerHelpers.MergeInto(ref c1, ref c2);
// The temperature should averaged weighted by capacity.
// (100*750+300*250)/(750+250)=150
// The total heat capacity should be the sum of the two initial capacities.
// The total energy should be conserved.
Assert.That(c1.Temperature, Is.EqualTo(150f).Within(1).Ulps);
Assert.That(c1.HeatCapacity, Is.EqualTo(750f + 250f).Within(1).Ulps);
Assert.That(c1.InternalEnergy, Is.EqualTo(energy1 + energy2).Within(1).Ulps);
// The second container should remain unchanged.
Assert.That(c2.Temperature, Is.EqualTo(300));
Assert.That(c2.HeatCapacity, Is.EqualTo(250f));
}
[Test]
public void Merge1PlusArrayTest()
{
// T = 200 K
// C = 50 J/K
var c1 = new HeatContainer(50f, 200f);
var energy1 = c1.InternalEnergy;
// Array of 40 heat containers, each with
// T = 100 K
// C = 5 J/K
const int n = 40;
var cA1 = new HeatContainer(5f, 100);
var cA = new HeatContainer[n];
var energyA = cA1.InternalEnergy * n;
for (var i = 0; i < cA.Length; i++)
{
cA[i] = cA1;
}
// Merge the array into the single heat container.
HeatContainerHelpers.MergeInto(ref c1, cA);
// The temperature should averaged weighted by capacity.
// (200*50+100*5*40)/(50+5*40)=120
// The total heat capacity should be the sum of the initial capacities.
// The total energy should be conserved.
Assert.That(c1.Temperature, Is.EqualTo(120f).Within(1).Ulps);
Assert.That(c1.HeatCapacity, Is.EqualTo(50f + 5 * n).Within(1).Ulps);
Assert.That(c1.InternalEnergy, Is.EqualTo(energy1 + energyA).Within(1).Ulps);
}
[Test]
public void MergeArrayTest()
{
// This heat container will be overwritten.
var c1 = new HeatContainer(50f, 200f);
// Array of 40 heat containers, each with
// T = 100 K
// C = 5 J/K
const int n = 40;
var cA1 = new HeatContainer(5f, 100);
var cA = new HeatContainer[n];
var energyA = cA1.InternalEnergy * n;
for (var i = 0; i < cA.Length; i++)
{
cA[i] = cA1;
}
// Merge the array into the single heat container.
HeatContainerHelpers.MergeAndCopy(ref c1, cA);
// The temperature of all merged containers was the same.
// The total heat capacity should be the sum of the initial capacities.
// The total energy should be the sum of the intial energies.
Assert.That(c1.Temperature, Is.EqualTo(100f));
Assert.That(c1.HeatCapacity, Is.EqualTo(5 * n).Within(1).Ulps);
Assert.That(c1.InternalEnergy, Is.EqualTo(energyA).Within(1).Ulps);
}
#endregion
#region Exchange
[Test]
public void EquilibriumQuery2BodyTest()
{
// Cold c1, hot c2.
var c1 = new HeatContainer(123f, 456f);
var c2 = new HeatContainer(987f, 654f);
var dQ11 = HeatContainerHelpers.EquilibriumHeatQuery(ref c1, ref c1);
var dQ12 = HeatContainerHelpers.EquilibriumHeatQuery(ref c1, ref c2);
var dQ21 = HeatContainerHelpers.EquilibriumHeatQuery(ref c2, ref c1);
var dQ22 = HeatContainerHelpers.EquilibriumHeatQuery(ref c2, ref c2);
var t11 = HeatContainerHelpers.EquilibriumTemperatureQuery(ref c1, ref c1);
var t12 = HeatContainerHelpers.EquilibriumTemperatureQuery(ref c1, ref c2);
var t21 = HeatContainerHelpers.EquilibriumTemperatureQuery(ref c2, ref c1);
var t22 = HeatContainerHelpers.EquilibriumTemperatureQuery(ref c2, ref c2);
// Containers should be in equilibrium with themselves.
Assert.That(dQ11, Is.Zero.Within(1).Ulps);
Assert.That(dQ22, Is.Zero.Within(1).Ulps);
Assert.That(t11, Is.EqualTo(c1.Temperature).Within(1).Ulps);
Assert.That(t22, Is.EqualTo(c2.Temperature).Within(1).Ulps);
// Heat should flow from hot to cold.
Assert.That(dQ12, Is.LessThan(0f));
Assert.That(dQ21, Is.GreaterThan(0f));
Assert.That(t12, Is.LessThan(c2.Temperature));
Assert.That(t21, Is.LessThan(c2.Temperature));
Assert.That(t12, Is.GreaterThan(c1.Temperature));
Assert.That(t21, Is.GreaterThan(c1.Temperature));
// The result should be symmetric.
Assert.That(dQ21, Is.EqualTo(-dQ12).Within(1).Ulps);
Assert.That(t12, Is.EqualTo(t21).Within(1).Ulps);
// Check that the heat flow indeed brings them into equilibrium.
HeatContainerHelpers.AddHeat(ref c1, -dQ12);
HeatContainerHelpers.AddHeat(ref c2, dQ12);
Assert.That(c1.Temperature, Is.EqualTo(c2.Temperature).Within(1).Ulps);
Assert.That(c1.Temperature, Is.EqualTo(t12).Within(1).Ulps);
Assert.That(c1.Temperature, Is.EqualTo(t21).Within(1).Ulps);
Assert.That(c2.Temperature, Is.EqualTo(t12).Within(1).Ulps);
Assert.That(c2.Temperature, Is.EqualTo(t21).Within(1).Ulps);
}
[Test]
public void Equilibrium2BodyTest()
{
// Cold c1, hot c2.
var c1 = new HeatContainer(123f, 456f);
var c2 = new HeatContainer(987f, 654f);
var totalEnergy = c1.InternalEnergy + c2.InternalEnergy;
// Bring them into equilibrium.
HeatContainerHelpers.Equilibrate(ref c1, ref c2);
// Total energy should be conserved.
Assert.That(c1.InternalEnergy + c2.InternalEnergy, Is.EqualTo(totalEnergy).Within(1).Ulps);
// The temperature should be equal, the capacities unchanged.
Assert.That(c1.Temperature, Is.EqualTo(c2.Temperature).Within(1).Ulps);
Assert.That(c1.HeatCapacity, Is.EqualTo(123f));
Assert.That(c2.HeatCapacity, Is.EqualTo(987f));
// Repeat with the out dQ overload.
c1 = new HeatContainer(123f, 456f);
c2 = new HeatContainer(987f, 654f);
totalEnergy = c1.InternalEnergy + c2.InternalEnergy;
var dQQuery = HeatContainerHelpers.EquilibriumHeatQuery(ref c1, ref c2);
// Bring them into equilibrium.
HeatContainerHelpers.Equilibrate(ref c1, ref c2, out var dQresult);
// Total energy should be conserved.
Assert.That(c1.InternalEnergy + c2.InternalEnergy, Is.EqualTo(totalEnergy).Within(1).Ulps);
// The temperature should be equal, the capacities unchanged.
Assert.That(c1.Temperature, Is.EqualTo(c2.Temperature).Within(1).Ulps);
Assert.That(c1.HeatCapacity, Is.EqualTo(123f));
Assert.That(c2.HeatCapacity, Is.EqualTo(987f));
// The output dQ should be the same as the query we did before.
Assert.That(dQQuery, Is.EqualTo(dQresult).Within(1).Ulps);
}
[Test]
public void Equilibrium3BodyTest()
{
// Cold c1, medium c2, hot c3.
var c1 = new HeatContainer(300f, 123f);
var c2 = new HeatContainer(200f, 234f);
var c3 = new HeatContainer(100f, 456f);
var totalEnergy = c1.InternalEnergy + c2.InternalEnergy + c3.InternalEnergy;
// Save as array.
var cN = new HeatContainer[3];
cN[0] = c1;
cN[1] = c2;
cN[2] = c3;
var tQuery = HeatContainerHelpers.EquilibriumTemperatureQuery(cN);
var tQuerydQ = HeatContainerHelpers.EquilibriumTemperatureQuery(cN, out var dQQuery);
// Both queries should result in the same temperature.
Assert.That(tQuery, Is.EqualTo(tQuerydQ).Within(1).Ulps);
// Heat flows from hot to cold.
Assert.That(tQuery, Is.GreaterThan(c1.Temperature));
Assert.That(tQuery, Is.LessThan(c3.Temperature));
Assert.That(dQQuery[0], Is.GreaterThan(0f));
Assert.That(dQQuery[2], Is.LessThan(0f));
// Total energy should be conserved.
Assert.That(dQQuery.Sum(), Is.Zero.Within(1).Ulps);
// Check if we actually reach equilibrium with the calculated heat flow.
HeatContainerHelpers.AddHeat(ref c1, dQQuery[0]);
HeatContainerHelpers.AddHeat(ref c2, dQQuery[1]);
HeatContainerHelpers.AddHeat(ref c3, dQQuery[2]);
Assert.That(c1.Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
Assert.That(c2.Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
Assert.That(c3.Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
// Put the array into equilibrium.
HeatContainerHelpers.Equilibrate(cN);
// Check if we actually reached equilibrium.
Assert.That(cN[0].Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
Assert.That(cN[1].Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
Assert.That(cN[2].Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
// Total energy should be conserved.
var newTotalEnergy = cN[0].InternalEnergy + cN[1].InternalEnergy + cN[2].InternalEnergy;
Assert.That(newTotalEnergy, Is.EqualTo(totalEnergy).Within(1).Ulps);
}
[Test]
public void Equilibrium1Plus3BodyTest()
{
// Cold c1, medium c2 and c3, hot c4.
var c1 = new HeatContainer(400f, 123f);
var c2 = new HeatContainer(300f, 234f);
var c3 = new HeatContainer(200f, 456f);
var c4 = new HeatContainer(100f, 567f);
var totalEnergy = c1.InternalEnergy + c2.InternalEnergy + c3.InternalEnergy + c4.InternalEnergy;
// Save as array.
var cN = new HeatContainer[3];
cN[0] = c1;
cN[1] = c2;
cN[2] = c3;
var tQuery = HeatContainerHelpers.EquilibriumTemperatureQuery(ref c4, cN);
// Heat flows from hot to cold.
Assert.That(tQuery, Is.GreaterThan(c1.Temperature));
Assert.That(tQuery, Is.LessThan(c4.Temperature));
// Total energy should be conserved.
Assert.That(tQuery * (c1.HeatCapacity + c2.HeatCapacity + c3.HeatCapacity + c4.HeatCapacity), Is.EqualTo(totalEnergy).Within(1).Ulps);
// Put everything into equilibrium.
HeatContainerHelpers.Equilibrate(ref c4, cN);
// Check if we actually reached equilibrium.
Assert.That(cN[0].Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
Assert.That(cN[1].Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
Assert.That(cN[2].Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
Assert.That(c4.Temperature, Is.EqualTo(tQuery).Within(1).Ulps);
// Total energy should be conserved.
var newTotalEnergy = cN[0].InternalEnergy + cN[1].InternalEnergy + cN[2].InternalEnergy + c4.InternalEnergy;
Assert.That(newTotalEnergy, Is.EqualTo(totalEnergy).Within(1).Ulps);
}
#endregion
#region Conduct
[Test]
public void Conduct1Test()
{
// T = 100 K
// C = 42 J/K
var c1 = new HeatContainer(42f, 100f);
var c2 = new HeatContainer(42f, 100f);
var c3 = new HeatContainer(42f, 100f);
var c4 = new HeatContainer(42f, 100f);
// Conduct heat with a heat bath of 200K for a small time step of 0.01s and a conductance of 1.
var dQ1 = HeatContainerHelpers.ConductHeat(ref c1, 200f, 0.01f, 100f);
// The temperature should be between 100 and 200K.
// The heat capacity should be unchanged.
Assert.That(c1.Temperature, Is.GreaterThan(100f));
Assert.That(c1.Temperature, Is.LessThan(200f));
Assert.That(c1.HeatCapacity, Is.EqualTo(42f));
// The conducted heat should positive, since the temperature got higher.
Assert.That(dQ1, Is.GreaterThan(0f));
// Check that removing the heat again brings us back where we were originally.
var c1Copy = c1;
HeatContainerHelpers.AddHeat(ref c1Copy, -dQ1);
Assert.That(c1Copy.Temperature, Is.EqualTo(100f).Within(1).Ulps);
// A greater temperature difference means a greater heat transfer.
var dQ2 = HeatContainerHelpers.ConductHeat(ref c2, 300f, 0.01f, 100f);
Assert.That(dQ2, Is.GreaterThan(dQ1));
Assert.That(c2.Temperature, Is.GreaterThan(c1.Temperature));
// A greater time step means a greater heat transfer.
var dQ3 = HeatContainerHelpers.ConductHeat(ref c3, 200f, 0.02f, 100f);
Assert.That(dQ3, Is.GreaterThan(dQ1));
Assert.That(c3.Temperature, Is.GreaterThan(c1.Temperature));
// A greater conductance means a greater heat transfer.
var dQ4 = HeatContainerHelpers.ConductHeat(ref c4, 200f, 0.01f, 200f);
Assert.That(dQ4, Is.GreaterThan(dQ1));
Assert.That(c4.Temperature, Is.GreaterThan(c1.Temperature));
// Make sure we don't overshoot with a too large time step and conductance.
var c5 = new HeatContainer(42f, 100f);
var dQ5 = HeatContainerHelpers.ConductHeat(ref c5, 200f, 10f, 10000f);
Assert.That(c5.Temperature, Is.EqualTo(200f).Within(1).Ulps);
// Check that the heat diff is still correct even when we would have overshot.
HeatContainerHelpers.AddHeat(ref c5, -dQ5);
Assert.That(c5.Temperature, Is.EqualTo(100f).Within(1).Ulps);
// Check that consecutive steps become smaller, but still get us closer to equilibrium.
var c6 = new HeatContainer(42f, 100f);
var t6Init = c6.Temperature;
var dQ6A = HeatContainerHelpers.ConductHeat(ref c6, 200f, 1f, 1f);
var t6A = c6.Temperature;
var dQ6B = HeatContainerHelpers.ConductHeat(ref c6, 200f, 1f, 1f);
var t6B = c6.Temperature;
Assert.That(dQ6A, Is.GreaterThan(dQ6B));
Assert.That(t6A, Is.GreaterThan(t6Init));
Assert.That(t6B, Is.GreaterThan(t6A));
// Check that we converge towards the heat bath temperature.
var c7 = new HeatContainer(42f, 100f);
var e7Init = c7.InternalEnergy;
var dQ7 = 0f;
for (var i = 0; i < 10000; i++)
{
dQ7 += HeatContainerHelpers.ConductHeat(ref c7, 200f, 1f, 1f);
}
Assert.That(c7.Temperature, Is.EqualTo(200f).Within(0.1).Percent);
Assert.That(c7.InternalEnergy - dQ7, Is.EqualTo(e7Init).Within(0.2).Percent);
}
[Test]
public void Conduct2Test()
{
// Temperatures at 100 K and 200 K
var cA = new HeatContainer(42f, 100f);
var cB = new HeatContainer(123f, 200f);
var totalEnergy = cA.InternalEnergy + cB.InternalEnergy;
var tEquilibrium = HeatContainerHelpers.EquilibriumTemperatureQuery(ref cA, ref cB);
var dQ = HeatContainerHelpers.ConductHeat(ref cA, ref cB, 1f, 1f);
// Heat flow from hot B to cold A should be positive.
Assert.That(dQ, Is.GreaterThan(0f));
// Energy should be conserved.
Assert.That(cA.InternalEnergy + cB.InternalEnergy, Is.EqualTo(totalEnergy));
// Check that we got closer to equilibrium, but did not reach it.
Assert.That(cA.Temperature, Is.GreaterThan(100f));
Assert.That(cA.Temperature, Is.LessThan(tEquilibrium));
Assert.That(cB.Temperature, Is.LessThan(200f));
Assert.That(cB.Temperature, Is.GreaterThan(tEquilibrium));
// Check that the given heat transfer amount is correct.
HeatContainerHelpers.AddHeat(ref cA, -dQ);
HeatContainerHelpers.AddHeat(ref cB, dQ);
Assert.That(cA.Temperature, Is.EqualTo(100f).Within(1).Ulps);
Assert.That(cB.Temperature, Is.EqualTo(200f).Within(1).Ulps);
// Reset containers.
cA = new HeatContainer(42f, 100f);
cB = new HeatContainer(123f, 200f);
// Check that we converge towards equilibrium.
for (var i = 0; i < 10000; i++)
{
HeatContainerHelpers.ConductHeat(ref cA, ref cB, 1f, 1f);
}
Assert.That(cA.Temperature, Is.EqualTo(tEquilibrium).Within(0.1).Percent);
Assert.That(cB.Temperature, Is.EqualTo(tEquilibrium).Within(0.1).Percent);
}
#endregion
}