Restore native plateaus and archive terrain comparisons for alpha.30.5
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 16:58:14 +02:00
parent e400f1e2c1
commit 7f81bf35a0
16 changed files with 612 additions and 13 deletions
+12
View File
@@ -21,6 +21,18 @@ le relief et les souterrains de la 24, retire la montgolfière et travaille les
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
[chantier et ses limites](docs/generation-alpha27.md). [chantier et ses limites](docs/generation-alpha27.md).
## Alpha.30.5 — plateaux et atlas du terrain
L’étirement vertical de la 30.4 est retiré. Les plateaux issus de la 24/27
reçoivent une déformation 3D modérée dans de larges zones ; aucune hauteur
sommitale nest imposée. Grottes, rivières courbes, brèches de cascade et
infrastructures sont conservées. Le plafond constructible reste Y640.
La comparaison devient reproductible : cartes de relief, épaisseur rocheuse
et coupes de densité des quatre champs, aux mêmes coordonnées et graines.
Voir [le terrain](docs/generation-alpha30.5.md), [loutil datlas](docs/terrain-atlas.md)
et [les vérifications](docs/testing-alpha30.5.md). **Nouveau monde requis.**
## Alpha.30.4 — massifs et ciel constructible ## Alpha.30.4 — massifs et ciel constructible
Les plateaux bas sont conservés ; les reliefs hauts se raccordent par des Les plateaux bas sont conservés ; les reliefs hauts se raccordent par des
+10
View File
@@ -1037,3 +1037,13 @@ courbes sur les replats supérieurs et libérer de lespace constructible jusq
Y639. Stabiliser un seul relief avant dajouter des personnalisations. Y639. Stabiliser un seul relief avant dajouter des personnalisations.
Larène du destin reste une idée, sans structure ajoutée. Larène du destin reste une idée, sans structure ajoutée.
Voir [la génération](generation-alpha30.4.md) et [les essais](testing-alpha30.4.md). Voir [la génération](generation-alpha30.4.md) et [les essais](testing-alpha30.4.md).
### WG-37 — Plateaux, relief 3D modéré et atlas — alpha.30.5
Branche `codex/plateau-atlas-alpha30-5`. Retirer l’étirement des coques de la
30.4 ; repartir des plateaux avec une déformation du bruit natif, sans pousser
les sommets vers Y360. Conserver les grottes, rivières courbes, sorties deau,
secrets et traversées. Comparer les champs natifs 27, 30.1, 30.4 et 30.5 par
coupes et cartes scientifiques, archivées avec leurs sources et leurs graines.
Les expéditions anciennes et les îles supérieures ciblées restent à définir.
Voir [la génération](generation-alpha30.5.md) et [latlas](terrain-atlas.md).
+54
View File
@@ -0,0 +1,54 @@
# Alpha.30.5 — plateaux et relief natif modéré
## Décision
La 30.4 est rejetée après essai : ses masses hautes étirent aussi les vides du
terrain, avec des coques entrecroisées peu praticables. Cette passe reprend les
plateaux de la base 24/27, les grottes et failles introduites en 28, et module
la déformation 3D native dans de grandes régions de l’île. Aucun nouveau bruit
fin de surface, cône, chambre artificielle ou quantification en étages nest ajouté.
Le déplacement dans les trois axes demeure celui de `CavesRelief28`, avec une
intensité maximale de 80 % et une répartition large calculée à l’échelle de
chaque taille. Certains secteurs gardent le plateau original. Larrivée et la
bande prévue pour la traversée conservent leur protection. Les profondeurs
jusqu’à Y170 restent identiques au champ 30.1.
Le remappage vertical vers Y360 et lunion entre terrain étiré et terrain bas
sont retirés du profil courant. Les sommets retrouvent la hauteur produite par
le champ natif ; **Y360 nest plus un objectif imposé**. Le plafond de construction
reste Y640, avec les mêmes nuages et tailles 512 / 724 / 1 024.
## Eau et exploration
La recherche des rivières revient au parcours courbe sur les surfaces
naturelles disponibles, sans priorité artificielle aux replats au-dessus de
Y330. Les brèches courtes dans les berges demeurent, avec écoulement vanilla.
Les cavités, marais, champignons, filons, secrets, rails, traversées souterraines,
navires restaurés et îlots restent dans le périmètre existant. La montgolfière
retirée depuis la 27 ne revient pas.
Le filtre borné des petits composants rocheux détachés reste actif. Il ne
prétend pas supprimer chaque fragment de chaque monde, ni les marches
inévitables de la représentation en blocs.
## Nouvelle méthode de travail
L[atlas natif](terrain-atlas.md) compare quatre champs avant décoration :
base 27 issue du retour à la 24, bruit global 30.1, étirement 30.4 et profil
30.5. Les coupes, cartes daltitude et épaisseurs sont archivées avec les données
brutes. Elles servent à examiner la géométrie avant d’évaluer le paysage en jeu.
Une métrique favorable seule ne valide pas son esthétique.
Lancienne déformation 30.4 reste disponible uniquement comme chemin de
comparaison scientifique dans le code. Le champ courant utilise toujours les
identifiants existants (`balanced_303` désigne le profil expérimental courant,
comme lors de la 30.4). Aucune migration nest réalisée : **créer un nouveau
monde**. Cette révision des profils dessai noffre pas de continuité du terrain
pour les chunks futurs dune ancienne sauvegarde.
Les îles supérieures, expansions choisies à partir de la graine et histoires
dexpédition seront définies plus tard. Aucun récit, événement ou continent
supplémentaire nest injecté dans cette livraison.
Résultats et limites : [vérifications alpha.30.5](testing-alpha30.5.md).
+75
View File
@@ -0,0 +1,75 @@
# Atlas scientifique du terrain
## Acquisition native
`TerrainAtlas305GameTests` charge les registres réels de Minecraft 26.3-pre-2
et les ressources Sanctuary. Il crée le même `RandomState` que le générateur
pour chaque graine et taille. Il compile les fonctions de densité natives,
sans réimplémentation du bruit en Python. Le serveur de test utilise un monde
plat ignoré pour démarrer rapidement ; aucun chunk Sanctuary ni aucune
expansion nest créé pour cet export.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryAtlasOnly=true \
-PsanctuaryAtlasSeeds=0,42,-7228211907433324401 \
-PsanctuaryAtlasRevision=REVISION_DU_CODE
python3.14 -m venv build/atlas-venv
build/atlas-venv/bin/pip install -r scripts/terrain-atlas-requirements.txt
build/atlas-venv/bin/python scripts/terrain_atlas.py --output build/atlas305 \
mods/sanctuary/build/run/gameTest/diagnostics/atlas305/terrain-*.json
```
Le tracé livré utilise Python 3.14 ; les versions de Matplotlib et NumPy
sont épinglées dans le fichier de dépendances.
Les propriétés atlas sont réservées aux tests. Les tests de génération complets
restent actifs dans `check` sans ce paramètre. Ne pas lancer deux Gradle de test
simultanément : leur dossier de monde temporaire est partagé. Les diagnostics
existants peuvent rester dans le dossier ; vérifier graines, version et provenance
avant den faire une archive. Aucune sauvegarde personnelle nest utilisée.
## Lecture des planches
- Cartes : nord en haut, X et Z en blocs, rapport daspect réel. Les quatre
profils partagent les mêmes échelles de couleur. La palette daltitude ne
représente pas les blocs ou les biomes ; du blanc nimplique pas de neige.
- Altitude : dernier solide rencontré, recherche verticale par pas de 2 blocs.
Une cellule grise signifie quaucun solide na été échantillonné dans la colonne.
- Épaisseur cumulée : nombre d’échantillons solides multiplié par 2 sur Y2..382.
Ce nest ni une densité physique, ni un volume exact de matériaux minables.
- Roche continue : épaisseur depuis le sommet jusquau premier vide échantillonné.
Un petit résultat peut signaler une voûte mince, un surplomb ou un fragment.
- Coupes : trois plans Z=0, Z=96 et Z≈−rayon/4, sur toute la hauteur naturelle.
Bleu pour la densité négative, rouge pour la positive ; frontière zéro en noir.
L’échelle colorée sature au-delà de ±0,3 ; les données conservées vont jusqu’à ±1.
Le pas horizontal des cartes est de 12 blocs, le pas vertical de 2. Les coupes
sont échantillonnées tous les 2 blocs dans les deux axes. Les images ne sont
pas lissées. Le champ est quantifié à 0,0001 en conservant son signe autour de
zéro. Les petits détails peuvent être manqués : le contrôle des fragments en
volume utilise séparément une résolution dun bloc dans les tests complets.
Leau, la lave, les matériaux de surface, végétaux, biomes et structures ne
figurent pas dans cet atlas de **densité avant décoration**. Les tests natifs
de rivières et de structures vérifient leur présence effective dans des chunks.
Une coupe 2D ne permet pas, seule, de conclure à la connectivité dune grotte 3D.
## Comparaison et conservation
Les tableaux utilisent les mêmes coordonnées dans chaque variante. La part
des plateaux conservés compare les colonnes de base dont le sommet atteint
Y200, avec une tolérance de deux blocs. Les statistiques d’épaisseur du toit
utilisent uniquement les colonnes atteignant Y200 dans **les quatre variantes**.
Ce sous-ensemble ne représente pas toutes les falaises de l’île.
Larchive de livraison contient JSON natifs, PNG, PDF, résumés, script de rendu,
versions Python et empreintes des fichiers. Elle est attachée à la release,
pour que les images restent récupérables même après nettoyage du dossier build.
Le manifeste relie les données au binaire livré et à la révision source.
Les vues sont générées par Matplotlib à partir des mesures ; aucune image
générative nintervient.
Le libellé « 24 / 27 » désigne le champ de la 27 restauré depuis la 24, pas
une simulation de tous les matériaux et structures historiques de la 24.
Les samplers 30.1 et 30.4 sont conservés pour comparer des transformations
exactes, et non des approximations dessinées pour illustrer un résultat attendu.
+96
View File
@@ -0,0 +1,96 @@
# Vérifications alpha.30.5
Essais natifs sur **Mac**, Minecraft 26.3-pre-2 et Java 25. Aucun essai Windows
natif. Latlas est une mesure du champ de terrain avant décoration, pas une
capture du rendu Minecraft ni une validation esthétique du créateur.
## Atlas natif
Trois tailles × trois graines, quatre champs comparés par cas : base 27,
global 30.1, étirement 30.4 et profil 30.5. Les deux acquisitions atlas passent
chacune les deux tests requis du serveur plat. Aucun chunk Sanctuary nest
généré pour cette acquisition. Les neuf exports prennent environ 3 min 30 s
de calcul natif au total, hors démarrage et tracé des planches.
| Diamètre | Graine | Sommet échantillonné 30.5 | Plateaux conservés à ±2 blocs |
|---|---|---|---|
| 512 | `-7228211907433324401` | Y258 | 82.7 % |
| 724 | `-7228211907433324401` | Y276 | 77.6 % |
| 1024 | `-7228211907433324401` | Y274 | 73.9 % |
| 512 | `0` | Y272 | 84.4 % |
| 724 | `0` | Y272 | 80.9 % |
| 1024 | `0` | Y272 | 79.1 % |
| 512 | `42` | Y270 | 74.3 % |
| 724 | `42` | Y278 | 70.7 % |
| 1024 | `42` | Y278 | 71.1 % |
« Plateaux » signifie ici les colonnes de base dont le sommet atteint Y200,
sur une grille horizontale de 12 blocs et verticale de 2 ; il ne sagit pas
dun inventaire de chaque bloc. Les coupes sont tracées avec des proportions
réelles et des échelles communes, sans interpolation daffichage.
L’épaisseur du toit ne saméliore pas partout par rapport à la 30.4 : la part
des toits de huit blocs ou moins passe notamment de 10,2 à 11,5 % dans le
Grand 0, sur les colonnes supérieures communes. Le changement recherché est
le retrait de l’étirement et des grandes coques hautes, **pas le remplissage
des cavités naturelles**. Les rapports complets conservent ces contre-indications.
Le calcul historique 30.4 et le calcul 30.1 sont comparés au relevé de la
30.4 livrée, Moyen 0, aux mêmes coordonnées : **79 860 signes de densité
identiques par variante, aucune différence**. Les témoins historiques de
latlas correspondent donc aux archives effectivement produites précédemment.
Reçu local : `build/alpha305-historical-equivalence.json`.
Larchive contient neuf paires de planches PNG/PDF, les neuf JSON natifs,
leurs résumés et empreintes, le script Matplotlib et ses dépendances.
Voir [la méthode datlas](terrain-atlas.md).
## Validation des chunks
`./gradlew check build` réussit en **7 min 32 s**. Les vérifications complètes
utilisent le Grand 1 024, graine `-7228211907433324401`, PID 74819 : **24/24**.
- Traversée de 374 blocs, 23 départs locaux, 52 chunks FULL ; un parcours
effectif de 532 pas relie ses deux accès en respectant le volume de collision
dun joueur. 71 cibles de passage sont accessibles.
- Sanctuaire minéral : départ natif, sol central et espace du gardien présents.
- Rivière courbe à Y243 ; huit cellules deau observées sous les ouvertures
après ticks vanilla. Les témoins du lit conservent leur eau.
- Quatre lieux aériens, quatre coffres, deux villageois ; la cascade de l’îlot
et sa végétation sont vérifiées dans les chunks générés.
- Le bloc à Y639 est accepté, Y640 refusé. Le journal dexpansion ne change pas.
- 7 436 échantillons de densité profonde sont identiques à la 30.1. Le volume
de contrôle passe de 12 petits composants détachés à zéro ; 1 536 points
restent identiques en ordre inverse de lecture.
Preuves : `build/alpha305-check-build.log` et `build/alpha305-evidence/large-user/`.
Le **Moyen 724, graine 0**, PID 75795, passe également **24/24** en 4 min 27 s.
Sa traversée de 272 blocs compte 19 départs et 30 chunks FULL, avec un parcours
de 426 pas entre les accès. Le sanctuaire minéral et les quatre lieux aériens
sont présents, avec quatre coffres et deux villageois. Rivière et lac restent
en eau à Y247 et Y251. Aucune brèche de berge nest retenue par le plan sur
ce témoin : aucune cascade fluviale nest donc revendiquée pour ce cas.
La cascade de l’îlot reste contrôlée séparément.
Les 5 819 échantillons profonds restent identiques ; le volume ciblé passe
de cinq petits composants détachés à zéro, avec 1 536 points identiques en
ordre inverse. Preuves : `build/alpha305-medium0.log` et
`build/alpha305-evidence/medium0/`.
`./gradlew assemblePack -x :sanctuary:check` réussit après le `check build`,
sans répéter les contrôles déjà passés. Le JAR, le MRpack et le ZIP damorçage
sont vérifiés localement : versions, Minecraft exact, codec, hauteur de 640,
dépendances épinglées et intégrité des archives. Les téléchargements publics
et la synchronisation Prism font lobjet de reçus de distribution séparés
référencés dans [packwiz](packwiz.md).
## Limites
Les coupes ne montrent ni la végétation, ni leau, ni les structures. Elles ne
prouvent pas la connectivité 3D des vides. Les grottes naturelles peuvent
encore produire des voûtes minces et des arches. Les tests de fragments portent
sur un volume ciblé à résolution dun bloc, pas sur chaque bloc du monde.
Aucun test de rendu client ou shader nouveau nest revendiqué dans cette passe.
Les contrôles fonctionnels du Blocodex sont côté serveur ; son interface na
pas été modifiée. Aucun essai Windows, aucune activation dexpansion ni
modification dune sauvegarde personnelle. Créer un nouveau monde.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20 loom_version=1.17.20
fabric_api_version=0.160.0+26.3 fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.30.4 mod_version=0.1.0-alpha.30.5
pack_version=0.1.0-alpha.30.4 pack_version=0.1.0-alpha.30.5
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
+7 -1
View File
@@ -36,6 +36,9 @@ tasks.register('cleanGameTestWorld', Delete) {
} }
tasks.named('runGameTest') { tasks.named('runGameTest') {
dependsOn('cleanGameTestWorld') dependsOn('cleanGameTestWorld')
systemProperty('sanctuary.test.atlasOnly', providers.gradleProperty('sanctuaryAtlasOnly').getOrElse('false'))
systemProperty('sanctuary.test.atlasSeeds', providers.gradleProperty('sanctuaryAtlasSeeds').getOrElse('0,42,-7228211907433324401'))
systemProperty('sanctuary.test.atlasRevision', providers.gradleProperty('sanctuaryAtlasRevision').getOrElse('working-tree'))
systemProperty('sanctuary.test.seed', providers.gradleProperty('sanctuaryTestSeed').getOrElse('0')) systemProperty('sanctuary.test.seed', providers.gradleProperty('sanctuaryTestSeed').getOrElse('0'))
systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10')) systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10'))
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample')) systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
@@ -84,6 +87,8 @@ tasks.named('runGameTest') {
} }
tasks.named('processGametestResources') { tasks.named('processGametestResources') {
def atlasOnly = providers.gradleProperty('sanctuaryAtlasOnly').map { it.toBoolean() }.getOrElse(false)
inputs.property('atlasOnly', atlasOnly)
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false) def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
inputs.property('benchmark', benchmark) inputs.property('benchmark', benchmark)
def unified = providers.gradleProperty('sanctuaryUnifiedTests').map { it.toBoolean() }.getOrElse(true) def unified = providers.gradleProperty('sanctuaryUnifiedTests').map { it.toBoolean() }.getOrElse(true)
@@ -127,10 +132,11 @@ tasks.named('processGametestResources') {
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false) def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse) inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') { filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark expand(gametest_entrypoint: atlasOnly ? 'fr.koka.sanctuary.gametest.TerrainAtlas305GameTests' : aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests' ? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests' : unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests') : 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
if (atlasOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
} }
} }
@@ -16,13 +16,13 @@ import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Compares registered native fields; does not place blocks or load additional chunks. */ /** Compares registered native fields; does not place blocks or load additional chunks. */
public final class Terraces29WorldGameTests { public final class Terraces29WorldGameTests {
@GameTest(maxTicks = 2400) @GameTest(maxTicks = 2400)
public void broadProvincesRestorePlateausAndRaiseNativeMasses(GameTestHelper helper) throws Exception { public void broadProvincesPreservePlateausWithoutStretchingCaves(GameTestHelper helper) throws Exception {
var level = helper.getLevel(); var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator(); var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION) var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, .getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + generator.capacity().players()))).value(); SanctuaryMod.id("population_" + generator.capacity().players()))).value();
helper.assertTrue(field.balanced303() && !field.terraces29() && !field.surface302(), "Alpha30.4 removes fine bands and uses broad geological provinces"); helper.assertTrue(field.balanced303() && !field.terraces29() && !field.surface302(), "Alpha30.5 keeps broad plateaus and a moderate native 3D field");
var random = generator.starterContext().random(); var random = generator.starterContext().random();
var current = random.getSampler(field); var previous = random.getSampler(field.legacy301()); var current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
var context = SamplerContext.builder().enableCaches().build(); var context = SamplerContext.builder().enableCaches().build();
@@ -43,7 +43,7 @@ public final class Terraces29WorldGameTests {
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged"); if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
} }
} }
helper.assertTrue(changed > 0, "The upper field now combines broad plateaus and raised rock"); helper.assertTrue(changed > 0, "The upper field now combines broad plateaus and native 3D rock");
report.put("unchanged_lower_density_samples", lowerSamples); report.put("unchanged_lower_density_samples", lowerSamples);
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed); report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
var sections = new LinkedHashMap<String, Object>(); var sections = new LinkedHashMap<String, Object>();
@@ -123,7 +123,7 @@ public final class Terraces29WorldGameTests {
} }
report.put("sizes",sizes); report.put("sizes",sizes);
report.put("passed", true); report.put("passed", true);
Path path = Path.of("diagnostics/alpha304", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json"); Path path = Path.of("diagnostics/alpha305", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Files.createDirectories(path.getParent()); Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report)); Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed(); helper.succeed();
@@ -0,0 +1,111 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.Gson;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.Interval;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Scientific export of native registered fields, before fluids, surfaces and structures.
* Flat test bootstrap only: no Sanctuary chunks or expansion reservations are needed. */
public final class TerrainAtlas305GameTests {
@GameTest(maxTicks = 120000)
public void exportNativeAtlas(GameTestHelper helper) throws Exception {
var registries = helper.getLevel().registryAccess();
String version = FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow()
.getMetadata().getVersion().getFriendlyString();
for (String seedText : System.getProperty("sanctuary.test.atlasSeeds", "0").split(",")) {
long seed = Long.parseLong(seedText.trim());
for (int players : new int[]{5, 10, 20}) {
long started = System.nanoTime();
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("unified_" + players))).value();
var random = RandomState.create(registries.lookupOrThrow(Registries.NOISE), seed, settings);
var field = (PopulationIslandDensity) registries.lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + players))).value();
int radius = (int)Math.round(field.capacity().radius());
int bound = ((radius + 32 + 11) / 12) * 12;
var functions = new LinkedHashMap<String, DensityFunction>();
functions.put("plateaus27", field.legacy27());
functions.put("global301", field.legacy301());
functions.put("stretch304", new Historical304(field));
functions.put("current305", field);
var variants = new LinkedHashMap<String, Object>();
for (var entry : functions.entrySet()) {
var sampler = random.getSampler(entry.getValue());
var context = SamplerContext.builder().enableCaches().build();
var columns = new ArrayList<Object>();
for (int z = -bound; z <= bound; z += 12) for (int x = -bound; x <= bound; x += 12) {
int top = 0, rock = 0, skin = 0;
boolean roof = false;
for (int y = 382; y >= 2; y -= 2) {
boolean solid = sampler.sampleValue(context, x, y, z) > 0;
if (solid) {
rock += 2;
if (top == 0) { top = y; roof = true; }
if (roof) skin += 2;
} else if (top != 0) roof = false;
}
columns.add(List.of(x, z, top, rock, skin));
}
var sections = new ArrayList<Object>();
for (int z : new int[]{0, 96, -(radius / 4)}) {
var rows = new ArrayList<Object>();
for (int y = 382; y >= 0; y -= 2) {
var row = new ArrayList<Integer>();
for (int x = -bound; x <= bound; x += 2) {
float density = sampler.sampleValue(context, x, y, z);
helper.assertTrue(Float.isFinite(density), "Native density is finite");
// Quantization preserves the sign around the zero isosurface.
int encoded = Math.round(Math.clamp(density, -1, 1) * 10000);
if (density > 0 && encoded == 0) encoded = 1;
if (density < 0 && encoded == 0) encoded = -1;
row.add(encoded);
}
rows.add(row);
}
sections.add(Map.of("z", z, "min_x", -bound, "max_y", 382, "step", 2, "density", rows));
}
variants.put(entry.getKey(), Map.of("columns", columns, "sections", sections));
}
var report = new LinkedHashMap<String, Object>();
report.put("schema", 1); report.put("version", version);
report.put("revision", System.getProperty("sanctuary.test.atlasRevision", "working-tree"));
report.put("minecraft", "26.3-pre-2"); report.put("seed", Long.toString(seed));
report.put("diameter", radius * 2); report.put("process_id", ProcessHandle.current().pid());
report.put("domain_y", List.of(0, 383)); report.put("build_ceiling_exclusive", 640);
report.put("map_step", 12); report.put("vertical_step", 2); report.put("density_scale", 10000);
report.put("columns", List.of("x", "z", "top_y", "sampled_solid_thickness", "uninterrupted_roof_thickness"));
report.put("scope", "Native density, before fluids, surface materials, vegetation and structures; no chunks generated for this export");
report.put("variants", variants);
report.put("export_seconds", (System.nanoTime() - started) / 1e9);
Path path = Path.of("diagnostics/atlas305", "terrain-" + radius * 2 + "-" + seed + ".json");
Files.createDirectories(path.getParent()); Files.writeString(path, new Gson().toJson(report));
SanctuaryMod.LOGGER.info("Terrain atlas exported {} in {} s", path, report.get("export_seconds"));
}
}
helper.succeed();
}
private record Historical304(PopulationIslandDensity base) implements DensityFunction {
@Override public DensitySampler compileSampler(CompileContext context) { return base.compileHistorical304(context); }
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new Historical304((PopulationIslandDensity)rule.rewrite(base));
}
@Override public Interval range() { return base.range(); }
@Override public int domainAxes() { return ALL_AXES; }
// Never serialized: this wrapper only selects the exact diagnostic sampler.
@Override public MapCodec<? extends DensityFunction> codec() { return base.codec(); }
}
}
@@ -46,6 +46,7 @@ public abstract class SanctuaryGameTestServerMixin {
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target = @Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;")) "Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() { private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.atlasOnly")) return net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT_ALL_DIMENSIONS;
if (Boolean.getBoolean("sanctuary.test.unified")) { if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 24); int generation = Integer.getInteger("sanctuary.test.generation", 24);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24) throw new IllegalArgumentException("Unsupported test generation"); if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24) throw new IllegalArgumentException("Unsupported test generation");
@@ -68,7 +69,7 @@ public abstract class SanctuaryGameTestServerMixin {
private static net.minecraft.world.level.levelgen.WorldDimensions sanctuary$customizeSize(WorldPreset preset, private static net.minecraft.world.level.levelgen.WorldDimensions sanctuary$customizeSize(WorldPreset preset,
net.minecraft.world.level.LevelSettings settings, net.minecraft.server.WorldLoader.DataLoadContext context) { net.minecraft.world.level.LevelSettings settings, net.minecraft.server.WorldLoader.DataLoadContext context) {
var dimensions = preset.createWorldDimensions(); var dimensions = preset.createWorldDimensions();
if (Boolean.getBoolean("sanctuary.test.unified")) { if (Boolean.getBoolean("sanctuary.test.unified") && !Boolean.getBoolean("sanctuary.test.atlasOnly")) {
var generator = (fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator) dimensions.overworld(); var generator = (fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator) dimensions.overworld();
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(), return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724)) generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
@@ -52,8 +52,7 @@ public final class PopulationHydrologyRuntime {
return sampler.sampleValue(SamplerContext.EMPTY_UNCACHED, (int) wx, y, (int) wz); return sampler.sampleValue(SamplerContext.EMPTY_UNCACHED, (int) wx, y, (int) wz);
}); });
long seed = PopulationIslandDensity.regionSeed(state.seed(), region); long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
var water = (current30(generator) ? PopulationHydrology.withHighPlateaus304(seed, memo) var water = PopulationHydrology.create(seed, memo).admitted();
: PopulationHydrology.create(seed, memo)).admitted();
if (current30(generator)) { if (current30(generator)) {
water = BankOutlets30.openBanks(seed, water, memo, 6); water = BankOutlets30.openBanks(seed, water, memo, 6);
} }
@@ -49,6 +49,13 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
} }
@Override public DensitySampler compileSampler(CompileContext context) { @Override public DensitySampler compileSampler(CompileContext context) {
return compileSampler(context, false);
}
/** Exact rejected30.4 field retained for the reproducible scientific comparison. */
public DensitySampler compileHistorical304(CompileContext context) {
return compileSampler(context, true);
}
private DensitySampler compileSampler(CompileContext context, boolean historical304) {
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context); var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context); var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context); var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
@@ -57,7 +64,10 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
var ridges = geology28 ? ridges28.compileSampler(context) : null; var ridges = geology28 ? ridges28.compileSampler(context) : null;
var erosion = geology28 ? erosion28.compileSampler(context) : null; var erosion = geology28 ? erosion28.compileSampler(context) : null;
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong(); long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
var balanced = balanced303 ? new HighPlateaus304(coarse, capacity.radius(), seed) : null; var balanced = balanced303 && historical304 ? new HighPlateaus304(coarse, capacity.radius(), seed) : null;
// A broad mask changes the strength of the native 3D deformation only. No Y stretch,
// union of two shells, summit cap or fine surface noise in the current profile.
var plateaus = balanced303 && !historical304 ? new BalancedRelief303(coarse, capacity.radius(), seed) : null;
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>(); var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
DensitySampler field = new DensitySampler() { DensitySampler field = new DensitySampler() {
private float terrainAt(SamplerContext sample, double x, double y, double z) { private float terrainAt(SamplerContext sample, double x, double y, double z) {
@@ -83,7 +93,8 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
double province = balanced != null ? balanced.province(x, z) : 0; double province = balanced != null ? balanced.province(x, z) : 0;
double mappedY = balanced != null && province > 0 ? HighPlateaus304.sourceY(y, province) : y; double mappedY = balanced != null && province > 0 ? HighPlateaus304.sourceY(y, province) : y;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0; double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
if (balanced != null) weight *= province; if (plateaus != null) weight *= .8 * plateaus.province(x, z);
else if (balanced != null) weight *= province;
else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius()); else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius()) double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
* Relief30.influence(seed, x, z, capacity.radius()) : 0; * Relief30.influence(seed, x, z, capacity.radius()) : 0;
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "0.1.0-alpha.30.4" version = "0.1.0-alpha.30.5"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Freeze a rendered native atlas with raw measurements, source revision and build hash."""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import zipfile
ROOT = Path(__file__).resolve().parents[1]
def sha(data):
return hashlib.sha256(data).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--raw", type=Path, required=True)
parser.add_argument("--rendered", type=Path, required=True)
parser.add_argument("--jar", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.output.exists():
raise SystemExit("Refusing to overwrite an existing immutable atlas")
status = subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT, text=True)
if status.strip():
raise SystemExit("Commit atlas code and documentation before freezing the archive")
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
jar_bytes = args.jar.read_bytes()
with zipfile.ZipFile(args.jar) as jar:
if jar.testzip() is not None:
raise SystemExit("Invalid built JAR")
version = json.loads(jar.read("fabric.mod.json"))["version"]
files = {}
cases = []
for source in sorted(args.raw.glob("terrain-*.json")):
raw = source.read_bytes()
data = json.loads(raw)
if data["version"] != version:
raise SystemExit(f"Stale native export: {source}")
rendered = args.rendered / source.stem
provenance = json.loads((rendered / "provenance.json").read_text())
if provenance["source_sha256"] != sha(raw):
raise SystemExit(f"Rendered atlas differs from source: {source}")
if provenance["script_sha256"] != sha((ROOT / "scripts/terrain_atlas.py").read_bytes()):
raise SystemExit("Rendering script changed after rendering")
files["raw/" + source.name] = raw
for name in ("maps.png", "maps.pdf", "sections.png", "sections.pdf", "summary.json", "provenance.json"):
files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
cases.append({"seed": data["seed"], "diameter": data["diameter"], "directory": source.stem})
identities = {(c["seed"], c["diameter"]) for c in cases}
if not cases or len(identities) != len(cases):
raise SystemExit("Empty atlas or duplicate seed/size")
for name in ("scripts/terrain_atlas.py", "scripts/terrain-atlas-requirements.txt",
"scripts/archive_terrain_atlas.py", "docs/terrain-atlas.md",
"docs/generation-alpha30.5.md", "docs/testing-alpha30.5.md"):
files[name] = (ROOT / name).read_bytes()
rows = ["# Sanctuary — atlas " + version, "", f"Source : `{commit}`.", "",
"Mesures natives avant décoration ; voir docs/terrain-atlas.md pour les échelles et limites.", "",
"| Diamètre | Graine | Cartes | Coupes |", "|---|---|---|---|"]
for c in cases:
base = "figures/" + c["directory"]
rows.append(f"| {c['diameter']} | {c['seed']} | [PNG]({base}/maps.png) / [PDF]({base}/maps.pdf) | [PNG]({base}/sections.png) / [PDF]({base}/sections.pdf) |")
rows += ["", "Reproduction des figures dans un environnement Python isolé :", "", "```sh",
"python -m pip install -r scripts/terrain-atlas-requirements.txt",
"python scripts/terrain_atlas.py --output rendered raw/terrain-*.json", "```", ""]
files["README.md"] = "\n".join(rows).encode()
files["manifest.json"] = (json.dumps({"version": version, "source_commit": commit,
"jar": args.jar.name, "jar_sha256": sha(jar_bytes), "cases": cases,
"files_sha256": {name: sha(content) for name, content in sorted(files.items())}}, indent=2) + "\n").encode()
args.output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(args.output, "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, content in sorted(files.items()):
info = zipfile.ZipInfo(name, date_time=(2000, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, content)
with zipfile.ZipFile(args.output) as archive:
assert archive.testzip() is None
for name, content in files.items():
assert archive.read(name) == content
print(json.dumps({"path": str(args.output), "cases": len(cases), "source_commit": commit,
"sha256": sha(args.output.read_bytes()), "bytes": args.output.stat().st_size}))
if __name__ == "__main__":
main()
+2
View File
@@ -0,0 +1,2 @@
matplotlib==3.11.2
numpy==2.5.3
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Render TerrainAtlas305GameTests native exports. No synthesized terrain or image smoothing.
python scripts/terrain_atlas.py --output build/atlas305 <native-json> [<native-json> ...]
See docs/terrain-atlas.md for acquisition, provenance and sampling limits.
"""
import argparse
import hashlib
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
import numpy as np
LABELS = {
"plateaus27": "Plateaux 24 / 27",
"global301": "Bruit 3D global · 30.1",
"stretch304": "Étirement vertical · 30.4",
"current305": "Plateaux + déformation 3D · 30.5",
}
def save(fig, directory, name):
fig.savefig(directory / f"{name}.png", dpi=150, facecolor="white")
fig.savefig(directory / f"{name}.pdf", facecolor="white")
plt.close(fig)
def render(source, output):
raw = source.read_bytes()
data = json.loads(raw)
if data["schema"] != 1 or list(data["variants"]) != list(LABELS):
raise ValueError(f"Unsupported atlas schema/variants: {source}")
target = output / source.stem
target.mkdir(parents=True, exist_ok=True)
title = f"Sanctuary · diamètre {data['diameter']} · graine {data['seed']}"
provenance = (f"Champ natif Minecraft {data['minecraft']} · {data['version']} · {data['revision']}\n"
"Avant eau, matériaux de surface, végétation et structures. Le plafond de construction reste Y640.")
variants = list(data["variants"].values())
maps = []
for variant in variants:
columns = np.asarray(variant["columns"], dtype=float)
xs, zs = np.unique(columns[:, 0]), np.unique(columns[:, 1])
if columns.shape[0] != len(xs) * len(zs):
raise ValueError("Incomplete native map grid")
maps.append(columns.reshape(len(zs), len(xs), 5))
step = data["map_step"]
extent = (xs[0] - step / 2, xs[-1] + step / 2, zs[-1] + step / 2, zs[0] - step / 2)
fig, axes = plt.subplots(3, 4, figsize=(18, 14), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nRelief et épaisseur de roche", fontsize=18)
metrics = [(2, "Altitude du sommet (Y)", "terrain", 0, 360),
(3, "Épaisseur solide cumulée (blocs)", "viridis", 0, 300),
(4, "Roche continue sous le sommet (blocs)", "magma", 0, 100)]
for row, (index, label, cmap, vmin, vmax) in enumerate(metrics):
for col, (key, grid) in enumerate(zip(LABELS, maps)):
ax = axes[row, col]
values = np.ma.masked_where(grid[:, :, 2] == 0, grid[:, :, index])
image = ax.imshow(values, extent=extent, origin="upper", interpolation="nearest",
cmap=cmap, vmin=vmin, vmax=vmax, aspect="equal")
ax.set_facecolor("#e8edf0")
ax.set_xlabel("X (blocs)")
if col == 0:
ax.set_ylabel("Z (blocs) · nord en haut")
if row == 0:
ax.set_title(LABELS[key], fontsize=11)
ax.plot(0, 0, "+", color="black", ms=8, label="Origine")
fig.colorbar(image, ax=axes[row, :], shrink=.85, label=label, extend="max")
fig.supxlabel(f"Pas horizontal : {step} blocs · vertical : {data['vertical_step']} blocs · gris = aucun solide échantillonné.\n" + provenance, fontsize=9)
save(fig, target, "maps")
section_count = len(variants[0]["sections"])
fig, axes = plt.subplots(4, section_count, figsize=(20, 12), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nCoupes de densité · proportions spatiales conservées", fontsize=18)
for row, (key, variant) in enumerate(zip(LABELS, variants)):
for col, section in enumerate(variant["sections"]):
ax = axes[row, col]
density = np.asarray(section["density"], dtype=float) / data["density_scale"]
dy = section["step"]
x = section["min_x"] + np.arange(density.shape[1]) * dy
y = section["max_y"] - np.arange(density.shape[0]) * dy
image = ax.imshow(density, extent=(x[0]-dy/2, x[-1]+dy/2, y[-1]-dy/2, y[0]+dy/2),
interpolation="nearest", cmap="RdBu_r", norm=TwoSlopeNorm(0, -.3, .3), aspect="equal")
ax.contour(x, y, density, levels=[0], colors="#262626", linewidths=.35)
if row == 0:
ax.set_title(f"Plan vertical Z = {section['z']}")
if col == 0:
ax.set_ylabel(LABELS[key] + "\nY (blocs)", fontsize=10)
ax.set_xlabel("X (blocs)")
ax.set_ylim(0, 384)
fig.colorbar(image, ax=axes, shrink=.6, label="Densité sans unité · bleu : vide (≤ 0) · rouge : roche (> 0)", extend="both")
fig.supxlabel("Coupes échantillonnées tous les 2 blocs, sans interpolation daffichage ; isosurface zéro en noir.\n" + provenance, fontsize=9)
save(fig, target, "sections")
summary = {"seed": data["seed"], "diameter": data["diameter"], "variants": {}}
base = maps[0][:, :, 2]
common = np.logical_and.reduce([m[:, :, 2] >= 200 for m in maps])
for key, grid in zip(LABELS, maps):
land = grid[:, :, 2] > 0
uplands = base >= 200
summary["variants"][key] = {
"sampled_land_columns": int(land.sum()),
"max_sampled_y": float(grid[:, :, 2].max()),
"original_uplands_within_2_blocks_fraction": float((np.abs(grid[:, :, 2]-base)[uplands] <= 2).mean()),
"common_upper_columns": int(common.sum()),
"median_roof_thickness_common_upper_columns": float(np.median(grid[:, :, 4][common])),
"roof_at_most_8_blocks_common_upper_fraction": float((grid[:, :, 4][common] <= 8).mean()),
}
(target / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
(target / "provenance.json").write_text(json.dumps({
"source": source.name, "source_sha256": hashlib.sha256(raw).hexdigest(),
"script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"matplotlib": matplotlib.__version__, "numpy": np.__version__,
"version": data["version"], "revision": data["revision"], "process_id": data["process_id"],
"scope": data["scope"], "map_step": step, "vertical_step": data["vertical_step"],
"density_quantization": 1 / data["density_scale"], "density_plot_saturation": [-.3, .3],
}, indent=2) + "\n")
print(json.dumps(summary))
return summary
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("sources", nargs="+", type=Path)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
summaries = [render(source, args.output) for source in args.sources]
(args.output / "summary.json").write_text(json.dumps(summaries, indent=2) + "\n")
if __name__ == "__main__":
main()