Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Numos.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
<Project Path="tests\Numos.API.Tests\Numos.API.Tests.csproj"/>
<Project Path="tests\Numos.CoreSim.IntegrationTests\Numos.CoreSim.IntegrationTests.csproj"/>
<Project Path="tests\Numos.CoreSim.Tests\Numos.CoreSim.Tests.csproj"/>
<Project Path="tests\Numos.Headless.Tests\Numos.Headless.Tests.csproj"/>
<Project Path="tests\Numos.Maths.Tests\Numos.Maths.Tests.csproj"/>
<Project Path="tests\Numos.SimDrawer.Tests\Numos.SimDrawer.Tests.csproj"/>
</Folder>

<Project Path="src\Numos.API.Dangerous\Numos.API.Dangerous.csproj"/>
<Project Path="src\Numos.API\Numos.API.csproj"/>
<Project Path="src\Numos.CoreSim\Numos.CoreSim.csproj"/>
<Project Path="src\Numos.Headless\Numos.Headless.csproj"/>
<Project Path="src\Numos.Maths\Numos.Maths.csproj"/>
<Project Path="src\Numos.SimDrawer\Numos.SimDrawer.csproj"/>
<Project Path="src\Numos.Viewer\Numos.Viewer.csproj"/>
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ The project will follow a regular semantic versioning structure when I feel comf
- Engine-agnostic, with a supported `Numos.API` facade over an internal simulation kernel
- Multithreaded intra-chunk advection and thermodynamics
- Singlethreaded cross-chunk boundary flow
- Simplified ideal gas law (`P = n \cdot T`)
- Constant volume voxels
- Ordered solver pipeline with replaceable/disableable built-in stages, custom delegates, and typed solver-owned configuration
- Separate supported solver context and opt-in `Numos.API.Dangerous` live-span context
- Ideal-gas pressure in pascals (`P = nRT/V`) with configurable, uniform voxel volume
- Sensible internal-energy transport using per-species molar heat capacity at constant volume
- Simulation-owned `IGasMixture` containers and sandboxed live voxel mixtures for canisters, pumps, and tools
- Attempts at being trimmable and Native AOT-compatible

## Bug Reports & Contributions
Expand All @@ -22,6 +25,18 @@ See `CONTRIBUTING.md` before contributing.

## Documentation
Documentation for APIs and the project itself is available under `/docs`.
Tracked numerical and lifecycle limitations are documented in [Known Issues](docs/known_issues.md).

### Headless debugging

`Numos.Headless` runs reproducible simulation experiments from newline-delimited JSON without opening the graphical
viewer. It can read commands interactively from standard input or replay a checked-in script, emitting one compact
JSON response per command for tools and automated comparisons. See the [headless runner guide](docs/headless_runner.md)
and the checked-in [two-voxel flow](examples/headless/two-voxel-flow.jsonl) and
[16×16 equilibrium](examples/headless/16x16-equilibrium.jsonl) experiments. The
[relative-pressure snap](examples/headless/16x16-relative-snap-equilibrium.jsonl) and
[mixed-gas relative-pressure snap](examples/headless/16x16-relative-snap-mixed-gas-equilibrium.jsonl) scenarios
exercise the production `0.1%` snap tolerance with the normal `0.1` Pa/tick minimum-transfer setting.

## Copyright, Credits & License
Numos is licensed under the MIT license. See `LICENSE.TXT` for more info.
Expand Down
676 changes: 523 additions & 153 deletions docs/atmospherics_technical_documentation.md

Large diffs are not rendered by default.

307 changes: 307 additions & 0 deletions docs/headless_runner.md

Large diffs are not rendered by default.

