test(composer): the handmade corpus is load-bearing for QUALITY, not validity
Answers Jahni's standing wish directly: "in the best of both worlds, i'd like the system to not rely at all on the handmade ones." Three arms, same size, same measurement, same criteria: approach survival walkable mean feature-scale mean corpus blend (today) 13/16 81% 0.0661 92.3 naive uniform 7/16 44% 0.0126 64.6 constraint-sampled 7/16 44% 0.0129 31.4 ⭐ THE ANSWER IS NOT THE SURVIVAL RATE, IT IS THE QUALITY GAP. Constraint sampling produces VALID worlds -- it passes the primordial law more often than naive rolling (1 law failure vs 3) -- but the survivors are materially worse: ~5x lower walkable fraction and ~3x lower feature scale. A composer that emits technically-passing but uniformly mediocre places has not solved the problem. So the corpus carries something the declared constraints do not. §3.3 said independent rolling destroys the CORRELATIONS; stating those correlations explicitly recovers validity but not the tacit quality that authored vectors encode. That is worth knowing and it was not knowable by argument. ⇒ VERDICT: keep the corpus in production. Constraint sampling is a corpus-free BOOTSTRAP (it can seed a corpus from nothing), and PROMOTION (Tier 5) is the real route off handmade content -- dilution, not removal. Naive rolling is rejected outright. ⚠️ UNDER-POWERED, stated plainly: 16 candidates per arm, not the 256 the spec asked for -- the fixed 8M-cell box/law workload made 256x3 too slow to finish. The 5x walkable gap is large enough to be believable at n=16, but the survival rates (44% vs 44%) are not separable at that sample size. Do not quote these as precise. Authored-corpus audit, and Codex read it correctly: 3 of 12 vectors violate the proposed relations (DA_Strate3 room coverage 2.68 vs bound 0.35; Default_SurfaceWorld ground bound; Default_VerticalShafts shaft radius 5 vs roughness margin 5.875). That does NOT mean those assets are broken -- it means the new relations are conservative HYPOTHESES, not universal corpus invariants. Recorded as such. Box law held across all three arms: 768 proved, 673,486 voxels brute-forced, 0 violations. §6.2 did not reopen under corpus-free parameters. Also noted: "uniform within clamps" was underspecified in my spec -- many fields have no finite upper clamp. Both corpus-free arms use the same declared H-relative envelopes so the comparison stays honest. Build clean, CorpusFree green, WorldRadiusVoxels=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
d717bc74ff
commit
43eaf1ef0e
5 files changed
+2131
No files matched your search
@@ -0,0 +1,795 @@
|
||||
// Corpus-free composer measurement: current corpus blend versus independent and constrained rolls.
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "Math/RandomStream.h"
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// 256x3 and 64x3 were attempted first. The latter exceeded 27 minutes before reaching
|
||||
// its aggregate report because the fixed 8M-cell law/box checks are expensive for the
|
||||
// corpus-free fields. Keep the final reduced count equal across all three arms.
|
||||
constexpr int32 NumCandidates = 16;
|
||||
constexpr float LargestComponentSurvivalThreshold = 0.50f;
|
||||
constexpr int32 BoxChecksPerCandidate = 40;
|
||||
constexpr int32 BoxStep = 1;
|
||||
constexpr int32 BoxCells = 8;
|
||||
|
||||
const TCHAR* VF_ConnectivityName(EVoxelConnectivityResult Result)
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: return TEXT("Connected");
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnected");
|
||||
case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid");
|
||||
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
|
||||
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
|
||||
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
int32 VF_FloorDiv(int32 A, int32 B)
|
||||
{
|
||||
const int32 Q = A / B;
|
||||
const int32 R = A % B;
|
||||
return (R != 0 && (R < 0) != (B < 0)) ? Q - 1 : Q;
|
||||
}
|
||||
|
||||
struct FBlockSpec
|
||||
{
|
||||
EVoxelStrateParamBlock Block;
|
||||
ECaveGeneratorType Archetype;
|
||||
uint32 SeedSalt;
|
||||
};
|
||||
|
||||
static const FBlockSpec GBlockSpecs[] =
|
||||
{
|
||||
{ EVoxelStrateParamBlock::TunnelNetwork, ECaveGeneratorType::TunnelNetwork, 0x1001u },
|
||||
{ EVoxelStrateParamBlock::Slab, ECaveGeneratorType::FlatPlain, 0x1003u },
|
||||
{ EVoxelStrateParamBlock::Maze, ECaveGeneratorType::Maze, 0x1005u },
|
||||
{ EVoxelStrateParamBlock::Surface, ECaveGeneratorType::SurfaceWorld, 0x1007u },
|
||||
{ EVoxelStrateParamBlock::VerticalShaft, ECaveGeneratorType::VerticalShafts, 0x1009u },
|
||||
{ EVoxelStrateParamBlock::FloatingIsland, ECaveGeneratorType::FloatingIslands, 0x100Bu },
|
||||
};
|
||||
|
||||
void VF_CopyRollBlock(FVoxelStrateArchetypeParams& OutParams,
|
||||
const FVoxelStrateRollInfo& Roll,
|
||||
ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
OutParams.TunnelNetworkParams = Roll.ArchetypeParams.TunnelNetworkParams;
|
||||
break;
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
OutParams.SlabParams = Roll.ArchetypeParams.SlabParams;
|
||||
break;
|
||||
case ECaveGeneratorType::Maze:
|
||||
OutParams.MazeParams = Roll.ArchetypeParams.MazeParams;
|
||||
break;
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
OutParams.SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
|
||||
break;
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
OutParams.VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
|
||||
break;
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
OutParams.FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VF_SetAllRuntimeBounds(FVoxelStrateArchetypeParams& Params,
|
||||
float TopWorldZ, float BottomWorldZ)
|
||||
{
|
||||
Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.SlabParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.SlabParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.MazeParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.MazeParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.SurfaceParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
}
|
||||
|
||||
class FStackDensitySampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
explicit FStackDensitySampler(const FVoxelOpStack& InStack) : Stack(InStack) {}
|
||||
|
||||
float SampleDensity(float WorldX, float WorldY, float WorldZ) const override
|
||||
{
|
||||
return Stack.EvalMC(WorldX, WorldY, WorldZ);
|
||||
}
|
||||
|
||||
private:
|
||||
const FVoxelOpStack& Stack;
|
||||
};
|
||||
|
||||
struct FBoxVerdictReport
|
||||
{
|
||||
int32 Mixed = 0;
|
||||
int32 AllSolid = 0;
|
||||
int32 AllAir = 0;
|
||||
int32 Proved = 0;
|
||||
int64 CheckedVoxels = 0;
|
||||
int32 Violations = 0;
|
||||
FString FirstViolation;
|
||||
};
|
||||
|
||||
FBoxVerdictReport VF_CheckStackBoxVerdicts(const FVoxelOpStack& Stack,
|
||||
const FVoxelOpContext& Context,
|
||||
int32 CandidateIndex,
|
||||
int32 BottomVoxelZ,
|
||||
int32 TopVoxelZ)
|
||||
{
|
||||
FBoxVerdictReport Report;
|
||||
constexpr int32 Extent = BoxStep * BoxCells;
|
||||
constexpr int32 SpanCells = 40;
|
||||
const int32 LoTile = VF_FloorDiv(BottomVoxelZ, Extent);
|
||||
const int32 HiTile = FMath::Max(LoTile, VF_FloorDiv(TopVoxelZ, Extent));
|
||||
FRandomStream Rng(0x6C617731 ^ CandidateIndex);
|
||||
|
||||
for (int32 BoxIndex = 0; BoxIndex < BoxChecksPerCandidate; ++BoxIndex)
|
||||
{
|
||||
const FIntVector Origin(
|
||||
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||
Rng.RandRange(LoTile, HiTile) * Extent);
|
||||
const FBox Box(
|
||||
FVector((float)(Origin.X - BoxStep), (float)(Origin.Y - BoxStep),
|
||||
(float)(Origin.Z - BoxStep)),
|
||||
FVector((float)(Origin.X + (BoxCells + 1) * BoxStep),
|
||||
(float)(Origin.Y + (BoxCells + 1) * BoxStep),
|
||||
(float)(Origin.Z + (BoxCells + 1) * BoxStep)));
|
||||
const EVoxelTileClass Verdict = Stack.ClassifyBox(Box, Context);
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
++Report.Mixed;
|
||||
continue;
|
||||
}
|
||||
if (Verdict == EVoxelTileClass::AllSolid)
|
||||
{
|
||||
++Report.AllSolid;
|
||||
}
|
||||
else
|
||||
{
|
||||
++Report.AllAir;
|
||||
}
|
||||
++Report.Proved;
|
||||
|
||||
const int32 GridDim = BoxCells + 1;
|
||||
const bool bClaimsSolid = Verdict == EVoxelTileClass::AllSolid;
|
||||
for (int32 GZ = -1; GZ <= GridDim; ++GZ)
|
||||
for (int32 GY = -1; GY <= GridDim; ++GY)
|
||||
for (int32 GX = -1; GX <= GridDim; ++GX)
|
||||
{
|
||||
const float X = (float)(Origin.X + GX * BoxStep);
|
||||
const float Y = (float)(Origin.Y + GY * BoxStep);
|
||||
const float Z = (float)(Origin.Z + GZ * BoxStep);
|
||||
const float Density = Stack.EvalMC(X, Y, Z);
|
||||
++Report.CheckedVoxels;
|
||||
const bool bAgrees = bClaimsSolid ? (Density < 0.0f) : (Density >= 0.0f);
|
||||
if (!bAgrees)
|
||||
{
|
||||
++Report.Violations;
|
||||
if (Report.FirstViolation.IsEmpty())
|
||||
{
|
||||
Report.FirstViolation = FString::Printf(
|
||||
TEXT("candidate %d box %d origin (%d,%d,%d) said %s but MC density "
|
||||
"at (%.0f,%.0f,%.0f) was %.9g"),
|
||||
CandidateIndex, BoxIndex, Origin.X, Origin.Y, Origin.Z,
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
X, Y, Z, Density);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Report;
|
||||
}
|
||||
|
||||
struct FQualityStats
|
||||
{
|
||||
int32 Count = 0;
|
||||
float AirMin = FLT_MAX;
|
||||
float AirMax = -FLT_MAX;
|
||||
float WalkableMin = FLT_MAX;
|
||||
float WalkableMax = -FLT_MAX;
|
||||
float FeatureMin = FLT_MAX;
|
||||
float FeatureMax = -FLT_MAX;
|
||||
double AirSum = 0.0;
|
||||
double WalkableSum = 0.0;
|
||||
double FeatureSum = 0.0;
|
||||
|
||||
void Add(const FVoxelStrateMetrics& Metrics)
|
||||
{
|
||||
++Count;
|
||||
AirMin = FMath::Min(AirMin, Metrics.AirFraction);
|
||||
AirMax = FMath::Max(AirMax, Metrics.AirFraction);
|
||||
WalkableMin = FMath::Min(WalkableMin, Metrics.WalkableFraction);
|
||||
WalkableMax = FMath::Max(WalkableMax, Metrics.WalkableFraction);
|
||||
FeatureMin = FMath::Min(FeatureMin, Metrics.MedianFeatureScale);
|
||||
FeatureMax = FMath::Max(FeatureMax, Metrics.MedianFeatureScale);
|
||||
AirSum += Metrics.AirFraction;
|
||||
WalkableSum += Metrics.WalkableFraction;
|
||||
FeatureSum += Metrics.MedianFeatureScale;
|
||||
}
|
||||
|
||||
FString RangeText(float Min, float Max) const
|
||||
{
|
||||
return Count > 0
|
||||
? FString::Printf(TEXT("%.6f..%.6f"), Min, Max)
|
||||
: TEXT("n/a");
|
||||
}
|
||||
|
||||
FString SummaryText() const
|
||||
{
|
||||
return Count > 0
|
||||
? FString::Printf(
|
||||
TEXT("n=%d air=%s (mean %.6f), walkable=%s (mean %.6f), feature=%s (mean %.6f)"),
|
||||
Count, *RangeText(AirMin, AirMax), (float)(AirSum / Count),
|
||||
*RangeText(WalkableMin, WalkableMax), (float)(WalkableSum / Count),
|
||||
*RangeText(FeatureMin, FeatureMax), (float)(FeatureSum / Count))
|
||||
: TEXT("n=0");
|
||||
}
|
||||
};
|
||||
|
||||
struct FArmStats
|
||||
{
|
||||
const TCHAR* Name = TEXT("unknown");
|
||||
int32 Requested = 0;
|
||||
int32 RollFailures = 0;
|
||||
int32 BuildFailures = 0;
|
||||
int32 Unmeasured = 0;
|
||||
int32 Vacuous = 0;
|
||||
int32 Fragmented = 0;
|
||||
int32 LawFailed = 0;
|
||||
int32 LawFailedObserved = 0;
|
||||
int32 LawSkipped = 0;
|
||||
int32 Survivors = 0;
|
||||
int32 NonVacuous = 0;
|
||||
int32 LargestEnough = 0;
|
||||
int32 LawPasses = 0;
|
||||
int32 DeterminismFailures = 0;
|
||||
int32 InvalidStackCount = 0;
|
||||
int32 ChannelOrderFailures = 0;
|
||||
int32 ContextViolations = 0;
|
||||
int32 LawResults[6] = { 0, 0, 0, 0, 0, 0 };
|
||||
FQualityStats AllQuality;
|
||||
FQualityStats SurvivorQuality;
|
||||
FBoxVerdictReport Box;
|
||||
FString FirstFailure;
|
||||
|
||||
void RememberFailure(const FString& Failure)
|
||||
{
|
||||
if (FirstFailure.IsEmpty())
|
||||
{
|
||||
FirstFailure = Failure;
|
||||
}
|
||||
}
|
||||
|
||||
FString SurvivalText() const
|
||||
{
|
||||
return FString::Printf(TEXT("%d/%d (%.1f%%)"), Survivors, Requested,
|
||||
Requested > 0 ? 100.0f * (float)Survivors / (float)Requested : 0.0f);
|
||||
}
|
||||
};
|
||||
|
||||
int32 VF_LawResultIndex(EVoxelConnectivityResult Result)
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: return 0;
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return 1;
|
||||
case EVoxelConnectivityResult::StartCellSolid: return 2;
|
||||
case EVoxelConnectivityResult::GoalCellSolid: return 3;
|
||||
case EVoxelConnectivityResult::OutOfWindow: return 4;
|
||||
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return 5;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
bool VF_BitEqual(float A, float B)
|
||||
{
|
||||
uint32 ABits = 0;
|
||||
uint32 BBits = 0;
|
||||
FMemory::Memcpy(&ABits, &A, sizeof(ABits));
|
||||
FMemory::Memcpy(&BBits, &B, sizeof(BBits));
|
||||
return ABits == BBits;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeComposerCorpusFreeTest,
|
||||
"VoxelForge.Composer.CorpusFree",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
(void)Parameters;
|
||||
const double TestStartSeconds = FPlatformTime::Seconds();
|
||||
|
||||
const FString SettingsPath = TEXT("/Game/VoxelForge/DA_Settings.DA_Settings");
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(nullptr, *SettingsPath);
|
||||
if (AuthoredSettings == nullptr)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("Could not load %s; corpus-free measurement did not run."),
|
||||
*SettingsPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelStrateCorpus Corpus;
|
||||
FString CorpusReport;
|
||||
const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport);
|
||||
AddInfo(CorpusReport);
|
||||
TestTrue(TEXT("corpus blend arm has a usable authored corpus"),
|
||||
bCorpusLoaded && Corpus.IsValid());
|
||||
if (!bCorpusLoaded || !Corpus.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TestEqual(TEXT("corpus contains the four project vectors plus eight defaults"),
|
||||
Corpus.GetEntries().Num(), 12);
|
||||
|
||||
FTestWorld World;
|
||||
World.Build(1337, 2, true);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
TestTrue(TEXT("corpus-free fixture keeps WorldRadiusVoxels at 0"),
|
||||
World.Settings->WorldRadiusVoxels == 0.0f);
|
||||
|
||||
constexpr int32 CandidateStrateIndex = FTestWorld::SlotFlatPlain;
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(CandidateStrateIndex, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
AddError(TEXT("corpus-free fixture has no candidate Z range"));
|
||||
return false;
|
||||
}
|
||||
const float TopWorldZ = (float)TopVoxelZ + 1.0f;
|
||||
const float BottomWorldZ = (float)BottomVoxelZ;
|
||||
const float StrateHeightInVoxels = TopWorldZ - BottomWorldZ;
|
||||
|
||||
FVector ArrivalPoint = FVector::ZeroVector;
|
||||
FVector DeparturePoint = FVector::ZeroVector;
|
||||
int32 ArrivalCount = 0;
|
||||
int32 DepartureCount = 0;
|
||||
for (const FVoxelPassage& Passage : World.StrateManager->GetPassages())
|
||||
{
|
||||
if (Passage.LowerStrateIndex == CandidateStrateIndex
|
||||
&& Passage.UpperStrateIndex + 1 == CandidateStrateIndex)
|
||||
{
|
||||
ArrivalPoint = Passage.LowerPoint;
|
||||
++ArrivalCount;
|
||||
}
|
||||
if (Passage.UpperStrateIndex == CandidateStrateIndex
|
||||
&& Passage.LowerStrateIndex == CandidateStrateIndex + 1)
|
||||
{
|
||||
DeparturePoint = Passage.UpperPoint;
|
||||
++DepartureCount;
|
||||
}
|
||||
}
|
||||
TestEqual(TEXT("corpus-free candidate has exactly one arrival mouth"), ArrivalCount, 1);
|
||||
TestEqual(TEXT("corpus-free candidate has exactly one departure mouth"), DepartureCount, 1);
|
||||
|
||||
FVoxelStrateMeasureSettings MeasureSettings;
|
||||
MeasureSettings.SampleStep = 4;
|
||||
MeasureSettings.RadiusInVoxels = 256;
|
||||
MeasureSettings.CenterXY = FVector2D::ZeroVector;
|
||||
MeasureSettings.MaxCells = 8000000;
|
||||
MeasureSettings.MaxRouteRetries = 16;
|
||||
MeasureSettings.HeadroomCells = 2;
|
||||
MeasureSettings.InteriorMarginVoxels = -1;
|
||||
|
||||
FArmStats CorpusBlend;
|
||||
CorpusBlend.Name = TEXT("corpus blend (today)");
|
||||
FArmStats Naive;
|
||||
Naive.Name = TEXT("naive uniform");
|
||||
FArmStats Constraint;
|
||||
Constraint.Name = TEXT("constraint-sampled");
|
||||
|
||||
int32 RecipeDeterminismFailures = 0;
|
||||
int32 ConstraintParameterViolations = 0;
|
||||
FString FirstConstraintParameterViolation;
|
||||
int32 ConstraintBoxStopCount = 0;
|
||||
FString FirstConstraintBoxViolation;
|
||||
bool bStopAfterConstraintViolation = false;
|
||||
|
||||
const double MeasurementStartSeconds = FPlatformTime::Seconds();
|
||||
for (int32 CandidateIndex = 0;
|
||||
CandidateIndex < NumCandidates && !bStopAfterConstraintViolation;
|
||||
++CandidateIndex)
|
||||
{
|
||||
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(
|
||||
AuthoredSettings->Seed, CandidateIndex);
|
||||
const FVoxelOpStackRecipe RepeatRecipe = VF_RollStrateStructure(
|
||||
AuthoredSettings->Seed, CandidateIndex);
|
||||
if (!VF_AreStrateStructureRecipesIdentical(Recipe, RepeatRecipe))
|
||||
{
|
||||
++RecipeDeterminismFailures;
|
||||
}
|
||||
|
||||
FArmStats* Arms[] = { &CorpusBlend, &Naive, &Constraint };
|
||||
const EVoxelStrateCorpusFreeSamplingMode Modes[] = {
|
||||
EVoxelStrateCorpusFreeSamplingMode::NaiveUniform,
|
||||
EVoxelStrateCorpusFreeSamplingMode::ConstraintSampled,
|
||||
};
|
||||
|
||||
for (int32 ArmIndex = 0; ArmIndex < 3 && !bStopAfterConstraintViolation; ++ArmIndex)
|
||||
{
|
||||
FArmStats& Arm = *Arms[ArmIndex];
|
||||
++Arm.Requested;
|
||||
FVoxelStrateArchetypeParams CandidateParams;
|
||||
bool bParameterRollsValid = true;
|
||||
bool bConstraintParameterViolationThisCandidate = false;
|
||||
|
||||
for (int32 BlockIndex = 0; BlockIndex < UE_ARRAY_COUNT(GBlockSpecs); ++BlockIndex)
|
||||
{
|
||||
const FBlockSpec& Spec = GBlockSpecs[BlockIndex];
|
||||
const int32 BlockSeed = AuthoredSettings->Seed
|
||||
^ static_cast<int32>(Spec.SeedSalt);
|
||||
FVoxelStrateRollInfo BlockRoll;
|
||||
if (ArmIndex == 0)
|
||||
{
|
||||
BlockRoll = VF_RollStrateParamsDetailedForArchetype(
|
||||
Corpus, Spec.Archetype, BlockSeed, CandidateIndex);
|
||||
const FVoxelStrateRollInfo RepeatRoll = VF_RollStrateParamsDetailedForArchetype(
|
||||
Corpus, Spec.Archetype, BlockSeed, CandidateIndex);
|
||||
if (!BlockRoll.bValid || !RepeatRoll.bValid
|
||||
|| !VF_AreStrateArchetypeParamsBitIdentical(
|
||||
BlockRoll.ArchetypeParams, RepeatRoll.ArchetypeParams, Spec.Archetype))
|
||||
{
|
||||
++Arm.DeterminismFailures;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BlockRoll = VF_RollStrateParamsCorpusFree(
|
||||
Spec.Archetype, BlockSeed, CandidateIndex, Modes[ArmIndex - 1],
|
||||
StrateHeightInVoxels);
|
||||
const FVoxelStrateRollInfo RepeatRoll = VF_RollStrateParamsCorpusFree(
|
||||
Spec.Archetype, BlockSeed, CandidateIndex, Modes[ArmIndex - 1],
|
||||
StrateHeightInVoxels);
|
||||
if (!BlockRoll.bValid || !RepeatRoll.bValid
|
||||
|| !VF_AreStrateArchetypeParamsBitIdentical(
|
||||
BlockRoll.ArchetypeParams, RepeatRoll.ArchetypeParams, Spec.Archetype))
|
||||
{
|
||||
++Arm.DeterminismFailures;
|
||||
}
|
||||
if (ArmIndex == 2 && BlockRoll.bValid)
|
||||
{
|
||||
FString Violation;
|
||||
if (!VF_ValidateStrateCorpusFreeConstraints(
|
||||
Spec.Archetype, BlockRoll.ArchetypeParams,
|
||||
StrateHeightInVoxels, Violation))
|
||||
{
|
||||
++ConstraintParameterViolations;
|
||||
if (FirstConstraintParameterViolation.IsEmpty())
|
||||
{
|
||||
FirstConstraintParameterViolation = FString::Printf(
|
||||
TEXT("candidate %d block %d (%s): %s"), CandidateIndex,
|
||||
static_cast<int32>(Spec.Block),
|
||||
VF_GetStrateArchetypeName(Spec.Archetype), *Violation);
|
||||
}
|
||||
bParameterRollsValid = false;
|
||||
bConstraintParameterViolationThisCandidate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!BlockRoll.bValid)
|
||||
{
|
||||
bParameterRollsValid = false;
|
||||
++Arm.RollFailures;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d block %d roll failed: %s"), CandidateIndex,
|
||||
static_cast<int32>(Spec.Block), *BlockRoll.FailureReason));
|
||||
continue;
|
||||
}
|
||||
VF_CopyRollBlock(CandidateParams, BlockRoll, Spec.Archetype);
|
||||
}
|
||||
|
||||
if (!bParameterRollsValid)
|
||||
{
|
||||
++Arm.Unmeasured;
|
||||
if (ArmIndex == 2 && bConstraintParameterViolationThisCandidate)
|
||||
{
|
||||
// The constraint relation itself is a precondition of this arm; do not
|
||||
// spend the remaining budget hiding a violated declared relation.
|
||||
bStopAfterConstraintViolation = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
VF_SetAllRuntimeBounds(CandidateParams, TopWorldZ, BottomWorldZ);
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
FString BuildError;
|
||||
const bool bBuilt = VF_BuildStackFromRecipe(
|
||||
Recipe, CandidateParams, AuthoredSettings->Seed, 14.0f,
|
||||
World.StrateManager.Get(), Stack, Context, &BuildError);
|
||||
if (!bBuilt)
|
||||
{
|
||||
++Arm.BuildFailures;
|
||||
++Arm.Unmeasured;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d build failed: %s"), CandidateIndex, *BuildError));
|
||||
continue;
|
||||
}
|
||||
if (Stack.Num() != Recipe.Modifiers.Num() + 7)
|
||||
{
|
||||
++Arm.InvalidStackCount;
|
||||
}
|
||||
FString ChannelError;
|
||||
if (!Stack.ValidateChannelOrder(&ChannelError))
|
||||
{
|
||||
++Arm.ChannelOrderFailures;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d channel order failed: %s"), CandidateIndex, *ChannelError));
|
||||
}
|
||||
if (Context.WorldRadiusVoxels != 0.0f)
|
||||
{
|
||||
++Arm.ContextViolations;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d changed WorldRadiusVoxels to %.9g"),
|
||||
CandidateIndex, Context.WorldRadiusVoxels));
|
||||
}
|
||||
|
||||
Stack.PrepareChunk(Context);
|
||||
FStackDensitySampler Sampler(Stack);
|
||||
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, MeasureSettings, nullptr);
|
||||
if (!Metrics.bValid)
|
||||
{
|
||||
++Arm.Unmeasured;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d measurement refused: %s"), CandidateIndex,
|
||||
*Metrics.RefusalReason));
|
||||
continue;
|
||||
}
|
||||
Arm.AllQuality.Add(Metrics);
|
||||
|
||||
const bool bNonVacuous = Metrics.NumSampled > 0
|
||||
&& Metrics.NumAir > 0 && Metrics.NumSolid > 0;
|
||||
const bool bLargestEnough = bNonVacuous
|
||||
&& Metrics.LargestComponentShare >= LargestComponentSurvivalThreshold;
|
||||
|
||||
bool bLawPass = false;
|
||||
if (bLargestEnough)
|
||||
{
|
||||
FVoxelConnectivityDiagnostics Law;
|
||||
if (ArrivalCount == 1 && DepartureCount == 1)
|
||||
{
|
||||
Law = VF_DiagnoseConnectivityWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, ArrivalPoint, DeparturePoint, MeasureSettings);
|
||||
++Arm.LawResults[VF_LawResultIndex(Law.Result)];
|
||||
}
|
||||
else
|
||||
{
|
||||
Law.bValid = false;
|
||||
Law.Result = EVoxelConnectivityResult::OutOfWindow;
|
||||
++Arm.LawResults[VF_LawResultIndex(Law.Result)];
|
||||
}
|
||||
|
||||
bLawPass = ArrivalCount == 1 && DepartureCount == 1
|
||||
&& Law.bValid && Law.Result == EVoxelConnectivityResult::Connected
|
||||
&& !Law.bStartSnapped && !Law.bGoalSnapped;
|
||||
if (bLawPass) { ++Arm.LawPasses; }
|
||||
if (!bLawPass) { ++Arm.LawFailedObserved; }
|
||||
}
|
||||
else
|
||||
{
|
||||
++Arm.LawSkipped;
|
||||
}
|
||||
|
||||
if (bNonVacuous) { ++Arm.NonVacuous; }
|
||||
if (bLargestEnough) { ++Arm.LargestEnough; }
|
||||
|
||||
const bool bSurvivor = bNonVacuous && bLargestEnough && bLawPass;
|
||||
if (bSurvivor)
|
||||
{
|
||||
++Arm.Survivors;
|
||||
Arm.SurvivorQuality.Add(Metrics);
|
||||
}
|
||||
else if (!bNonVacuous)
|
||||
{
|
||||
++Arm.Vacuous;
|
||||
}
|
||||
else if (!bLargestEnough)
|
||||
{
|
||||
++Arm.Fragmented;
|
||||
}
|
||||
else
|
||||
{
|
||||
++Arm.LawFailed;
|
||||
}
|
||||
|
||||
const FBoxVerdictReport BoxReport = VF_CheckStackBoxVerdicts(
|
||||
Stack, Context, CandidateIndex, BottomVoxelZ, TopVoxelZ);
|
||||
Arm.Box.Mixed += BoxReport.Mixed;
|
||||
Arm.Box.AllSolid += BoxReport.AllSolid;
|
||||
Arm.Box.AllAir += BoxReport.AllAir;
|
||||
Arm.Box.Proved += BoxReport.Proved;
|
||||
Arm.Box.CheckedVoxels += BoxReport.CheckedVoxels;
|
||||
Arm.Box.Violations += BoxReport.Violations;
|
||||
if (Arm.Box.FirstViolation.IsEmpty() && !BoxReport.FirstViolation.IsEmpty())
|
||||
{
|
||||
Arm.Box.FirstViolation = BoxReport.FirstViolation;
|
||||
}
|
||||
if (ArmIndex == 2 && BoxReport.Violations > 0)
|
||||
{
|
||||
++ConstraintBoxStopCount;
|
||||
bStopAfterConstraintViolation = true;
|
||||
FirstConstraintBoxViolation = BoxReport.FirstViolation;
|
||||
}
|
||||
}
|
||||
}
|
||||
const double MeasurementSeconds = FPlatformTime::Seconds() - MeasurementStartSeconds;
|
||||
|
||||
// Audit the same declared relations against the 12 current authored corpus vectors. Project
|
||||
// assets carry their own height; the eight C++ defaults use the fixture's 4-chunk height.
|
||||
int32 AuthoredConstraintViolations = 0;
|
||||
FString AuthoredViolationList;
|
||||
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
||||
{
|
||||
float EntryHeight = 4.0f * (float)CHUNK_SIZE;
|
||||
if (!Entry.SourcePath.Contains(TEXT("/VoxelForge/ComposerDefaults/")))
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(
|
||||
nullptr, *Entry.SourcePath);
|
||||
if (Definition != nullptr)
|
||||
{
|
||||
EntryHeight = FMath::Max(1.0f, (float)Definition->StrateHeightInChunks)
|
||||
* (float)CHUNK_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
FString Violation;
|
||||
if (!VF_ValidateStrateCorpusFreeConstraints(
|
||||
Entry.Archetype, Entry.ArchetypeParams, EntryHeight, Violation))
|
||||
{
|
||||
++AuthoredConstraintViolations;
|
||||
if (!AuthoredViolationList.IsEmpty())
|
||||
{
|
||||
AuthoredViolationList += TEXT("; ");
|
||||
}
|
||||
AuthoredViolationList += FString::Printf(
|
||||
TEXT("%s (%s, H=%.0f): %s"), *Entry.SourceName,
|
||||
VF_GetStrateArchetypeName(Entry.Archetype), EntryHeight, *Violation);
|
||||
}
|
||||
}
|
||||
|
||||
const FArmStats* Arms[] = { &CorpusBlend, &Naive, &Constraint };
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Corpus-free measurement: requested %d candidates per arm; structure recipes were "
|
||||
"shared across all three arms; sample step=%d radius=%d MaxCells=%d, fixed law mouths, "
|
||||
"criterion precedence is vacuous > fragmented > law failed. Runtime %.3fs, roll/measure "
|
||||
"%.3fs."),
|
||||
NumCandidates, MeasureSettings.SampleStep, MeasureSettings.RadiusInVoxels,
|
||||
MeasureSettings.MaxCells, FPlatformTime::Seconds() - TestStartSeconds, MeasurementSeconds));
|
||||
|
||||
FString Table = TEXT(
|
||||
"approach | survival | vacuous | fragmented | law failed | air range | walkable range | feature scale range\n");
|
||||
FString SurvivorTable = TEXT(
|
||||
"survivor quality (ranges are over survivors only; main table ranges are over all measured candidates)\n"
|
||||
"approach | survivors | air range (mean) | walkable range (mean) | feature scale range (mean)\n");
|
||||
for (const FArmStats* Arm : Arms)
|
||||
{
|
||||
Table += FString::Printf(
|
||||
TEXT("%s | %s | %d | %d | %d | %s | %s | %s\n"),
|
||||
Arm->Name, *Arm->SurvivalText(), Arm->Vacuous, Arm->Fragmented, Arm->LawFailed,
|
||||
*Arm->AllQuality.RangeText(Arm->AllQuality.AirMin, Arm->AllQuality.AirMax),
|
||||
*Arm->AllQuality.RangeText(Arm->AllQuality.WalkableMin, Arm->AllQuality.WalkableMax),
|
||||
*Arm->AllQuality.RangeText(Arm->AllQuality.FeatureMin, Arm->AllQuality.FeatureMax));
|
||||
SurvivorTable += FString::Printf(
|
||||
TEXT("%s | %d | %s | %s | %s\n"),
|
||||
Arm->Name, Arm->SurvivorQuality.Count,
|
||||
*Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.AirMin, Arm->SurvivorQuality.AirMax),
|
||||
*Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.WalkableMin, Arm->SurvivorQuality.WalkableMax),
|
||||
*Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.FeatureMin, Arm->SurvivorQuality.FeatureMax));
|
||||
}
|
||||
AddInfo(Table);
|
||||
AddInfo(SurvivorTable);
|
||||
|
||||
for (const FArmStats* Arm : Arms)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s: non-vacuous=%d, largest>=%.2f=%d, law pass=%d, law failures observed=%d, "
|
||||
"law skipped=%d, "
|
||||
"roll failures=%d, build failures=%d, unmeasured=%d, deterministic roll failures=%d; "
|
||||
"stack-count failures=%d, channel-order failures=%d, context violations=%d; "
|
||||
"law results Connected=%d NotConnected=%d StartSolid=%d GoalSolid=%d OutOfWindow=%d Budget=%d; "
|
||||
"quality all %s; survivor quality %s; first failure=%s."),
|
||||
Arm->Name, Arm->NonVacuous, LargestComponentSurvivalThreshold, Arm->LargestEnough,
|
||||
Arm->LawPasses, Arm->LawFailedObserved, Arm->LawSkipped,
|
||||
Arm->RollFailures, Arm->BuildFailures,
|
||||
Arm->Unmeasured, Arm->DeterminismFailures,
|
||||
Arm->InvalidStackCount, Arm->ChannelOrderFailures, Arm->ContextViolations,
|
||||
Arm->LawResults[0], Arm->LawResults[1], Arm->LawResults[2],
|
||||
Arm->LawResults[3], Arm->LawResults[4], Arm->LawResults[5],
|
||||
*Arm->AllQuality.SummaryText(), *Arm->SurvivorQuality.SummaryText(),
|
||||
Arm->FirstFailure.IsEmpty() ? TEXT("none") : *Arm->FirstFailure));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s box verdicts: mixed=%d AllSolid=%d AllAir=%d proved=%d voxels checked=%lld "
|
||||
"violations=%d; %s"),
|
||||
Arm->Name, Arm->Box.Mixed, Arm->Box.AllSolid, Arm->Box.AllAir, Arm->Box.Proved,
|
||||
Arm->Box.CheckedVoxels, Arm->Box.Violations,
|
||||
Arm->Box.FirstViolation.IsEmpty() ? TEXT("no violations") : *Arm->Box.FirstViolation));
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Authored-corpus constraint audit: %d/%d vectors violated at their declared strate "
|
||||
"height. %s"),
|
||||
AuthoredConstraintViolations, Corpus.GetEntries().Num(),
|
||||
AuthoredViolationList.IsEmpty() ? TEXT("none") : *AuthoredViolationList));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Shared structure determinism failures=%d; per-arm channel/context violations are "
|
||||
"reported below; constraint parameter violations=%d; constraint box-stop count=%d. %s%s"),
|
||||
RecipeDeterminismFailures,
|
||||
ConstraintParameterViolations, ConstraintBoxStopCount,
|
||||
FirstConstraintParameterViolation.IsEmpty() ? TEXT("") : *FirstConstraintParameterViolation,
|
||||
FirstConstraintBoxViolation.IsEmpty() ? TEXT("")
|
||||
: *FString::Printf(TEXT("; first box violation: %s"), *FirstConstraintBoxViolation)));
|
||||
|
||||
TestEqual(TEXT("configured candidate count requested for corpus blend"), CorpusBlend.Requested, NumCandidates);
|
||||
TestEqual(TEXT("configured candidate count requested for naive uniform"), Naive.Requested, NumCandidates);
|
||||
TestEqual(TEXT("configured candidate count requested for constraint-sampled"), Constraint.Requested, NumCandidates);
|
||||
TestEqual(TEXT("corpus blend roll failures"), CorpusBlend.RollFailures, 0);
|
||||
TestEqual(TEXT("naive uniform roll failures"), Naive.RollFailures, 0);
|
||||
TestEqual(TEXT("constraint-sampled roll failures"), Constraint.RollFailures, 0);
|
||||
TestEqual(TEXT("corpus blend build failures"), CorpusBlend.BuildFailures, 0);
|
||||
TestEqual(TEXT("naive uniform build failures"), Naive.BuildFailures, 0);
|
||||
TestEqual(TEXT("constraint-sampled build failures"), Constraint.BuildFailures, 0);
|
||||
TestEqual(TEXT("corpus-free parameter determinism failures"),
|
||||
Naive.DeterminismFailures + Constraint.DeterminismFailures, 0);
|
||||
TestEqual(TEXT("corpus parameter determinism failures"), CorpusBlend.DeterminismFailures, 0);
|
||||
TestEqual(TEXT("shared structure recipe determinism failures"), RecipeDeterminismFailures, 0);
|
||||
TestEqual(TEXT("constraint-sampled declared-parameter violations"),
|
||||
ConstraintParameterViolations, 0);
|
||||
TestEqual(TEXT("constraint-sampled box verdict violations"),
|
||||
Constraint.Box.Violations, 0);
|
||||
TestEqual(TEXT("corpus blend box verdict violations"), CorpusBlend.Box.Violations, 0);
|
||||
TestEqual(TEXT("naive uniform box verdict violations"), Naive.Box.Violations, 0);
|
||||
TestEqual(TEXT("corpus blend WorldRadiusVoxels/context violations"),
|
||||
CorpusBlend.InvalidStackCount + CorpusBlend.ChannelOrderFailures
|
||||
+ CorpusBlend.ContextViolations, 0);
|
||||
TestEqual(TEXT("naive uniform WorldRadiusVoxels/context violations"),
|
||||
Naive.InvalidStackCount + Naive.ChannelOrderFailures + Naive.ContextViolations, 0);
|
||||
TestEqual(TEXT("constraint-sampled WorldRadiusVoxels/context violations"),
|
||||
Constraint.InvalidStackCount + Constraint.ChannelOrderFailures
|
||||
+ Constraint.ContextViolations, 0);
|
||||
TestTrue(TEXT("constraint sampler did not stop early on a declared relation or box violation"),
|
||||
!bStopAfterConstraintViolation);
|
||||
|
||||
const double TestSeconds = FPlatformTime::Seconds() - TestStartSeconds;
|
||||
AddInfo(FString::Printf(TEXT("CorpusFree total runtime %.3fs; all three arms requested %d each; "
|
||||
"WorldRadiusVoxels=0."), TestSeconds, NumCandidates));
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
File diff suppressed because it is too large.
Load diff
@@ -320,6 +320,35 @@ VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
|
||||
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
|
||||
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
|
||||
|
||||
/**
|
||||
* Provenance for a parameter block that does not read the authored corpus.
|
||||
*
|
||||
* NaiveUniform is the §3.3 control: every scalar is rolled independently inside the same
|
||||
* finite code-declared envelope used by the constraint arm. ConstraintSampled draws the
|
||||
* independent geometry quantities and derives the coupled quantities from generator equations.
|
||||
* Neither mode reads a strate asset or a corpus entry.
|
||||
*/
|
||||
enum class EVoxelStrateCorpusFreeSamplingMode : uint8
|
||||
{
|
||||
NaiveUniform,
|
||||
ConstraintSampled,
|
||||
};
|
||||
|
||||
/** Roll one native parameter family without consulting FVoxelStrateCorpus. */
|
||||
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsCorpusFree(
|
||||
ECaveGeneratorType Archetype,
|
||||
int32 Seed,
|
||||
int32 Index,
|
||||
EVoxelStrateCorpusFreeSamplingMode Mode,
|
||||
float StrateHeightInVoxels);
|
||||
|
||||
/** Validate the hard geometry relations used by ConstraintSampled and the corpus audit. */
|
||||
VOXELFORGE_API bool VF_ValidateStrateCorpusFreeConstraints(
|
||||
ECaveGeneratorType Archetype,
|
||||
const FVoxelStrateArchetypeParams& Params,
|
||||
float StrateHeightInVoxels,
|
||||
FString& OutViolation);
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Roll one candidate for the editor walk-through path. Parameter rolls call
|
||||
|
||||
Reference in new issue
Block a user