Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
gas_reference.cpp
Go to the documentation of this file.
1// gas_reference.cpp — 気体の流れの CPU リファレンス G1・G2(gas_reference.h・07 §2.1・§2.2・ADR-0043)。
2// 1 小刻み = (1) セルから導く値(質量・温度・圧力)→ (2) 面の力(圧力の力積・重さ)を当てる → (3) 力を当てた後の運動量で
3// 面の質量の流れを出し、風上のセルごとに出ていく割合を合計 1 までに抑える → (4) 面ごとに移す成分の物質量を決める
4// (MUSCL で面へ延ばした割合・乱数の丸め。G2)→ 風上のセルごとに合計が中身を超えないよう抑える → (5) 移す。
5// どの段も前の段の結果を読み、次の配列へ整数の足し引きだけで書く(順番に依存しない。04 R2・R3)。
6#include "sim/gas_reference.h"
7
8#include <algorithm>
9#include <cassert>
10#include <limits>
11
12#include "common/fixed.hlsli"
13
14namespace bicameral::sim {
15
16 namespace {
17
18 namespace fx = bicameral::fx;
19
20 // --- 物理の定数(整数。導き方はコメントに)---
21
22 // 理想気体: p(µPa)= n(µmol)× T(mK)× R ÷ V × 1e-3 = n × T × 0.0665157(R = 8.314462618 J/(mol·K)・V = 0.125 m³)。Q32
23 constexpr uint64_t PRESSURE_PER_AMOUNT_TEMPERATURE_Q32 = 285682760;
24
25 // 層の重さで下がる圧力: m(mg)× 1e-6 × g ÷ A(0.25 m²)= m × 39.2266 µPa(g = 9.80665 m/s²)
26 constexpr uint64_t HYDROSTATIC_MICROPASCAL_PER_MASS_X10000 = 392266;
27
28 constexpr uint64_t GRAVITY_MICROMETER_PER_SECOND2 = 9806650;
29 constexpr uint64_t MICRO = 1000000;
30 constexpr uint64_t ONE_Q32 = (uint64_t)1 << 32;
31 constexpr uint64_t LOW_32_BITS = ONE_Q32 - 1;
32
33 // 1 セルの物質量の上限(µmol)。移す物質量を Q32 の 64bit で計算するため(0.125 m³ に 2147 mol = 大気の約 400 倍)
34 constexpr uint64_t MAX_CELL_AMOUNT = (uint64_t)1 << 31;
35
36 // 丸めの乱数の用途(FxHash64。R6)
37 constexpr uint32_t GAS_ROUNDING_PURPOSE = 0x47610001u;
38
39 // 面の片側(箱の中のセル・開いた境界の外・壁)
40 enum class SideKind : uint8_t {
41 Cell,
44 };
45
46 struct FaceSide {
48 uint32_t index = 0; // Cell: セルの番号 / Ghost: 層 z
49 };
50
51 struct GasFace {
52 FaceSide left; // 軸のマイナス側
53 FaceSide right; // 軸のプラス側
54 FaceSide leftOuter; // left のさらにマイナス側(MUSCL の勾配)
55 FaceSide rightOuter; // right のさらにプラス側
56 uint32_t axis = 0;
57 uint32_t id = 0; // 面の番号(乱数の丸めの ID。並べた順ではなく場所から決める。R6)
58 int64_t flow = 0; // 質量の流れ(mg/小刻み。左 → 右が正)
59 int64_t impulse = 0; // 面の圧力の力積(左のセルから引き、右のセルへ足す)
60 uint64_t fraction = 0; // 風上の中身のうち移す割合(Q32)
61
62 // --- 重さ(縦の面だけ。基準との質量の差 × g を面の両側の半分ずつのセルで受ける)---
63 int64_t weightLeft = 0; // 左のセルの運動量 z から引く力積
64 int64_t weightRight = 0;
65
66 // --- 移す成分の物質量(µmol。MUSCL と乱数の丸めの後、風上のセルごとに中身を超えないよう抑えた値)---
67 std::array<uint64_t, GAS_MAX_SPECIES> moved = {};
68 };
69
70 uint64_t MulShiftU64(uint64_t a, uint64_t b, uint32_t shift) {
71 return fx::FxShiftRightU128(fx::FxMulU64Full(a, b), shift);
72 }
73
74 uint64_t TotalAmount(const GasConfig& config, const GasCell& cell) {
75 uint64_t total = 0;
76 for (uint32_t s = 0; s < config.speciesCount; ++s)
77 total += cell.amounts[s];
78
79 return total;
80 }
81
82 // 熱容量(nJ/K)= Σ 物質量(µmol)× 比熱(mJ/(mol·K))
83 uint64_t HeatCapacity(const GasConfig& config, const GasCell& cell) {
84 uint64_t capacity = 0;
85 for (uint32_t s = 0; s < config.speciesCount; ++s)
86 capacity += cell.amounts[s] * config.species[s].heatCapacity;
87
88 return capacity;
89 }
90
91 // 化学のエネルギー(µJ)= Σ 物質量 × H0
92 int64_t ChemicalEnergy(const GasConfig& config, const GasCell& cell) {
93 int64_t chemical = 0;
94 for (uint32_t s = 0; s < config.speciesCount; ++s)
95 chemical += (int64_t)cell.amounts[s] * config.species[s].formationEnergy;
96
97 return chemical;
98 }
99
100 uint64_t CellMass(const GasConfig& config, const GasCoefficients& coefficients, const GasCell& cell) {
101 uint64_t mass = 0;
102 for (uint32_t s = 0; s < config.speciesCount; ++s)
103 mass += MulShiftU64(cell.amounts[s], coefficients.massPerAmount[s], 32);
104
105 return mass;
106 }
107
108 // 温度(mK)= 熱(µJ)× 1e6 ÷ 熱容量(nJ/K)。熱が 0 以下なら 0
109 uint32_t CellTemperature(const GasConfig& config, const GasCell& cell) {
110 const int64_t heat = cell.energy * 1000 - ChemicalEnergy(config, cell);
111 const uint64_t capacity = HeatCapacity(config, cell);
112 if (heat <= 0 || capacity == 0)
113 return 0;
114
115 const uint64_t temperature = fx::FxDivU128By64(fx::FxMulU64Full((uint64_t)heat, MICRO), capacity).quotient;
116 assert(temperature <= std::numeric_limits<uint32_t>::max());
117
118 return (uint32_t)temperature;
119 }
120
121 // 本物の圧力(µPa)= n × T × R ÷ V
122 int64_t CellPressure(const GasConfig& config, const GasCell& cell, uint32_t temperature) {
123 const fx::FxU128 amountTemperature = fx::FxMulU64Full(TotalAmount(config, cell), temperature);
124 assert(amountTemperature.hi == 0);
125
126 return (int64_t)MulShiftU64(amountTemperature.lo, PRESSURE_PER_AMOUNT_TEMPERATURE_Q32, 32);
127 }
128
129 // 質量・温度・本物の圧力(p̃ は基準が要るので呼ぶ側で)
130 GasDerived DeriveRaw(const GasConfig& config, const GasCoefficients& coefficients, const GasCell& cell) {
131 const uint64_t amount = TotalAmount(config, cell);
132 assert(amount < MAX_CELL_AMOUNT);
133
134 GasDerived derived;
135 derived.mass = CellMass(config, coefficients, cell);
136 derived.inverseMass = derived.mass == 0 ? 0 : std::numeric_limits<uint64_t>::max() / derived.mass;
137 derived.inverseAmount = amount == 0 ? 0 : std::numeric_limits<uint64_t>::max() / amount;
138 derived.temperature = CellTemperature(config, cell);
139 derived.pressure = CellPressure(config, cell, derived.temperature);
140
141 return derived;
142 }
143
144 // --- 小刻みの部品 ---
145
146 uint32_t AxisSize(const GasConfig& config, uint32_t axis) {
147 return axis == 0 ? config.sizeX : (axis == 1 ? config.sizeY : config.sizeZ);
148 }
149
150 FaceSide BoundarySide(GasBoundary boundary, uint32_t z) {
151 if (boundary == GasBoundary::Open)
152 return {.kind = SideKind::Ghost, .index = z};
153
154 return {.kind = SideKind::Wall, .index = 0};
155 }
156
157 std::array<uint32_t, GAS_AXES> CellCoordinate(const GasBox& box, uint32_t index) {
158 const uint32_t sizeX = box.config.sizeX;
159 const uint32_t sizeY = box.config.sizeY;
160
161 return {index % sizeX, (index / sizeX) % sizeY, index / (sizeX * sizeY)};
162 }
163
164 // セル cell から軸 axis の座標を coordinate(箱の外なら −1 や size)に替えた所の側。周期的なら反対側へ回し、それ以外の外は境界
165 FaceSide SideAt(const GasBox& box, std::array<uint32_t, GAS_AXES> cell, uint32_t axis, int64_t coordinate) {
166 const int64_t size = AxisSize(box.config, axis);
167 const GasBoundary boundary = box.config.boundary[axis];
168 if (coordinate < 0 || coordinate >= size) {
169 if (boundary != GasBoundary::Periodic)
170 return BoundarySide(boundary, cell[2]);
171
172 coordinate = ((coordinate % size) + size) % size;
173 }
174
175 cell[axis] = (uint32_t)coordinate;
176 return {.kind = SideKind::Cell, .index = box.Index(cell[0], cell[1], cell[2])};
177 }
178
179 // セル cell の軸 axis のマイナス側の面(high なら端のセルのプラス側の境界の面)。両側と、その外側の隣(MUSCL の勾配)
180 GasFace MakeFace(const GasBox& box, std::array<uint32_t, GAS_AXES> cell, uint32_t axis, bool high) {
181 const int64_t left = high ? (int64_t)cell[axis] : (int64_t)cell[axis] - 1;
182 GasFace face;
183 face.axis = axis;
184 face.leftOuter = SideAt(box, cell, axis, left - 1);
185 face.left = SideAt(box, cell, axis, left);
186 face.right = SideAt(box, cell, axis, left + 1);
187 face.rightOuter = SideAt(box, cell, axis, left + 2);
188 face.id = ((box.Index(cell[0], cell[1], cell[2]) * GAS_AXES) + axis) * 2 + (high ? 1 : 0);
189
190 return face;
191 }
192
193 // 面の一覧: どの面も 1 回だけ(セルのマイナス側の面 + 端のセルのプラス側の境界の面)
194 std::vector<GasFace> EnumerateFaces(const GasBox& box) {
195 const GasConfig& config = box.config;
196 std::vector<GasFace> faces;
197 faces.reserve((box.cells.size() * GAS_AXES) + 64);
198
199 for (uint32_t index = 0; index < box.cells.size(); ++index) {
200 const std::array<uint32_t, GAS_AXES> cell = CellCoordinate(box, index);
201 for (uint32_t axis = 0; axis < GAS_AXES; ++axis) {
202 faces.push_back(MakeFace(box, cell, axis, false));
203
204 const bool lastCell = cell[axis] + 1 == AxisSize(config, axis);
205 if (lastCell && config.boundary[axis] != GasBoundary::Periodic)
206 faces.push_back(MakeFace(box, cell, axis, true));
207 }
208 }
209
210 return faces;
211 }
212
213 uint32_t CellLayer(const GasBox& box, uint32_t index) {
214 return index / (box.config.sizeX * box.config.sizeY);
215 }
216
217 const GasCell& SideCell(const GasBox& box, const std::vector<GasCell>& cells, FaceSide side) {
218 return side.kind == SideKind::Ghost ? box.ghost[side.index] : cells[side.index];
219 }
220
221 const GasDerived& SideDerived(const GasBox& box, const std::vector<GasDerived>& derived, FaceSide side) {
222 return side.kind == SideKind::Ghost ? box.referenceDerived[side.index] : derived[side.index];
223 }
224
225 // 基準との質量の差(mg)。開いた境界の外は基準そのもの(0)
226 int64_t Excess(const GasBox& box, const std::vector<GasDerived>& derived, FaceSide side) {
227 if (side.kind != SideKind::Cell)
228 return 0;
229
230 return (int64_t)derived[side.index].mass - (int64_t)box.referenceDerived[CellLayer(box, side.index)].mass;
231 }
232
233 // 質量の差 Δm(mg)が 1 セルの高さで作る静水圧の差の半分(µPa)= Δm × 39.2266 ÷ 2
234 int64_t HalfHydrostatic(int64_t excess) {
235 return excess * (int64_t)HYDROSTATIC_MICROPASCAL_PER_MASS_X10000 / 20000;
236 }
237
238 // 圧力の拡散を起こす圧力の差: p̃_左 − p̃_右。縦の面では、両側の質量の差の平均が作る静水圧の差を引く
239 // (釣り合った重い層・軽い層を拡散が押し上げ続けると、重力と引き合って偽の循環が回る。力の釣り合いをそろえる。07 §2.1)
240 int64_t DrivingPressure(const GasBox& box, const std::vector<GasDerived>& derived, const GasFace& face) {
241 const int64_t difference = SideDerived(box, derived, face.left).scaledPressure -
242 SideDerived(box, derived, face.right).scaledPressure;
243 if (face.axis != 2 || !box.config.gravity)
244 return difference;
245
246 return difference - HalfHydrostatic(Excess(box, derived, face.left) + Excess(box, derived, face.right));
247 }
248
249 // 縦の面の重さ: 面の両側の質量の差の平均 × g を、両側のセルが 1/4 ずつ受ける(セルから見ると 1/4・1/2・1/4 で均した重さ。
250 // 1 セルおきの温度の縞が作る重さの縞は面の平均で消える〔中心の流れが応じない縞を力で育てない〕。
251 // 圧力の差の中心差分と同じ形なので、静水圧の釣り合いが離散の式でそろう。07 §2.1)
252 void ComputeFaceWeight(const GasBox& box, const std::vector<GasDerived>& derived, GasFace& face) {
253 if (face.axis != 2 || !box.config.gravity)
254 return;
255
256 const auto coefficient = (int64_t)box.coefficients.gravityImpulse;
257 if (face.left.kind == SideKind::Wall) {
258 face.weightRight = fx::FxMulShiftS64(Excess(box, derived, face.right), coefficient, 17);
259 return;
260 }
261
262 if (face.right.kind == SideKind::Wall) {
263 face.weightLeft = fx::FxMulShiftS64(Excess(box, derived, face.left), coefficient, 17);
264 return;
265 }
266
267 const int64_t quarter = fx::FxMulShiftS64(
268 Excess(box, derived, face.left) + Excess(box, derived, face.right), coefficient, 18);
269 face.weightLeft = quarter;
270 face.weightRight = quarter;
271 }
272
273 // 面の力: 圧力の力積と重さ(小刻みの初めの導く値だけから)
274 void ComputeFaceForce(const GasBox& box, const std::vector<GasDerived>& derived, GasFace& face) {
275 const GasCoefficients& coefficients = box.coefficients;
276 ComputeFaceWeight(box, derived, face);
277
278 // --- 壁: 鏡に映した圧力(両側とも壁の手前のセルの p̃)---
279 if (face.left.kind == SideKind::Wall || face.right.kind == SideKind::Wall) {
280 // 縦の壁は、壁の手前のセルの圧力を半セル分の静水圧で壁まで延ばす(床は高く・天井は低く。重さの釣り合いとそろえる)
281 const bool wallBelow = face.left.kind == SideKind::Wall;
282 const FaceSide inside = wallBelow ? face.right : face.left;
283 int64_t pressure = derived[inside.index].scaledPressure;
284 if (face.axis == 2 && box.config.gravity) {
285 const int64_t hydrostatic = HalfHydrostatic(Excess(box, derived, inside));
286 pressure += wallBelow ? hydrostatic : -hydrostatic;
287 }
288
289 face.impulse = fx::FxMulShiftS64(pressure * 2, (int64_t)coefficients.pressureImpulse, 16);
290 return;
291 }
292
293 const int64_t pressureSum = SideDerived(box, derived, face.left).scaledPressure +
294 SideDerived(box, derived, face.right).scaledPressure;
295 face.impulse = fx::FxMulShiftS64(pressureSum, (int64_t)coefficients.pressureImpulse, 16);
296 }
297
298 // 面の質量の流れ(力を当てた後の運動量 + 小刻みの初めの圧力。前進・後退の順〔forward-backward〕)。壁は 0
299 void ComputeFaceFlow(const GasBox& box, const std::vector<GasDerived>& derived,
300 const std::vector<GasCell>& forced, GasFace& face) {
301 if (face.left.kind == SideKind::Wall || face.right.kind == SideKind::Wall)
302 return;
303
304 const int64_t momentumSum = SideCell(box, forced, face.left).momentum[face.axis] +
305 SideCell(box, forced, face.right).momentum[face.axis];
306
307 // --- 中心の流れ + 圧力の拡散(Rusanov の散逸を圧力の差にかける: 接触面〔温度・成分の境〕を散らさず、静止大気では 0)---
308 const int64_t central = fx::FxMulShiftS64(momentumSum, (int64_t)box.coefficients.centralFlow, 52);
309 const int64_t diffusion = fx::FxMulShiftS64(DrivingPressure(box, derived, face),
310 (int64_t)box.coefficients.pressureFlow, 40);
311 face.flow = central + diffusion;
312 }
313
314 // 風上のセルの中身のうち移す割合(Q32。抑える前)
315 uint64_t RawFraction(const GasBox& box, const std::vector<GasDerived>& derived, const GasFace& face) {
316 if (face.flow == 0)
317 return 0;
318
319 const FaceSide donor = face.flow > 0 ? face.left : face.right;
320 const uint64_t inverseMass = SideDerived(box, derived, donor).inverseMass;
321
322 return MulShiftU64(fx::FxAbsU64(face.flow), inverseMass, 32);
323 }
324
325 // a ÷ b を最も近い整数に(ちょうど半分は 0 から遠い側へ)。b > 0
326 int64_t DivideNearest(int64_t a, uint64_t b) {
327 const uint64_t magnitude = fx::FxAbsU64(a);
328 const fx::FxU128 product = {.hi = 0, .lo = magnitude};
329 const fx::FxDivResult result = fx::FxDivU128By64(product, b);
330 const uint64_t quotient = result.quotient + (result.remainder * 2 >= b ? 1 : 0);
331
332 return fx::FxApplySign(quotient, a < 0);
333 }
334
335 // 移す塊のエネルギー: 熱は風上の熱を熱容量の比で分け(塊の温度 = 風上の温度)、化学のエネルギーは移す物質量から。
336 // 物質量の切り捨てとエネルギーの切り捨てが別々だと、塊の温度が少しずつ偏り、流れに沿って熱が運ばれる偽の温度の縞が
337 // 浮力で対流を育てる(試作で見た。07 §2.1)。丸めは最も近い整数(偏りなし)
338 int64_t MovedEnergy(const GasConfig& config, const GasCell& source, const GasCell& moved) {
339 const uint64_t sourceCapacity = HeatCapacity(config, source);
340 const int64_t movedChemical = ChemicalEnergy(config, moved);
341 if (sourceCapacity == 0)
342 return DivideNearest(movedChemical, 1000);
343
344 const int64_t sourceHeat = source.energy * 1000 - ChemicalEnergy(config, source);
345 const fx::FxU128 numerator = fx::FxMulU64Full(fx::FxAbsU64(sourceHeat), HeatCapacity(config, moved));
346 const fx::FxDivResult share = fx::FxDivU128By64(numerator, sourceCapacity);
347 const uint64_t heat = share.quotient + (share.remainder * 2 >= sourceCapacity ? 1 : 0);
348 const int64_t movedHeat = fx::FxApplySign(heat, sourceHeat < 0);
349
350 return DivideNearest(movedHeat + movedChemical, 1000);
351 }
352
353 // 中身の一部を移す(風上 → 風下)。どちらかが開いた境界の外なら帳簿へ
354 void Transfer(GasBox& box, const std::vector<GasCell>& forced, std::vector<GasCell>& next,
355 const GasFace& face) {
356 if (face.fraction == 0)
357 return;
358
359 const bool forward = face.flow > 0;
360 const FaceSide donor = forward ? face.left : face.right;
361 const FaceSide receiver = forward ? face.right : face.left;
362 const GasCell& source = SideCell(box, forced, donor);
363 const uint32_t speciesCount = box.config.speciesCount;
364
365 GasCell moved;
366 moved.amounts = face.moved;
367 moved.energy = MovedEnergy(box.config, source, moved);
368 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
369 moved.momentum[axis] = fx::FxMulShiftS64(source.momentum[axis], (int64_t)face.fraction, 32);
370
371 // --- 引く側 ---
372 if (donor.kind == SideKind::Cell) {
373 GasCell& cell = next[donor.index];
374 for (uint32_t s = 0; s < speciesCount; ++s) {
375 assert(cell.amounts[s] >= moved.amounts[s]);
376 cell.amounts[s] -= moved.amounts[s];
377 }
378
379 cell.energy -= moved.energy;
380 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
381 cell.momentum[axis] -= moved.momentum[axis];
382 } else {
383 for (uint32_t s = 0; s < speciesCount; ++s)
384 box.ledger.amounts[s] += (int64_t)moved.amounts[s];
385
386 box.ledger.energy += moved.energy;
387 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
388 box.ledger.momentum[axis] += moved.momentum[axis];
389 }
390
391 // --- 足す側 ---
392 if (receiver.kind == SideKind::Cell) {
393 GasCell& cell = next[receiver.index];
394 for (uint32_t s = 0; s < speciesCount; ++s)
395 cell.amounts[s] += moved.amounts[s];
396
397 cell.energy += moved.energy;
398 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
399 cell.momentum[axis] += moved.momentum[axis];
400 } else {
401 for (uint32_t s = 0; s < speciesCount; ++s)
402 box.ledger.amounts[s] -= (int64_t)moved.amounts[s];
403
404 box.ledger.energy -= moved.energy;
405 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
406 box.ledger.momentum[axis] -= moved.momentum[axis];
407 }
408 }
409
410 // 面の力積: 左のセルから引き、右のセルへ足す。箱の外(壁・開いた境界)の側は帳簿へ
411 void ApplyImpulse(GasBox& box, std::vector<GasCell>& next, const GasFace& face) {
412 // --- 重さ(箱の外から働く力なので帳簿へ)---
413 if (face.left.kind == SideKind::Cell && face.weightLeft != 0) {
414 next[face.left.index].momentum[2] -= face.weightLeft;
415 box.ledger.momentum[2] -= face.weightLeft;
416 }
417
418 if (face.right.kind == SideKind::Cell && face.weightRight != 0) {
419 next[face.right.index].momentum[2] -= face.weightRight;
420 box.ledger.momentum[2] -= face.weightRight;
421 }
422
423 if (face.impulse == 0)
424 return;
425
426 if (face.left.kind == SideKind::Cell)
427 next[face.left.index].momentum[face.axis] -= face.impulse;
428 else
429 box.ledger.momentum[face.axis] += face.impulse;
430
431 if (face.right.kind == SideKind::Cell)
432 next[face.right.index].momentum[face.axis] += face.impulse;
433 else
434 box.ledger.momentum[face.axis] -= face.impulse;
435 }
436
437 // 出ていく割合の合計が 1 を超えるセルは、その面の割合を比例して縮める(07「共通の形」。負の中身を作らない)
438 void LimitOutflow(const GasBox& box, const std::vector<GasDerived>& derived, std::vector<GasFace>& faces) {
439 std::vector<uint64_t> outflow(box.cells.size(), 0);
440 for (GasFace& face : faces) {
441 face.fraction = RawFraction(box, derived, face);
442 const FaceSide donor = face.flow > 0 ? face.left : face.right;
443 if (face.fraction != 0 && donor.kind == SideKind::Cell)
444 outflow[donor.index] += face.fraction;
445 }
446
447 for (GasFace& face : faces) {
448 const FaceSide donor = face.flow > 0 ? face.left : face.right;
449 if (face.fraction == 0 || donor.kind != SideKind::Cell || outflow[donor.index] <= ONE_Q32)
450 continue;
451
452 face.fraction = fx::FxDivU128By64(fx::FxMulU64Full(face.fraction, ONE_Q32), outflow[donor.index])
453 .quotient;
454 }
455 }
456
457 // --- 移す成分の物質量(G2。07 §2.2)---
458
459 // 制限した勾配(a = 風上 − その外側、b = 風下 − 風上)。符号が違うか 0 なら 0(山と谷では延ばさず、新しい山と谷を作らない)。
460 // minmod は絶対値の小さい方、MC(monotonized central)は minmod(2a, 2b, (a + b) ÷ 2)。どちらも TVD(07 §2.2)
461 int64_t LimitedSlope(GasReconstruction reconstruction, int64_t a, int64_t b) {
462 if (a == 0 || b == 0 || (a > 0) != (b > 0))
463 return 0;
464
465 const int64_t sign = a > 0 ? 1 : -1;
466 const int64_t smaller = std::min(a * sign, b * sign);
467 if (reconstruction == GasReconstruction::Minmod)
468 return smaller * sign;
469
470 const int64_t central = ((a * sign) + (b * sign)) / 2;
471 return std::min(2 * smaller, central) * sign;
472 }
473
474 // 側の成分 s の割合(Q32。物質量 ÷ 全物質量。割り算は導く値の逆数 1 つ。ADR-0010)
475 int64_t SpeciesRatio(const GasBox& box, const std::vector<GasCell>& cells,
476 const std::vector<GasDerived>& derived, FaceSide side, uint32_t s) {
477 const uint64_t inverseAmount = SideDerived(box, derived, side).inverseAmount;
478
479 return (int64_t)MulShiftU64(SideCell(box, cells, side).amounts[s], inverseAmount, 32);
480 }
481
482 // 風上のセルの割合から面へ延ばす差 ½(1 − ν)σ(Q32)。σ = 制限した勾配、ν = 出ていく割合。
483 // (1 − ν)は小刻みの間に面を通る塊の平均にするため(van Leer の 1 段の MUSCL。ν ≤ 1 で新しい山と谷を作らない)。
484 // 風上が開いた境界の外(一様)か、外側が壁なら 0(1 次の風上)
485 int64_t HalfSlope(const GasBox& box, const std::vector<GasCell>& cells, const std::vector<GasDerived>& derived,
486 const GasFace& face, uint32_t s) {
487 const bool forward = face.flow > 0;
488 const FaceSide outer = forward ? face.leftOuter : face.rightOuter;
489 const FaceSide donor = forward ? face.left : face.right;
490 const FaceSide receiver = forward ? face.right : face.left;
491 if (donor.kind != SideKind::Cell || outer.kind == SideKind::Wall || receiver.kind == SideKind::Wall)
492 return 0;
493
494 const int64_t donorRatio = SpeciesRatio(box, cells, derived, donor, s);
495 const int64_t slope = LimitedSlope(box.config.reconstruction,
496 donorRatio - SpeciesRatio(box, cells, derived, outer, s),
497 SpeciesRatio(box, cells, derived, receiver, s) - donorRatio);
498
499 return fx::FxMulShiftS64(slope, (int64_t)(ONE_Q32 - face.fraction), 33);
500 }
501
502 // 面で移す成分 s の物質量(Q32 の µmol)= 割合 × 風上の物質量 + 延ばした差 × 割合 × 風上の全物質量。
503 // 延ばした割合は制限で風上の 0〜2 倍に収まるので負にならない(丸めの分は 0 で止める)
504 uint64_t MovedAmountQ32(const GasBox& box, const std::vector<GasCell>& cells,
505 const std::vector<GasDerived>& derived, const GasFace& face, uint32_t s) {
506 const GasCell& source = SideCell(box, cells, face.flow > 0 ? face.left : face.right);
507 const uint64_t moved = source.amounts[s] * face.fraction;
509 return moved;
510
511 const int64_t halfSlope = HalfSlope(box, cells, derived, face, s);
512 const uint64_t total = TotalAmount(box.config, source) * face.fraction;
513 const uint64_t correction = MulShiftU64(total, fx::FxAbsU64(halfSlope), 32);
514 if (halfSlope >= 0)
515 return moved + correction;
516
517 return moved > correction ? moved - correction : 0;
518 }
519
520 // Q32 の物質量を整数へ。端数は決定的な乱数と比べて丸める(期待値が端数どおり。切り捨てだと割合 × 物質量 < 1 の微量の成分が
521 // 永久に動かない〔D-428 の嘘〕。R6・D-431 と同じ考え)。乱数の丸めを切った設定なら切り捨て(G1)
522 uint64_t RoundAmount(const GasBox& box, uint64_t amountQ32, uint64_t hash) {
523 const uint64_t whole = amountQ32 >> 32;
524 if (!box.config.stochasticRounding)
525 return whole;
526
527 return whole + ((hash >> 32) < (amountQ32 & LOW_32_BITS) ? 1 : 0);
528 }
529
530 // 面ごとに移す成分の物質量。乱数は(世界のシード, 小刻みの通し番号, 面の番号, 用途)+ 成分(R6。並べた順に依存しない)
531 void ComputeMovedAmounts(const GasBox& box, const std::vector<GasCell>& cells,
532 const std::vector<GasDerived>& derived, uint64_t roundingTick,
533 std::vector<GasFace>& faces) {
534 for (GasFace& face : faces) {
535 if (face.fraction == 0)
536 continue;
537
538 const uint64_t faceHash = fx::FxHash64(box.config.randomSeed, roundingTick, face.id,
540 for (uint32_t s = 0; s < box.config.speciesCount; ++s) {
541 const uint64_t amountQ32 = MovedAmountQ32(box, cells, derived, face, s);
542 face.moved[s] = RoundAmount(box, amountQ32, fx::FxHashCombine(faceHash, s));
543 }
544 }
545 }
546
547 // 風上のセルごとに、成分ごとの移す物質量の合計が中身を超えたら比例して縮める(切り捨て)。
548 // 延ばした割合(風上の最大 2 倍)と丸め上げで、出ていく割合の合計が 1 に近いセルでは超えうる
549 void LimitMovedAmounts(const GasBox& box, const std::vector<GasCell>& cells, std::vector<GasFace>& faces) {
550 const uint32_t speciesCount = box.config.speciesCount;
551 std::vector<std::array<uint64_t, GAS_MAX_SPECIES>> outgoing(box.cells.size());
552 for (const GasFace& face : faces) {
553 const FaceSide donor = face.flow > 0 ? face.left : face.right;
554 if (face.fraction == 0 || donor.kind != SideKind::Cell)
555 continue;
556
557 for (uint32_t s = 0; s < speciesCount; ++s)
558 outgoing[donor.index][s] += face.moved[s];
559 }
560
561 for (GasFace& face : faces) {
562 const FaceSide donor = face.flow > 0 ? face.left : face.right;
563 if (face.fraction == 0 || donor.kind != SideKind::Cell)
564 continue;
565
566 const GasCell& source = cells[donor.index];
567 for (uint32_t s = 0; s < speciesCount; ++s) {
568 const uint64_t total = outgoing[donor.index][s];
569 if (total <= source.amounts[s])
570 continue;
571
572 face.moved[s] = fx::FxDivU128By64(fx::FxMulU64Full(face.moved[s], source.amounts[s]), total)
573 .quotient;
574 }
575 }
576 }
577
578 std::vector<GasDerived> DeriveAll(const GasBox& box) {
579 std::vector<GasDerived> derived(box.cells.size());
580 for (uint32_t index = 0; index < box.cells.size(); ++index)
581 derived[index] = DeriveGasCell(box, box.cells[index], CellLayer(box, index));
582
583 return derived;
584 }
585
586 void Substep(GasBox& box, const std::vector<GasFace>& layout, uint64_t roundingTick) {
587 // --- (1) 導く値 ---
588 const std::vector<GasDerived> derived = DeriveAll(box);
589
590 // --- (2) 面の力(圧力の力積・重さ)を先に当てる ---
591 std::vector<GasFace> faces = layout;
592 for (GasFace& face : faces)
593 ComputeFaceForce(box, derived, face);
594
595 std::vector<GasCell> forced = box.cells;
596 for (const GasFace& face : faces)
597 ApplyImpulse(box, forced, face);
598
599 // --- (3) 力を当てた後の運動量で面の質量の流れ・出ていく割合を抑える ---
600 // 前進・後退の順: 力と流れを同じ古い値から出すと(前進 Euler)、重さと音の波が組んだゆっくりした振動が育つ
601 // (小刻み 4・c̃ 30 m/s で 10^4 刻みに 10 倍。試作で見た。07 §2.1)
602 for (GasFace& face : faces)
603 ComputeFaceFlow(box, derived, forced, face);
604
605 LimitOutflow(box, derived, faces);
606
607 // --- (4) 移す成分の物質量(MUSCL で面へ延ばした割合・乱数の丸め。G2)---
608 ComputeMovedAmounts(box, forced, derived, roundingTick, faces);
609 LimitMovedAmounts(box, forced, faces);
610
611 // --- (5) 風上の中身(力を当てた後)を移す ---
612 std::vector<GasCell> next = forced;
613 for (const GasFace& face : faces)
614 Transfer(box, forced, next, face);
615
616 box.cells = std::move(next);
617 }
618
619 } // namespace
620
622 assert(config.substeps > 0 && config.soundSpeedMmPerS > 0);
623 const uint64_t substeps = config.substeps;
624 GasCoefficients coefficients;
625
626 // mg/µmol = M(mg/mol)÷ 1e6
627 for (uint32_t s = 0; s < config.speciesCount; ++s)
628 coefficients.massPerAmount[s] = ((uint64_t)config.species[s].molarMass << 32) / MICRO;
629
630 // 中心の流れ: F(mg)=(P_左 + P_右)÷ 2 × 2^-20 × Δt ÷ Δx、Δt ÷ Δx = 1 ÷ (30 × 小刻み)(s/m)
631 coefficients.centralFlow = ((uint64_t)1 << 31) / (30 * substeps);
632
633 // 圧力の拡散: F(mg)= Δp̃(µPa)× 1e-6 × A × Δt ÷ (2 c̃)× 1e6 = Δp̃ × 25 ÷ (12 × 小刻み × c̃(mm/s))
634 coefficients.pressureFlow = (25 * ((uint64_t)1 << 40)) / (12 * substeps * config.soundSpeedMmPerS);
635
636 // 面の力積: (p̃_左 + p̃_右)÷ 2 × 1e-6 × A × Δt × 1e6(mg/kg)× 2^20 =(和)× 2^15 ÷ (15 × 小刻み)
637 coefficients.pressureImpulse = ((uint64_t)1 << 31) / (15 * substeps);
638
639 // 浮力: Δm(mg)× g × Δt × 2^20 = Δm × 9.80665 × 2^20 ÷ (60 × 小刻み)
640 coefficients.gravityImpulse = (GRAVITY_MICROMETER_PER_SECOND2 << 36) / (60 * substeps * MICRO);
641
642 return coefficients;
643 }
644
645 GasCell MakeGasCell(const GasConfig& config, std::span<const uint64_t> amounts, uint32_t temperatureMilliKelvin) {
646 GasCell cell;
647 for (uint32_t s = 0; s < config.speciesCount && s < amounts.size(); ++s)
648 cell.amounts[s] = amounts[s];
649
650 // 熱(µJ)= 熱容量(nJ/K)× T(mK)÷ 1e6
651 const uint64_t heat = fx::FxDivU128By64(fx::FxMulU64Full(HeatCapacity(config, cell), temperatureMilliKelvin),
652 MICRO)
653 .quotient;
654 cell.energy = fx::FxDivS64((int64_t)heat + ChemicalEnergy(config, cell), 1000);
655
656 return cell;
657 }
658
659 std::vector<GasCell> MakeHydrostaticReference(const GasConfig& config, std::span<const uint32_t> fractionsQ32,
660 uint32_t temperatureMilliKelvin,
661 uint64_t surfacePressureMicroPascal) {
662 const GasCoefficients coefficients = MakeGasCoefficients(config);
663 std::vector<GasCell> layers;
664 uint64_t pressure = surfacePressureMicroPascal;
665
666 for (uint32_t z = 0; z < config.sizeZ; ++z) {
667 // --- 物質量 n = p ÷ (T × R ÷ V)---
668 const uint64_t divisor = PRESSURE_PER_AMOUNT_TEMPERATURE_Q32 * temperatureMilliKelvin;
669 const uint64_t total = fx::FxDivU128By64(fx::FxMulU64Full(pressure, ONE_Q32), divisor).quotient;
670
671 std::array<uint64_t, GAS_MAX_SPECIES> amounts = {};
672 for (uint32_t s = 0; s < config.speciesCount && s < fractionsQ32.size(); ++s)
673 amounts[s] = MulShiftU64(total, fractionsQ32[s], 32);
674
675 const GasCell cell = MakeGasCell(config, amounts, temperatureMilliKelvin);
676 layers.push_back(cell);
677
678 // --- 上の層の圧力 = この層の重さの分だけ下げる(釣り合いは近似でよい: 力は基準との差で入れるので、基準そのものは止まっている)---
679 const uint64_t mass = CellMass(config, coefficients, cell);
680 pressure -= mass * HYDROSTATIC_MICROPASCAL_PER_MASS_X10000 / 10000;
681 }
682
683 return layers;
684 }
685
686 GasBox MakeGasBox(const GasConfig& config, std::vector<GasCell> reference) {
687 assert(reference.size() == config.sizeZ);
688 GasBox box;
689 box.config = config;
690 box.coefficients = MakeGasCoefficients(config);
691 box.reference = std::move(reference);
692
693 // --- 基準の導く値(p̃ = 0)と開いた境界の外(基準 + 風)---
694 for (const GasCell& layer : box.reference) {
695 box.referenceDerived.push_back(DeriveRaw(config, box.coefficients, layer));
696
697 GasCell ghost = layer;
698 const auto mass = (int64_t)box.referenceDerived.back().mass;
699 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
700 ghost.momentum[axis] = mass * config.windVelocity[axis];
701
702 box.ghost.push_back(ghost);
703 }
704
705 // --- α = c̃² ÷ c_iso²、c_iso² = p ÷ ρ = p(µPa)× 0.125 ÷ m(mg)(地面の層)---
706 // α(Q32)= c̃(mm/s)² × m × 8 × 2^32 ÷ (p × 1e6)
707 const GasDerived& ground = box.referenceDerived.front();
708 const uint64_t sound = config.soundSpeedMmPerS;
709 const fx::FxU128 numerator = fx::FxMulU64Full(sound * sound * 8 * ground.mass, ONE_Q32);
710 box.coefficients.soundScale = fx::FxDivU128By64(numerator, (uint64_t)ground.pressure * MICRO).quotient;
712
713 // --- セル = 層の基準 + 風 ---
714 box.cells.resize((size_t)config.sizeX * config.sizeY * config.sizeZ);
715 for (uint32_t z = 0; z < config.sizeZ; ++z) {
716 for (uint32_t y = 0; y < config.sizeY; ++y) {
717 for (uint32_t x = 0; x < config.sizeX; ++x)
718 box.cells[box.Index(x, y, z)] = box.ghost[z];
719 }
720 }
721
722 return box;
723 }
724
725 GasDerived DeriveGasCell(const GasBox& box, const GasCell& cell, uint32_t z) {
726 GasDerived derived = DeriveRaw(box.config, box.coefficients, cell);
727
728 // p̃ = α(p − p_基準)。基準より熱いセルは α × T_基準 ÷ T(実効の音速を c̃ に抑える。c_iso² ∝ T)
729 const int64_t deviation = derived.pressure - box.referenceDerived[z].pressure;
730 int64_t scaled = fx::FxMulShiftS64(deviation, (int64_t)box.coefficients.soundScale, 32);
731 const uint32_t soundTemperature = box.coefficients.soundTemperature;
732 if (derived.temperature > soundTemperature)
733 scaled = fx::FxDivS64(fx::FxMulShiftS64(scaled, soundTemperature, 0), derived.temperature);
734
735 derived.scaledPressure = scaled;
736
737 return derived;
738 }
739
740 int64_t GasVelocity(const GasBox& box, const GasCell& cell, uint32_t axis) {
741 const uint64_t mass = CellMass(box.config, box.coefficients, cell);
742 if (mass == 0)
743 return 0;
744
745 return fx::FxDivS64(cell.momentum[axis], (int64_t)mass);
746 }
747
748 void StepGas(GasBox& box) {
749 const std::vector<GasFace> layout = EnumerateFaces(box);
750 for (uint32_t substep = 0; substep < box.config.substeps; ++substep)
751 Substep(box, layout, (box.tick * box.config.substeps) + substep);
752
753 ++box.tick;
754 }
755
756 GasTotals SumGas(const GasBox& box) {
757 GasTotals totals;
758 for (const GasCell& cell : box.cells) {
759 for (uint32_t s = 0; s < box.config.speciesCount; ++s)
760 totals.amounts[s] += cell.amounts[s];
761
762 totals.energy += cell.energy;
763 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
764 totals.momentum[axis] += cell.momentum[axis];
765 }
766
767 return totals;
768 }
769
770 uint64_t HashGas(const GasBox& box) {
771 uint64_t hash = fx::FxMix64(box.tick);
772 for (const GasCell& cell : box.cells) {
773 for (uint32_t s = 0; s < box.config.speciesCount; ++s)
774 hash = fx::FxHashCombine(hash, cell.amounts[s]);
775
776 hash = fx::FxHashCombine(hash, (uint64_t)cell.energy);
777 for (uint32_t axis = 0; axis < GAS_AXES; ++axis)
778 hash = fx::FxHashCombine(hash, (uint64_t)cell.momentum[axis]);
779 }
780
781 return hash;
782 }
783
784} // namespace bicameral::sim
FaceSide BoundarySide(GasBoundary boundary, uint32_t z)
void ComputeFaceWeight(const GasBox &box, const std::vector< GasDerived > &derived, GasFace &face)
int64_t LimitedSlope(GasReconstruction reconstruction, int64_t a, int64_t b)
GasFace MakeFace(const GasBox &box, std::array< uint32_t, GAS_AXES > cell, uint32_t axis, bool high)
std::vector< GasFace > EnumerateFaces(const GasBox &box)
void ComputeFaceForce(const GasBox &box, const std::vector< GasDerived > &derived, GasFace &face)
int64_t Excess(const GasBox &box, const std::vector< GasDerived > &derived, FaceSide side)
uint32_t CellTemperature(const GasConfig &config, const GasCell &cell)
int64_t ChemicalEnergy(const GasConfig &config, const GasCell &cell)
uint64_t TotalAmount(const GasConfig &config, const GasCell &cell)
void ApplyImpulse(GasBox &box, std::vector< GasCell > &next, const GasFace &face)
std::vector< GasDerived > DeriveAll(const GasBox &box)
uint64_t RoundAmount(const GasBox &box, uint64_t amountQ32, uint64_t hash)
const GasCell & SideCell(const GasBox &box, const std::vector< GasCell > &cells, FaceSide side)
uint64_t MulShiftU64(uint64_t a, uint64_t b, uint32_t shift)
void ComputeFaceFlow(const GasBox &box, const std::vector< GasDerived > &derived, const std::vector< GasCell > &forced, GasFace &face)
int64_t DrivingPressure(const GasBox &box, const std::vector< GasDerived > &derived, const GasFace &face)
int64_t SpeciesRatio(const GasBox &box, const std::vector< GasCell > &cells, const std::vector< GasDerived > &derived, FaceSide side, uint32_t s)
void LimitMovedAmounts(const GasBox &box, const std::vector< GasCell > &cells, std::vector< GasFace > &faces)
void Substep(GasBox &box, const std::vector< GasFace > &layout, uint64_t roundingTick)
uint64_t HeatCapacity(const GasConfig &config, const GasCell &cell)
void LimitOutflow(const GasBox &box, const std::vector< GasDerived > &derived, std::vector< GasFace > &faces)
uint64_t RawFraction(const GasBox &box, const std::vector< GasDerived > &derived, const GasFace &face)
uint32_t CellLayer(const GasBox &box, uint32_t index)
FaceSide SideAt(const GasBox &box, std::array< uint32_t, GAS_AXES > cell, uint32_t axis, int64_t coordinate)
int64_t CellPressure(const GasConfig &config, const GasCell &cell, uint32_t temperature)
void Transfer(GasBox &box, const std::vector< GasCell > &forced, std::vector< GasCell > &next, const GasFace &face)
int64_t MovedEnergy(const GasConfig &config, const GasCell &source, const GasCell &moved)
void ComputeMovedAmounts(const GasBox &box, const std::vector< GasCell > &cells, const std::vector< GasDerived > &derived, uint64_t roundingTick, std::vector< GasFace > &faces)
uint64_t CellMass(const GasConfig &config, const GasCoefficients &coefficients, const GasCell &cell)
GasDerived DeriveRaw(const GasConfig &config, const GasCoefficients &coefficients, const GasCell &cell)
uint32_t AxisSize(const GasConfig &config, uint32_t axis)
std::array< uint32_t, GAS_AXES > CellCoordinate(const GasBox &box, uint32_t index)
const GasDerived & SideDerived(const GasBox &box, const std::vector< GasDerived > &derived, FaceSide side)
int64_t HalfSlope(const GasBox &box, const std::vector< GasCell > &cells, const std::vector< GasDerived > &derived, const GasFace &face, uint32_t s)
uint64_t MovedAmountQ32(const GasBox &box, const std::vector< GasCell > &cells, const std::vector< GasDerived > &derived, const GasFace &face, uint32_t s)
GasDerived DeriveGasCell(const GasBox &box, const GasCell &cell, uint32_t z)
GasCoefficients MakeGasCoefficients(const GasConfig &config)
uint64_t HashGas(const GasBox &box)
int64_t GasVelocity(const GasBox &box, const GasCell &cell, uint32_t axis)
constexpr uint32_t GAS_AXES
void StepGas(GasBox &box)
GasBox MakeGasBox(const GasConfig &config, std::vector< GasCell > reference)
GasTotals SumGas(const GasBox &box)
std::vector< GasCell > MakeHydrostaticReference(const GasConfig &config, std::span< const uint32_t > fractionsQ32, uint32_t temperatureMilliKelvin, uint64_t surfacePressureMicroPascal)
GasCell MakeGasCell(const GasConfig &config, std::span< const uint64_t > amounts, uint32_t temperatureMilliKelvin)
uint32_t Index(uint32_t x, uint32_t y, uint32_t z) const
std::vector< GasCell > reference
std::vector< GasCell > ghost
std::vector< GasCell > cells
std::vector< GasDerived > referenceDerived
GasCoefficients coefficients
std::array< uint64_t, GAS_MAX_SPECIES > amounts
std::array< int64_t, GAS_AXES > momentum
std::array< uint64_t, GAS_MAX_SPECIES > massPerAmount
std::array< GasBoundary, GAS_AXES > boundary
std::array< int32_t, GAS_AXES > windVelocity
GasReconstruction reconstruction
std::array< GasSpecies, GAS_MAX_SPECIES > species
std::array< int64_t, GAS_MAX_SPECIES > amounts
std::array< int64_t, GAS_AXES > momentum
std::array< int64_t, GAS_AXES > momentum
std::array< uint64_t, GAS_MAX_SPECIES > amounts