diff --git a/source/Base/Resources.h b/source/Base/Resources.h index bb521edaf9..df4c251558 100644 --- a/source/Base/Resources.h +++ b/source/Base/Resources.h @@ -4,7 +4,7 @@ namespace Const { - std::string const ProgramVersion = "5.0.0-alpha.21"; + std::string const ProgramVersion = "5.0.0-alpha.22"; std::string const DiscordURL = "https://discord.gg/7bjyZdXXQ2"; std::string const AlienServerURL = "api.alien-project.org"; diff --git a/source/Base/StringHelper.cpp b/source/Base/StringHelper.cpp index 410a18e62d..4ebeaa8ffd 100644 --- a/source/Base/StringHelper.cpp +++ b/source/Base/StringHelper.cpp @@ -24,6 +24,11 @@ std::string StringHelper::format(uint64_t n, char separator) } std::string StringHelper::format(float v, int fracPartDecimals) +{ + return format(static_cast(v), fracPartDecimals); +} + +std::string StringHelper::format(double v, int fracPartDecimals) { std::string result; if (v < 0) { @@ -31,7 +36,7 @@ std::string StringHelper::format(float v, int fracPartDecimals) v = -v; } auto scale = static_cast(std::llround(std::pow(10.0, fracPartDecimals))); - auto scaled = static_cast(std::llround(static_cast(v) * scale)); + auto scaled = static_cast(std::llround(v * scale)); result += format(scaled / scale); if (fracPartDecimals > 0) { result += "."; @@ -105,6 +110,23 @@ std::string StringHelper::formatInHex(uint64_t value) return ss.str(); } +std::string StringHelper::formatInThousands(double value) +{ + if (value == 0.0) { + return "0"; + } + auto result = format(value / 1000, 3); + if (result.find('.') != std::string::npos) { + while (result.back() == '0') { + result.pop_back(); + } + if (result.back() == '.') { + result.pop_back(); + } + } + return result + "K"; +} + void StringHelper::copy(char* target, int maxSize, std::string const& source) { auto sourceSize = source.size(); diff --git a/source/Base/StringHelper.h b/source/Base/StringHelper.h index d1ca9fd24b..67d152e04e 100644 --- a/source/Base/StringHelper.h +++ b/source/Base/StringHelper.h @@ -10,10 +10,12 @@ class StringHelper public: static std::string format(uint64_t n, char separator = ','); static std::string format(float v, int decimalsAfterPoint); + static std::string format(double v, int decimalsAfterPoint); static std::string format(std::chrono::seconds duration); static std::string format(std::chrono::milliseconds duration); static std::string format(std::chrono::system_clock::time_point const& timePoint); static std::string formatInHex(uint64_t value); + static std::string formatInThousands(double value); //e.g. 12000 -> "12K", 1000000 -> "1,000K" static void copy(char* target, int maxSize, std::string const& source); static bool compare(char const* target, int maxSize, char const* source); diff --git a/source/Cli/Main.cpp b/source/Cli/Main.cpp index d5a25ec0ea..50f37d7737 100644 --- a/source/Cli/Main.cpp +++ b/source/Cli/Main.cpp @@ -36,7 +36,7 @@ int main(int argc, char** argv) app.add_option( "-o", outputFilename, - "Specifies the name of the output file for the simulation. The *.settings.json and *.statistics.csv file will also be saved."); + "Specifies the name of the output file for the simulation. The *.settings.json and *.statistics.bin file will also be saved."); app.add_option("-t", timesteps, "The number of time steps to be calculated."); app.add_flag( "-p,--profile", diff --git a/source/EngineImpl/DescConverterService.cpp b/source/EngineImpl/DescConverterService.cpp index f411481bf9..7538473116 100644 --- a/source/EngineImpl/DescConverterService.cpp +++ b/source/EngineImpl/DescConverterService.cpp @@ -936,6 +936,7 @@ CreatureDesc DescConverterService::createCreatureDesc(TOs const& to, int creatur result._lineageId = creatureTO.lineageId; NumberGenerator::get().adaptMaxLineageId(creatureTO.lineageId); result._accumulatedMutations = creatureTO.accumulatedMutations; + result._accumulatedMutationsInLineage = creatureTO.accumulatedMutationsInLineage; result._headUpdateId = creatureTO.headUpdateId; return result; @@ -1258,6 +1259,7 @@ void DescConverterService::convertCreatureToTO( creatureTO.mutationState = creatureDesc._mutationState; creatureTO.lineageId = creatureDesc._lineageId; creatureTO.accumulatedMutations = creatureDesc._accumulatedMutations; + creatureTO.accumulatedMutationsInLineage = creatureDesc._accumulatedMutationsInLineage; creatureTO.genomeArrayIndex = genomeTOIndexById.at(creatureDesc._genomeId); } diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index 41f536e128..bfb7a0305c 100644 --- a/source/EngineImpl/EngineWorker.cpp +++ b/source/EngineImpl/EngineWorker.cpp @@ -99,11 +99,6 @@ Desc EngineWorker::getInspectedSimulationData(std::vector objectsIds) return DescConverterService::get().convertTOtoDescription(dataTO); } -StatisticsRawData EngineWorker::getStatisticsRawData() const -{ - return _simulationCudaFacade->getStatisticsRawData(); -} - StatisticsHistory const& EngineWorker::getStatisticsHistory() const { return _simulationCudaFacade->getStatisticsHistory(); @@ -114,6 +109,11 @@ void EngineWorker::setStatisticsHistory(StatisticsHistoryData const& data) _simulationCudaFacade->setStatisticsHistory(data); } +StatisticsEntry EngineWorker::getStatisticsEntry() const +{ + return _simulationCudaFacade->getStatisticsEntry(); +} + void EngineWorker::addAndSelectSimulationData(Desc&& dataToUpdate) { EngineWorkerGuard access(this); @@ -263,7 +263,6 @@ void EngineWorker::setCurrentTimestep(uint64_t value) { EngineWorkerGuard access(this); _simulationCudaFacade->setCurrentTimestep(value); - resetTimeIntervalStatistics(); } SimulationParameters EngineWorker::getSimulationParameters() const @@ -538,10 +537,6 @@ void EngineWorker::testOnly_syncNumberGenerator() NumberGenerator::get().adaptMaxIds(maxIds); } -void EngineWorker::resetTimeIntervalStatistics() -{ - _simulationCudaFacade->resetTimeIntervalStatistics(); -} void EngineWorker::processJobs() { diff --git a/source/EngineImpl/EngineWorker.h b/source/EngineImpl/EngineWorker.h index ec95473593..5fa1bc97ee 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include #include -#include #include @@ -55,9 +55,9 @@ class EngineWorker Desc getSimulationData(IntVector2D const& rectUpperLeft, IntVector2D const& rectLowerRight); Desc getSelectedSimulationData(bool includeClusters); Desc getInspectedSimulationData(std::vector objectsIds); - StatisticsRawData getStatisticsRawData() const; StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); + StatisticsEntry getStatisticsEntry() const; void addAndSelectSimulationData(Desc&& dataToUpdate); void setSimulationData(Desc const& dataToUpdate); @@ -132,7 +132,6 @@ class EngineWorker void testOnly_syncNumberGenerator(); private: - void resetTimeIntervalStatistics(); void processJobs(); void syncSimulationWithRenderingIfDesired(); diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index 15bee58a1f..000b96c177 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -50,7 +49,10 @@ namespace { - std::chrono::milliseconds const StatisticsUpdate(30); + // The evolution statistics kernels are heavy; running them at wall-clock-throttled (i.e. run-to-run varying) + // timesteps perturbs the simulation's GPU execution timing and makes simulation runs non-reproducible. + // They are therefore scheduled at deterministic timestep intervals instead. + auto constexpr EvolutionStatisticsUpdateInterval = 10; ArraySizesForGpuEntities const PreviewCapacityGpu{10000, 10000, 10000000}; ArraySizesForTOs const PreviewCapacityTO{1000, 1000, 1000, 10000, 10000, 10000, 10000000}; } @@ -76,7 +78,6 @@ _SimulationCudaFacade::_SimulationCudaFacade(uint64_t timestep, SettingsForSimul _cudaTOProvider = std::make_shared<_CudaTOProvider>(); _cudaSimulationStatistics = std::make_shared(); _cudaPreviewStatistics = std::make_shared(); - _maxAgeBalancer = std::make_shared<_MaxAgeBalancer>(); _simulationTimestep = timestep; _cudaSimulationData->init({_settings.worldSizeX, _settings.worldSizeY}, timestep); @@ -112,6 +113,7 @@ _SimulationCudaFacade::~_SimulationCudaFacade() noexcept _cudaSimulationData->free(); _cudaPreviewData->free(); _cudaSimulationStatistics->free(); + _cudaPreviewStatistics->free(); _cudaSelectionResult->free(); SimulationKernelsService::get().shutdown(); @@ -430,26 +432,22 @@ ArraySizesForTOs _SimulationCudaFacade::estimateCapacityNeededForTO() const return DataAccessKernelsService::get().estimateCapacityNeededForTO(_settings.cudaSettings, getSimulationDataPtrCopy()); } -StatisticsRawData _SimulationCudaFacade::getStatisticsRawData() +void _SimulationCudaFacade::updateStatistics() { - std::lock_guard lock(_mutexForStatistics); - if (_statisticsData) { - return *_statisticsData; - } else { - return StatisticsRawData(); - } + updateEvolutionStatistics(); } -void _SimulationCudaFacade::updateStatistics() +void _SimulationCudaFacade::updateEvolutionStatistics() { StatisticsKernelsService::get().updateStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); syncAndCheck(); + auto statisticsEntry = _cudaSimulationStatistics->getStatisticsEntry(); { std::lock_guard lock(_mutexForStatistics); - _statisticsData = _cudaSimulationStatistics->getStatistics(); + _statisticsEntry = statisticsEntry; } - StatisticsService::get().addDataPoint(_statisticsHistory, _statisticsData->timeline, getCurrentTimestep()); + StatisticsService::get().addDataPoint(_statisticsHistory, statisticsEntry, getCurrentTimestep()); } StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const @@ -457,14 +455,19 @@ StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const return _statisticsHistory; } -void _SimulationCudaFacade::setStatisticsHistory(StatisticsHistoryData const& data) +StatisticsEntry _SimulationCudaFacade::getStatisticsEntry() { - StatisticsService::get().rewriteHistory(_statisticsHistory, data, getCurrentTimestep()); + std::lock_guard lock(_mutexForStatistics); + if (_statisticsEntry) { + return *_statisticsEntry; + } else { + return StatisticsEntry(); + } } -void _SimulationCudaFacade::resetTimeIntervalStatistics() +void _SimulationCudaFacade::setStatisticsHistory(StatisticsHistoryData const& data) { - _cudaSimulationStatistics->resetAccumulatedStatistics(); + StatisticsService::get().rewriteHistory(_statisticsHistory, data, getCurrentTimestep()); } uint64_t _SimulationCudaFacade::getCurrentTimestep() const @@ -582,7 +585,7 @@ TOs _SimulationCudaFacade::getPreviewData() void _SimulationCudaFacade::testOnly_mutate(uint64_t objectId) { checkAndProcessSimulationParameterChanges(); - TestKernelsService::get().testOnly_mutate(_settings.cudaSettings, getSimulationDataPtrCopy(), objectId); + TestKernelsService::get().testOnly_mutate(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics, objectId); syncAndCheck(); resizeArraysIfNecessary(); @@ -811,19 +814,15 @@ void _SimulationCudaFacade::calcTimestepsInternal(uint64_t timesteps, bool force resizeArraysIfNecessary(); } - auto statistics = getStatisticsRawData(); { std::lock_guard lock(_mutexForSimulationParameters); - if (SimulationParametersUpdateService::get().updateSimulationParametersAfterTimestep( - _settings, _maxAgeBalancer, simulationData, getCurrentTimestep(), statistics)) { + if (SimulationParametersUpdateService::get().updateSimulationParametersAfterTimestep(_settings, simulationData, getCurrentTimestep())) { CHECK_FOR_DEVICE_ERRORS( cudaMemcpyToSymbol(cudaSimulationParameters, &_settings.simulationParameters, sizeof(SimulationParameters), 0, cudaMemcpyHostToDevice)); } } - auto now = std::chrono::steady_clock::now(); - if (!_lastStatisticsUpdateTime || now - *_lastStatisticsUpdateTime > StatisticsUpdate) { - _lastStatisticsUpdateTime = now; - updateStatistics(); + if (getCurrentTimestep() % EvolutionStatisticsUpdateInterval == 0) { + updateEvolutionStatistics(); } } if (forceUpdateStatistics) { diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index da23a9c41a..211c193eba 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -13,12 +13,12 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include @@ -83,12 +83,11 @@ public: ArraySizesForTOs estimateCapacityNeededForTO() const; - StatisticsRawData getStatisticsRawData(); void updateStatistics(); StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); + StatisticsEntry getStatisticsEntry(); - void resetTimeIntervalStatistics(); uint64_t getCurrentTimestep() const; void setCurrentTimestep(uint64_t timestep); @@ -121,6 +120,8 @@ public: private: void initCuda(); + void updateEvolutionStatistics(); + void syncAndCheck(); void copyDataTOtoGpu(TOs const& cudaTO, TOs const& to); void copyDataTOtoHost(TOs const& to, TOs const& cudaTO); @@ -153,10 +154,8 @@ private: TOProvider _collectionTOProvider; mutable std::mutex _mutexForStatistics; - std::optional _lastStatisticsUpdateTime; - std::optional _statisticsData; StatisticsHistory _statisticsHistory; + std::optional _statisticsEntry; std::shared_ptr _cudaSimulationStatistics; std::shared_ptr _cudaPreviewStatistics; - MaxAgeBalancer _maxAgeBalancer; }; diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index c43d465cb0..19bfd8e055 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -320,11 +320,6 @@ IntVector2D _SimulationFacadeImpl::getWorldSize() const return _worldSize; } -StatisticsRawData _SimulationFacadeImpl::getStatisticsRawData() const -{ - return _worker.getStatisticsRawData(); -} - StatisticsHistory const& _SimulationFacadeImpl::getStatisticsHistory() const { return _worker.getStatisticsHistory(); @@ -335,6 +330,11 @@ void _SimulationFacadeImpl::setStatisticsHistory(StatisticsHistoryData const& da _worker.setStatisticsHistory(data); } +StatisticsEntry _SimulationFacadeImpl::getStatisticsEntry() const +{ + return _worker.getStatisticsEntry(); +} + std::optional _SimulationFacadeImpl::getTpsRestriction() const { auto result = _worker.getTpsRestriction(); diff --git a/source/EngineImpl/SimulationFacadeImpl.h b/source/EngineImpl/SimulationFacadeImpl.h index 9a7225dc49..9cfc0ccead 100644 --- a/source/EngineImpl/SimulationFacadeImpl.h +++ b/source/EngineImpl/SimulationFacadeImpl.h @@ -88,9 +88,9 @@ class _SimulationFacadeImpl : public _SimulationFacade bool updateSelectionIfNecessary() override; IntVector2D getWorldSize() const override; - StatisticsRawData getStatisticsRawData() const override; StatisticsHistory const& getStatisticsHistory() const override; void setStatisticsHistory(StatisticsHistoryData const& data) override; + StatisticsEntry getStatisticsEntry() const override; std::optional getTpsRestriction() const override; void setTpsRestriction(std::optional const& value) override; diff --git a/source/EngineImpl/SimulationParametersUpdateService.cu b/source/EngineImpl/SimulationParametersUpdateService.cu index 8d5f0f86d6..afdb714ab2 100644 --- a/source/EngineImpl/SimulationParametersUpdateService.cu +++ b/source/EngineImpl/SimulationParametersUpdateService.cu @@ -6,7 +6,6 @@ #include #include -#include #include SimulationParameters SimulationParametersUpdateService::integrateChanges( @@ -42,10 +41,8 @@ SimulationParameters SimulationParametersUpdateService::integrateChanges( bool SimulationParametersUpdateService::updateSimulationParametersAfterTimestep( SettingsForSimulation& settings, - MaxAgeBalancer const& maxAgeBalancer, SimulationData const& simulationData, - uint64_t timestep, - StatisticsRawData const& statistics) + uint64_t timestep) { auto result = false; @@ -92,7 +89,5 @@ bool SimulationParametersUpdateService::updateSimulationParametersAfterTimestep( result = true; } - result |= maxAgeBalancer->balance(settings.simulationParameters, statistics, timestep); - return result; } diff --git a/source/EngineImpl/SimulationParametersUpdateService.cuh b/source/EngineImpl/SimulationParametersUpdateService.cuh index 17625a27cc..0def1858e7 100644 --- a/source/EngineImpl/SimulationParametersUpdateService.cuh +++ b/source/EngineImpl/SimulationParametersUpdateService.cuh @@ -4,7 +4,6 @@ #include #include -#include #include @@ -20,8 +19,6 @@ public: bool updateSimulationParametersAfterTimestep( SettingsForSimulation& settings, - MaxAgeBalancer const& maxAgeBalancer, SimulationData const& simulationData, - uint64_t timestep, - StatisticsRawData const& statistics); //returns true if parameters have been changed + uint64_t timestep); //returns true if parameters have been changed }; diff --git a/source/EngineImpl/StatisticsKernelsService.cu b/source/EngineImpl/StatisticsKernelsService.cu index 98942d2dc0..e854d78505 100644 --- a/source/EngineImpl/StatisticsKernelsService.cu +++ b/source/EngineImpl/StatisticsKernelsService.cu @@ -6,12 +6,19 @@ void StatisticsKernelsService::init() {} void StatisticsKernelsService::shutdown() {} -void StatisticsKernelsService::updateStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics) +void StatisticsKernelsService::updateStatistics( + CudaSettings const& gpuSettings, + SimulationData const& data, + SimulationStatistics const& simulationStatistics) { - KERNEL_CALL(cudaUpdateTimestepStatistics_substep1, data, simulationStatistics); - KERNEL_CALL(cudaUpdateTimestepStatistics_substep2, data, simulationStatistics); - KERNEL_CALL(cudaUpdateTimestepStatistics_substep3, data, simulationStatistics); - KERNEL_CALL_1_1(cudaUpdateHistogramData_substep1, data, simulationStatistics); - KERNEL_CALL(cudaUpdateHistogramData_substep2, data, simulationStatistics); - KERNEL_CALL(cudaUpdateHistogramData_substep3, data, simulationStatistics); + KERNEL_CALL(cudaUpdateEvolutionStatistics_substep1, data, simulationStatistics); + KERNEL_CALL(cudaUpdateEvolutionStatistics_substep2, data, simulationStatistics); + KERNEL_CALL(cudaUpdateEvolutionStatistics_substep3, data, simulationStatistics); + KERNEL_CALL(cudaUpdateEvolutionStatistics_substep4, data, simulationStatistics); + KERNEL_CALL_1_1(cudaUpdateEvolutionStatistics_substep5, data, simulationStatistics); + if (simulationStatistics.isLineageAccumulatorGCNeeded()) { + KERNEL_CALL(cudaPrepareLineageAccumulatorGC, simulationStatistics); + KERNEL_CALL(cudaLineageAccumulatorGC, simulationStatistics); + KERNEL_CALL_1_1(cudaFinishLineageAccumulatorGC, simulationStatistics); + } } diff --git a/source/EngineImpl/StatisticsKernelsService.cuh b/source/EngineImpl/StatisticsKernelsService.cuh index 8caa9f110e..9051f3332b 100644 --- a/source/EngineImpl/StatisticsKernelsService.cuh +++ b/source/EngineImpl/StatisticsKernelsService.cuh @@ -16,6 +16,8 @@ public: void init(); void shutdown(); + // Runs at deterministic timesteps (not wall-clock throttled): heavy kernels here would otherwise + // perturb the simulation's execution timing at run-to-run varying timesteps. void updateStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics); private: diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index 98df8a0418..295a6cc0f0 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -1,6 +1,9 @@ #include "StatisticsService.cuh" +#include +#include + #include #include @@ -10,104 +13,174 @@ namespace auto constexpr MaxSamples = 1000; } -void StatisticsService::addDataPoint(StatisticsHistory& history, TimelineStatistics const& newTimelineStatistics, uint64_t timestep) +void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry const& overallStatistics, uint64_t timestep) { std::lock_guard lock(history.getMutex()); auto& historyData = history.getDataRef(); - if (!historyData.empty() && historyData.back().time > toDouble(timestep) + NEAR_ZERO) { - historyData.clear(); + auto time = toDouble(timestep); + if (!historyData.overall.empty() && historyData.overall.back().time > time + NEAR_ZERO) { + historyData = StatisticsHistoryData(); + _overallState = TimelineState(); + _lineageStates.clear(); } - if (!_lastTimelineStatistics || historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta / 100 * (_numDataPoints + 1)) { - auto newDataPoint = [&] { - if (!_lastTimelineStatistics && !historyData.empty()) { - - //reuse last entry if no raw statistics is available - auto result = historyData.back(); - result.time = toDouble(timestep); - return result; - } else { - return StatisticsConverterService::get().convert(newTimelineStatistics, timestep, toDouble(timestep), _lastTimelineStatistics, _lastTimestep); - } - }(); - - _lastTimelineStatistics = newTimelineStatistics; - _lastTimestep = timestep; - _accumulatedDataPoint = _accumulatedDataPoint.has_value() ? *_accumulatedDataPoint + newDataPoint : newDataPoint; - ++_numDataPoints; - } + auto forceSampling = !_lastTimestep.has_value(); + if (forceSampling && !historyData.overall.empty()) { - if (_accumulatedDataPoint.has_value() && (historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta)) { - auto newDataPoint = *_accumulatedDataPoint / _numDataPoints; - _numDataPoints = 0; - _accumulatedDataPoint.reset(); + // Reuse last entries if no statistics is available + auto overallBackTime = historyData.overall.back().time; + auto overallSample = historyData.overall.back(); + overallSample.time = time; + overallSample.timestep = time; + updateTimeline(historyData.overall, _overallState, overallSample, time, true); - //remove last entry if timestep has not changed - if (!historyData.empty() && abs(historyData.back().time - toDouble(timestep)) < NEAR_ZERO) { - historyData.pop_back(); - } - historyData.emplace_back(newDataPoint); - - //compress history after MaxSamples - if (historyData.size() > MaxSamples) { - std::vector newData; - newData.reserve(historyData.size() / 2); - for (size_t i = 0; i < (historyData.size() - 1) / 2; ++i) { - DataPointCollection interpolatedDataPoint = (historyData.at(i * 2) + historyData.at(i * 2 + 1)) / 2.0; - interpolatedDataPoint.time = historyData.at(i * 2).time; - newData.emplace_back(interpolatedDataPoint); + // Only lineages that were still sampled at the end of the history are continued; extinct ones keep their last entry + for (auto& [lineageId, samples] : historyData.lineages) { + if (samples.empty()) { + continue; + } + auto& state = _lineageStates[lineageId]; + if (overallBackTime - samples.back().time > state.longtermTimestepDelta * 2) { + continue; } - newData.emplace_back(historyData.back()); - historyData.swap(newData); + auto lineageSample = samples.back(); + lineageSample.time = time; + lineageSample.timestep = time; + updateTimeline(samples, state, lineageSample, time, true); + } + } else { + auto dataPoints = StatisticsConverterService::get().convert(overallStatistics, timestep, time); + + OverallSample overallSample; + overallSample.time = dataPoints.time; + overallSample.timestep = dataPoints.timestep; + overallSample.systemClock = dataPoints.systemClock; + overallSample.data = dataPoints.overall; + updateTimeline(historyData.overall, _overallState, overallSample, time, forceSampling); + + for (auto const& [lineageId, lineageDataPoint] : dataPoints.lineages) { + LineageSample lineageSample; + lineageSample.time = dataPoints.time; + lineageSample.timestep = dataPoints.timestep; + lineageSample.systemClock = dataPoints.systemClock; + lineageSample.data = lineageDataPoint; + updateTimeline(historyData.lineages[lineageId], _lineageStates[lineageId], lineageSample, time, forceSampling); + } - _longtermTimestepDelta *= 2.0; + // Finalize timelines of vanished lineages with their pending samples + for (auto stateIt = _lineageStates.begin(); stateIt != _lineageStates.end();) { + if (!dataPoints.lineages.contains(stateIt->first)) { + if (stateIt->second.accumulatedSample.has_value()) { + emitSample(historyData.lineages[stateIt->first], stateIt->second, time); + } + stateIt = _lineageStates.erase(stateIt); + } else { + ++stateIt; + } } } + + _lastTimestep = timestep; } void StatisticsService::resetTime(StatisticsHistory& history, uint64_t timestep) { std::lock_guard lock(history.getMutex()); - auto& data = history.getDataRef(); - if (data.empty()) { - return; - } + auto& historyData = history.getDataRef(); + auto time = toDouble(timestep); - auto prevTimestep = data.back().time; - if (!data.empty() && prevTimestep > 0) { - _longtermTimestepDelta *= toDouble(timestep) / prevTimestep; - if (_longtermTimestepDelta < DefaultTimeStepDelta) { - _longtermTimestepDelta = DefaultTimeStepDelta; - } - } else { - _longtermTimestepDelta = DefaultTimeStepDelta; - } + resetTimelineTime(historyData.overall, _overallState, time); - std::vector newData; - newData.reserve(data.size()); - for (size_t i = 0; i < data.size(); ++i) { - if (data.at(i).time < toDouble(timestep)) { - newData.emplace_back(data.at(i)); + for (auto timelineIt = historyData.lineages.begin(); timelineIt != historyData.lineages.end();) { + auto& samples = timelineIt->second; + if (auto stateIt = _lineageStates.find(timelineIt->first); stateIt != _lineageStates.end()) { + resetTimelineTime(samples, stateIt->second, time); + } else { + std::erase_if(samples, [&](LineageSample const& sample) { return sample.time >= time; }); } + timelineIt = samples.empty() ? historyData.lineages.erase(timelineIt) : std::next(timelineIt); } - data.swap(newData); - _accumulatedDataPoint.reset(); - _numDataPoints = 0; } void StatisticsService::rewriteHistory(StatisticsHistory& history, StatisticsHistoryData const& newHistoryData, uint64_t timestep) { - _accumulatedDataPoint.reset(); - _numDataPoints = 0; - _lastTimelineStatistics.reset(); + _overallState = TimelineState(); + _lineageStates.clear(); _lastTimestep.reset(); - if (!newHistoryData.empty()) { - _longtermTimestepDelta = max(DefaultTimeStepDelta, (timestep - newHistoryData.front().time) / toDouble(newHistoryData.size())); - } else { - _longtermTimestepDelta = DefaultTimeStepDelta; + + if (!newHistoryData.overall.empty()) { + _overallState.longtermTimestepDelta = + std::max(DefaultTimeStepDelta, (toDouble(timestep) - newHistoryData.overall.front().time) / toDouble(newHistoryData.overall.size())); + } + for (auto const& [lineageId, samples] : newHistoryData.lineages) { + if (!samples.empty()) { + _lineageStates[lineageId].longtermTimestepDelta = + std::max(DefaultTimeStepDelta, (samples.back().time - samples.front().time) / toDouble(samples.size())); + } } std::lock_guard lock(history.getMutex()); history.getDataRef() = newHistoryData; } + +template +void StatisticsService::updateTimeline( + std::vector>& samples, + TimelineState& state, + TimedSample const& rawSample, + double time, + bool forceSampling) +{ + if (forceSampling || samples.empty() || time - samples.back().time > state.longtermTimestepDelta / 100 * (state.numDataPoints + 1)) { + state.accumulatedSample = state.accumulatedSample.has_value() ? *state.accumulatedSample + rawSample : rawSample; + ++state.numDataPoints; + } + + if (state.accumulatedSample.has_value() && (samples.empty() || time - samples.back().time > state.longtermTimestepDelta)) { + emitSample(samples, state, time); + } +} + +template +void StatisticsService::emitSample(std::vector>& samples, TimelineState& state, double time) +{ + auto newSample = *state.accumulatedSample / toDouble(state.numDataPoints); + state.numDataPoints = 0; + state.accumulatedSample.reset(); + + // Remove last entry if timestep has not changed + if (!samples.empty() && std::abs(samples.back().time - time) < NEAR_ZERO) { + samples.pop_back(); + } + samples.emplace_back(newSample); + + // Compress timeline after MaxSamples + if (samples.size() > MaxSamples) { + std::vector> newSamples; + newSamples.reserve(samples.size() / 2); + for (size_t i = 0; i < (samples.size() - 1) / 2; ++i) { + auto interpolatedSample = (samples.at(i * 2) + samples.at(i * 2 + 1)) / 2.0; + interpolatedSample.time = samples.at(i * 2).time; + interpolatedSample.timestep = samples.at(i * 2).timestep; + newSamples.emplace_back(interpolatedSample); + } + newSamples.emplace_back(samples.back()); + samples.swap(newSamples); + + state.longtermTimestepDelta *= 2.0; + } +} + +template +void StatisticsService::resetTimelineTime(std::vector>& samples, TimelineState& state, double time) +{ + if (!samples.empty() && samples.back().time > 0) { + state.longtermTimestepDelta = std::max(DefaultTimeStepDelta, state.longtermTimestepDelta * time / samples.back().time); + } else { + state.longtermTimestepDelta = DefaultTimeStepDelta; + } + std::erase_if(samples, [&](TimedSample const& sample) { return sample.time >= time; }); + state.accumulatedSample.reset(); + state.numDataPoints = 0; +} diff --git a/source/EngineImpl/StatisticsService.cuh b/source/EngineImpl/StatisticsService.cuh index bc648f00f4..77ef3b5fda 100644 --- a/source/EngineImpl/StatisticsService.cuh +++ b/source/EngineImpl/StatisticsService.cuh @@ -1,7 +1,12 @@ +#pragma once + #include +#include +#include #include +#include #include #include @@ -11,18 +16,36 @@ class StatisticsService MAKE_SINGLETON(StatisticsService); public: - void addDataPoint(StatisticsHistory& history, TimelineStatistics const& newTimelineStatistics, uint64_t timestep); + void addDataPoint(StatisticsHistory& history, StatisticsEntry const& overallStatistics, uint64_t timestep); void resetTime(StatisticsHistory& history, uint64_t timestep); void rewriteHistory(StatisticsHistory& history, StatisticsHistoryData const& newHistoryData, uint64_t timestep); private: static auto constexpr DefaultTimeStepDelta = 10.0; - double _longtermTimestepDelta = DefaultTimeStepDelta; - - int _numDataPoints = 0; - std::optional _accumulatedDataPoint; - - std::optional _lastTimelineStatistics; + template + struct TimelineState + { + double longtermTimestepDelta = DefaultTimeStepDelta; + int numDataPoints = 0; + std::optional> accumulatedSample; + }; + + template + void updateTimeline( + std::vector>& samples, + TimelineState& state, + TimedSample const& rawSample, + double time, + bool forceSampling); + + template + void emitSample(std::vector>& samples, TimelineState& state, double time); + + template + void resetTimelineTime(std::vector>& samples, TimelineState& state, double time); + + TimelineState _overallState; + std::unordered_map> _lineageStates; std::optional _lastTimestep; }; diff --git a/source/EngineImpl/TestKernelsService.cu b/source/EngineImpl/TestKernelsService.cu index 4e7326d9e9..e2568d1da0 100644 --- a/source/EngineImpl/TestKernelsService.cu +++ b/source/EngineImpl/TestKernelsService.cu @@ -15,9 +15,9 @@ void TestKernelsService::shutdown() CudaMemoryManager::getInstance().freeMemory(_cudaBoolResult); } -void TestKernelsService::testOnly_mutate(CudaSettings const& gpuSettings, SimulationData const& data, uint64_t objectId) +void TestKernelsService::testOnly_mutate(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& statistics, uint64_t objectId) { - KERNEL_CALL_MOD(cudaTestMutate, NEURONS_PER_CELL, data, objectId); + KERNEL_CALL_MOD(cudaTestMutate, NEURONS_PER_CELL, data, statistics, objectId); } void TestKernelsService::testOnly_removeUnusedGenes(CudaSettings const& gpuSettings, SimulationData const& data, uint64_t objectId) diff --git a/source/EngineImpl/TestKernelsService.cuh b/source/EngineImpl/TestKernelsService.cuh index 5736613708..6c5b7ff485 100644 --- a/source/EngineImpl/TestKernelsService.cuh +++ b/source/EngineImpl/TestKernelsService.cuh @@ -14,7 +14,7 @@ public: void init(); void shutdown(); - void testOnly_mutate(CudaSettings const& gpuSettings, SimulationData const& data, uint64_t objectId); + void testOnly_mutate(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& statistics, uint64_t objectId); void testOnly_removeUnusedGenes(CudaSettings const& gpuSettings, SimulationData const& data, uint64_t objectId); void testOnly_createConnection(CudaSettings const& gpuSettings, SimulationData const& data, uint64_t objectId1, uint64_t objectId2); void testOnly_createConnectionWithAbsAngle( diff --git a/source/EngineInterface/CMakeLists.txt b/source/EngineInterface/CMakeLists.txt index 663b1fe276..0bc46b4165 100644 --- a/source/EngineInterface/CMakeLists.txt +++ b/source/EngineInterface/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(EngineInterface NeuralNetWeight.h NumberGenerator.cpp NumberGenerator.h + StatisticsEntry.h ParametersEditService.cpp ParametersEditService.h ParametersFilter.h @@ -60,8 +61,7 @@ add_library(EngineInterface StatisticsConverterService.cpp StatisticsConverterService.h StatisticsHistory.cpp - StatisticsHistory.h - StatisticsRawData.h) + StatisticsHistory.h) target_link_libraries(EngineInterface Base) diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index 1813bc30f9..654f7b2d33 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -1,22 +1,74 @@ #include "DataPointCollection.h" -DataPoint DataPoint::operator+(DataPoint const& other) const +OverallDataPoint OverallDataPoint::operator+(OverallDataPoint const& other) const { - DataPoint result; - for (int i = 0; i < MAX_COLORS; ++i) { - result.values[i] = values[i] + other.values[i]; - } - result.summedValues = summedValues + other.summedValues; + OverallDataPoint result; + result.numCreatures = numCreatures + other.numCreatures; + result.averageCreatureCells = averageCreatureCells + other.averageCreatureCells; + result.averageGenomeNodes = averageGenomeNodes + other.averageGenomeNodes; + result.creatureEnergy = creatureEnergy + other.creatureEnergy; + result.averageMutationRate = averageMutationRate + other.averageMutationRate; + result.averageGeneration = averageGeneration + other.averageGeneration; + result.numLineages = numLineages + other.numLineages; + result.numSolidObjects = numSolidObjects + other.numSolidObjects; + result.numFluidObjects = numFluidObjects + other.numFluidObjects; + result.numCellObjects = numCellObjects + other.numCellObjects; + result.numEnergyParticles = numEnergyParticles + other.numEnergyParticles; + result.accumCreatedCreatures = accumCreatedCreatures + other.accumCreatedCreatures; + result.accumMutations = accumMutations + other.accumMutations; return result; } -DataPoint DataPoint::operator/(double divisor) const +OverallDataPoint OverallDataPoint::operator/(double divisor) const { - DataPoint result; - for (int i = 0; i < MAX_COLORS; ++i) { - result.values[i] = values[i] / divisor; - } - result.summedValues = summedValues / divisor; + OverallDataPoint result; + result.numCreatures = numCreatures / divisor; + result.averageCreatureCells = averageCreatureCells / divisor; + result.averageGenomeNodes = averageGenomeNodes / divisor; + result.creatureEnergy = creatureEnergy / divisor; + result.averageMutationRate = averageMutationRate / divisor; + result.averageGeneration = averageGeneration / divisor; + result.numLineages = numLineages / divisor; + result.numSolidObjects = numSolidObjects / divisor; + result.numFluidObjects = numFluidObjects / divisor; + result.numCellObjects = numCellObjects / divisor; + result.numEnergyParticles = numEnergyParticles / divisor; + result.accumCreatedCreatures = accumCreatedCreatures / divisor; + result.accumMutations = accumMutations / divisor; + return result; +} + +LineageDataPoint LineageDataPoint::operator+(LineageDataPoint const& other) const +{ + LineageDataPoint result; + result.colorBitset = colorBitset | other.colorBitset; + result.representativeCellId = representativeCellId != 0 ? representativeCellId : other.representativeCellId; + result.numCreatures = numCreatures + other.numCreatures; + result.numGenomes = numGenomes + other.numGenomes; + result.sumCreatureCells = sumCreatureCells + other.sumCreatureCells; + result.sumCreatureGenerations = sumCreatureGenerations + other.sumCreatureGenerations; + result.sumGenomeNodes = sumGenomeNodes + other.sumGenomeNodes; + result.sumMutationRates = sumMutationRates + other.sumMutationRates; + result.sumCreatureEnergy = sumCreatureEnergy + other.sumCreatureEnergy; + result.numCreatedCreatures = numCreatedCreatures + other.numCreatedCreatures; + result.totalMutations = totalMutations + other.totalMutations; + return result; +} + +LineageDataPoint LineageDataPoint::operator/(double divisor) const +{ + LineageDataPoint result; + result.colorBitset = colorBitset; + result.representativeCellId = representativeCellId; + result.numCreatures = numCreatures / divisor; + result.numGenomes = numGenomes / divisor; + result.sumCreatureCells = sumCreatureCells / divisor; + result.sumCreatureGenerations = sumCreatureGenerations / divisor; + result.sumGenomeNodes = sumGenomeNodes / divisor; + result.sumMutationRates = sumMutationRates / divisor; + result.sumCreatureEnergy = sumCreatureEnergy / divisor; + result.numCreatedCreatures = numCreatedCreatures / divisor; + result.totalMutations = totalMutations / divisor; return result; } @@ -24,32 +76,18 @@ DataPointCollection DataPointCollection::operator+(DataPointCollection const& ot { DataPointCollection result; result.time = time + other.time; + result.timestep = timestep + other.timestep; result.systemClock = systemClock + other.systemClock; - result.numObjects = numObjects + other.numObjects; - result.numSelfReplicators = numSelfReplicators + other.numSelfReplicators; - result.numColonies = numColonies + other.numColonies; - result.numViruses = numViruses + other.numViruses; - result.numFreeCells = numFreeCells + other.numFreeCells; - result.numEnergyParticles = numEnergyParticles + other.numEnergyParticles; - result.averageGenomeCells = averageGenomeCells + other.averageGenomeCells; - result.averageNumCells = averageNumCells + other.averageNumCells; - result.varianceNumCells = varianceNumCells + other.varianceNumCells; - result.maxNumCellsOfColonies = maxNumCellsOfColonies + other.maxNumCellsOfColonies; - result.totalEnergy = totalEnergy + other.totalEnergy; - result.numCreatedCells = numCreatedCells + other.numCreatedCells; - result.numAttacks = numAttacks + other.numAttacks; - result.numMuscleActivities = numMuscleActivities + other.numMuscleActivities; - result.numDefenderActivities = numDefenderActivities + other.numDefenderActivities; - result.numDepotActivities = numDepotActivities + other.numDepotActivities; - result.numInjectionActivities = numInjectionActivities + other.numInjectionActivities; - result.numCompletedInjections = numCompletedInjections + other.numCompletedInjections; - result.numGeneratorPulses = numGeneratorPulses + other.numGeneratorPulses; - result.numNeuronActivities = numNeuronActivities + other.numNeuronActivities; - result.numSensorActivities = numSensorActivities + other.numSensorActivities; - result.numSensorMatches = numSensorMatches + other.numSensorMatches; - result.numReconnectorCreated = numReconnectorCreated + other.numReconnectorCreated; - result.numReconnectorRemoved = numReconnectorRemoved + other.numReconnectorRemoved; - result.numDetonations = numDetonations + other.numDetonations; + result.overall = overall + other.overall; + result.lineages = lineages; + for (auto const& [lineageId, dataPoint] : other.lineages) { + auto it = result.lineages.find(lineageId); + if (it != result.lineages.end()) { + it->second = it->second + dataPoint; + } else { + result.lineages.emplace(lineageId, dataPoint); + } + } return result; } @@ -57,31 +95,12 @@ DataPointCollection DataPointCollection::operator/(double divisor) const { DataPointCollection result; result.time = time / divisor; + result.timestep = timestep / divisor; result.systemClock = systemClock / divisor; - result.numObjects = numObjects / divisor; - result.numSelfReplicators = numSelfReplicators / divisor; - result.numColonies = numColonies / divisor; - result.numViruses = numViruses / divisor; - result.numFreeCells = numFreeCells / divisor; - result.numEnergyParticles = numEnergyParticles / divisor; - result.averageGenomeCells = averageGenomeCells / divisor; - result.averageNumCells = averageNumCells / divisor; - result.varianceNumCells = varianceNumCells / divisor; - result.maxNumCellsOfColonies = maxNumCellsOfColonies / divisor; - result.totalEnergy = totalEnergy / divisor; - result.numCreatedCells = numCreatedCells / divisor; - result.numAttacks = numAttacks / divisor; - result.numMuscleActivities = numMuscleActivities / divisor; - result.numDefenderActivities = numDefenderActivities / divisor; - result.numDepotActivities = numDepotActivities / divisor; - result.numInjectionActivities = numInjectionActivities / divisor; - result.numCompletedInjections = numCompletedInjections / divisor; - result.numGeneratorPulses = numGeneratorPulses / divisor; - result.numNeuronActivities = numNeuronActivities / divisor; - result.numSensorActivities = numSensorActivities / divisor; - result.numSensorMatches = numSensorMatches / divisor; - result.numReconnectorCreated = numReconnectorCreated / divisor; - result.numReconnectorRemoved = numReconnectorRemoved / divisor; - result.numDetonations = numDetonations / divisor; + result.overall = overall / divisor; + result.lineages = lineages; + for (auto& [lineageId, dataPoint] : result.lineages) { + dataPoint = dataPoint / divisor; + } return result; } diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index 3099978f42..88d42118e1 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -1,47 +1,56 @@ #pragma once -#include +#include +#include -struct DataPoint +struct OverallDataPoint { - double values[MAX_COLORS] = {0, 0, 0, 0, 0, 0, 0}; - double summedValues = 0; + double numCreatures = 0; + double averageCreatureCells = 0; + double averageGenomeNodes = 0; + double creatureEnergy = 0; + double averageMutationRate = 0; + double averageGeneration = 0; + double numLineages = 0; + double numSolidObjects = 0; + double numFluidObjects = 0; + double numCellObjects = 0; + double numEnergyParticles = 0; - DataPoint operator+(DataPoint const& other) const; - DataPoint operator/(double divisor) const; + double accumCreatedCreatures = 0; // Raw accumulated value; rates are derived GUI-side + double accumMutations = 0; // Raw accumulated value; rates are derived GUI-side + + OverallDataPoint operator+(OverallDataPoint const& other) const; + OverallDataPoint operator/(double divisor) const; +}; + +struct LineageDataPoint +{ + uint32_t colorBitset = 0; + uint64_t representativeCellId = 0; // Cell of a creature with the highest generation; only meaningful in recent samples + double numCreatures = 0; + double numGenomes = 0; + double sumCreatureCells = 0; + double sumCreatureGenerations = 0; + double sumGenomeNodes = 0; + double sumMutationRates = 0; + double sumCreatureEnergy = 0; + + double numCreatedCreatures = 0; + double totalMutations = 0; + + LineageDataPoint operator+(LineageDataPoint const& other) const; + LineageDataPoint operator/(double divisor) const; }; struct DataPointCollection { - double time; //could be a time step or real-time + double time = 0; + double timestep = 0; double systemClock = 0; - DataPoint numObjects; - DataPoint numSelfReplicators; - DataPoint numColonies; - DataPoint numViruses; - DataPoint numFreeCells; - DataPoint numEnergyParticles; - DataPoint averageGenomeCells; - DataPoint averageNumCells; - DataPoint varianceNumCells; - DataPoint maxNumCellsOfColonies; - DataPoint totalEnergy; - - DataPoint numCreatedCells; - DataPoint numAttacks; - DataPoint numMuscleActivities; - DataPoint numDefenderActivities; - DataPoint numDepotActivities; - DataPoint numInjectionActivities; - DataPoint numCompletedInjections; - DataPoint numGeneratorPulses; - DataPoint numNeuronActivities; - DataPoint numSensorActivities; - DataPoint numSensorMatches; - DataPoint numReconnectorCreated; - DataPoint numReconnectorRemoved; - DataPoint numDetonations; + OverallDataPoint overall; + std::unordered_map lineages; DataPointCollection operator+(DataPointCollection const& other) const; DataPointCollection operator/(double divisor) const; diff --git a/source/EngineInterface/Definitions.h b/source/EngineInterface/Definitions.h index 27cb1d7c46..26b6553232 100644 --- a/source/EngineInterface/Definitions.h +++ b/source/EngineInterface/Definitions.h @@ -28,10 +28,6 @@ struct SettingsForSimulation; class _SimulationFacade; using SimulationFacade = std::shared_ptr<_SimulationFacade>; -struct TimelineStatistics; -struct HistogramData; -struct StatisticsRawData; - class SpaceCalculator; class ShapeGenerator; diff --git a/source/EngineInterface/Desc.h b/source/EngineInterface/Desc.h index 53197166aa..0353f7c9a1 100644 --- a/source/EngineInterface/Desc.h +++ b/source/EngineInterface/Desc.h @@ -588,6 +588,7 @@ struct CreatureDesc MEMBER(CreatureDesc, MutationState, mutationState, MutationState_Mutated); MEMBER(CreatureDesc, int, lineageId, 0); MEMBER(CreatureDesc, float, accumulatedMutations, 0.0f); + MEMBER(CreatureDesc, float, accumulatedMutationsInLineage, 0.0f); // Process data MEMBER(CreatureDesc, int, headUpdateId, 0); diff --git a/source/EngineInterface/DescValidationService.cpp b/source/EngineInterface/DescValidationService.cpp index 3a72c48357..9c1b6fb325 100644 --- a/source/EngineInterface/DescValidationService.cpp +++ b/source/EngineInterface/DescValidationService.cpp @@ -325,6 +325,7 @@ void DescValidationService::validateAndCorrect(ExtendedObjectDesc& extendedObjec auto& creature = extendedObject.creature.value(); creature._lineageId = std::max(creature._lineageId, 0); creature._accumulatedMutations = std::max(creature._accumulatedMutations, 0.0f); + creature._accumulatedMutationsInLineage = std::max(creature._accumulatedMutationsInLineage, 0.0f); } if (object.getObjectType() == ObjectType_Cell) { diff --git a/source/EngineInterface/SimulationFacade.h b/source/EngineInterface/SimulationFacade.h index 1ee1880d2c..f47d5c0d94 100644 --- a/source/EngineInterface/SimulationFacade.h +++ b/source/EngineInterface/SimulationFacade.h @@ -4,6 +4,7 @@ #include "DataPointCollection.h" #include "Definitions.h" #include "GeometryBuffers.h" +#include "StatisticsEntry.h" #include "PreviewDesc.h" #include "SelectionShallowData.h" #include "SettingsForSimulation.h" @@ -103,9 +104,9 @@ class _SimulationFacade //************ //* Statistics //************ - virtual StatisticsRawData getStatisticsRawData() const = 0; virtual StatisticsHistory const& getStatisticsHistory() const = 0; virtual void setStatisticsHistory(StatisticsHistoryData const& data) = 0; + virtual StatisticsEntry getStatisticsEntry() const = 0; //******************** //* Preview simulation diff --git a/source/EngineInterface/SimulationParameters.cpp b/source/EngineInterface/SimulationParameters.cpp index eb8f806bc8..a12149d898 100644 --- a/source/EngineInterface/SimulationParameters.cpp +++ b/source/EngineInterface/SimulationParameters.cpp @@ -416,6 +416,10 @@ ParametersSpec const& SimulationParameters::getSpec() .name("Maximum age") .reference(IntSpec().member(&SimulationParameters::maxCellAge).min(1).max(1e7).logarithmic(true).infinity(true)) .description("Defines the maximum age of a cell. If a cell exceeds this age it will be transformed to an energy particle."), + ParameterSpec() + .name("Maximum free cell age") + .reference(IntSpec().member(&SimulationParameters::freeCellMaxAge).min(1).max(1e7).logarithmic(true).infinity(true)) + .description("The maximal age of free cells (= cells that arise from energy particles) can be set here."), ParameterSpec() .name("Minimum energy") .reference(FloatSpec().member(&SimulationParameters::minCellEnergy).min(10.0f).max(200.0f).format("%.1f")) @@ -682,28 +686,6 @@ ParametersSpec const& SimulationParameters::getSpec() .reference(FloatSpec().member(&SimulationParameters::radiationAbsorptionLowVelocityPenalty).min(0.0f).max(1.0f).format("%.2f")) .description("When this parameter is increased, slowly moving cells will absorb less energy from an incoming energy particle."), }), - ParameterGroupSpec() - .name("Cell age limiter") - .expertToggle(&SimulationParameters::cellAgeLimiterToggle) - .parameters({ - ParameterSpec() - .name("Maximum free cell age") - .reference(IntSpec().member(&SimulationParameters::freeCellMaxAge).min(1).max(1e7).logarithmic(true).infinity(true)) - .description("The maximal age of free cells (= cells that arise from energy particles) can be set here."), - ParameterSpec() - .name("Reset age after construction") - .reference(BoolSpec().member(&SimulationParameters::resetCellAgeAfterActivation)) - .description( - "If this option is activated, the age of the cells is reset to 0 after the construction of their cell network is completed, " - "i.e. when the state of the cells changes from 'Under construction' to 'Ready'. This option is particularly useful if a low " - "'Maximum inactive cell age' is set, as cell networks that are under construction are inactive and could die immediately after " - "completion if their construction takes a long time."), - ParameterSpec() - .name("Maximum age balancing") - .reference(IntSpec().member(&SimulationParameters::maxCellAgeBalancerInterval).min(1e3).max(1e6).logarithmic(true)) - .description("Adjusts the maximum age at regular intervals. It increases the maximum age for the cell color where the fewest " - "replicators exist. Conversely, the maximum age is decreased for the cell color with the most replicators."), - }), ParameterGroupSpec() .name("Cell color transition rules") .expertToggle(&SimulationParameters::colorTransitionRulesToggle) diff --git a/source/EngineInterface/SimulationParameters.h b/source/EngineInterface/SimulationParameters.h index 36ec841fb1..7caffffef2 100644 --- a/source/EngineInterface/SimulationParameters.h +++ b/source/EngineInterface/SimulationParameters.h @@ -98,6 +98,7 @@ struct SimulationParameters // Cell life cycle BaseParameter> maxCellAge = {ColorVector::uniform(Infinity::value)}; + BaseParameter> freeCellMaxAge = {ColorVector::uniform(Infinity::value)}; BaseLayerParameter> minCellEnergy = {.baseValue = ColorVector::uniform(50.0f)}; BaseParameter> normalCellEnergy = {ColorVector::uniform(100.0f)}; BaseLayerParameter> cellDeathProbability = {.baseValue = ColorVector::uniform(0.001f)}; @@ -176,12 +177,6 @@ struct SimulationParameters BaseParameter> radiationAbsorptionHighVelocityPenalty = {ColorVector::uniform(0.0f)}; BaseLayerParameter> radiationAbsorptionLowVelocityPenalty = {.baseValue = ColorVector::uniform(0.0f)}; - // Expert settings: Cell age limiter - ExpertToggle cellAgeLimiterToggle = {false}; - BaseParameter> freeCellMaxAge = {ColorVector::uniform(Infinity::value)}; - BaseParameter resetCellAgeAfterActivation = {false}; // Candidate for deletion - EnableableBaseParameter maxCellAgeBalancerInterval = {.value = 10000, .enabled = false}; - // Expert settings: Cell color transition rules ExpertToggle colorTransitionRulesToggle = {false}; BaseLayerParameter> colorTransitionRules; diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index 3e32ed89e8..8a2f527e37 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -4,139 +4,49 @@ #include -namespace -{ - template - DataPoint getDataPointBySummation(ColorVector const& values) - { - DataPoint result; - result.summedValues = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - result.values[i] = toDouble(values[i]); - result.summedValues += result.values[i]; - } - return result; - } - - template - DataPoint getDataPointByMaximation(ColorVector const& values) - { - DataPoint result; - result.summedValues = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - result.values[i] = toDouble(values[i]); - result.summedValues = std::max(result.summedValues, result.values[i]); - } - return result; - } - - template - DataPoint getDataPointByAveraging(ColorVector const& summedValue, ColorVector const& numSelfReplicators) - { - DataPoint result; - auto sumSummedValue = 0.0; - auto sumNumSelfReplicators = 0.0; - for (int i = 0; i < MAX_COLORS; ++i) { - result.values[i] = toDouble(summedValue[i]); - sumSummedValue += result.values[i]; - sumNumSelfReplicators += numSelfReplicators[i]; - if (numSelfReplicators[i] > 0) { - result.values[i] /= numSelfReplicators[i]; - } - } - result.summedValues = sumNumSelfReplicators > 0 ? sumSummedValue / sumNumSelfReplicators : sumSummedValue; - return result; - } - - DataPoint getDataPointForProcessProperty( - ColorVector const& values, - ColorVector const& lastValues, - ColorVector const& numNonFreeCells, - double deltaTimesteps) - { - DataPoint result; - result.summedValues = 0; - auto sumNumFreeCells = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - if (lastValues[i] > values[i] || numNonFreeCells[i] == 0) { - result.values[i] = 0; - } else { - result.values[i] = toDouble(values[i] - lastValues[i]) / deltaTimesteps / toDouble(numNonFreeCells[i]); - result.summedValues += toDouble(values[i] - lastValues[i]) / deltaTimesteps; - sumNumFreeCells += numNonFreeCells[i]; - } - } - if (sumNumFreeCells != 0) { - result.summedValues /= toDouble(sumNumFreeCells); - } else { - result.summedValues = 0; - } - return result; - } - - -} - DataPointCollection StatisticsConverterService::convert( - TimelineStatistics const& data, + StatisticsEntry const& statisticsEntry, uint64_t timestep, - double time, - std::optional const& lastData, - std::optional lastTimestep) + double time) { DataPointCollection result; result.time = time; + result.timestep = toDouble(timestep); auto now = std::chrono::system_clock::now(); auto unixEpoch = std::chrono::time_point(); result.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); - result.numObjects = getDataPointBySummation(data.timestep.numObjects); - result.numSelfReplicators = getDataPointBySummation(data.timestep.numSelfReplicators); - result.numColonies = getDataPointBySummation(data.timestep.numColonies); - result.numViruses = getDataPointBySummation(data.timestep.numViruses); - result.numFreeCells = getDataPointBySummation(data.timestep.numFreeCells); - result.numEnergyParticles = getDataPointBySummation(data.timestep.numEnergyParticles); - result.averageNumCells = getDataPointByAveraging(data.timestep.numObjects, data.timestep.numSelfReplicators); - result.totalEnergy = getDataPointBySummation(data.timestep.totalEnergy); - - auto deltaTimesteps = lastTimestep ? toDouble(timestep) - toDouble(*lastTimestep) : 1.0; - if (deltaTimesteps < NEAR_ZERO) { - deltaTimesteps = 1.0; - } - - auto lastDataValue = lastData.value_or(data); - ColorVector numNonFreeCells; - for (int i = 0; i < MAX_COLORS; ++i) { - numNonFreeCells[i] = data.timestep.numObjects[i] - data.timestep.numFreeCells[i]; + auto const& o = statisticsEntry.overallEntry; + result.overall.numCreatures = toDouble(o.numCreatures); + result.overall.averageCreatureCells = o.numCreatures > 0 ? toDouble(o.sumCreatureCells) / o.numCreatures : 0.0; + result.overall.averageGeneration = o.numCreatures > 0 ? toDouble(o.sumCreatureGenerations) / o.numCreatures : 0.0; + result.overall.averageGenomeNodes = o.numGenomes > 0 ? toDouble(o.sumGenomeNodes) / o.numGenomes : 0.0; + result.overall.averageMutationRate = o.numGenomes > 0 ? toDouble(o.sumMutationRates) / o.numGenomes : 0.0; + result.overall.creatureEnergy = toDouble(o.sumCreatureEnergy); + result.overall.numLineages = toDouble(o.numActiveLineages); + result.overall.numSolidObjects = toDouble(o.numSolidObjects); + result.overall.numFluidObjects = toDouble(o.numFluidObjects); + result.overall.numCellObjects = toDouble(o.numCellObjects); + result.overall.numEnergyParticles = toDouble(o.numEnergyParticles); + result.overall.accumCreatedCreatures = toDouble(o.numCreatedCreatures); + result.overall.accumMutations = toDouble(o.totalMutations); + + for (auto const& entry : statisticsEntry.entries) { + LineageDataPoint point; + point.colorBitset = entry.colorBitset; + point.representativeCellId = entry.representativeCellId; + point.numCreatures = toDouble(entry.numCreatures); + point.numGenomes = toDouble(entry.numGenomes); + point.sumCreatureCells = toDouble(entry.sumCreatureCells); + point.sumCreatureGenerations = toDouble(entry.sumCreatureGenerations); + point.sumGenomeNodes = toDouble(entry.sumGenomeNodes); + point.sumMutationRates = toDouble(entry.sumMutationRates); + point.sumCreatureEnergy = toDouble(entry.sumCreatureEnergy); + point.numCreatedCreatures = toDouble(entry.numCreatedCreatures); + point.totalMutations = entry.totalMutations; + result.lineages[entry.lineageId] = point; } - result.numCreatedCells = - getDataPointForProcessProperty(data.accumulated.numCreatedCells, lastDataValue.accumulated.numCreatedCells, numNonFreeCells, deltaTimesteps); - result.numAttacks = getDataPointForProcessProperty(data.accumulated.numAttacks, lastDataValue.accumulated.numAttacks, numNonFreeCells, deltaTimesteps); - result.numMuscleActivities = - getDataPointForProcessProperty(data.accumulated.numMuscleActivities, lastDataValue.accumulated.numMuscleActivities, numNonFreeCells, deltaTimesteps); - result.numDefenderActivities = getDataPointForProcessProperty( - data.accumulated.numDefenderActivities, lastDataValue.accumulated.numDefenderActivities, numNonFreeCells, deltaTimesteps); - result.numDepotActivities = - getDataPointForProcessProperty(data.accumulated.numDepotActivities, lastDataValue.accumulated.numDepotActivities, numNonFreeCells, deltaTimesteps); - result.numInjectionActivities = getDataPointForProcessProperty( - data.accumulated.numInjectionActivities, lastDataValue.accumulated.numInjectionActivities, numNonFreeCells, deltaTimesteps); - result.numCompletedInjections = getDataPointForProcessProperty( - data.accumulated.numCompletedInjections, lastDataValue.accumulated.numCompletedInjections, numNonFreeCells, deltaTimesteps); - result.numGeneratorPulses = - getDataPointForProcessProperty(data.accumulated.numGeneratorPulses, lastDataValue.accumulated.numGeneratorPulses, numNonFreeCells, deltaTimesteps); - result.numNeuronActivities = - getDataPointForProcessProperty(data.accumulated.numNeuronActivities, lastDataValue.accumulated.numNeuronActivities, numNonFreeCells, deltaTimesteps); - result.numSensorActivities = - getDataPointForProcessProperty(data.accumulated.numSensorActivities, lastDataValue.accumulated.numSensorActivities, numNonFreeCells, deltaTimesteps); - result.numSensorMatches = - getDataPointForProcessProperty(data.accumulated.numSensorMatches, lastDataValue.accumulated.numSensorMatches, numNonFreeCells, deltaTimesteps); - result.numReconnectorCreated = getDataPointForProcessProperty( - data.accumulated.numReconnectorCreated, lastDataValue.accumulated.numReconnectorCreated, numNonFreeCells, deltaTimesteps); - result.numReconnectorRemoved = getDataPointForProcessProperty( - data.accumulated.numReconnectorRemoved, lastDataValue.accumulated.numReconnectorRemoved, numNonFreeCells, deltaTimesteps); - result.numDetonations = - getDataPointForProcessProperty(data.accumulated.numDetonations, lastDataValue.accumulated.numDetonations, numNonFreeCells, deltaTimesteps); return result; } diff --git a/source/EngineInterface/StatisticsConverterService.h b/source/EngineInterface/StatisticsConverterService.h index 7f518826fd..36479b5ca3 100644 --- a/source/EngineInterface/StatisticsConverterService.h +++ b/source/EngineInterface/StatisticsConverterService.h @@ -3,16 +3,12 @@ #include #include +#include class StatisticsConverterService { MAKE_SINGLETON(StatisticsConverterService); public: - DataPointCollection convert( - TimelineStatistics const& newData, - uint64_t timestep, - double time, - std::optional const& lastData, - std::optional lastTimestep); + DataPointCollection convert(StatisticsEntry const& statisticsEntry, uint64_t timestep, double time); }; diff --git a/source/EngineInterface/StatisticsEntry.h b/source/EngineInterface/StatisticsEntry.h new file mode 100644 index 0000000000..c3581f6153 --- /dev/null +++ b/source/EngineInterface/StatisticsEntry.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +struct OverallStatisticsEntry +{ + uint32_t numCreatures = 0; + uint32_t numGenomes = 0; + float sumCreatureCells = 0; + float sumCreatureGenerations = 0; + float sumGenomeNodes = 0; + float sumMutationRates = 0; + float sumCreatureEnergy = 0; + float sumAccumulatedMutations = 0; + uint32_t numSolidObjects = 0; + uint32_t numFluidObjects = 0; + uint32_t numCellObjects = 0; + uint32_t numEnergyParticles = 0; + uint32_t numActiveLineages = 0; + + unsigned long long numCreatedCreatures = 0; // Accumulated, never reset + float totalMutations = 0; // Accumulated, never reset +}; + +struct LineageStatisticsEntry +{ + uint32_t lineageId = 0; + uint32_t colorBitset = 0; + uint32_t numCreatures = 0; + uint32_t numGenomes = 0; + float sumCreatureCells = 0; + float sumCreatureGenerations = 0; + float sumGenomeNodes = 0; + float sumMutationRates = 0; + float sumCreatureEnergy = 0; + uint64_t representativeCellId = 0; // Cell of a creature with the highest generation; 0 = not set + + uint64_t numCreatedCreatures = 0; // Accumulated, never reset + double totalMutations = 0; // Accumulated, never reset +}; + +struct StatisticsEntry +{ + OverallStatisticsEntry overallEntry; + std::vector entries; // Per-lineage statistics, sorted by lineageId +}; diff --git a/source/EngineInterface/StatisticsHistory.h b/source/EngineInterface/StatisticsHistory.h index 030739951b..c240b25fe7 100644 --- a/source/EngineInterface/StatisticsHistory.h +++ b/source/EngineInterface/StatisticsHistory.h @@ -1,11 +1,51 @@ #pragma once +#include #include +#include #include #include "DataPointCollection.h" -using StatisticsHistoryData = std::vector; +template +struct TimedSample +{ + double time = 0; // Time since simulation start + double timestep = 0; + double systemClock = 0; + DataPoint data; + + TimedSample operator+(TimedSample const& other) const + { + TimedSample result; + result.time = time + other.time; + result.timestep = timestep + other.timestep; + result.systemClock = systemClock + other.systemClock; + result.data = data + other.data; + return result; + } + + TimedSample operator/(double divisor) const + { + TimedSample result; + result.time = time / divisor; + result.timestep = timestep / divisor; + result.systemClock = systemClock / divisor; + result.data = data / divisor; + return result; + } +}; + +using OverallSample = TimedSample; +using LineageSample = TimedSample; + +//the overall data and each lineage have separate timelines so that sampling density and compression +//can be controlled independently: young lineages keep a high sampling rate even in old simulations +struct StatisticsHistoryData +{ + std::vector overall; + std::unordered_map> lineages; +}; class StatisticsHistory { diff --git a/source/EngineInterface/StatisticsRawData.h b/source/EngineInterface/StatisticsRawData.h deleted file mode 100644 index b7041c1d7a..0000000000 --- a/source/EngineInterface/StatisticsRawData.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include -#include - -struct TimestepStatistics -{ - ColorVector numObjects = {}; - ColorVector numSelfReplicators = {}; - ColorVector numColonies = {}; - ColorVector numViruses = {}; - ColorVector numFreeCells = {}; - ColorVector numEnergyParticles = {}; - ColorVector totalEnergy = {}; -}; - -struct AccumulatedStatistics -{ - ColorVector numCreatedCells = {}; - ColorVector numCreatedReplicators = {}; - ColorVector numAttacks = {}; - ColorVector numMuscleActivities = {}; - ColorVector numDefenderActivities = {}; - ColorVector numDepotActivities = {}; - ColorVector numInjectionActivities = {}; - ColorVector numCompletedInjections = {}; - ColorVector numGeneratorPulses = {}; - ColorVector numNeuronActivities = {}; - ColorVector numSensorActivities = {}; - ColorVector numSensorMatches = {}; - ColorVector numReconnectorCreated = {}; - ColorVector numReconnectorRemoved = {}; - ColorVector numDetonations = {}; -}; - -struct TimelineStatistics -{ - TimestepStatistics timestep; - AccumulatedStatistics accumulated; -}; - -struct HistogramData -{ - int maxAge = 0; - int numCellsByColorBySlot[MAX_COLORS][MAX_HISTOGRAM_SLOTS]; -}; - -struct StatisticsRawData -{ - TimelineStatistics timeline; - HistogramData histogram; -}; - -inline double sumColorVector(ColorVector const& v) -{ - auto result = 0.0; - for (int i = 0; i < MAX_COLORS; ++i) { - result += v[i]; - } - return result; -}; diff --git a/source/EngineKernels/AttackerProcessor.cuh b/source/EngineKernels/AttackerProcessor.cuh index 01b6cb3e91..6d41d3d665 100644 --- a/source/EngineKernels/AttackerProcessor.cuh +++ b/source/EngineKernels/AttackerProcessor.cuh @@ -236,7 +236,6 @@ __device__ __inline__ void AttackerProcessor::processCell(SimulationData& data, cell->event = CellEvent_Attacking; cell->eventCounter = 6; - statistics.incNumAttacks(object->color); } // Radiation @@ -304,7 +303,6 @@ __inline__ __device__ int AttackerProcessor::countDefenderCells(SimulationStatis } if (connectedObject->typeData.cell.cellType == CellType_Defender && connectedObject->typeData.cell.cellTypeData.defender.mode == DefenderMode_DefendAgainstAttacker) { - statistics.incNumDefenderActivities(connectedObject->color); ++result; } } diff --git a/source/EngineKernels/CMakeLists.txt b/source/EngineKernels/CMakeLists.txt index c889e197fc..94a9e27985 100644 --- a/source/EngineKernels/CMakeLists.txt +++ b/source/EngineKernels/CMakeLists.txt @@ -51,8 +51,6 @@ add_library(EngineKernels Map.cuh MapSectionCollector.cuh Math.cuh - MaxAgeBalancer.cu - MaxAgeBalancer.cuh MemoryProcessor.cuh MuscleProcessor.cuh MutationProcessor.cuh diff --git a/source/EngineKernels/CellProcessor.cuh b/source/EngineKernels/CellProcessor.cuh index cc9fa6cfeb..9f3d8f044f 100644 --- a/source/EngineKernels/CellProcessor.cuh +++ b/source/EngineKernels/CellProcessor.cuh @@ -121,9 +121,6 @@ __inline__ __device__ void CellProcessor::cellStateTransition_calcFutureState(Si } else { if (origCellState == CellState_Activating) { cellState = CellState_Ready; - if (cudaSimulationParameters.cellAgeLimiterToggle.value && cudaSimulationParameters.resetCellAgeAfterActivation.value) { - atomicExch(&object->typeData.cell.age, 0); - } } else if (origCellState == CellState_Constructing) { if (isNeighborActivating) { cellState = CellState_Activating; @@ -455,10 +452,7 @@ __inline__ __device__ void CellProcessor::decay(SimulationData& data, bool isPre } // Free cell age radiation - auto cellMaxAge = Infinity::value; - if (object->type == ObjectType_FreeCell && cudaSimulationParameters.cellAgeLimiterToggle.value) { - cellMaxAge = cudaSimulationParameters.freeCellMaxAge.value[object->color]; - } + auto cellMaxAge = cudaSimulationParameters.freeCellMaxAge.value[object->color]; if (cellMaxAge > 0 && object->typeData.freeCell.age > cellMaxAge) { objectDestruction = true; } diff --git a/source/EngineKernels/ConstructorProcessor.cuh b/source/EngineKernels/ConstructorProcessor.cuh index da90050dd2..5d9c723f30 100644 --- a/source/EngineKernels/ConstructorProcessor.cuh +++ b/source/EngineKernels/ConstructorProcessor.cuh @@ -40,8 +40,8 @@ private: float neededDepotEnergy; }; __inline__ __device__ static void processCell(SimulationData& data, SimulationStatistics& statistics, Object* object, bool isPreview); - __inline__ __device__ static void mutateGenome(SimulationData& data, Object* object); - __inline__ __device__ static Creature* findOrCreateNewCreature(SimulationData& data, Object* object); + __inline__ __device__ static void mutateGenome(SimulationData& data, SimulationStatistics& statistics, Object* object); + __inline__ __device__ static Creature* findOrCreateNewCreature(SimulationData& data, SimulationStatistics& statistics, Object* object); __inline__ __device__ static ConstructionData createConstructionData(Object* object); __inline__ __device__ static Object* @@ -179,14 +179,14 @@ __inline__ __device__ void ConstructorProcessor::processCell(SimulationData& dat } // Important: mutate the host genome before it is cloned for the offspring. - mutateGenome(data, object); + mutateGenome(data, statistics, object); // The actual construction runs on a single thread. if (threadIdx.x != 0) { return; } - constructor.offspring = findOrCreateNewCreature(data, object); + constructor.offspring = findOrCreateNewCreature(data, statistics, object); // Check again after cloning the creature, because the offspring genome may diverge from the host genome. if (ConstructorHelper::isFinished(object, *constructor.offspring->genome)) { @@ -217,7 +217,7 @@ __inline__ __device__ void ConstructorProcessor::processCell(SimulationData& dat } } -__inline__ __device__ void ConstructorProcessor::mutateGenome(SimulationData& data, Object* object) +__inline__ __device__ void ConstructorProcessor::mutateGenome(SimulationData& data, SimulationStatistics& statistics, Object* object) { auto& cell = object->typeData.cell; auto& constructor = cell.constructor; @@ -238,7 +238,7 @@ __inline__ __device__ void ConstructorProcessor::mutateGenome(SimulationData& da __syncthreads(); if (clonedGenome != nullptr) { - MutationProcessor::applyMutations(data, cell.creature, clonedGenome); + MutationProcessor::applyMutations(data, statistics, cell.creature, clonedGenome); MutationProcessor::removeUnreachableGenesFromRoot(data, clonedGenome); if (threadIdx.x == 0) { cell.creature->genome = clonedGenome; @@ -247,7 +247,7 @@ __inline__ __device__ void ConstructorProcessor::mutateGenome(SimulationData& da __syncthreads(); } -__inline__ __device__ Creature* ConstructorProcessor::findOrCreateNewCreature(SimulationData& data, Object* object) +__inline__ __device__ Creature* ConstructorProcessor::findOrCreateNewCreature(SimulationData& data, SimulationStatistics& statistics, Object* object) { auto& constructor = object->typeData.cell.constructor; @@ -274,6 +274,7 @@ __inline__ __device__ Creature* ConstructorProcessor::findOrCreateNewCreature(Si factory.init(&data); auto result = factory.cloneCreature(object->typeData.cell.creature); result->numCells = 0; + statistics.incCreatedCreature(result->lineageId); return result; } @@ -637,8 +638,6 @@ __inline__ __device__ Object* ConstructorProcessor::constructCellIntern( } } - statistics.incNumCreatedCells(hostObject->color); - return result; } diff --git a/source/EngineKernels/DataAccessKernels.cu b/source/EngineKernels/DataAccessKernels.cu index b6d4180c1c..09dd3f90ec 100644 --- a/source/EngineKernels/DataAccessKernels.cu +++ b/source/EngineKernels/DataAccessKernels.cu @@ -300,6 +300,7 @@ namespace creatureTO.mutationState = creature->mutationState; creatureTO.lineageId = creature->lineageId; creatureTO.accumulatedMutations = creature->accumulatedMutations; + creatureTO.accumulatedMutationsInLineage = creature->accumulatedMutationsInLineage; creatureTO.headUpdateId = creature->headUpdateId; creatureTO.genomeArrayIndex = creature->genome->genomeIndex; diff --git a/source/EngineKernels/Definitions.cuh b/source/EngineKernels/Definitions.cuh index d19c60554a..2cb0ac69e1 100644 --- a/source/EngineKernels/Definitions.cuh +++ b/source/EngineKernels/Definitions.cuh @@ -34,9 +34,6 @@ class SelectionKernelsService; class StatisticsKernelsService; class TestKernelsService; -class _MaxAgeBalancer; -using MaxAgeBalancer = std::shared_ptr<_MaxAgeBalancer>; - class _CudaTOProvider; using CudaTOProvider = std::shared_ptr<_CudaTOProvider>; diff --git a/source/EngineKernels/DetonatorProcessor.cuh b/source/EngineKernels/DetonatorProcessor.cuh index 614c542da2..42c5dd2294 100644 --- a/source/EngineKernels/DetonatorProcessor.cuh +++ b/source/EngineKernels/DetonatorProcessor.cuh @@ -42,7 +42,6 @@ __device__ __inline__ void DetonatorProcessor::processCell(SimulationData& data, object->typeData.cell.event = CellEvent_Detonation; object->typeData.cell.eventCounter = 10; detonator.countdown = 0; - statistics.incNumDetonations(object->color); data.objectMap.executeForEach( object->pos, cudaSimulationParameters.detonatorRadius.value[object->color], object->detached(), [&](Object* const& otherObject) { if (otherObject == object) { diff --git a/source/EngineKernels/Entities.cuh b/source/EngineKernels/Entities.cuh index ae06d890e8..5aa78d3620 100644 --- a/source/EngineKernels/Entities.cuh +++ b/source/EngineKernels/Entities.cuh @@ -450,7 +450,8 @@ struct Creature MutationState mutationState; uint32_t lineageId; - float accumulatedMutations; + float accumulatedMutations; // Never reset, total over the whole ancestry + float accumulatedMutationsInLineage; // Reset when a new lineage is formed // Process data uint32_t headUpdateId; // Will be updated regularly to trigger head updates diff --git a/source/EngineKernels/EntityFactory.cuh b/source/EngineKernels/EntityFactory.cuh index 871476c397..dd34e3bbf2 100644 --- a/source/EngineKernels/EntityFactory.cuh +++ b/source/EngineKernels/EntityFactory.cuh @@ -618,6 +618,7 @@ __inline__ __device__ void EntityFactory::changeCreatureFromTO(CreatureTO const& creature->mutationState = creatureTO.mutationState; creature->lineageId = creatureTO.lineageId; creature->accumulatedMutations = creatureTO.accumulatedMutations; + creature->accumulatedMutationsInLineage = creatureTO.accumulatedMutationsInLineage; creature->headUpdateId = creatureTO.headUpdateId; } diff --git a/source/EngineKernels/GeneratorProcessor.cuh b/source/EngineKernels/GeneratorProcessor.cuh index d31a199ed7..771340377c 100644 --- a/source/EngineKernels/GeneratorProcessor.cuh +++ b/source/EngineKernels/GeneratorProcessor.cuh @@ -64,7 +64,5 @@ __inline__ __device__ void GeneratorProcessor::process(SimulationData& data, Sim if (generator.numPulses >= period) { generator.numPulses -= period; } - - statistics.incNumGeneratorPulses(object->color); } } diff --git a/source/EngineKernels/InjectorProcessor.cuh b/source/EngineKernels/InjectorProcessor.cuh index 182441e215..871455f0ea 100644 --- a/source/EngineKernels/InjectorProcessor.cuh +++ b/source/EngineKernels/InjectorProcessor.cuh @@ -93,7 +93,6 @@ __inline__ __device__ int InjectorProcessor::countDefenderCells(SimulationStatis auto connectedObject = object->connections[i].object; if (connectedObject->typeData.cell.cellType == CellType_Defender && connectedObject->typeData.cell.cellTypeData.defender.mode == DefenderMode_DefendAgainstInjector) { - statistics.incNumDefenderActivities(connectedObject->color); ++result; } } diff --git a/source/EngineKernels/MaxAgeBalancer.cu b/source/EngineKernels/MaxAgeBalancer.cu deleted file mode 100644 index c37fb0caa7..0000000000 --- a/source/EngineKernels/MaxAgeBalancer.cu +++ /dev/null @@ -1,111 +0,0 @@ -#include - -#include "Base.cuh" -#include "MaxAgeBalancer.cuh" - -namespace -{ - auto constexpr AdaptionRatio = 1.3; - auto constexpr AdaptionFactor = 1.1; - auto constexpr MaxCellAge = 300000; - auto constexpr MinReplicatorsUpperValue = 100; - auto constexpr MinReplicatorsLowerValue = 20; -} - -bool _MaxAgeBalancer::balance(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep) -{ - auto result = false; - if (parameters.cellAgeLimiterToggle.value && parameters.maxCellAgeBalancerInterval.enabled) { - initializeIfNecessary(parameters, timestep); - result |= doAdaptionIfNecessary(parameters, statistics, timestep); - } - - saveLastState(parameters); - - return result; -} - -void _MaxAgeBalancer::initializeIfNecessary(SimulationParameters const& parameters, uint64_t timestep) -{ - auto needsInitialization = false; - for (int i = 0; i < MAX_COLORS; ++i) { - if (parameters.maxCellAge.value[i] != _lastObjectMaxAge[i]) { - needsInitialization = true; - } - } - if (parameters.maxCellAgeBalancerInterval.enabled != _lastAdaptiveCellMaxAge) { - needsInitialization = true; - } - - if (needsInitialization) { - for (int i = 0; i < MAX_COLORS; ++i) { - _cellMaxAge[i] = parameters.maxCellAge.value[i]; - } - startNewMeasurement(timestep); - } -} - -bool _MaxAgeBalancer::doAdaptionIfNecessary(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep) -{ - auto result = false; - for (int i = 0; i < MAX_COLORS; ++i) { - _numReplicators[i] += statistics.timeline.timestep.numSelfReplicators[i]; - } - ++_numMeasurements; - if (timestep - *_lastTimestep > parameters.maxCellAgeBalancerInterval.value) { - uint64_t maxReplicators = 0; - uint64_t averageReplicators = 0; - int numAveragedReplicators = 0; - std::vector colors; - for (int color = 0; color < MAX_COLORS; ++color) { - if (maxReplicators < _numReplicators[color]) { - maxReplicators = _numReplicators[color]; - } - if (_numReplicators[color] / _numMeasurements > MinReplicatorsLowerValue) { - averageReplicators += _numReplicators[color]; - ++numAveragedReplicators; - } - } - if (numAveragedReplicators > 0) { - averageReplicators /= numAveragedReplicators; - } - - if (averageReplicators > 0) { - for (int color = 0; color < MAX_COLORS; ++color) { - if (toDouble(_numReplicators[color]) / _numMeasurements > MinReplicatorsUpperValue - && toDouble(_numReplicators[color]) / toDouble(averageReplicators) > AdaptionRatio) { - _cellMaxAge[color] /= AdaptionFactor; - } else if ( - _cellMaxAge[color] < MaxCellAge - && (_numReplicators[color] / _numMeasurements <= MinReplicatorsLowerValue - || toDouble(averageReplicators) / toDouble(_numReplicators[color]) > AdaptionRatio)) { - _cellMaxAge[color] *= AdaptionFactor; - } - } - - for (int i = 0; i < MAX_COLORS; ++i) { - parameters.maxCellAge.value[i] = toInt(_cellMaxAge[i]); - } - result = true; - } - startNewMeasurement(timestep); - } - return result; -} - -void _MaxAgeBalancer::startNewMeasurement(uint64_t timestep) -{ - _lastTimestep = timestep; - for (int i = 0; i < MAX_COLORS; ++i) { - _numReplicators[i] = 0; - } - _numMeasurements = 0; -} - -void _MaxAgeBalancer::saveLastState(SimulationParameters const& parameters) -{ - for (int i = 0; i < MAX_COLORS; ++i) { - _lastObjectMaxAge[i] = parameters.maxCellAge.value[i]; - } - _lastAdaptiveCellMaxAge = parameters.maxCellAgeBalancerInterval.enabled; -} diff --git a/source/EngineKernels/MaxAgeBalancer.cuh b/source/EngineKernels/MaxAgeBalancer.cuh deleted file mode 100644 index b771020d1c..0000000000 --- a/source/EngineKernels/MaxAgeBalancer.cuh +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -class _MaxAgeBalancer -{ -public: - //returns true if parameters have been changed - bool balance(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep); - -private: - void initializeIfNecessary(SimulationParameters const& parameters, uint64_t timestep); - bool doAdaptionIfNecessary(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep); - void startNewMeasurement(uint64_t timestep); - void saveLastState(SimulationParameters const& parameters); - - ColorVector _numReplicators = {}; - int _numMeasurements = 0; - std::optional _lastTimestep; - ColorVector _cellMaxAge = {}; //cloned parameter with double precision - - bool _lastAdaptiveCellMaxAge = false; - ColorVector _lastObjectMaxAge = {}; -}; diff --git a/source/EngineKernels/MuscleProcessor.cuh b/source/EngineKernels/MuscleProcessor.cuh index 24e3596df3..a114dc04b2 100644 --- a/source/EngineKernels/MuscleProcessor.cuh +++ b/source/EngineKernels/MuscleProcessor.cuh @@ -238,7 +238,6 @@ __inline__ __device__ void MuscleProcessor::autoBending(SimulationData& data, Si auto acceleration = direction * angleDelta * cudaSimulationParameters.muscleBendingAcceleration.value[object->color] / 30.0f * TIMESTEPS_PER_CELL_FUNCTION; applyAcceleration(object, acceleration); - statistics.incNumMuscleActivities(object->color); radiate(data, object, activation); } @@ -330,7 +329,6 @@ __inline__ __device__ void MuscleProcessor::manualBending(SimulationData& data, auto acceleration = direction * angleDelta * cudaSimulationParameters.muscleBendingAcceleration.value[object->color] / 30.0f * TIMESTEPS_PER_CELL_FUNCTION; applyAcceleration(object, acceleration); - statistics.incNumMuscleActivities(object->color); radiate(data, object, activation); } @@ -383,7 +381,6 @@ __inline__ __device__ void MuscleProcessor::angleBending(SimulationData& data, S atomicAdd(&bendingInfo.connectionNext->angleFromPrevious, -angleDelta); object->typeData.cell.frontAngle -= angleDelta; - statistics.incNumMuscleActivities(object->color); radiate(data, object, activation); } @@ -457,7 +454,6 @@ __inline__ __device__ void MuscleProcessor::autoCrawling(SimulationData& data, S applyAcceleration(object, acceleration); crawling.lastActualDistance = actualDistance; - statistics.incNumMuscleActivities(object->color); radiate(data, object, activation); } @@ -526,7 +522,6 @@ __inline__ __device__ void MuscleProcessor::manualCrawling(SimulationData& data, applyAcceleration(object, acceleration); crawling.lastActualDistance = actualDistance; - statistics.incNumMuscleActivities(object->color); radiate(data, object, activation); } @@ -543,7 +538,6 @@ __inline__ __device__ void MuscleProcessor::directMovement(SimulationData& data, object->vel += direction; object->typeData.cell.cellTypeData.muscle.lastMovementX = direction.x; object->typeData.cell.cellTypeData.muscle.lastMovementY = direction.y; - statistics.incNumMuscleActivities(object->color); radiate(data, object, activation); } diff --git a/source/EngineKernels/MutationProcessor.cuh b/source/EngineKernels/MutationProcessor.cuh index 0ed0287f19..efe4cfce33 100644 --- a/source/EngineKernels/MutationProcessor.cuh +++ b/source/EngineKernels/MutationProcessor.cuh @@ -19,7 +19,7 @@ namespace cg_mutation = cooperative_groups; class MutationProcessor { public: - __inline__ __device__ static void applyMutations(SimulationData& data, Creature* creature, Genome* genome); + __inline__ __device__ static void applyMutations(SimulationData& data, SimulationStatistics& statistics, Creature* creature, Genome* genome); __inline__ __device__ static void removeUnreachableGenesFromRoot(SimulationData& data, Genome* genome); private: @@ -55,8 +55,12 @@ private: __inline__ __device__ static void removeNode(SimulationData& data, Gene& gene, int position); __inline__ __device__ static void correctGenome(SimulationData& data, Genome* genome); - __inline__ __device__ static void - updateAccumulatedMutationsAndLineageId(SimulationData& data, Creature* creature, Genome* genome, float& accumulatedMutations); + __inline__ __device__ static void updateAccumulatedMutationsAndLineageId( + SimulationData& data, + SimulationStatistics& statistics, + Creature* creature, + Genome* genome, + float& accumulatedMutations); __inline__ __device__ static float generateGaussian(SimulationData& data); __inline__ __device__ static bool isRandomEvent(SimulationData& data, float probability); @@ -66,7 +70,7 @@ private: /************************************************************************/ /* Implementation */ /************************************************************************/ -__inline__ __device__ void MutationProcessor::applyMutations(SimulationData& data, Creature* creature, Genome* genome) +__inline__ __device__ void MutationProcessor::applyMutations(SimulationData& data, SimulationStatistics& statistics, Creature* creature, Genome* genome) { __shared__ float accumulatedMutations; auto block = cg_mutation::this_thread_block(); @@ -128,7 +132,7 @@ __inline__ __device__ void MutationProcessor::applyMutations(SimulationData& dat } block.sync(); - updateAccumulatedMutationsAndLineageId(data, creature, genome, accumulatedMutations); + updateAccumulatedMutationsAndLineageId(data, statistics, creature, genome, accumulatedMutations); } __inline__ __device__ void MutationProcessor::applyMutations_neurons(SimulationData& data, Genome* genome, float& accumulatedMutations) @@ -1866,19 +1870,27 @@ __inline__ __device__ void MutationProcessor::applyMutations_meta(SimulationData } } -__inline__ __device__ void -MutationProcessor::updateAccumulatedMutationsAndLineageId(SimulationData& data, Creature* creature, Genome* genome, float& accumulatedMutations) +__inline__ __device__ void MutationProcessor::updateAccumulatedMutationsAndLineageId( + SimulationData& data, + SimulationStatistics& statistics, + Creature* creature, + Genome* genome, + float& accumulatedMutations) { auto laneId = cg_mutation::this_thread_block().thread_rank(); if (laneId == 0) { auto numberOfNodes = getNumberOfNodes(genome); auto denominator = numberOfNodes > 0 ? toFloat(numberOfNodes) : 1.0f; - - creature->accumulatedMutations += accumulatedMutations / denominator; - if (creature->accumulatedMutations > cudaSimulationParameters.newLineageThreshold.value) { + auto delta = accumulatedMutations / denominator; + creature->accumulatedMutations += delta; + creature->accumulatedMutationsInLineage += delta; + if (delta > 0) { + statistics.addMutations(creature->lineageId, delta); + } + if (creature->accumulatedMutationsInLineage > cudaSimulationParameters.newLineageThreshold.value) { creature->lineageId = data.primaryNumberGen.createLineageId(); - creature->accumulatedMutations = 0; + creature->accumulatedMutationsInLineage = 0; } } } diff --git a/source/EngineKernels/ReconnectorProcessor.cuh b/source/EngineKernels/ReconnectorProcessor.cuh index bcb469cf43..ed51820620 100644 --- a/source/EngineKernels/ReconnectorProcessor.cuh +++ b/source/EngineKernels/ReconnectorProcessor.cuh @@ -132,7 +132,6 @@ __inline__ __device__ void ReconnectorProcessor::tryCreateConnection(SimulationD if (object->numConnections < MAX_OBJECT_CONNECTIONS && closestCell->numConnections < MAX_OBJECT_CONNECTIONS) { ObjectConnectionProcessor::scheduleAddConnectionPair(data, object, closestCell); object->typeData.cell.signal.channels[Channels::ReconnectorSuccess] = 1; - statistics.incNumReconnectorCreated(object->color); } lock.releaseLock(); } @@ -161,7 +160,6 @@ __inline__ __device__ void ReconnectorProcessor::removeConnections(SimulationDat if (shouldRemove) { ObjectConnectionProcessor::scheduleDeleteConnectionPair(data, object, connectedObject); object->typeData.cell.signal.channels[Channels::ReconnectorSuccess] = 1; - statistics.incNumReconnectorRemoved(object->color); } } object->releaseLock(); diff --git a/source/EngineKernels/SensorProcessor.cuh b/source/EngineKernels/SensorProcessor.cuh index d343be577f..4c546e8ec6 100644 --- a/source/EngineKernels/SensorProcessor.cuh +++ b/source/EngineKernels/SensorProcessor.cuh @@ -70,8 +70,6 @@ __inline__ __device__ void SensorProcessor::processCell(SimulationData& data, Si __syncthreads(); if (isTriggered) { - statistics.incNumSensorActivities(object->color); - if (object->typeData.cell.cellTypeData.sensor.mode != SensorMode_Telemetry) { processDetection(data, statistics, object); } else { @@ -228,7 +226,6 @@ __inline__ __device__ void SensorProcessor::initialScan(SimulationData& data, Si auto refAngle = Math::angleOfVector(ObjectConnectionProcessor::calcReferenceDirection(data, object)); auto relAngle = Math::getNormalizedAngle(absAngle - refAngle - object->typeData.cell.frontAngle, -180.0f); writeSignal(object->typeData.cell.signal, relAngle, density, distance); - statistics.incNumSensorMatches(object->color); auto matchPos = object->pos + Math::unitVectorOfAngle(absAngle) * distance; data.objectMap.correctPosition(matchPos); @@ -324,7 +321,6 @@ __inline__ __device__ void SensorProcessor::relocateLastMatch(SimulationData& da auto relAngle = Math::getNormalizedAngle(absAngle - refAngle - object->typeData.cell.frontAngle, -180.0f); writeSignal(object->typeData.cell.signal, relAngle, density, distance); - statistics.incNumSensorMatches(object->color); object->typeData.cell.cellTypeData.sensor.lastMatchAvailable = true; object->typeData.cell.cellTypeData.sensor.lastMatch.creatureIdPart = creatureIdPart; diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index f022756b8c..b0256d5be5 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -1,6 +1,8 @@ #pragma once -#include +#include + +#include #include "Base.cuh" #include "CudaMemoryManager.cuh" @@ -10,129 +12,341 @@ class SimulationStatistics public: __host__ void init() { - CudaMemoryManager::getInstance().acquireMemory(1, _data); - CudaMemoryManager::getInstance().acquireMemory(MutantToColorCountMapSize, _mutantToMutantStatisticsMap); - CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(StatisticsRawData))); + _lineageArrayCapacity = 1 << 18; + + CudaMemoryManager::getInstance().acquireMemory(1, _overallStatisticsEntry); + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageStatisticsEntries); + + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageMap); + + CudaMemoryManager::getInstance().acquireMemory(1, _lineageAccumulatorMapControl); + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[0]); + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[1]); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_overallStatisticsEntry, 0, sizeof(OverallStatisticsEntry))); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMapControl, 0, sizeof(LineageAccumulatorMapControl))); + + // Values must start at zero; the lineageId key column (first member) is set to LineageIdEmpty + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMap, 0, sizeof(LineageMapEntry) * _lineageArrayCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageMap, sizeof(LineageMapEntry), 0xff, sizeof(uint32_t), _lineageArrayCapacity)); + for (int i = 0; i < 2; ++i) { + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMaps[i], 0, sizeof(LineageAccumulatorMapEntry) * _lineageArrayCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageAccumulatorMaps[i], sizeof(LineageAccumulatorMapEntry), 0xff, sizeof(uint32_t), _lineageArrayCapacity)); + } } __host__ void free() { - CudaMemoryManager::getInstance().freeMemory(_data); - CudaMemoryManager::getInstance().freeMemory(_mutantToMutantStatisticsMap); + CudaMemoryManager::getInstance().freeMemory(_overallStatisticsEntry); + CudaMemoryManager::getInstance().freeMemory(_lineageStatisticsEntries); + + CudaMemoryManager::getInstance().freeMemory(_lineageMap); + + CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMapControl); + CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[0]); + CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[1]); } - __host__ StatisticsRawData getStatistics() + __host__ StatisticsEntry getStatisticsEntry() { - StatisticsRawData result; - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _data, sizeof(StatisticsRawData), cudaMemcpyDeviceToHost)); + StatisticsEntry result; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result.overallEntry, _overallStatisticsEntry, sizeof(OverallStatisticsEntry), cudaMemcpyDeviceToHost)); + + auto control = getLineageMapControl(); + result.entries.resize(control.numCompactEntries); + if (control.numCompactEntries > 0) { + CHECK_FOR_DEVICE_ERRORS( + cudaMemcpy(result.entries.data(), _lineageStatisticsEntries, sizeof(LineageStatisticsEntry) * control.numCompactEntries, cudaMemcpyDeviceToHost)); + } + std::sort(result.entries.begin(), result.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); return result; } - //timestep statistics - __inline__ __device__ void resetTimestepData() + __host__ bool isLineageAccumulatorGCNeeded() const { - if (threadIdx.x == 0 && blockIdx.x == 0) { - _data->timeline.timestep = TimestepStatistics(); - } - auto partition = calcSystemThreadPartition(MutantToColorCountMapSize); - for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { - _mutantToMutantStatisticsMap[index].count = 0; - _mutantToMutantStatisticsMap[index].numObjects = 0; - _mutantToMutantStatisticsMap[index].color = 0; - } + auto control = getLineageMapControl(); + return control.numUsedAccumulatorSlots[control.activeAccumulatorBuffer] > static_cast(_lineageArrayCapacity) / 2; + } + + //evolution statistics (timestep) + __inline__ __device__ void resetEvolutionStatistics() + { + // numCreatedCreatures and totalMutations are accumulated, never reset here + auto& overall = *_overallStatisticsEntry; + overall.numCreatures = 0; + overall.numGenomes = 0; + overall.sumCreatureCells = 0; + overall.sumCreatureGenerations = 0; + overall.sumGenomeNodes = 0; + overall.sumMutationRates = 0; + overall.sumCreatureEnergy = 0; + overall.sumAccumulatedMutations = 0; + overall.numSolidObjects = 0; + overall.numFluidObjects = 0; + overall.numCellObjects = 0; + overall.numEnergyParticles = 0; + overall.numActiveLineages = 0; + } + __inline__ __device__ void incNumSolidObjects() { atomicAdd(&_overallStatisticsEntry->numSolidObjects, 1u); } + __inline__ __device__ void incNumFluidObjects() { atomicAdd(&_overallStatisticsEntry->numFluidObjects, 1u); } + __inline__ __device__ void incNumCellObjects() { atomicAdd(&_overallStatisticsEntry->numCellObjects, 1u); } + __inline__ __device__ void incNumEnergyParticles() { atomicAdd(&_overallStatisticsEntry->numEnergyParticles, 1u); } + __inline__ __device__ void addCreatureStatistics(uint32_t numCells, uint32_t generation, float accumulatedMutations) + { + auto& overall = *_overallStatisticsEntry; + atomicAdd(&overall.numCreatures, 1u); + atomicAdd(&overall.sumCreatureCells, toFloat(numCells)); + atomicAdd(&overall.sumCreatureGenerations, toFloat(generation)); + atomicAdd(&overall.sumAccumulatedMutations, accumulatedMutations); } + __inline__ __device__ void addGenomeStatistics(float numNodes, float meanMutationRate) + { + auto& overall = *_overallStatisticsEntry; + atomicAdd(&overall.numGenomes, 1u); + atomicAdd(&overall.sumGenomeNodes, numNodes); + atomicAdd(&overall.sumMutationRates, meanMutationRate); + } + __inline__ __device__ void addCreatureEnergy(float value) { atomicAdd(&_overallStatisticsEntry->sumCreatureEnergy, value); } + + //lineage statistics + __inline__ __device__ int getLineageMapCapacity() const { return _lineageArrayCapacity; } + __inline__ __device__ void resetLineageMapSlot(int index) + { + auto& slot = _lineageMap[index]; + slot.lineageId = LineageIdEmpty; + slot.colorBitset = 0; + slot.numCreatures = 0; + slot.numGenomes = 0; + slot.sumCreatureCells = 0; + slot.sumCreatureGenerations = 0; + slot.sumGenomeNodes = 0; + slot.sumMutationRates = 0; + slot.sumCreatureEnergy = 0; + slot.maxCreatureGeneration = 0; + slot.representativeCellId = 0; + } + __inline__ __device__ void resetCompactLineageCounter() { _lineageAccumulatorMapControl->numCompactEntries = 0; } - __inline__ __device__ void incNumObjects(int color) { atomicAdd(&(_data->timeline.timestep.numObjects[color]), 1); } - __inline__ __device__ void incNumReplicator(int color) { atomicAdd(&_data->timeline.timestep.numSelfReplicators[color], 1); } - __inline__ __device__ int getNumReplicators() + __inline__ __device__ int insertOrFindLineageSlot(uint32_t lineageId) { - auto result = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - result += _data->timeline.timestep.numSelfReplicators[i]; + auto mask = _lineageArrayCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageArrayCapacity; ++i) { + auto origLineageId = atomicCAS(&_lineageMap[index].lineageId, LineageIdEmpty, lineageId); + if (origLineageId == LineageIdEmpty || origLineageId == lineageId) { + return index; + } + index = (index + 1) & mask; } - return result; + return -1; } - __inline__ __device__ void incNumViruses(int color) { atomicAdd(&_data->timeline.timestep.numViruses[color], 1); } - __inline__ __device__ void incNumFreeCells(int color) { atomicAdd(&_data->timeline.timestep.numFreeCells[color], 1); } - __inline__ __device__ void incNumParticles(int color) { atomicAdd(&_data->timeline.timestep.numEnergyParticles[color], 1); } - __inline__ __device__ void addEnergy(int color, float valueToAdd) { atomicAdd(&_data->timeline.timestep.totalEnergy[color], valueToAdd); } - __inline__ __device__ void addNumObjects(int color, float valueToAdd) { atomicAdd(&_data->timeline.timestep.numObjects[color], valueToAdd); } - __inline__ __device__ double getSummedNumCells() + __inline__ __device__ int findLineageSlot(uint32_t lineageId) const { - auto result = 0.0; - for (int i = 0; i < MAX_COLORS; ++i) { - result += toDouble(_data->timeline.timestep.numObjects[i]); + auto mask = _lineageArrayCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageArrayCapacity; ++i) { + auto slotLineageId = _lineageMap[index].lineageId; + if (slotLineageId == lineageId) { + return index; + } + if (slotLineageId == LineageIdEmpty) { + return -1; + } + index = (index + 1) & mask; } - return result; + return -1; } - __inline__ __device__ void incMutant(int color, uint32_t lineageId, float numCells) + __inline__ __device__ void addLineageCreatureData(int slotIndex, uint32_t numCells, uint32_t generation) { - atomicAdd(&_mutantToMutantStatisticsMap[lineageId % MutantToColorCountMapSize].count, 1); - atomicMax(&_mutantToMutantStatisticsMap[lineageId % MutantToColorCountMapSize].color, color); - atomicAdd(&_mutantToMutantStatisticsMap[lineageId % MutantToColorCountMapSize].numObjects, numCells); + auto& slot = _lineageMap[slotIndex]; + atomicAdd(&slot.numCreatures, 1u); + atomicAdd(&slot.sumCreatureCells, toFloat(numCells)); + atomicAdd(&slot.sumCreatureGenerations, toFloat(generation)); + atomicMax(&slot.maxCreatureGeneration, generation); } - __inline__ __device__ void halveNumConnections() + __inline__ __device__ void updateLineageRepresentativeCell(int slotIndex, uint32_t generation, uint64_t cellId) { - for (int i = 0; i < MAX_COLORS; ++i) { - _data->timeline.timestep.numFreeCells[i] /= 2; + auto& slot = _lineageMap[slotIndex]; + if (generation == slot.maxCreatureGeneration) { + atomicCAS(reinterpret_cast(&slot.representativeCellId), 0ull, static_cast(cellId)); } } - - //accumulated statistics - __host__ void resetAccumulatedStatistics() + __inline__ __device__ void addLineageGenomeData(int slotIndex, float numNodes, float meanMutationRate, uint32_t nodeColorBitset) + { + auto& slot = _lineageMap[slotIndex]; + atomicAdd(&slot.numGenomes, 1u); + atomicAdd(&slot.sumGenomeNodes, numNodes); + atomicAdd(&slot.sumMutationRates, meanMutationRate); + atomicOr(&slot.colorBitset, nodeColorBitset); + } + __inline__ __device__ void addLineageEnergy(int slotIndex, float energy) { - StatisticsRawData hostData; - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&hostData, _data, sizeof(StatisticsRawData), cudaMemcpyDeviceToHost)); - hostData.timeline.accumulated = AccumulatedStatistics(); - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(_data, &hostData, sizeof(StatisticsRawData), cudaMemcpyHostToDevice)); + auto& slot = _lineageMap[slotIndex]; + atomicAdd(&slot.sumCreatureEnergy, energy); + } + __inline__ __device__ void compactLineageSlot(int index) + { + auto const& slot = _lineageMap[index]; + if (slot.lineageId == LineageIdEmpty || slot.numCreatures == 0) { + return; + } + auto entryIndex = atomicAdd(&_lineageAccumulatorMapControl->numCompactEntries, 1u); + auto& entry = _lineageStatisticsEntries[entryIndex]; + entry.lineageId = slot.lineageId; + entry.colorBitset = slot.colorBitset; + entry.numCreatures = slot.numCreatures; + entry.numGenomes = slot.numGenomes; + entry.sumCreatureCells = slot.sumCreatureCells; + entry.sumCreatureGenerations = slot.sumCreatureGenerations; + entry.sumGenomeNodes = slot.sumGenomeNodes; + entry.sumMutationRates = slot.sumMutationRates; + entry.sumCreatureEnergy = slot.sumCreatureEnergy; + entry.representativeCellId = slot.representativeCellId; + entry.numCreatedCreatures = 0; + entry.totalMutations = 0; + auto accumulatorIndex = findAccumulatorSlot(slot.lineageId); + if (accumulatorIndex >= 0) { + auto const& accumulatorSlot = getActiveAccumulatorMap()[accumulatorIndex]; + entry.numCreatedCreatures = accumulatorSlot.numCreatedCreatures; + entry.totalMutations = accumulatorSlot.totalMutations; + } } + __inline__ __device__ void finalizeLineageStatistics() { _overallStatisticsEntry->numActiveLineages = _lineageAccumulatorMapControl->numCompactEntries; } - __inline__ __device__ void incNumCreatedCells(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numCreatedCells[color], uint64_t(1)); } - __inline__ __device__ void incNumCreatedReplicators(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numCreatedReplicators[color], uint64_t(1)); } - __inline__ __device__ void incNumAttacks(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numAttacks[color], uint64_t(1)); } - __inline__ __device__ void incNumMuscleActivities(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numMuscleActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumDefenderActivities(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numDefenderActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumDepotActivities(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numDepotActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumInjectionActivities(int color) + //lineage accumulator map (persistent, garbage-collected occasionally) + __inline__ __device__ void resetInactiveAccumulatorSlot(int index) { - alienAtomicAdd64(&_data->timeline.accumulated.numInjectionActivities[color], uint64_t(1)); + auto& slot = _lineageAccumulatorMaps[1 - _lineageAccumulatorMapControl->activeAccumulatorBuffer][index]; + slot.lineageId = LineageIdEmpty; + slot.numCreatedCreatures = 0; + slot.totalMutations = 0; + if (index == 0) { + _lineageAccumulatorMapControl->numUsedAccumulatorSlots[1 - _lineageAccumulatorMapControl->activeAccumulatorBuffer] = 0; + } } - __inline__ __device__ void incNumCompletedInjections(int color) + __inline__ __device__ void migrateActiveAccumulatorSlot(int index) { - alienAtomicAdd64(&_data->timeline.accumulated.numCompletedInjections[color], uint64_t(1)); + auto activeBuffer = _lineageAccumulatorMapControl->activeAccumulatorBuffer; + auto const& slot = _lineageAccumulatorMaps[activeBuffer][index]; + if (slot.lineageId == LineageIdEmpty) { + return; + } + if (findLineageSlot(slot.lineageId) < 0) { + return; + } + auto targetIndex = insertAccumulatorSlot(1 - activeBuffer, slot.lineageId); + if (targetIndex >= 0) { + auto& targetSlot = _lineageAccumulatorMaps[1 - activeBuffer][targetIndex]; + targetSlot.numCreatedCreatures = slot.numCreatedCreatures; + targetSlot.totalMutations = slot.totalMutations; + } } - __inline__ __device__ void incNumGeneratorPulses(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numGeneratorPulses[color], uint64_t(1)); } - __inline__ __device__ void incNumNeuronActivities(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numNeuronActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumSensorActivities(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numSensorActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumSensorMatches(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numSensorMatches[color], uint64_t(1)); } - __inline__ __device__ void incNumReconnectorCreated(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numReconnectorCreated[color], uint64_t(1)); } - __inline__ __device__ void incNumReconnectorRemoved(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numReconnectorRemoved[color], uint64_t(1)); } - __inline__ __device__ void incNumDetonations(int color) { alienAtomicAdd64(&_data->timeline.accumulated.numDetonations[color], uint64_t(1)); } + __inline__ __device__ void flipAccumulatorBuffers() { _lineageAccumulatorMapControl->activeAccumulatorBuffer = 1 - _lineageAccumulatorMapControl->activeAccumulatorBuffer; } - //histogram - __inline__ __device__ void resetHistogramData() + __inline__ __device__ void incCreatedCreature(uint32_t lineageId) { - _data->histogram.maxAge = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - for (int j = 0; j < MAX_HISTOGRAM_SLOTS; ++j) { - _data->histogram.numCellsByColorBySlot[i][j] = 0; - } + atomicAdd(&_overallStatisticsEntry->numCreatedCreatures, 1ull); + auto slotIndex = findOrInsertAccumulatorSlot(lineageId); + if (slotIndex >= 0) { + atomicAdd(&getActiveAccumulatorMap()[slotIndex].numCreatedCreatures, 1ull); + } + } + __inline__ __device__ void addMutations(uint32_t lineageId, float value) + { + atomicAdd(&_overallStatisticsEntry->totalMutations, value); + auto slotIndex = findOrInsertAccumulatorSlot(lineageId); + if (slotIndex >= 0) { + atomicAdd(&getActiveAccumulatorMap()[slotIndex].totalMutations, value); } } - __inline__ __device__ void incNumCells(int color, int slot) { atomicAdd(&(_data->histogram.numCellsByColorBySlot[color][slot]), 1); } - __inline__ __device__ void maxAge(int value) { atomicMax(&_data->histogram.maxAge, value); } - __inline__ __device__ int getMaxAge() const { return _data->histogram.maxAge; } private: - StatisticsRawData* _data; + static auto constexpr LineageIdEmpty = 0xffffffffu; - //for diversity calculation - static auto constexpr MutantToColorCountMapSize = 1 << 20; - struct MutantStatistics + struct LineageMapEntry { - uint32_t color; - uint32_t count; - float numObjects; + uint32_t lineageId; // LineageIdEmpty = slot is unused + uint32_t colorBitset; + uint32_t numCreatures; + uint32_t numGenomes; + float sumCreatureCells; + float sumCreatureGenerations; + float sumGenomeNodes; + float sumMutationRates; + float sumCreatureEnergy; + uint32_t maxCreatureGeneration; + uint64_t representativeCellId; // Cell of a creature with the highest generation; 0 = not set }; - MutantStatistics* _mutantToMutantStatisticsMap; + struct LineageAccumulatorMapEntry + { + uint32_t lineageId; // LineageIdEmpty = slot is unused + unsigned long long numCreatedCreatures; + double totalMutations; + }; + struct LineageAccumulatorMapControl + { + uint32_t numCompactEntries; + uint32_t activeAccumulatorBuffer; + uint32_t numUsedAccumulatorSlots[2]; + }; + + __host__ LineageAccumulatorMapControl getLineageMapControl() const + { + LineageAccumulatorMapControl result; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _lineageAccumulatorMapControl, sizeof(LineageAccumulatorMapControl), cudaMemcpyDeviceToHost)); + return result; + } + + __inline__ __device__ LineageAccumulatorMapEntry* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageAccumulatorMapControl->activeAccumulatorBuffer]; } + + __inline__ __device__ int insertAccumulatorSlot(uint32_t bufferIndex, uint32_t lineageId) + { + auto map = _lineageAccumulatorMaps[bufferIndex]; + auto mask = _lineageArrayCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageArrayCapacity; ++i) { + auto origLineageId = atomicCAS(&map[index].lineageId, LineageIdEmpty, lineageId); + if (origLineageId == LineageIdEmpty) { + atomicAdd(&_lineageAccumulatorMapControl->numUsedAccumulatorSlots[bufferIndex], 1u); + return index; + } + if (origLineageId == lineageId) { + return index; + } + index = (index + 1) & mask; + } + return -1; + } + __inline__ __device__ int findOrInsertAccumulatorSlot(uint32_t lineageId) + { + return insertAccumulatorSlot(_lineageAccumulatorMapControl->activeAccumulatorBuffer, lineageId); + } + __inline__ __device__ int findAccumulatorSlot(uint32_t lineageId) + { + auto map = getActiveAccumulatorMap(); + auto mask = _lineageArrayCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageArrayCapacity; ++i) { + auto slotLineageId = map[index].lineageId; + if (slotLineageId == lineageId) { + return index; + } + if (slotLineageId == LineageIdEmpty) { + return -1; + } + index = (index + 1) & mask; + } + return -1; + } + + int _lineageArrayCapacity; // Used for all lineage maps and arrays + + OverallStatisticsEntry* _overallStatisticsEntry; + LineageStatisticsEntry* _lineageStatisticsEntries; + + // Lineage map for timestep values + LineageMapEntry* _lineageMap; + + // Lineage map for accumulated values (with history => needs migration) + LineageAccumulatorMapControl* _lineageAccumulatorMapControl; + LineageAccumulatorMapEntry* _lineageAccumulatorMaps[2]; }; diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 1a420fc7ff..f890f50f9a 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -1,111 +1,191 @@ #include "StatisticsKernels.cuh" -__global__ void cudaUpdateTimestepStatistics_substep1(SimulationData data, SimulationStatistics statistics) +namespace { - statistics.resetTimestepData(); + __device__ float calcMeanMutationRate(MutationRates const& rates) + { + auto sum = 0.0f; + for (int i = 0; i < 2; ++i) { + sum += rates.neuronMutations[i].nodeProbability; + sum += rates.connectionMutations[i].nodeProbability; + sum += rates.cellTypePropertiesMutations[i].nodeProbability; + sum += rates.geometryMutations[i].geneProbability; + sum += rates.constructorMutations[i].nodeProbability; + } + sum += rates.cellTypeModeMutation.nodeProbability; + sum += rates.cellTypeMutation.nodeProbability; + sum += rates.voidMutation.nodeProbability; + sum += rates.extendGeneMutation.geneProbability; + sum += rates.addNodeMutation.nodeProbability; + sum += rates.trimGeneMutation.geneProbability; + sum += rates.deleteNodeMutation.nodeProbability; + sum += rates.copyNodeSectionMutation.geneProbability; + sum += rates.moveNodeSectionMutation.geneProbability; + sum += rates.duplicateGeneMutation.geneProbability; + sum += rates.deleteGeneMutation.geneProbability; + return sum / 21.0f; + } + + __device__ int getCachedLineageSlot(Creature* creature, int lineageMapCapacity) + { + auto slotIndexPlusOne = creature->creatureIndex; + if (slotIndexPlusOne >= 1 && slotIndexPlusOne <= static_cast(lineageMapCapacity)) { + return toInt(slotIndexPlusOne) - 1; + } + return -1; + } } -__global__ void cudaUpdateTimestepStatistics_substep2(SimulationData data, SimulationStatistics statistics) +__global__ void cudaUpdateEvolutionStatistics_substep1(SimulationData data, SimulationStatistics statistics) { + if (threadIdx.x == 0 && blockIdx.x == 0) { + statistics.resetEvolutionStatistics(); + statistics.resetCompactLineageCounter(); + } + { + auto const partition = calcSystemThreadPartition(statistics.getLineageMapCapacity()); + for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { + statistics.resetLineageMapSlot(index); + } + } { auto& objects = data.entities.objects; auto const partition = calcSystemThreadPartition(objects.getNumEntries()); - for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { auto& object = objects.at(index); - statistics.incNumObjects(object->color); - statistics.addEnergy(object->color, object->getEnergy()); - if (object->type == ObjectType_FreeCell) { - statistics.incNumFreeCells(object->color); + if (object->type != ObjectType_Cell) { + continue; } - //if (object->typeData.cell.cellType == CellType_Constructor && GenomeDecoder::containsSelfReplication(object->typeData.cell.cellTypeData.constructor)) { - // statistics.incNumReplicator(object->color); - // statistics.incMutant(object->color, object->lineageId, object->numObjects); - // auto numNodes = GenomeDecoder::getNumNodesRecursively(object->typeData.cell.cellTypeData.constructor.genome, object->typeData.cell.cellTypeData.constructor.genomeSize, true, true); - // statistics.addNumGenomeNodes(object->color, numNodes); - // statistics.addNumCells(object->color, object->numObjects); - //} - //if (object->typeData.cell.cellType == CellType_Injector && GenomeDecoder::containsSelfReplication(object->typeData.cell.cellTypeData.injector)) { - // statistics.incNumViruses(object->color); - //} + auto creature = object->typeData.cell.creature; + if (!creature) { + continue; + } + creature->creatureIndex = VALUE_NOT_SET_UINT64; + creature->genome->genomeIndex = VALUE_NOT_SET_UINT64; } } +} + +__global__ void cudaUpdateEvolutionStatistics_substep2(SimulationData data, SimulationStatistics statistics) +{ { auto& particles = data.entities.energies; auto const partition = calcSystemThreadPartition(particles.getNumEntries()); - for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { - auto& particle = particles.at(index); - statistics.incNumParticles(particle->color); - statistics.addEnergy(particle->color, particle->energy); + if (particles.at(index)) { + statistics.incNumEnergyParticles(); + } } } -} - -__global__ void cudaUpdateTimestepStatistics_substep3(SimulationData data, SimulationStatistics statistics) -{ - if (threadIdx.x == 0 && blockIdx.x == 0) { - statistics.halveNumConnections(); - } - - { - //auto& objects = data.entities.objects; - //auto const partition = calcAllThreadsPartition(cells.getNumEntries()); - - //auto numReplicators = toDouble(statistics.getNumReplicators()); - //auto averageNumCells = statistics.getSummedNumCells() / numReplicators; - - //for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { - // auto& object = cells.at(index); - //if (object->typeData.cell.cellType == CellType_Constructor && GenomeDecoder::containsSelfReplication(object->typeData.cell.cellTypeData.constructor)) { - // auto variance = toDouble(object->numObjects) - averageNumCells; - // variance = variance * variance / numReplicators; - // statistics.addToNumCellsVariance(object->color, variance); - //} - //} - } -} - -__global__ void cudaUpdateHistogramData_substep1(SimulationData data, SimulationStatistics statistics) -{ - statistics.resetHistogramData(); -} - -__global__ void cudaUpdateHistogramData_substep2(SimulationData data, SimulationStatistics statistics) -{ auto& objects = data.entities.objects; auto const partition = calcSystemThreadPartition(objects.getNumEntries()); for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { auto& object = objects.at(index); - if (object->isStatic()) { + if (object->type == ObjectType_Solid) { + statistics.incNumSolidObjects(); + } else if (object->type == ObjectType_Fluid) { + statistics.incNumFluidObjects(); + } else if (object->type == ObjectType_Cell) { + statistics.incNumCellObjects(); + } + if (object->type != ObjectType_Cell) { continue; } - if (object->type == ObjectType_Cell) { - statistics.maxAge(object->typeData.cell.age); - } else if (object->type == ObjectType_FreeCell) { - statistics.maxAge(object->typeData.freeCell.age); + auto creature = object->typeData.cell.creature; + if (!creature) { + continue; + } + auto origCreatureIndex = alienAtomicExch64(&creature->creatureIndex, static_cast(0)); // 0 = member is currently initialized + if (origCreatureIndex == VALUE_NOT_SET_UINT64) { + statistics.addCreatureStatistics(creature->numCells, creature->generation, creature->accumulatedMutations); + auto slotIndex = statistics.insertOrFindLineageSlot(creature->lineageId); + if (slotIndex >= 0) { + statistics.addLineageCreatureData(slotIndex, creature->numCells, creature->generation); + alienAtomicExch64(&creature->creatureIndex, static_cast(slotIndex) + 1); + } + } else if (origCreatureIndex != 0) { + // Another thread already finished the initialization; restore its value (see DataAccessKernels) + alienAtomicExch64(&creature->creatureIndex, origCreatureIndex); } } } -__global__ void cudaUpdateHistogramData_substep3(SimulationData data, SimulationStatistics statistics) +__global__ void cudaUpdateEvolutionStatistics_substep3(SimulationData data, SimulationStatistics statistics) { auto& objects = data.entities.objects; auto const partition = calcSystemThreadPartition(objects.getNumEntries()); - auto maxAge = statistics.getMaxAge(); for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { auto& object = objects.at(index); - if (object->isStatic()) { + if (object->type != ObjectType_Cell) { continue; } - if (object->type == ObjectType_Cell) { - auto slot = object->typeData.cell.age * MAX_HISTOGRAM_SLOTS / (maxAge + 1); - statistics.incNumCells(object->color, slot); - } else if (object->type == ObjectType_FreeCell) { - auto slot = object->typeData.freeCell.age * MAX_HISTOGRAM_SLOTS / (maxAge + 1); - statistics.incNumCells(object->color, slot); + auto creature = object->typeData.cell.creature; + if (!creature) { + continue; } + auto slotIndex = getCachedLineageSlot(creature, statistics.getLineageMapCapacity()); + + auto genome = creature->genome; + auto origGenomeIndex = alienAtomicExch64(&genome->genomeIndex, static_cast(0)); + if (origGenomeIndex == VALUE_NOT_SET_UINT64) { + auto numNodes = 0; + auto nodeColorBitset = 0u; + for (int i = 0; i < genome->numGenes; ++i) { + auto const& gene = genome->genes[i]; + numNodes += gene.numNodes; + for (int j = 0; j < gene.numNodes; ++j) { + nodeColorBitset |= 1u << gene.nodes[j].color; + } + } + auto meanMutationRate = calcMeanMutationRate(genome->mutationRates); + statistics.addGenomeStatistics(toFloat(numNodes), meanMutationRate); + if (slotIndex >= 0) { + statistics.addLineageGenomeData(slotIndex, toFloat(numNodes), meanMutationRate, nodeColorBitset); + } + } + + auto energy = object->getEnergy(); + statistics.addCreatureEnergy(energy); + if (slotIndex >= 0) { + statistics.addLineageEnergy(slotIndex, energy); + statistics.updateLineageRepresentativeCell(slotIndex, creature->generation, object->id); + } + } +} + +__global__ void cudaUpdateEvolutionStatistics_substep4(SimulationData data, SimulationStatistics statistics) +{ + auto const partition = calcSystemThreadPartition(statistics.getLineageMapCapacity()); + for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { + statistics.compactLineageSlot(index); } } + +__global__ void cudaUpdateEvolutionStatistics_substep5(SimulationData data, SimulationStatistics statistics) +{ + statistics.finalizeLineageStatistics(); +} + +__global__ void cudaPrepareLineageAccumulatorGC(SimulationStatistics statistics) +{ + auto const partition = calcSystemThreadPartition(statistics.getLineageMapCapacity()); + for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { + statistics.resetInactiveAccumulatorSlot(index); + } +} + +__global__ void cudaLineageAccumulatorGC(SimulationStatistics statistics) +{ + auto const partition = calcSystemThreadPartition(statistics.getLineageMapCapacity()); + for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { + statistics.migrateActiveAccumulatorSlot(index); + } +} + +__global__ void cudaFinishLineageAccumulatorGC(SimulationStatistics statistics) +{ + statistics.flipAccumulatorBuffers(); +} + diff --git a/source/EngineKernels/StatisticsKernels.cuh b/source/EngineKernels/StatisticsKernels.cuh index b96a57dfcb..bd617728ea 100644 --- a/source/EngineKernels/StatisticsKernels.cuh +++ b/source/EngineKernels/StatisticsKernels.cuh @@ -6,10 +6,13 @@ #include "SimulationData.cuh" #include "SimulationStatistics.cuh" -__global__ void cudaUpdateTimestepStatistics_substep1(SimulationData data, SimulationStatistics statistics); -__global__ void cudaUpdateTimestepStatistics_substep2(SimulationData data, SimulationStatistics statistics); -__global__ void cudaUpdateTimestepStatistics_substep3(SimulationData data, SimulationStatistics statistics); +__global__ void cudaUpdateEvolutionStatistics_substep1(SimulationData data, SimulationStatistics statistics); +__global__ void cudaUpdateEvolutionStatistics_substep2(SimulationData data, SimulationStatistics statistics); +__global__ void cudaUpdateEvolutionStatistics_substep3(SimulationData data, SimulationStatistics statistics); +__global__ void cudaUpdateEvolutionStatistics_substep4(SimulationData data, SimulationStatistics statistics); +__global__ void cudaUpdateEvolutionStatistics_substep5(SimulationData data, SimulationStatistics statistics); + +__global__ void cudaPrepareLineageAccumulatorGC(SimulationStatistics statistics); +__global__ void cudaLineageAccumulatorGC(SimulationStatistics statistics); +__global__ void cudaFinishLineageAccumulatorGC(SimulationStatistics statistics); -__global__ void cudaUpdateHistogramData_substep1(SimulationData data, SimulationStatistics statistics); -__global__ void cudaUpdateHistogramData_substep2(SimulationData data, SimulationStatistics statistics); -__global__ void cudaUpdateHistogramData_substep3(SimulationData data, SimulationStatistics statistics); diff --git a/source/EngineKernels/TOs.cuh b/source/EngineKernels/TOs.cuh index 44720eaeb0..bbe6e309cc 100644 --- a/source/EngineKernels/TOs.cuh +++ b/source/EngineKernels/TOs.cuh @@ -497,6 +497,7 @@ struct CreatureTO uint32_t lineageId; float accumulatedMutations; + float accumulatedMutationsInLineage; // Process data uint32_t headUpdateId; diff --git a/source/EngineKernels/TestKernels.cu b/source/EngineKernels/TestKernels.cu index ce9d5cdf0a..3f91b1da61 100644 --- a/source/EngineKernels/TestKernels.cu +++ b/source/EngineKernels/TestKernels.cu @@ -4,7 +4,7 @@ #include "ObjectConnectionProcessor.cuh" #include "TestKernels.cuh" -__global__ void cudaTestMutate(SimulationData data, uint64_t objectId) +__global__ void cudaTestMutate(SimulationData data, SimulationStatistics statistics, uint64_t objectId) { DEVICE_CHECK(blockDim.x == NEURONS_PER_CELL); @@ -25,7 +25,7 @@ __global__ void cudaTestMutate(SimulationData data, uint64_t objectId) block.sync(); if (shouldMutate) { - MutationProcessor::applyMutations(data, object->typeData.cell.creature, object->typeData.cell.creature->genome); + MutationProcessor::applyMutations(data, statistics, object->typeData.cell.creature, object->typeData.cell.creature->genome); } block.sync(); } diff --git a/source/EngineKernels/TestKernels.cuh b/source/EngineKernels/TestKernels.cuh index 12666adb64..4ed72b86c9 100644 --- a/source/EngineKernels/TestKernels.cuh +++ b/source/EngineKernels/TestKernels.cuh @@ -4,8 +4,9 @@ #include "sm_60_atomic_functions.h" #include "SimulationData.cuh" +#include "SimulationStatistics.cuh" -__global__ void cudaTestMutate(SimulationData data, uint64_t objectId); +__global__ void cudaTestMutate(SimulationData data, SimulationStatistics statistics, uint64_t objectId); __global__ void cudaTestRemoveUnreachableGenesFromRoot(SimulationData data, uint64_t objectId); __global__ void cudaTestCreateConnection(SimulationData data, uint64_t objectId1, uint64_t objectId2); __global__ void cudaTestCreateConnectionWithAbsAngle( diff --git a/source/EngineTestData/DescTestDataFactory.cpp b/source/EngineTestData/DescTestDataFactory.cpp index e4210ae7e4..b5046d911b 100644 --- a/source/EngineTestData/DescTestDataFactory.cpp +++ b/source/EngineTestData/DescTestDataFactory.cpp @@ -226,6 +226,7 @@ std::pair DescTestDataFactory::createNonDefaultCreatur .mutationState(MutationState_MutationInProgress) .lineageId(502) .accumulatedMutations(0.05f) + .accumulatedMutationsInLineage(0.06f) .genomeId(genome._id); return {creature, genome}; diff --git a/source/EngineTests/AccumulatedMutationTests.cpp b/source/EngineTests/AccumulatedMutationTests.cpp index a76c2b901d..f5044312bd 100644 --- a/source/EngineTests/AccumulatedMutationTests.cpp +++ b/source/EngineTests/AccumulatedMutationTests.cpp @@ -107,6 +107,7 @@ TEST_P(AccumulatedMutationTests_AllTypes, accumulatedMutations_increases) auto actualCreature = getMutatedCreature(); EXPECT_GT(actualCreature._accumulatedMutations, 0.0f); + EXPECT_GT(actualCreature._accumulatedMutationsInLineage, 0.0f); } TEST_F(AccumulatedMutationTests, accumulatedMutations_metaMutationDoesNotAccount) @@ -134,6 +135,7 @@ TEST_F(AccumulatedMutationTests, accumulatedMutations_metaMutationDoesNotAccount auto actualCreature = getMutatedCreature(); EXPECT_EQ(actualCreature._accumulatedMutations, 0.0f); + EXPECT_EQ(actualCreature._accumulatedMutationsInLineage, 0.0f); EXPECT_EQ(actualCreature._lineageId, 42); } @@ -141,7 +143,8 @@ TEST_F(AccumulatedMutationTests, accumulatedMutations_createsNewLineageId) { auto genome = createTestGenome(); - auto data = Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42).accumulatedMutations(11.0f), genome); + auto data = + Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42).accumulatedMutations(12.0f).accumulatedMutationsInLineage(12.0f), genome); _parameters.newLineageThreshold.value = 0.1f; _simulationFacade->setSimulationParameters(_parameters); @@ -151,4 +154,6 @@ TEST_F(AccumulatedMutationTests, accumulatedMutations_createsNewLineageId) auto actualCreature = getMutatedCreature(); EXPECT_GT(actualCreature._lineageId, 42); + EXPECT_EQ(actualCreature._accumulatedMutationsInLineage, 0.0f); + EXPECT_GT(actualCreature._accumulatedMutations, 11.0f); } diff --git a/source/EngineTests/CMakeLists.txt b/source/EngineTests/CMakeLists.txt index 9baf90fc2d..66884d6775 100644 --- a/source/EngineTests/CMakeLists.txt +++ b/source/EngineTests/CMakeLists.txt @@ -25,6 +25,7 @@ PUBLIC DetonatorTests.cpp DuplicateGeneMutationTests.cpp EditTests.cpp + EvolutionStatisticsTests.cpp DigestorTests.cpp EnergyFlowTests.cpp EnergyParticleTests.cpp diff --git a/source/EngineTests/ConstructorMutationTests.cpp b/source/EngineTests/ConstructorMutationTests.cpp index 9b7ceaaf77..24b159b28f 100644 --- a/source/EngineTests/ConstructorMutationTests.cpp +++ b/source/EngineTests/ConstructorMutationTests.cpp @@ -93,7 +93,7 @@ TEST_F(ConstructorMutationTests, mutatesCreatureWhileConstructingOffspring) _parameters.externalEnergyControlToggle.value = true; _parameters.externalEnergy.value = 1000.0f; - _parameters.newLineageThreshold.value = 100.0f; // keep accumulatedMutations from resetting + _parameters.newLineageThreshold.value = 100.0f; // keep accumulatedMutationsInLineage from resetting _simulationFacade->setSimulationParameters(_parameters); _simulationFacade->setSimulationData(data); diff --git a/source/EngineTests/CreatureTests.cpp b/source/EngineTests/CreatureTests.cpp index 09865cbd5e..bfc798cd50 100644 --- a/source/EngineTests/CreatureTests.cpp +++ b/source/EngineTests/CreatureTests.cpp @@ -261,17 +261,23 @@ TEST_P(CreatureTests_BendingMuscles, constructCreatureWithOneLegAndSpikes) // Check front angles if (muscleMode != MuscleMode_AngleBending) { for (int i = 0; i < 5; ++i) { - EXPECT_TRUE(approxCompareAngles(0.0f, body.at(i).getCellRef()._frontAngle.value())); + EXPECT_TRUE(approxCompareAngles(0.0f, body.at(i).getCellRef()._frontAngle.value())) + << "body " << i << ": " << body.at(i).getCellRef()._frontAngle.value(); } - EXPECT_TRUE(approxCompareAngles(-180.0f, body.at(5).getCellRef()._frontAngle.value())); + EXPECT_TRUE(approxCompareAngles(-180.0f, body.at(5).getCellRef()._frontAngle.value())) << "body 5: " << body.at(5).getCellRef()._frontAngle.value(); for (int i = 0; i < 4; ++i) { - EXPECT_TRUE(approxCompareAngles(90.0f, leg.at(i).getCellRef()._frontAngle.value())); + EXPECT_TRUE(approxCompareAngles(90.0f, leg.at(i).getCellRef()._frontAngle.value())) + << "leg " << i << ": " << leg.at(i).getCellRef()._frontAngle.value(); } - EXPECT_TRUE(approxCompareAngles(0.0f, spikes1.at(0).getCellRef()._frontAngle.value())); - EXPECT_TRUE(approxCompareAngles(0.0f, spikes1.at(1).getCellRef()._frontAngle.value())); - EXPECT_TRUE(approxCompareAngles(180.0f, spikes2.at(0).getCellRef()._frontAngle.value())); - EXPECT_TRUE(approxCompareAngles(180.0f, spikes2.at(1).getCellRef()._frontAngle.value())); + EXPECT_TRUE(approxCompareAngles(0.0f, spikes1.at(0).getCellRef()._frontAngle.value())) + << "spikes1 0: " << spikes1.at(0).getCellRef()._frontAngle.value(); + EXPECT_TRUE(approxCompareAngles(0.0f, spikes1.at(1).getCellRef()._frontAngle.value())) + << "spikes1 1: " << spikes1.at(1).getCellRef()._frontAngle.value(); + EXPECT_TRUE(approxCompareAngles(180.0f, spikes2.at(0).getCellRef()._frontAngle.value())) + << "spikes2 0: " << spikes2.at(0).getCellRef()._frontAngle.value(); + EXPECT_TRUE(approxCompareAngles(180.0f, spikes2.at(1).getCellRef()._frontAngle.value())) + << "spikes2 1: " << spikes2.at(1).getCellRef()._frontAngle.value(); } // Check angles without muscle distortions @@ -290,21 +296,26 @@ TEST_P(CreatureTests_BendingMuscles, constructCreatureWithOneLegAndSpikes) // Check angles for first cell leg ASSERT_EQ(1, leg.at(0)._connections.size()); - // Check angles for second cell leg + // Check angles for second cell leg (the initial angle of a connection is stored in the connected muscle) ASSERT_EQ(4, leg.at(1)._connections.size()); - EXPECT_TRUE(approxCompareAngles(90.0f, getInitialAngle(leg.at(0)))); // initial angle connection is stored in connected muscle - EXPECT_TRUE(approxCompareAngles(90.0, leg.at(1)._connections.at(0)._angleFromPrevious)); - EXPECT_TRUE(approxCompareAngles(90.0, leg.at(1)._connections.at(1)._angleFromPrevious)); + EXPECT_TRUE(approxCompareAngles(90.0f, getInitialAngle(leg.at(0)))) << "initialAngle leg 0: " << getInitialAngle(leg.at(0)); + EXPECT_TRUE(approxCompareAngles(90.0, leg.at(1)._connections.at(0)._angleFromPrevious)) + << "leg 1 conn 0: " << leg.at(1)._connections.at(0)._angleFromPrevious; + EXPECT_TRUE(approxCompareAngles(90.0, leg.at(1)._connections.at(1)._angleFromPrevious)) + << "leg 1 conn 1: " << leg.at(1)._connections.at(1)._angleFromPrevious; // Check angles for third cell leg ASSERT_EQ(2, leg.at(2)._connections.size()); - EXPECT_TRUE(approxCompareAngles(180.0, leg.at(2)._connections.at(0)._angleFromPrevious)); + EXPECT_TRUE(approxCompareAngles(180.0, leg.at(2)._connections.at(0)._angleFromPrevious)) + << "leg 2 conn 0: " << leg.at(2)._connections.at(0)._angleFromPrevious; - // Check angles for forth cell leg + // Check angles for forth cell leg (the initial angle of a connection is stored in the connected muscle) ASSERT_EQ(4, leg.at(3)._connections.size()); - EXPECT_TRUE(approxCompareAngles(90.0f, getInitialAngle(leg.at(2)))); // initial angle connection is stored in connected muscle - EXPECT_TRUE(approxCompareAngles(90.0, leg.at(3)._connections.at(0)._angleFromPrevious)); - EXPECT_TRUE(approxCompareAngles(90.0, leg.at(3)._connections.at(1)._angleFromPrevious)); + EXPECT_TRUE(approxCompareAngles(90.0f, getInitialAngle(leg.at(2)))) << "initialAngle leg 2: " << getInitialAngle(leg.at(2)); + EXPECT_TRUE(approxCompareAngles(90.0, leg.at(3)._connections.at(0)._angleFromPrevious)) + << "leg 3 conn 0: " << leg.at(3)._connections.at(0)._angleFromPrevious; + EXPECT_TRUE(approxCompareAngles(90.0, leg.at(3)._connections.at(1)._angleFromPrevious)) + << "leg 3 conn 1: " << leg.at(3)._connections.at(1)._angleFromPrevious; } // Regression test for issue where muscles connected to void were not constructed with the correct angles diff --git a/source/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp new file mode 100644 index 0000000000..a3cfcbd17b --- /dev/null +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -0,0 +1,112 @@ +#include + +#include +#include + +#include "MutationTestsBase.h" + +class EvolutionStatisticsTests : public MutationTestsBase +{}; + +TEST_F(EvolutionStatisticsTests, basicCounts) +{ + auto genome42 = GenomeDesc().genes({GeneDesc().nodes({NodeDesc().color(2), NodeDesc().color(5)})}); + auto data = Desc() + .addCreature({ObjectDesc().id(1), ObjectDesc().id(2).pos({1.0f, 0.0f})}, CreatureDesc().lineageId(42).generation(3), genome42) + .addCreature({ObjectDesc().id(3).pos({10.0f, 10.0f})}, CreatureDesc().lineageId(43).generation(5), GenomeDesc()); + + _simulationFacade->setSimulationData(data); + + auto entries = _simulationFacade->getStatisticsEntry(); + EXPECT_EQ(2u, entries.overallEntry.numCreatures); + EXPECT_EQ(2u, entries.overallEntry.numGenomes); + EXPECT_EQ(2u, entries.overallEntry.numActiveLineages); + EXPECT_EQ(3u, entries.overallEntry.numCellObjects); + EXPECT_EQ(0u, entries.overallEntry.numSolidObjects); + EXPECT_EQ(0u, entries.overallEntry.numFluidObjects); + EXPECT_FLOAT_EQ(3.0f, entries.overallEntry.sumCreatureCells); + EXPECT_FLOAT_EQ(8.0f, entries.overallEntry.sumCreatureGenerations); + EXPECT_GT(entries.overallEntry.sumCreatureEnergy, 0.0f); + + ASSERT_EQ(2, entries.entries.size()); + auto findEntry = [&](uint32_t lineageId) -> LineageStatisticsEntry const* { + for (auto const& entry : entries.entries) { + if (entry.lineageId == lineageId) { + return &entry; + } + } + return nullptr; + }; + auto const* entry42 = findEntry(42); + ASSERT_TRUE(entry42 != nullptr); + EXPECT_EQ(1, entry42->numCreatures); + EXPECT_EQ(1, entry42->numGenomes); + EXPECT_FLOAT_EQ(2.0f, entry42->sumCreatureCells); + EXPECT_FLOAT_EQ(3.0f, entry42->sumCreatureGenerations); + EXPECT_GT(entry42->sumCreatureEnergy, 0.0f); + EXPECT_EQ((1u << 2) | (1u << 5), entry42->colorBitset); //derived from genome node colors, not cell colors + + auto const* entry43 = findEntry(43); + ASSERT_TRUE(entry43 != nullptr); + EXPECT_EQ(1, entry43->numCreatures); + EXPECT_FLOAT_EQ(1.0f, entry43->sumCreatureCells); + EXPECT_FLOAT_EQ(5.0f, entry43->sumCreatureGenerations); +} + +TEST_F(EvolutionStatisticsTests, representativeCell) +{ + auto data = Desc() + .addCreature({ObjectDesc().id(1), ObjectDesc().id(2).pos({1.0f, 0.0f})}, CreatureDesc().lineageId(42).generation(3), GenomeDesc()) + .addCreature({ObjectDesc().id(3).pos({10.0f, 10.0f})}, CreatureDesc().lineageId(42).generation(5), GenomeDesc()); + + _simulationFacade->setSimulationData(data); + + auto entries = _simulationFacade->getStatisticsEntry(); + ASSERT_EQ(1, entries.entries.size()); + EXPECT_EQ(3u, entries.entries.front().representativeCellId); //cell of the creature with the highest generation +} + +TEST_F(EvolutionStatisticsTests, genomeNodesAndMutationRates) +{ + auto genome = createTestGenome(); + auto numNodes = 0; + for (auto const& gene : genome._genes) { + numNodes += toInt(gene._nodes.size()); + } + genome._mutationRates._voidMutation = VoidMutationDesc().nodeProbability(0.21f); + + auto data = Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42), genome); + _simulationFacade->setSimulationData(data); + + auto entries = _simulationFacade->getStatisticsEntry(); + EXPECT_EQ(1u, entries.overallEntry.numGenomes); + EXPECT_FLOAT_EQ(toFloat(numNodes), entries.overallEntry.sumGenomeNodes); + EXPECT_FLOAT_EQ(0.01f, entries.overallEntry.sumMutationRates); //mean over 21 probability values: 0.21 / 21 +} + +TEST_F(EvolutionStatisticsTests, accumulatedMutations) +{ + auto genome = createTestGenome(); + genome._mutationRates._neuronMutations[0] = + NeuronMutationDesc().nodeProbability(1.0f).weightChangeSigma(1.0f).biasChangeSigma(1.0f).actfnChangeProbability(1.0f); + + auto data = Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42), genome); + + auto parameters = _parameters; + parameters.newLineageThreshold.value = 1.0e30f; + _simulationFacade->setSimulationParameters(parameters); + + _simulationFacade->setSimulationData(data); + for (int i = 0; i < 10; ++i) { + _simulationFacade->testOnly_mutate(1); + } + _simulationFacade->calcTimesteps(1); + + auto entries = _simulationFacade->getStatisticsEntry(); + EXPECT_GT(entries.overallEntry.sumAccumulatedMutations, 0.0f); + EXPECT_GT(entries.overallEntry.totalMutations, 0.0f); + + ASSERT_EQ(1, entries.entries.size()); + EXPECT_EQ(42, entries.entries.front().lineageId); + EXPECT_GT(entries.entries.front().totalMutations, 0.0f); +} diff --git a/source/EngineTests/StatisticsTests.cpp b/source/EngineTests/StatisticsTests.cpp index 1478335ff6..9d281b7e8a 100644 --- a/source/EngineTests/StatisticsTests.cpp +++ b/source/EngineTests/StatisticsTests.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "IntegrationTestFramework.h" @@ -36,7 +35,7 @@ TEST_F(StatisticsTests, selfReplicatorWithRepetitionsInGenome) //}); //_simulationFacade->setSimulationData(data); - //auto statistics = _simulationFacade->getStatisticsRawData(); + //auto statistics = _simulationFacade->getTimelineStatistics(); //EXPECT_EQ(1, statistics.timeline.timestep.numObjects[0]); //EXPECT_EQ(1, statistics.timeline.timestep.numSelfReplicators[0]); @@ -61,7 +60,7 @@ TEST_F(StatisticsTests, selfReplicatorWithInfiniteRepetitionsInGenome) //}); //_simulationFacade->setSimulationData(data); - //auto statistics = _simulationFacade->getStatisticsRawData(); + //auto statistics = _simulationFacade->getTimelineStatistics(); //EXPECT_EQ(1, statistics.timeline.timestep.numObjects[0]); //EXPECT_EQ(1, statistics.timeline.timestep.numSelfReplicators[0]); @@ -85,7 +84,7 @@ TEST_F(StatisticsTests, nonSelfReplicatorWithRepetitionsInGenome) //}); //_simulationFacade->setSimulationData(data); - //auto statistics = _simulationFacade->getStatisticsRawData(); + //auto statistics = _simulationFacade->getTimelineStatistics(); //EXPECT_EQ(1, statistics.timeline.timestep.numObjects[0]); //EXPECT_EQ(0, statistics.timeline.timestep.numSelfReplicators[0]); diff --git a/source/Gui/BrowserWindow.cpp b/source/Gui/BrowserWindow.cpp index 77e89f21a9..d75909ab0a 100644 --- a/source/Gui/BrowserWindow.cpp +++ b/source/Gui/BrowserWindow.cpp @@ -41,7 +41,6 @@ #include "NetworkTransferController.h" #include "OpenGLHelper.h" #include "OverlayController.h" -#include "StatisticsWindow.h" #include "StyleRepository.h" #include "UploadSimulationDialog.h" #include "Viewport.h" diff --git a/source/Gui/CMakeLists.txt b/source/Gui/CMakeLists.txt index bfb5def80f..8f9fe09262 100644 --- a/source/Gui/CMakeLists.txt +++ b/source/Gui/CMakeLists.txt @@ -35,6 +35,8 @@ PUBLIC EditorModel.h EditSimulationDialog.cpp EditSimulationDialog.h + EvolutionDashboardWindow.cpp + EvolutionDashboardWindow.h ExitDialog.cpp ExitDialog.h FileTransferController.cpp @@ -63,8 +65,6 @@ PUBLIC GuiLogger.cpp GuiLogger.h HelpStrings.h - HistogramLiveStatistics.cpp - HistogramLiveStatistics.h ImageToPatternDialog.cpp ImageToPatternDialog.h InspectionWindow.cpp @@ -157,12 +157,8 @@ PUBLIC SpecificationGuiService.h StartupCheckService.cpp StartupCheckService.h - StatisticsWindow.cpp - StatisticsWindow.h StyleRepository.cpp StyleRepository.h - TableLiveStatistics.cpp - TableLiveStatistics.h TemporalControlWindow.cpp TemporalControlWindow.h TimelineLiveStatistics.cpp diff --git a/source/Gui/Definitions.h b/source/Gui/Definitions.h index f0e5deac9a..b0ccd7039e 100644 --- a/source/Gui/Definitions.h +++ b/source/Gui/Definitions.h @@ -23,7 +23,7 @@ class SpatialControlWindow; class SimulationParametersMainWindow; -class StatisticsWindow; +class EvolutionDashboardWindow; class SimulationInteractionController; diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp new file mode 100644 index 0000000000..1d7fca419d --- /dev/null +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -0,0 +1,1181 @@ +#include "EvolutionDashboardWindow.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +#include "AlienGui.h" +#include "GenomeEditorWindow.h" +#include "OverlayController.h" +#include "StyleRepository.h" + +namespace +{ + auto constexpr LiveStatisticsDeltaTime = 50; //in millisec + auto constexpr RateAveragingTimesteps = 1000.0; //rate reference sample: the most recent sample at least this many time steps older + + auto constexpr ColorChipSize = 22.0f; + auto constexpr SwatchSize = 11.0f; + auto constexpr SwatchGap = 3.0f; + auto constexpr PlotLabelColumnWidth = 150.0f; + auto constexpr MinPlotHeight = 40.0f; + auto constexpr MaxPlotHeight = 300.0f; + auto constexpr TimeAxisExtraHeight = 20.0f; + auto constexpr DefaultTimelinesHeight = 380.0f; + + ImColor const CardBackgroundColor = ImColor(0.095f, 0.117f, 0.165f, 1.0f); + ImColor const CardBorderColor = ImColor(0.165f, 0.196f, 0.270f, 1.0f); + + struct MetricDef + { + char const* tableHeader; + char const* plotName; + int tableDecimals; + int plotDecimals; + }; + + //the last two metrics are rates per 1K time steps, both in the table and in the timeline plots + MetricDef const Metrics[EvolutionDashboardWindow::NumMetrics] = { + {"Creatures", "Creatures", 0, 0}, + {"Avg cells", "Avg cells", 1, 1}, + {"Avg nodes", "Avg nodes", 1, 1}, + {"Internal energy", "Internal energy", 0, 0}, + {"Avg mut. rate", "Avg mutation rate", 4, 4}, + {"Avg generation", "Avg generation", 0, 0}, + {"Created /1K", "Created /1K", 2, 2}, + {"Mutations /1K", "Mutations /1K", 4, 4}, + }; + + ImColor toImColor(uint32_t rgb, float alpha = 1.0f, float brightness = 1.0f) + { + return ImColor( + toInt(toFloat((rgb >> 16) & 0xff) * brightness), + toInt(toFloat((rgb >> 8) & 0xff) * brightness), + toInt(toFloat(rgb & 0xff) * brightness), + toInt(alpha * 255.0f)); + } + + //palette as used by the former statistics window; one colormap color per plot row, + //or one per selected lineage if multiple lineages are plotted together; + //stepping through the colormap with an alternating stride of 1 and 2 makes adjacent rows distinct + ImColor getPlotColor(int metricIndex, int seriesIndex, int numSeries) + { + auto index = numSeries > 1 ? seriesIndex : metricIndex; + return ImColor(ImPlot::GetColormapColor((index / 2 * 3 + index % 2) % 11, ImPlotColormap_Cool)); + } + + std::string formatMetricValue(double value, int decimals) + { + if (std::isnan(value)) { + return "-"; + } + if (value >= toDouble(Infinity::value)) { //parameters use FLT_MAX to represent infinity + return "infinity"; + } + return StringHelper::format(value, decimals); + } + + std::string convertSystemClockToString(double systemClock) + { + auto time_t = static_cast(systemClock); + std::tm* tm = std::localtime(&time_t); + + char buffer[100]; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm); + return std::string(buffer); + } + + void rightAlignedText(std::string const& text) + { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(text.c_str()).x); + ImGui::TextUnformatted(text.c_str()); + } + + float drawColorSwatches(ImDrawList* drawList, ImVec2 const& pos, int colorBitset, float lineHeight, std::array const& cellColors) + { + auto swatchSize = scale(SwatchSize); + auto gap = scale(SwatchGap); + auto offsetY = (lineHeight - swatchSize) / 2; + auto x = pos.x; + for (int i = 0; i < MAX_COLORS; ++i) { + if (colorBitset & (1 << i)) { + drawList->AddRectFilled({x, pos.y + offsetY}, {x + swatchSize, pos.y + offsetY + swatchSize}, toImColor(cellColors.at(i)), scale(3.0f)); + x += swatchSize + gap; + } + } + return x - pos.x; + } + + double calcRate(double accumValue, double lastAccumValue, double delta) + { + if (delta <= 0) { + return 0.0; + } + return std::max(0.0, (accumValue - lastAccumValue) / delta); //negative deltas occur after accumulated statistics have been reset + } + + OverallDataPoint const& overallDataOf(DataPointCollection const& sample) + { + return sample.overall; + } + + OverallDataPoint const& overallDataOf(OverallSample const& sample) + { + return sample.data; + } + + template + double getOverallMetricValue(Sample const& sample, Sample const* referenceSample, int metricIndex) + { + auto const& data = overallDataOf(sample); + switch (metricIndex) { + case 0: + return data.numCreatures; + case 1: + return data.averageCreatureCells; + case 2: + return data.averageGenomeNodes; + case 3: + return data.creatureEnergy; + case 4: + return data.averageMutationRate; + case 5: + return data.averageGeneration; + case 6: + case 7: { + if (!referenceSample) { + return std::numeric_limits::quiet_NaN(); + } + auto const& referenceData = overallDataOf(*referenceSample); + auto deltaKSteps = (sample.timestep - referenceSample->timestep) / 1000.0; + if (metricIndex == 6) { + return calcRate(data.accumCreatedCreatures, referenceData.accumCreatedCreatures, deltaKSteps); + } else { + return calcRate(data.accumMutations, referenceData.accumMutations, deltaKSteps); + } + } + default: + return 0.0; + } + } + + double getLineageMetricValue(LineageDataPoint const& entry, LineageDataPoint const* lastEntry, double rateDelta, int metricIndex) + { + switch (metricIndex) { + case 0: + return entry.numCreatures; + case 1: + return entry.numCreatures > 0 ? entry.sumCreatureCells / entry.numCreatures : 0.0; + case 2: + return entry.numGenomes > 0 ? entry.sumGenomeNodes / entry.numGenomes : 0.0; + case 3: + return entry.sumCreatureEnergy; + case 4: + return entry.numGenomes > 0 ? entry.sumMutationRates / entry.numGenomes : 0.0; + case 5: + return entry.numCreatures > 0 ? entry.sumCreatureGenerations / entry.numCreatures : 0.0; + case 6: + return lastEntry ? calcRate(entry.numCreatedCreatures, lastEntry->numCreatedCreatures, rateDelta) : std::numeric_limits::quiet_NaN(); + case 7: + return lastEntry ? calcRate(entry.totalMutations, lastEntry->totalMutations, rateDelta) : std::numeric_limits::quiet_NaN(); + default: + return 0.0; + } + } + + double getLineageSampleMetricValue(LineageSample const& sample, LineageSample const* referenceSample, int metricIndex) + { + auto deltaKSteps = referenceSample ? (sample.timestep - referenceSample->timestep) / 1000.0 : 0.0; + return getLineageMetricValue(sample.data, referenceSample ? &referenceSample->data : nullptr, deltaKSteps, metricIndex); + } + + LineageDataPoint const* findLineageEntry(DataPointCollection const& sample, uint32_t lineageId) + { + auto it = sample.lineages.find(lineageId); + return it != sample.lineages.end() ? &it->second : nullptr; + } + + int formatTimestepsInThousands(double value, char* buff, int size, void*) + { + return snprintf(buff, size, "%s", StringHelper::formatInThousands(value).c_str()); + } + + //most recent sample that is at least RateAveragingTimesteps older; falls back to the oldest sample + size_t findRateReferenceIndex(std::vector const& history, double timestep) + { + size_t result = 0; + for (size_t i = 0; i < history.size(); ++i) { + if (history.at(i).timestep + RateAveragingTimesteps > timestep) { + break; + } + result = i; + } + return result; + } + + //builds the x points and metric series of one timeline; reference samples for the rate metrics are selected + //via a trailing window of RateAveragingTimesteps time steps + template + void buildPlotSeries(Target& target, std::vector const& source, size_t firstIndex, bool useTimeAsX, MetricEvaluator const& evaluateMetric) + { + auto getX = [&](Sample const& sample) { return useTimeAsX ? sample.time : sample.timestep; }; + + target.series = {}; + target.timePoints.clear(); + target.systemClockPoints.clear(); + + size_t referenceIndex = 0; //reference samples may also lie before the visible range + for (auto sampleIndex = firstIndex; sampleIndex < source.size(); ++sampleIndex) { + auto const& sample = source.at(sampleIndex); + while (referenceIndex + 1 < sampleIndex && source.at(referenceIndex + 1).timestep + RateAveragingTimesteps <= sample.timestep) { + ++referenceIndex; + } + //rates over references younger than the averaging window would produce spikes at the left plot border; NaN suppresses those samples + auto const& referenceSample = source.at(referenceIndex); + auto referenceIsOldEnough = referenceIndex < sampleIndex && referenceSample.timestep + RateAveragingTimesteps <= sample.timestep; + auto const* reference = referenceIsOldEnough ? &referenceSample : nullptr; + target.timePoints.emplace_back(getX(sample)); + if (!useTimeAsX) { + target.systemClockPoints.emplace_back(sample.systemClock); + } + for (int i = 0; i < EvolutionDashboardWindow::NumMetrics; ++i) { + target.series.at(i).emplace_back(evaluateMetric(sample, reference, i)); + } + } + } +} + +EvolutionDashboardWindow::EvolutionDashboardWindow() + : AlienWindow("Evolution dashboard", "windows.evolution dashboard", false, true, {scale(700.0f), scale(500.0f)}) +{} + +void EvolutionDashboardWindow::initIntern() +{ + _timelinesHeight = GlobalSettings::get().getValue("windows.evolution dashboard.timelines height", scale(DefaultTimelinesHeight)); + _timelineMode = GlobalSettings::get().getValue("windows.evolution dashboard.timeline mode", _timelineMode); + _lastSteps = GlobalSettings::get().getValue("windows.evolution dashboard.last steps", _lastSteps); + _timeHorizon = GlobalSettings::get().getValue("windows.evolution dashboard.time horizon", _timeHorizon); + _plotHeight = GlobalSettings::get().getValue("windows.evolution dashboard.plot height", _plotHeight); + _colorFilter = GlobalSettings::get().getValue("windows.evolution dashboard.color filter", _colorFilter); + _sortColumnIndex = GlobalSettings::get().getValue("windows.evolution dashboard.sort column", _sortColumnIndex); + _sortAscending = GlobalSettings::get().getValue("windows.evolution dashboard.sort ascending", _sortAscending); + validateAndCorrect(); +} + +void EvolutionDashboardWindow::shutdownIntern() +{ + GlobalSettings::get().setValue("windows.evolution dashboard.timelines height", _timelinesHeight); + GlobalSettings::get().setValue("windows.evolution dashboard.timeline mode", _timelineMode); + GlobalSettings::get().setValue("windows.evolution dashboard.last steps", _lastSteps); + GlobalSettings::get().setValue("windows.evolution dashboard.time horizon", _timeHorizon); + GlobalSettings::get().setValue("windows.evolution dashboard.plot height", _plotHeight); + GlobalSettings::get().setValue("windows.evolution dashboard.color filter", _colorFilter); + GlobalSettings::get().setValue("windows.evolution dashboard.sort column", _sortColumnIndex); + GlobalSettings::get().setValue("windows.evolution dashboard.sort ascending", _sortAscending); +} + +void EvolutionDashboardWindow::processBackground() +{ + auto timepoint = std::chrono::steady_clock::now(); + auto duration = + _lastTimepoint.has_value() ? static_cast(std::chrono::duration_cast(timepoint - *_lastTimepoint).count()) : 0; + if (_lastTimepoint && duration <= LiveStatisticsDeltaTime) { + return; + } + _lastTimepoint = timepoint; + + auto sessionId = _SimulationFacade::get()->getSessionId(); + if (_lastSessionId.has_value() && *_lastSessionId != sessionId) { + _timelineLiveStatistics.clear(); + } + _lastSessionId = sessionId; + + auto overallStatistics = _SimulationFacade::get()->getStatisticsEntry(); + _timelineLiveStatistics.update(overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); +} + +void EvolutionDashboardWindow::processIntern() +{ + //scale the plot section proportionally when the window is resized + auto windowHeight = ImGui::GetWindowSize().y; + if (_lastWindowHeight.has_value() && *_lastWindowHeight > 0 && *_lastWindowHeight != windowHeight) { + _timelinesHeight *= windowHeight / *_lastWindowHeight; + validateAndCorrect(); + } + _lastWindowHeight = windowHeight; + + updateCellColors(); + updateDisplayData(); + processHeader(); + processFilterBar(); + + if (ImGui::BeginChild("##lineageTable", {0, -_timelinesHeight})) { + processLineageTable(); + } + ImGui::EndChild(); + + AlienGui::MovableHorizontalSeparator(AlienGui::MovableHorizontalSeparatorParameters().additive(false), _timelinesHeight); + + //apply selection changes from the table immediately; otherwise the plots are empty for one frame + updateDisplayData(); + + if (ImGui::BeginChild("##timelineSection", {0, 0})) { + processTimelineSection(); + } + ImGui::EndChild(); +} + +void EvolutionDashboardWindow::updateCellColors() +{ + auto const& customizationColors = _SimulationFacade::get()->getSimulationParameters().customizationColors.value; + for (int i = 0; i < MAX_COLORS; ++i) { + _cellColors.at(i) = customizationColors.values[i].toRgbColor(); + } +} + +void EvolutionDashboardWindow::updateDisplayData() +{ + updateTableData(); + updatePlotData(); +} + +void EvolutionDashboardWindow::updateTableData() +{ + auto const& liveHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); + auto liveBackTime = !liveHistory.empty() ? liveHistory.back().time : -1.0; + if (_lastTableBackTime.has_value() && *_lastTableBackTime == liveBackTime) { + return; + } + _lastTableBackTime = liveBackTime; + + //table rows from the latest live sample (rates per 1K time steps) + _lineages.clear(); + if (liveHistory.empty()) { + return; + } + auto const& lastSample = liveHistory.back(); + auto referenceIndex = findRateReferenceIndex(liveHistory, lastSample.timestep); + for (auto const& [lineageId, entry] : lastSample.lineages) { + LineageDisplayData lineage; + lineage.id = toInt(lineageId); + lineage.colorBitset = toInt(entry.colorBitset); + lineage.representativeCellId = entry.representativeCellId; + LineageDataPoint const* referenceEntry = nullptr; + auto referenceTimestep = 0.0; + for (auto sampleIndex = referenceIndex; sampleIndex + 1 < liveHistory.size(); ++sampleIndex) { //young lineages: use their oldest sample + if (auto const* candidate = findLineageEntry(liveHistory.at(sampleIndex), lineageId)) { + referenceEntry = candidate; + referenceTimestep = liveHistory.at(sampleIndex).timestep; + break; + } + } + for (int i = 0; i < NumMetrics; ++i) { + lineage.currentValues.at(i) = getLineageMetricValue(entry, referenceEntry, (lastSample.timestep - referenceTimestep) / 1000.0, i); + } + _lineages.emplace_back(std::move(lineage)); + } + sortLineages(); + + //current values for the table summary row + DataPointCollection const* referenceDataPoints = liveHistory.size() >= 2 ? &liveHistory.at(referenceIndex) : nullptr; + for (int i = 0; i < NumMetrics; ++i) { + _allLineages.currentValues.at(i) = getOverallMetricValue(lastSample, referenceDataPoints, i); + } +} + +void EvolutionDashboardWindow::sortLineages() +{ + std::sort(_lineages.begin(), _lineages.end(), [column = _sortColumnIndex, ascending = _sortAscending](auto const& lhs, auto const& rhs) { + if (column == 0) { + return ascending ? lhs.id < rhs.id : lhs.id > rhs.id; + } + auto lhsValue = lhs.currentValues.at(column - 1); + auto rhsValue = rhs.currentValues.at(column - 1); + auto lhsIsNan = std::isnan(lhsValue); + auto rhsIsNan = std::isnan(rhsValue); + if (lhsIsNan || rhsIsNan) { + if (lhsIsNan != rhsIsNan) { + return rhsIsNan; //rows without a value are sorted to the bottom + } + return lhs.id < rhs.id; + } + if (lhsValue != rhsValue) { + return ascending ? lhsValue < rhsValue : lhsValue > rhsValue; + } + return lhs.id < rhs.id; + }); +} + +void EvolutionDashboardWindow::updatePlotData() +{ + //the long-term history carries per-lineage data and is therefore expensive to copy; process it under its mutex instead + if (_timelineMode == TimelineMode_EntireHistory) { + auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); + std::lock_guard lock(statisticsHistory.getMutex()); + rebuildPlotSeries(statisticsHistory.getDataRef()); + } else { + rebuildPlotSeries(_timelineLiveStatistics.getDataPointCollectionHistory()); + } +} + +void EvolutionDashboardWindow::rebuildPlotSeries(std::vector const& source) +{ + //rebuild only when the underlying data or view settings have changed + auto sourceBackTime = !source.empty() ? source.back().time : -1.0; + RebuildKey key{_timelineMode, _lastSteps, _timeHorizon, sourceBackTime, _selectedLineageIds}; + if (_lastRebuildKey && *_lastRebuildKey == key) { + return; + } + _lastRebuildKey = std::move(key); + + auto useTimeAsX = _timelineMode == TimelineMode_RealTime; + auto getX = [&](DataPointCollection const& sample) { return useTimeAsX ? sample.time : sample.timestep; }; + + size_t firstIndex = 0; + if (!source.empty()) { + auto startX = std::numeric_limits::lowest(); + if (_timelineMode == TimelineMode_RealTime) { + startX = source.back().time - toDouble(_timeHorizon); + } else if (_timelineMode == TimelineMode_LastSteps) { + startX = source.back().timestep - toDouble(_lastSteps); + } + while (firstIndex < source.size() && getX(source.at(firstIndex)) < startX) { + ++firstIndex; + } + //keep one sample left of the visible range so the lines extend beyond the left plot border + //(they are clipped there); otherwise a flickering gap remains between border and first sample + if (firstIndex > 0) { + --firstIndex; + } + } + + //"all lineages" series (the current values are maintained by updateTableData) + _allLineages.id = -1; + buildPlotSeries(_allLineages, source, firstIndex, useTimeAsX, getOverallMetricValue); + + //per-lineage series for the selected lineages + _plottedLineages.clear(); + for (auto const& selectedId : _selectedLineageIds) { + LineageDisplayData lineage; + lineage.id = selectedId; + struct RateReference + { + double timestep = 0; + LineageDataPoint const* entry = nullptr; + }; + std::vector rateReferences; //already visited samples containing the lineage; may also lie before the visible range + size_t rateReferenceIndex = 0; + for (size_t sampleIndex = 0; sampleIndex < source.size(); ++sampleIndex) { + auto const& sample = source.at(sampleIndex); + auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); + if (!entry) { + continue; + } + lineage.colorBitset = toInt(entry->colorBitset); + while (rateReferenceIndex + 1 < rateReferences.size() + && rateReferences.at(rateReferenceIndex + 1).timestep + RateAveragingTimesteps <= sample.timestep) { + ++rateReferenceIndex; + } + if (sampleIndex >= firstIndex) { + lineage.timePoints.emplace_back(getX(sample)); + if (!useTimeAsX) { + lineage.systemClockPoints.emplace_back(sample.systemClock); + } + //rates over references younger than the averaging window would produce spikes at the left plot border; NaN suppresses those samples + auto referenceIsOldEnough = + !rateReferences.empty() && rateReferences.at(rateReferenceIndex).timestep + RateAveragingTimesteps <= sample.timestep; + auto const* lastEntry = referenceIsOldEnough ? rateReferences.at(rateReferenceIndex).entry : nullptr; + auto deltaKSteps = referenceIsOldEnough ? (sample.timestep - rateReferences.at(rateReferenceIndex).timestep) / 1000.0 : 0.0; + for (int i = 0; i < NumMetrics; ++i) { + lineage.series.at(i).emplace_back(getLineageMetricValue(*entry, lastEntry, deltaKSteps, i)); + } + } + rateReferences.push_back({sample.timestep, entry}); + } + if (lineage.colorBitset == 0) { + for (auto const& tableLineage : _lineages) { + if (tableLineage.id == selectedId) { + lineage.colorBitset = tableLineage.colorBitset; + } + } + } + _plottedLineages.emplace_back(std::move(lineage)); + } +} + +void EvolutionDashboardWindow::rebuildPlotSeries(StatisticsHistoryData const& source) +{ + //rebuild only when the underlying data or view settings have changed; the lineage timelines are + //sampled independently of the overall timeline and therefore contribute their own back times + auto sourceBackTime = !source.overall.empty() ? source.overall.back().time : -1.0; + std::vector lineageBackTimes; + lineageBackTimes.reserve(_selectedLineageIds.size()); + for (auto const& selectedId : _selectedLineageIds) { + auto timelineIt = source.lineages.find(toUInt32(selectedId)); + lineageBackTimes.emplace_back(timelineIt != source.lineages.end() && !timelineIt->second.empty() ? timelineIt->second.back().time : -1.0); + } + RebuildKey key{_timelineMode, _lastSteps, _timeHorizon, sourceBackTime, _selectedLineageIds, std::move(lineageBackTimes)}; + if (_lastRebuildKey && *_lastRebuildKey == key) { + return; + } + _lastRebuildKey = std::move(key); + + //"all lineages" series (the current values are maintained by updateTableData) + _allLineages.id = -1; + buildPlotSeries(_allLineages, source.overall, 0, false, getOverallMetricValue); + + //per-lineage series for the selected lineages + _plottedLineages.clear(); + for (auto const& selectedId : _selectedLineageIds) { + LineageDisplayData lineage; + lineage.id = selectedId; + if (auto timelineIt = source.lineages.find(toUInt32(selectedId)); timelineIt != source.lineages.end() && !timelineIt->second.empty()) { + lineage.colorBitset = toInt(timelineIt->second.back().data.colorBitset); + buildPlotSeries(lineage, timelineIt->second, 0, false, getLineageSampleMetricValue); + } + if (lineage.colorBitset == 0) { + for (auto const& tableLineage : _lineages) { + if (tableLineage.id == selectedId) { + lineage.colorBitset = tableLineage.colorBitset; + } + } + } + _plottedLineages.emplace_back(std::move(lineage)); + } +} + +void EvolutionDashboardWindow::processHeader() +{ + auto const& style = ImGui::GetStyle(); + auto cardWidth = (ImGui::GetContentRegionAvail().x - 3 * style.ItemSpacing.x) / 6; + auto cardHeight = + style.WindowPadding.y * 2 + ImGui::GetTextLineHeight() * 3 + StyleRepository::get().getLargeFont()->FontSize + style.ItemSpacing.y * 3 + scale(6.0f); + + auto const& liveGlobalHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); + auto const* lastDataPoints = !liveGlobalHistory.empty() ? &liveGlobalHistory.back() : nullptr; + + auto solids = lastDataPoints ? lastDataPoints->overall.numSolidObjects : 0.0; + auto fluids = lastDataPoints ? lastDataPoints->overall.numFluidObjects : 0.0; + auto cells = lastDataPoints ? lastDataPoints->overall.numCellObjects : 0.0; + auto creatureCells = lastDataPoints ? lastDataPoints->overall.numCreatures * lastDataPoints->overall.averageCreatureCells : 0.0; + auto freeCells = std::max(0.0, cells - creatureCells); + auto energyParticles = lastDataPoints ? lastDataPoints->overall.numEnergyParticles : 0.0; + processCard( + "Entities", + formatMetricValue(solids + fluids + cells + energyParticles, 0), + {{"Solids", formatMetricValue(solids, 0)}, + {"Fluids", formatMetricValue(fluids, 0)}, + {"Cells", formatMetricValue(cells, 0)}, + {"Free cells", formatMetricValue(freeCells, 0)}, + {"Energy particles", formatMetricValue(energyParticles, 0)}}, + cardWidth * 2, + cardHeight); + ImGui::SameLine(); + + auto numLineages = lastDataPoints ? lastDataPoints->overall.numLineages : 0.0; + auto numLineagesAbove5Percent = 0; + auto numLineagesAbove1Percent = 0; + if (lastDataPoints) { + for (auto const& [lineageId, entry] : lastDataPoints->lineages) { + if (entry.numCreatures >= lastDataPoints->overall.numCreatures / 20) { + ++numLineagesAbove5Percent; + } + if (entry.numCreatures >= lastDataPoints->overall.numCreatures / 100) { + ++numLineagesAbove1Percent; + } + } + } + processCard( + "Lineages", + formatMetricValue(numLineages, 0), + {{"> 1% creatures", formatMetricValue(toDouble(numLineagesAbove1Percent), 0)}, + {"> 5% creatures", formatMetricValue(toDouble(numLineagesAbove5Percent), 0)}}, + cardWidth, + cardHeight); + ImGui::SameLine(); + + processCard("Creatures", formatMetricValue(_allLineages.currentValues.at(0), 0), {}, cardWidth, cardHeight); + ImGui::SameLine(); + + auto internalEnergy = lastDataPoints ? lastDataPoints->overall.creatureEnergy : 0.0; + auto externalEnergy = toDouble(_SimulationFacade::get()->getSimulationParameters().externalEnergy.value); + processCard( + "Total energy", + formatMetricValue(internalEnergy + externalEnergy, 0), + {{"Internal energy", formatMetricValue(internalEnergy, 0)}, {"External energy", formatMetricValue(externalEnergy, 0)}}, + cardWidth * 2, + cardHeight); +} + +void EvolutionDashboardWindow::processCard( + std::string const& label, + std::string const& value, + std::vector> const& subValues, + float width, + float height) +{ + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, scale(6.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, (ImU32)CardBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_Border, (ImU32)CardBorderColor); + if (ImGui::BeginChild(("##card" + label).c_str(), {width, height}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text(label); + ImGui::PopStyleColor(); + + ImGui::PushFont(StyleRepository::get().getLargeFont()); + AlienGui::Text(value); + ImGui::PopFont(); + + auto firstSubValue = true; + for (auto const& [subLabel, subValue] : subValues) { + if (!firstSubValue) { + ImGui::SameLine(0, scale(18.0f)); + } + firstSubValue = false; + + //draw an ellipsis instead of sub-values that do not fit into the card + auto groupWidth = std::max(ImGui::CalcTextSize(subLabel.c_str()).x, ImGui::CalcTextSize(subValue.c_str()).x); + if (ImGui::GetContentRegionAvail().x < groupWidth) { + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text("..."); + ImGui::PopStyleColor(); + break; + } + + ImGui::BeginGroup(); + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text(subLabel); + ImGui::PopStyleColor(); + AlienGui::Text(subValue); + ImGui::EndGroup(); + } + } + ImGui::EndChild(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + +void EvolutionDashboardWindow::processFilterBar() +{ + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text("Filter by customizations"); + ImGui::PopStyleColor(); + ImGui::SameLine(0, scale(12.0f)); + + auto drawList = ImGui::GetWindowDrawList(); + auto chipSize = scale(ColorChipSize); + for (int i = 0; i < MAX_COLORS; ++i) { + ImGui::PushID(i); + auto pos = ImGui::GetCursorScreenPos(); + if (ImGui::InvisibleButton("##chip", {chipSize, chipSize})) { + _colorFilter ^= 1 << i; + } + auto active = (_colorFilter & (1 << i)) != 0; + auto color = toImColor(_cellColors.at(i), active ? 1.0f : 0.2f, 0.75f); + drawList->AddRectFilled(pos, {pos.x + chipSize, pos.y + chipSize}, color, scale(5.0f)); + if (active) { + drawList->AddRect( + {pos.x - scale(1.5f), pos.y - scale(1.5f)}, + {pos.x + chipSize + scale(1.5f), pos.y + chipSize + scale(1.5f)}, + ImColor(255, 255, 255, 255), + scale(6.0f), + 0, + scale(1.5f)); + } + auto label = std::to_string(i + 1); + auto labelSize = ImGui::CalcTextSize(label.c_str()); + drawList->AddText({pos.x + (chipSize - labelSize.x) / 2, pos.y + (chipSize - labelSize.y) / 2}, ImColor(0, 0, 0, active ? 220 : 120), label.c_str()); + ImGui::PopID(); + ImGui::SameLine(0, scale(5.0f)); + } + ImGui::NewLine(); + ImGui::Spacing(); +} + +void EvolutionDashboardWindow::processLineageTable() +{ + auto flags = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable; + if (ImGui::BeginTable("##lineages", NumMetrics + 1, flags)) { + ImGui::TableSetupScrollFreeze(1, 2); + ImGui::TableSetupColumn("Lineage", ImGuiTableColumnFlags_WidthFixed, scale(150.0f)); + for (auto const& metric : Metrics) { + ImGui::TableSetupColumn(metric.tableHeader, ImGuiTableColumnFlags_WidthFixed, scale(105.0f)); + } + + auto drawList = ImGui::GetWindowDrawList(); + + //header row with labels centered within the visible part of each column; columns can be + //partially hidden behind the frozen lineage column or cut off at the right window border + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int column = 0; column < NumMetrics + 1; ++column) { + ImGui::TableSetColumnIndex(column); + ImGui::PushID(column); + std::string label = ImGui::TableGetColumnName(column); + if (column == _sortColumnIndex) { + label += _sortAscending ? " " ICON_FA_CARET_UP : " " ICON_FA_CARET_DOWN; + } + auto textWidth = ImGui::CalcTextSize(label.c_str()).x; + auto cellPos = ImGui::GetCursorScreenPos(); + auto cellMaxX = cellPos.x + ImGui::GetContentRegionAvail().x; + if (ImGui::Selectable("##header", false)) { + if (column == _sortColumnIndex) { + _sortAscending = !_sortAscending; + } else { + _sortColumnIndex = column; + _sortAscending = column == 0; //metric columns show the largest values on top by default + } + sortLineages(); + } + auto visibleMinX = std::max(cellPos.x, drawList->GetClipRectMin().x); + auto visibleMaxX = std::min(cellMaxX, drawList->GetClipRectMax().x); + auto textPosX = std::clamp((visibleMinX + visibleMaxX - textWidth) / 2, cellPos.x, std::max(cellPos.x, cellMaxX - textWidth)); + drawList->AddText({textPosX, cellPos.y}, ImGui::GetColorU32(ImGuiCol_Text), label.c_str()); + ImGui::PopID(); + } + + //summary row + ImGui::TableNextRow(); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImColor(0.13f, 0.16f, 0.23f, 1.0f)); + ImGui::TableSetColumnIndex(0); + auto allSelected = _selectedLineageIds.empty(); + if (ImGui::Selectable("##rowAll", allSelected, ImGuiSelectableFlags_SpanAllColumns)) { + _selectedLineageIds.clear(); + } + ImGui::SameLine(0, 0); + + //draw the pin icon slightly smaller and shifted so it aligns nicely with the row text + auto iconSize = ImGui::GetFontSize() * 0.75f; + auto iconPos = ImGui::GetCursorScreenPos(); + drawList->AddText( + ImGui::GetFont(), + iconSize, + {iconPos.x + scale(2.0f), iconPos.y + (ImGui::GetTextLineHeight() - iconSize) / 2 + scale(1.0f)}, + ImGui::GetColorU32(ImGuiCol_Text), + ICON_FA_THUMBTACK); + auto iconWidth = ImGui::GetFont()->CalcTextSizeA(iconSize, FLT_MAX, 0.0f, ICON_FA_THUMBTACK).x; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + iconWidth + scale(7.0f)); + AlienGui::Text("All lineages (" + std::to_string(_lineages.size()) + ")"); + for (int i = 0; i < NumMetrics; ++i) { + ImGui::TableSetColumnIndex(i + 1); + rightAlignedText(formatMetricValue(_allLineages.currentValues.at(i), Metrics[i].tableDecimals)); + } + + //lineage rows + auto maxNumColors = 1; + for (auto const& lineage : _lineages) { + if (_colorFilter & lineage.colorBitset) { + maxNumColors = std::max(maxNumColors, std::popcount(static_cast(lineage.colorBitset))); + } + } + auto swatchSlotWidth = toFloat(maxNumColors) * (scale(SwatchSize) + scale(SwatchGap)); + for (auto const& lineage : _lineages) { + if ((_colorFilter & lineage.colorBitset) == 0) { + continue; + } + ImGui::PushID(toInt(lineage.id)); + ImGui::TableNextRow(); + + //genome button on the left of the creatures column, shrunk like the pin icon and bottom-aligned within the row; + //submitted before the row selectable, otherwise the selectable resets the hover timer and the tooltip never shows + ImGui::TableSetColumnIndex(1); + auto lineHeight = ImGui::GetTextLineHeight(); + ImGui::SetWindowFontScale(0.75f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {scale(3.0f), 0.0f}); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + lineHeight - ImGui::GetTextLineHeight()); + auto triggerOpenGenome = + AlienGui::ActionButton(AlienGui::ActionButtonParameters() + .buttonText(ICON_FA_DNA) + .tooltip("Open the genome of a representative creature (highest generation) in the genome editor")); + ImGui::PopStyleVar(); + ImGui::SetWindowFontScale(1.0f); + if (triggerOpenGenome && lineage.representativeCellId != 0) { + onOpenRepresentativeGenome(lineage.representativeCellId); + } + + ImGui::TableSetColumnIndex(0); + drawColorSwatches(drawList, ImGui::GetCursorScreenPos(), lineage.colorBitset, ImGui::GetTextLineHeight(), _cellColors); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchSlotWidth + scale(4.0f)); + AlienGui::Text("Lineage #" + std::to_string(lineage.id)); + + ImGui::SameLine(); + auto selected = _selectedLineageIds.contains(lineage.id); + if (ImGui::Selectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap)) { + if (ImGui::GetIO().KeyCtrl) { + if (selected) { + _selectedLineageIds.erase(lineage.id); + } else { + _selectedLineageIds.insert(lineage.id); + } + } else { + _selectedLineageIds.clear(); + _selectedLineageIds.insert(lineage.id); + } + } + for (int i = 0; i < NumMetrics; ++i) { + ImGui::TableSetColumnIndex(i + 1); + rightAlignedText(formatMetricValue(lineage.currentValues.at(i), Metrics[i].tableDecimals)); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } +} + +void EvolutionDashboardWindow::onOpenRepresentativeGenome(uint64_t cellId) +{ + auto inspectedData = _SimulationFacade::get()->getInspectedSimulationData({cellId}); + for (auto const& object : inspectedData._objects) { + if (object._id == cellId && object.getObjectType() == ObjectType_Cell) { + auto const& creature = inspectedData.getCreatureRef(object.getCellRef()._creatureId); + GenomeEditorWindow::get().openTab(inspectedData.getGenomeRef(creature._genomeId)); + return; + } + } + printOverlayMessage("The creature no longer exists"); +} + +void EvolutionDashboardWindow::processTimelineSection() +{ + processTimelineHeader(); + + //rebuild the plot series right after the header widgets changed the horizon; otherwise the widened + //x-axis shows a gap on the left for one frame because the series are still trimmed to the old horizon + updatePlotData(); + + std::vector plottedLineages; + if (_selectedLineageIds.empty()) { + plottedLineages.emplace_back(&_allLineages); + } else { + for (auto const& lineage : _plottedLineages) { + plottedLineages.emplace_back(&lineage); + } + } + + //in entire-history mode the time axis is pinned below the scrolling plot area so it stays visible; + //reserve its full height (axis plot + table cell padding + item spacing), otherwise the section gets its own scrollbar + auto pinnedTimeAxis = _timelineMode == TimelineMode_EntireHistory; + auto const& style = ImGui::GetStyle(); + auto timeAxisHeight = scale(TimeAxisExtraHeight) + style.CellPadding.y * 2.0f + style.ItemSpacing.y; + auto scrollbarWidth = 0.0f; + if (ImGui::BeginChild("##timelinePlots", {0, pinnedTimeAxis ? -timeAxisHeight : 0.0f})) { + processTimelinePlots(plottedLineages); + if (ImGui::GetScrollMaxY() > 0.0f) { + scrollbarWidth = ImGui::GetStyle().ScrollbarSize; + } + } + ImGui::EndChild(); + if (pinnedTimeAxis) { + processTimeAxis(plottedLineages, scrollbarWidth); + } +} + +void EvolutionDashboardWindow::processTimelineHeader() +{ + ImGui::Spacing(); + std::vector modeValues{"Real-time", "Last time steps", "Entire history"}; + AlienGui::Switcher(AlienGui::SwitcherParameters().name("Mode").width(260.0f).textWidth(45.0f).values(modeValues), &_timelineMode); + + //sliders keep their default width and only shrink when the window gets too narrow + auto numSliders = _timelineMode == TimelineMode_EntireHistory ? 1.0f : 2.0f; + ImGui::SameLine(0, scale(20.0f)); + auto sliderWidth = std::clamp((ImGui::GetContentRegionAvail().x - scale(20.0f) * (numSliders - 1.0f)) / numSliders, scale(140.0f), scale(320.0f)); + if (_timelineMode == TimelineMode_RealTime) { + if (ImGui::BeginChild("##timeHorizon", {sliderWidth, ImGui::GetFrameHeight()})) { + AlienGui::SliderFloat( + AlienGui::SliderFloatParameters().name("Time horizon").min(1.0f).max(TimelineLiveStatistics::MaxLiveHistory).format("%.1f s").textWidth(100.0f), + &_timeHorizon); + validateAndCorrect(); + } + ImGui::EndChild(); + ImGui::SameLine(0, scale(20.0f)); + } + if (_timelineMode == TimelineMode_LastSteps) { + if (ImGui::BeginChild("##lastSteps", {sliderWidth, ImGui::GetFrameHeight()})) { + AlienGui::SliderInt( + AlienGui::SliderIntParameters().name("Steps").min(1000).max(TimelineLiveStatistics::MaxLiveSteps).logarithmic(true).textWidth(100.0f), + &_lastSteps); + validateAndCorrect(); + } + ImGui::EndChild(); + ImGui::SameLine(0, scale(20.0f)); + } + if (ImGui::BeginChild("##plotHeight", {sliderWidth, ImGui::GetFrameHeight()})) { + AlienGui::SliderFloat( + AlienGui::SliderFloatParameters().name("Plot height").min(MinPlotHeight).max(MaxPlotHeight).format("%.0f").textWidth(100.0f), &_plotHeight); + validateAndCorrect(); + } + ImGui::EndChild(); + + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text("Timeline filter"); + ImGui::PopStyleColor(); + ImGui::SameLine(0, scale(12.0f)); + if (_selectedLineageIds.empty()) { + AlienGui::Text("All lineages"); + } else { + auto drawList = ImGui::GetWindowDrawList(); + for (auto const& lineage : _plottedLineages) { + auto swatchesWidth = drawColorSwatches(drawList, ImGui::GetCursorScreenPos(), lineage.colorBitset, ImGui::GetTextLineHeight(), _cellColors); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchesWidth + scale(4.0f)); + AlienGui::Text("Lineage #" + std::to_string(lineage.id)); + ImGui::SameLine(0, scale(14.0f)); + } + ImGui::NewLine(); + } + ImGui::Spacing(); +} + +void EvolutionDashboardWindow::processTimelinePlots(std::vector const& plottedLineages) +{ + //labels and current values are placed to the right of the widgets, matching the general ALIEN layout + if (ImGui::BeginTable("##plots", 2, ImGuiTableFlags_None)) { + ImGui::TableSetupColumn("##plot"); + ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, scale(PlotLabelColumnWidth)); + for (int i = 0; i < NumMetrics; ++i) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + processTimelinePlot(plottedLineages, i); + + ImGui::TableSetColumnIndex(1); + AlienGui::Text(Metrics[i].plotName); + auto seriesIndex = 0; + for (auto const* lineage : plottedLineages) { + ImGui::PushFont(StyleRepository::get().getMediumBoldFont()); + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)getPlotColor(i, seriesIndex, toInt(plottedLineages.size()))); + auto const& series = lineage->series.at(i); + AlienGui::Text(!series.empty() ? formatMetricValue(series.back(), Metrics[i].plotDecimals) : "-"); + ImGui::PopStyleColor(); + ImGui::PopFont(); + ++seriesIndex; + } + } + ImGui::EndTable(); + } +} + +void EvolutionDashboardWindow::processTimelinePlot(std::vector const& plottedLineages, int metricIndex) +{ + auto upperBound = 0.0; + auto minTime = std::numeric_limits::max(); + auto maxTime = std::numeric_limits::lowest(); + auto hasData = false; + for (auto const* lineage : plottedLineages) { + if (lineage->timePoints.size() < 2) { + continue; + } + hasData = true; + minTime = std::min(minTime, lineage->timePoints.front()); + maxTime = std::max(maxTime, lineage->timePoints.back()); + for (auto const& value : lineage->series.at(metricIndex)) { + if (std::isfinite(value)) { + upperBound = std::max(upperBound, value); + } + } + } + if (!hasData) { + minTime = 0.0; + maxTime = 1.0; + } else if (_timelineMode == TimelineMode_RealTime) { + minTime = maxTime - toDouble(_timeHorizon); + } else if (_timelineMode == TimelineMode_LastSteps) { + minTime = maxTime - toDouble(_lastSteps); + } + upperBound *= 1.35; + + ImGui::PushID(metricIndex); + ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); + ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); + ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0.3f, 0.3f, 0.3f, ImGui::GetStyle().Alpha)); + ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); + ImPlot::SetNextAxesLimits(minTime, maxTime, 0, upperBound + NEAR_ZERO, ImGuiCond_Always); + + if (ImPlot::BeginPlot( + "##plot", ImVec2(-1, scale(_plotHeight)), ImPlotFlags_NoLegend | ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMouseText)) { + ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_NoTickLabels); + ImPlot::SetupAxis(ImAxis_Y1, "", ImPlotAxisFlags_NoTickLabels); + ImPlot::SetupAxisFormat(ImAxis_Y1, ""); + auto seriesIndex = 0; + for (auto const* lineage : plottedLineages) { + auto count = toInt(lineage->timePoints.size()); + auto const& series = lineage->series.at(metricIndex); + + //rate series start with NaN samples until a sufficiently old rate reference exists; skip them + auto offset = 0; + while (offset < count && std::isnan(series.at(offset))) { + ++offset; + } + count -= offset; + if (count < 2) { + ++seriesIndex; + continue; + } + ImGui::PushID(toInt(lineage->id) + 1); + auto color = getPlotColor(metricIndex, seriesIndex, toInt(plottedLineages.size())); + ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); + ImPlot::PushStyleColor(ImPlotCol_Fill, (ImU32)color); + ImPlot::PlotLine("##line", lineage->timePoints.data() + offset, series.data() + offset, count); + ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.5f * ImGui::GetStyle().Alpha); + ImPlot::PlotShaded("##shaded", lineage->timePoints.data() + offset, series.data() + offset, count); + ImPlot::PopStyleVar(); + ImPlot::PopStyleColor(2); + if (ImGui::GetStyle().Alpha == 1.0f) { + ImPlot::Annotation( + lineage->timePoints.back(), + series.back(), + color, + ImVec2(-10.0f, 10.0f), + true, + "%s", + formatMetricValue(series.back(), Metrics[metricIndex].plotDecimals).c_str()); + } + ImGui::PopID(); + ++seriesIndex; + } + if (ImGui::GetStyle().Alpha == 1.0f && ImPlot::IsPlotHovered() && plottedLineages.size() == 1 && plottedLineages.front()->timePoints.size() >= 2) { + auto const* lineage = plottedLineages.front(); + drawValuesAtMouseCursor( + lineage->series.at(metricIndex), + lineage->timePoints, + lineage->systemClockPoints, + minTime, + maxTime, + upperBound + NEAR_ZERO, + Metrics[metricIndex].plotDecimals); + } + ImPlot::EndPlot(); + } + ImPlot::PopStyleVar(); + ImPlot::PopStyleColor(3); + ImGui::PopID(); +} + +void EvolutionDashboardWindow::processTimeAxis(std::vector const& plottedLineages, float rightMargin) +{ + auto minTime = std::numeric_limits::max(); + auto maxTime = std::numeric_limits::lowest(); + auto hasData = false; + for (auto const* lineage : plottedLineages) { + if (lineage->timePoints.size() < 2) { + continue; + } + hasData = true; + minTime = std::min(minTime, lineage->timePoints.front()); + maxTime = std::max(maxTime, lineage->timePoints.back()); + } + if (!hasData) { + minTime = 0.0; + maxTime = 1.0; + } + + //the label column mirrors the plot table above (widened by the scrollbar if present) so the axis stays aligned with the plots + if (ImGui::BeginTable("##timeAxis", 2, ImGuiTableFlags_None)) { + ImGui::TableSetupColumn("##axis"); + ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, scale(PlotLabelColumnWidth) + rightMargin); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, 0.0f)); + ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, 0.0f)); + ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0.0f, 0.0f, 0.0f, 0.0f)); + ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); + ImPlot::SetNextAxesLimits(minTime, maxTime, 0, 1.0, ImGuiCond_Always); + if (ImPlot::BeginPlot("##timeAxis", ImVec2(-1, scale(TimeAxisExtraHeight)), ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) { + ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_NoHighlight); + ImPlot::SetupAxisFormat(ImAxis_X1, formatTimestepsInThousands, nullptr); + ImPlot::SetupAxis(ImAxis_Y1, "", ImPlotAxisFlags_NoTickLabels | ImPlotAxisFlags_NoTickMarks | ImPlotAxisFlags_NoHighlight); + ImPlot::EndPlot(); + } + ImPlot::PopStyleVar(); + ImPlot::PopStyleColor(3); + ImGui::EndTable(); + } +} + +void EvolutionDashboardWindow::drawValuesAtMouseCursor( + std::vector const& series, + std::vector const& timePoints, + std::vector const& systemClockPoints, + double startTime, + double endTime, + double upperBound, + int fracPartDecimals) +{ + auto count = toInt(timePoints.size()); + auto hasSystemClock = !systemClockPoints.empty(); + + auto mousePos = ImPlot::GetPlotMousePos(); + mousePos.x = std::max(startTime, std::min(endTime, mousePos.x)); + mousePos.y = series.at(0); + auto systemClockEntry = hasSystemClock ? systemClockPoints.at(0) : 0.0; + for (int i = 1; i < count; ++i) { + if (timePoints.at(i) > mousePos.x) { + mousePos.y = series.at(i); + if (hasSystemClock) { + systemClockEntry = systemClockPoints.at(i); + } + break; + } + } + auto valueAtCursor = mousePos.y; //may be NaN for rate series without a sufficiently old rate reference + mousePos.y = std::isnan(mousePos.y) ? 0.0 : std::max(0.0, std::min(upperBound, mousePos.y)); + + ImPlot::PushStyleColor(ImPlotCol_InlayText, ImColor::HSV(0.0f, 0.0f, 1.0f).Value); + ImPlot::PlotText(ICON_FA_GENDERLESS, mousePos.x, mousePos.y, {scale(1.0f), scale(2.0f)}); + ImPlot::PopStyleColor(); + + ImPlot::PushStyleColor(ImPlotCol_Line, ImColor::HSV(0.0f, 0.0f, 1.0f).Value); + ImPlot::PlotInfLines("", &mousePos.x, 1); + ImPlot::PopStyleColor(); + + char label[256]; + auto leftSideFactor = mousePos.x > (startTime + endTime) / 2 ? -1.0f : 1.0f; + if (hasSystemClock) { + auto dateTimeString = systemClockEntry != 0 ? convertSystemClockToString(systemClockEntry) : std::string("-"); + snprintf( + label, + sizeof(label), + "Time step: %s\nTimestamp: %s\nValue: %s", + StringHelper::format(toFloat(mousePos.x), 0).c_str(), + dateTimeString.c_str(), + formatMetricValue(valueAtCursor, fracPartDecimals).c_str()); + } else { + snprintf( + label, + sizeof(label), + "Relative time: %s\nValue: %s", + StringHelper::format(toFloat(mousePos.x), 0).c_str(), + formatMetricValue(valueAtCursor, fracPartDecimals).c_str()); + } + ImPlot::PlotText(label, mousePos.x, upperBound, {leftSideFactor * (scale(5.0f) + ImGui::CalcTextSize(label).x / 2), scale(28.0f)}); +} + +void EvolutionDashboardWindow::validateAndCorrect() +{ + _timelinesHeight = std::max(scale(100.0f), _timelinesHeight); + _timelineMode = std::clamp(_timelineMode, static_cast(TimelineMode_RealTime), static_cast(TimelineMode_EntireHistory)); + _lastSteps = std::clamp(_lastSteps, 1000, TimelineLiveStatistics::MaxLiveSteps); + _timeHorizon = std::clamp(_timeHorizon, 1.0f, TimelineLiveStatistics::MaxLiveHistory); + _plotHeight = std::clamp(_plotHeight, MinPlotHeight, MaxPlotHeight); + _sortColumnIndex = std::clamp(_sortColumnIndex, 0, NumMetrics); + _colorFilter &= (1 << MAX_COLORS) - 1; + if (_colorFilter == 0) { + _colorFilter = (1 << MAX_COLORS) - 1; + } +} diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h new file mode 100644 index 0000000000..ec36b4f8ed --- /dev/null +++ b/source/Gui/EvolutionDashboardWindow.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "AlienWindow.h" +#include "Definitions.h" +#include "TimelineLiveStatistics.h" + +class EvolutionDashboardWindow : public AlienWindow +{ + MAKE_SINGLETON_NO_DEFAULT_CONSTRUCTION(EvolutionDashboardWindow); + +public: + static auto constexpr NumMetrics = 8; + +private: + struct LineageDisplayData; + + EvolutionDashboardWindow(); + + void initIntern() override; + void shutdownIntern() override; + void processIntern() override; + void processBackground() override; + + void updateCellColors(); + void updateDisplayData(); + void updateTableData(); + void sortLineages(); + void updatePlotData(); + void rebuildPlotSeries(std::vector const& source); + void rebuildPlotSeries(StatisticsHistoryData const& source); + void processHeader(); + void processCard( + std::string const& label, + std::string const& value, + std::vector> const& subValues, + float width, + float height); + void processFilterBar(); + void processLineageTable(); + void onOpenRepresentativeGenome(uint64_t cellId); + void processTimelineSection(); + void processTimelineHeader(); + void processTimelinePlots(std::vector const& plottedLineages); + void processTimelinePlot(std::vector const& plottedLineages, int metricIndex); + void processTimeAxis(std::vector const& plottedLineages, float rightMargin); + void drawValuesAtMouseCursor( + std::vector const& series, + std::vector const& timePoints, + std::vector const& systemClockPoints, + double startTime, + double endTime, + double upperBound, + int fracPartDecimals); + + void validateAndCorrect(); + + struct LineageDisplayData + { + int64_t id = 0; //-1 = "all lineages" + int colorBitset = 0; + uint64_t representativeCellId = 0; //cell of a creature with the highest generation; 0 = not available + std::array currentValues = {}; + std::array, NumMetrics> series = {}; //only filled for plotted lineages + std::vector timePoints; //only filled for plotted lineages + std::vector systemClockPoints; //only filled when the x-axis represents time steps + }; + + std::vector _lineages; //table rows from the latest sample, sorted by the selected table column + std::vector _plottedLineages; + LineageDisplayData _allLineages; + + std::array _cellColors = {}; + + std::set _selectedLineageIds; + int _colorFilter = 0x3ff; //bitset for MAX_COLORS cell colors + + int _sortColumnIndex = 1; //0 = lineage column, 1..NumMetrics = metric columns + bool _sortAscending = false; + + using TimelineMode = int; + enum TimelineMode_ + { + TimelineMode_RealTime, + TimelineMode_LastSteps, + TimelineMode_EntireHistory + }; + TimelineMode _timelineMode = TimelineMode_EntireHistory; + + int _lastSteps = 100000; + float _timeHorizon = 10.0f; //in seconds + float _timelinesHeight = 0; + float _plotHeight = 60.0f; + std::optional _lastWindowHeight; + + struct RebuildKey + { + int timelineMode = 0; + int lastSteps = 0; + float timeHorizon = 0; + double sourceBackTime = 0; + std::set selectedLineageIds; + std::vector lineageBackTimes; //only used for the long-term history where each lineage has its own timeline + + bool operator==(RebuildKey const&) const = default; + }; + std::optional _lastRebuildKey; + std::optional _lastTableBackTime; + + // Live statistics + TimelineLiveStatistics _timelineLiveStatistics; + std::optional _lastSessionId; + std::optional _lastTimepoint; +}; diff --git a/source/Gui/GeneEditorWidget.cpp b/source/Gui/GeneEditorWidget.cpp index 955487b969..b287a9bb24 100644 --- a/source/Gui/GeneEditorWidget.cpp +++ b/source/Gui/GeneEditorWidget.cpp @@ -74,7 +74,7 @@ void _GeneEditorWidget::processHeaderData() if (ImGui::BeginChild("GeneHeader", ImVec2(0, -_layoutData->nodeListHeight), 0, 0)) { auto& gene = _editData->getSelectedGeneRef(); - _editData->updateGeometry(gene._shape); // Do it every time in order to avoid check for changes + _editData->updateGeometry(); // Do it every time in order to avoid check for changes AlienGui::DynamicTableLayout table(HeaderMinColumnWidth); if (table.begin()) { diff --git a/source/Gui/GenomeEditorWindow.cpp b/source/Gui/GenomeEditorWindow.cpp index 90df13423f..fa1bd86c32 100644 --- a/source/Gui/GenomeEditorWindow.cpp +++ b/source/Gui/GenomeEditorWindow.cpp @@ -38,11 +38,12 @@ void GenomeEditorWindow::openTab(GenomeDesc const& genome, bool forceNewTab, boo if (_tabs.size() == 1 && _tabs.front()->isEmpty()) { _tabs.clear(); } + auto normalizedGenome = _GenomeTabWidget::normalizeForEditor(genome); std::optional tabIndex; if (!forceNewTab) { for (auto const& [index, tab] : _tabs | boost::adaptors::indexed(0)) { auto tabGenome = tab->getGenomeDesc(); - if (genome.equalWithoutId(tabGenome)) { + if (normalizedGenome.equalWithoutId(tabGenome)) { tabIndex = toInt(index); } } @@ -51,7 +52,7 @@ void GenomeEditorWindow::openTab(GenomeDesc const& genome, bool forceNewTab, boo _tabIndexToSelect = *tabIndex; _tabs.at(*tabIndex)->resetOriginal(); } else { - onScheduleAddTab(genome); + onScheduleAddTab(normalizedGenome); } } diff --git a/source/Gui/GenomeTabEditData.h b/source/Gui/GenomeTabEditData.h index 32c8cd5746..f543a4448d 100644 --- a/source/Gui/GenomeTabEditData.h +++ b/source/Gui/GenomeTabEditData.h @@ -68,18 +68,21 @@ struct _GenomeTabEditData return gene._nodes.at(nodeIndex.value()); } - void updateGeometry(ConstructorShape shape) + // The reference angles of the middle nodes are derived data: the constructor regenerates them + // from the gene's shape during construction + static void updateGeneGeometry(GeneDesc& gene) { ShapeGenerator shapeGenerator; - auto& gene = getSelectedGeneRef(); auto numNodes = gene._nodes.size(); int index = 0; for (auto& node : gene._nodes) { - auto shapeGenerationResult = shapeGenerator.generateNextConstructionData(shape); + auto shapeGenerationResult = shapeGenerator.generateNextConstructionData(gene._shape); if (index > 0 && index < numNodes - 1) { node._referenceAngle = shapeGenerationResult.angle; } ++index; } } + + void updateGeometry() { updateGeneGeometry(getSelectedGeneRef()); } }; diff --git a/source/Gui/GenomeTabWidget.cpp b/source/Gui/GenomeTabWidget.cpp index 7f5ed87d21..5590c29238 100644 --- a/source/Gui/GenomeTabWidget.cpp +++ b/source/Gui/GenomeTabWidget.cpp @@ -23,6 +23,15 @@ GenomeTabWidget _GenomeTabWidget::create(GenomeWindowEditData const& genomeEditD return GenomeTabWidget(new _GenomeTabWidget(genomeEditData, genome, layoutData)); } +GenomeDesc _GenomeTabWidget::normalizeForEditor(GenomeDesc genome) +{ + DescValidationService::get().validateAndCorrect(genome); + for (auto& gene : genome._genes) { + _GenomeTabEditData::updateGeneGeometry(gene); + } + return genome; +} + void _GenomeTabWidget::process() { doLayout(); @@ -131,11 +140,10 @@ _GenomeTabWidget::_GenomeTabWidget( _editData = std::make_shared<_GenomeTabEditData>(++_sequence, genome); _editData->id = ++_sequence; - auto validatedGenome = genome; - DescValidationService::get().validateAndCorrect(validatedGenome); + auto normalizedGenome = normalizeForEditor(genome); - _editData->genome = validatedGenome; - _editData->origGenome = validatedGenome; + _editData->genome = normalizedGenome; + _editData->origGenome = normalizedGenome; if (!genome._genes.empty()) { _editData->selectedGeneIndex = 0; diff --git a/source/Gui/GenomeTabWidget.h b/source/Gui/GenomeTabWidget.h index c77d9053a0..bb2d88a059 100644 --- a/source/Gui/GenomeTabWidget.h +++ b/source/Gui/GenomeTabWidget.h @@ -11,6 +11,9 @@ class _GenomeTabWidget public: static GenomeTabWidget create(GenomeWindowEditData const& genomeEditData, GenomeDesc const& genome, GenomeTabLayoutData const& layoutData = nullptr); + // Validated genome with normalized derived geometry, as it is maintained by the editor widgets + static GenomeDesc normalizeForEditor(GenomeDesc genome); + void process(); int getTabId() const; diff --git a/source/Gui/HistogramLiveStatistics.cpp b/source/Gui/HistogramLiveStatistics.cpp deleted file mode 100644 index 33fc037772..0000000000 --- a/source/Gui/HistogramLiveStatistics.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "HistogramLiveStatistics.h" - -bool HistogramLiveStatistics::isDataAvailable() const -{ - return _lastHistogramData.has_value(); -} - -std::optional const& HistogramLiveStatistics::getData() const -{ - return _lastHistogramData; -} - -void HistogramLiveStatistics::update(HistogramData const& data) -{ - _lastHistogramData = data; -} diff --git a/source/Gui/HistogramLiveStatistics.h b/source/Gui/HistogramLiveStatistics.h deleted file mode 100644 index 37fad165c1..0000000000 --- a/source/Gui/HistogramLiveStatistics.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -#include "Definitions.h" - -class HistogramLiveStatistics -{ -public: - bool isDataAvailable() const; - - std::optional const& getData() const; - void update(HistogramData const& data); - -private: - std::optional _lastHistogramData; -}; diff --git a/source/Gui/InspectionWindow.cpp b/source/Gui/InspectionWindow.cpp index 6e9466ed6c..262389b2c3 100644 --- a/source/Gui/InspectionWindow.cpp +++ b/source/Gui/InspectionWindow.cpp @@ -581,7 +581,8 @@ void _InspectionWindow::processCreatureProperties(ExtendedObjectDesc& extendedOb inspectorText("Generation", std::to_string(creature._generation)); inspectorText("Num cells", std::to_string(creature._numCells)); AlienGui::InputInt(AlienGui::InputIntParameters().name("Lineage id").textWidth(TextWidth), creature._lineageId); - AlienGui::InputFloat(AlienGui::InputFloatParameters().name("Accumulated mutations").format("%.5f").textWidth(TextWidth), creature._accumulatedMutations); + inspectorText("Mutations (lineage)", std::to_string(creature._accumulatedMutationsInLineage)); + inspectorText("Mutations (total)", std::to_string(creature._accumulatedMutations)); auto& genome = extendedObject.genome.value(); inspectorText("Genome name", genome._name); inspectorText("Resistance to injection", genome._resistanceToInjection ? "Yes" : "No"); diff --git a/source/Gui/MainLoopController.cpp b/source/Gui/MainLoopController.cpp index 36c276db98..8fbbe23b37 100644 --- a/source/Gui/MainLoopController.cpp +++ b/source/Gui/MainLoopController.cpp @@ -29,6 +29,7 @@ #include "DeleteUserDialog.h" #include "DisplaySettingsDialog.h" #include "EditorController.h" +#include "EvolutionDashboardWindow.h" #include "ExitDialog.h" #include "FileTransferController.h" #include "FpsController.h" @@ -52,7 +53,6 @@ #include "SimulationParametersMainWindow.h" #include "SimulationView.h" #include "SpatialControlWindow.h" -#include "StatisticsWindow.h" #include "StyleRepository.h" #include "TemporalControlWindow.h" #include "UiController.h" @@ -406,8 +406,13 @@ void MainLoopController::processMenubar() .closeMenuWhenItemClicked(false), [&] { SpatialControlWindow::get().setOn(!SpatialControlWindow::get().isOn()); }); AlienGui::MenuItem( - AlienGui::MenuItemParameters().name("Statistics").keyAlt(true).key(ImGuiKey_3).selected(StatisticsWindow::get().isOn()).closeMenuWhenItemClicked(false), - [&] { StatisticsWindow::get().setOn(!StatisticsWindow::get().isOn()); }); + AlienGui::MenuItemParameters() + .name("Evolution dashboard") + .keyAlt(true) + .key(ImGuiKey_3) + .selected(EvolutionDashboardWindow::get().isOn()) + .closeMenuWhenItemClicked(false), + [&] { EvolutionDashboardWindow::get().setOn(!EvolutionDashboardWindow::get().isOn()); }); AlienGui::MenuItem( AlienGui::MenuItemParameters() .name("Simulation parameters") diff --git a/source/Gui/MainWindow.cpp b/source/Gui/MainWindow.cpp index 8a4ac43f05..dd2990d693 100644 --- a/source/Gui/MainWindow.cpp +++ b/source/Gui/MainWindow.cpp @@ -41,6 +41,7 @@ #include "DisplaySettingsDialog.h" #include "EditSimulationDialog.h" #include "EditorController.h" +#include "EvolutionDashboardWindow.h" #include "ExitDialog.h" #include "FileTransferController.h" #include "FpsController.h" @@ -74,7 +75,6 @@ #include "SimulationView.h" #include "SpatialControlWindow.h" #include "StartupCheckService.h" -#include "StatisticsWindow.h" #include "StyleRepository.h" #include "TemporalControlWindow.h" #include "UiController.h" @@ -125,7 +125,7 @@ _MainWindow::_MainWindow() EditorController::get().setup(); SimulationView::get().setup(); SimulationInteractionController::get().setup(); - StatisticsWindow::get().setup(); + EvolutionDashboardWindow::get().setup(); TemporalControlWindow::get().setup(); SpatialControlWindow::get().setup(); SimulationParametersMainWindow::get().setup(); diff --git a/source/Gui/SelectionWindow.cpp b/source/Gui/SelectionWindow.cpp index 7694567629..ca921f5808 100644 --- a/source/Gui/SelectionWindow.cpp +++ b/source/Gui/SelectionWindow.cpp @@ -24,35 +24,35 @@ void SelectionWindow::processIntern() if (table.begin()) { auto selection = EditorModel::get().getSelectionShallowData(); + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Cells"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::TextUnformatted(StringHelper::format(selection.numObjects).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); table.next(); + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Connected cells"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::TextUnformatted(StringHelper::format(selection.numClusterCells).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); table.next(); + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Creatures"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::TextUnformatted(StringHelper::format(selection.numCreatures).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); table.next(); + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Energy particles"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::TextUnformatted(StringHelper::format(selection.numEnergyParticles).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); table.next(); diff --git a/source/Gui/SpatialControlWindow.cpp b/source/Gui/SpatialControlWindow.cpp index 31a9997cb5..9bbcd2d4ed 100644 --- a/source/Gui/SpatialControlWindow.cpp +++ b/source/Gui/SpatialControlWindow.cpp @@ -21,7 +21,7 @@ void SpatialControlWindow::initIntern() ResizeWorldDialog::get().setup(); auto& settings = GlobalSettings::get(); - Viewport::get().setZoomSensitivity(settings.getValue("windows.spatial control.zoom sensitivity factor", Viewport::get().getZoomSensitivity())); + Viewport::get().setZoomSensitivity(settings.getValue("windows.spatial control.zoom sensitivity", Viewport::get().getZoomSensitivity())); } SpatialControlWindow::SpatialControlWindow() @@ -54,27 +54,27 @@ void SpatialControlWindow::processIntern() if (ImGui::BeginChild("##", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar)) { + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("World size"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); auto worldSize = _SimulationFacade::get()->getWorldSize(); - ImGui::TextUnformatted((StringHelper::format(worldSize.x) + " x " + StringHelper::format(worldSize.y)).c_str()); - ImGui::PopStyleColor(); + ImGui::TextUnformatted((StringHelper::format(worldSize.x) + ", " + StringHelper::format(worldSize.y)).c_str()); ImGui::PopFont(); + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Zoom factor"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::TextUnformatted(StringHelper::format(Viewport::get().getZoomFactor(), 2).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Center position"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); auto centerPos = Viewport::get().getCenterInWorldPos(); ImGui::TextUnformatted((StringHelper::format(centerPos.x, 1) + ", " + StringHelper::format(centerPos.y, 1)).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); AlienGui::Separator(); diff --git a/source/Gui/StatisticsWindow.cpp b/source/Gui/StatisticsWindow.cpp deleted file mode 100644 index d0bb8423be..0000000000 --- a/source/Gui/StatisticsWindow.cpp +++ /dev/null @@ -1,858 +0,0 @@ -#include "StatisticsWindow.h" - -#include -#include -#include - -#include - -#include -#include -#include - -#include - -#include -#include - -#include -#include -#include - -#include - -#include "AlienGui.h" -#include "GenericFileDialog.h" -#include "GenericMessageDialog.h" -#include "StyleRepository.h" - -namespace -{ - auto constexpr RightColumnWidth = 175.0f; - auto constexpr RightColumnWidthTimeline = 150.0f; - auto constexpr RightColumnWidthTable = 200.0f; - auto constexpr LiveStatisticsDeltaTime = 50; //in millisec - auto constexpr SettingsHeight = 130.0f; - - std::vector createPlotTypeStrings() - { - std::vector result = {"Accumulate values for all colors", "Break down by color"}; - for (int color = 0; color < MAX_COLORS; ++color) { - result.emplace_back("Color #" + std::to_string(color + 1)); - } - return result; - } -} - -void StatisticsWindow::initIntern() -{ - - auto path = std::filesystem::current_path(); - if (path.has_parent_path()) { - path = path.parent_path(); - } - _startingPath = GlobalSettings::get().getValue("windows.statistics.starting path", path.string()); - _settingsOpen = GlobalSettings::get().getValue("windows.statistics.settings.open", _settingsOpen); - _settingsHeight = GlobalSettings::get().getValue("windows.statistics.settings.height", scale(SettingsHeight)); - _plotHeight = GlobalSettings::get().getValue("windows.statistics.plot height", _plotHeight); - _plotMode = GlobalSettings::get().getValue("windows.statistics.mode", _plotMode); - _timeHorizonForLiveStatistics = GlobalSettings::get().getValue("windows.statistics.live horizon", _timeHorizonForLiveStatistics); - _timeHorizonForLongtermStatistics = GlobalSettings::get().getValue("windows.statistics.long term horizon", _timeHorizonForLongtermStatistics); - _plotType = GlobalSettings::get().getValue("windows.statistics.plot type", _plotType); - _plotScale = GlobalSettings::get().getValue("windows.statistics.plot scale", _plotScale); - auto collapsedPlotIndexJoinedString = GlobalSettings::get().getValue("windows.statistics.collapsed plot indices", std::string()); - - if (!collapsedPlotIndexJoinedString.empty()) { - std::vector collapsedPlotIndexStrings; - boost::split(collapsedPlotIndexStrings, collapsedPlotIndexJoinedString, boost::is_any_of(" ")); - for (auto const& s : collapsedPlotIndexStrings) { - _collapsedPlotIndices.emplace(std::stoi(s)); - } - } - validateAndCorrect(); -} - -StatisticsWindow::StatisticsWindow() - : AlienWindow("Statistics", "windows.statistics", false) -{} - -void StatisticsWindow::shutdownIntern() -{ - GlobalSettings::get().setValue("windows.statistics.starting path", _startingPath); - GlobalSettings::get().setValue("windows.statistics.settings.open", _settingsOpen); - GlobalSettings::get().setValue("windows.statistics.settings.height", _settingsHeight); - GlobalSettings::get().setValue("windows.statistics.plot height", _plotHeight); - GlobalSettings::get().setValue("windows.statistics.mode", _plotMode); - GlobalSettings::get().setValue("windows.statistics.live horizon", _timeHorizonForLiveStatistics); - GlobalSettings::get().setValue("windows.statistics.long term horizon", _timeHorizonForLongtermStatistics); - GlobalSettings::get().setValue("windows.statistics.plot type", _plotType); - GlobalSettings::get().setValue("windows.statistics.plot scale", _plotScale); - - std::vector collapsedPlotIndexStrings; - for (auto const& index : _collapsedPlotIndices) { - collapsedPlotIndexStrings.emplace_back(std::to_string(index)); - } - GlobalSettings::get().setValue("windows.statistics.collapsed plot indices", boost::join(collapsedPlotIndexStrings, " ")); -} - -void StatisticsWindow::processIntern() -{ - if (ImGui::BeginChild("##statistics", {0, _settingsOpen ? -_settingsHeight : -scale(40.0f)})) { - if (ImGui::BeginTabBar("##Statistics", ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_FittingPolicyResizeDown)) { - - if (ImGui::BeginTabItem("Timelines")) { - if (ImGui::BeginChild("##timelines", ImVec2(0, 0), 0)) { - processTimelinesTab(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Histograms")) { - if (ImGui::BeginChild("##histograms", ImVec2(0, 0), 0)) { - processHistogramsTab(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Throughput")) { - if (ImGui::BeginChild("##throughput", ImVec2(0, 0), 0)) { - processTablesTab(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - } - ImGui::EndChild(); - processSettings(); -} - -void StatisticsWindow::processTimelinesTab() -{ - ImGui::Spacing(); - - AlienGui::Switcher(AlienGui::SwitcherParameters().name("Mode").textWidth(RightColumnWidth).values({"Real-time plots", "Entire history plots"}), &_plotMode); - - AlienGui::Switcher(AlienGui::SwitcherParameters().name("Plot type").textWidth(RightColumnWidth).values(createPlotTypeStrings()), &_plotType); - - if (ImGui::BeginChild("##plots", ImVec2(0, 0), false)) { - processTimelineStatistics(); - } - ImGui::EndChild(); -} - -void StatisticsWindow::processHistogramsTab() -{ - if (!_histogramLiveStatistics.isDataAvailable()) { - return; - } - auto const& customizationColors = _SimulationFacade::get()->getSimulationParameters().customizationColors.value; - ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha * 0.5 * Const::WindowAlpha)); - ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha * 0.5 * Const::WindowAlpha)); - auto const& histogramData = _histogramLiveStatistics.getData(); - auto maxNumObjects = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - for (int j = 0; j < MAX_HISTOGRAM_SLOTS; ++j) { - auto value = histogramData->numCellsByColorBySlot[i][j]; - maxNumObjects = std::max(maxNumObjects, value); - } - } - - //round maxNumObjects - if (!_histogramUpperBound || toFloat(maxNumObjects) > *_histogramUpperBound * 0.9f || toFloat(maxNumObjects) < *_histogramUpperBound * 0.5f) { - _histogramUpperBound = toFloat(maxNumObjects) * 1.3f; - } - - ImPlot::SetNextAxesLimits(0, toFloat(MAX_HISTOGRAM_SLOTS), 0, *_histogramUpperBound, ImGuiCond_Always); - - auto getLabelString = [](int value) { - if (value >= 1000) { - return std::to_string(value / 1000) + "K"; - } else { - return std::to_string(value); - } - }; - - //y-ticks - char const* labelsY[6]; - double positionsY[6]; - for (int i = 0; i < 5; ++i) { - labelsY[i] = ""; - positionsY[i] = *_histogramUpperBound / 5 * i; - } - auto temp = getLabelString(maxNumObjects); - labelsY[5] = temp.c_str(); - positionsY[5] = toFloat(maxNumObjects); - - //x-ticks - char const* labelsX[5]; - std::string labelsX_temp[5]; - double positionsX[5]; - - auto slotAge = histogramData->maxAge / MAX_HISTOGRAM_SLOTS; - for (int i = 0; i < 5; ++i) { - labelsX_temp[i] = getLabelString(slotAge * ((MAX_HISTOGRAM_SLOTS - 1) / 4) * i); - labelsX[i] = labelsX_temp[i].c_str(); - positionsX[i] = toFloat(((MAX_HISTOGRAM_SLOTS - 1) / 4) * i); - } - - - //plot histogram - if (ImPlot::BeginPlot("##Histograms", ImVec2(-1, -1))) { - ImPlot::SetupAxisTicks(ImAxis_Y1, positionsY, 5, labelsY); - ImPlot::SetupAxisTicks(ImAxis_X1, positionsX, 5, labelsX); - ImPlot::SetupAxes("Age", "Object count"); - ImPlot::SetupAxisFormat(ImAxis_X1, ""); - auto const width = 1.0f / MAX_COLORS; - for (int i = 0; i < MAX_COLORS; ++i) { - float h, s, v; - AlienGui::ConvertRGBtoHSV(customizationColors.values[i].toRgbColor(), h, s, v); - ImPlot::PushStyleColor(ImPlotCol_Fill, (ImVec4)ImColor::HSV(h, s /** 3 / 4*/, v /** 3 / 4*/, ImGui::GetStyle().Alpha)); - ImPlot::PlotBars((" ##" + std::to_string(i)).c_str(), histogramData->numCellsByColorBySlot[i], MAX_HISTOGRAM_SLOTS, width, width * i); - ImPlot::PopStyleColor(1); - } - ImPlot::EndPlot(); - } - ImPlot::PopStyleColor(2); -} - -void StatisticsWindow::processTablesTab() -{ - if (!_tableLiveStatistics.isDataAvailable()) { - return; - } - - ImGui::PushID(3); - if (ImGui::BeginTable( - "##throughput", - 2, - ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_BordersOuterV, - ImVec2(-1, 0))) { - - ImGui::TableSetupColumn("##"); - ImGui::TableSetupColumn("##", ImGuiTableColumnFlags_WidthFixed, scale(RightColumnWidthTable)); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - AlienGui::Text(StringHelper::format(_tableLiveStatistics.getCreatedCellsPerSecond())); - - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Created cells / sec"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - AlienGui::Text(StringHelper::format(_tableLiveStatistics.getCreatedReplicatorsPerSecond())); - - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Created self-replicators / sec"); - - ImGui::EndTable(); - } - ImGui::PopID(); -} - -void StatisticsWindow::processSettings() -{ - ImGui::Spacing(); - ImGui::Spacing(); - if (_settingsOpen) { - AlienGui::MovableHorizontalSeparator(AlienGui::MovableHorizontalSeparatorParameters().additive(false), _settingsHeight); - } - - _settingsOpen = AlienGui::BeginTreeNode(AlienGui::TreeNodeParameters().name("Settings").rank(AlienGui::TreeNodeRank::High).defaultOpen(_settingsOpen)); - if (_settingsOpen) { - if (ImGui::BeginChild("##addons", {scale(0), 0})) { - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - scale(RightColumnWidth)); - if (_plotMode == 0) { - AlienGui::SliderFloat( - AlienGui::SliderFloatParameters() - .name("Time horizon") - .min(1.0f) - .max(TimelineLiveStatistics::MaxLiveHistory) - .format("%.1f s") - .textWidth(RightColumnWidth), - &_timeHorizonForLiveStatistics); - } - if (_plotMode == 1) { - AlienGui::SliderFloat( - AlienGui::SliderFloatParameters().name("Time horizon").min(1.0f).max(100.0f).format("%.0f percent").textWidth(RightColumnWidth), - &_timeHorizonForLongtermStatistics); - } - - AlienGui::SliderFloat( - AlienGui::SliderFloatParameters().name("Plot height").min(MinPlotHeight).max(1000.0f).format("%.0f").textWidth(RightColumnWidth), &_plotHeight); - AlienGui::Switcher(AlienGui::SwitcherParameters().name("Scale").textWidth(RightColumnWidth).values({"Linear", "Logarithmic"}), &_plotScale); - } - ImGui::EndChild(); - } - AlienGui::EndTreeNode(); -} - -void StatisticsWindow::processTimelineStatistics() -{ - ImGui::Spacing(); - AlienGui::Group(AlienGui::GroupParameters().text("Time step data")); - ImGui::PushID(1); - int row = 0; - if (ImGui::BeginTable("##", 2, ImGuiTableFlags_BordersInnerH, ImVec2(-1, 0))) { - ImGui::TableSetupColumn("##"); - ImGui::TableSetupColumn("##", ImGuiTableColumnFlags_WidthFixed, scale(RightColumnWidthTimeline)); - - ImPlot::PushColormap(ImPlotColormap_Cool); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numObjects); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Objects"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numEnergyParticles); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Energy particles"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::totalEnergy); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Contained energy"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numSelfReplicators); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Self-replicators"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numColonies); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Diversity"); - ImGui::SameLine(); - AlienGui::HelpMarker("The number of colonies is displayed. A colony is a set of at least 20 same mutants."); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::averageGenomeCells, 2); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Num genotype\ncells average"); - ImGui::SameLine(); - AlienGui::HelpMarker("The average number of encoded cells in the genomes is displayed."); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::averageNumCells, 2); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Genome complexity\naverage"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::varianceNumCells, 2); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Genome complexity\nvariance"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::maxNumCellsOfColonies, 2); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Genome complexity\nmaximum"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numViruses); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Viruses"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numFreeCells); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Free cells"); - - ImPlot::PopColormap(); - - ImGui::EndTable(); - } - ImGui::PopID(); - - ImGui::Spacing(); - AlienGui::Group(AlienGui::GroupParameters().text("Processes per time step and active cell")); - ImGui::PushID(2); - if (ImGui::BeginTable("##", 2, ImGuiTableFlags_BordersInnerH, ImVec2(-1, 0))) { - ImGui::TableSetupColumn("##"); - ImGui::TableSetupColumn("##", ImGuiTableColumnFlags_WidthFixed, scale(RightColumnWidthTimeline)); - ImPlot::PushColormap(ImPlotColormap_Cool); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numCreatedCells, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Created cells"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numAttacks, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Attacks"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numMuscleActivities, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Muscle activities"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numDepotActivities, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Transmitter activities"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numDefenderActivities, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Defender activities"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numGeneratorPulses, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Generator pulses"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numNeuronActivities, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Neural activities"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numSensorActivities, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Sensor activities"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numSensorMatches, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Sensor matches"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numInjectionActivities, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Injection activities"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numCompletedInjections, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Completed injections"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numReconnectorCreated, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Reconnector creations"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numReconnectorRemoved, 6); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Reconnector deletions"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - processPlot(row++, &DataPointCollection::numDetonations, 8); - ImGui::TableSetColumnIndex(1); - AlienGui::Text("Detonations"); - - ImPlot::PopColormap(); - ImGui::EndTable(); - } - ImGui::PopID(); -} - -void StatisticsWindow::processPlot(int row, DataPoint DataPointCollection::*valuesPtr, int fracPartDecimals) -{ - auto isCollapsed = _collapsedPlotIndices.contains(row); - ImGui::PushID(row); - if (AlienGui::CollapseButton(isCollapsed)) { - if (isCollapsed) { - _collapsedPlotIndices.erase(row); - } else { - _collapsedPlotIndices.insert(row); - } - } - ImGui::PopID(); - ImGui::SameLine(); - - auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); - - std::lock_guard lock(statisticsHistory.getMutex()); - auto longtermStatistics = &statisticsHistory.getDataRef(); - - //create dummy history if empty - std::vector dummy = {DataPointCollection()}; - if (longtermStatistics->empty()) { - longtermStatistics = &dummy; - } - - auto const& dataPointCollectionHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); - auto count = _plotMode == 0 ? toInt(dataPointCollectionHistory.size()) : toInt(longtermStatistics->size()); - auto startTime = _plotMode == 0 ? dataPointCollectionHistory.back().time - toDouble(_timeHorizonForLiveStatistics) - : longtermStatistics->back().time - - (longtermStatistics->back().time - longtermStatistics->front().time) * toDouble(_timeHorizonForLongtermStatistics) / 100; - auto endTime = _plotMode == 0 ? dataPointCollectionHistory.back().time : longtermStatistics->back().time; - auto values = _plotMode == 0 ? &(dataPointCollectionHistory[0].*valuesPtr) : &((*longtermStatistics)[0].*valuesPtr); - auto timePoints = _plotMode == 0 ? &dataPointCollectionHistory[0].time : &(*longtermStatistics)[0].time; - auto systemClock = _plotMode == 0 ? nullptr : &(*longtermStatistics)[0].systemClock; - - switch (_plotType) { - case 0: - plotSumColorsIntern(row, values, timePoints, systemClock, count, startTime, endTime, fracPartDecimals); - break; - case 1: - plotByColorIntern(row, values, timePoints, count, startTime, endTime, fracPartDecimals); - break; - default: - plotForColorIntern(row, values, _plotType - 2, timePoints, systemClock, count, startTime, endTime, fracPartDecimals); - break; - } - ImGui::Spacing(); -} - -void StatisticsWindow::processBackground() -{ - auto timepoint = std::chrono::steady_clock::now(); - auto duration = - _lastTimepoint.has_value() ? static_cast(std::chrono::duration_cast(timepoint - *_lastTimepoint).count()) : 0; - if (!_lastTimepoint || duration > LiveStatisticsDeltaTime) { - _lastTimepoint = timepoint; - auto rawStatistics = _SimulationFacade::get()->getStatisticsRawData(); - _histogramLiveStatistics.update(rawStatistics.histogram); - _timelineLiveStatistics.update(rawStatistics.timeline, _SimulationFacade::get()->getCurrentTimestep()); - _tableLiveStatistics.update(rawStatistics.timeline); - } -} - -namespace -{ - double getMaxWithDataPointStride(double const* data, double const* timePoints, double startTime, int count) - { - auto constexpr strideDouble = sizeof(DataPointCollection) / sizeof(double); - - auto result = 0.0; - for (int i = count / 20; i < count; ++i) { - if (timePoints[i * strideDouble] >= startTime - NEAR_ZERO) { - result = std::max(result, data[i * strideDouble]); - } - } - return result; - } -} - -void StatisticsWindow::plotSumColorsIntern( - int row, - DataPoint const* dataPoints, - double const* timePoints, - double const* systemClock, - int count, - double startTime, - double endTime, - int fracPartDecimals) -{ - auto constexpr strideBytes = sizeof(DataPointCollection); - auto constexpr strideDouble = sizeof(DataPointCollection) / sizeof(double); - - double const* plotDataY = reinterpret_cast(dataPoints) + MAX_COLORS; - double upperBound = getMaxWithDataPointStride(plotDataY, timePoints, startTime, count); - double endValue = count > 0 ? plotDataY[(count - 1) * strideDouble] : 0.0; - upperBound = getUpperBound(upperBound); - - ImGui::PushID(row); - ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0.3f, 0.3f, 0.3f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); - ImPlot::SetNextAxesLimits(startTime, endTime, 0, upperBound, ImGuiCond_Always); - - if (ImPlot::BeginPlot("##", ImVec2(-1, scale(calcPlotHeight(row))), ImPlotFlags_NoMouseText)) { - ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_NoTickLabels); - ImPlot::SetupAxis(ImAxis_Y1, "", ImPlotAxisFlags_NoTickLabels); - ImPlot::SetupAxisFormat(ImAxis_X1, ""); - ImPlot::SetupAxisFormat(ImAxis_Y1, ""); - setPlotScale(); - auto color = ImPlot::GetColormapColor((row % 21) <= 10 ? (row % 21) : 20 - (row % 21)); - if (ImGui::GetStyle().Alpha == 1.0f) { - ImPlot::Annotation( - endTime, - endValue, - ImPlot::GetLastItemColor(), - ImVec2(-10.0f, 15.0f), - true, - "%s", - StringHelper::format(toFloat(endValue), fracPartDecimals).c_str()); - } - if (count > 0) { - ImPlot::PushStyleColor(ImPlotCol_Line, color); - ImPlot::PlotLine("##", timePoints, plotDataY, count, 0, 0, strideBytes); - ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.5f * ImGui::GetStyle().Alpha); - ImPlot::PlotShaded("##", timePoints, plotDataY, count, 0, 0, 0, strideBytes); - ImPlot::PopStyleVar(); - ImPlot::PopStyleColor(); - } - if (ImGui::GetStyle().Alpha == 1.0f && ImPlot::IsPlotHovered() && count > 0) { - drawValuesAtMouseCursor(plotDataY, timePoints, systemClock, count, startTime, endTime, upperBound, fracPartDecimals); - } - ImPlot::EndPlot(); - } - ImPlot::PopStyleVar(); - ImPlot::PopStyleColor(3); - ImGui::PopID(); -} - -void StatisticsWindow::plotByColorIntern( - int row, - DataPoint const* values, - double const* timePoints, - int count, - double startTime, - double endTime, - int fracPartDecimals) -{ - auto const& customizationColors = _SimulationFacade::get()->getSimulationParameters().customizationColors.value; - auto upperBound = 0.0; - for (int i = 0; i < MAX_COLORS; ++i) { - upperBound = std::max(upperBound, getMaxWithDataPointStride(reinterpret_cast(values) + i, timePoints, startTime, count)); - } - upperBound = getUpperBound(upperBound); - - ImGui::PushID(row); - ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0.3f, 0.3f, 0.3f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); - ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 1.5f); - ImPlot::SetNextAxesLimits(startTime, endTime, 0, upperBound, ImGuiCond_Always); - - auto isCollapsed = _collapsedPlotIndices.contains(row); - auto flags = _plotHeight > 159.0f && !isCollapsed ? ImPlotFlags_None : ImPlotFlags_NoLegend; - if (ImPlot::BeginPlot("##", ImVec2(-1, scale(calcPlotHeight(row))), flags)) { - ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_NoTickLabels); - ImPlot::SetupAxis(ImAxis_Y1, "", ImPlotAxisFlags_NoTickLabels); - ImPlot::SetupAxisFormat(ImAxis_X1, ""); - ImPlot::SetupAxisFormat(ImAxis_Y1, ""); - setPlotScale(); - for (int i = 0; i < MAX_COLORS; ++i) { - ImGui::PushID(i); - auto colorRaw = customizationColors.values[i].toRgbColor(); - ImColor color(toInt((colorRaw >> 16) & 0xff), toInt((colorRaw >> 8) & 0xff), toInt(colorRaw & 0xff)); - - ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); - auto endValue = count > 0 ? *(reinterpret_cast(reinterpret_cast(values) + (count - 1)) + i) : 0.0f; - auto labelId = StringHelper::format(toFloat(endValue), fracPartDecimals); - ImPlot::PlotLine(labelId.c_str(), timePoints, reinterpret_cast(values) + i, count, 0, 0, sizeof(DataPointCollection)); - ImPlot::PopStyleColor(); - ImGui::PopID(); - } - - ImPlot::EndPlot(); - } - ImPlot::PopStyleVar(2); - ImPlot::PopStyleColor(3); - ImGui::PopID(); -} - -void StatisticsWindow::plotForColorIntern( - int row, - DataPoint const* values, - int colorIndex, - double const* timePoints, - double const* systemClock, - int count, - double startTime, - double endTime, - int fracPartDecimals) -{ - auto constexpr strideBytes = sizeof(DataPointCollection); - auto constexpr strideDouble = sizeof(DataPointCollection) / sizeof(double); - auto const& customizationColors = _SimulationFacade::get()->getSimulationParameters().customizationColors.value; - - auto valuesForColor = reinterpret_cast(values) + colorIndex; - auto upperBound = getMaxWithDataPointStride(valuesForColor, timePoints, startTime, count); - upperBound = getUpperBound(upperBound); - auto endValue = count > 0 ? valuesForColor[(count - 1) * strideDouble] : 0.0; - - ImGui::PushID(row); - ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0.0f, 0.0f, 0.0f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0.3f, 0.3f, 0.3f, ImGui::GetStyle().Alpha)); - ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); - ImPlot::SetNextAxesLimits(startTime, endTime, 0, upperBound, ImGuiCond_Always); - if (ImPlot::BeginPlot("##", ImVec2(-1, scale(calcPlotHeight(row))))) { - ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_NoTickLabels); - ImPlot::SetupAxis(ImAxis_Y1, "", ImPlotAxisFlags_NoTickLabels); - ImPlot::SetupAxisFormat(ImAxis_X1, ""); - ImPlot::SetupAxisFormat(ImAxis_Y1, ""); - setPlotScale(); - - float h, s, v; - AlienGui::ConvertRGBtoHSV(customizationColors.values[colorIndex].toRgbColor(), h, s, v); - auto color = static_cast(ImColor::HSV(h, s, v)); - if (ImGui::GetStyle().Alpha == 1.0f) { - ImPlot::Annotation( - endTime, - endValue, - ImPlot::GetLastItemColor(), - ImVec2(-10.0f, 10.0f), - true, - "%s", - StringHelper::format(toFloat(endValue), fracPartDecimals).c_str()); - } - if (count > 0) { - ImPlot::PushStyleColor(ImPlotCol_Line, color); - ImPlot::PlotLine("##", timePoints, valuesForColor, count, 0, 0, strideBytes); - ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.5f * ImGui::GetStyle().Alpha); - ImPlot::PlotShaded("##", timePoints, valuesForColor, count, 0, 0, 0, strideBytes); - ImPlot::PopStyleVar(); - ImPlot::PopStyleColor(); - if (ImGui::GetStyle().Alpha == 1.0f && ImPlot::IsPlotHovered()) { - drawValuesAtMouseCursor(valuesForColor, timePoints, systemClock, count, startTime, endTime, upperBound, fracPartDecimals); - } - } - ImPlot::EndPlot(); - } - ImPlot::PopStyleVar(); - ImPlot::PopStyleColor(3); - ImGui::PopID(); -} - -void StatisticsWindow::setPlotScale() -{ - if (_plotScale == PlotScale_Linear) { - ImPlot::SetupAxisScale(ImAxis_Y1, ImPlotScale_Linear); - return; - } - if (_plotScale == PlotScale_Logarithmic) { - ImPlot::SetupAxisScale( - ImAxis_Y1, - [](double value, void* user_data) { return log(value * 1000 + 1.0) / log(2.0); }, - [](double value, void* user_data) { return (pow(2.0f, value) - 1.0) / 1000; }); - return; - } - THROW_NOT_IMPLEMENTED(); -} - -double StatisticsWindow::getUpperBound(double maxValue) -{ - if (_plotScale == PlotScale_Linear) { - return maxValue * 1.5; - } - if (_plotScale == PlotScale_Logarithmic) { - if (maxValue > 10) { - return maxValue * pow(maxValue, 0.5); - } - return maxValue * 1.5; - } - THROW_NOT_IMPLEMENTED(); -} - -namespace -{ - std::string convertSystemClockToString(double systemClock) - { - auto time_t = static_cast(systemClock); - std::tm* tm = std::localtime(&time_t); - - char buffer[100]; - std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm); - return std::string(buffer); - } -} - -void StatisticsWindow::drawValuesAtMouseCursor( - double const* dataPoints, - double const* timePoints, - double const* systemClock, - int count, - double startTime, - double endTime, - double upperBound, - int fracPartDecimals) -{ - auto constexpr stride = sizeof(DataPointCollection) / sizeof(double); - - auto mousePos = ImPlot::GetPlotMousePos(); - mousePos.x = std::max(startTime, std::min(endTime, mousePos.x)); - mousePos.y = dataPoints[0]; - - auto dateTimeString = [&] { - if (systemClock == nullptr) { - for (int i = 1; i < count; ++i) { - if (timePoints[i * stride] > mousePos.x) { - mousePos.y = dataPoints[i * stride]; - break; - } - } - return std::string(); - } - auto systemClockEntry = systemClock[0]; - for (int i = 1; i < count; ++i) { - if (timePoints[i * stride] > mousePos.x) { - mousePos.y = dataPoints[i * stride]; - systemClockEntry = systemClock[i * stride]; - break; - } - } - mousePos.y = std::max(0.0, std::min(upperBound, mousePos.y)); - - return systemClockEntry != 0 ? convertSystemClockToString(systemClockEntry) : std::string("-"); - }(); - - ImPlot::PushStyleColor(ImPlotCol_InlayText, ImColor::HSV(0.0f, 0.0f, 1.0f).Value); - ImPlot::PlotText(ICON_FA_GENDERLESS, mousePos.x, mousePos.y, {scale(1.0f), scale(2.0f)}); - ImPlot::PopStyleColor(); - - ImPlot::PushStyleColor(ImPlotCol_Line, ImColor::HSV(0.0f, 0.0f, 1.0f).Value); - ImPlot::PlotInfLines("", &mousePos.x, 1); - ImPlot::PopStyleColor(); - - char label[256]; - auto leftSideFactor = mousePos.x > (startTime + endTime) / 2 ? -1.0f : 1.0f; - if (!dateTimeString.empty()) { - snprintf( - label, - sizeof(label), - "Time step: %s\nTimestamp: %s\nValue: %s", - StringHelper::format(mousePos.x, 0).c_str(), - dateTimeString.c_str(), - StringHelper::format(mousePos.y, fracPartDecimals).c_str()); - } else { - snprintf( - label, - sizeof(label), - "Relative time: %s\nValue: %s", - StringHelper::format(mousePos.x, 0).c_str(), - StringHelper::format(mousePos.y, fracPartDecimals).c_str()); - } - ImPlot::PlotText(label, mousePos.x, upperBound, {leftSideFactor * (scale(5.0f) + ImGui::CalcTextSize(label).x / 2), scale(28.0f)}); -} - -void StatisticsWindow::validateAndCorrect() -{ - _timeHorizonForLiveStatistics = std::max(1.0f, std::min(TimelineLiveStatistics::MaxLiveHistory, _timeHorizonForLiveStatistics)); - _timeHorizonForLongtermStatistics = std::max(1.0f, std::min(100.0f, _timeHorizonForLongtermStatistics)); - _plotType = std::clamp(_plotType, static_cast(PlotType_Accumulated), PlotType_Color0 + MAX_COLORS - 1); -} - -float StatisticsWindow::calcPlotHeight(int row) const -{ - auto isCollapsed = _collapsedPlotIndices.contains(row); - return isCollapsed ? 25.0f : _plotHeight; -} diff --git a/source/Gui/StatisticsWindow.h b/source/Gui/StatisticsWindow.h deleted file mode 100644 index 37713f0681..0000000000 --- a/source/Gui/StatisticsWindow.h +++ /dev/null @@ -1,121 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include - -#include "AlienWindow.h" -#include "Definitions.h" -#include "HistogramLiveStatistics.h" -#include "TableLiveStatistics.h" -#include "TimelineLiveStatistics.h" - -struct ImPlotPoint; - -class StatisticsWindow : public AlienWindow -{ - MAKE_SINGLETON_NO_DEFAULT_CONSTRUCTION(StatisticsWindow); - -private: - StatisticsWindow(); - - void initIntern() override; - void shutdownIntern() override; - void processIntern() override; - - void processTimelinesTab(); - void processHistogramsTab(); - void processTablesTab(); - void processSettings(); - - void processTimelineStatistics(); - - void processPlot(int row, DataPoint DataPointCollection::*valuesPtr, int fracPartDecimals = 0); - - void processBackground() override; - - void plotSumColorsIntern( - int row, - DataPoint const* dataPoints, - double const* timePoints, - double const* systemClock, - int count, - double startTime, - double endTime, - int fracPartDecimals); - void plotByColorIntern(int row, DataPoint const* values, double const* timePoints, int count, double startTime, double endTime, int fracPartDecimals); - void plotForColorIntern( - int row, - DataPoint const* values, - int colorIndex, - double const* timePoints, - double const* systemClock, - int count, - double startTime, - double endTime, - int fracPartDecimals); - - void setPlotScale(); - double getUpperBound(double maxValue); - - void drawValuesAtMouseCursor( - double const* dataPoints, - double const* timePoints, - double const* systemClock, - int count, - double startTime, - double endTime, - double upperBound, - int fracPartDecimals); - - void validateAndCorrect(); - - float calcPlotHeight(int row) const; - - std::string _startingPath; - - bool _settingsOpen = false; - float _settingsHeight = 0; - - using PlotScale = int; - enum PlotScale_ - { - PlotScale_Linear, - PlotScale_Logarithmic, - }; - PlotScale _plotScale = PlotScale_Linear; - - using PlotType = int; - enum PlotType_ - { - PlotType_Accumulated, - PlotType_ByColor, - PlotType_Color0 - }; - PlotType _plotType = PlotType_Accumulated; - - using PlotMode = int; - enum PlotMode_ - { - PlotMode_RealTime, - PlotMode_EntireHistory - }; - PlotMode _plotMode = PlotMode_RealTime; - - static auto constexpr MinPlotHeight = 80.0f; - float _plotHeight = MinPlotHeight; - - std::optional _histogramUpperBound; - std::map> _cachedTimelines; - std::unordered_set _collapsedPlotIndices; - - float _timeHorizonForLiveStatistics = 10.0f; //in seconds - float _timeHorizonForLongtermStatistics = 100.0f; //in percent - std::optional _lastTimepoint; - TimelineLiveStatistics _timelineLiveStatistics; - HistogramLiveStatistics _histogramLiveStatistics; - TableLiveStatistics _tableLiveStatistics; -}; diff --git a/source/Gui/TableLiveStatistics.cpp b/source/Gui/TableLiveStatistics.cpp deleted file mode 100644 index 9c17497953..0000000000 --- a/source/Gui/TableLiveStatistics.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "TableLiveStatistics.h" - -namespace -{ - auto constexpr TimeInterval = 5000; //in millisec - - uint64_t sum(ColorVector const& valueByColor) - { - uint64_t result = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - result += valueByColor[i]; - } - return result; - } -} - -bool TableLiveStatistics::isDataAvailable() const -{ - return _currentData.has_value(); -} - -float TableLiveStatistics::getCreatedCellsPerSecond() const -{ - if (!_lastDataTimepoint.has_value()) { - return 0; - } - return calcObjectsPerSecond(_lastData->accumulated.numCreatedCells, _currentData->accumulated.numCreatedCells); -} - -float TableLiveStatistics::getCreatedReplicatorsPerSecond() const -{ - return calcObjectsPerSecond(_lastData->accumulated.numCreatedReplicators, _currentData->accumulated.numCreatedReplicators); -} - -void TableLiveStatistics::update(TimelineStatistics const& data) -{ - auto timepoint = std::chrono::steady_clock::now(); - - if (!_currentDataTimepoint.has_value()) { - _currentData = data; - _currentDataTimepoint = timepoint; - return; - } - - auto duration = static_cast(std::chrono::duration_cast(timepoint - *_currentDataTimepoint).count()); - if (duration > TimeInterval) { - _lastData = _currentData; - _lastDataTimepoint = _currentDataTimepoint; - - _currentData = data; - _currentDataTimepoint = timepoint; - } -} - -float TableLiveStatistics::calcObjectsPerSecond(ColorVector const& lastCount, ColorVector const& currentCount) const -{ - auto currentCreatedObjects = sum(currentCount); - auto lastCreatedObjects = sum(lastCount); - return (toFloat(currentCreatedObjects) - toFloat(lastCreatedObjects)) - / toFloat(std::chrono::duration_cast(*_currentDataTimepoint - *_lastDataTimepoint).count()) * TimeInterval; -} diff --git a/source/Gui/TableLiveStatistics.h b/source/Gui/TableLiveStatistics.h deleted file mode 100644 index 7346bbca80..0000000000 --- a/source/Gui/TableLiveStatistics.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include - -#include "Definitions.h" - -class TableLiveStatistics -{ -public: - bool isDataAvailable() const; - - float getCreatedCellsPerSecond() const; - float getCreatedReplicatorsPerSecond() const; - - void update(TimelineStatistics const& data); - -private: - float calcObjectsPerSecond(ColorVector const& lastCount, ColorVector const& currentCount) const; - - std::optional _currentData; - std::optional _currentDataTimepoint; - - std::optional _lastData; - std::optional _lastDataTimepoint; -}; diff --git a/source/Gui/TemporalControlWindow.cpp b/source/Gui/TemporalControlWindow.cpp index 5845cfbea7..9e14e38e79 100644 --- a/source/Gui/TemporalControlWindow.cpp +++ b/source/Gui/TemporalControlWindow.cpp @@ -13,7 +13,6 @@ #include "AlienGui.h" #include "DelayedExecutionController.h" #include "OverlayController.h" -#include "StatisticsWindow.h" #include "StyleRepository.h" #include @@ -74,34 +73,34 @@ void TemporalControlWindow::processIntern() void TemporalControlWindow::processTpsInfo() { + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Time steps per second"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value /*0xffa07050*/); ImGui::TextUnformatted(StringHelper::format(_SimulationFacade::get()->getTps(), 1).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); } void TemporalControlWindow::processTotalTimestepsInfo() { + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Total time steps"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::TextUnformatted(StringHelper::format(_SimulationFacade::get()->getCurrentTimestep()).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); } void TemporalControlWindow::processRealTimeInfo() { + ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::Text("Real-time"); + ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - ImGui::PushStyleColor(ImGuiCol_Text, Const::TextDecentColor.Value); ImGui::TextUnformatted(StringHelper::format(_SimulationFacade::get()->getRealTime()).c_str()); - ImGui::PopStyleColor(); ImGui::PopFont(); } @@ -252,11 +251,6 @@ void TemporalControlWindow::applySnapshot(Snapshot const& snapshot) } parameters.externalEnergy = origParameters.externalEnergy; - if (parameters.maxCellAgeBalancerInterval.enabled || origParameters.maxCellAgeBalancerInterval.enabled) { - for (int i = 0; i < MAX_COLORS; ++i) { - parameters.maxCellAge.value[i] = origParameters.maxCellAge.value[i]; - } - } auto simRunning = _SimulationFacade::get()->isSimulationRunning(); if (simRunning) { _SimulationFacade::get()->pauseSimulation(); diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index 75995636ce..4dc1222d52 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -7,14 +7,23 @@ #include #include -#include + +namespace +{ + auto constexpr MaxSampleCount = 20000; //hard cap to bound memory when the simulation stalls +} std::vector const& TimelineLiveStatistics::getDataPointCollectionHistory() const { return _dataPointCollectionHistory; } -void TimelineLiveStatistics::update(TimelineStatistics const& data, uint64_t timestep) +void TimelineLiveStatistics::clear() +{ + _dataPointCollectionHistory.clear(); +} + +void TimelineLiveStatistics::update(StatisticsEntry const& overallStatistics, uint64_t timestep) { truncate(); @@ -24,16 +33,25 @@ void TimelineLiveStatistics::update(TimelineStatistics const& data, uint64_t tim _timeSinceSimStart += toDouble(duration) / 1000; - auto newDataPoint = StatisticsConverterService::get().convert(data, timestep, _timeSinceSimStart, _lastData, _lastTimestep); + auto newDataPoint = StatisticsConverterService::get().convert(overallStatistics, timestep, _timeSinceSimStart); _dataPointCollectionHistory.emplace_back(newDataPoint); - _lastData = data; _lastTimestep = timestep; _lastTimepoint = timepoint; } void TimelineLiveStatistics::truncate() { - if (!_dataPointCollectionHistory.empty() && _dataPointCollectionHistory.back().time - _dataPointCollectionHistory.front().time > (MaxLiveHistory + 1.0)) { + //keep enough history to cover both the real-time window (time-based) and the "Last time steps" window + //(step-based); a sample is only dropped once it is no longer needed by either, with a hard count cap + //to bound memory if the simulation stalls (timestep barely advancing over real time) + while (_dataPointCollectionHistory.size() > 1) { + auto const& front = _dataPointCollectionHistory.front(); + auto const& back = _dataPointCollectionHistory.back(); + auto exceedsSampleCount = toInt(_dataPointCollectionHistory.size()) > MaxSampleCount; + auto exceedsTimeAndStepWindow = back.time - front.time > (MaxLiveHistory + 1.0) && back.timestep - front.timestep > MaxLiveSteps; + if (!exceedsSampleCount && !exceedsTimeAndStepWindow) { + break; + } _dataPointCollectionHistory.erase(_dataPointCollectionHistory.begin()); } } diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index a63fe7ec36..1fff81624a 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -4,17 +4,20 @@ #include #include -#include #include -#include +#include class TimelineLiveStatistics { public: static auto constexpr MaxLiveHistory = 240.0f; //in seconds + static auto constexpr MaxLiveSteps = 100000; //max span retained for "Last time steps" mode std::vector const& getDataPointCollectionHistory() const; - void update(TimelineStatistics const& statistics, uint64_t timestep); + void update(StatisticsEntry const& overallStatistics, uint64_t timestep); + + //discards the accumulated history, e.g. after a different simulation has been loaded + void clear(); private: void truncate(); @@ -25,5 +28,4 @@ class TimelineLiveStatistics std::optional _lastTimestep; std::optional _lastTimepoint; - std::optional _lastData; }; diff --git a/source/PersisterImpl/PersisterWorker.cpp b/source/PersisterImpl/PersisterWorker.cpp index 357ceb8087..9079086431 100644 --- a/source/PersisterImpl/PersisterWorker.cpp +++ b/source/PersisterImpl/PersisterWorker.cpp @@ -663,8 +663,7 @@ _PersisterWorker::PersisterRequestResultOrError _PersisterWorker::processRequest .filename = filename, .projectName = deserializedData.auxiliaryData.simulationParameters.projectName.value, .timestep = deserializedData.auxiliaryData.timestep, - .timestamp = requestData.sharedDeserializedSimulation->getTimestamp(), - .statisticsRawData = requestData.sharedDeserializedSimulation->getStatisticsRawData()}); + .timestamp = requestData.sharedDeserializedSimulation->getTimestamp()}); if (requestData.resetDeserializedSimulation) { requestData.sharedDeserializedSimulation->reset(); diff --git a/source/PersisterInterface/SaveDeserializedSimulationResultData.h b/source/PersisterInterface/SaveDeserializedSimulationResultData.h index 1562479aae..953957d7d9 100644 --- a/source/PersisterInterface/SaveDeserializedSimulationResultData.h +++ b/source/PersisterInterface/SaveDeserializedSimulationResultData.h @@ -9,5 +9,4 @@ struct SaveDeserializedSimulationResultData std::string projectName; uint64_t timestep = 0; std::chrono::system_clock::time_point timestamp; - StatisticsRawData statisticsRawData; }; diff --git a/source/PersisterInterface/SerializerService.cpp b/source/PersisterInterface/SerializerService.cpp index 1792743919..157bb14eb8 100644 --- a/source/PersisterInterface/SerializerService.cpp +++ b/source/PersisterInterface/SerializerService.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -1127,6 +1128,7 @@ namespace auto constexpr Id_Creature_MutationState = 7; auto constexpr Id_Creature_LineageId = 3; auto constexpr Id_Creature_AccumulatedMutations = 9; + auto constexpr Id_Creature_AccumulatedMutationsInLineage = 10; auto constexpr Id_Solid_Energy = 0; @@ -1856,6 +1858,7 @@ namespace cereal scope.addMember(Id_Creature_MutationState, data._mutationState, defaultObject._mutationState); scope.addMember(Id_Creature_LineageId, data._lineageId, defaultObject._lineageId); scope.addMember(Id_Creature_AccumulatedMutations, data._accumulatedMutations, defaultObject._accumulatedMutations); + scope.addMember(Id_Creature_AccumulatedMutationsInLineage, data._accumulatedMutationsInLineage, defaultObject._accumulatedMutationsInLineage); } SPLIT_SERIALIZATION(CreatureDesc) @@ -1896,7 +1899,7 @@ bool SerializerService::serializeSimulationToFiles(std::filesystem::path const& std::filesystem::path settingsFilename(filename); settingsFilename.replace_extension(std::filesystem::path(".settings.json")); std::filesystem::path statisticsFilename(filename); - statisticsFilename.replace_extension(std::filesystem::path(".statistics.csv")); + statisticsFilename.replace_extension(std::filesystem::path(".statistics.bin")); { zstr::ofstream stream(filename.string(), std::ios::binary); @@ -1932,7 +1935,7 @@ bool SerializerService::deserializeSimulationFromFiles(DeserializedSimulation& d std::filesystem::path settingsFilename(filename); settingsFilename.replace_extension(std::filesystem::path(".settings.json")); std::filesystem::path statisticsFilename(filename); - statisticsFilename.replace_extension(std::filesystem::path(".statistics.csv")); + statisticsFilename.replace_extension(std::filesystem::path(".statistics.bin")); if (!deserializeDescription(data.mainData, filename)) { return false; @@ -1964,7 +1967,9 @@ bool SerializerService::deleteSimulation(std::filesystem::path const& filename) std::filesystem::path settingsFilename(filename); settingsFilename.replace_extension(std::filesystem::path(".settings.json")); std::filesystem::path statisticsFilename(filename); - statisticsFilename.replace_extension(std::filesystem::path(".statistics.csv")); + statisticsFilename.replace_extension(std::filesystem::path(".statistics.bin")); + std::filesystem::path legacyStatisticsFilename(filename); + legacyStatisticsFilename.replace_extension(std::filesystem::path(".statistics.csv")); if (!std::filesystem::remove(filename)) { return false; @@ -1972,9 +1977,10 @@ bool SerializerService::deleteSimulation(std::filesystem::path const& filename) if (!std::filesystem::remove(settingsFilename)) { return false; } - if (!std::filesystem::remove(statisticsFilename)) { - return false; - } + + //statistics files are optional + std::filesystem::remove(statisticsFilename); + std::filesystem::remove(legacyStatisticsFilename); return true; } catch (...) { return false; @@ -2249,278 +2255,302 @@ void SerializerService::deserializeSimulationParameters(SimulationParameters& pa parameters = SettingsParserService::get().decodeSimulationParameters(tree); } +/************************************************************************/ +/* Statistics history */ +/************************************************************************/ namespace { - std::string toString(double value) + auto constexpr Id_StatisticsHistory_Overall = 0; + auto constexpr Id_StatisticsHistory_Lineages = 1; + + auto constexpr Id_Timeline_Time = 0; + auto constexpr Id_Timeline_Timestep = 1; + auto constexpr Id_Timeline_SystemClock = 2; + + auto constexpr Id_OverallTimeline_NumCreatures = 3; + auto constexpr Id_OverallTimeline_AverageCreatureCells = 4; + auto constexpr Id_OverallTimeline_AverageGenomeNodes = 5; + auto constexpr Id_OverallTimeline_CreatureEnergy = 6; + auto constexpr Id_OverallTimeline_AverageMutationRate = 7; + auto constexpr Id_OverallTimeline_AverageGeneration = 8; + auto constexpr Id_OverallTimeline_NumLineages = 9; + auto constexpr Id_OverallTimeline_NumSolidObjects = 10; + auto constexpr Id_OverallTimeline_NumFluidObjects = 11; + auto constexpr Id_OverallTimeline_NumCellObjects = 12; + auto constexpr Id_OverallTimeline_NumEnergyParticles = 13; + auto constexpr Id_OverallTimeline_AccumCreatedCreatures = 14; + auto constexpr Id_OverallTimeline_AccumMutations = 15; + + auto constexpr Id_LineageTimeline_ColorBitset = 3; + auto constexpr Id_LineageTimeline_RepresentativeCellId = 4; + auto constexpr Id_LineageTimeline_NumCreatures = 5; + auto constexpr Id_LineageTimeline_NumGenomes = 6; + auto constexpr Id_LineageTimeline_SumCreatureCells = 7; + auto constexpr Id_LineageTimeline_SumCreatureGenerations = 8; + auto constexpr Id_LineageTimeline_SumGenomeNodes = 9; + auto constexpr Id_LineageTimeline_SumMutationRates = 10; + auto constexpr Id_LineageTimeline_SumCreatureEnergy = 11; + auto constexpr Id_LineageTimeline_NumCreatedCreatures = 12; + auto constexpr Id_LineageTimeline_TotalMutations = 13; + + //timelines are stored column-wise: each column is an individually tagged block so that + //added fields load as defaults from old files and unknown fields from new files are skipped + struct OverallTimeline + { + std::vector time; + std::vector timestep; + std::vector systemClock; + std::vector numCreatures; + std::vector averageCreatureCells; + std::vector averageGenomeNodes; + std::vector creatureEnergy; + std::vector averageMutationRate; + std::vector averageGeneration; + std::vector numLineages; + std::vector numSolidObjects; + std::vector numFluidObjects; + std::vector numCellObjects; + std::vector numEnergyParticles; + std::vector accumCreatedCreatures; + std::vector accumMutations; + }; + + struct LineageTimeline + { + std::vector time; + std::vector timestep; + std::vector systemClock; + std::vector colorBitset; + std::vector representativeCellId; + std::vector numCreatures; + std::vector numGenomes; + std::vector sumCreatureCells; + std::vector sumCreatureGenerations; + std::vector sumGenomeNodes; + std::vector sumMutationRates; + std::vector sumCreatureEnergy; + std::vector numCreatedCreatures; + std::vector totalMutations; + }; + + struct StatisticsTimelines { - std::stringstream ss; - ss << std::fixed << std::setprecision(9) << value; - return ss.str(); - } + OverallTimeline overall; + std::unordered_map lineages; + }; - struct ColumnDescription + struct OverallColumn { - std::string name; - bool colorDependent = false; + int id; + std::vector OverallTimeline::* column; + double OverallDataPoint::* field; + }; + std::vector const OverallColumns = { + {Id_OverallTimeline_NumCreatures, &OverallTimeline::numCreatures, &OverallDataPoint::numCreatures}, + {Id_OverallTimeline_AverageCreatureCells, &OverallTimeline::averageCreatureCells, &OverallDataPoint::averageCreatureCells}, + {Id_OverallTimeline_AverageGenomeNodes, &OverallTimeline::averageGenomeNodes, &OverallDataPoint::averageGenomeNodes}, + {Id_OverallTimeline_CreatureEnergy, &OverallTimeline::creatureEnergy, &OverallDataPoint::creatureEnergy}, + {Id_OverallTimeline_AverageMutationRate, &OverallTimeline::averageMutationRate, &OverallDataPoint::averageMutationRate}, + {Id_OverallTimeline_AverageGeneration, &OverallTimeline::averageGeneration, &OverallDataPoint::averageGeneration}, + {Id_OverallTimeline_NumLineages, &OverallTimeline::numLineages, &OverallDataPoint::numLineages}, + {Id_OverallTimeline_NumSolidObjects, &OverallTimeline::numSolidObjects, &OverallDataPoint::numSolidObjects}, + {Id_OverallTimeline_NumFluidObjects, &OverallTimeline::numFluidObjects, &OverallDataPoint::numFluidObjects}, + {Id_OverallTimeline_NumCellObjects, &OverallTimeline::numCellObjects, &OverallDataPoint::numCellObjects}, + {Id_OverallTimeline_NumEnergyParticles, &OverallTimeline::numEnergyParticles, &OverallDataPoint::numEnergyParticles}, + {Id_OverallTimeline_AccumCreatedCreatures, &OverallTimeline::accumCreatedCreatures, &OverallDataPoint::accumCreatedCreatures}, + {Id_OverallTimeline_AccumMutations, &OverallTimeline::accumMutations, &OverallDataPoint::accumMutations}, }; - std::vector const ColumnDescriptions = { - {"Time step", false}, - {"Cells", true}, - {"Self-replicators", true}, - {"Viruses", true}, - {"Free cells", true}, - {"Energy particles", true}, - {"Average genome cells", true}, - {"Total energy", true}, - {"Created cells", true}, - {"Attacks", true}, - {"Muscle activities", true}, - {"Depot activities", true}, - {"Defender activities", true}, - {"Injection activities", true}, - {"Completed injections", true}, - {"Generator pulses", true}, - {"Neuron activities", true}, - {"Sensor activities", true}, - {"Sensor matches", true}, - {"Reconnector creations", true}, - {"Reconnector deletions", true}, - {"Detonations", true}, - {"Colonies", true}, - {"Genome complexity average", true}, - {"Genome complexity Maximum", true}, - {"Genome complexity variance", true}, - {"System clock", false}}; - - std::variant getDataRef(int colIndex, DataPointCollection& dataPoints) - { - if (colIndex == 0) { - return &dataPoints.time; - } else if (colIndex == 1) { - return &dataPoints.numObjects; - } else if (colIndex == 2) { - return &dataPoints.numSelfReplicators; - } else if (colIndex == 3) { - return &dataPoints.numViruses; - } else if (colIndex == 4) { - return &dataPoints.numFreeCells; - } else if (colIndex == 5) { - return &dataPoints.numEnergyParticles; - } else if (colIndex == 6) { - return &dataPoints.averageGenomeCells; - } else if (colIndex == 7) { - return &dataPoints.totalEnergy; - } else if (colIndex == 8) { - return &dataPoints.numCreatedCells; - } else if (colIndex == 9) { - return &dataPoints.numAttacks; - } else if (colIndex == 10) { - return &dataPoints.numMuscleActivities; - } else if (colIndex == 11) { - return &dataPoints.numDefenderActivities; - } else if (colIndex == 12) { - return &dataPoints.numDepotActivities; - } else if (colIndex == 13) { - return &dataPoints.numInjectionActivities; - } else if (colIndex == 14) { - return &dataPoints.numCompletedInjections; - } else if (colIndex == 15) { - return &dataPoints.numGeneratorPulses; - } else if (colIndex == 16) { - return &dataPoints.numNeuronActivities; - } else if (colIndex == 17) { - return &dataPoints.numSensorActivities; - } else if (colIndex == 18) { - return &dataPoints.numSensorMatches; - } else if (colIndex == 19) { - return &dataPoints.numReconnectorCreated; - } else if (colIndex == 20) { - return &dataPoints.numReconnectorRemoved; - } else if (colIndex == 21) { - return &dataPoints.numDetonations; - } else if (colIndex == 22) { - return &dataPoints.numColonies; - } else if (colIndex == 23) { - return &dataPoints.averageNumCells; - } else if (colIndex == 24) { - return &dataPoints.maxNumCellsOfColonies; - } else if (colIndex == 25) { - return &dataPoints.varianceNumCells; - } else if (colIndex == 26) { - return &dataPoints.systemClock; - } - THROW_NOT_IMPLEMENTED(); - } - void load(int startIndex, std::vector& serializedData, double& value) + struct LineageColumn { - if (startIndex < serializedData.size()) { - value = std::stod(serializedData.at(startIndex)); - } - } + int id; + std::vector LineageTimeline::* column; + double LineageDataPoint::* field; + }; + std::vector const LineageColumns = { + {Id_LineageTimeline_NumCreatures, &LineageTimeline::numCreatures, &LineageDataPoint::numCreatures}, + {Id_LineageTimeline_NumGenomes, &LineageTimeline::numGenomes, &LineageDataPoint::numGenomes}, + {Id_LineageTimeline_SumCreatureCells, &LineageTimeline::sumCreatureCells, &LineageDataPoint::sumCreatureCells}, + {Id_LineageTimeline_SumCreatureGenerations, &LineageTimeline::sumCreatureGenerations, &LineageDataPoint::sumCreatureGenerations}, + {Id_LineageTimeline_SumGenomeNodes, &LineageTimeline::sumGenomeNodes, &LineageDataPoint::sumGenomeNodes}, + {Id_LineageTimeline_SumMutationRates, &LineageTimeline::sumMutationRates, &LineageDataPoint::sumMutationRates}, + {Id_LineageTimeline_SumCreatureEnergy, &LineageTimeline::sumCreatureEnergy, &LineageDataPoint::sumCreatureEnergy}, + {Id_LineageTimeline_NumCreatedCreatures, &LineageTimeline::numCreatedCreatures, &LineageDataPoint::numCreatedCreatures}, + {Id_LineageTimeline_TotalMutations, &LineageTimeline::totalMutations, &LineageDataPoint::totalMutations}, + }; - void save(std::vector& serializedData, double& value) + template + void extractTimingColumns(Timeline& timeline, std::vector const& samples) { - serializedData.emplace_back(toString(value)); + timeline.time.reserve(samples.size()); + timeline.timestep.reserve(samples.size()); + timeline.systemClock.reserve(samples.size()); + for (auto const& sample : samples) { + timeline.time.emplace_back(sample.time); + timeline.timestep.emplace_back(sample.timestep); + timeline.systemClock.emplace_back(sample.systemClock); + } } - void load(int startIndex, std::vector& serializedData, DataPoint& dataPoint) + template + std::vector createSamplesWithTiming(Timeline const& timeline) { - for (int i = 0; i < MAX_COLORS; ++i) { - auto index = startIndex + i; - if (index < serializedData.size()) { - dataPoint.values[i] = std::stod(serializedData.at(index)); - } + std::vector result(timeline.time.size()); + for (auto&& [sample, value] : std::views::zip(result, timeline.time)) { + sample.time = value; + } + for (auto&& [sample, value] : std::views::zip(result, timeline.timestep)) { + sample.timestep = value; } - if (startIndex + MAX_COLORS < serializedData.size()) { - dataPoint.summedValues = std::stod(serializedData.at(startIndex + MAX_COLORS)); + for (auto&& [sample, value] : std::views::zip(result, timeline.systemClock)) { + sample.systemClock = value; } + return result; } - void save(std::vector& serializedData, DataPoint& dataPoint) + template + void extractDataColumns(Timeline& timeline, std::vector const& samples, Columns const& columns) { - for (int i = 0; i < MAX_COLORS; ++i) { - serializedData.emplace_back(toString(dataPoint.values[i])); + for (auto const& [id, column, field] : columns) { + auto& values = timeline.*column; + values.reserve(samples.size()); + for (auto const& sample : samples) { + values.emplace_back(sample.data.*field); + } } - serializedData.emplace_back(toString(dataPoint.summedValues)); } - struct ParsedColumnInfo - { - std::string name; - std::optional colIndex; - int size = 0; - }; - void load(std::vector const& colInfos, std::vector& serializedData, DataPointCollection& dataPoints) + template + void applyDataColumns(std::vector& samples, Timeline const& timeline, Columns const& columns) { - int startIndex = 0; - for (auto const& colInfo : colInfos) { - if (!colInfo.colIndex.has_value()) { - startIndex += colInfo.size; - continue; - } - auto col = colInfo.colIndex.value(); - auto data = getDataRef(col, dataPoints); - if (std::holds_alternative(data)) { - load(startIndex, serializedData, *std::get(data)); + for (auto const& [id, column, field] : columns) { + for (auto&& [sample, value] : std::views::zip(samples, timeline.*column)) { + sample.data.*field = value; } - if (std::holds_alternative(data)) { - load(startIndex, serializedData, *std::get(data)); - } - startIndex += colInfo.size; } } - void save(std::vector& serializedData, DataPointCollection& dataPoints) + OverallTimeline toTimeline(std::vector const& samples) { - int index = 0; - for (auto const& column : ColumnDescriptions) { - auto data = getDataRef(index, dataPoints); - if (std::holds_alternative(data)) { - save(serializedData, *std::get(data)); - } - if (std::holds_alternative(data)) { - save(serializedData, *std::get(data)); - } - ++index; - } + OverallTimeline result; + extractTimingColumns(result, samples); + extractDataColumns(result, samples, OverallColumns); + return result; } - std::string getPrincipalPart(std::string const& colName) + std::vector toSamples(OverallTimeline const& timeline) { - auto colNameTrimmed = boost::algorithm::trim_left_copy(colName); - size_t pos = colNameTrimmed.find(" ("); - if (pos != std::string::npos) { - return colNameTrimmed.substr(0, pos); - } else { - return colNameTrimmed; + auto result = createSamplesWithTiming(timeline); + applyDataColumns(result, timeline, OverallColumns); + return result; + } + + LineageTimeline toTimeline(std::vector const& samples) + { + LineageTimeline result; + extractTimingColumns(result, samples); + extractDataColumns(result, samples, LineageColumns); + result.colorBitset.reserve(samples.size()); + result.representativeCellId.reserve(samples.size()); + for (auto const& sample : samples) { + result.colorBitset.emplace_back(sample.data.colorBitset); + result.representativeCellId.emplace_back(sample.data.representativeCellId); } + return result; } - std::optional getColumnIndex(std::string const& colName) + std::vector toSamples(LineageTimeline const& timeline) { - for (auto const& [index, colDescription] : ColumnDescriptions | boost::adaptors::indexed(0)) { - if (colDescription.name == colName) { - return toInt(index); - } + auto result = createSamplesWithTiming(timeline); + applyDataColumns(result, timeline, LineageColumns); + for (auto&& [sample, value] : std::views::zip(result, timeline.colorBitset)) { + sample.data.colorBitset = value; } - return std::nullopt; + for (auto&& [sample, value] : std::views::zip(result, timeline.representativeCellId)) { + sample.data.representativeCellId = value; + } + return result; } } -void SerializerService::serializeStatistics(StatisticsHistoryData const& statistics, std::ostream& stream) const +namespace cereal { - //header row - auto writeLabelAllColors = [&stream](auto const& name) { - for (int i = 0; i < MAX_COLORS; ++i) { - if (i != 0) { - stream << ","; - } - stream << name << " (color " << i << ")"; + template + void loadSave(SerializationTask task, Archive& ar, OverallTimeline& data) + { + auto scope = getSerializationScope(task, ar); + scope.addDesc(Id_Timeline_Time, data.time); + scope.addDesc(Id_Timeline_Timestep, data.timestep); + scope.addDesc(Id_Timeline_SystemClock, data.systemClock); + for (auto const& [id, column, field] : OverallColumns) { + scope.addDesc(id, data.*column); } - stream << "," << name << " (accumulated)"; - }; + } + SPLIT_SERIALIZATION(OverallTimeline) - int index = 0; - for (auto const& [colName, colorDependent] : ColumnDescriptions) { - if (index != 0) { - stream << ", "; - } - if (!colorDependent) { - stream << colName; - } else { - writeLabelAllColors(colName); + template + void loadSave(SerializationTask task, Archive& ar, LineageTimeline& data) + { + auto scope = getSerializationScope(task, ar); + scope.addDesc(Id_Timeline_Time, data.time); + scope.addDesc(Id_Timeline_Timestep, data.timestep); + scope.addDesc(Id_Timeline_SystemClock, data.systemClock); + scope.addDesc(Id_LineageTimeline_ColorBitset, data.colorBitset); + scope.addDesc(Id_LineageTimeline_RepresentativeCellId, data.representativeCellId); + for (auto const& [id, column, field] : LineageColumns) { + scope.addDesc(id, data.*column); } - ++index; } - stream << std::endl; + SPLIT_SERIALIZATION(LineageTimeline) - //content - for (auto dataPoints : statistics) { - std::vector entries; - save(entries, dataPoints); - stream << boost::join(entries, ",") << std::endl; + template + void loadSave(SerializationTask task, Archive& ar, StatisticsTimelines& data) + { + auto scope = getSerializationScope(task, ar); + scope.addDesc(Id_StatisticsHistory_Overall, data.overall); + scope.addDesc(Id_StatisticsHistory_Lineages, data.lineages); } + SPLIT_SERIALIZATION(StatisticsTimelines) } -void SerializerService::deserializeStatistics(StatisticsHistoryData& statistics, std::istream& stream) const +void SerializerService::serializeStatistics(StatisticsHistoryData const& statistics, std::ostream& stream) const { - statistics.clear(); - - std::vector> data; - - // header line - std::string header; - std::getline(stream, header); + StatisticsTimelines timelines; + timelines.overall = toTimeline(statistics.overall); + for (auto const& [lineageId, samples] : statistics.lineages) { + timelines.lineages.emplace(lineageId, toTimeline(samples)); + } - std::vector colNames; - boost::split(colNames, header, boost::is_any_of(",")); + zstr::ostream compressedStream(stream); + cereal::PortableBinaryOutputArchive archive(compressedStream); + archive(Const::ProgramVersion); + archive(timelines); + compressedStream.flush(); +} - std::vector colInfos; - for (auto const& colName : colNames) { - auto principalPart = getPrincipalPart(colName); +void SerializerService::deserializeStatistics(StatisticsHistoryData& statistics, std::istream& stream) const +{ + //the statistics history is auxiliary data: an unreadable or outdated file yields an empty history instead of failing the simulation load + statistics = StatisticsHistoryData(); + try { + zstr::istream decompressedStream(stream); + cereal::PortableBinaryInputArchive archive(decompressedStream); - if (colInfos.empty()) { - colInfos.emplace_back(principalPart, getColumnIndex(principalPart), 1); - } else { - auto& lastColInfo = colInfos.back(); - if (lastColInfo.name == principalPart) { - ++lastColInfo.size; - } else { - colInfos.emplace_back(principalPart, getColumnIndex(principalPart), 1); - } + std::string version; + archive(version); + if (!VersionParserService::get().isVersionValid(version) || VersionParserService::get().isVersionOutdated(version)) { + return; } - } - - // data lines - while (std::getline(stream, header)) { - std::vector entries; - boost::split(entries, header, boost::is_any_of(",")); - DataPointCollection dataPoints; - load(colInfos, entries, dataPoints); + StatisticsTimelines timelines; + archive(timelines); - statistics.emplace_back(dataPoints); + statistics.overall = toSamples(timelines.overall); + for (auto const& [lineageId, timeline] : timelines.lineages) { + statistics.lineages.emplace(lineageId, toSamples(timeline)); + } + } catch (...) { + statistics = StatisticsHistoryData(); } } diff --git a/source/PersisterInterface/SharedDeserializedSimulation.h b/source/PersisterInterface/SharedDeserializedSimulation.h index 8663d1bf95..3b40f534f3 100644 --- a/source/PersisterInterface/SharedDeserializedSimulation.h +++ b/source/PersisterInterface/SharedDeserializedSimulation.h @@ -8,18 +8,6 @@ class _SharedDeserializedSimulation { public: - void setLastStatisticsData(StatisticsRawData const& value) - { - std::lock_guard lock(_mutex); - _statisticsRawData = value; - } - - StatisticsRawData getStatisticsRawData() const - { - std::lock_guard lock(_mutex); - return _statisticsRawData; - } - void setDeserializedSimulation(DeserializedSimulation&& value) { std::lock_guard lock(_mutex); @@ -42,7 +30,6 @@ class _SharedDeserializedSimulation void reset() { setDeserializedSimulation(DeserializedSimulation()); - setLastStatisticsData(StatisticsRawData()); } bool isEmpty() const @@ -54,7 +41,6 @@ class _SharedDeserializedSimulation private: mutable std::mutex _mutex; DeserializedSimulation _deserializedSimulation; - StatisticsRawData _statisticsRawData; std::chrono::system_clock::time_point _timestamp; }; -using SharedDeserializedSimulation = std::shared_ptr<_SharedDeserializedSimulation>; \ No newline at end of file +using SharedDeserializedSimulation = std::shared_ptr<_SharedDeserializedSimulation>; diff --git a/source/PersisterTests/SerializerServiceTest.cpp b/source/PersisterTests/SerializerServiceTest.cpp index 3dc76539e5..fe9bd71286 100644 --- a/source/PersisterTests/SerializerServiceTest.cpp +++ b/source/PersisterTests/SerializerServiceTest.cpp @@ -1,3 +1,5 @@ +#include + #include #include @@ -31,6 +33,139 @@ class SerializerServiceTests : public ::testing::Test SerializerService* _serializerService; }; +namespace +{ + OverallSample createOverallSample(double base) + { + OverallSample result; + result.time = base; + result.timestep = base + 1; + result.systemClock = base + 2; + result.data.numCreatures = base + 3; + result.data.averageCreatureCells = base + 4; + result.data.averageGenomeNodes = base + 5; + result.data.creatureEnergy = base + 6; + result.data.averageMutationRate = base + 7; + result.data.averageGeneration = base + 8; + result.data.numLineages = base + 9; + result.data.numSolidObjects = base + 10; + result.data.numFluidObjects = base + 11; + result.data.numCellObjects = base + 12; + result.data.numEnergyParticles = base + 13; + result.data.accumCreatedCreatures = base + 14; + result.data.accumMutations = base + 15; + return result; + } + + LineageSample createLineageSample(double base) + { + LineageSample result; + result.time = base; + result.timestep = base + 1; + result.systemClock = base + 2; + result.data.colorBitset = static_cast(base) + 3; + result.data.representativeCellId = (1ull << 60) + static_cast(base); + result.data.numCreatures = base + 4; + result.data.numGenomes = base + 5; + result.data.sumCreatureCells = base + 6; + result.data.sumCreatureGenerations = base + 7; + result.data.sumGenomeNodes = base + 8; + result.data.sumMutationRates = base + 9; + result.data.sumCreatureEnergy = base + 10; + result.data.numCreatedCreatures = base + 11; + result.data.totalMutations = base + 12; + return result; + } + + void compare(OverallSample const& expected, OverallSample const& actual) + { + EXPECT_EQ(expected.time, actual.time); + EXPECT_EQ(expected.timestep, actual.timestep); + EXPECT_EQ(expected.systemClock, actual.systemClock); + EXPECT_EQ(expected.data.numCreatures, actual.data.numCreatures); + EXPECT_EQ(expected.data.averageCreatureCells, actual.data.averageCreatureCells); + EXPECT_EQ(expected.data.averageGenomeNodes, actual.data.averageGenomeNodes); + EXPECT_EQ(expected.data.creatureEnergy, actual.data.creatureEnergy); + EXPECT_EQ(expected.data.averageMutationRate, actual.data.averageMutationRate); + EXPECT_EQ(expected.data.averageGeneration, actual.data.averageGeneration); + EXPECT_EQ(expected.data.numLineages, actual.data.numLineages); + EXPECT_EQ(expected.data.numSolidObjects, actual.data.numSolidObjects); + EXPECT_EQ(expected.data.numFluidObjects, actual.data.numFluidObjects); + EXPECT_EQ(expected.data.numCellObjects, actual.data.numCellObjects); + EXPECT_EQ(expected.data.numEnergyParticles, actual.data.numEnergyParticles); + EXPECT_EQ(expected.data.accumCreatedCreatures, actual.data.accumCreatedCreatures); + EXPECT_EQ(expected.data.accumMutations, actual.data.accumMutations); + } + + void compare(LineageSample const& expected, LineageSample const& actual) + { + EXPECT_EQ(expected.time, actual.time); + EXPECT_EQ(expected.timestep, actual.timestep); + EXPECT_EQ(expected.systemClock, actual.systemClock); + EXPECT_EQ(expected.data.colorBitset, actual.data.colorBitset); + EXPECT_EQ(expected.data.representativeCellId, actual.data.representativeCellId); + EXPECT_EQ(expected.data.numCreatures, actual.data.numCreatures); + EXPECT_EQ(expected.data.numGenomes, actual.data.numGenomes); + EXPECT_EQ(expected.data.sumCreatureCells, actual.data.sumCreatureCells); + EXPECT_EQ(expected.data.sumCreatureGenerations, actual.data.sumCreatureGenerations); + EXPECT_EQ(expected.data.sumGenomeNodes, actual.data.sumGenomeNodes); + EXPECT_EQ(expected.data.sumMutationRates, actual.data.sumMutationRates); + EXPECT_EQ(expected.data.sumCreatureEnergy, actual.data.sumCreatureEnergy); + EXPECT_EQ(expected.data.numCreatedCreatures, actual.data.numCreatedCreatures); + EXPECT_EQ(expected.data.totalMutations, actual.data.totalMutations); + } + + void compare(StatisticsHistoryData const& expected, StatisticsHistoryData const& actual) + { + ASSERT_EQ(expected.overall.size(), actual.overall.size()); + for (auto const& [expectedSample, actualSample] : std::views::zip(expected.overall, actual.overall)) { + compare(expectedSample, actualSample); + } + ASSERT_EQ(expected.lineages.size(), actual.lineages.size()); + for (auto const& [lineageId, expectedSamples] : expected.lineages) { + auto actualSamplesIt = actual.lineages.find(lineageId); + ASSERT_TRUE(actualSamplesIt != actual.lineages.end()); + ASSERT_EQ(expectedSamples.size(), actualSamplesIt->second.size()); + for (auto const& [expectedSample, actualSample] : std::views::zip(expectedSamples, actualSamplesIt->second)) { + compare(expectedSample, actualSample); + } + } + } +} + +TEST_F(SerializerServiceTests, statisticsHistory) +{ + DeserializedSimulation before; + for (int i = 0; i < 5; ++i) { + before.statistics.overall.emplace_back(createOverallSample(toDouble(i) * 100)); + } + before.statistics.lineages.emplace(7, std::vector{createLineageSample(1000), createLineageSample(2000)}); + before.statistics.lineages.emplace(42, std::vector{createLineageSample(3000)}); + before.statistics.lineages.emplace(43, std::vector{}); + + SerializedSimulation serialized; + ASSERT_TRUE(_serializerService->serializeSimulationToStrings(serialized, before)); + + DeserializedSimulation after; + ASSERT_TRUE(_serializerService->deserializeSimulationFromStrings(after, serialized)); + + compare(before.statistics, after.statistics); +} + +TEST_F(SerializerServiceTests, emptyStatisticsHistory) +{ + DeserializedSimulation before; + + SerializedSimulation serialized; + ASSERT_TRUE(_serializerService->serializeSimulationToStrings(serialized, before)); + + DeserializedSimulation after; + ASSERT_TRUE(_serializerService->deserializeSimulationFromStrings(after, serialized)); + + EXPECT_TRUE(after.statistics.overall.empty()); + EXPECT_TRUE(after.statistics.lineages.empty()); +} + TEST_F(SerializerServiceTests, singleEnergyParticle) { Desc data;