bayes for days
sds docs LANCES.md
11 kB
Markdown
at main

A random lance corpus #

sds lances draws lances the way MegaMek's own force generator draws them, and writes each one in the shape sds_core::formation reads. It exists so that a question of the form "what does the bot actually meet?" is answered against forces somebody could field rather than against forces nobody would.

./sds.sh lances --count 20000 --out runs/lances.jsonl
cargo run --release -p sds-core --example lances -- runs/lances.jsonl

Why not uniform sampling #

Four Meks picked at random from the four thousand in the database is not a force. It has no faction, no year and no doctrine behind it, and the compositions that matter are exactly the ones it cannot produce: a corpus built that way had no Anvil, no Order and no Rifle lance in twenty thousand forces, which made it unable to answer the question it was built for. A corpus that cannot produce the cases anyone cares about is worse than no corpus, because its numbers look real.

megamek.client.ratgenerator is the availability data MegaMek draws its own forces from - who fields what, in which year, at which equipment rating. A lance out of it is a lance a player could meet.

What the corpus is #

JSON Lines, in SdsForces's shape, so a lance corpus and a suite dump are read by the same consumer:

  • The first line is a manifest: the seed, the count, the sampling settings, and what MegaMek answered with - era count, faction count, model count and the size of the design database. Two corpora whose manifests differ are not comparable, and the manifest is what says so.
  • Every line after it is one lance: scenario is the draw that produced it (faction/year, plus /rating when one was drawn), force is its ordinal, and units is the four machines.

It is not committed, and will not be #

The bytes carry every machine's tonnage, armour, movement and weapons. That is a bulk copy of MegaMek's CC BY-NC-SA unit data, and this repository ships none of it - the same reason crates/sds-core/tests/corpus/forces.jsonl is regenerated rather than shipped.

Membership alone would be small - four names and a faction is eighty bytes, so twenty thousand lances is under two megabytes - but it would not be useful. Nothing can classify a list of names without the facts, so a committed membership file would have to be joined against a dump on every use, and it would still be a bulk transcription of MegaMek's availability tables. What is committed instead is the generator, its defaults, and the manifest format that says which corpus a number came from.

Reproducibility #

Two seeded streams, and neither reads a clock: one picks the faction, year and rating, the other picks machines out of the table those build. Raising --count extends a corpus rather than reshuffling it.

The draw over the table is ours rather than UnitTable.generateUnits, and the two reasons are worth knowing before changing it back:

  • generateUnit draws through Compute.randomInt, which is process-wide. Reading a design reaches the same RNG - Observation.averageDamage resolves a cluster weapon by building a throwaway game and asking the handler - so one stream for both makes each lance depend on which weapons the lance before it carried.
  • A table does not hand its entries back in a fixed order. The generator fills its era data on background threads and a table inherits their insertion order, so the same seed mapped to different machines on about one run in three until the entries were sorted by their own text first.

What that costs is the salvage branch: salvagePct and the salvage table are private, and a draw that read them would be back to calling generateUnit. A drawn lance is what the faction fields, with nothing recovered from somebody else's wreck in it.

How exact it is #

sds lances prints [sds] digest <sha256> over the lance lines, so "is this the same corpus" is one comparison rather than a diff of eighty megabytes.

Two runs of the same seed agree on every parameter and every draw count, and on about 99.6% of lances. The residual is one machine in one lance, almost always another variant of the same chassis: a table entry can name a chassis rather than a model, UnitTable.getMekSummary picks the variant when it does, and it picks through the process-wide RNG that MegaMek's own background loading also touches. Measured over five runs of 3,000: 0.07% to 0.43% of lances differed, one machine each.

That is two orders of magnitude below the resolution of any row in the table below, so it does not affect a rate. It does mean a digest is evidence of sameness and not of difference - quote the seed and the settings, and check a number you care about against a second seed rather than against a second run of the first.

A corpus is reproducible from (seed, count, size, scope, ratings) against a given MegaMek release. A different release has different availability data and gives a different corpus; the manifest's era, faction, model and design counts are what catch that.

Sampling settings #

--scope decides who may field a lance.

  • active (the default) draws a year first and then a faction that FactionRecord.isActiveInYear says existed in it. MegaMek carries 355 faction records across 39 eras and most of those pairs are a state that did not exist in that year. Drawing them anyway and letting the table come back empty is not neutral - it silently reweights the corpus towards whoever happened to be long-lived.
  • any ignores the year. It is here to measure what active is worth, not to be used.
  • major is active without the records FactionRecord.isMinor flags.

