A physical accurate worldbuilder.
Find a file
Fabian Schober 25b06cb6f3 Bring back a test suite
70 tests, `make test`, ~0.8s, stdlib unittest -- restoring the runner the
deleted Makefile target used. No new dependency: subTest covers the
parametrized tables, and pytest would buy nothing here.

The suite loads the real seed world and asserts what comes out rather than
mocking collaborators. The physics is a pure function of default.yml, so
running it is the test, and most of this is a transcription of numbers
CLAUDE.md already presented as regression baselines -- "Sol must land at
1.000 RSol / 5770.8 K ... that is the regression test" described a suite
nobody had written down.

Covered: the physics anchors and the perturbation sweeps that prove the
habitability response stays graded; the loader's round-trips, format
identity and error taxonomy; god.scope() isolation over 8 threads; the HTTP
contracts including a 500 still carrying its CORS header; the CLI; and the
create verb's parent rules. make test-packaging is separate (~15s) and
builds a wheel to run the CLI and API from with no source tree.

Two hazards the fixtures close: a test reaching cli.main must call
clean_process_world, since BaseService.run loads into the ambient registry
by design; and anything that could reach an unanswered question patches
sys.stdin.isatty, or InteractionService.ask blocks forever instead of
failing when the suite is run from a terminal.

Writing it found three real problems:

- create Moon sol ran the orbit cascade before discovering a Star cannot
  host a moon, so it created an orbit and then refused, leaving it behind
  in a world the caller was told nothing had been added to. Legality is now
  decided in a first pass, before anything is created.
- create asked its questions before validating --set names and --id, so a
  typo surfaced only after three prompts had been answered.
- magnetosphere.py documented Earth's magnetopause at 9.6 planetary radii
  and the bow shock at 12.5, in five places including user-facing text
  mirrored into the docs site. The code computes 9.52 and 12.37.

Mutation-probed rather than trusted green: feeding Star.radius kilograms
fails 6 tests, disabling the MRO walk in api/errors.py fails 5, and
removing the moons backref, the wrong-dimension unit check or the
non-finite float strip each fails its own. That found a real gap -- dropping
FACTOR_FLOOR failed nothing, because the oxygen cases never approach the
floor. A cold orbit does: 0.251 with the clamp, exactly 0.000000 without,
which is the binary collapse it exists to stop. That test now exists.

Claude-Session: https://claude.ai/code/session_01YPjQ97SEywbgUq5f66apv4
2026-09-09 15:00:16 +02:00
docker Pyproject update 2026-09-05 12:10:47 +02:00
frontend Bring back a test suite 2026-09-09 15:00:16 +02:00
notebooks Prototype of Star Class, before LLM use 2026-07-22 12:41:28 +02:00
ressources Document the CLI, every frontend tab, and the property docs site 2026-09-05 12:10:23 +02:00
scripts Bring back a test suite 2026-09-09 15:00:16 +02:00
src/shamash Bring back a test suite 2026-09-09 15:00:16 +02:00
tests Bring back a test suite 2026-09-09 15:00:16 +02:00
.dockerignore Add Docker Compose, a Sky Color box, and object-creation UI/backend 2026-09-03 21:50:20 +02:00
.gitignore Add Docker Compose, a Sky Color box, and object-creation UI/backend 2026-09-03 21:50:20 +02:00
CLAUDE.md Bring back a test suite 2026-09-09 15:00:16 +02:00
docker-compose.yml Fix docker compose build: naturedynamics is public, use HTTPS not SSH 2026-09-04 09:44:53 +02:00
Makefile Bring back a test suite 2026-09-09 15:00:16 +02:00
Notes.md Updated Readme 2026-09-01 19:48:08 +02:00
pyproject.toml Release 2026.1.1 2026-09-09 14:26:15 +02:00
pyrightconfig.json input/output matrix notebooks 2026-07-21 16:01:07 +02:00
README.md Bring back a test suite 2026-09-09 15:00:16 +02:00
requirements.txt Pyproject update 2026-09-05 12:10:47 +02:00

Shamash

This aims to be a physically accurate worldbuilder. A lot of these calculations are rather educated guesses than well-founded scientific principals, but they all have their foundation in recent scientific discoveries and laws of nature.

Based on the great Artifexian's Work "The Worldsmith" I wanted to continue his journey. Intruiged by building on the very idea of quantifying reality - but at the same time horrified by looking at a spreadsheet - the only logical next step was to build a python application. :)

