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 |
||
|---|---|---|
| docker | ||
| frontend | ||
| notebooks | ||
| ressources | ||
| scripts | ||
| src/shamash | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Makefile | ||
| Notes.md | ||
| pyproject.toml | ||
| pyrightconfig.json | ||
| README.md | ||
| requirements.txt | ||
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, alsomake api. With it running,http://127.0.0.1:8000/docsis the interactive schema andGET /v1/world/defaultreturns the same worldmake runprints.
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.
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.
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.
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.
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.
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, stdlibunittest, ~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 numbersCLAUDE.mdalready 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 thecreateverb's parent rules. - Mutation-probed while being written, which is the only reason to trust
it: feeding
Star.radiuskilograms fails 6 tests, droppingFACTOR_FLOORfails the hostile-world test, removing thePlanet.moonsbackref fails the tidal one. make test-packagingis separate (~15 s): it builds a wheel and runs the CLI and the API from it with no source tree in sight.
- It loads the real seed world and asserts what comes out, rather than
mocking collaborators: the physics is a pure function of
- Two bugs the suite found before it was finished
create Moon solran 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.createasked its questions before validating--setnames 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.pydocumented 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.-pand-eare byte-identical per format;shamash -f json -pis byte-identical toGET /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.loadsnow parses a root-level{as JSON. PyYAML follows YAML 1.1, where5e-05without a dot is a string, so the app could not read back a file it had just written with-f json -e.
- Notation moved out of both the codec and the HTTP layer into
- CLI verbs —
shamash create <Type> <parent_id>cli.pygrows subparsers and aVERBSmap; flags work on either side of the verb.-wnames 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 asurface_temperature, moon atmospheres start working with nothing to update. - Services never prompt. They call
self.ask(questions); the CLI injectsInteractionService.askand everything else getsrefuse_to_ask, which raises a 422 carrying the questions — a service reading stdin would block a uvicorn worker forever, andworld:create-orbitruns the same code. --setand--answermake 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 ., fetchingnaturedynamicsover plain HTTPS — the repo is public, no credentials needed) +docker/frontend.Dockerfile(nginx, proxying/v1,/healthz,/openapi.jsonto 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:computeand 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, asgod.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.jsonalone is not enough, and assuming otherwise is the trap.api/schemas.pytypesEntityastype+id+extra="allow"on purpose, so OpenAPI describes the envelope,Quantityand the error body — and zero entity fields. The per-type schema lives only atGET /v1/schema/entities.frontend/scripts/generate.tsgenerates 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, thenmake apiandmake uiin two shells.make ui-genregenerates 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'ssky_color, a moon grey, an airless planet a darker grey. - A command row for
load world/save world/load default. Loading posts the file asapplication/yaml, so the loader's opinions stay in one place and a JSON world file works through the same path.
- 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
- 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.
- Single-scattered starlight through the Rayleigh column, von Kries adapted
to the local star. Earth reads
2026.0.4
- Stateless HTTP API (FastAPI), so a web frontend can do what
-wdoes- Phase 0: make the package survive outside its source checkout
- Resolve version/name/description without reading
pyproject.tomlat import - Ship
data/default.ymlas 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
- Resolve version/name/description without reading
- Request-scoped
godregistry (contextvars) Worldloader.loads/load_data/to_dictseams, JSON and YAML in/outAppError-> HTTP status, inherited down the MROapi/package: app, routers, schemas, JSON/YAML content negotiationPOST /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
- Phase 0: make the package survive outside its source checkout
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/naturedynamicsand is installed into the venv. - Release Versioning and Cycle ?
- As of 2026-09-01 naturedynamics is no longer a submodule; it lives at
- 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
AppErrorlogs 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 establishedlog_level=as the mechanism.
- Containerise — done under 2026.1.0 above, once unblocked on
2026-09-01 by the package no longer reading
pyproject.tomlat import, shipping its seed world as package data, and running (CLI and API both) from a wheel with no source tree. make venvpins--python 3.13while the venv in use is 3.14.6- Both satisfy
requires-python >=3.12, so nothing is broken; it just meansmake venvbuilds a different interpreter than everything is tested on.
- Both satisfy




