-
Notifications
You must be signed in to change notification settings - Fork 15
Temperature Computation
This page explains how temperature computation is handled in the mod. The simulation is based on an implicit finite-difference heat diffusion solver, which produces stable results even with large timesteps.
Temperature is only computed for sections (16×16×16 blocks) within a 16-block radius around BlockEntity instances implementing IHaveTemperature.
Each active section relies on four data layers (more detail here):
-
TemperatureDataLayer— stores current and default temperatures. -
ConductionDataLayer— stores thermal conductivity (how fast heat diffuses). -
ResilienceDataLayer— stores the "return-to-default" rate (how fast a block stabilises to equilibrium).
If any of the three are missing for a section, the section is skipped for that tick.
The simulation discretises the heat equation as:
C * (T_next - T_current) / dt = k∇²T_next + β * resilience * (T_default - T_next)
Unlike an explicit scheme where T_next can be computed directly, this equation couples all voxel temperatures together. Rearranging yields the linear system:
A * T_next = T_current + b_source
which is solved every tick using Conjugate Gradient. The advantage is unconditional numerical stability — the timestep can be as large as needed without oscillations.
Constants used:
-
DT = 1/20— one game tick. -
CAPACITY = 3e4— thermal capacity of all voxels. -
γ = DT / CAPACITY— diffusion time-scale. -
β = 1e5 * DT / CAPACITY— resilience time-scale.
For each voxel at global index i, one row of matrix A is assembled as follows.
Diagonal entry:
A[i][i] = 1 + resilience * β + Σ k_eff(i, j) (sum over face neighbors j)
Off-diagonal entry (for each neighbor j inside the active matrix):
A[i][j] = -k_eff(i, j)
Where k_eff is the harmonic mean of the two conductivities:
k_eff = 2 * k_self * k_neighbor / (k_self + k_neighbor)
Using the harmonic mean ensures that a low-conductivity block always bottlenecks heat flow, regardless of its neighbour's value.
Right-hand side:
rhs[i] = T_current[i] + resilience * β * T_default[i] + Σ k_eff * T_boundary
Boundary neighbors (sections outside the active matrix) contribute their temperature directly to the RHS rather than as off-diagonal entries. If no temperature layer exists for a boundary section, 300 K is assumed as a default.
After the solve, for each section:
- If any voxel's temperature changed by more than ε = 0.1 K, the section is marked as needing to tick next step.
- If the changed voxel lies on a section boundary, the adjacent section is also marked for ticking — this allows thermal fronts to propagate naturally across section borders.
- If no voxel changed significantly, the section is marked as clean and will not be ticked next time.
Block entities implementing IHaveTemperature (e.g. FuelAssembly, HeatExchanger) act as heat sources or sinks. Before the matrix is solved each tick:
- Their voxel's
temperature,defaultTemperature,conductivity, andresilienceare overwritten in the relevant data layers. - The surrounding voxels are stamped to mark those rows as dirty so the matrix reflects the new values.
After the solve, each source receives the temperature delta computed by the solver:
source.addTemperature(T_next[idx] - T_current[idx]);This lets block entities track their own thermal state in response to the surrounding environment.
- The matrix is cached between ticks and only rebuilt when the set of active sections changes (sections added or removed). Individual row updates are done in-place for dirty voxels.
- Solver working arrays (
cgR,cgP,cgAp) are allocated once and reused every tick to avoid per-tick garbage collection pressure. - The previous tick's solution is used as a warm start for the next solve, significantly reducing the number of Conjugate Gradient iterations needed in steady state.
- If a voxel has no conduction, resilience, or default-temperature data, it is given an identity row — it simply keeps its current temperature.
Last updated: June 2026 — Author: Real Ant Engineer