[!important] AI Disclaimer Yeah, I know, this sucks. Until version 2026.0.3 I wrote everything myself, except some exporting functionality. The architecture, the Design Patterns, I came up with that myself. But with time being the only thing that does not accumulate over time (weird, right?) I simply couldn't stand another project wasting away over the years, and me not having the energy to finish it. Claude assists me heavily in writing the API and UI.

So, before the whole thing became a webfrontend with an API I designed it to be a CLI-Application. I like to work from the terminal, but after having hundreds of parameters scrolling past your tired eyes this was just not feasable any more. So I turned the CLI into an API (which still can be used as CLI as well) and created a React-Frontend.

Worldbuilding is an incredibly complex process. This application will never aim at calculating EVERYTHING, you know. I know some of you (including me) have those thoughts and it is only natural to desire a feeling of absolute control over reality. Understanding, caluclating, quantifiying everything until its smallest detail. But look into yourself. The motivation will most likely be fear, or some derivation of it. You are afraid to not be able to hold the world you are building in the palm of your hand.

But the very thing you are seeking to do - describing a reality by natural laws - is something we humans perfected since 300.000 years by immitating our natural surroundings (impressions), using them to express our feelings, pain and pleasure (expressions). This enables you to describe your newly formed reality in a way far more compley than any table of figures can. Put your world into writing, not into a table, so other humans can comprehend what you are creating.

Getting started

A Makefile is provided. This step made local development easier, before I set up Docker.

make venv make install make run

Without any paramaters, shamash reads src/shamash/data/default.yml per default which represents our very solar system. make run is configured to use the -p flag, which pretty-prints out all calculations.

Installing gives you two console scripts:

  • shamash -p — the CLI.
  • shamash-api --reload — the HTTP API, also make api. With it running, http://127.0.0.1:8000/docs is the interactive schema and GET /v1/world/default returns the same world make run prints.

Neither needs the source tree: both run from an installed wheel.

The CLI application

shamash is the terminal-first way to use this project — load a world, optionally change what gets read or written, and exit. A plain shamash prints nothing; it just loads the world and returns, which is what makes it safe to script. Every flag is optional and they compose:

shamash                    # load src/shamash/data/default.yml, print nothing
shamash -w my_world.yml    # load a different world file instead
shamash -p                 # also pretty-print the loaded (and computed) world to stdout
shamash -e out.yml         # also export the loaded world to out.yml
shamash -f json            # write JSON instead of YAML (also: csv)
shamash -i new_world.yml   # copy the input world to new_world.yml, to seed a new one, then exit
shamash -l                 # also log to the configured log file (stderr always gets the log otherwise)

-p and -e render through the exact same writer, so shamash -p > world.yml produces a file that reloads with -w byte-for-byte. That holds per format: -f json -e world.json reloads with -w too, and gives back the identical world. On a terminal, -p syntax-highlights the output with rich; piped or redirected, it falls back to the plain string, so the redirect stays clean.

-f csv is the flat one, and the exception: one row per scalar rather than a tree, entity,type,property,source,value,unit,dimension, with compound values exploded into rows of their own (main_star_apparent_size[0], atm_composition.o_2). It is the same view the frontend's matrix tab shows, and it is one-way — a world cannot be read back from it. Use it to pull a computed world into a spreadsheet or a plotting script:

shamash -f csv -p | awk -F, '$3 == "habitability"'

Verbs

Flags act on a world; verbs change one. create makes one entity and hangs it off an existing one:

shamash create Atmosphere earth -w my.yml        # asks for what it needs
shamash create Planet sol -w my.yml              # offers sol's free orbits, or makes one
shamash create Star --id alpha --set mass=1.0:MSol --set current_age=4.6:Gyr -w my.yml

Flags work on either side of the verb, so shamash -w my.yml create Orbit sol reads the same.

Required fields it isn't given, it asks for; --set key=value answers one up front. --set also reaches the optional fields it never asks about — a planet's core (--set core_density=10900), a star's wind — and refuses a name the type doesn't have, so a typo is a visible error rather than a value that quietly never arrives. Values are SI unless you give a unit — --set mass=1.0:MSol goes through the same conversion and the same checks a world file's {value: 1.0, unit: MSol} does, so a wrong dimension is refused rather than quietly scaled.

Choices with no safe default are asked too: a body that already has an atmosphere, a parent that already has orbits, a file about to be overwritten. --answer name=value settles those up front (--answer strategy=add), which is what makes the whole thing scriptable — including questions about an entity create makes along the way, like the orbit a new planet needs (--answer semi_major_axis=1.52:ASol). With nothing to ask and no terminal to ask at, it refuses instead of hanging — a piped run tells you exactly which answers it was missing and exits 1.