307 changes: 307 additions & 0 deletions docs/known_issues.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions examples/headless/two-voxel-flow.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{"protocolVersion":1,"id":"create","op":"createSimulation","name":"Two-voxel flow","dimensions":{"x":2,"y":1,"z":1},"config":{"defaultTemperatureFallbackK":300,"defaultMolarHeatCapacityAtConstantVolume":1,"voxelVolumeM3":8.31446262,"saturationReferencePressurePa":1000,"defaultDiffusionCoefficient":0,"bulkFlowCoefficient":0.25,"bulkFlowDamping":0.5,"lowPressureDeltaThresholdPa":5,"minimumPressureTransferPa":0,"vacuumThresholdPa":0,"sleepThreshold":2147483647,"sleepEpsilonPa":0,"voxelSnapPressureRelativeEpsilon":0.001,"voxelSnappingEnabled":false,"voxelSnapTemperatureEpsilonK":0.01,"voxelSnapMoleFractionEpsilon":0.001,"thermalConductance":0.05,"condensationRateFactor":0.5,"maxPressureTransferFractionPerNeighbor":0.16}}
{"protocolVersion":1,"id":"gas","op":"addGas","gas":{"name":"First","molarHeatCapacityAtConstantVolume":1,"boilingPointK":0,"condensationEnabled":false,"molarEnthalpyOfVaporization":0,"liquidId":-1,"diffusionCoefficient":0}}
{"protocolVersion":1,"id":"chunk","op":"addChunk","position":{"x":0,"y":0,"z":0},"classification":1}
{"protocolVersion":1,"id":"temperature-left","op":"setVoxelTemperature","position":{"x":0,"y":0,"z":0},"voxel":{"x":0,"y":0,"z":0},"temperatureK":300}
{"protocolVersion":1,"id":"temperature-right","op":"setVoxelTemperature","position":{"x":0,"y":0,"z":0},"voxel":{"x":1,"y":0,"z":0},"temperatureK":300}
{"protocolVersion":1,"id":"inject","op":"injectGas","position":{"x":0,"y":0,"z":0},"voxel":{"x":0,"y":0,"z":0},"gasId":0,"moles":2,"temperatureK":300}
{"protocolVersion":1,"id":"before","op":"observe","includeVoxels":true}
{"protocolVersion":1,"id":"tick-1","op":"tick","count":1}
{"protocolVersion":1,"id":"after","op":"observe","includeVoxels":true}
{"protocolVersion":1,"id":"exit","op":"exit"}
13 changes: 8 additions & 5 deletions src/Numos.API.Dangerous/AtmosDangerousApi.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using Numos.CoreSim;

namespace Numos.API.Dangerous;

/// <summary>
Expand All @@ -20,10 +18,15 @@ namespace Numos.API.Dangerous;
/// </remarks>
public readonly struct AtmosDangerousApi
{
private readonly AtmosKernel _kernel;
private readonly AtmosSimulation _simulation;

internal AtmosDangerousApi(AtmosKernel kernel)
internal AtmosDangerousApi(AtmosSimulation simulation)
{
_kernel = kernel;
_simulation = simulation;
}

/// <summary>
/// Gets the dangerous registration surface for the simulation's shared solver pipeline.
/// </summary>
public AtmosDangerousSolverPipeline Solvers => new(_simulation);
}
193 changes: 193 additions & 0 deletions src/Numos.API.Dangerous/AtmosDangerousSolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using Numos.CoreSim;
using Numos.CoreSim.Datatypes.Primitives;
using Numos.CoreSim.Solvers;
using Numos.Maths;

namespace Numos.API.Dangerous;

/// <summary>
/// A low-level solver that operates on live simulation storage.
/// </summary>
/// <remarks>
/// The context and its views are valid only for the callback. Numos performs no validation or cache repair
/// after arbitrary span writes; the solver is responsible for maintaining all invariants it touches.
/// </remarks>
public delegate void AtmosDangerousSolver(AtmosDangerousSolverContext context);

/// <summary>
/// A low-level custom solver that owns its strongly typed configuration.
/// </summary>
/// <typeparam name="TConfig">The solver-specific reference type retained by the solver.</typeparam>
/// <remarks>
/// This interface provides configuration ownership only. Its solver still receives live, unchecked storage
/// and therefore has the same compatibility and invariant-maintenance responsibilities as
/// <see cref="AtmosDangerousSolver" />. Registration does not transfer ownership or disposal responsibility.
/// </remarks>
public interface IAtmosDangerousSolver<out TConfig> where TConfig : class
{
/// <summary>The configuration owned by this solver.</summary>
TConfig Config { get; }

/// <summary>Executes the solver against live simulation storage.</summary>
/// <param name="context">The unchecked simulation surface for the current tick.</param>
void Solve(AtmosDangerousSolverContext context);
}