--ratings decides equipment quality. unspecified is MegaMek's own default and merges every rating band into one table. drawn picks one of the faction's rating levels per lance, which splits the corpus by how well equipped a force is.

Reading the output #

examples/lances.rs prints two columns per formation and the gap between them is the point. satisfies is how many lances meet the criteria at all - what the corpus can reach. wins is how many are classified as it once formation::ordered's precedence has run. A formation rare because the corpus cannot produce it and one rare because precedence eats it are different findings with different fixes, and one column cannot tell them apart.

How big a row has to be before it means anything #

Two corpora of 20,000 at different seeds, same settings, compared row by row:

row size (satisfies) largest disagreement
above 2,000 1.7%
700 to 2,000 16.4%
200 to 700 8.7%
under 200 28.3%

A binomial underestimates this and should not be used to plan a sample. A row of 1,250 would be ±2.8% if every lance were an independent trial, and Hammer moved 1,246 to 1,449. Lances are clustered: a draw fixes a faction and a year and then takes four correlated designs out of one table, so the effective sample is a good deal smaller than the lance count. Measure the spread with a second seed rather than computing it.

What that leaves usable at 20,000 lances: rows above 2,000 carry a rate, rows between 200 and 2,000 carry an order of magnitude, and rows under 200 say "this happens" and nothing more.

The wins column is worse than satisfies at the same size, because it is what survives precedence rather than what qualifies. Hammer moved 3 to 12 between the two seeds and Light Battle 143 to 109. Quote a wins figure only where the satisfies row above it is large.

What this corpus cannot reach #

Order is the one formation of the thirty-three that no lance satisfied in 80,000 draws across four settings. It wants four machines of the same model, and the generator draws each slot independently from the faction's table, so more samples is not the fix. (Its faction: DC gate is not what excludes it: Formation::qualifies does not read faction at all, because a wire unit does not carry one.)

MegaMek.client.ratgenerator.FormationType.generateFormation draws a force that satisfies a named formation, which is the tool for that row and for Artillery Fire, Light Recon and Horde beside it. It is not what this corpus is - a targeted draw answers "what does an Order lance look like" and this answers "what turns up" - and the two should not be mixed in one file.

What twenty thousand lances came out as #

--seed 20260904 --count 20000, defaults otherwise: the corpus crates/sds-core/tests/corpus/lances.txt records, which is what formation::ORDER is pinned to. Every lance is classified as something, because Support admits any four ground Meks and is last in the order. The rows above 2,000 lances:

formation satisfies wins keeps
Support 20,000 566 2.8%
Command 12,504 973 7.8%
Security 7,124 1,407 19.8%
Ranger 6,967 1,624 23.3%
Urban 6,426 1,703 26.5%
Heavy Battle 5,737 1,587 27.7%
Anvil * 5,285 0 withheld
Battle * 5,149 0 withheld
Berserker/Close 5,149 1,176 22.8%
Medium Battle 2,927 1,391 47.5%
Fire Support 2,418 1,231 50.9%

* is a formation the catalogue withholds: it can qualify and can never be handed to a force, so its wins is zero by construction rather than by precedence.

What the order is derived from #

examples/precedence.rs reads a corpus and derives the precedence order from it: each position goes to the formation with the fewest lances no formation above it has already claimed. That is what the corpus is for, and the file it writes is the artifact formation::ORDER is pinned to - the order is edited to match by hand, and nothing recomputes it at build time.

Two things measured before that order was landed are worth keeping, because both are the sort of thing somebody will propose again:

Move the least-used formations to the top and repeat. It converges - a fixed point by iteration seven for one to four moved per round, a period-two cycle for five to ten, and a period-33 cycle at thirteen - and it is worse than one pass of the rule above on every measure, at every j. It is also unstable: the order it settles on moved a formation by up to 19 positions between corpus seeds, where the one-pass rule moves nothing by more than two.

Sort by the raw satisfies count. Simpler, and measurably worse: it leaves Ranger 3.6% of lances where taking the residue leaves it 8.1%. What a formation can still win depends on what everything above it took, and two formations with the same count can overlap the order above them completely differently.

The two sampling settings are worth less than they look. --ratings drawn moves nothing above its own noise. --scope any gives almost the same distribution and wastes 63% of its draws on faction-year pairs that did not exist - 54,654 draws for 20,000 lances, against 21,436 for the default.