What can host what is not a list anywhere; each relationship declares what the child reads off its parent, and a type qualifies by providing it:

$ shamash create Atmosphere sol -w my.yml
Atmosphere cannot hang off 'sol': a Star has no surface_temperature,
which Atmosphere.parent_moon_or_planet needs. Types that do: Planet.

That sentence is assembled from the models every time, so it follows them — the day a Moon can compute a surface_temperature, moon atmospheres start working and this refusal stops being produced, with nothing to update.

create writes back to the file -w names, or to -e when given. It refuses without one of them (the default world lives inside the installed package), asks before overwriting unless -y, and refuses -f csv, which does not reload.

shamash-api --reload (or make api) exposes the same load/compute/export cycle over HTTP instead — see the Frontend section below for what talks to it.

The frontend

The web UI (frontend/, React + Vite + TypeScript) is a client to that same API: it never runs any physics itself, it just posts the world you're editing and renders back whatever comes out of a compute. Every edit debounces into one request and the whole screen re-renders from the response, so what you see is always something the CLI could also have printed.

Start it with make ui-install once, then make api and make ui in two terminals. The tab bar along the top is the whole app:

Overview

The first thing you see after loading a world: every entity in it — star, planet, moon, atmosphere, biosphere, magnetosphere — laid out as a grid of read-only cards, plus the orbit diagrams for whatever orbits exist. It's the "give me the whole system at a glance" view, and the command row above it (LOAD WORLD / SAVE WORLD / LOAD DEFAULT / EXPORT PDF) is present on every tab.

Overview tab

Live View

The system animated on one scale: orbit distances and the habitable zone (the green ring) are to scale, so "is this planet in the zone" is something you can just look at. Body sizes, moon orbits and time are deliberately not to scale — the caption says so — but the period ratios between orbits are real. Hovering a body pops up its live computed values.

Live View tab

Star / Planet / Moon

One editable form per entity type, generated from the API's own schema (GET /v1/schema/entities) rather than hand-written — so a new dataclass field or computed property shows up here with no frontend change. Each field shows its unit, and a checkbox in the status bar toggles inline docs and diagnostics for fields that failed to compute.

Envelopes

The three entities that wrap a body rather than being one: Atmosphere, Biosphere and Magnetosphere. Grouped together because none of them has mass or an orbit of its own — they all just point at a planet or moon by id.

Envelopes tab

Orbits

Every Orbit in the world as its own editable card, independent of which body it belongs to — useful once a system has more orbits than the Overview grid comfortably shows.

Matrix

Every field and computed property, for every entity, as one flat, filterable, sortable table — entity, type, whether it's an input or computed, value, unit, dimension. It's the "just show me all 140 numbers" view, and doubles as a way to spot which properties are unavailable (hide unavailable toggle) without hunting through per-type tabs.

Matrix tab

YAML

The world as raw YAML in a CodeMirror editor — the same text shamash -p prints and shamash -e writes. Edit it directly and it recomputes like any other tab.

Settings

Reads and writes Settings.Config (the orbit-generation defaults such as moon-orbit slot count and Laplace resonance ratio) through GET/PUT /v1/settings. Unlike every world tab, saving here persists to disk and changes process-wide behaviour rather than just the in-memory draft.

Documentation

Every field and computed property that carries a description gets a hover popover on its label (toggle it via the descriptions checkbox in the status bar), and its "docs →" link opens a dedicated page in a vendored, offline-capable docsify site under frontend/public/docs/ — one page per property, organized by entity type in the sidebar, with LaTeX-rendered formulas and links back to the source relation. Coverage is complete: all 128 documented fields and computed properties across the 7 entity types have a page, and nothing is orphaned.

Property documentation

Docker