/// <summary>
/// Low-level state supplied to a dangerous solver for one fixed tick.
/// </summary>
public readonly ref struct AtmosDangerousSolverContext
{
private readonly AtmosSolverExecutionContext _context;

internal AtmosDangerousSolverContext(AtmosSolverExecutionContext context)
{
_context = context;
}

/// <summary>The one-based tick number currently being solved.</summary>
public int TickCount => _context.TickCount;

/// <summary>The mutable configuration reference captured at the beginning of this tick.</summary>
/// <remarks>Replacing the simulation configuration during this tick does not replace this reference.</remarks>
public AtmosConfig Config => _context.Configuration;

/// <summary>The number of chunks in the tick snapshot.</summary>
public int ChunkCount => _context.Chunks.Length;

/// <summary>Returns a live view of a chunk in the tick snapshot.</summary>
public AtmosDangerousChunk GetChunk(int index)
{
return new AtmosDangerousChunk(_context.Chunks[index]);
}

/// <summary>
/// Injects gas using the normalized SHC and pressure coefficient captured for this tick.
/// </summary>
public void InjectGasToVoxel(int chunkIndex, ushort localVoxelIndex, int gasId, float moles,
float temperature)
{
AtmosChunk chunk = _context.Chunks[chunkIndex];
int roomId = chunk.VoxelRoomMap[localVoxelIndex];
if (roomId == VoxelClassification.RoomSolid || roomId == VoxelClassification.RoomVoid)
return;

chunk.WakeVoxel(localVoxelIndex);
GasInjectionSolver.InjectDuringTick(
chunk, localVoxelIndex, gasId, moles, temperature, _context.TickConfig);
}
}

/// <summary>
/// Live, unchecked views over one solver chunk.
/// </summary>
public readonly ref struct AtmosDangerousChunk
{
private readonly AtmosChunk _chunk;

internal AtmosDangerousChunk(AtmosChunk chunk)
{
_chunk = chunk;
}

/// <summary>The chunk-grid position.</summary>
public Int3 Position => _chunk.GridPosition;

/// <summary>The chunk dimensions.</summary>
public Int3 Dimensions => _chunk.Dimensions;

/// <summary>The number of addressable voxels.</summary>
public int VoxelCount => _chunk.VoxelCount;

/// <summary>Whether built-in stages that honor sleeping currently process this chunk.</summary>
public bool IsAwake => _chunk.IsAwake;

/// <summary>Gets or sets the unchecked sleep counter.</summary>
public int SleepTimer { get => _chunk.SleepTimer; set => _chunk.SleepTimer = value; }

/// <summary>The number of active gas channels.</summary>
public int ActiveGasCount => _chunk.ActiveGasCount;

/// <summary>The number of valid active-air indices.</summary>
public int ActiveAirCount => _chunk.ActiveAirCount;

/// <summary>The number of active room IDs.</summary>
public int ActiveRoomCount => _chunk.ActiveRoomCount;

/// <summary>Live per-voxel temperature storage.</summary>
public Span<float> Temperature => _chunk.Temperature.AsSpan();

/// <summary>Live per-voxel pressure-cache storage.</summary>
public Span<float> TotalPressure => _chunk.TotalPressure.AsSpan();

/// <summary>Live per-voxel heat-capacity-cache storage.</summary>
public Span<float> TotalHeatCapacity => _chunk.TotalHeatCapacity.AsSpan();

/// <summary>Live per-voxel room-classification storage.</summary>
public Span<int> VoxelRoomMap => _chunk.VoxelRoomMap.AsSpan();

/// <summary>Live active-air indices, limited to the current valid count.</summary>
public Span<ushort> ActiveAirIndices => _chunk.ActiveAirIndices.AsSpan(0, _chunk.ActiveAirCount);

/// <summary>Live active-room IDs, limited to the current valid count.</summary>
public Span<int> ActiveRoomIds => _chunk.ActiveRoomIds.AsSpan(0, _chunk.ActiveRoomCount);

/// <summary>Returns a live gas-channel view by active-channel index.</summary>
public AtmosDangerousGasChannel GetGasChannel(int index)
{
if ((uint)index >= (uint)_chunk.ActiveGasCount)
throw new ArgumentOutOfRangeException(nameof(index));
return new AtmosDangerousGasChannel(_chunk.ActiveGases[index], _chunk.VoxelCount);
}

/// <summary>Maps unchecked local coordinates to a flat voxel index.</summary>
public ushort GetVoxelIndex(int x, int y, int z)
{
return _chunk.GetIndex(x, y, z);
}

/// <summary>Wakes and activates a room using the chunk topology operation.</summary>
public void WakeRoom(int roomId)
{
_chunk.WakeRoom(roomId);
}

/// <summary>Puts the chunk to sleep using the chunk lifecycle operation.</summary>
public void Sleep()
{
_chunk.Sleep();
}

/// <summary>Rebuilds the active-air index after raw topology edits.</summary>
public void RebuildActiveAirIndices()
{
_chunk.RebuildActiveAirIndices();
}

/// <summary>Advances the presentation revision after raw observable writes.</summary>
public void MarkChanged()
{
_chunk.MarkChanged();
}
}

