Restore native plateaus and archive terrain comparisons for alpha.30.5
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -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
|
||||
[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 n’est 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), [l’outil d’atlas](docs/terrain-atlas.md)
|
||||
et [les vérifications](docs/testing-alpha30.5.md). **Nouveau monde requis.**
|
||||
|
||||
## Alpha.30.4 — massifs et ciel constructible
|
||||
|
||||
Les plateaux bas sont conservés ; les reliefs hauts se raccordent par des
|
||||
|
||||
@@ -1037,3 +1037,13 @@ courbes sur les replats supérieurs et libérer de l’espace constructible jusq
|
||||
Y639. Stabiliser un seul relief avant d’ajouter des personnalisations.
|
||||
L’arè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).
|
||||
|
||||
### 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 d’eau,
|
||||
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 [l’atlas](terrain-atlas.md).
|
||||
|
||||
@@ -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 n’est 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. L’arrivé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 l’union entre terrain étiré et terrain bas
|
||||
sont retirés du profil courant. Les sommets retrouvent la hauteur produite par
|
||||
le champ natif ; **Y360 n’est 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 d’altitude 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.
|
||||
|
||||
L’ancienne 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 n’est réalisée : **créer un nouveau
|
||||
monde**. Cette révision des profils d’essai n’offre pas de continuité du terrain
|
||||
pour les chunks futurs d’une ancienne sauvegarde.
|
||||
|
||||
Les îles supérieures, expansions choisies à partir de la graine et histoires
|
||||
d’expédition seront définies plus tard. Aucun récit, événement ou continent
|
||||
supplémentaire n’est injecté dans cette livraison.
|
||||
|
||||
Résultats et limites : [vérifications alpha.30.5](testing-alpha30.5.md).
|
||||
@@ -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 n’est 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 d’en faire une archive. Aucune sauvegarde personnelle n’est utilisée.
|
||||
|
||||
## Lecture des planches
|
||||
|
||||
- Cartes : nord en haut, X et Z en blocs, rapport d’aspect réel. Les quatre
|
||||
profils partagent les mêmes échelles de couleur. La palette d’altitude ne
|
||||
représente pas les blocs ou les biomes ; du blanc n’implique pas de neige.
|
||||
- Altitude : dernier solide rencontré, recherche verticale par pas de 2 blocs.
|
||||
Une cellule grise signifie qu’aucun solide n’a été échantillonné dans la colonne.
|
||||
- Épaisseur cumulée : nombre d’échantillons solides multiplié par 2 sur Y2..382.
|
||||
Ce n’est ni une densité physique, ni un volume exact de matériaux minables.
|
||||
- Roche continue : épaisseur depuis le sommet jusqu’au 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 d’un bloc dans les tests complets.
|
||||
|
||||
L’eau, 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é d’une 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.
|
||||
|
||||
L’archive 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 n’intervient.
|
||||
|
||||
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.
|
||||
@@ -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. L’atlas 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 n’est
|
||||
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 s’agit pas
|
||||
d’un inventaire de chaque bloc. Les coupes sont tracées avec des proportions
|
||||
réelles et des échelles communes, sans interpolation d’affichage.
|
||||
|
||||
L’épaisseur du toit ne s’amé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
|
||||
l’atlas correspondent donc aux archives effectivement produites précédemment.
|
||||
Reçu local : `build/alpha305-historical-equivalence.json`.
|
||||
|
||||
L’archive 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 d’atlas](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
|
||||
d’un 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 d’eau 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 d’expansion 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 n’est retenue par le plan sur
|
||||
ce témoin : aucune cascade fluviale n’est 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 d’amorç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 l’objet 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 l’eau, 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 d’un bloc, pas sur chaque bloc du monde.
|
||||
Aucun test de rendu client ou shader nouveau n’est revendiqué dans cette passe.
|
||||
Les contrôles fonctionnels du Blocodex sont côté serveur ; son interface n’a
|
||||
pas été modifiée. Aucun essai Windows, aucune activation d’expansion ni
|
||||
modification d’une sauvegarde personnelle. Créer un nouveau monde.
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.30.4
|
||||
pack_version=0.1.0-alpha.30.4
|
||||
mod_version=0.1.0-alpha.30.5
|
||||
pack_version=0.1.0-alpha.30.5
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -36,6 +36,9 @@ tasks.register('cleanGameTestWorld', Delete) {
|
||||
}
|
||||
tasks.named('runGameTest') {
|
||||
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.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10'))
|
||||
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
|
||||
@@ -84,6 +87,8 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
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)
|
||||
inputs.property('benchmark', benchmark)
|
||||
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)
|
||||
inputs.property('voidReuse', voidReuse)
|
||||
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'
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
if (atlasOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -16,13 +16,13 @@ import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
/** Compares registered native fields; does not place blocks or load additional chunks. */
|
||||
public final class Terraces29WorldGameTests {
|
||||
@GameTest(maxTicks = 2400)
|
||||
public void broadProvincesRestorePlateausAndRaiseNativeMasses(GameTestHelper helper) throws Exception {
|
||||
public void broadProvincesPreservePlateausWithoutStretchingCaves(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
||||
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 current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
|
||||
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");
|
||||
}
|
||||
}
|
||||
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_upper_samples", unchanged); report.put("changed_upper_samples", changed);
|
||||
var sections = new LinkedHashMap<String, Object>();
|
||||
@@ -123,7 +123,7 @@ public final class Terraces29WorldGameTests {
|
||||
}
|
||||
report.put("sizes",sizes);
|
||||
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.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.succeed();
|
||||
|
||||
+111
@@ -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(); }
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -46,6 +46,7 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
|
||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||
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")) {
|
||||
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");
|
||||
@@ -68,7 +69,7 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
private static net.minecraft.world.level.levelgen.WorldDimensions sanctuary$customizeSize(WorldPreset preset,
|
||||
net.minecraft.world.level.LevelSettings settings, net.minecraft.server.WorldLoader.DataLoadContext context) {
|
||||
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();
|
||||
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
|
||||
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
|
||||
|
||||
+1
-2
@@ -52,8 +52,7 @@ public final class PopulationHydrologyRuntime {
|
||||
return sampler.sampleValue(SamplerContext.EMPTY_UNCACHED, (int) wx, y, (int) wz);
|
||||
});
|
||||
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
||||
var water = (current30(generator) ? PopulationHydrology.withHighPlateaus304(seed, memo)
|
||||
: PopulationHydrology.create(seed, memo)).admitted();
|
||||
var water = PopulationHydrology.create(seed, memo).admitted();
|
||||
if (current30(generator)) {
|
||||
water = BankOutlets30.openBanks(seed, water, memo, 6);
|
||||
}
|
||||
|
||||
+13
-2
@@ -49,6 +49,13 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
}
|
||||
|
||||
@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 coarse = sculpt.compileSampler(context); var fine = detail.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 erosion = geology28 ? erosion28.compileSampler(context) : null;
|
||||
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>();
|
||||
DensitySampler field = new DensitySampler() {
|
||||
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 mappedY = balanced != null && province > 0 ? HighPlateaus304.sourceY(y, province) : y;
|
||||
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());
|
||||
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
|
||||
* Relief30.influence(seed, x, z, capacity.radius()) : 0;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.30.4"
|
||||
version = "0.1.0-alpha.30.5"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,2 @@
|
||||
matplotlib==3.11.2
|
||||
numpy==2.5.3
|
||||
@@ -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 d’affichage ; 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()
|
||||
Reference in New Issue
Block a user