docker compose up --build builds and runs both the API and the frontend (nginx, proxying /v1, /healthz and /openapi.json to the API container the same way vite.config.ts does in dev — the browser only ever talks to one origin, http://localhost:8080, so CORS never comes up). No credentials needed: naturedynamics is on PyPI and installs like any other dependency.

Releases

Development happens on origin (dev.fs.or.at). A release is an annotated tag on main, and codeberg only ever sees tagged states:

git push origin main                          # every day
git tag -a v2026.1.1 -m "CLI verbs and output formats"
git push origin --tags                        # keep the markers on dev too
git push codeberg main v2026.1.1              # publish

Tags go to both remotes; commits go to origin until they are released. That way the release history survives on the remote that has everything, rather than only on the one it is published to.

So git tag is the list of releases, each heading below names one, and the version in pyproject.toml is bumped in the commit the tag points at. __version__.py reads it from there — see the note in CLAUDE.md about why the checkout's pyproject.toml wins over importlib.metadata.

2026.1.2

  • Bring back a test suite — make test, stdlib unittest, ~1 s
    • It loads the real seed world and asserts what comes out, rather than mocking collaborators: the physics is a pure function of default.yml, so running it is the test. Most of it is a transcription of numbers CLAUDE.md already presented as regression baselines — Sol at 1.000 RSol / 5770.8 K / G2.9, Earth habitability 1.000, #B3F49A, magnetopause 9.52 R⊕ — which described a suite nobody had written down.
    • 70 tests over the physics anchors, the loader's round-trips and error taxonomy, god.scope() isolation under 8 threads, the HTTP contracts (including that a 500 still carries its CORS header), the CLI, and the create verb's parent rules.
    • Mutation-probed while being written, which is the only reason to trust it: feeding Star.radius kilograms fails 6 tests, dropping FACTOR_FLOOR fails the hostile-world test, removing the Planet.moons backref fails the tidal one.
    • make test-packaging is separate (~15 s): it builds a wheel and runs the CLI and the API from it with no source tree in sight.
  • Two bugs the suite found before it was finished
    • create Moon sol ran the orbit cascade before discovering a Star cannot host a moon, so it created an orbit and then refused — leaving it behind in a world the caller was told nothing had been added to. Legality is now decided first, so a refusal costs nothing.
    • create asked its questions before validating --set names and the --id, so a typo was reported only after you had answered three prompts. Both are checked up front now.
  • Corrected numbers the docs stated but the code did not produce
    • magnetosphere.py documented Earth's magnetopause at 9.6 planetary radii and the bow shock at 12.5, in five places including user-facing text mirrored into the docs site. The code computes 9.52 and 12.37.

2026.1.1

  • Output formats for the CLI — -f yaml|json|csv
    • Notation moved out of both the codec and the HTTP layer into services/world/formats.py, so there is one YAML dumper call and one JSON dumper call in the codebase and the CLI and API cannot drift. -p and -e are byte-identical per format; shamash -f json -p is byte-identical to GET /v1/world/default.
    • csv is the flat view — one row per scalar, the same shape the frontend's matrix tab shows — and is one-way.
    • Worldloader.loads now parses a root-level { as JSON. PyYAML follows YAML 1.1, where 5e-05 without a dot is a string, so the app could not read back a file it had just written with -f json -e.
  • CLI verbs — shamash create <Type> <parent_id>
    • cli.py grows subparsers and a VERBS map; flags work on either side of the verb. -w names the file the verb modifies.
    • What can host what is declared per relationship as parent_needs — what the child reads off its parent — never as a table of types. The refusal message is assembled from the models, so it follows them: the day a Moon can compute a surface_temperature, moon atmospheres start working with nothing to update.
    • Services never prompt. They call self.ask(questions); the CLI injects InteractionService.ask and everything else gets refuse_to_ask, which raises a 422 carrying the questions — a service reading stdin would block a uvicorn worker forever, and world:create-orbit runs the same code.
    • --set and --answer make a create fully scriptable; with nothing left to ask it needs no terminal at all.

2026.1.0

  • Put the whole thing in a Docker container for convenience and deployment
    • docker-compose.yml + docker/api.Dockerfile (pip install ., fetching naturedynamics over plain HTTPS — the repo is public, no credentials needed) + docker/frontend.Dockerfile (nginx, proxying /v1, /healthz, /openapi.json to the API container). See the "Docker" section above.
  • Web frontend — React, in frontend/
    • Settled by a measurement rather than a preference. A full seed-world compute is ~18 ms for 7.7 KB of JSON, so every edit can debounce into one POST /v1/world:compute and replace the whole snapshot. The client needs no physics and no derived-value graph of its own — the reactive graph already exists on the server, as god.summons() plus lazy @property.
    • That is what decides it. Angular's strongest cards here are signals and Reactive Forms, and neither has anything to do: there is nothing to derive on the client, and the form is a flat set of numeric inputs generated from /v1/schema/entities. What the UI actually is — an unstyled dense numeric matrix and hand-drawn animated orbit diagrams — is where React's headless ecosystem (TanStack Table/Virtual, d3 interop) has no Angular equivalent that is both free and design-language-neutral.
    • /openapi.json alone is not enough, and assuming otherwise is the trap. api/schemas.py types Entity as type + id + extra="allow" on purpose, so OpenAPI describes the envelope, Quantity and the error body — and zero entity fields. The per-type schema lives only at GET /v1/schema/entities. frontend/scripts/generate.ts generates from both; the second half is what lets one generic component render all 54 fields and 70 computed properties.
    • Stack: Vite, React 19, TanStack Query (keepPreviousData, so a recompute never blanks the screen), TanStack Table for the matrix tab, TanStack Router for the tabs, zustand for the draft, CodeMirror 6 for the YAML tab.
    • make ui-install, then make api and make ui in two shells. make ui-gen regenerates the typed client against a running API.
    • Nine tabs, including a Live View: the system animated on one scale, with the habitable zone drawn on that same scale and hover tooltips on every body. Bodies are coloured from the model — a star by star_color, a planet by its atmosphere's sky_color, a moon grey, an airless planet a darker grey.
    • A command row for load world / save world / load default. Loading posts the file as application/yaml, so the loader's opinions stay in one place and a JSON world file works through the same path.
  • Atmospheric sky_color — the colour of the sky from the ground
    • Single-scattered starlight through the Rayleigh column, von Kries adapted to the local star. Earth reads #788CC4; a 90 atm column washes out to near-white, which is what a saturated column should do.

2026.0.4

  • Stateless HTTP API (FastAPI), so a web frontend can do what -w does
    • Phase 0: make the package survive outside its source checkout
      • Resolve version/name/description without reading pyproject.toml at import
      • Ship data/default.yml as declared package data
      • Stop the exporter crashing on optional fields a world leaves out
      • Validate a world file's units against the dimension the field declares
      • Log normal conditions at DEBUG instead of ERROR
    • Request-scoped god registry (contextvars)
    • Worldloader.loads/load_data/to_dict seams, JSON and YAML in/out
    • AppError -> HTTP status, inherited down the MRO
    • api/ package: app, routers, schemas, JSON/YAML content negotiation
    • POST /v1/world:compute, GET /v1/world/default, GET /v1/schema/entities
    • Console entry points (shamash, shamash-api), make api
    • Fix the review findings: caller mistakes returning 500, blind 500 logs, CORS-less error bodies

v2026.0.3

  • Refactor and Introduce Atmospheric model
    • Maybe even in naturedynamics, taking atmospheric models from omniastro
  • Introduce Plant Color and Biosphere Module
    • Based on Star and Atmosphere
  • Geomagnetic Dynamics/ Solarmagnetic Dynamics
  • Make Habitability not true or false but a probability scale, taking into account:
    • Class of the Star and Brightness
    • Calculation Method relative habitability to how far the planet is away

v2026.0.2

Settled on name for project: shamash.

  • Rename Project
  • Refactor export/import handling
    • do not use pprint, but format in cli as well
    • use flag for cli dump
  • Unify Units handling, refactor bad claude code
    • Every function should have their units as metadata with them - decorators?
    • Distances in space are AU/Parsecs/Lightyears
  • Fix main_star_apparent_size=<error: division by zero>
  • Improve logging
    • Add a flag to write to a log file
  • Star Class Calculation
  • Add Star Classes as Enums, maybe directly into naturedynamics
  • Fix Hillsphere Calculation and Inner/Outer Moon Zone Error
  • Also push naturedynamics when pushing worldcreator
    • As of 2026-09-01 naturedynamics is no longer a submodule; it lives at ~/Dev/projects/naturedynamics and is installed into the venv.
    • Release Versioning and Cycle ?
  • Add Horizon Distance
  • Add proper error handling
  • Add a flag to init a new world yaml file
    • shamash init new_world
  • Add Flag to point to yaml world file
  • Make color calculation refined and add Hexcolor support

v2026.0.1

Finally, first working version finished, shamash.

Backlog

  • Add Habitable Zone Shift Factor for planets, to shift habitable zone bonds,
    • regarding how many greenhouse gases exist
    • Axial Tilt
    • Orbital Period
  • Make Apparent Brightness, etc a function that can be calculated at any given point of the orbit.
  • Add LSol as Unit for Luminosity
  • Change ASol to AU
  • Decide what log level a caller's mistake deserves
    • AppError logs at ERROR on construction. Over HTTP every 422 is the client's bad world, not a server incident, so a busy API fills the error log with things no operator can act on — but the same exception from the CLI, against a world file you just edited, is worth an ERROR. One class, two contexts; Phase 0 already established log_level= as the mechanism.
  • Containerise — done under 2026.1.0 above, once unblocked on 2026-09-01 by the package no longer reading pyproject.toml at import, shipping its seed world as package data, and running (CLI and API both) from a wheel with no source tree.
  • make venv pins --python 3.13 while the venv in use is 3.14.6
    • Both satisfy requires-python >=3.12, so nothing is broken; it just means make venv builds a different interpreter than everything is tested on.