/// <summary>
/// Live, unchecked view over one structure-of-arrays gas channel.
/// </summary>
public readonly ref struct AtmosDangerousGasChannel
{
private readonly GasChannel _channel;
private readonly int _voxelCount;

internal AtmosDangerousGasChannel(GasChannel channel, int voxelCount)
{
_channel = channel;
_voxelCount = voxelCount;
}

/// <summary>The gas registry ID represented by this channel.</summary>
public int GasId => _channel.GasId;

/// <summary>Live per-voxel mole storage for this gas.</summary>
public Span<float> Moles => _channel.Moles.AsSpan(0, _voxelCount);
}
75 changes: 75 additions & 0 deletions src/Numos.API.Dangerous/AtmosDangerousSolverPipeline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using JetBrains.Annotations;
using Numos.CoreSim;
using Numos.CoreSim.Solvers;

namespace Numos.API.Dangerous;

/// <summary>
/// Registers custom stages that receive unchecked live simulation views.
/// </summary>
/// <remarks>
/// Pipeline edits made by a running stage take effect on the next tick. Registered custom solver instances
/// remain caller-owned and are not disposed by the pipeline.
/// </remarks>
public readonly struct AtmosDangerousSolverPipeline
{
private readonly AtmosSimulation _simulation;

internal AtmosDangerousSolverPipeline(AtmosSimulation simulation)
{
_simulation = simulation;
}

/// <summary>Appends a dangerous custom solver to the shared pipeline.</summary>
[PublicAPI]
public void Register(string name, AtmosDangerousSolver solver)
{
ArgumentNullException.ThrowIfNull(solver);
_simulation.Kernel.RegisterSolver(name, SolverStepKind.Dangerous,
context => solver(new AtmosDangerousSolverContext(context)));
}

/// <summary>Appends a dangerous solver that owns strongly typed configuration.</summary>
[PublicAPI]
public void Register<TConfig>(string name, IAtmosDangerousSolver<TConfig> solver) where TConfig : class
{
ArgumentNullException.ThrowIfNull(solver);
Register(name, solver.Solve);
}

/// <summary>Registers a dangerous solver immediately before an existing stage.</summary>
[PublicAPI]
public void RegisterBefore(string existingName, string name, AtmosDangerousSolver solver)
{
ArgumentNullException.ThrowIfNull(solver);
_simulation.Kernel.RegisterSolverBefore(existingName, name, SolverStepKind.Dangerous,
context => solver(new AtmosDangerousSolverContext(context)));
}

/// <summary>Registers a configured dangerous solver immediately before an existing stage.</summary>
[PublicAPI]
public void RegisterBefore<TConfig>(string existingName, string name, IAtmosDangerousSolver<TConfig> solver)
where TConfig : class
{
ArgumentNullException.ThrowIfNull(solver);
RegisterBefore(existingName, name, solver.Solve);
}

/// <summary>Registers a dangerous solver immediately after an existing stage.</summary>
[PublicAPI]
public void RegisterAfter(string existingName, string name, AtmosDangerousSolver solver)
{
ArgumentNullException.ThrowIfNull(solver);
_simulation.Kernel.RegisterSolverAfter(existingName, name, SolverStepKind.Dangerous,
context => solver(new AtmosDangerousSolverContext(context)));
}

/// <summary>Registers a configured dangerous solver immediately after an existing stage.</summary>
[PublicAPI]
public void RegisterAfter<TConfig>(string existingName, string name, IAtmosDangerousSolver<TConfig> solver)
where TConfig : class
{
ArgumentNullException.ThrowIfNull(solver);
RegisterAfter(existingName, name, solver.Solve);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static class AtmosSimulationDangerousExtensions
public static AtmosDangerousApi Dangerous(this AtmosSimulation simulation)
{
ArgumentNullException.ThrowIfNull(simulation);
return new AtmosDangerousApi(simulation.Kernel);
_ = simulation.Kernel;
return new AtmosDangerousApi(simulation);
}
}
Loading
Loading