From 9de5547e6d88a0e4a7f8f92f6132a809ed8dbd6f Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sat, 11 Jul 2026 12:08:26 +0200 Subject: [PATCH 01/41] Add evolution dashboard window with dummy data Co-Authored-By: Claude Fable 5 --- source/Gui/CMakeLists.txt | 2 + source/Gui/Definitions.h | 2 + source/Gui/EvolutionDashboardWindow.cpp | 559 ++++++++++++++++++++++++ source/Gui/EvolutionDashboardWindow.h | 77 ++++ source/Gui/MainLoopController.cpp | 9 + source/Gui/MainWindow.cpp | 2 + 6 files changed, 651 insertions(+) create mode 100644 source/Gui/EvolutionDashboardWindow.cpp create mode 100644 source/Gui/EvolutionDashboardWindow.h diff --git a/source/Gui/CMakeLists.txt b/source/Gui/CMakeLists.txt index bfb5def80..6feabf321 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 diff --git a/source/Gui/Definitions.h b/source/Gui/Definitions.h index f0e5deac9..99afc2811 100644 --- a/source/Gui/Definitions.h +++ b/source/Gui/Definitions.h @@ -25,6 +25,8 @@ class SimulationParametersMainWindow; class StatisticsWindow; +class EvolutionDashboardWindow; + class SimulationInteractionController; class GpuSettingsDialog; diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp new file mode 100644 index 000000000..9400f45a7 --- /dev/null +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -0,0 +1,559 @@ +#include "EvolutionDashboardWindow.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include "AlienGui.h" +#include "StyleRepository.h" + +namespace +{ + auto constexpr HeaderCardHeight = 92.0f; + auto constexpr SparklineWidth = 90.0f; + auto constexpr SparklineHeight = 26.0f; + auto constexpr ColorChipSize = 22.0f; + auto constexpr PlotLabelColumnWidth = 150.0f; + auto constexpr PlotHeight = 60.0f; + auto constexpr PlotHeightWithAxis = 80.0f; + auto constexpr DefaultTimelinesHeight = 380.0f; + auto constexpr TimeSpan = 1250000.0; + + ImColor const CardBackgroundColor = ImColor(0.095f, 0.117f, 0.165f, 1.0f); + ImColor const CardBorderColor = ImColor(0.165f, 0.196f, 0.270f, 1.0f); + ImColor const PositiveDeltaColor = ImColor(0.19f, 0.77f, 0.55f, 1.0f); + ImColor const NegativeDeltaColor = ImColor(0.94f, 0.32f, 0.32f, 1.0f); + ImColor const SumSeriesColor = ImColor(0.78f, 0.78f, 0.78f, 1.0f); + + struct MetricDef + { + char const* tableHeader; + char const* plotName; + double baseValue; + int decimals; //-1: scientific notation + }; + + MetricDef const Metrics[EvolutionDashboardWindow::NumMetrics] = { + {"Creatures", "Creatures", 1500.0, 0}, + {"Avg size", "Avg creature size", 15.0, 1}, + {"Avg cells", "Avg cells", 40.0, 1}, + {"Avg nodes", "Avg nodes", 60.0, 1}, + {"Sum energy", "Sum energy", 150000.0, 0}, + {"Avg mut. rate", "Avg mutation rate", 2.0e-4, -1}, + {"Avg age (gen)", "Avg age (generations)", 200.0, 0}, + {"Created /s", "Created creatures / s", 3.0, 1}, + {"Mutations /s", "Mutations / s", 1.0, 1}, + }; + + float const MetricDeltas[EvolutionDashboardWindow::NumMetrics] = {4.1f, 0.6f, 1.2f, 0.0f, -0.4f, -2.3f, 5.0f, 1.1f, -0.8f}; + + ImColor toImColor(uint32_t rgb, float alpha = 1.0f) + { + return ImColor(toInt((rgb >> 16) & 0xff), toInt((rgb >> 8) & 0xff), toInt(rgb & 0xff), toInt(alpha * 255.0f)); + } + + std::string formatMetricValue(double value, int decimals) + { + if (decimals == -1) { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%.1e", value); + return buffer; + } + if (value >= 1e6) { + return StringHelper::format(toFloat(value / 1e6), 2) + " M"; + } + if (value >= 1e4) { + return StringHelper::format(toFloat(value / 1e3), 0) + " K"; + } + return StringHelper::format(toFloat(value), decimals); + } + + void rightAlignedText(std::string const& text) + { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(text.c_str()).x); + ImGui::TextUnformatted(text.c_str()); + } + + std::vector createRandomWalk(std::mt19937& rng, int count, double baseValue, double volatility, double trend) + { + std::uniform_real_distribution distribution(-1.0, 1.0); + std::vector result; + result.reserve(count); + auto value = baseValue * (0.4 + 0.4 * (distribution(rng) + 1.0) / 2); + for (int i = 0; i < count; ++i) { + value += baseValue * (distribution(rng) * volatility + trend); + value = std::max(baseValue * 0.05, value); + result.emplace_back(value); + } + return result; + } +} + +EvolutionDashboardWindow::EvolutionDashboardWindow() + : AlienWindow("Evolution Dashboard", "windows.evolution dashboard", false) +{} + +void EvolutionDashboardWindow::initIntern() +{ + _timelinesHeight = GlobalSettings::get().getValue("windows.evolution dashboard.timelines height", scale(DefaultTimelinesHeight)); + _timelineMode = GlobalSettings::get().getValue("windows.evolution dashboard.timeline mode", _timelineMode); + _plotScale = GlobalSettings::get().getValue("windows.evolution dashboard.plot scale", _plotScale); + _lastSteps = GlobalSettings::get().getValue("windows.evolution dashboard.last steps", _lastSteps); + _colorFilter = GlobalSettings::get().getValue("windows.evolution dashboard.color filter", _colorFilter); + validateAndCorrect(); + + generateDummyData(); +} + +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.plot scale", _plotScale); + GlobalSettings::get().setValue("windows.evolution dashboard.last steps", _lastSteps); + GlobalSettings::get().setValue("windows.evolution dashboard.color filter", _colorFilter); +} + +void EvolutionDashboardWindow::processIntern() +{ + processHeader(); + processFilterBar(); + + if (ImGui::BeginChild("##lineageTable", {0, -_timelinesHeight})) { + processLineageTable(); + } + ImGui::EndChild(); + + AlienGui::MovableHorizontalSeparator(AlienGui::MovableHorizontalSeparatorParameters().additive(false), _timelinesHeight); + + if (ImGui::BeginChild("##timelineSection", {0, 0})) { + processTimelineSection(); + } + ImGui::EndChild(); +} + +void EvolutionDashboardWindow::processHeader() +{ + auto spacing = ImGui::GetStyle().ItemSpacing.x; + auto cardWidth = (ImGui::GetContentRegionAvail().x - 4 * spacing) / 6; + + processKpiCard("SUM ENERGY", "1.24 M", 0.8f, 0, cardWidth); + ImGui::SameLine(); + processKpiCard("EXTERNAL ENERGY", "356 K", -1.2f, 1, cardWidth); + ImGui::SameLine(); + processEntitiesCard(cardWidth * 2); + ImGui::SameLine(); + processKpiCard("LINEAGES", StringHelper::format(toFloat(_lineages.size()), 0), 2.4f, 2, cardWidth); + ImGui::SameLine(); + processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), 0.5f, 3, cardWidth); +} + +void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width) +{ + 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, scale(HeaderCardHeight)}, true)) { + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text(label); + ImGui::PopStyleColor(); + + ImGui::PushFont(StyleRepository::get().getLargeFont()); + AlienGui::Text(value); + ImGui::PopFont(); + + char deltaText[32]; + snprintf(deltaText, sizeof(deltaText), "%+.1f %% / min", delta); + ImGui::PushStyleColor(ImGuiCol_Text, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + AlienGui::Text(deltaText); + ImGui::PopStyleColor(); + + auto const& sparkline = _headerSparklines.at(sparklineIndex); + ImGui::SetCursorPos({width - scale(SparklineWidth + 12.0f), scale(HeaderCardHeight - SparklineHeight - 12.0f)}); + ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); + ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0, 0, 0, 0)); + ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0, 0, 0, 0)); + ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0, 0, 0, 0)); + if (ImPlot::BeginPlot(("##spark" + label).c_str(), {scale(SparklineWidth), scale(SparklineHeight)}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) { + ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); + auto [minIt, maxIt] = std::minmax_element(sparkline.begin(), sparkline.end()); + ImPlot::SetupAxesLimits(0, toDouble(sparkline.size() - 1), *minIt * 0.9, *maxIt * 1.1, ImGuiCond_Always); + ImPlot::PushStyleColor(ImPlotCol_Line, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + ImPlot::PlotLine("##", sparkline.data(), toInt(sparkline.size())); + ImPlot::PopStyleColor(); + ImPlot::EndPlot(); + } + ImPlot::PopStyleColor(3); + ImPlot::PopStyleVar(); + } + ImGui::EndChild(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + +void EvolutionDashboardWindow::processEntitiesCard(float width) +{ + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, scale(6.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, (ImU32)CardBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_Border, (ImU32)CardBorderColor); + if (ImGui::BeginChild("##cardEntities", {width, scale(HeaderCardHeight)}, true)) { + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text("ENTITIES"); + ImGui::PopStyleColor(); + + ImGui::PushFont(StyleRepository::get().getLargeFont()); + AlienGui::Text("486,120"); + ImGui::PopFont(); + + std::pair const subValues[] = { + {"Solids", "12,410"}, + {"Fluid particles", "213,880"}, + {"Cells", "245,230"}, + {"Energy particles", "14,600"}, + }; + for (auto const& [subLabel, subValue] : subValues) { + ImGui::BeginGroup(); + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text(subLabel); + ImGui::PopStyleColor(); + AlienGui::Text(subValue); + ImGui::EndGroup(); + ImGui::SameLine(0, scale(18.0f)); + } + } + ImGui::EndChild(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + +void EvolutionDashboardWindow::processFilterBar() +{ + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text("CELL COLORS"); + 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(Const::DefaultCustomizationColors[i], active ? 1.0f : 0.2f); + 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, 150), + 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_RowBg; + if (ImGui::BeginTable("##lineages", NumMetrics + 1, flags)) { + ImGui::TableSetupScrollFreeze(0, 2); + ImGui::TableSetupColumn("Lineage", ImGuiTableColumnFlags_WidthFixed, scale(130.0f)); + for (auto const& metric : Metrics) { + ImGui::TableSetupColumn(metric.tableHeader); + } + ImGui::TableHeadersRow(); + + //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); + 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].decimals)); + } + + //lineage rows + auto drawList = ImGui::GetWindowDrawList(); + for (auto const& lineage : _lineages) { + if ((_colorFilter & (1 << lineage.color)) == 0) { + continue; + } + ImGui::PushID(lineage.id); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + auto selected = _selectedLineageIds.contains(lineage.id); + if (ImGui::Selectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns)) { + if (ImGui::GetIO().KeyCtrl) { + if (selected) { + _selectedLineageIds.erase(lineage.id); + } else { + _selectedLineageIds.insert(lineage.id); + } + } else { + _selectedLineageIds.clear(); + _selectedLineageIds.insert(lineage.id); + } + } + ImGui::SameLine(0, 0); + auto pos = ImGui::GetCursorScreenPos(); + auto swatchSize = scale(11.0f); + auto offsetY = (ImGui::GetTextLineHeight() - swatchSize) / 2; + drawList->AddRectFilled( + {pos.x, pos.y + offsetY}, + {pos.x + swatchSize, pos.y + offsetY + swatchSize}, + toImColor(Const::DefaultCustomizationColors[lineage.color]), + scale(3.0f)); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchSize + scale(7.0f)); + AlienGui::Text("Lineage #" + std::to_string(lineage.id)); + for (int i = 0; i < NumMetrics; ++i) { + ImGui::TableSetColumnIndex(i + 1); + rightAlignedText(formatMetricValue(lineage.currentValues.at(i), Metrics[i].decimals)); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } +} + +void EvolutionDashboardWindow::processTimelineSection() +{ + processTimelineHeader(); + if (ImGui::BeginChild("##timelinePlots", {0, 0})) { + processTimelinePlots(); + } + ImGui::EndChild(); +} + +void EvolutionDashboardWindow::processTimelineHeader() +{ + ImGui::Spacing(); + AlienGui::Switcher( + AlienGui::SwitcherParameters().name("Mode").textWidth(scale(120.0f)).values({"Real-time", "Entire history", "Last X steps"}), &_timelineMode); + if (_timelineMode == TimelineMode_LastSteps) { + AlienGui::InputInt(AlienGui::InputIntParameters().name("Steps").textWidth(scale(120.0f)), _lastSteps); + validateAndCorrect(); + } + AlienGui::Switcher(AlienGui::SwitcherParameters().name("Scale").textWidth(scale(120.0f)).values({"Linear", "Logarithmic"}), &_plotScale); + + 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 : _lineages) { + if (!_selectedLineageIds.contains(lineage.id)) { + continue; + } + auto pos = ImGui::GetCursorScreenPos(); + auto swatchSize = scale(10.0f); + auto offsetY = (ImGui::GetTextLineHeight() - swatchSize) / 2; + drawList->AddRectFilled( + {pos.x, pos.y + offsetY}, + {pos.x + swatchSize, pos.y + offsetY + swatchSize}, + toImColor(Const::DefaultCustomizationColors[lineage.color]), + scale(3.0f)); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchSize + scale(5.0f)); + AlienGui::Text("Lineage #" + std::to_string(lineage.id)); + ImGui::SameLine(0, scale(14.0f)); + } + ImGui::NewLine(); + } + ImGui::Spacing(); +} + +void EvolutionDashboardWindow::processTimelinePlots() +{ + if (ImGui::BeginTable("##plots", 2, ImGuiTableFlags_None)) { + ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, scale(PlotLabelColumnWidth)); + ImGui::TableSetupColumn("##plot"); + for (int i = 0; i < NumMetrics; ++i) { + auto showTimeAxis = i == NumMetrics - 1; + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + AlienGui::Text(Metrics[i].plotName); + ImGui::PushFont(StyleRepository::get().getMediumBoldFont()); + AlienGui::Text(formatMetricValue(_allLineages.currentValues.at(i), Metrics[i].decimals)); + ImGui::PopFont(); + char deltaText[32]; + snprintf(deltaText, sizeof(deltaText), "%+.1f %%", MetricDeltas[i]); + ImGui::PushStyleColor(ImGuiCol_Text, MetricDeltas[i] >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + AlienGui::Text(deltaText); + ImGui::PopStyleColor(); + + ImGui::TableSetColumnIndex(1); + processTimelinePlot(i, showTimeAxis); + } + ImGui::EndTable(); + } +} + +void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTimeAxis) +{ + auto count = toInt(_timePoints.size()); + auto offset = 0; + if (_timelineMode == TimelineMode_RealTime) { + offset = count * 9 / 10; + } else if (_timelineMode == TimelineMode_LastSteps) { + auto numPoints = toInt(std::lround(toDouble(_lastSteps) / TimeSpan * count)); + offset = std::max(0, count - std::max(2, numPoints)); + } + count -= offset; + + std::vector plottedLineages; + if (_selectedLineageIds.empty()) { + plottedLineages.emplace_back(&_allLineages); + } else { + for (auto const& lineage : _lineages) { + if (_selectedLineageIds.contains(lineage.id)) { + plottedLineages.emplace_back(&lineage); + } + } + } + + auto upperBound = 0.0; + for (auto const* lineage : plottedLineages) { + auto const& series = lineage->series.at(metricIndex); + for (int i = offset; i < offset + count; ++i) { + upperBound = std::max(upperBound, series.at(i)); + } + } + 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(_timePoints.at(offset), _timePoints.at(offset + count - 1), 0, upperBound, ImGuiCond_Always); + + auto height = showTimeAxis ? PlotHeightWithAxis : PlotHeight; + if (ImPlot::BeginPlot( + "##plot", ImVec2(-1, scale(height)), ImPlotFlags_NoLegend | ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMouseText)) { + ImPlot::SetupAxis(ImAxis_X1, "", showTimeAxis ? ImPlotAxisFlags_None : ImPlotAxisFlags_NoTickLabels); + ImPlot::SetupAxis(ImAxis_Y1, "", ImPlotAxisFlags_NoTickLabels); + ImPlot::SetupAxisFormat(ImAxis_Y1, ""); + if (_plotScale == PlotScale_Logarithmic) { + ImPlot::SetupAxisScale( + ImAxis_Y1, + [](double value, void* userData) { return log(value * 1000 + 1.0) / log(2.0); }, + [](double value, void* userData) { return (pow(2.0, value) - 1.0) / 1000; }); + } + for (auto const* lineage : plottedLineages) { + ImGui::PushID(lineage->id + 1); + auto color = lineage->id == -1 ? SumSeriesColor : toImColor(Const::DefaultCustomizationColors[lineage->color]); + auto const& series = lineage->series.at(metricIndex); + ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); + ImPlot::PlotLine("##line", _timePoints.data() + offset, series.data() + offset, count); + if (_plotScale == PlotScale_Linear) { + ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.15f * ImGui::GetStyle().Alpha); + ImPlot::PlotShaded("##shaded", _timePoints.data() + offset, series.data() + offset, count); + ImPlot::PopStyleVar(); + } + ImPlot::PopStyleColor(); + if (ImGui::GetStyle().Alpha == 1.0f) { + ImPlot::Annotation( + _timePoints.at(offset + count - 1), + series.at(offset + count - 1), + color, + ImVec2(-10.0f, 10.0f), + true, + "%s", + formatMetricValue(series.at(offset + count - 1), Metrics[metricIndex].decimals).c_str()); + } + ImGui::PopID(); + } + ImPlot::EndPlot(); + } + ImPlot::PopStyleVar(); + ImPlot::PopStyleColor(3); + ImGui::PopID(); +} + +void EvolutionDashboardWindow::generateDummyData() +{ + std::mt19937 rng(20260711); + std::uniform_int_distribution colorDistribution(0, MAX_COLORS - 1); + std::uniform_real_distribution trendDistribution(-0.002, 0.004); + + _timePoints.clear(); + for (int i = 0; i < NumTimePoints; ++i) { + _timePoints.emplace_back(TimeSpan * i / (NumTimePoints - 1)); + } + + _lineages.clear(); + int const lineageIds[] = {3, 7, 12, 18, 21, 24, 29, 33, 38, 42, 47, 51}; + for (auto id : lineageIds) { + DummyLineage lineage; + lineage.id = id; + lineage.color = colorDistribution(rng); + for (int i = 0; i < NumMetrics; ++i) { + lineage.series.at(i) = createRandomWalk(rng, NumTimePoints, Metrics[i].baseValue, 0.015, trendDistribution(rng)); + lineage.currentValues.at(i) = lineage.series.at(i).back(); + } + _lineages.emplace_back(lineage); + } + + _allLineages.id = -1; + for (int i = 0; i < NumMetrics; ++i) { + auto& sumSeries = _allLineages.series.at(i); + sumSeries.assign(NumTimePoints, 0.0); + auto isSummable = i == 0 || i == 4 || i == 7 || i == 8; //creatures, sum energy, created/s, mutations/s + for (auto const& lineage : _lineages) { + for (int j = 0; j < NumTimePoints; ++j) { + sumSeries.at(j) += lineage.series.at(i).at(j); + } + } + if (!isSummable) { + for (auto& value : sumSeries) { + value /= toDouble(_lineages.size()); + } + } + _allLineages.currentValues.at(i) = sumSeries.back(); + } + + for (auto& sparkline : _headerSparklines) { + sparkline = createRandomWalk(rng, 40, 1.0, 0.03, trendDistribution(rng)); + } +} + +void EvolutionDashboardWindow::validateAndCorrect() +{ + _timelinesHeight = std::max(scale(100.0f), _timelinesHeight); + _timelineMode = std::clamp(_timelineMode, static_cast(TimelineMode_RealTime), static_cast(TimelineMode_LastSteps)); + _plotScale = std::clamp(_plotScale, static_cast(PlotScale_Linear), static_cast(PlotScale_Logarithmic)); + _lastSteps = std::clamp(_lastSteps, 1000, toInt(TimeSpan)); + _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 000000000..bd00f7a60 --- /dev/null +++ b/source/Gui/EvolutionDashboardWindow.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +#include + +#include + +#include "AlienWindow.h" +#include "Definitions.h" + +class EvolutionDashboardWindow : public AlienWindow +{ + MAKE_SINGLETON_NO_DEFAULT_CONSTRUCTION(EvolutionDashboardWindow); + +public: + static auto constexpr NumMetrics = 9; + static auto constexpr NumTimePoints = 300; + +private: + EvolutionDashboardWindow(); + + void initIntern() override; + void shutdownIntern() override; + void processIntern() override; + + void processHeader(); + void processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width); + void processEntitiesCard(float width); + void processFilterBar(); + void processLineageTable(); + void processTimelineSection(); + void processTimelineHeader(); + void processTimelinePlots(); + void processTimelinePlot(int metricIndex, bool showTimeAxis); + + void generateDummyData(); + void validateAndCorrect(); + + struct DummyLineage + { + int id = 0; + int color = 0; + std::array currentValues = {}; + std::array, NumMetrics> series = {}; + }; + + std::vector _lineages; + DummyLineage _allLineages; + std::vector _timePoints; + std::array, 4> _headerSparklines = {}; + + std::set _selectedLineageIds; + int _colorFilter = 0x3ff; //bitset for MAX_COLORS cell colors + + using TimelineMode = int; + enum TimelineMode_ + { + TimelineMode_RealTime, + TimelineMode_EntireHistory, + TimelineMode_LastSteps + }; + TimelineMode _timelineMode = TimelineMode_EntireHistory; + + using PlotScale = int; + enum PlotScale_ + { + PlotScale_Linear, + PlotScale_Logarithmic + }; + PlotScale _plotScale = PlotScale_Linear; + + int _lastSteps = 100000; + float _timelinesHeight = 0; +}; diff --git a/source/Gui/MainLoopController.cpp b/source/Gui/MainLoopController.cpp index 36c276db9..0fb4c0ec2 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" @@ -422,6 +423,14 @@ void MainLoopController::processMenubar() AlienGui::MenuItem( AlienGui::MenuItemParameters().name("Log").keyAlt(true).key(ImGuiKey_6).selected(LogWindow::get().isOn()).closeMenuWhenItemClicked(false), [&] { LogWindow::get().setOn(!LogWindow::get().isOn()); }); + AlienGui::MenuItem( + AlienGui::MenuItemParameters() + .name("Evolution dashboard") + .keyAlt(true) + .key(ImGuiKey_7) + .selected(EvolutionDashboardWindow::get().isOn()) + .closeMenuWhenItemClicked(false), + [&] { EvolutionDashboardWindow::get().setOn(!EvolutionDashboardWindow::get().isOn()); }); AlienGui::EndMenu(); AlienGui::BeginMenu(" " ICON_FA_PEN_ALT " Editor ", _editorMenuOpened); diff --git a/source/Gui/MainWindow.cpp b/source/Gui/MainWindow.cpp index 8a4ac43f0..2514d511b 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" @@ -126,6 +127,7 @@ _MainWindow::_MainWindow() SimulationView::get().setup(); SimulationInteractionController::get().setup(); StatisticsWindow::get().setup(); + EvolutionDashboardWindow::get().setup(); TemporalControlWindow::get().setup(); SpatialControlWindow::get().setup(); SimulationParametersMainWindow::get().setup(); From f5d7686d390c8b446449168f06e703654a33ee5b Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 09:29:11 +0200 Subject: [PATCH 02/41] Fix dashboard layout and support multi-color lineages Co-Authored-By: Claude Fable 5 --- source/Gui/EvolutionDashboardWindow.cpp | 74 +++++++++++++++---------- source/Gui/EvolutionDashboardWindow.h | 2 +- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 9400f45a7..c9bf5a52c 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -1,6 +1,7 @@ #include "EvolutionDashboardWindow.h" #include +#include #include #include #include @@ -18,7 +19,7 @@ namespace { - auto constexpr HeaderCardHeight = 92.0f; + auto constexpr HeaderCardHeight = 112.0f; auto constexpr SparklineWidth = 90.0f; auto constexpr SparklineHeight = 26.0f; auto constexpr ColorChipSize = 22.0f; @@ -83,6 +84,32 @@ namespace ImGui::TextUnformatted(text.c_str()); } + int getFirstColor(int colorBitset) + { + for (int i = 0; i < MAX_COLORS; ++i) { + if (colorBitset & (1 << i)) { + return i; + } + } + return 0; + } + + float drawColorSwatches(ImDrawList* drawList, ImVec2 const& pos, int colorBitset, float lineHeight) + { + auto swatchSize = scale(11.0f); + auto gap = scale(3.0f); + 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(Const::DefaultCustomizationColors[i]), scale(3.0f)); + x += swatchSize + gap; + } + } + return x - pos.x; + } + std::vector createRandomWalk(std::mt19937& rng, int count, double baseValue, double volatility, double trend) { std::uniform_real_distribution distribution(-1.0, 1.0); @@ -162,7 +189,8 @@ void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::str 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, scale(HeaderCardHeight)}, true)) { + if (ImGui::BeginChild( + ("##card" + label).c_str(), {width, scale(HeaderCardHeight)}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); AlienGui::Text(label); ImGui::PopStyleColor(); @@ -205,7 +233,7 @@ void EvolutionDashboardWindow::processEntitiesCard(float width) ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, scale(6.0f)); ImGui::PushStyleColor(ImGuiCol_ChildBg, (ImU32)CardBackgroundColor); ImGui::PushStyleColor(ImGuiCol_Border, (ImU32)CardBorderColor); - if (ImGui::BeginChild("##cardEntities", {width, scale(HeaderCardHeight)}, true)) { + if (ImGui::BeginChild("##cardEntities", {width, scale(HeaderCardHeight)}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); AlienGui::Text("ENTITIES"); ImGui::PopStyleColor(); @@ -275,12 +303,12 @@ void EvolutionDashboardWindow::processFilterBar() void EvolutionDashboardWindow::processLineageTable() { - auto flags = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg; + auto flags = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable; if (ImGui::BeginTable("##lineages", NumMetrics + 1, flags)) { - ImGui::TableSetupScrollFreeze(0, 2); - ImGui::TableSetupColumn("Lineage", ImGuiTableColumnFlags_WidthFixed, scale(130.0f)); + ImGui::TableSetupScrollFreeze(1, 2); + ImGui::TableSetupColumn("Lineage", ImGuiTableColumnFlags_WidthFixed, scale(150.0f)); for (auto const& metric : Metrics) { - ImGui::TableSetupColumn(metric.tableHeader); + ImGui::TableSetupColumn(metric.tableHeader, ImGuiTableColumnFlags_WidthFixed, scale(105.0f)); } ImGui::TableHeadersRow(); @@ -302,7 +330,7 @@ void EvolutionDashboardWindow::processLineageTable() //lineage rows auto drawList = ImGui::GetWindowDrawList(); for (auto const& lineage : _lineages) { - if ((_colorFilter & (1 << lineage.color)) == 0) { + if ((_colorFilter & lineage.colorBitset) == 0) { continue; } ImGui::PushID(lineage.id); @@ -322,15 +350,8 @@ void EvolutionDashboardWindow::processLineageTable() } } ImGui::SameLine(0, 0); - auto pos = ImGui::GetCursorScreenPos(); - auto swatchSize = scale(11.0f); - auto offsetY = (ImGui::GetTextLineHeight() - swatchSize) / 2; - drawList->AddRectFilled( - {pos.x, pos.y + offsetY}, - {pos.x + swatchSize, pos.y + offsetY + swatchSize}, - toImColor(Const::DefaultCustomizationColors[lineage.color]), - scale(3.0f)); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchSize + scale(7.0f)); + auto swatchesWidth = drawColorSwatches(drawList, ImGui::GetCursorScreenPos(), lineage.colorBitset, ImGui::GetTextLineHeight()); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchesWidth + scale(4.0f)); AlienGui::Text("Lineage #" + std::to_string(lineage.id)); for (int i = 0; i < NumMetrics; ++i) { ImGui::TableSetColumnIndex(i + 1); @@ -374,15 +395,8 @@ void EvolutionDashboardWindow::processTimelineHeader() if (!_selectedLineageIds.contains(lineage.id)) { continue; } - auto pos = ImGui::GetCursorScreenPos(); - auto swatchSize = scale(10.0f); - auto offsetY = (ImGui::GetTextLineHeight() - swatchSize) / 2; - drawList->AddRectFilled( - {pos.x, pos.y + offsetY}, - {pos.x + swatchSize, pos.y + offsetY + swatchSize}, - toImColor(Const::DefaultCustomizationColors[lineage.color]), - scale(3.0f)); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchSize + scale(5.0f)); + auto swatchesWidth = drawColorSwatches(drawList, ImGui::GetCursorScreenPos(), lineage.colorBitset, ImGui::GetTextLineHeight()); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchesWidth + scale(4.0f)); AlienGui::Text("Lineage #" + std::to_string(lineage.id)); ImGui::SameLine(0, scale(14.0f)); } @@ -470,7 +484,7 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim } for (auto const* lineage : plottedLineages) { ImGui::PushID(lineage->id + 1); - auto color = lineage->id == -1 ? SumSeriesColor : toImColor(Const::DefaultCustomizationColors[lineage->color]); + auto color = lineage->id == -1 ? SumSeriesColor : toImColor(Const::DefaultCustomizationColors[getFirstColor(lineage->colorBitset)]); auto const& series = lineage->series.at(metricIndex); ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); ImPlot::PlotLine("##line", _timePoints.data() + offset, series.data() + offset, count); @@ -503,6 +517,7 @@ void EvolutionDashboardWindow::generateDummyData() { std::mt19937 rng(20260711); std::uniform_int_distribution colorDistribution(0, MAX_COLORS - 1); + std::uniform_int_distribution numColorsDistribution(1, 3); std::uniform_real_distribution trendDistribution(-0.002, 0.004); _timePoints.clear(); @@ -515,7 +530,10 @@ void EvolutionDashboardWindow::generateDummyData() for (auto id : lineageIds) { DummyLineage lineage; lineage.id = id; - lineage.color = colorDistribution(rng); + auto numColors = numColorsDistribution(rng); + while (std::popcount(static_cast(lineage.colorBitset)) < numColors) { + lineage.colorBitset |= 1 << colorDistribution(rng); + } for (int i = 0; i < NumMetrics; ++i) { lineage.series.at(i) = createRandomWalk(rng, NumTimePoints, Metrics[i].baseValue, 0.015, trendDistribution(rng)); lineage.currentValues.at(i) = lineage.series.at(i).back(); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index bd00f7a60..873f0f30b 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -42,7 +42,7 @@ class EvolutionDashboardWindow : public AlienWindow struct DummyLineage { int id = 0; - int color = 0; + int colorBitset = 0; //at least one of the MAX_COLORS cell colors std::array currentValues = {}; std::array, NumMetrics> series = {}; }; From 831a672291e6fde282f22d03168ea9bf3a81d6ea Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 09:53:45 +0200 Subject: [PATCH 03/41] Refine dashboard layout and use live customization colors --- source/Gui/EvolutionDashboardWindow.cpp | 91 +++++++++++++++++-------- source/Gui/EvolutionDashboardWindow.h | 8 ++- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index c9bf5a52c..f62a2be9b 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -12,17 +12,18 @@ #include #include -#include +#include #include "AlienGui.h" #include "StyleRepository.h" namespace { - auto constexpr HeaderCardHeight = 112.0f; auto constexpr SparklineWidth = 90.0f; auto constexpr SparklineHeight = 26.0f; auto constexpr ColorChipSize = 22.0f; + auto constexpr SwatchSize = 11.0f; + auto constexpr SwatchGap = 3.0f; auto constexpr PlotLabelColumnWidth = 150.0f; auto constexpr PlotHeight = 60.0f; auto constexpr PlotHeightWithAxis = 80.0f; @@ -94,16 +95,15 @@ namespace return 0; } - float drawColorSwatches(ImDrawList* drawList, ImVec2 const& pos, int colorBitset, float lineHeight) + float drawColorSwatches(ImDrawList* drawList, ImVec2 const& pos, int colorBitset, float lineHeight, std::array const& cellColors) { - auto swatchSize = scale(11.0f); - auto gap = scale(3.0f); + 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(Const::DefaultCustomizationColors[i]), scale(3.0f)); + drawList->AddRectFilled({x, pos.y + offsetY}, {x + swatchSize, pos.y + offsetY + swatchSize}, toImColor(cellColors.at(i)), scale(3.0f)); x += swatchSize + gap; } } @@ -152,6 +152,7 @@ void EvolutionDashboardWindow::shutdownIntern() void EvolutionDashboardWindow::processIntern() { + updateCellColors(); processHeader(); processFilterBar(); @@ -168,29 +169,38 @@ void EvolutionDashboardWindow::processIntern() 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::processHeader() { - auto spacing = ImGui::GetStyle().ItemSpacing.x; - auto cardWidth = (ImGui::GetContentRegionAvail().x - 4 * spacing) / 6; + auto const& style = ImGui::GetStyle(); + auto cardWidth = (ImGui::GetContentRegionAvail().x - 4 * style.ItemSpacing.x) / 6; + auto cardHeight = + style.WindowPadding.y * 2 + ImGui::GetTextLineHeight() * 3 + StyleRepository::get().getLargeFont()->FontSize + style.ItemSpacing.y * 3 + scale(6.0f); - processKpiCard("SUM ENERGY", "1.24 M", 0.8f, 0, cardWidth); + processEntitiesCard(cardWidth * 2, cardHeight); ImGui::SameLine(); - processKpiCard("EXTERNAL ENERGY", "356 K", -1.2f, 1, cardWidth); + processKpiCard("SUM ENERGY", "1.24 M", 0.8f, 0, cardWidth, cardHeight); ImGui::SameLine(); - processEntitiesCard(cardWidth * 2); + processKpiCard("EXTERNAL ENERGY", "356 K", -1.2f, 1, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("LINEAGES", StringHelper::format(toFloat(_lineages.size()), 0), 2.4f, 2, cardWidth); + processKpiCard("LINEAGES", StringHelper::format(toFloat(_lineages.size()), 0), 2.4f, 2, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), 0.5f, 3, cardWidth); + processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), 0.5f, 3, cardWidth, cardHeight); } -void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width) +void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, 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, scale(HeaderCardHeight)}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + 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(); @@ -206,7 +216,7 @@ void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::str ImGui::PopStyleColor(); auto const& sparkline = _headerSparklines.at(sparklineIndex); - ImGui::SetCursorPos({width - scale(SparklineWidth + 12.0f), scale(HeaderCardHeight - SparklineHeight - 12.0f)}); + ImGui::SetCursorPos({width - scale(SparklineWidth + 12.0f), height - scale(SparklineHeight + 12.0f)}); ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0, 0, 0, 0)); ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0, 0, 0, 0)); @@ -228,12 +238,12 @@ void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::str ImGui::PopStyleVar(); } -void EvolutionDashboardWindow::processEntitiesCard(float width) +void EvolutionDashboardWindow::processEntitiesCard(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("##cardEntities", {width, scale(HeaderCardHeight)}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + if (ImGui::BeginChild("##cardEntities", {width, height}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); AlienGui::Text("ENTITIES"); ImGui::PopStyleColor(); @@ -280,7 +290,7 @@ void EvolutionDashboardWindow::processFilterBar() _colorFilter ^= 1 << i; } auto active = (_colorFilter & (1 << i)) != 0; - auto color = toImColor(Const::DefaultCustomizationColors[i], active ? 1.0f : 0.2f); + auto color = toImColor(_cellColors.at(i), active ? 1.0f : 0.2f); drawList->AddRectFilled(pos, {pos.x + chipSize, pos.y + chipSize}, color, scale(5.0f)); if (active) { drawList->AddRect( @@ -310,7 +320,16 @@ void EvolutionDashboardWindow::processLineageTable() for (auto const& metric : Metrics) { ImGui::TableSetupColumn(metric.tableHeader, ImGuiTableColumnFlags_WidthFixed, scale(105.0f)); } - ImGui::TableHeadersRow(); + + //header row with centered labels + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int column = 0; column < NumMetrics + 1; ++column) { + ImGui::TableSetColumnIndex(column); + auto columnName = ImGui::TableGetColumnName(column); + auto textWidth = ImGui::CalcTextSize(columnName).x; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - textWidth) / 2)); + ImGui::TextUnformatted(columnName); + } //summary row ImGui::TableNextRow(); @@ -329,6 +348,13 @@ void EvolutionDashboardWindow::processLineageTable() //lineage rows auto drawList = ImGui::GetWindowDrawList(); + 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; @@ -350,8 +376,8 @@ void EvolutionDashboardWindow::processLineageTable() } } ImGui::SameLine(0, 0); - auto swatchesWidth = drawColorSwatches(drawList, ImGui::GetCursorScreenPos(), lineage.colorBitset, ImGui::GetTextLineHeight()); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + swatchesWidth + scale(4.0f)); + 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)); for (int i = 0; i < NumMetrics; ++i) { ImGui::TableSetColumnIndex(i + 1); @@ -376,12 +402,17 @@ void EvolutionDashboardWindow::processTimelineHeader() { ImGui::Spacing(); AlienGui::Switcher( - AlienGui::SwitcherParameters().name("Mode").textWidth(scale(120.0f)).values({"Real-time", "Entire history", "Last X steps"}), &_timelineMode); + AlienGui::SwitcherParameters().name("Mode").width(260.0f).textWidth(45.0f).values({"Real-time", "Entire history", "Last X steps"}), &_timelineMode); + ImGui::SameLine(0, scale(20.0f)); + AlienGui::Switcher(AlienGui::SwitcherParameters().name("Scale").width(220.0f).textWidth(45.0f).values({"Linear", "Logarithmic"}), &_plotScale); if (_timelineMode == TimelineMode_LastSteps) { - AlienGui::InputInt(AlienGui::InputIntParameters().name("Steps").textWidth(scale(120.0f)), _lastSteps); - validateAndCorrect(); + ImGui::SameLine(0, scale(20.0f)); + if (ImGui::BeginChild("##lastSteps", {scale(200.0f), ImGui::GetFrameHeight()})) { + AlienGui::InputInt(AlienGui::InputIntParameters().name("Steps").textWidth(45.0f), _lastSteps); + validateAndCorrect(); + } + ImGui::EndChild(); } - AlienGui::Switcher(AlienGui::SwitcherParameters().name("Scale").textWidth(scale(120.0f)).values({"Linear", "Logarithmic"}), &_plotScale); ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); AlienGui::Text("TIMELINE FILTER"); @@ -395,7 +426,7 @@ void EvolutionDashboardWindow::processTimelineHeader() if (!_selectedLineageIds.contains(lineage.id)) { continue; } - auto swatchesWidth = drawColorSwatches(drawList, ImGui::GetCursorScreenPos(), lineage.colorBitset, ImGui::GetTextLineHeight()); + 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)); @@ -484,7 +515,7 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim } for (auto const* lineage : plottedLineages) { ImGui::PushID(lineage->id + 1); - auto color = lineage->id == -1 ? SumSeriesColor : toImColor(Const::DefaultCustomizationColors[getFirstColor(lineage->colorBitset)]); + auto color = lineage->id == -1 ? SumSeriesColor : toImColor(_cellColors.at(getFirstColor(lineage->colorBitset))); auto const& series = lineage->series.at(metricIndex); ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); ImPlot::PlotLine("##line", _timePoints.data() + offset, series.data() + offset, count); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index 873f0f30b..71eaf1bcb 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -26,9 +27,10 @@ class EvolutionDashboardWindow : public AlienWindow void shutdownIntern() override; void processIntern() override; + void updateCellColors(); void processHeader(); - void processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width); - void processEntitiesCard(float width); + void processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width, float height); + void processEntitiesCard(float width, float height); void processFilterBar(); void processLineageTable(); void processTimelineSection(); @@ -52,6 +54,8 @@ class EvolutionDashboardWindow : public AlienWindow std::vector _timePoints; std::array, 4> _headerSparklines = {}; + std::array _cellColors = {}; + std::set _selectedLineageIds; int _colorFilter = 0x3ff; //bitset for MAX_COLORS cell colors From ef8e0f3242b1676e4c2fd0f08b260e5a45a42955 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 10:05:46 +0200 Subject: [PATCH 04/41] Make dashboard maximizable, brighten chip ring, rename color filter --- source/Gui/EvolutionDashboardWindow.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index f62a2be9b..c9fbe7629 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -126,7 +126,7 @@ namespace } EvolutionDashboardWindow::EvolutionDashboardWindow() - : AlienWindow("Evolution Dashboard", "windows.evolution dashboard", false) + : AlienWindow("Evolution Dashboard", "windows.evolution dashboard", false, true) {} void EvolutionDashboardWindow::initIntern() @@ -277,7 +277,7 @@ void EvolutionDashboardWindow::processFilterBar() { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); - AlienGui::Text("CELL COLORS"); + AlienGui::Text("Filter by colors"); ImGui::PopStyleColor(); ImGui::SameLine(0, scale(12.0f)); @@ -296,7 +296,7 @@ void EvolutionDashboardWindow::processFilterBar() 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, 150), + ImColor(255, 255, 255, 255), scale(6.0f), 0, scale(1.5f)); From 4f90dee1de8b51015c2d33a8d3473c88d8e3c118 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 12:46:08 +0200 Subject: [PATCH 05/41] Split accumulatedMutations into lineage and total counters accumulatedMutationsInLineage resets on lineage switch (old behavior); accumulatedMutations becomes a never-reset lifetime total. --- source/EngineImpl/DescConverterService.cpp | 2 ++ source/EngineInterface/Desc.h | 1 + source/EngineInterface/DescValidationService.cpp | 1 + source/EngineKernels/DataAccessKernels.cu | 1 + source/EngineKernels/Entities.cuh | 3 ++- source/EngineKernels/EntityFactory.cuh | 1 + source/EngineKernels/MutationProcessor.cuh | 9 +++++---- source/EngineKernels/TOs.cuh | 1 + source/EngineTestData/DescTestDataFactory.cpp | 1 + source/EngineTests/AccumulatedMutationTests.cpp | 7 ++++++- source/EngineTests/ConstructorMutationTests.cpp | 2 +- source/Gui/InspectionWindow.cpp | 6 +++++- source/PersisterInterface/SerializerService.cpp | 4 ++++ 13 files changed, 31 insertions(+), 8 deletions(-) diff --git a/source/EngineImpl/DescConverterService.cpp b/source/EngineImpl/DescConverterService.cpp index f411481bf..753847311 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/EngineInterface/Desc.h b/source/EngineInterface/Desc.h index 53197166a..0353f7c9a 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 3a72c4835..9c1b6fb32 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/EngineKernels/DataAccessKernels.cu b/source/EngineKernels/DataAccessKernels.cu index b6d4180c1..09dd3f90e 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/Entities.cuh b/source/EngineKernels/Entities.cuh index ae06d890e..5aa78d362 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 871476c39..dd34e3bbf 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/MutationProcessor.cuh b/source/EngineKernels/MutationProcessor.cuh index 0ed0287f1..344335c6e 100644 --- a/source/EngineKernels/MutationProcessor.cuh +++ b/source/EngineKernels/MutationProcessor.cuh @@ -1874,11 +1874,12 @@ MutationProcessor::updateAccumulatedMutationsAndLineageId(SimulationData& data, 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 (creature->accumulatedMutationsInLineage > cudaSimulationParameters.newLineageThreshold.value) { creature->lineageId = data.primaryNumberGen.createLineageId(); - creature->accumulatedMutations = 0; + creature->accumulatedMutationsInLineage = 0; } } } diff --git a/source/EngineKernels/TOs.cuh b/source/EngineKernels/TOs.cuh index 44720eaeb..bbe6e309c 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/EngineTestData/DescTestDataFactory.cpp b/source/EngineTestData/DescTestDataFactory.cpp index e4210ae7e..b5046d911 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 a76c2b901..13bc8b82b 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(11.0f).accumulatedMutationsInLineage(11.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/ConstructorMutationTests.cpp b/source/EngineTests/ConstructorMutationTests.cpp index 9b7ceaaf7..24b159b28 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/Gui/InspectionWindow.cpp b/source/Gui/InspectionWindow.cpp index 6e9466ed6..65fd92c3d 100644 --- a/source/Gui/InspectionWindow.cpp +++ b/source/Gui/InspectionWindow.cpp @@ -581,7 +581,11 @@ 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); + AlienGui::InputFloat( + AlienGui::InputFloatParameters().name("Accumulated mutations (lineage)").format("%.5f").textWidth(TextWidth), + creature._accumulatedMutationsInLineage); + AlienGui::InputFloat( + AlienGui::InputFloatParameters().name("Accumulated mutations (total)").format("%.5f").textWidth(TextWidth), creature._accumulatedMutations); auto& genome = extendedObject.genome.value(); inspectorText("Genome name", genome._name); inspectorText("Resistance to injection", genome._resistanceToInjection ? "Yes" : "No"); diff --git a/source/PersisterInterface/SerializerService.cpp b/source/PersisterInterface/SerializerService.cpp index 179274391..63237cb06 100644 --- a/source/PersisterInterface/SerializerService.cpp +++ b/source/PersisterInterface/SerializerService.cpp @@ -1127,6 +1127,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 +1857,9 @@ 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); + + // Migration: files saved before this field existed used `accumulatedMutations` for the in-lineage value + scope.addMember(Id_Creature_AccumulatedMutationsInLineage, data._accumulatedMutationsInLineage, data._accumulatedMutations); } SPLIT_SERIALIZATION(CreatureDesc) From eef8db6d9bcea055a5625a2299a1219a3b5b6e25 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 13:08:02 +0200 Subject: [PATCH 06/41] CR changes --- source/Gui/InspectionWindow.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source/Gui/InspectionWindow.cpp b/source/Gui/InspectionWindow.cpp index 65fd92c3d..262389b2c 100644 --- a/source/Gui/InspectionWindow.cpp +++ b/source/Gui/InspectionWindow.cpp @@ -581,11 +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 (lineage)").format("%.5f").textWidth(TextWidth), - creature._accumulatedMutationsInLineage); - AlienGui::InputFloat( - AlienGui::InputFloatParameters().name("Accumulated mutations (total)").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"); From 0acb345f1633eafa7d41e198fc7043643029f615 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 13:10:52 +0200 Subject: [PATCH 07/41] Remove obsolete migration pattern --- source/PersisterInterface/SerializerService.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/PersisterInterface/SerializerService.cpp b/source/PersisterInterface/SerializerService.cpp index 63237cb06..0048d3d52 100644 --- a/source/PersisterInterface/SerializerService.cpp +++ b/source/PersisterInterface/SerializerService.cpp @@ -1857,9 +1857,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); - - // Migration: files saved before this field existed used `accumulatedMutations` for the in-lineage value - scope.addMember(Id_Creature_AccumulatedMutationsInLineage, data._accumulatedMutationsInLineage, data._accumulatedMutations); + scope.addMember(Id_Creature_AccumulatedMutationsInLineage, data._accumulatedMutationsInLineage, defaultObject._accumulatedMutationsInLineage); } SPLIT_SERIALIZATION(CreatureDesc) From 852585aa7c7774ab0411bd883ee664163621c7b5 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 15:01:53 +0200 Subject: [PATCH 08/41] Test fixed --- source/EngineTests/AccumulatedMutationTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/EngineTests/AccumulatedMutationTests.cpp b/source/EngineTests/AccumulatedMutationTests.cpp index 13bc8b82b..f5044312b 100644 --- a/source/EngineTests/AccumulatedMutationTests.cpp +++ b/source/EngineTests/AccumulatedMutationTests.cpp @@ -144,7 +144,7 @@ TEST_F(AccumulatedMutationTests, accumulatedMutations_createsNewLineageId) auto genome = createTestGenome(); auto data = - Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42).accumulatedMutations(11.0f).accumulatedMutationsInLineage(11.0f), genome); + Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42).accumulatedMutations(12.0f).accumulatedMutationsInLineage(12.0f), genome); _parameters.newLineageThreshold.value = 0.1f; _simulationFacade->setSimulationParameters(_parameters); From 5da002c4c65fbf91b172e813b4e1557e274e7fec Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 16:10:00 +0200 Subject: [PATCH 09/41] Feed Evolution Dashboard with real simulation data Co-Authored-By: Claude Fable 5 --- source/EngineImpl/EngineWorker.cpp | 10 + source/EngineImpl/EngineWorker.h | 4 + source/EngineImpl/SimulationCudaFacade.cu | 24 +- source/EngineImpl/SimulationCudaFacade.cuh | 8 + source/EngineImpl/SimulationFacadeImpl.cpp | 10 + source/EngineImpl/SimulationFacadeImpl.h | 2 + source/EngineImpl/StatisticsKernelsService.cu | 10 + source/EngineImpl/TestKernelsService.cu | 4 +- source/EngineImpl/TestKernelsService.cuh | 2 +- source/EngineInterface/CMakeLists.txt | 5 + .../EngineInterface/DataPointCollection.cpp | 24 + source/EngineInterface/DataPointCollection.h | 14 + source/EngineInterface/LineageHistory.cpp | 23 + source/EngineInterface/LineageHistory.h | 29 + .../EngineInterface/LineageHistoryService.cpp | 89 +++ .../EngineInterface/LineageHistoryService.h | 23 + source/EngineInterface/LineageStatistics.h | 25 + source/EngineInterface/SimulationFacade.h | 4 + .../StatisticsConverterService.cpp | 14 + source/EngineInterface/StatisticsRawData.h | 20 + source/EngineInterfaceTests/CMakeLists.txt | 1 + .../LineageHistoryServiceTests.cpp | 163 ++++++ source/EngineKernels/ConstructorProcessor.cuh | 15 +- source/EngineKernels/MutationProcessor.cuh | 25 +- source/EngineKernels/SimulationStatistics.cuh | 294 +++++++++- source/EngineKernels/StatisticsKernels.cu | 169 ++++++ source/EngineKernels/StatisticsKernels.cuh | 10 + source/EngineKernels/TestKernels.cu | 4 +- source/EngineKernels/TestKernels.cuh | 3 +- source/EngineTests/CMakeLists.txt | 4 +- .../EngineTests/EvolutionStatisticsTests.cpp | 104 ++++ source/Gui/EvolutionDashboardWindow.cpp | 516 +++++++++++++----- source/Gui/EvolutionDashboardWindow.h | 50 +- .../PersisterInterface/SerializerService.cpp | 38 +- 34 files changed, 1568 insertions(+), 172 deletions(-) create mode 100644 source/EngineInterface/LineageHistory.cpp create mode 100644 source/EngineInterface/LineageHistory.h create mode 100644 source/EngineInterface/LineageHistoryService.cpp create mode 100644 source/EngineInterface/LineageHistoryService.h create mode 100644 source/EngineInterface/LineageStatistics.h create mode 100644 source/EngineInterfaceTests/LineageHistoryServiceTests.cpp create mode 100644 source/EngineTests/EvolutionStatisticsTests.cpp diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index 41f536e12..c8de8ed60 100644 --- a/source/EngineImpl/EngineWorker.cpp +++ b/source/EngineImpl/EngineWorker.cpp @@ -114,6 +114,16 @@ void EngineWorker::setStatisticsHistory(StatisticsHistoryData const& data) _simulationCudaFacade->setStatisticsHistory(data); } +RawLineageStatistics EngineWorker::getRawLineageStatistics() const +{ + return _simulationCudaFacade->getRawLineageStatistics(); +} + +LineageHistory const& EngineWorker::getLineageHistory() const +{ + return _simulationCudaFacade->getLineageHistory(); +} + void EngineWorker::addAndSelectSimulationData(Desc&& dataToUpdate) { EngineWorkerGuard access(this); diff --git a/source/EngineImpl/EngineWorker.h b/source/EngineImpl/EngineWorker.h index ec9547359..0b72ab58d 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -58,6 +60,8 @@ class EngineWorker StatisticsRawData getStatisticsRawData() const; StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); + RawLineageStatistics getRawLineageStatistics() const; + LineageHistory const& getLineageHistory() const; void addAndSelectSimulationData(Desc&& dataToUpdate); void setSimulationData(Desc const& dataToUpdate); diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index 15bee58a1..6eb8a1d81 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -51,6 +51,7 @@ namespace { std::chrono::milliseconds const StatisticsUpdate(30); + auto constexpr PreviewLineageMapCapacity = 1 << 12; ArraySizesForGpuEntities const PreviewCapacityGpu{10000, 10000, 10000000}; ArraySizesForTOs const PreviewCapacityTO{1000, 1000, 1000, 10000, 10000, 10000, 10000000}; } @@ -82,7 +83,7 @@ _SimulationCudaFacade::_SimulationCudaFacade(uint64_t timestep, SettingsForSimul _cudaSimulationData->init({_settings.worldSizeX, _settings.worldSizeY}, timestep); _cudaPreviewData->init({_settingsForPreview.worldSizeX, _settingsForPreview.worldSizeY}, 0); _cudaSimulationStatistics->init(); - _cudaPreviewStatistics->init(); + _cudaPreviewStatistics->init(PreviewLineageMapCapacity); _cudaSelectionResult->init(); GarbageCollectorKernelsService::get().init(); @@ -112,6 +113,7 @@ _SimulationCudaFacade::~_SimulationCudaFacade() noexcept _cudaSimulationData->free(); _cudaPreviewData->free(); _cudaSimulationStatistics->free(); + _cudaPreviewStatistics->free(); _cudaSelectionResult->free(); SimulationKernelsService::get().shutdown(); @@ -445,11 +447,14 @@ void _SimulationCudaFacade::updateStatistics() StatisticsKernelsService::get().updateStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); syncAndCheck(); + auto lineageStatistics = _cudaSimulationStatistics->getLineageStatistics(); { std::lock_guard lock(_mutexForStatistics); _statisticsData = _cudaSimulationStatistics->getStatistics(); + _lineageStatisticsData = lineageStatistics; } StatisticsService::get().addDataPoint(_statisticsHistory, _statisticsData->timeline, getCurrentTimestep()); + _lineageHistoryService.addSample(_lineageHistory, lineageStatistics, getCurrentTimestep()); } StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const @@ -457,6 +462,21 @@ StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const return _statisticsHistory; } +RawLineageStatistics _SimulationCudaFacade::getRawLineageStatistics() +{ + std::lock_guard lock(_mutexForStatistics); + if (_lineageStatisticsData) { + return *_lineageStatisticsData; + } else { + return RawLineageStatistics(); + } +} + +LineageHistory const& _SimulationCudaFacade::getLineageHistory() const +{ + return _lineageHistory; +} + void _SimulationCudaFacade::setStatisticsHistory(StatisticsHistoryData const& data) { StatisticsService::get().rewriteHistory(_statisticsHistory, data, getCurrentTimestep()); @@ -582,7 +602,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(); diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index da23a9c41..54a53ea50 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -13,6 +13,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -87,6 +90,8 @@ public: void updateStatistics(); StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); + RawLineageStatistics getRawLineageStatistics(); + LineageHistory const& getLineageHistory() const; void resetTimeIntervalStatistics(); uint64_t getCurrentTimestep() const; @@ -156,6 +161,9 @@ private: std::optional _lastStatisticsUpdateTime; std::optional _statisticsData; StatisticsHistory _statisticsHistory; + std::optional _lineageStatisticsData; + LineageHistory _lineageHistory; + LineageHistoryService _lineageHistoryService; std::shared_ptr _cudaSimulationStatistics; std::shared_ptr _cudaPreviewStatistics; MaxAgeBalancer _maxAgeBalancer; diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index c43d465cb..da19943dc 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -335,6 +335,16 @@ void _SimulationFacadeImpl::setStatisticsHistory(StatisticsHistoryData const& da _worker.setStatisticsHistory(data); } +RawLineageStatistics _SimulationFacadeImpl::getRawLineageStatistics() const +{ + return _worker.getRawLineageStatistics(); +} + +LineageHistory const& _SimulationFacadeImpl::getLineageHistory() const +{ + return _worker.getLineageHistory(); +} + std::optional _SimulationFacadeImpl::getTpsRestriction() const { auto result = _worker.getTpsRestriction(); diff --git a/source/EngineImpl/SimulationFacadeImpl.h b/source/EngineImpl/SimulationFacadeImpl.h index 9a7225dc4..f46549d5a 100644 --- a/source/EngineImpl/SimulationFacadeImpl.h +++ b/source/EngineImpl/SimulationFacadeImpl.h @@ -91,6 +91,8 @@ class _SimulationFacadeImpl : public _SimulationFacade StatisticsRawData getStatisticsRawData() const override; StatisticsHistory const& getStatisticsHistory() const override; void setStatisticsHistory(StatisticsHistoryData const& data) override; + RawLineageStatistics getRawLineageStatistics() const override; + LineageHistory const& getLineageHistory() const override; std::optional getTpsRestriction() const override; void setTpsRestriction(std::optional const& value) override; diff --git a/source/EngineImpl/StatisticsKernelsService.cu b/source/EngineImpl/StatisticsKernelsService.cu index 98942d2dc..eb2287f43 100644 --- a/source/EngineImpl/StatisticsKernelsService.cu +++ b/source/EngineImpl/StatisticsKernelsService.cu @@ -11,6 +11,16 @@ void StatisticsKernelsService::updateStatistics(CudaSettings const& gpuSettings, KERNEL_CALL(cudaUpdateTimestepStatistics_substep1, data, simulationStatistics); KERNEL_CALL(cudaUpdateTimestepStatistics_substep2, data, simulationStatistics); KERNEL_CALL(cudaUpdateTimestepStatistics_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); + } KERNEL_CALL_1_1(cudaUpdateHistogramData_substep1, data, simulationStatistics); KERNEL_CALL(cudaUpdateHistogramData_substep2, data, simulationStatistics); KERNEL_CALL(cudaUpdateHistogramData_substep3, data, simulationStatistics); diff --git a/source/EngineImpl/TestKernelsService.cu b/source/EngineImpl/TestKernelsService.cu index 4e7326d9e..e2568d1da 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 573661370..6c5b7ff48 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 663b1fe27..5e299de60 100644 --- a/source/EngineInterface/CMakeLists.txt +++ b/source/EngineInterface/CMakeLists.txt @@ -26,6 +26,11 @@ add_library(EngineInterface GeometryBuffers.h Ids.h InspectedEntityIds.h + LineageHistory.cpp + LineageHistory.h + LineageHistoryService.cpp + LineageHistoryService.h + LineageStatistics.h LocationHelper.cpp LocationHelper.h NeuralNetWeight.h diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index 1813bc30f..a1159347b 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -50,6 +50,18 @@ DataPointCollection DataPointCollection::operator+(DataPointCollection const& ot result.numReconnectorCreated = numReconnectorCreated + other.numReconnectorCreated; result.numReconnectorRemoved = numReconnectorRemoved + other.numReconnectorRemoved; result.numDetonations = numDetonations + other.numDetonations; + 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.accumCreatedCreatures = accumCreatedCreatures + other.accumCreatedCreatures; + result.accumMutations = accumMutations + other.accumMutations; return result; } @@ -83,5 +95,17 @@ DataPointCollection DataPointCollection::operator/(double divisor) const result.numReconnectorCreated = numReconnectorCreated / divisor; result.numReconnectorRemoved = numReconnectorRemoved / divisor; result.numDetonations = numDetonations / divisor; + 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.accumCreatedCreatures = accumCreatedCreatures / divisor; + result.accumMutations = accumMutations / divisor; return result; } diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index 3099978f4..4af4f6c9f 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -43,6 +43,20 @@ struct DataPointCollection DataPoint numReconnectorRemoved; DataPoint numDetonations; + // Evolution dashboard values (not color-resolved) + 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 accumCreatedCreatures = 0; // Raw accumulated value; rates are derived GUI-side + double accumMutations = 0; // Raw accumulated value; rates are derived GUI-side + DataPointCollection operator+(DataPointCollection const& other) const; DataPointCollection operator/(double divisor) const; }; diff --git a/source/EngineInterface/LineageHistory.cpp b/source/EngineInterface/LineageHistory.cpp new file mode 100644 index 000000000..5896d2356 --- /dev/null +++ b/source/EngineInterface/LineageHistory.cpp @@ -0,0 +1,23 @@ +#include "LineageHistory.h" + +LineageHistoryData LineageHistory::getCopiedData() const +{ + std::lock_guard lock(_mutex); + auto copy = _data; + return copy; +} + +std::mutex& LineageHistory::getMutex() const +{ + return _mutex; +} + +LineageHistoryData& LineageHistory::getDataRef() +{ + return _data; +} + +LineageHistoryData const& LineageHistory::getDataRef() const +{ + return _data; +} diff --git a/source/EngineInterface/LineageHistory.h b/source/EngineInterface/LineageHistory.h new file mode 100644 index 000000000..6dcaf39d9 --- /dev/null +++ b/source/EngineInterface/LineageHistory.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "LineageStatistics.h" + +struct LineageSample +{ + double time = 0; // Could be a time step or real-time + double systemClock = 0; + std::vector entries; // Sorted by lineageId +}; + +using LineageHistoryData = std::vector; + +class LineageHistory +{ +public: + LineageHistoryData getCopiedData() const; + + std::mutex& getMutex() const; + LineageHistoryData& getDataRef(); + LineageHistoryData const& getDataRef() const; + +private: + mutable std::mutex _mutex; + LineageHistoryData _data; +}; diff --git a/source/EngineInterface/LineageHistoryService.cpp b/source/EngineInterface/LineageHistoryService.cpp new file mode 100644 index 000000000..cd4f6190f --- /dev/null +++ b/source/EngineInterface/LineageHistoryService.cpp @@ -0,0 +1,89 @@ +#include "LineageHistoryService.h" + +#include +#include + +#include + +void LineageHistoryService::addSample(LineageHistory& history, RawLineageStatistics const& rawStatistics, 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(); + _longtermTimestepDelta = DefaultTimestepDelta; + } + + if (!historyData.empty() && toDouble(timestep) - historyData.back().time < _longtermTimestepDelta) { + return; + } + + LineageSample sample; + sample.time = toDouble(timestep); + auto now = std::chrono::system_clock::now(); + auto unixEpoch = std::chrono::time_point(); + sample.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); + sample.entries = rawStatistics.entries; + std::sort(sample.entries.begin(), sample.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); + historyData.emplace_back(std::move(sample)); + + if (historyData.size() > MaxSamples) { + LineageHistoryData newData; + newData.reserve(historyData.size() / 2 + 1); + for (size_t i = 0; i < (historyData.size() - 1) / 2; ++i) { + newData.emplace_back(mergeSamples(historyData.at(i * 2), historyData.at(i * 2 + 1))); + } + newData.emplace_back(historyData.back()); + historyData.swap(newData); + + _longtermTimestepDelta *= 2.0; + } +} + +void LineageHistoryService::reset() +{ + _longtermTimestepDelta = DefaultTimestepDelta; +} + +LineageSample LineageHistoryService::mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample) +{ + LineageSample result; + result.time = earlierSample.time; + result.systemClock = (earlierSample.systemClock + laterSample.systemClock) / 2; + result.entries.reserve(earlierSample.entries.size() + laterSample.entries.size()); + + auto mergeEntries = [](LineageStatisticsEntry const& lhs, LineageStatisticsEntry const& rhs) { + LineageStatisticsEntry result; + result.lineageId = lhs.lineageId; + result.colorBitset = lhs.colorBitset | rhs.colorBitset; + result.numCreatures = (lhs.numCreatures + rhs.numCreatures) / 2; + result.numGenomes = (lhs.numGenomes + rhs.numGenomes) / 2; + result.sumCreatureCells = (lhs.sumCreatureCells + rhs.sumCreatureCells) / 2; + result.sumCreatureGenerations = (lhs.sumCreatureGenerations + rhs.sumCreatureGenerations) / 2; + result.sumAccumulatedMutations = (lhs.sumAccumulatedMutations + rhs.sumAccumulatedMutations) / 2; + result.sumGenomeNodes = (lhs.sumGenomeNodes + rhs.sumGenomeNodes) / 2; + result.sumMutationRates = (lhs.sumMutationRates + rhs.sumMutationRates) / 2; + result.sumCreatureEnergy = (lhs.sumCreatureEnergy + rhs.sumCreatureEnergy) / 2; + result.numCreatedCreatures = std::max(lhs.numCreatedCreatures, rhs.numCreatedCreatures); + result.totalMutations = std::max(lhs.totalMutations, rhs.totalMutations); + return result; + }; + + auto earlierIter = earlierSample.entries.begin(); + auto laterIter = laterSample.entries.begin(); + while (earlierIter != earlierSample.entries.end() || laterIter != laterSample.entries.end()) { + if (laterIter == laterSample.entries.end() || (earlierIter != earlierSample.entries.end() && earlierIter->lineageId < laterIter->lineageId)) { + result.entries.emplace_back(*earlierIter); + ++earlierIter; + } else if (earlierIter == earlierSample.entries.end() || laterIter->lineageId < earlierIter->lineageId) { + result.entries.emplace_back(*laterIter); + ++laterIter; + } else { + result.entries.emplace_back(mergeEntries(*earlierIter, *laterIter)); + ++earlierIter; + ++laterIter; + } + } + return result; +} diff --git a/source/EngineInterface/LineageHistoryService.h b/source/EngineInterface/LineageHistoryService.h new file mode 100644 index 000000000..1a026f63e --- /dev/null +++ b/source/EngineInterface/LineageHistoryService.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "LineageHistory.h" +#include "LineageStatistics.h" + +class LineageHistoryService +{ +public: + void addSample(LineageHistory& history, RawLineageStatistics const& rawStatistics, uint64_t timestep); + void reset(); + + // Merges two consecutive samples: union of the lineage ids, momentary values are averaged + // and accumulated values are maximized where a lineage is present in both samples. + static LineageSample mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample); + +private: + static auto constexpr MaxSamples = 1000; + static auto constexpr DefaultTimestepDelta = 10.0; + + double _longtermTimestepDelta = DefaultTimestepDelta; +}; diff --git a/source/EngineInterface/LineageStatistics.h b/source/EngineInterface/LineageStatistics.h new file mode 100644 index 000000000..93ced7290 --- /dev/null +++ b/source/EngineInterface/LineageStatistics.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +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 sumAccumulatedMutations = 0; + float sumGenomeNodes = 0; + float sumMutationRates = 0; + float sumCreatureEnergy = 0; + uint32_t numCreatedCreatures = 0; // Accumulated, never reset + float totalMutations = 0; // Accumulated, never reset +}; + +struct RawLineageStatistics +{ + std::vector entries; +}; diff --git a/source/EngineInterface/SimulationFacade.h b/source/EngineInterface/SimulationFacade.h index 1ee1880d2..0ed0a7833 100644 --- a/source/EngineInterface/SimulationFacade.h +++ b/source/EngineInterface/SimulationFacade.h @@ -4,6 +4,8 @@ #include "DataPointCollection.h" #include "Definitions.h" #include "GeometryBuffers.h" +#include "LineageHistory.h" +#include "LineageStatistics.h" #include "PreviewDesc.h" #include "SelectionShallowData.h" #include "SettingsForSimulation.h" @@ -106,6 +108,8 @@ class _SimulationFacade virtual StatisticsRawData getStatisticsRawData() const = 0; virtual StatisticsHistory const& getStatisticsHistory() const = 0; virtual void setStatisticsHistory(StatisticsHistoryData const& data) = 0; + virtual RawLineageStatistics getRawLineageStatistics() const = 0; + virtual LineageHistory const& getLineageHistory() const = 0; //******************** //* Preview simulation diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index 3e32ed89e..0ad04393a 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -100,6 +100,20 @@ DataPointCollection StatisticsConverterService::convert( result.averageNumCells = getDataPointByAveraging(data.timestep.numObjects, data.timestep.numSelfReplicators); result.totalEnergy = getDataPointBySummation(data.timestep.totalEnergy); + auto const& evolution = data.timestep.evolution; + result.numCreatures = toDouble(evolution.numCreatures); + result.averageCreatureCells = evolution.numCreatures > 0 ? toDouble(evolution.sumCreatureCells) / evolution.numCreatures : 0.0; + result.averageGeneration = evolution.numCreatures > 0 ? toDouble(evolution.sumCreatureGenerations) / evolution.numCreatures : 0.0; + result.averageGenomeNodes = evolution.numGenomes > 0 ? toDouble(evolution.sumGenomeNodes) / evolution.numGenomes : 0.0; + result.averageMutationRate = evolution.numGenomes > 0 ? toDouble(evolution.sumMutationRates) / evolution.numGenomes : 0.0; + result.creatureEnergy = toDouble(evolution.sumCreatureEnergy); + result.numLineages = toDouble(evolution.numActiveLineages); + result.numSolidObjects = toDouble(evolution.numSolidObjects); + result.numFluidObjects = toDouble(evolution.numFluidObjects); + result.numCellObjects = toDouble(evolution.numCellObjects); + result.accumCreatedCreatures = toDouble(data.accumulated.numCreatedCreatures); + result.accumMutations = data.accumulated.totalMutations; + auto deltaTimesteps = lastTimestep ? toDouble(timestep) - toDouble(*lastTimestep) : 1.0; if (deltaTimesteps < NEAR_ZERO) { deltaTimesteps = 1.0; diff --git a/source/EngineInterface/StatisticsRawData.h b/source/EngineInterface/StatisticsRawData.h index b7041c1d7..ec0054cb4 100644 --- a/source/EngineInterface/StatisticsRawData.h +++ b/source/EngineInterface/StatisticsRawData.h @@ -3,6 +3,23 @@ #include #include +struct EvolutionStatistics +{ + int numCreatures = 0; + float sumCreatureCells = 0; + float sumCreatureGenerations = 0; + float sumAccumulatedMutations = 0; + int numGenomes = 0; + float sumGenomeNodes = 0; + float sumMutationRates = 0; // Sum over genomes of the per-genome mean of all mutation probabilities + float sumCreatureEnergy = 0; + int numSolidObjects = 0; + int numFluidObjects = 0; + int numCellObjects = 0; + int numActiveLineages = 0; + int lineageMapOverflow = 0; +}; + struct TimestepStatistics { ColorVector numObjects = {}; @@ -12,6 +29,7 @@ struct TimestepStatistics ColorVector numFreeCells = {}; ColorVector numEnergyParticles = {}; ColorVector totalEnergy = {}; + EvolutionStatistics evolution; }; struct AccumulatedStatistics @@ -31,6 +49,8 @@ struct AccumulatedStatistics ColorVector numReconnectorCreated = {}; ColorVector numReconnectorRemoved = {}; ColorVector numDetonations = {}; + uint64_t numCreatedCreatures = 0; + double totalMutations = 0; }; struct TimelineStatistics diff --git a/source/EngineInterfaceTests/CMakeLists.txt b/source/EngineInterfaceTests/CMakeLists.txt index 68e3effb5..aa5409e3b 100644 --- a/source/EngineInterfaceTests/CMakeLists.txt +++ b/source/EngineInterfaceTests/CMakeLists.txt @@ -4,6 +4,7 @@ PUBLIC DescValidationServiceTests.cpp GenomeDescEditServiceTests.cpp GenomeDescInfoServiceTests.cpp + LineageHistoryServiceTests.cpp ParametersEditServiceTests.cpp PreviewDescConverterServiceTests.cpp SpecificationFilterServiceTests.cpp diff --git a/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp b/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp new file mode 100644 index 000000000..8c43b815e --- /dev/null +++ b/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp @@ -0,0 +1,163 @@ +#include + +#include + +class LineageHistoryServiceTests : public ::testing::Test +{ +protected: + static LineageStatisticsEntry createEntry(uint32_t lineageId) + { + LineageStatisticsEntry result; + result.lineageId = lineageId; + return result; + } + + static RawLineageStatistics createRawStatistics(std::vector entries) + { + RawLineageStatistics result; + result.entries = std::move(entries); + return result; + } +}; + +TEST_F(LineageHistoryServiceTests, mergeSamples_disjointIds) +{ + LineageSample earlierSample; + earlierSample.time = 100; + earlierSample.systemClock = 10; + earlierSample.entries = {createEntry(1), createEntry(3)}; + + LineageSample laterSample; + laterSample.time = 200; + laterSample.systemClock = 20; + laterSample.entries = {createEntry(2), createEntry(4)}; + + auto result = LineageHistoryService::mergeSamples(earlierSample, laterSample); + + EXPECT_EQ(100, result.time); + EXPECT_EQ(15, result.systemClock); + ASSERT_EQ(4, result.entries.size()); + EXPECT_EQ(1, result.entries.at(0).lineageId); + EXPECT_EQ(2, result.entries.at(1).lineageId); + EXPECT_EQ(3, result.entries.at(2).lineageId); + EXPECT_EQ(4, result.entries.at(3).lineageId); +} + +TEST_F(LineageHistoryServiceTests, mergeSamples_commonIds) +{ + auto entry1 = createEntry(7); + entry1.colorBitset = 0x01; + entry1.numCreatures = 10; + entry1.numGenomes = 8; + entry1.sumCreatureCells = 100; + entry1.sumCreatureGenerations = 50; + entry1.sumAccumulatedMutations = 20; + entry1.sumGenomeNodes = 300; + entry1.sumMutationRates = 4; + entry1.sumCreatureEnergy = 1000; + entry1.numCreatedCreatures = 40; + entry1.totalMutations = 5; + + auto entry2 = createEntry(7); + entry2.colorBitset = 0x02; + entry2.numCreatures = 20; + entry2.numGenomes = 12; + entry2.sumCreatureCells = 200; + entry2.sumCreatureGenerations = 70; + entry2.sumAccumulatedMutations = 40; + entry2.sumGenomeNodes = 500; + entry2.sumMutationRates = 6; + entry2.sumCreatureEnergy = 3000; + entry2.numCreatedCreatures = 60; + entry2.totalMutations = 9; + + LineageSample earlierSample; + earlierSample.time = 100; + earlierSample.systemClock = 10; + earlierSample.entries = {entry1}; + + LineageSample laterSample; + laterSample.time = 200; + laterSample.systemClock = 20; + laterSample.entries = {entry2}; + + auto result = LineageHistoryService::mergeSamples(earlierSample, laterSample); + + ASSERT_EQ(1, result.entries.size()); + auto const& merged = result.entries.front(); + EXPECT_EQ(7, merged.lineageId); + EXPECT_EQ(0x03, merged.colorBitset); + EXPECT_EQ(15, merged.numCreatures); + EXPECT_EQ(10, merged.numGenomes); + EXPECT_FLOAT_EQ(150, merged.sumCreatureCells); + EXPECT_FLOAT_EQ(60, merged.sumCreatureGenerations); + EXPECT_FLOAT_EQ(30, merged.sumAccumulatedMutations); + EXPECT_FLOAT_EQ(400, merged.sumGenomeNodes); + EXPECT_FLOAT_EQ(5, merged.sumMutationRates); + EXPECT_FLOAT_EQ(2000, merged.sumCreatureEnergy); + EXPECT_EQ(60, merged.numCreatedCreatures); //accumulated values are maximized + EXPECT_FLOAT_EQ(9, merged.totalMutations); +} + +TEST_F(LineageHistoryServiceTests, addSample_sortsEntriesById) +{ + LineageHistoryService service; + LineageHistory history; + + service.addSample(history, createRawStatistics({createEntry(5), createEntry(1), createEntry(3)}), 100); + + auto data = history.getCopiedData(); + ASSERT_EQ(1, data.size()); + ASSERT_EQ(3, data.front().entries.size()); + EXPECT_EQ(1, data.front().entries.at(0).lineageId); + EXPECT_EQ(3, data.front().entries.at(1).lineageId); + EXPECT_EQ(5, data.front().entries.at(2).lineageId); +} + +TEST_F(LineageHistoryServiceTests, addSample_skipsSamplesBelowCadence) +{ + LineageHistoryService service; + LineageHistory history; + + service.addSample(history, createRawStatistics({createEntry(1)}), 100); + service.addSample(history, createRawStatistics({createEntry(2)}), 105); //below the default cadence of 10 timesteps + service.addSample(history, createRawStatistics({createEntry(3)}), 120); + + auto data = history.getCopiedData(); + ASSERT_EQ(2, data.size()); + EXPECT_EQ(100, data.at(0).time); + EXPECT_EQ(120, data.at(1).time); +} + +TEST_F(LineageHistoryServiceTests, addSample_clearsHistoryOnTimeRegression) +{ + LineageHistoryService service; + LineageHistory history; + + service.addSample(history, createRawStatistics({createEntry(1)}), 1000); + service.addSample(history, createRawStatistics({createEntry(2)}), 100); + + auto data = history.getCopiedData(); + ASSERT_EQ(1, data.size()); + EXPECT_EQ(100, data.front().time); + ASSERT_EQ(1, data.front().entries.size()); + EXPECT_EQ(2, data.front().entries.front().lineageId); +} + +TEST_F(LineageHistoryServiceTests, addSample_compressesHistory) +{ + LineageHistoryService service; + LineageHistory history; + + auto timestep = uint64_t(0); + for (int i = 0; i < 1100; ++i) { + service.addSample(history, createRawStatistics({createEntry(1)}), timestep); + timestep += 10; + } + + auto data = history.getCopiedData(); + EXPECT_LE(data.size(), 1001); + for (size_t i = 1; i < data.size(); ++i) { + EXPECT_LT(data.at(i - 1).time, data.at(i).time); + } +} diff --git a/source/EngineKernels/ConstructorProcessor.cuh b/source/EngineKernels/ConstructorProcessor.cuh index da90050dd..110e6747f 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; } diff --git a/source/EngineKernels/MutationProcessor.cuh b/source/EngineKernels/MutationProcessor.cuh index 344335c6e..efe4cfce3 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,8 +1870,12 @@ __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) { @@ -1877,6 +1885,9 @@ MutationProcessor::updateAccumulatedMutationsAndLineageId(SimulationData& data, 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->accumulatedMutationsInLineage = 0; diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index f022756b8..0b36f7a15 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -1,5 +1,6 @@ #pragma once +#include #include #include "Base.cuh" @@ -8,17 +9,40 @@ class SimulationStatistics { public: - __host__ void init() + static auto constexpr DefaultLineageMapCapacity = 1 << 18; // Power of two + static auto constexpr LineageIdEmpty = 0xffffffffu; + + __host__ void init(int lineageMapCapacity = DefaultLineageMapCapacity) { + _lineageMapCapacity = lineageMapCapacity; CudaMemoryManager::getInstance().acquireMemory(1, _data); CudaMemoryManager::getInstance().acquireMemory(MutantToColorCountMapSize, _mutantToMutantStatisticsMap); + CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageMap); + CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageCompactData); + CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[0]); + CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[1]); + CudaMemoryManager::getInstance().acquireMemory(1, _lineageMapControl); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(StatisticsRawData))); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMapControl, 0, sizeof(LineageMapControl))); + + // Values must start at zero; the lineageId key column (first member) is set to LineageIdEmpty + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMap, 0, sizeof(LineageMapSlot) * _lineageMapCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageMap, sizeof(LineageMapSlot), 0xff, sizeof(uint32_t), _lineageMapCapacity)); + for (int i = 0; i < 2; ++i) { + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMaps[i], 0, sizeof(LineageAccumulatorSlot) * _lineageMapCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageAccumulatorMaps[i], sizeof(LineageAccumulatorSlot), 0xff, sizeof(uint32_t), _lineageMapCapacity)); + } } __host__ void free() { CudaMemoryManager::getInstance().freeMemory(_data); CudaMemoryManager::getInstance().freeMemory(_mutantToMutantStatisticsMap); + CudaMemoryManager::getInstance().freeMemory(_lineageMap); + CudaMemoryManager::getInstance().freeMemory(_lineageCompactData); + CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[0]); + CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[1]); + CudaMemoryManager::getInstance().freeMemory(_lineageMapControl); } __host__ StatisticsRawData getStatistics() @@ -28,6 +52,24 @@ public: return result; } + __host__ RawLineageStatistics getLineageStatistics() + { + auto control = getLineageMapControl(); + RawLineageStatistics result; + result.entries.resize(control.numCompactEntries); + if (control.numCompactEntries > 0) { + CHECK_FOR_DEVICE_ERRORS( + cudaMemcpy(result.entries.data(), _lineageCompactData, sizeof(LineageStatisticsEntry) * control.numCompactEntries, cudaMemcpyDeviceToHost)); + } + return result; + } + + __host__ bool isLineageAccumulatorGCNeeded() const + { + auto control = getLineageMapControl(); + return control.numUsedAccumulatorSlots[control.activeAccumulatorBuffer] > static_cast(_lineageMapCapacity) / 2; + } + //timestep statistics __inline__ __device__ void resetTimestepData() { @@ -78,6 +120,158 @@ public: } } + //evolution statistics (timestep) + __inline__ __device__ void incNumSolidObjects() { atomicAdd(&_data->timeline.timestep.evolution.numSolidObjects, 1); } + __inline__ __device__ void incNumFluidObjects() { atomicAdd(&_data->timeline.timestep.evolution.numFluidObjects, 1); } + __inline__ __device__ void incNumCellObjects() { atomicAdd(&_data->timeline.timestep.evolution.numCellObjects, 1); } + __inline__ __device__ void addCreatureStatistics(uint32_t numCells, uint32_t generation, float accumulatedMutations) + { + auto& evolution = _data->timeline.timestep.evolution; + atomicAdd(&evolution.numCreatures, 1); + atomicAdd(&evolution.sumCreatureCells, toFloat(numCells)); + atomicAdd(&evolution.sumCreatureGenerations, toFloat(generation)); + atomicAdd(&evolution.sumAccumulatedMutations, accumulatedMutations); + } + __inline__ __device__ void addGenomeStatistics(float numNodes, float meanMutationRate) + { + auto& evolution = _data->timeline.timestep.evolution; + atomicAdd(&evolution.numGenomes, 1); + atomicAdd(&evolution.sumGenomeNodes, numNodes); + atomicAdd(&evolution.sumMutationRates, meanMutationRate); + } + __inline__ __device__ void addCreatureEnergy(float value) { atomicAdd(&_data->timeline.timestep.evolution.sumCreatureEnergy, value); } + + //lineage statistics + __inline__ __device__ int getLineageMapCapacity() const { return _lineageMapCapacity; } + __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.sumAccumulatedMutations = 0; + slot.sumGenomeNodes = 0; + slot.sumMutationRates = 0; + slot.sumCreatureEnergy = 0; + } + __inline__ __device__ void resetCompactLineageCounter() { _lineageMapControl->numCompactEntries = 0; } + + __inline__ __device__ int insertOrFindLineageSlot(uint32_t lineageId) + { + auto mask = _lineageMapCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageMapCapacity; ++i) { + auto origLineageId = atomicCAS(&_lineageMap[index].lineageId, LineageIdEmpty, lineageId); + if (origLineageId == LineageIdEmpty || origLineageId == lineageId) { + return index; + } + index = (index + 1) & mask; + } + _data->timeline.timestep.evolution.lineageMapOverflow = 1; + return -1; + } + __inline__ __device__ int findLineageSlot(uint32_t lineageId) const + { + auto mask = _lineageMapCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageMapCapacity; ++i) { + auto slotLineageId = _lineageMap[index].lineageId; + if (slotLineageId == lineageId) { + return index; + } + if (slotLineageId == LineageIdEmpty) { + return -1; + } + index = (index + 1) & mask; + } + return -1; + } + __inline__ __device__ void addLineageCreatureData(int slotIndex, uint32_t numCells, uint32_t generation, float accumulatedMutations) + { + auto& slot = _lineageMap[slotIndex]; + atomicAdd(&slot.numCreatures, 1u); + atomicAdd(&slot.sumCreatureCells, toFloat(numCells)); + atomicAdd(&slot.sumCreatureGenerations, toFloat(generation)); + atomicAdd(&slot.sumAccumulatedMutations, accumulatedMutations); + } + __inline__ __device__ void addLineageGenomeData(int slotIndex, float numNodes, float meanMutationRate) + { + auto& slot = _lineageMap[slotIndex]; + atomicAdd(&slot.numGenomes, 1u); + atomicAdd(&slot.sumGenomeNodes, numNodes); + atomicAdd(&slot.sumMutationRates, meanMutationRate); + } + __inline__ __device__ void addLineageCellData(int slotIndex, float energy, int color) + { + auto& slot = _lineageMap[slotIndex]; + atomicAdd(&slot.sumCreatureEnergy, energy); + atomicOr(&slot.colorBitset, 1u << color); + } + __inline__ __device__ void compactLineageSlot(int index) + { + auto const& slot = _lineageMap[index]; + if (slot.lineageId == LineageIdEmpty || slot.numCreatures == 0) { + return; + } + auto entryIndex = atomicAdd(&_lineageMapControl->numCompactEntries, 1u); + auto& entry = _lineageCompactData[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.sumAccumulatedMutations = slot.sumAccumulatedMutations; + entry.sumGenomeNodes = slot.sumGenomeNodes; + entry.sumMutationRates = slot.sumMutationRates; + entry.sumCreatureEnergy = slot.sumCreatureEnergy; + 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() + { + _data->timeline.timestep.evolution.numActiveLineages = toInt(_lineageMapControl->numCompactEntries); + } + + //lineage accumulator map (persistent, garbage-collected occasionally) + __inline__ __device__ void resetInactiveAccumulatorSlot(int index) + { + auto& slot = _lineageAccumulatorMaps[1 - _lineageMapControl->activeAccumulatorBuffer][index]; + slot.lineageId = LineageIdEmpty; + slot.numCreatedCreatures = 0; + slot.totalMutations = 0; + if (index == 0) { + _lineageMapControl->numUsedAccumulatorSlots[1 - _lineageMapControl->activeAccumulatorBuffer] = 0; + } + } + __inline__ __device__ void migrateActiveAccumulatorSlot(int index) + { + auto activeBuffer = _lineageMapControl->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 flipAccumulatorBuffers() { _lineageMapControl->activeAccumulatorBuffer = 1 - _lineageMapControl->activeAccumulatorBuffer; } + //accumulated statistics __host__ void resetAccumulatedStatistics() { @@ -109,6 +303,23 @@ public: __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 incCreatedCreature(uint32_t lineageId) + { + alienAtomicAdd64(&_data->timeline.accumulated.numCreatedCreatures, uint64_t(1)); + auto slotIndex = findOrInsertAccumulatorSlot(lineageId); + if (slotIndex >= 0) { + atomicAdd(&getActiveAccumulatorMap()[slotIndex].numCreatedCreatures, 1u); + } + } + __inline__ __device__ void addMutations(uint32_t lineageId, float value) + { + atomicAdd(&_data->timeline.accumulated.totalMutations, toDouble(value)); + auto slotIndex = findOrInsertAccumulatorSlot(lineageId); + if (slotIndex >= 0) { + atomicAdd(&getActiveAccumulatorMap()[slotIndex].totalMutations, value); + } + } + //histogram __inline__ __device__ void resetHistogramData() { @@ -124,7 +335,88 @@ public: __inline__ __device__ int getMaxAge() const { return _data->histogram.maxAge; } private: + struct LineageMapSlot + { + uint32_t lineageId; // LineageIdEmpty = slot is unused + uint32_t colorBitset; + uint32_t numCreatures; + uint32_t numGenomes; + float sumCreatureCells; + float sumCreatureGenerations; + float sumAccumulatedMutations; + float sumGenomeNodes; + float sumMutationRates; + float sumCreatureEnergy; + }; + struct LineageAccumulatorSlot + { + uint32_t lineageId; // LineageIdEmpty = slot is unused + uint32_t numCreatedCreatures; + float totalMutations; + }; + struct LineageMapControl + { + uint32_t numCompactEntries; + uint32_t activeAccumulatorBuffer; + uint32_t numUsedAccumulatorSlots[2]; + }; + + __host__ LineageMapControl getLineageMapControl() const + { + LineageMapControl result; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _lineageMapControl, sizeof(LineageMapControl), cudaMemcpyDeviceToHost)); + return result; + } + + __inline__ __device__ LineageAccumulatorSlot* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageMapControl->activeAccumulatorBuffer]; } + + __inline__ __device__ int insertAccumulatorSlot(uint32_t bufferIndex, uint32_t lineageId) + { + auto map = _lineageAccumulatorMaps[bufferIndex]; + auto mask = _lineageMapCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageMapCapacity; ++i) { + auto origLineageId = atomicCAS(&map[index].lineageId, LineageIdEmpty, lineageId); + if (origLineageId == LineageIdEmpty) { + atomicAdd(&_lineageMapControl->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(_lineageMapControl->activeAccumulatorBuffer, lineageId); + } + __inline__ __device__ int findAccumulatorSlot(uint32_t lineageId) + { + auto map = getActiveAccumulatorMap(); + auto mask = _lineageMapCapacity - 1; + auto index = toInt((lineageId * 2654435761u) & mask); + for (int i = 0; i < _lineageMapCapacity; ++i) { + auto slotLineageId = map[index].lineageId; + if (slotLineageId == lineageId) { + return index; + } + if (slotLineageId == LineageIdEmpty) { + return -1; + } + index = (index + 1) & mask; + } + return -1; + } + StatisticsRawData* _data; + int _lineageMapCapacity; + + LineageMapSlot* _lineageMap; + LineageStatisticsEntry* _lineageCompactData; + LineageAccumulatorSlot* _lineageAccumulatorMaps[2]; + LineageMapControl* _lineageMapControl; //for diversity calculation static auto constexpr MutantToColorCountMapSize = 1 << 20; diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 1a420fc7f..8b3bb9c58 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -17,6 +17,12 @@ __global__ void cudaUpdateTimestepStatistics_substep2(SimulationData data, Simul statistics.addEnergy(object->color, object->getEnergy()); if (object->type == ObjectType_FreeCell) { statistics.incNumFreeCells(object->color); + } else 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->typeData.cell.cellType == CellType_Constructor && GenomeDecoder::containsSelfReplication(object->typeData.cell.cellTypeData.constructor)) { // statistics.incNumReplicator(object->color); @@ -66,6 +72,169 @@ __global__ void cudaUpdateTimestepStatistics_substep3(SimulationData data, Simul } } +namespace +{ + __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 cudaUpdateEvolutionStatistics_substep1(SimulationData data, SimulationStatistics statistics) +{ + if (threadIdx.x == 0 && blockIdx.x == 0) { + 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); + if (object->type != ObjectType_Cell) { + continue; + } + 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& 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->type != ObjectType_Cell) { + continue; + } + 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, creature->accumulatedMutations); + alienAtomicExch64(&creature->creatureIndex, static_cast(slotIndex) + 1); + } + } + } +} + +__global__ void cudaUpdateEvolutionStatistics_substep3(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->type != ObjectType_Cell) { + continue; + } + 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; + for (int i = 0; i < genome->numGenes; ++i) { + numNodes += genome->genes[i].numNodes; + } + auto meanMutationRate = calcMeanMutationRate(genome->mutationRates); + statistics.addGenomeStatistics(toFloat(numNodes), meanMutationRate); + if (slotIndex >= 0) { + statistics.addLineageGenomeData(slotIndex, toFloat(numNodes), meanMutationRate); + } + } + + auto energy = object->getEnergy(); + statistics.addCreatureEnergy(energy); + if (slotIndex >= 0) { + statistics.addLineageCellData(slotIndex, energy, object->color); + } + } +} + +__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(); +} + __global__ void cudaUpdateHistogramData_substep1(SimulationData data, SimulationStatistics statistics) { statistics.resetHistogramData(); diff --git a/source/EngineKernels/StatisticsKernels.cuh b/source/EngineKernels/StatisticsKernels.cuh index b96a57dfc..16da830cd 100644 --- a/source/EngineKernels/StatisticsKernels.cuh +++ b/source/EngineKernels/StatisticsKernels.cuh @@ -10,6 +10,16 @@ __global__ void cudaUpdateTimestepStatistics_substep1(SimulationData data, Simul __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/TestKernels.cu b/source/EngineKernels/TestKernels.cu index ce9d5cdf0..3f91b1da6 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 12666adb6..4ed72b86c 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/EngineTests/CMakeLists.txt b/source/EngineTests/CMakeLists.txt index 9baf90fc2..79fd679ac 100644 --- a/source/EngineTests/CMakeLists.txt +++ b/source/EngineTests/CMakeLists.txt @@ -55,7 +55,9 @@ PUBLIC TrimGeneMutationTests.cpp UnusedGeneRemovalTests.cpp VoidMutationTests.cpp - VoidTests.cpp) + VoidTests.cpp + # Runs last: consumes GPU RNG state, which would otherwise shift the mutation sequences of subsequent suites + EvolutionStatisticsTests.cpp) target_link_libraries(EngineTests Base) target_link_libraries(EngineTests EngineKernels) diff --git a/source/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp new file mode 100644 index 000000000..09e1c00c6 --- /dev/null +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -0,0 +1,104 @@ +#include + +#include +#include +#include + +#include "MutationTestsBase.h" + +class EvolutionStatisticsTests : public MutationTestsBase +{}; + +TEST_F(EvolutionStatisticsTests, basicCounts) +{ + 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(43).generation(5), GenomeDesc()); + + _simulationFacade->setSimulationData(data); + + auto statistics = _simulationFacade->getStatisticsRawData(); + auto const& evolution = statistics.timeline.timestep.evolution; + EXPECT_EQ(2, evolution.numCreatures); + EXPECT_EQ(2, evolution.numGenomes); + EXPECT_EQ(2, evolution.numActiveLineages); + EXPECT_EQ(3, evolution.numCellObjects); + EXPECT_EQ(0, evolution.numSolidObjects); + EXPECT_EQ(0, evolution.numFluidObjects); + EXPECT_EQ(0, evolution.lineageMapOverflow); + EXPECT_FLOAT_EQ(3.0f, evolution.sumCreatureCells); + EXPECT_FLOAT_EQ(8.0f, evolution.sumCreatureGenerations); + EXPECT_GT(evolution.sumCreatureEnergy, 0.0f); + + auto lineageStatistics = _simulationFacade->getRawLineageStatistics(); + ASSERT_EQ(2, lineageStatistics.entries.size()); + auto findEntry = [&](uint32_t lineageId) -> LineageStatisticsEntry const* { + for (auto const& entry : lineageStatistics.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_NE(0, entry42->colorBitset); + + 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, 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 statistics = _simulationFacade->getStatisticsRawData(); + auto const& evolution = statistics.timeline.timestep.evolution; + EXPECT_EQ(1, evolution.numGenomes); + EXPECT_FLOAT_EQ(toFloat(numNodes), evolution.sumGenomeNodes); + EXPECT_FLOAT_EQ(0.01f, evolution.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 statistics = _simulationFacade->getStatisticsRawData(); + EXPECT_GT(statistics.timeline.timestep.evolution.sumAccumulatedMutations, 0.0f); + EXPECT_GT(statistics.timeline.accumulated.totalMutations, 0.0); + + auto lineageStatistics = _simulationFacade->getRawLineageStatistics(); + ASSERT_EQ(1, lineageStatistics.entries.size()); + EXPECT_EQ(42, lineageStatistics.entries.front().lineageId); + EXPECT_GT(lineageStatistics.entries.front().totalMutations, 0.0f); +} diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index c9fbe7629..52935590a 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -3,12 +3,13 @@ #include #include #include -#include +#include #include #include #include +#include #include #include @@ -19,6 +20,10 @@ namespace { + auto constexpr LiveStatisticsDeltaTime = 50; //in millisec + auto constexpr MaxLiveHistory = 240.0; //in seconds + auto constexpr NumSparklinePoints = 40; + auto constexpr SparklineWidth = 90.0f; auto constexpr SparklineHeight = 26.0f; auto constexpr ColorChipSize = 22.0f; @@ -28,36 +33,32 @@ namespace auto constexpr PlotHeight = 60.0f; auto constexpr PlotHeightWithAxis = 80.0f; auto constexpr DefaultTimelinesHeight = 380.0f; - auto constexpr TimeSpan = 1250000.0; ImColor const CardBackgroundColor = ImColor(0.095f, 0.117f, 0.165f, 1.0f); ImColor const CardBorderColor = ImColor(0.165f, 0.196f, 0.270f, 1.0f); ImColor const PositiveDeltaColor = ImColor(0.19f, 0.77f, 0.55f, 1.0f); ImColor const NegativeDeltaColor = ImColor(0.94f, 0.32f, 0.32f, 1.0f); ImColor const SumSeriesColor = ImColor(0.78f, 0.78f, 0.78f, 1.0f); + ImColor const OverflowWarningColor = ImColor(0.94f, 0.62f, 0.22f, 1.0f); struct MetricDef { char const* tableHeader; char const* plotName; - double baseValue; int decimals; //-1: scientific notation }; MetricDef const Metrics[EvolutionDashboardWindow::NumMetrics] = { - {"Creatures", "Creatures", 1500.0, 0}, - {"Avg size", "Avg creature size", 15.0, 1}, - {"Avg cells", "Avg cells", 40.0, 1}, - {"Avg nodes", "Avg nodes", 60.0, 1}, - {"Sum energy", "Sum energy", 150000.0, 0}, - {"Avg mut. rate", "Avg mutation rate", 2.0e-4, -1}, - {"Avg age (gen)", "Avg age (generations)", 200.0, 0}, - {"Created /s", "Created creatures / s", 3.0, 1}, - {"Mutations /s", "Mutations / s", 1.0, 1}, + {"Creatures", "Creatures", 0}, + {"Avg cells", "Avg cells", 1}, + {"Avg nodes", "Avg nodes", 1}, + {"Sum energy", "Sum energy", 0}, + {"Avg mut. rate", "Avg mutation rate", -1}, + {"Avg age (gen)", "Avg age (generations)", 0}, + {"Created /s", "Created creatures / s", 2}, + {"Mutations /s", "Mutations / s", 1}, }; - float const MetricDeltas[EvolutionDashboardWindow::NumMetrics] = {4.1f, 0.6f, 1.2f, 0.0f, -0.4f, -2.3f, 5.0f, 1.1f, -0.8f}; - ImColor toImColor(uint32_t rgb, float alpha = 1.0f) { return ImColor(toInt((rgb >> 16) & 0xff), toInt((rgb >> 8) & 0xff), toInt(rgb & 0xff), toInt(alpha * 255.0f)); @@ -110,18 +111,96 @@ namespace return x - pos.x; } - std::vector createRandomWalk(std::mt19937& rng, int count, double baseValue, double volatility, double trend) + double calcRate(double accumValue, double lastAccumValue, double clock, double lastClock) + { + auto deltaClock = clock - lastClock; + if (deltaClock <= 0) { + return 0.0; + } + return std::max(0.0, (accumValue - lastAccumValue) / deltaClock); //negative deltas occur after accumulated statistics have been reset + } + + double getGlobalMetricValue(DataPointCollection const& dataPoints, DataPointCollection const* lastDataPoints, bool clockFromTime, int metricIndex) + { + switch (metricIndex) { + case 0: + return dataPoints.numCreatures; + case 1: + return dataPoints.averageCreatureCells; + case 2: + return dataPoints.averageGenomeNodes; + case 3: + return dataPoints.creatureEnergy; + case 4: + return dataPoints.averageMutationRate; + case 5: + return dataPoints.averageGeneration; + case 6: + case 7: { + if (!lastDataPoints) { + return 0.0; + } + auto clock = clockFromTime ? dataPoints.time : dataPoints.systemClock; + auto lastClock = clockFromTime ? lastDataPoints->time : lastDataPoints->systemClock; + if (metricIndex == 6) { + return calcRate(dataPoints.accumCreatedCreatures, lastDataPoints->accumCreatedCreatures, clock, lastClock); + } else { + return calcRate(dataPoints.accumMutations, lastDataPoints->accumMutations, clock, lastClock); + } + } + default: + return 0.0; + } + } + + double getLineageMetricValue(LineageStatisticsEntry const& entry, LineageStatisticsEntry const* lastEntry, double clock, double lastClock, int metricIndex) { - std::uniform_real_distribution distribution(-1.0, 1.0); - std::vector result; - result.reserve(count); - auto value = baseValue * (0.4 + 0.4 * (distribution(rng) + 1.0) / 2); - for (int i = 0; i < count; ++i) { - value += baseValue * (distribution(rng) * volatility + trend); - value = std::max(baseValue * 0.05, value); - result.emplace_back(value); + switch (metricIndex) { + case 0: + return toDouble(entry.numCreatures); + case 1: + return entry.numCreatures > 0 ? toDouble(entry.sumCreatureCells) / entry.numCreatures : 0.0; + case 2: + return entry.numGenomes > 0 ? toDouble(entry.sumGenomeNodes) / entry.numGenomes : 0.0; + case 3: + return toDouble(entry.sumCreatureEnergy); + case 4: + return entry.numGenomes > 0 ? toDouble(entry.sumMutationRates) / entry.numGenomes : 0.0; + case 5: + return entry.numCreatures > 0 ? toDouble(entry.sumCreatureGenerations) / entry.numCreatures : 0.0; + case 6: + return lastEntry ? calcRate(toDouble(entry.numCreatedCreatures), toDouble(lastEntry->numCreatedCreatures), clock, lastClock) : 0.0; + case 7: + return lastEntry ? calcRate(toDouble(entry.totalMutations), toDouble(lastEntry->totalMutations), clock, lastClock) : 0.0; + default: + return 0.0; } - return result; + } + + LineageStatisticsEntry const* findLineageEntry(LineageSample const& sample, uint32_t lineageId) + { + auto it = + std::lower_bound(sample.entries.begin(), sample.entries.end(), lineageId, [](auto const& entry, uint32_t id) { return entry.lineageId < id; }); + if (it != sample.entries.end() && it->lineageId == lineageId) { + return &*it; + } + return nullptr; + } + + double calcWindowDeltaPercent(std::vector const& series) + { + if (series.size() < 2 || std::abs(series.front()) < NEAR_ZERO) { + return 0.0; + } + return (series.back() / series.front() - 1.0) * 100; + } + + double calcDeltaPercentPerMinute(std::vector const& series, double windowInSeconds) + { + if (windowInSeconds < NEAR_ZERO) { + return 0.0; + } + return calcWindowDeltaPercent(series) / (windowInSeconds / 60); } } @@ -137,8 +216,6 @@ void EvolutionDashboardWindow::initIntern() _lastSteps = GlobalSettings::get().getValue("windows.evolution dashboard.last steps", _lastSteps); _colorFilter = GlobalSettings::get().getValue("windows.evolution dashboard.color filter", _colorFilter); validateAndCorrect(); - - generateDummyData(); } void EvolutionDashboardWindow::shutdownIntern() @@ -150,9 +227,43 @@ void EvolutionDashboardWindow::shutdownIntern() GlobalSettings::get().setValue("windows.evolution dashboard.color filter", _colorFilter); } +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; + _timeSinceSimStart += toDouble(duration) / 1000; + + auto rawStatistics = _SimulationFacade::get()->getStatisticsRawData(); + _timelineLiveStatistics.update(rawStatistics.timeline, _SimulationFacade::get()->getCurrentTimestep()); + _lineageMapOverflow = rawStatistics.timeline.timestep.evolution.lineageMapOverflow != 0; + + auto rawLineageStatistics = _SimulationFacade::get()->getRawLineageStatistics(); + LineageSample sample; + sample.time = _timeSinceSimStart; + sample.systemClock = _timeSinceSimStart; + sample.entries = std::move(rawLineageStatistics.entries); + std::sort(sample.entries.begin(), sample.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); + _liveLineageHistory.emplace_back(std::move(sample)); + while (!_liveLineageHistory.empty() && _liveLineageHistory.back().time - _liveLineageHistory.front().time > MaxLiveHistory + 1.0) { + _liveLineageHistory.erase(_liveLineageHistory.begin()); + } + + _externalEnergySeries.emplace_back(toDouble(_SimulationFacade::get()->getSimulationParameters().externalEnergy.value)); + auto maxExternalEnergyValues = toInt(std::lround(MaxLiveHistory * 1000 / LiveStatisticsDeltaTime)); + while (toInt(_externalEnergySeries.size()) > maxExternalEnergyValues) { + _externalEnergySeries.erase(_externalEnergySeries.begin()); + } +} + void EvolutionDashboardWindow::processIntern() { updateCellColors(); + updateDisplayData(); processHeader(); processFilterBar(); @@ -177,6 +288,157 @@ void EvolutionDashboardWindow::updateCellColors() } } +void EvolutionDashboardWindow::updateDisplayData() +{ + //rebuild only when the underlying data or view settings have changed + auto liveBackTime = !_liveLineageHistory.empty() ? _liveLineageHistory.back().time : -1.0; + auto historyBackTime = -1.0; + if (_timelineMode != TimelineMode_RealTime) { + auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); + std::lock_guard lock(statisticsHistory.getMutex()); + if (!statisticsHistory.getDataRef().empty()) { + historyBackTime = statisticsHistory.getDataRef().back().time; + } + } + RebuildKey key{_timelineMode, _lastSteps, liveBackTime, historyBackTime, _selectedLineageIds}; + if (_lastRebuildKey && *_lastRebuildKey == key) { + return; + } + _lastRebuildKey = std::move(key); + + //table rows from the latest live sample + _lineages.clear(); + if (!_liveLineageHistory.empty()) { + auto const& lastSample = _liveLineageHistory.back(); + auto const* previousSample = _liveLineageHistory.size() >= 2 ? &_liveLineageHistory.at(_liveLineageHistory.size() - 2) : nullptr; + for (auto const& entry : lastSample.entries) { + LineageDisplayData lineage; + lineage.id = toInt(entry.lineageId); + lineage.colorBitset = toInt(entry.colorBitset); + auto const* previousEntry = previousSample ? findLineageEntry(*previousSample, entry.lineageId) : nullptr; + for (int i = 0; i < NumMetrics; ++i) { + lineage.currentValues.at(i) = getLineageMetricValue(entry, previousEntry, lastSample.time, previousSample ? previousSample->time : 0.0, i); + } + _lineages.emplace_back(std::move(lineage)); + } + std::sort(_lineages.begin(), _lineages.end(), [](auto const& lhs, auto const& rhs) { return lhs.currentValues.at(0) > rhs.currentValues.at(0); }); + } + + //data source for the timeline plots + auto useTimeAsClock = _timelineMode == TimelineMode_RealTime; + std::vector globalSource; + LineageHistoryData lineageSource; + if (_timelineMode == TimelineMode_RealTime) { + globalSource = _timelineLiveStatistics.getDataPointCollectionHistory(); + lineageSource = _liveLineageHistory; + } else { + auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); + { + std::lock_guard lock(statisticsHistory.getMutex()); + globalSource = statisticsHistory.getDataRef(); + } + lineageSource = _SimulationFacade::get()->getLineageHistory().getCopiedData(); + if (_timelineMode == TimelineMode_LastSteps && !globalSource.empty()) { + auto startTime = globalSource.back().time - toDouble(_lastSteps); + std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startTime; }); + std::erase_if(lineageSource, [&](auto const& sample) { return sample.time < startTime; }); + } + } + + //"all lineages" series + _allLineages.id = -1; + _allLineages.colorBitset = 0; + _allLineages.timePoints.clear(); + for (int i = 0; i < NumMetrics; ++i) { + _allLineages.series.at(i).clear(); + } + for (size_t sampleIndex = 0; sampleIndex < globalSource.size(); ++sampleIndex) { + auto const& dataPoints = globalSource.at(sampleIndex); + auto const* lastDataPoints = sampleIndex > 0 ? &globalSource.at(sampleIndex - 1) : nullptr; + _allLineages.timePoints.emplace_back(dataPoints.time); + for (int i = 0; i < NumMetrics; ++i) { + _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, lastDataPoints, useTimeAsClock, i)); + } + } + auto const& liveGlobalHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); + if (!liveGlobalHistory.empty()) { + auto const& lastDataPoints = liveGlobalHistory.back(); + auto const* previousDataPoints = liveGlobalHistory.size() >= 2 ? &liveGlobalHistory.at(liveGlobalHistory.size() - 2) : nullptr; + for (int i = 0; i < NumMetrics; ++i) { + _allLineages.currentValues.at(i) = getGlobalMetricValue(lastDataPoints, previousDataPoints, true, i); + } + } + for (int i = 0; i < NumMetrics; ++i) { + _metricWindowDeltas.at(i) = calcWindowDeltaPercent(_allLineages.series.at(i)); + } + + //per-lineage series for the selected lineages + _plottedLineages.clear(); + if (!_selectedLineageIds.empty()) { + for (auto const& selectedId : _selectedLineageIds) { + LineageDisplayData lineage; + lineage.id = selectedId; + LineageSample const* lastSampleWithEntry = nullptr; + LineageStatisticsEntry const* lastEntry = nullptr; + for (auto const& sample : lineageSource) { + auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); + if (!entry) { + continue; + } + lineage.colorBitset = toInt(entry->colorBitset); + lineage.timePoints.emplace_back(sample.time); + auto clock = useTimeAsClock ? sample.time : sample.systemClock; + auto lastClock = lastSampleWithEntry ? (useTimeAsClock ? lastSampleWithEntry->time : lastSampleWithEntry->systemClock) : 0.0; + for (int i = 0; i < NumMetrics; ++i) { + lineage.series.at(i).emplace_back(getLineageMetricValue(*entry, lastEntry, clock, lastClock, i)); + } + lastSampleWithEntry = &sample; + lastEntry = entry; + } + for (auto const& tableLineage : _lineages) { + if (tableLineage.id == selectedId) { + lineage.currentValues = tableLineage.currentValues; + if (lineage.colorBitset == 0) { + lineage.colorBitset = tableLineage.colorBitset; + } + } + } + _plottedLineages.emplace_back(std::move(lineage)); + } + } + + //header sparklines and deltas from the live statistics + auto liveWindowInSeconds = liveGlobalHistory.size() >= 2 ? liveGlobalHistory.back().time - liveGlobalHistory.front().time : 0.0; + for (int cardIndex = 0; cardIndex < 4; ++cardIndex) { + auto& sparkline = _headerSparklines.at(cardIndex); + sparkline.clear(); + std::vector fullSeries; + if (cardIndex == 1) { + fullSeries = _externalEnergySeries; + } else { + fullSeries.reserve(liveGlobalHistory.size()); + for (auto const& dataPoints : liveGlobalHistory) { + switch (cardIndex) { + case 0: + fullSeries.emplace_back(dataPoints.totalEnergy.summedValues); + break; + case 2: + fullSeries.emplace_back(dataPoints.numLineages); + break; + case 3: + fullSeries.emplace_back(dataPoints.numCreatures); + break; + } + } + } + _headerDeltas.at(cardIndex) = calcDeltaPercentPerMinute(fullSeries, liveWindowInSeconds); + auto stride = std::max(size_t(1), fullSeries.size() / NumSparklinePoints); + for (size_t i = 0; i < fullSeries.size(); i += stride) { + sparkline.emplace_back(fullSeries.at(i)); + } + } +} + void EvolutionDashboardWindow::processHeader() { auto const& style = ImGui::GetStyle(); @@ -184,15 +446,21 @@ void EvolutionDashboardWindow::processHeader() 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; + processEntitiesCard(cardWidth * 2, cardHeight); ImGui::SameLine(); - processKpiCard("SUM ENERGY", "1.24 M", 0.8f, 0, cardWidth, cardHeight); + auto sumEnergy = lastDataPoints ? lastDataPoints->totalEnergy.summedValues : 0.0; + processKpiCard("SUM ENERGY", formatMetricValue(sumEnergy, 0), toFloat(_headerDeltas.at(0)), 0, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("EXTERNAL ENERGY", "356 K", -1.2f, 1, cardWidth, cardHeight); + auto externalEnergy = !_externalEnergySeries.empty() ? _externalEnergySeries.back() : 0.0; + processKpiCard("EXTERNAL ENERGY", formatMetricValue(externalEnergy, 0), toFloat(_headerDeltas.at(1)), 1, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("LINEAGES", StringHelper::format(toFloat(_lineages.size()), 0), 2.4f, 2, cardWidth, cardHeight); + auto numLineages = lastDataPoints ? lastDataPoints->numLineages : 0.0; + processKpiCard("LINEAGES", formatMetricValue(numLineages, 0), toFloat(_headerDeltas.at(2)), 2, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), 0.5f, 3, cardWidth, cardHeight); + processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), toFloat(_headerDeltas.at(3)), 3, cardWidth, cardHeight); } void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width, float height) @@ -216,22 +484,25 @@ void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::str ImGui::PopStyleColor(); auto const& sparkline = _headerSparklines.at(sparklineIndex); - ImGui::SetCursorPos({width - scale(SparklineWidth + 12.0f), height - scale(SparklineHeight + 12.0f)}); - ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); - ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0, 0, 0, 0)); - ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0, 0, 0, 0)); - ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0, 0, 0, 0)); - if (ImPlot::BeginPlot(("##spark" + label).c_str(), {scale(SparklineWidth), scale(SparklineHeight)}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) { - ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); - auto [minIt, maxIt] = std::minmax_element(sparkline.begin(), sparkline.end()); - ImPlot::SetupAxesLimits(0, toDouble(sparkline.size() - 1), *minIt * 0.9, *maxIt * 1.1, ImGuiCond_Always); - ImPlot::PushStyleColor(ImPlotCol_Line, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); - ImPlot::PlotLine("##", sparkline.data(), toInt(sparkline.size())); - ImPlot::PopStyleColor(); - ImPlot::EndPlot(); + if (sparkline.size() >= 2) { + ImGui::SetCursorPos({width - scale(SparklineWidth + 12.0f), height - scale(SparklineHeight + 12.0f)}); + ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); + ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0, 0, 0, 0)); + ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0, 0, 0, 0)); + ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0, 0, 0, 0)); + if (ImPlot::BeginPlot( + ("##spark" + label).c_str(), {scale(SparklineWidth), scale(SparklineHeight)}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) { + ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); + auto [minIt, maxIt] = std::minmax_element(sparkline.begin(), sparkline.end()); + ImPlot::SetupAxesLimits(0, toDouble(sparkline.size() - 1), *minIt * 0.9, *maxIt * 1.1 + NEAR_ZERO, ImGuiCond_Always); + ImPlot::PushStyleColor(ImPlotCol_Line, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + ImPlot::PlotLine("##", sparkline.data(), toInt(sparkline.size())); + ImPlot::PopStyleColor(); + ImPlot::EndPlot(); + } + ImPlot::PopStyleColor(3); + ImPlot::PopStyleVar(); } - ImPlot::PopStyleColor(3); - ImPlot::PopStyleVar(); } ImGui::EndChild(); ImGui::PopStyleColor(2); @@ -240,6 +511,21 @@ void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::str void EvolutionDashboardWindow::processEntitiesCard(float width, float height) { + auto const& liveGlobalHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); + auto solids = 0.0; + auto fluids = 0.0; + auto cells = 0.0; + auto energyParticles = 0.0; + auto total = 0.0; + if (!liveGlobalHistory.empty()) { + auto const& dataPoints = liveGlobalHistory.back(); + solids = dataPoints.numSolidObjects; + fluids = dataPoints.numFluidObjects; + cells = dataPoints.numCellObjects; + energyParticles = dataPoints.numEnergyParticles.summedValues; + total = dataPoints.numObjects.summedValues + energyParticles; + } + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, scale(6.0f)); ImGui::PushStyleColor(ImGuiCol_ChildBg, (ImU32)CardBackgroundColor); ImGui::PushStyleColor(ImGuiCol_Border, (ImU32)CardBorderColor); @@ -249,21 +535,21 @@ void EvolutionDashboardWindow::processEntitiesCard(float width, float height) ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); - AlienGui::Text("486,120"); + AlienGui::Text(formatMetricValue(total, 0)); ImGui::PopFont(); - std::pair const subValues[] = { - {"Solids", "12,410"}, - {"Fluid particles", "213,880"}, - {"Cells", "245,230"}, - {"Energy particles", "14,600"}, + std::pair const subValues[] = { + {"Solids", solids}, + {"Fluid particles", fluids}, + {"Cells", cells}, + {"Energy particles", energyParticles}, }; for (auto const& [subLabel, subValue] : subValues) { ImGui::BeginGroup(); ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); AlienGui::Text(subLabel); ImGui::PopStyleColor(); - AlienGui::Text(subValue); + AlienGui::Text(formatMetricValue(subValue, 0)); ImGui::EndGroup(); ImGui::SameLine(0, scale(18.0f)); } @@ -307,6 +593,12 @@ void EvolutionDashboardWindow::processFilterBar() ImGui::PopID(); ImGui::SameLine(0, scale(5.0f)); } + if (_lineageMapOverflow) { + ImGui::SameLine(0, scale(20.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)OverflowWarningColor); + AlienGui::Text("Too many lineages: some are not tracked"); + ImGui::PopStyleColor(); + } ImGui::NewLine(); ImGui::Spacing(); } @@ -359,7 +651,7 @@ void EvolutionDashboardWindow::processLineageTable() if ((_colorFilter & lineage.colorBitset) == 0) { continue; } - ImGui::PushID(lineage.id); + ImGui::PushID(toInt(lineage.id)); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); auto selected = _selectedLineageIds.contains(lineage.id); @@ -422,10 +714,7 @@ void EvolutionDashboardWindow::processTimelineHeader() AlienGui::Text("All lineages"); } else { auto drawList = ImGui::GetWindowDrawList(); - for (auto const& lineage : _lineages) { - if (!_selectedLineageIds.contains(lineage.id)) { - continue; - } + 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)); @@ -450,8 +739,8 @@ void EvolutionDashboardWindow::processTimelinePlots() AlienGui::Text(formatMetricValue(_allLineages.currentValues.at(i), Metrics[i].decimals)); ImGui::PopFont(); char deltaText[32]; - snprintf(deltaText, sizeof(deltaText), "%+.1f %%", MetricDeltas[i]); - ImGui::PushStyleColor(ImGuiCol_Text, MetricDeltas[i] >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + snprintf(deltaText, sizeof(deltaText), "%+.1f %%", _metricWindowDeltas.at(i)); + ImGui::PushStyleColor(ImGuiCol_Text, _metricWindowDeltas.at(i) >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); AlienGui::Text(deltaText); ImGui::PopStyleColor(); @@ -464,33 +753,35 @@ void EvolutionDashboardWindow::processTimelinePlots() void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTimeAxis) { - auto count = toInt(_timePoints.size()); - auto offset = 0; - if (_timelineMode == TimelineMode_RealTime) { - offset = count * 9 / 10; - } else if (_timelineMode == TimelineMode_LastSteps) { - auto numPoints = toInt(std::lround(toDouble(_lastSteps) / TimeSpan * count)); - offset = std::max(0, count - std::max(2, numPoints)); - } - count -= offset; - - std::vector plottedLineages; + std::vector plottedLineages; if (_selectedLineageIds.empty()) { plottedLineages.emplace_back(&_allLineages); } else { - for (auto const& lineage : _lineages) { - if (_selectedLineageIds.contains(lineage.id)) { - plottedLineages.emplace_back(&lineage); - } + for (auto const& lineage : _plottedLineages) { + plottedLineages.emplace_back(&lineage); } } auto upperBound = 0.0; + auto minTime = std::numeric_limits::max(); + auto maxTime = std::numeric_limits::lowest(); + auto hasData = false; for (auto const* lineage : plottedLineages) { - auto const& series = lineage->series.at(metricIndex); - for (int i = offset; i < offset + count; ++i) { - upperBound = std::max(upperBound, series.at(i)); + 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)) { + upperBound = std::max(upperBound, value); + } + } + if (!hasData) { + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text("No data"); + ImGui::PopStyleColor(); + return; } upperBound *= 1.35; @@ -499,7 +790,7 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim 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(_timePoints.at(offset), _timePoints.at(offset + count - 1), 0, upperBound, ImGuiCond_Always); + ImPlot::SetNextAxesLimits(minTime, maxTime, 0, upperBound + NEAR_ZERO, ImGuiCond_Always); auto height = showTimeAxis ? PlotHeightWithAxis : PlotHeight; if (ImPlot::BeginPlot( @@ -514,26 +805,30 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim [](double value, void* userData) { return (pow(2.0, value) - 1.0) / 1000; }); } for (auto const* lineage : plottedLineages) { - ImGui::PushID(lineage->id + 1); + auto count = toInt(lineage->timePoints.size()); + if (count < 2) { + continue; + } + ImGui::PushID(toInt(lineage->id) + 1); auto color = lineage->id == -1 ? SumSeriesColor : toImColor(_cellColors.at(getFirstColor(lineage->colorBitset))); auto const& series = lineage->series.at(metricIndex); ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); - ImPlot::PlotLine("##line", _timePoints.data() + offset, series.data() + offset, count); + ImPlot::PlotLine("##line", lineage->timePoints.data(), series.data(), count); if (_plotScale == PlotScale_Linear) { ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.15f * ImGui::GetStyle().Alpha); - ImPlot::PlotShaded("##shaded", _timePoints.data() + offset, series.data() + offset, count); + ImPlot::PlotShaded("##shaded", lineage->timePoints.data(), series.data(), count); ImPlot::PopStyleVar(); } ImPlot::PopStyleColor(); if (ImGui::GetStyle().Alpha == 1.0f) { ImPlot::Annotation( - _timePoints.at(offset + count - 1), - series.at(offset + count - 1), + lineage->timePoints.back(), + series.back(), color, ImVec2(-10.0f, 10.0f), true, "%s", - formatMetricValue(series.at(offset + count - 1), Metrics[metricIndex].decimals).c_str()); + formatMetricValue(series.back(), Metrics[metricIndex].decimals).c_str()); } ImGui::PopID(); } @@ -544,63 +839,12 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim ImGui::PopID(); } -void EvolutionDashboardWindow::generateDummyData() -{ - std::mt19937 rng(20260711); - std::uniform_int_distribution colorDistribution(0, MAX_COLORS - 1); - std::uniform_int_distribution numColorsDistribution(1, 3); - std::uniform_real_distribution trendDistribution(-0.002, 0.004); - - _timePoints.clear(); - for (int i = 0; i < NumTimePoints; ++i) { - _timePoints.emplace_back(TimeSpan * i / (NumTimePoints - 1)); - } - - _lineages.clear(); - int const lineageIds[] = {3, 7, 12, 18, 21, 24, 29, 33, 38, 42, 47, 51}; - for (auto id : lineageIds) { - DummyLineage lineage; - lineage.id = id; - auto numColors = numColorsDistribution(rng); - while (std::popcount(static_cast(lineage.colorBitset)) < numColors) { - lineage.colorBitset |= 1 << colorDistribution(rng); - } - for (int i = 0; i < NumMetrics; ++i) { - lineage.series.at(i) = createRandomWalk(rng, NumTimePoints, Metrics[i].baseValue, 0.015, trendDistribution(rng)); - lineage.currentValues.at(i) = lineage.series.at(i).back(); - } - _lineages.emplace_back(lineage); - } - - _allLineages.id = -1; - for (int i = 0; i < NumMetrics; ++i) { - auto& sumSeries = _allLineages.series.at(i); - sumSeries.assign(NumTimePoints, 0.0); - auto isSummable = i == 0 || i == 4 || i == 7 || i == 8; //creatures, sum energy, created/s, mutations/s - for (auto const& lineage : _lineages) { - for (int j = 0; j < NumTimePoints; ++j) { - sumSeries.at(j) += lineage.series.at(i).at(j); - } - } - if (!isSummable) { - for (auto& value : sumSeries) { - value /= toDouble(_lineages.size()); - } - } - _allLineages.currentValues.at(i) = sumSeries.back(); - } - - for (auto& sparkline : _headerSparklines) { - sparkline = createRandomWalk(rng, 40, 1.0, 0.03, trendDistribution(rng)); - } -} - void EvolutionDashboardWindow::validateAndCorrect() { _timelinesHeight = std::max(scale(100.0f), _timelinesHeight); _timelineMode = std::clamp(_timelineMode, static_cast(TimelineMode_RealTime), static_cast(TimelineMode_LastSteps)); _plotScale = std::clamp(_plotScale, static_cast(PlotScale_Linear), static_cast(PlotScale_Logarithmic)); - _lastSteps = std::clamp(_lastSteps, 1000, toInt(TimeSpan)); + _lastSteps = std::clamp(_lastSteps, 1000, 1000000000); _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 index 71eaf1bcb..2f2bb5f5b 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -1,24 +1,28 @@ #pragma once #include +#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 = 9; - static auto constexpr NumTimePoints = 300; + static auto constexpr NumMetrics = 8; private: EvolutionDashboardWindow(); @@ -26,8 +30,10 @@ class EvolutionDashboardWindow : public AlienWindow void initIntern() override; void shutdownIntern() override; void processIntern() override; + void processBackground() override; void updateCellColors(); + void updateDisplayData(); void processHeader(); void processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width, float height); void processEntitiesCard(float width, float height); @@ -38,25 +44,28 @@ class EvolutionDashboardWindow : public AlienWindow void processTimelinePlots(); void processTimelinePlot(int metricIndex, bool showTimeAxis); - void generateDummyData(); void validateAndCorrect(); - struct DummyLineage + struct LineageDisplayData { - int id = 0; - int colorBitset = 0; //at least one of the MAX_COLORS cell colors + int64_t id = 0; //-1 = "all lineages" + int colorBitset = 0; std::array currentValues = {}; - std::array, NumMetrics> series = {}; + std::array, NumMetrics> series = {}; //only filled for plotted lineages + std::vector timePoints; //only filled for plotted lineages }; - std::vector _lineages; - DummyLineage _allLineages; - std::vector _timePoints; + std::vector _lineages; //table rows from the latest sample, sorted by creature count + std::vector _plottedLineages; + LineageDisplayData _allLineages; + std::array _metricWindowDeltas = {}; std::array, 4> _headerSparklines = {}; + std::array _headerDeltas = {}; + bool _lineageMapOverflow = false; std::array _cellColors = {}; - std::set _selectedLineageIds; + std::set _selectedLineageIds; int _colorFilter = 0x3ff; //bitset for MAX_COLORS cell colors using TimelineMode = int; @@ -78,4 +87,23 @@ class EvolutionDashboardWindow : public AlienWindow int _lastSteps = 100000; float _timelinesHeight = 0; + + struct RebuildKey + { + int timelineMode = 0; + int lastSteps = 0; + double liveBackTime = 0; + double historyBackTime = 0; + std::set selectedLineageIds; + + bool operator==(RebuildKey const&) const = default; + }; + std::optional _lastRebuildKey; + + //live statistics + TimelineLiveStatistics _timelineLiveStatistics; + LineageHistoryData _liveLineageHistory; + std::vector _externalEnergySeries; + double _timeSinceSimStart = 0; //in seconds + std::optional _lastTimepoint; }; diff --git a/source/PersisterInterface/SerializerService.cpp b/source/PersisterInterface/SerializerService.cpp index 0048d3d52..bfd63c510 100644 --- a/source/PersisterInterface/SerializerService.cpp +++ b/source/PersisterInterface/SerializerService.cpp @@ -2292,7 +2292,19 @@ namespace {"Genome complexity average", true}, {"Genome complexity Maximum", true}, {"Genome complexity variance", true}, - {"System clock", false}}; + {"System clock", false}, + {"Creature count", false}, + {"Average creature cells", false}, + {"Average genome nodes", false}, + {"Creature energy", false}, + {"Average mutation rate", false}, + {"Average generation", false}, + {"Lineage count", false}, + {"Solid objects", false}, + {"Fluid objects", false}, + {"Cell objects", false}, + {"Accumulated created creatures", false}, + {"Accumulated mutations", false}}; std::variant getDataRef(int colIndex, DataPointCollection& dataPoints) { @@ -2350,6 +2362,30 @@ namespace return &dataPoints.varianceNumCells; } else if (colIndex == 26) { return &dataPoints.systemClock; + } else if (colIndex == 27) { + return &dataPoints.numCreatures; + } else if (colIndex == 28) { + return &dataPoints.averageCreatureCells; + } else if (colIndex == 29) { + return &dataPoints.averageGenomeNodes; + } else if (colIndex == 30) { + return &dataPoints.creatureEnergy; + } else if (colIndex == 31) { + return &dataPoints.averageMutationRate; + } else if (colIndex == 32) { + return &dataPoints.averageGeneration; + } else if (colIndex == 33) { + return &dataPoints.numLineages; + } else if (colIndex == 34) { + return &dataPoints.numSolidObjects; + } else if (colIndex == 35) { + return &dataPoints.numFluidObjects; + } else if (colIndex == 36) { + return &dataPoints.numCellObjects; + } else if (colIndex == 37) { + return &dataPoints.accumCreatedCreatures; + } else if (colIndex == 38) { + return &dataPoints.accumMutations; } THROW_NOT_IMPLEMENTED(); } From 4fc59dcbd16e5c86e73ee1b84f095788394f314d Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 21:36:04 +0200 Subject: [PATCH 10/41] Remove unused per-lineage sumAccumulatedMutations, derive lineage colors from genome nodes Co-Authored-By: Claude Fable 5 --- source/EngineInterface/LineageHistoryService.cpp | 1 - source/EngineInterface/LineageStatistics.h | 1 - .../LineageHistoryServiceTests.cpp | 3 --- source/EngineKernels/SimulationStatistics.cuh | 12 ++++-------- source/EngineKernels/StatisticsKernels.cu | 13 +++++++++---- source/EngineTests/EvolutionStatisticsTests.cpp | 5 +++-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/source/EngineInterface/LineageHistoryService.cpp b/source/EngineInterface/LineageHistoryService.cpp index cd4f6190f..d4450c4c1 100644 --- a/source/EngineInterface/LineageHistoryService.cpp +++ b/source/EngineInterface/LineageHistoryService.cpp @@ -61,7 +61,6 @@ LineageSample LineageHistoryService::mergeSamples(LineageSample const& earlierSa result.numGenomes = (lhs.numGenomes + rhs.numGenomes) / 2; result.sumCreatureCells = (lhs.sumCreatureCells + rhs.sumCreatureCells) / 2; result.sumCreatureGenerations = (lhs.sumCreatureGenerations + rhs.sumCreatureGenerations) / 2; - result.sumAccumulatedMutations = (lhs.sumAccumulatedMutations + rhs.sumAccumulatedMutations) / 2; result.sumGenomeNodes = (lhs.sumGenomeNodes + rhs.sumGenomeNodes) / 2; result.sumMutationRates = (lhs.sumMutationRates + rhs.sumMutationRates) / 2; result.sumCreatureEnergy = (lhs.sumCreatureEnergy + rhs.sumCreatureEnergy) / 2; diff --git a/source/EngineInterface/LineageStatistics.h b/source/EngineInterface/LineageStatistics.h index 93ced7290..31ef62836 100644 --- a/source/EngineInterface/LineageStatistics.h +++ b/source/EngineInterface/LineageStatistics.h @@ -11,7 +11,6 @@ struct LineageStatisticsEntry uint32_t numGenomes = 0; float sumCreatureCells = 0; float sumCreatureGenerations = 0; - float sumAccumulatedMutations = 0; float sumGenomeNodes = 0; float sumMutationRates = 0; float sumCreatureEnergy = 0; diff --git a/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp b/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp index 8c43b815e..f2b9fbaa2 100644 --- a/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp +++ b/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp @@ -51,7 +51,6 @@ TEST_F(LineageHistoryServiceTests, mergeSamples_commonIds) entry1.numGenomes = 8; entry1.sumCreatureCells = 100; entry1.sumCreatureGenerations = 50; - entry1.sumAccumulatedMutations = 20; entry1.sumGenomeNodes = 300; entry1.sumMutationRates = 4; entry1.sumCreatureEnergy = 1000; @@ -64,7 +63,6 @@ TEST_F(LineageHistoryServiceTests, mergeSamples_commonIds) entry2.numGenomes = 12; entry2.sumCreatureCells = 200; entry2.sumCreatureGenerations = 70; - entry2.sumAccumulatedMutations = 40; entry2.sumGenomeNodes = 500; entry2.sumMutationRates = 6; entry2.sumCreatureEnergy = 3000; @@ -91,7 +89,6 @@ TEST_F(LineageHistoryServiceTests, mergeSamples_commonIds) EXPECT_EQ(10, merged.numGenomes); EXPECT_FLOAT_EQ(150, merged.sumCreatureCells); EXPECT_FLOAT_EQ(60, merged.sumCreatureGenerations); - EXPECT_FLOAT_EQ(30, merged.sumAccumulatedMutations); EXPECT_FLOAT_EQ(400, merged.sumGenomeNodes); EXPECT_FLOAT_EQ(5, merged.sumMutationRates); EXPECT_FLOAT_EQ(2000, merged.sumCreatureEnergy); diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 0b36f7a15..c5f84dcaa 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -152,7 +152,6 @@ public: slot.numGenomes = 0; slot.sumCreatureCells = 0; slot.sumCreatureGenerations = 0; - slot.sumAccumulatedMutations = 0; slot.sumGenomeNodes = 0; slot.sumMutationRates = 0; slot.sumCreatureEnergy = 0; @@ -189,26 +188,25 @@ public: } return -1; } - __inline__ __device__ void addLineageCreatureData(int slotIndex, uint32_t numCells, uint32_t generation, float accumulatedMutations) + __inline__ __device__ void addLineageCreatureData(int slotIndex, uint32_t numCells, uint32_t generation) { auto& slot = _lineageMap[slotIndex]; atomicAdd(&slot.numCreatures, 1u); atomicAdd(&slot.sumCreatureCells, toFloat(numCells)); atomicAdd(&slot.sumCreatureGenerations, toFloat(generation)); - atomicAdd(&slot.sumAccumulatedMutations, accumulatedMutations); } - __inline__ __device__ void addLineageGenomeData(int slotIndex, float numNodes, float meanMutationRate) + __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 addLineageCellData(int slotIndex, float energy, int color) + __inline__ __device__ void addLineageEnergy(int slotIndex, float energy) { auto& slot = _lineageMap[slotIndex]; atomicAdd(&slot.sumCreatureEnergy, energy); - atomicOr(&slot.colorBitset, 1u << color); } __inline__ __device__ void compactLineageSlot(int index) { @@ -224,7 +222,6 @@ public: entry.numGenomes = slot.numGenomes; entry.sumCreatureCells = slot.sumCreatureCells; entry.sumCreatureGenerations = slot.sumCreatureGenerations; - entry.sumAccumulatedMutations = slot.sumAccumulatedMutations; entry.sumGenomeNodes = slot.sumGenomeNodes; entry.sumMutationRates = slot.sumMutationRates; entry.sumCreatureEnergy = slot.sumCreatureEnergy; @@ -343,7 +340,6 @@ private: uint32_t numGenomes; float sumCreatureCells; float sumCreatureGenerations; - float sumAccumulatedMutations; float sumGenomeNodes; float sumMutationRates; float sumCreatureEnergy; diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 8b3bb9c58..8c5fbc33c 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -156,7 +156,7 @@ __global__ void cudaUpdateEvolutionStatistics_substep2(SimulationData data, Simu statistics.addCreatureStatistics(creature->numCells, creature->generation, creature->accumulatedMutations); auto slotIndex = statistics.insertOrFindLineageSlot(creature->lineageId); if (slotIndex >= 0) { - statistics.addLineageCreatureData(slotIndex, creature->numCells, creature->generation, creature->accumulatedMutations); + statistics.addLineageCreatureData(slotIndex, creature->numCells, creature->generation); alienAtomicExch64(&creature->creatureIndex, static_cast(slotIndex) + 1); } } @@ -183,20 +183,25 @@ __global__ void cudaUpdateEvolutionStatistics_substep3(SimulationData data, Simu 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) { - numNodes += genome->genes[i].numNodes; + 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); + statistics.addLineageGenomeData(slotIndex, toFloat(numNodes), meanMutationRate, nodeColorBitset); } } auto energy = object->getEnergy(); statistics.addCreatureEnergy(energy); if (slotIndex >= 0) { - statistics.addLineageCellData(slotIndex, energy, object->color); + statistics.addLineageEnergy(slotIndex, energy); } } } diff --git a/source/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp index 09e1c00c6..6be2598e8 100644 --- a/source/EngineTests/EvolutionStatisticsTests.cpp +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -11,8 +11,9 @@ 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), GenomeDesc()) + .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); @@ -47,7 +48,7 @@ TEST_F(EvolutionStatisticsTests, basicCounts) EXPECT_FLOAT_EQ(2.0f, entry42->sumCreatureCells); EXPECT_FLOAT_EQ(3.0f, entry42->sumCreatureGenerations); EXPECT_GT(entry42->sumCreatureEnergy, 0.0f); - EXPECT_NE(0, entry42->colorBitset); + 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); From cd178652febe0cb31e412cb8673bed3bc19bfd9e Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 22:41:42 +0200 Subject: [PATCH 11/41] Run evolution statistics at deterministic timesteps The heavy evolution/lineage kernels were wall-clock throttled, so they ran at run-to-run varying timesteps and perturbed the GPU execution timing of subsequent simulation kernels, making runs non-reproducible and construction tests flaky. They are now scheduled every 10 timesteps. Also restore creatureIndex for losing threads in the dedup protocol. Co-Authored-By: Claude Fable 5 --- source/EngineImpl/SimulationCudaFacade.cu | 31 +++++++++++++++++-- source/EngineImpl/SimulationCudaFacade.cuh | 3 ++ source/EngineImpl/StatisticsKernelsService.cu | 13 ++++++-- .../EngineImpl/StatisticsKernelsService.cuh | 4 +++ source/EngineKernels/SimulationStatistics.cuh | 5 +++ source/EngineKernels/StatisticsKernels.cu | 17 ++++++---- 6 files changed, 61 insertions(+), 12 deletions(-) diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index 6eb8a1d81..b413d246f 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -51,6 +51,11 @@ 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; auto constexpr PreviewLineageMapCapacity = 1 << 12; ArraySizesForGpuEntities const PreviewCapacityGpu{10000, 10000, 10000000}; ArraySizesForTOs const PreviewCapacityTO{1000, 1000, 1000, 10000, 10000, 10000, 10000000}; @@ -443,17 +448,33 @@ StatisticsRawData _SimulationCudaFacade::getStatisticsRawData() } void _SimulationCudaFacade::updateStatistics() +{ + updateEvolutionStatistics(); + updateTimestepStatistics(); +} + +void _SimulationCudaFacade::updateTimestepStatistics() { StatisticsKernelsService::get().updateStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); syncAndCheck(); - auto lineageStatistics = _cudaSimulationStatistics->getLineageStatistics(); { std::lock_guard lock(_mutexForStatistics); _statisticsData = _cudaSimulationStatistics->getStatistics(); - _lineageStatisticsData = lineageStatistics; } StatisticsService::get().addDataPoint(_statisticsHistory, _statisticsData->timeline, getCurrentTimestep()); +} + +void _SimulationCudaFacade::updateEvolutionStatistics() +{ + StatisticsKernelsService::get().updateEvolutionStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); + syncAndCheck(); + + auto lineageStatistics = _cudaSimulationStatistics->getLineageStatistics(); + { + std::lock_guard lock(_mutexForStatistics); + _lineageStatisticsData = lineageStatistics; + } _lineageHistoryService.addSample(_lineageHistory, lineageStatistics, getCurrentTimestep()); } @@ -840,10 +861,14 @@ void _SimulationCudaFacade::calcTimestepsInternal(uint64_t timesteps, bool force cudaMemcpyToSymbol(cudaSimulationParameters, &_settings.simulationParameters, sizeof(SimulationParameters), 0, cudaMemcpyHostToDevice)); } } + if (getCurrentTimestep() % EvolutionStatisticsUpdateInterval == 0) { + updateEvolutionStatistics(); + } + auto now = std::chrono::steady_clock::now(); if (!_lastStatisticsUpdateTime || now - *_lastStatisticsUpdateTime > StatisticsUpdate) { _lastStatisticsUpdateTime = now; - updateStatistics(); + updateTimestepStatistics(); } } if (forceUpdateStatistics) { diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index 54a53ea50..5bd436cb2 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -126,6 +126,9 @@ public: private: void initCuda(); + void updateTimestepStatistics(); + void updateEvolutionStatistics(); + void syncAndCheck(); void copyDataTOtoGpu(TOs const& cudaTO, TOs const& to); void copyDataTOtoHost(TOs const& to, TOs const& cudaTO); diff --git a/source/EngineImpl/StatisticsKernelsService.cu b/source/EngineImpl/StatisticsKernelsService.cu index eb2287f43..00ca311c0 100644 --- a/source/EngineImpl/StatisticsKernelsService.cu +++ b/source/EngineImpl/StatisticsKernelsService.cu @@ -11,6 +11,16 @@ void StatisticsKernelsService::updateStatistics(CudaSettings const& gpuSettings, 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); +} + +void StatisticsKernelsService::updateEvolutionStatistics( + CudaSettings const& gpuSettings, + SimulationData const& data, + SimulationStatistics const& simulationStatistics) +{ KERNEL_CALL(cudaUpdateEvolutionStatistics_substep1, data, simulationStatistics); KERNEL_CALL(cudaUpdateEvolutionStatistics_substep2, data, simulationStatistics); KERNEL_CALL(cudaUpdateEvolutionStatistics_substep3, data, simulationStatistics); @@ -21,7 +31,4 @@ void StatisticsKernelsService::updateStatistics(CudaSettings const& gpuSettings, KERNEL_CALL(cudaLineageAccumulatorGC, simulationStatistics); KERNEL_CALL_1_1(cudaFinishLineageAccumulatorGC, simulationStatistics); } - KERNEL_CALL_1_1(cudaUpdateHistogramData_substep1, data, simulationStatistics); - KERNEL_CALL(cudaUpdateHistogramData_substep2, data, simulationStatistics); - KERNEL_CALL(cudaUpdateHistogramData_substep3, data, simulationStatistics); } diff --git a/source/EngineImpl/StatisticsKernelsService.cuh b/source/EngineImpl/StatisticsKernelsService.cuh index 8caa9f110..6cc60cd4c 100644 --- a/source/EngineImpl/StatisticsKernelsService.cuh +++ b/source/EngineImpl/StatisticsKernelsService.cuh @@ -18,6 +18,10 @@ public: void updateStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics); + // 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 updateEvolutionStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics); + private: StatisticsKernelsService() = default; }; diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index c5f84dcaa..4d52c8c53 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -74,7 +74,11 @@ public: __inline__ __device__ void resetTimestepData() { if (threadIdx.x == 0 && blockIdx.x == 0) { + // Evolution statistics are collected at deterministic timesteps (separate kernel sequence) + // and must survive the wall-clock-throttled timestep statistics updates. + auto evolution = _data->timeline.timestep.evolution; _data->timeline.timestep = TimestepStatistics(); + _data->timeline.timestep.evolution = evolution; } auto partition = calcSystemThreadPartition(MutantToColorCountMapSize); for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { @@ -121,6 +125,7 @@ public: } //evolution statistics (timestep) + __inline__ __device__ void resetEvolutionStatistics() { _data->timeline.timestep.evolution = EvolutionStatistics(); } __inline__ __device__ void incNumSolidObjects() { atomicAdd(&_data->timeline.timestep.evolution.numSolidObjects, 1); } __inline__ __device__ void incNumFluidObjects() { atomicAdd(&_data->timeline.timestep.evolution.numFluidObjects, 1); } __inline__ __device__ void incNumCellObjects() { atomicAdd(&_data->timeline.timestep.evolution.numCellObjects, 1); } diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 8c5fbc33c..1f09caaa9 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -17,12 +17,6 @@ __global__ void cudaUpdateTimestepStatistics_substep2(SimulationData data, Simul statistics.addEnergy(object->color, object->getEnergy()); if (object->type == ObjectType_FreeCell) { statistics.incNumFreeCells(object->color); - } else 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->typeData.cell.cellType == CellType_Constructor && GenomeDecoder::containsSelfReplication(object->typeData.cell.cellTypeData.constructor)) { // statistics.incNumReplicator(object->color); @@ -111,6 +105,7 @@ namespace __global__ void cudaUpdateEvolutionStatistics_substep1(SimulationData data, SimulationStatistics statistics) { if (threadIdx.x == 0 && blockIdx.x == 0) { + statistics.resetEvolutionStatistics(); statistics.resetCompactLineageCounter(); } { @@ -144,6 +139,13 @@ __global__ void cudaUpdateEvolutionStatistics_substep2(SimulationData data, Simu for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { auto& object = objects.at(index); + 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; } @@ -159,6 +161,9 @@ __global__ void cudaUpdateEvolutionStatistics_substep2(SimulationData data, Simu 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); } } } From c26dd23af55ff940d5425b761dcdee37ab695eb9 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Sun, 12 Jul 2026 22:41:43 +0200 Subject: [PATCH 12/41] Print actual angles on creature test failures Co-Authored-By: Claude Fable 5 --- source/EngineTests/CreatureTests.cpp | 43 +++++++++++++++++----------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/source/EngineTests/CreatureTests.cpp b/source/EngineTests/CreatureTests.cpp index 09865cbd5..bfc798cd5 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 From 888df0932caabfeb0b00c59af68712841cca0d8f Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 17:29:14 +0200 Subject: [PATCH 13/41] Minor changes --- source/EngineKernels/SimulationStatistics.cuh | 17 +++++++++-------- source/EngineTests/CMakeLists.txt | 5 ++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 4d52c8c53..8d1b2edf9 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -19,8 +19,8 @@ public: CudaMemoryManager::getInstance().acquireMemory(MutantToColorCountMapSize, _mutantToMutantStatisticsMap); CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageMap); CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageCompactData); - CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[0]); - CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[1]); + CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[0]); + CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[1]); CudaMemoryManager::getInstance().acquireMemory(1, _lineageMapControl); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(StatisticsRawData))); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMapControl, 0, sizeof(LineageMapControl))); @@ -29,8 +29,8 @@ public: CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMap, 0, sizeof(LineageMapSlot) * _lineageMapCapacity)); CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageMap, sizeof(LineageMapSlot), 0xff, sizeof(uint32_t), _lineageMapCapacity)); for (int i = 0; i < 2; ++i) { - CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMaps[i], 0, sizeof(LineageAccumulatorSlot) * _lineageMapCapacity)); - CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageAccumulatorMaps[i], sizeof(LineageAccumulatorSlot), 0xff, sizeof(uint32_t), _lineageMapCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMaps[i], 0, sizeof(LineageAccumulatorMapSlot) * _lineageMapCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageAccumulatorMaps[i], sizeof(LineageAccumulatorMapSlot), 0xff, sizeof(uint32_t), _lineageMapCapacity)); } } @@ -349,7 +349,7 @@ private: float sumMutationRates; float sumCreatureEnergy; }; - struct LineageAccumulatorSlot + struct LineageAccumulatorMapSlot { uint32_t lineageId; // LineageIdEmpty = slot is unused uint32_t numCreatedCreatures; @@ -369,7 +369,7 @@ private: return result; } - __inline__ __device__ LineageAccumulatorSlot* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageMapControl->activeAccumulatorBuffer]; } + __inline__ __device__ LineageAccumulatorMapSlot* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageMapControl->activeAccumulatorBuffer]; } __inline__ __device__ int insertAccumulatorSlot(uint32_t bufferIndex, uint32_t lineageId) { @@ -415,9 +415,10 @@ private: int _lineageMapCapacity; LineageMapSlot* _lineageMap; - LineageStatisticsEntry* _lineageCompactData; - LineageAccumulatorSlot* _lineageAccumulatorMaps[2]; + LineageAccumulatorMapSlot* _lineageAccumulatorMaps[2]; + LineageMapControl* _lineageMapControl; + LineageStatisticsEntry* _lineageCompactData; //for diversity calculation static auto constexpr MutantToColorCountMapSize = 1 << 20; diff --git a/source/EngineTests/CMakeLists.txt b/source/EngineTests/CMakeLists.txt index 79fd679ac..66884d677 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 @@ -55,9 +56,7 @@ PUBLIC TrimGeneMutationTests.cpp UnusedGeneRemovalTests.cpp VoidMutationTests.cpp - VoidTests.cpp - # Runs last: consumes GPU RNG state, which would otherwise shift the mutation sequences of subsequent suites - EvolutionStatisticsTests.cpp) + VoidTests.cpp) target_link_libraries(EngineTests Base) target_link_libraries(EngineTests EngineKernels) From ac40125a813ac45c1c9087072b3b059ac42caf18 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 17:56:39 +0200 Subject: [PATCH 14/41] Replace evolution/creature statistics with dedicated OverallStatisticsEntry Move EvolutionStatistics and the accumulated numCreatedCreatures/totalMutations out of the bulk-copied StatisticsRawData into a standalone OverallStatisticsEntry buffer on SimulationStatistics, analogous to the per-lineage entries. Also drop the unused MutantStatistics diversity-tracking map, and make the Evolution Dashboard timeline plots always render a filled area like StatisticsWindow. --- source/EngineImpl/EngineWorker.cpp | 5 + source/EngineImpl/EngineWorker.h | 2 + source/EngineImpl/SimulationCudaFacade.cu | 16 +++- source/EngineImpl/SimulationCudaFacade.cuh | 3 + source/EngineImpl/SimulationFacadeImpl.cpp | 5 + source/EngineImpl/SimulationFacadeImpl.h | 1 + source/EngineImpl/StatisticsService.cu | 9 +- source/EngineImpl/StatisticsService.cuh | 7 +- source/EngineInterface/CMakeLists.txt | 1 + source/EngineInterface/OverallStatistics.h | 22 +++++ source/EngineInterface/SimulationFacade.h | 2 + .../StatisticsConverterService.cpp | 27 +++--- .../StatisticsConverterService.h | 2 + source/EngineInterface/StatisticsRawData.h | 20 ---- source/EngineKernels/SimulationStatistics.cuh | 96 +++++++++---------- source/EngineKernels/StatisticsKernels.cu | 1 - .../EngineTests/EvolutionStatisticsTests.cpp | 39 ++++---- source/Gui/EvolutionDashboardWindow.cpp | 13 ++- source/Gui/StatisticsWindow.cpp | 3 +- source/Gui/TimelineLiveStatistics.cpp | 4 +- source/Gui/TimelineLiveStatistics.h | 3 +- 21 files changed, 162 insertions(+), 119 deletions(-) create mode 100644 source/EngineInterface/OverallStatistics.h diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index c8de8ed60..a5eaac719 100644 --- a/source/EngineImpl/EngineWorker.cpp +++ b/source/EngineImpl/EngineWorker.cpp @@ -119,6 +119,11 @@ RawLineageStatistics EngineWorker::getRawLineageStatistics() const return _simulationCudaFacade->getRawLineageStatistics(); } +OverallStatisticsEntry EngineWorker::getOverallStatistics() const +{ + return _simulationCudaFacade->getOverallStatistics(); +} + LineageHistory const& EngineWorker::getLineageHistory() const { return _simulationCudaFacade->getLineageHistory(); diff --git a/source/EngineImpl/EngineWorker.h b/source/EngineImpl/EngineWorker.h index 0b72ab58d..5252974ab 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ class EngineWorker StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); RawLineageStatistics getRawLineageStatistics() const; + OverallStatisticsEntry getOverallStatistics() const; LineageHistory const& getLineageHistory() const; void addAndSelectSimulationData(Desc&& dataToUpdate); diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index b413d246f..f1aeff4e6 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -458,11 +458,13 @@ void _SimulationCudaFacade::updateTimestepStatistics() StatisticsKernelsService::get().updateStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); syncAndCheck(); + OverallStatisticsEntry overallStatistics; { std::lock_guard lock(_mutexForStatistics); _statisticsData = _cudaSimulationStatistics->getStatistics(); + overallStatistics = _overallStatisticsData.value_or(OverallStatisticsEntry()); } - StatisticsService::get().addDataPoint(_statisticsHistory, _statisticsData->timeline, getCurrentTimestep()); + StatisticsService::get().addDataPoint(_statisticsHistory, _statisticsData->timeline, overallStatistics, getCurrentTimestep()); } void _SimulationCudaFacade::updateEvolutionStatistics() @@ -471,9 +473,11 @@ void _SimulationCudaFacade::updateEvolutionStatistics() syncAndCheck(); auto lineageStatistics = _cudaSimulationStatistics->getLineageStatistics(); + auto overallStatistics = _cudaSimulationStatistics->getOverallStatistics(); { std::lock_guard lock(_mutexForStatistics); _lineageStatisticsData = lineageStatistics; + _overallStatisticsData = overallStatistics; } _lineageHistoryService.addSample(_lineageHistory, lineageStatistics, getCurrentTimestep()); } @@ -493,6 +497,16 @@ RawLineageStatistics _SimulationCudaFacade::getRawLineageStatistics() } } +OverallStatisticsEntry _SimulationCudaFacade::getOverallStatistics() +{ + std::lock_guard lock(_mutexForStatistics); + if (_overallStatisticsData) { + return *_overallStatisticsData; + } else { + return OverallStatisticsEntry(); + } +} + LineageHistory const& _SimulationCudaFacade::getLineageHistory() const { return _lineageHistory; diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index 5bd436cb2..1776d78f7 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,7 @@ public: StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); RawLineageStatistics getRawLineageStatistics(); + OverallStatisticsEntry getOverallStatistics(); LineageHistory const& getLineageHistory() const; void resetTimeIntervalStatistics(); @@ -165,6 +167,7 @@ private: std::optional _statisticsData; StatisticsHistory _statisticsHistory; std::optional _lineageStatisticsData; + std::optional _overallStatisticsData; LineageHistory _lineageHistory; LineageHistoryService _lineageHistoryService; std::shared_ptr _cudaSimulationStatistics; diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index da19943dc..e7bb38d28 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -340,6 +340,11 @@ RawLineageStatistics _SimulationFacadeImpl::getRawLineageStatistics() const return _worker.getRawLineageStatistics(); } +OverallStatisticsEntry _SimulationFacadeImpl::getOverallStatistics() const +{ + return _worker.getOverallStatistics(); +} + LineageHistory const& _SimulationFacadeImpl::getLineageHistory() const { return _worker.getLineageHistory(); diff --git a/source/EngineImpl/SimulationFacadeImpl.h b/source/EngineImpl/SimulationFacadeImpl.h index f46549d5a..66deebc30 100644 --- a/source/EngineImpl/SimulationFacadeImpl.h +++ b/source/EngineImpl/SimulationFacadeImpl.h @@ -92,6 +92,7 @@ class _SimulationFacadeImpl : public _SimulationFacade StatisticsHistory const& getStatisticsHistory() const override; void setStatisticsHistory(StatisticsHistoryData const& data) override; RawLineageStatistics getRawLineageStatistics() const override; + OverallStatisticsEntry getOverallStatistics() const override; LineageHistory const& getLineageHistory() const override; std::optional getTpsRestriction() const override; diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index 98df8a041..f9c85e6fb 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -10,7 +10,11 @@ namespace auto constexpr MaxSamples = 1000; } -void StatisticsService::addDataPoint(StatisticsHistory& history, TimelineStatistics const& newTimelineStatistics, uint64_t timestep) +void StatisticsService::addDataPoint( + StatisticsHistory& history, + TimelineStatistics const& newTimelineStatistics, + OverallStatisticsEntry const& overallStatistics, + uint64_t timestep) { std::lock_guard lock(history.getMutex()); auto& historyData = history.getDataRef(); @@ -28,7 +32,8 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, TimelineStatist result.time = toDouble(timestep); return result; } else { - return StatisticsConverterService::get().convert(newTimelineStatistics, timestep, toDouble(timestep), _lastTimelineStatistics, _lastTimestep); + return StatisticsConverterService::get().convert( + newTimelineStatistics, overallStatistics, timestep, toDouble(timestep), _lastTimelineStatistics, _lastTimestep); } }(); diff --git a/source/EngineImpl/StatisticsService.cuh b/source/EngineImpl/StatisticsService.cuh index bc648f00f..eb97e1233 100644 --- a/source/EngineImpl/StatisticsService.cuh +++ b/source/EngineImpl/StatisticsService.cuh @@ -2,6 +2,7 @@ #include +#include #include #include @@ -11,7 +12,11 @@ class StatisticsService MAKE_SINGLETON(StatisticsService); public: - void addDataPoint(StatisticsHistory& history, TimelineStatistics const& newTimelineStatistics, uint64_t timestep); + void addDataPoint( + StatisticsHistory& history, + TimelineStatistics const& newTimelineStatistics, + OverallStatisticsEntry const& overallStatistics, + uint64_t timestep); void resetTime(StatisticsHistory& history, uint64_t timestep); void rewriteHistory(StatisticsHistory& history, StatisticsHistoryData const& newHistoryData, uint64_t timestep); diff --git a/source/EngineInterface/CMakeLists.txt b/source/EngineInterface/CMakeLists.txt index 5e299de60..ece228a8b 100644 --- a/source/EngineInterface/CMakeLists.txt +++ b/source/EngineInterface/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(EngineInterface NeuralNetWeight.h NumberGenerator.cpp NumberGenerator.h + OverallStatistics.h ParametersEditService.cpp ParametersEditService.h ParametersFilter.h diff --git a/source/EngineInterface/OverallStatistics.h b/source/EngineInterface/OverallStatistics.h new file mode 100644 index 000000000..639034ad7 --- /dev/null +++ b/source/EngineInterface/OverallStatistics.h @@ -0,0 +1,22 @@ +#pragma once + +#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 numActiveLineages = 0; + uint32_t lineageMapOverflow = 0; + uint32_t numCreatedCreatures = 0; // Accumulated, never reset + float totalMutations = 0; // Accumulated, never reset +}; diff --git a/source/EngineInterface/SimulationFacade.h b/source/EngineInterface/SimulationFacade.h index 0ed0a7833..6970dbaf8 100644 --- a/source/EngineInterface/SimulationFacade.h +++ b/source/EngineInterface/SimulationFacade.h @@ -6,6 +6,7 @@ #include "GeometryBuffers.h" #include "LineageHistory.h" #include "LineageStatistics.h" +#include "OverallStatistics.h" #include "PreviewDesc.h" #include "SelectionShallowData.h" #include "SettingsForSimulation.h" @@ -109,6 +110,7 @@ class _SimulationFacade virtual StatisticsHistory const& getStatisticsHistory() const = 0; virtual void setStatisticsHistory(StatisticsHistoryData const& data) = 0; virtual RawLineageStatistics getRawLineageStatistics() const = 0; + virtual OverallStatisticsEntry getOverallStatistics() const = 0; virtual LineageHistory const& getLineageHistory() const = 0; //******************** diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index 0ad04393a..5b8af0e84 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -79,6 +79,7 @@ namespace DataPointCollection StatisticsConverterService::convert( TimelineStatistics const& data, + OverallStatisticsEntry const& overallStatistics, uint64_t timestep, double time, std::optional const& lastData, @@ -100,19 +101,19 @@ DataPointCollection StatisticsConverterService::convert( result.averageNumCells = getDataPointByAveraging(data.timestep.numObjects, data.timestep.numSelfReplicators); result.totalEnergy = getDataPointBySummation(data.timestep.totalEnergy); - auto const& evolution = data.timestep.evolution; - result.numCreatures = toDouble(evolution.numCreatures); - result.averageCreatureCells = evolution.numCreatures > 0 ? toDouble(evolution.sumCreatureCells) / evolution.numCreatures : 0.0; - result.averageGeneration = evolution.numCreatures > 0 ? toDouble(evolution.sumCreatureGenerations) / evolution.numCreatures : 0.0; - result.averageGenomeNodes = evolution.numGenomes > 0 ? toDouble(evolution.sumGenomeNodes) / evolution.numGenomes : 0.0; - result.averageMutationRate = evolution.numGenomes > 0 ? toDouble(evolution.sumMutationRates) / evolution.numGenomes : 0.0; - result.creatureEnergy = toDouble(evolution.sumCreatureEnergy); - result.numLineages = toDouble(evolution.numActiveLineages); - result.numSolidObjects = toDouble(evolution.numSolidObjects); - result.numFluidObjects = toDouble(evolution.numFluidObjects); - result.numCellObjects = toDouble(evolution.numCellObjects); - result.accumCreatedCreatures = toDouble(data.accumulated.numCreatedCreatures); - result.accumMutations = data.accumulated.totalMutations; + auto const& overall = overallStatistics; + result.numCreatures = toDouble(overall.numCreatures); + result.averageCreatureCells = overall.numCreatures > 0 ? toDouble(overall.sumCreatureCells) / overall.numCreatures : 0.0; + result.averageGeneration = overall.numCreatures > 0 ? toDouble(overall.sumCreatureGenerations) / overall.numCreatures : 0.0; + result.averageGenomeNodes = overall.numGenomes > 0 ? toDouble(overall.sumGenomeNodes) / overall.numGenomes : 0.0; + result.averageMutationRate = overall.numGenomes > 0 ? toDouble(overall.sumMutationRates) / overall.numGenomes : 0.0; + result.creatureEnergy = toDouble(overall.sumCreatureEnergy); + result.numLineages = toDouble(overall.numActiveLineages); + result.numSolidObjects = toDouble(overall.numSolidObjects); + result.numFluidObjects = toDouble(overall.numFluidObjects); + result.numCellObjects = toDouble(overall.numCellObjects); + result.accumCreatedCreatures = toDouble(overall.numCreatedCreatures); + result.accumMutations = toDouble(overall.totalMutations); auto deltaTimesteps = lastTimestep ? toDouble(timestep) - toDouble(*lastTimestep) : 1.0; if (deltaTimesteps < NEAR_ZERO) { diff --git a/source/EngineInterface/StatisticsConverterService.h b/source/EngineInterface/StatisticsConverterService.h index 7f518826f..fa562e279 100644 --- a/source/EngineInterface/StatisticsConverterService.h +++ b/source/EngineInterface/StatisticsConverterService.h @@ -3,6 +3,7 @@ #include #include +#include class StatisticsConverterService { @@ -11,6 +12,7 @@ class StatisticsConverterService public: DataPointCollection convert( TimelineStatistics const& newData, + OverallStatisticsEntry const& overallStatistics, uint64_t timestep, double time, std::optional const& lastData, diff --git a/source/EngineInterface/StatisticsRawData.h b/source/EngineInterface/StatisticsRawData.h index ec0054cb4..b7041c1d7 100644 --- a/source/EngineInterface/StatisticsRawData.h +++ b/source/EngineInterface/StatisticsRawData.h @@ -3,23 +3,6 @@ #include #include -struct EvolutionStatistics -{ - int numCreatures = 0; - float sumCreatureCells = 0; - float sumCreatureGenerations = 0; - float sumAccumulatedMutations = 0; - int numGenomes = 0; - float sumGenomeNodes = 0; - float sumMutationRates = 0; // Sum over genomes of the per-genome mean of all mutation probabilities - float sumCreatureEnergy = 0; - int numSolidObjects = 0; - int numFluidObjects = 0; - int numCellObjects = 0; - int numActiveLineages = 0; - int lineageMapOverflow = 0; -}; - struct TimestepStatistics { ColorVector numObjects = {}; @@ -29,7 +12,6 @@ struct TimestepStatistics ColorVector numFreeCells = {}; ColorVector numEnergyParticles = {}; ColorVector totalEnergy = {}; - EvolutionStatistics evolution; }; struct AccumulatedStatistics @@ -49,8 +31,6 @@ struct AccumulatedStatistics ColorVector numReconnectorCreated = {}; ColorVector numReconnectorRemoved = {}; ColorVector numDetonations = {}; - uint64_t numCreatedCreatures = 0; - double totalMutations = 0; }; struct TimelineStatistics diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 8d1b2edf9..2a56f45b5 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "Base.cuh" @@ -16,13 +17,14 @@ public: { _lineageMapCapacity = lineageMapCapacity; CudaMemoryManager::getInstance().acquireMemory(1, _data); - CudaMemoryManager::getInstance().acquireMemory(MutantToColorCountMapSize, _mutantToMutantStatisticsMap); + CudaMemoryManager::getInstance().acquireMemory(1, _overallStatisticsEntry); CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageMap); CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageCompactData); CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[0]); CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[1]); CudaMemoryManager::getInstance().acquireMemory(1, _lineageMapControl); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(StatisticsRawData))); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_overallStatisticsEntry, 0, sizeof(OverallStatisticsEntry))); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMapControl, 0, sizeof(LineageMapControl))); // Values must start at zero; the lineageId key column (first member) is set to LineageIdEmpty @@ -37,7 +39,7 @@ public: __host__ void free() { CudaMemoryManager::getInstance().freeMemory(_data); - CudaMemoryManager::getInstance().freeMemory(_mutantToMutantStatisticsMap); + CudaMemoryManager::getInstance().freeMemory(_overallStatisticsEntry); CudaMemoryManager::getInstance().freeMemory(_lineageMap); CudaMemoryManager::getInstance().freeMemory(_lineageCompactData); CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[0]); @@ -52,6 +54,13 @@ public: return result; } + __host__ OverallStatisticsEntry getOverallStatistics() + { + OverallStatisticsEntry result; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _overallStatisticsEntry, sizeof(OverallStatisticsEntry), cudaMemcpyDeviceToHost)); + return result; + } + __host__ RawLineageStatistics getLineageStatistics() { auto control = getLineageMapControl(); @@ -74,17 +83,7 @@ public: __inline__ __device__ void resetTimestepData() { if (threadIdx.x == 0 && blockIdx.x == 0) { - // Evolution statistics are collected at deterministic timesteps (separate kernel sequence) - // and must survive the wall-clock-throttled timestep statistics updates. - auto evolution = _data->timeline.timestep.evolution; _data->timeline.timestep = TimestepStatistics(); - _data->timeline.timestep.evolution = evolution; - } - 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; } } @@ -111,12 +110,6 @@ public: } return result; } - __inline__ __device__ void incMutant(int color, uint32_t lineageId, float numCells) - { - atomicAdd(&_mutantToMutantStatisticsMap[lineageId % MutantToColorCountMapSize].count, 1); - atomicMax(&_mutantToMutantStatisticsMap[lineageId % MutantToColorCountMapSize].color, color); - atomicAdd(&_mutantToMutantStatisticsMap[lineageId % MutantToColorCountMapSize].numObjects, numCells); - } __inline__ __device__ void halveNumConnections() { for (int i = 0; i < MAX_COLORS; ++i) { @@ -125,26 +118,43 @@ public: } //evolution statistics (timestep) - __inline__ __device__ void resetEvolutionStatistics() { _data->timeline.timestep.evolution = EvolutionStatistics(); } - __inline__ __device__ void incNumSolidObjects() { atomicAdd(&_data->timeline.timestep.evolution.numSolidObjects, 1); } - __inline__ __device__ void incNumFluidObjects() { atomicAdd(&_data->timeline.timestep.evolution.numFluidObjects, 1); } - __inline__ __device__ void incNumCellObjects() { atomicAdd(&_data->timeline.timestep.evolution.numCellObjects, 1); } + __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.numActiveLineages = 0; + overall.lineageMapOverflow = 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 addCreatureStatistics(uint32_t numCells, uint32_t generation, float accumulatedMutations) { - auto& evolution = _data->timeline.timestep.evolution; - atomicAdd(&evolution.numCreatures, 1); - atomicAdd(&evolution.sumCreatureCells, toFloat(numCells)); - atomicAdd(&evolution.sumCreatureGenerations, toFloat(generation)); - atomicAdd(&evolution.sumAccumulatedMutations, 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& evolution = _data->timeline.timestep.evolution; - atomicAdd(&evolution.numGenomes, 1); - atomicAdd(&evolution.sumGenomeNodes, numNodes); - atomicAdd(&evolution.sumMutationRates, meanMutationRate); + auto& overall = *_overallStatisticsEntry; + atomicAdd(&overall.numGenomes, 1u); + atomicAdd(&overall.sumGenomeNodes, numNodes); + atomicAdd(&overall.sumMutationRates, meanMutationRate); } - __inline__ __device__ void addCreatureEnergy(float value) { atomicAdd(&_data->timeline.timestep.evolution.sumCreatureEnergy, value); } + __inline__ __device__ void addCreatureEnergy(float value) { atomicAdd(&_overallStatisticsEntry->sumCreatureEnergy, value); } //lineage statistics __inline__ __device__ int getLineageMapCapacity() const { return _lineageMapCapacity; } @@ -174,7 +184,7 @@ public: } index = (index + 1) & mask; } - _data->timeline.timestep.evolution.lineageMapOverflow = 1; + _overallStatisticsEntry->lineageMapOverflow = 1; return -1; } __inline__ __device__ int findLineageSlot(uint32_t lineageId) const @@ -239,10 +249,7 @@ public: entry.totalMutations = accumulatorSlot.totalMutations; } } - __inline__ __device__ void finalizeLineageStatistics() - { - _data->timeline.timestep.evolution.numActiveLineages = toInt(_lineageMapControl->numCompactEntries); - } + __inline__ __device__ void finalizeLineageStatistics() { _overallStatisticsEntry->numActiveLineages = _lineageMapControl->numCompactEntries; } //lineage accumulator map (persistent, garbage-collected occasionally) __inline__ __device__ void resetInactiveAccumulatorSlot(int index) @@ -307,7 +314,7 @@ public: __inline__ __device__ void incCreatedCreature(uint32_t lineageId) { - alienAtomicAdd64(&_data->timeline.accumulated.numCreatedCreatures, uint64_t(1)); + atomicAdd(&_overallStatisticsEntry->numCreatedCreatures, 1u); auto slotIndex = findOrInsertAccumulatorSlot(lineageId); if (slotIndex >= 0) { atomicAdd(&getActiveAccumulatorMap()[slotIndex].numCreatedCreatures, 1u); @@ -315,7 +322,7 @@ public: } __inline__ __device__ void addMutations(uint32_t lineageId, float value) { - atomicAdd(&_data->timeline.accumulated.totalMutations, toDouble(value)); + atomicAdd(&_overallStatisticsEntry->totalMutations, value); auto slotIndex = findOrInsertAccumulatorSlot(lineageId); if (slotIndex >= 0) { atomicAdd(&getActiveAccumulatorMap()[slotIndex].totalMutations, value); @@ -412,6 +419,7 @@ private: } StatisticsRawData* _data; + OverallStatisticsEntry* _overallStatisticsEntry; int _lineageMapCapacity; LineageMapSlot* _lineageMap; @@ -419,14 +427,4 @@ private: LineageMapControl* _lineageMapControl; LineageStatisticsEntry* _lineageCompactData; - - //for diversity calculation - static auto constexpr MutantToColorCountMapSize = 1 << 20; - struct MutantStatistics - { - uint32_t color; - uint32_t count; - float numObjects; - }; - MutantStatistics* _mutantToMutantStatisticsMap; }; diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 1f09caaa9..450b738cc 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -20,7 +20,6 @@ __global__ void cudaUpdateTimestepStatistics_substep2(SimulationData data, Simul } //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); diff --git a/source/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp index 6be2598e8..b55f6aa51 100644 --- a/source/EngineTests/EvolutionStatisticsTests.cpp +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -2,7 +2,6 @@ #include #include -#include #include "MutationTestsBase.h" @@ -18,18 +17,17 @@ TEST_F(EvolutionStatisticsTests, basicCounts) _simulationFacade->setSimulationData(data); - auto statistics = _simulationFacade->getStatisticsRawData(); - auto const& evolution = statistics.timeline.timestep.evolution; - EXPECT_EQ(2, evolution.numCreatures); - EXPECT_EQ(2, evolution.numGenomes); - EXPECT_EQ(2, evolution.numActiveLineages); - EXPECT_EQ(3, evolution.numCellObjects); - EXPECT_EQ(0, evolution.numSolidObjects); - EXPECT_EQ(0, evolution.numFluidObjects); - EXPECT_EQ(0, evolution.lineageMapOverflow); - EXPECT_FLOAT_EQ(3.0f, evolution.sumCreatureCells); - EXPECT_FLOAT_EQ(8.0f, evolution.sumCreatureGenerations); - EXPECT_GT(evolution.sumCreatureEnergy, 0.0f); + auto overall = _simulationFacade->getOverallStatistics(); + EXPECT_EQ(2u, overall.numCreatures); + EXPECT_EQ(2u, overall.numGenomes); + EXPECT_EQ(2u, overall.numActiveLineages); + EXPECT_EQ(3u, overall.numCellObjects); + EXPECT_EQ(0u, overall.numSolidObjects); + EXPECT_EQ(0u, overall.numFluidObjects); + EXPECT_EQ(0u, overall.lineageMapOverflow); + EXPECT_FLOAT_EQ(3.0f, overall.sumCreatureCells); + EXPECT_FLOAT_EQ(8.0f, overall.sumCreatureGenerations); + EXPECT_GT(overall.sumCreatureEnergy, 0.0f); auto lineageStatistics = _simulationFacade->getRawLineageStatistics(); ASSERT_EQ(2, lineageStatistics.entries.size()); @@ -69,11 +67,10 @@ TEST_F(EvolutionStatisticsTests, genomeNodesAndMutationRates) auto data = Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42), genome); _simulationFacade->setSimulationData(data); - auto statistics = _simulationFacade->getStatisticsRawData(); - auto const& evolution = statistics.timeline.timestep.evolution; - EXPECT_EQ(1, evolution.numGenomes); - EXPECT_FLOAT_EQ(toFloat(numNodes), evolution.sumGenomeNodes); - EXPECT_FLOAT_EQ(0.01f, evolution.sumMutationRates); //mean over 21 probability values: 0.21 / 21 + auto overall = _simulationFacade->getOverallStatistics(); + EXPECT_EQ(1u, overall.numGenomes); + EXPECT_FLOAT_EQ(toFloat(numNodes), overall.sumGenomeNodes); + EXPECT_FLOAT_EQ(0.01f, overall.sumMutationRates); //mean over 21 probability values: 0.21 / 21 } TEST_F(EvolutionStatisticsTests, accumulatedMutations) @@ -94,9 +91,9 @@ TEST_F(EvolutionStatisticsTests, accumulatedMutations) } _simulationFacade->calcTimesteps(1); - auto statistics = _simulationFacade->getStatisticsRawData(); - EXPECT_GT(statistics.timeline.timestep.evolution.sumAccumulatedMutations, 0.0f); - EXPECT_GT(statistics.timeline.accumulated.totalMutations, 0.0); + auto overall = _simulationFacade->getOverallStatistics(); + EXPECT_GT(overall.sumAccumulatedMutations, 0.0f); + EXPECT_GT(overall.totalMutations, 0.0f); auto lineageStatistics = _simulationFacade->getRawLineageStatistics(); ASSERT_EQ(1, lineageStatistics.entries.size()); diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 52935590a..18d2af508 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -239,8 +239,9 @@ void EvolutionDashboardWindow::processBackground() _timeSinceSimStart += toDouble(duration) / 1000; auto rawStatistics = _SimulationFacade::get()->getStatisticsRawData(); - _timelineLiveStatistics.update(rawStatistics.timeline, _SimulationFacade::get()->getCurrentTimestep()); - _lineageMapOverflow = rawStatistics.timeline.timestep.evolution.lineageMapOverflow != 0; + auto overallStatistics = _SimulationFacade::get()->getOverallStatistics(); + _timelineLiveStatistics.update(rawStatistics.timeline, overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); + _lineageMapOverflow = overallStatistics.lineageMapOverflow != 0; auto rawLineageStatistics = _SimulationFacade::get()->getRawLineageStatistics(); LineageSample sample; @@ -814,11 +815,9 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim auto const& series = lineage->series.at(metricIndex); ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); ImPlot::PlotLine("##line", lineage->timePoints.data(), series.data(), count); - if (_plotScale == PlotScale_Linear) { - ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.15f * ImGui::GetStyle().Alpha); - ImPlot::PlotShaded("##shaded", lineage->timePoints.data(), series.data(), count); - ImPlot::PopStyleVar(); - } + ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.5f * ImGui::GetStyle().Alpha); + ImPlot::PlotShaded("##shaded", lineage->timePoints.data(), series.data(), count); + ImPlot::PopStyleVar(); ImPlot::PopStyleColor(); if (ImGui::GetStyle().Alpha == 1.0f) { ImPlot::Annotation( diff --git a/source/Gui/StatisticsWindow.cpp b/source/Gui/StatisticsWindow.cpp index d0bb8423b..2c1e12c7e 100644 --- a/source/Gui/StatisticsWindow.cpp +++ b/source/Gui/StatisticsWindow.cpp @@ -535,8 +535,9 @@ void StatisticsWindow::processBackground() if (!_lastTimepoint || duration > LiveStatisticsDeltaTime) { _lastTimepoint = timepoint; auto rawStatistics = _SimulationFacade::get()->getStatisticsRawData(); + auto overallStatistics = _SimulationFacade::get()->getOverallStatistics(); _histogramLiveStatistics.update(rawStatistics.histogram); - _timelineLiveStatistics.update(rawStatistics.timeline, _SimulationFacade::get()->getCurrentTimestep()); + _timelineLiveStatistics.update(rawStatistics.timeline, overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); _tableLiveStatistics.update(rawStatistics.timeline); } } diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index 75995636c..239a24eee 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -14,7 +14,7 @@ std::vector const& TimelineLiveStatistics::getDataPointColl return _dataPointCollectionHistory; } -void TimelineLiveStatistics::update(TimelineStatistics const& data, uint64_t timestep) +void TimelineLiveStatistics::update(TimelineStatistics const& data, OverallStatisticsEntry const& overallStatistics, uint64_t timestep) { truncate(); @@ -24,7 +24,7 @@ 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(data, overallStatistics, timestep, _timeSinceSimStart, _lastData, _lastTimestep); _dataPointCollectionHistory.emplace_back(newDataPoint); _lastData = data; _lastTimestep = timestep; diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index a63fe7ec3..ea7c9e8ff 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -6,6 +6,7 @@ #include #include +#include #include class TimelineLiveStatistics @@ -14,7 +15,7 @@ class TimelineLiveStatistics static auto constexpr MaxLiveHistory = 240.0f; //in seconds std::vector const& getDataPointCollectionHistory() const; - void update(TimelineStatistics const& statistics, uint64_t timestep); + void update(TimelineStatistics const& statistics, OverallStatisticsEntry const& overallStatistics, uint64_t timestep); private: void truncate(); From 766ad0313aeed2c0ee29e1607c91c3c597da77fe Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 18:29:26 +0200 Subject: [PATCH 15/41] SimulationStatistics simplified, part 1 --- source/EngineImpl/SimulationCudaFacade.cu | 3 +- source/EngineKernels/SimulationStatistics.cuh | 97 ++++++++++--------- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index f1aeff4e6..e2e500aa9 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -56,7 +56,6 @@ namespace // 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; - auto constexpr PreviewLineageMapCapacity = 1 << 12; ArraySizesForGpuEntities const PreviewCapacityGpu{10000, 10000, 10000000}; ArraySizesForTOs const PreviewCapacityTO{1000, 1000, 1000, 10000, 10000, 10000, 10000000}; } @@ -88,7 +87,7 @@ _SimulationCudaFacade::_SimulationCudaFacade(uint64_t timestep, SettingsForSimul _cudaSimulationData->init({_settings.worldSizeX, _settings.worldSizeY}, timestep); _cudaPreviewData->init({_settingsForPreview.worldSizeX, _settingsForPreview.worldSizeY}, 0); _cudaSimulationStatistics->init(); - _cudaPreviewStatistics->init(PreviewLineageMapCapacity); + _cudaPreviewStatistics->init(); _cudaSelectionResult->init(); GarbageCollectorKernelsService::get().init(); diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 2a56f45b5..33d3c84ba 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -10,29 +10,26 @@ class SimulationStatistics { public: - static auto constexpr DefaultLineageMapCapacity = 1 << 18; // Power of two - static auto constexpr LineageIdEmpty = 0xffffffffu; - - __host__ void init(int lineageMapCapacity = DefaultLineageMapCapacity) + __host__ void init() { - _lineageMapCapacity = lineageMapCapacity; + _lineageArrayCapacity = 1 << 18; CudaMemoryManager::getInstance().acquireMemory(1, _data); CudaMemoryManager::getInstance().acquireMemory(1, _overallStatisticsEntry); - CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageMap); - CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageCompactData); - CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[0]); - CudaMemoryManager::getInstance().acquireMemory(_lineageMapCapacity, _lineageAccumulatorMaps[1]); - CudaMemoryManager::getInstance().acquireMemory(1, _lineageMapControl); + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageMap); + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageStatisticsEntries); + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[0]); + CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[1]); + CudaMemoryManager::getInstance().acquireMemory(1, _lineageAccumulatorMapControl); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(StatisticsRawData))); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_overallStatisticsEntry, 0, sizeof(OverallStatisticsEntry))); - CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMapControl, 0, sizeof(LineageMapControl))); + 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(LineageMapSlot) * _lineageMapCapacity)); - CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageMap, sizeof(LineageMapSlot), 0xff, sizeof(uint32_t), _lineageMapCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageMap, 0, sizeof(LineageMapSlot) * _lineageArrayCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageMap, sizeof(LineageMapSlot), 0xff, sizeof(uint32_t), _lineageArrayCapacity)); for (int i = 0; i < 2; ++i) { - CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMaps[i], 0, sizeof(LineageAccumulatorMapSlot) * _lineageMapCapacity)); - CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageAccumulatorMaps[i], sizeof(LineageAccumulatorMapSlot), 0xff, sizeof(uint32_t), _lineageMapCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMaps[i], 0, sizeof(LineageAccumulatorMapSlot) * _lineageArrayCapacity)); + CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageAccumulatorMaps[i], sizeof(LineageAccumulatorMapSlot), 0xff, sizeof(uint32_t), _lineageArrayCapacity)); } } @@ -41,10 +38,10 @@ public: CudaMemoryManager::getInstance().freeMemory(_data); CudaMemoryManager::getInstance().freeMemory(_overallStatisticsEntry); CudaMemoryManager::getInstance().freeMemory(_lineageMap); - CudaMemoryManager::getInstance().freeMemory(_lineageCompactData); + CudaMemoryManager::getInstance().freeMemory(_lineageStatisticsEntries); CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[0]); CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[1]); - CudaMemoryManager::getInstance().freeMemory(_lineageMapControl); + CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMapControl); } __host__ StatisticsRawData getStatistics() @@ -68,7 +65,7 @@ public: result.entries.resize(control.numCompactEntries); if (control.numCompactEntries > 0) { CHECK_FOR_DEVICE_ERRORS( - cudaMemcpy(result.entries.data(), _lineageCompactData, sizeof(LineageStatisticsEntry) * control.numCompactEntries, cudaMemcpyDeviceToHost)); + cudaMemcpy(result.entries.data(), _lineageStatisticsEntries, sizeof(LineageStatisticsEntry) * control.numCompactEntries, cudaMemcpyDeviceToHost)); } return result; } @@ -76,7 +73,7 @@ public: __host__ bool isLineageAccumulatorGCNeeded() const { auto control = getLineageMapControl(); - return control.numUsedAccumulatorSlots[control.activeAccumulatorBuffer] > static_cast(_lineageMapCapacity) / 2; + return control.numUsedAccumulatorSlots[control.activeAccumulatorBuffer] > static_cast(_lineageArrayCapacity) / 2; } //timestep statistics @@ -157,7 +154,7 @@ public: __inline__ __device__ void addCreatureEnergy(float value) { atomicAdd(&_overallStatisticsEntry->sumCreatureEnergy, value); } //lineage statistics - __inline__ __device__ int getLineageMapCapacity() const { return _lineageMapCapacity; } + __inline__ __device__ int getLineageMapCapacity() const { return _lineageArrayCapacity; } __inline__ __device__ void resetLineageMapSlot(int index) { auto& slot = _lineageMap[index]; @@ -171,13 +168,13 @@ public: slot.sumMutationRates = 0; slot.sumCreatureEnergy = 0; } - __inline__ __device__ void resetCompactLineageCounter() { _lineageMapControl->numCompactEntries = 0; } + __inline__ __device__ void resetCompactLineageCounter() { _lineageAccumulatorMapControl->numCompactEntries = 0; } __inline__ __device__ int insertOrFindLineageSlot(uint32_t lineageId) { - auto mask = _lineageMapCapacity - 1; + auto mask = _lineageArrayCapacity - 1; auto index = toInt((lineageId * 2654435761u) & mask); - for (int i = 0; i < _lineageMapCapacity; ++i) { + for (int i = 0; i < _lineageArrayCapacity; ++i) { auto origLineageId = atomicCAS(&_lineageMap[index].lineageId, LineageIdEmpty, lineageId); if (origLineageId == LineageIdEmpty || origLineageId == lineageId) { return index; @@ -189,9 +186,9 @@ public: } __inline__ __device__ int findLineageSlot(uint32_t lineageId) const { - auto mask = _lineageMapCapacity - 1; + auto mask = _lineageArrayCapacity - 1; auto index = toInt((lineageId * 2654435761u) & mask); - for (int i = 0; i < _lineageMapCapacity; ++i) { + for (int i = 0; i < _lineageArrayCapacity; ++i) { auto slotLineageId = _lineageMap[index].lineageId; if (slotLineageId == lineageId) { return index; @@ -229,8 +226,8 @@ public: if (slot.lineageId == LineageIdEmpty || slot.numCreatures == 0) { return; } - auto entryIndex = atomicAdd(&_lineageMapControl->numCompactEntries, 1u); - auto& entry = _lineageCompactData[entryIndex]; + auto entryIndex = atomicAdd(&_lineageAccumulatorMapControl->numCompactEntries, 1u); + auto& entry = _lineageStatisticsEntries[entryIndex]; entry.lineageId = slot.lineageId; entry.colorBitset = slot.colorBitset; entry.numCreatures = slot.numCreatures; @@ -249,22 +246,22 @@ public: entry.totalMutations = accumulatorSlot.totalMutations; } } - __inline__ __device__ void finalizeLineageStatistics() { _overallStatisticsEntry->numActiveLineages = _lineageMapControl->numCompactEntries; } + __inline__ __device__ void finalizeLineageStatistics() { _overallStatisticsEntry->numActiveLineages = _lineageAccumulatorMapControl->numCompactEntries; } //lineage accumulator map (persistent, garbage-collected occasionally) __inline__ __device__ void resetInactiveAccumulatorSlot(int index) { - auto& slot = _lineageAccumulatorMaps[1 - _lineageMapControl->activeAccumulatorBuffer][index]; + auto& slot = _lineageAccumulatorMaps[1 - _lineageAccumulatorMapControl->activeAccumulatorBuffer][index]; slot.lineageId = LineageIdEmpty; slot.numCreatedCreatures = 0; slot.totalMutations = 0; if (index == 0) { - _lineageMapControl->numUsedAccumulatorSlots[1 - _lineageMapControl->activeAccumulatorBuffer] = 0; + _lineageAccumulatorMapControl->numUsedAccumulatorSlots[1 - _lineageAccumulatorMapControl->activeAccumulatorBuffer] = 0; } } __inline__ __device__ void migrateActiveAccumulatorSlot(int index) { - auto activeBuffer = _lineageMapControl->activeAccumulatorBuffer; + auto activeBuffer = _lineageAccumulatorMapControl->activeAccumulatorBuffer; auto const& slot = _lineageAccumulatorMaps[activeBuffer][index]; if (slot.lineageId == LineageIdEmpty) { return; @@ -279,7 +276,7 @@ public: targetSlot.totalMutations = slot.totalMutations; } } - __inline__ __device__ void flipAccumulatorBuffers() { _lineageMapControl->activeAccumulatorBuffer = 1 - _lineageMapControl->activeAccumulatorBuffer; } + __inline__ __device__ void flipAccumulatorBuffers() { _lineageAccumulatorMapControl->activeAccumulatorBuffer = 1 - _lineageAccumulatorMapControl->activeAccumulatorBuffer; } //accumulated statistics __host__ void resetAccumulatedStatistics() @@ -344,6 +341,8 @@ public: __inline__ __device__ int getMaxAge() const { return _data->histogram.maxAge; } private: + static auto constexpr LineageIdEmpty = 0xffffffffu; + struct LineageMapSlot { uint32_t lineageId; // LineageIdEmpty = slot is unused @@ -362,31 +361,31 @@ private: uint32_t numCreatedCreatures; float totalMutations; }; - struct LineageMapControl + struct LineageAccumulatorMapControl { uint32_t numCompactEntries; uint32_t activeAccumulatorBuffer; uint32_t numUsedAccumulatorSlots[2]; }; - __host__ LineageMapControl getLineageMapControl() const + __host__ LineageAccumulatorMapControl getLineageMapControl() const { - LineageMapControl result; - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _lineageMapControl, sizeof(LineageMapControl), cudaMemcpyDeviceToHost)); + LineageAccumulatorMapControl result; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _lineageAccumulatorMapControl, sizeof(LineageAccumulatorMapControl), cudaMemcpyDeviceToHost)); return result; } - __inline__ __device__ LineageAccumulatorMapSlot* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageMapControl->activeAccumulatorBuffer]; } + __inline__ __device__ LineageAccumulatorMapSlot* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageAccumulatorMapControl->activeAccumulatorBuffer]; } __inline__ __device__ int insertAccumulatorSlot(uint32_t bufferIndex, uint32_t lineageId) { auto map = _lineageAccumulatorMaps[bufferIndex]; - auto mask = _lineageMapCapacity - 1; + auto mask = _lineageArrayCapacity - 1; auto index = toInt((lineageId * 2654435761u) & mask); - for (int i = 0; i < _lineageMapCapacity; ++i) { + for (int i = 0; i < _lineageArrayCapacity; ++i) { auto origLineageId = atomicCAS(&map[index].lineageId, LineageIdEmpty, lineageId); if (origLineageId == LineageIdEmpty) { - atomicAdd(&_lineageMapControl->numUsedAccumulatorSlots[bufferIndex], 1u); + atomicAdd(&_lineageAccumulatorMapControl->numUsedAccumulatorSlots[bufferIndex], 1u); return index; } if (origLineageId == lineageId) { @@ -398,14 +397,14 @@ private: } __inline__ __device__ int findOrInsertAccumulatorSlot(uint32_t lineageId) { - return insertAccumulatorSlot(_lineageMapControl->activeAccumulatorBuffer, lineageId); + return insertAccumulatorSlot(_lineageAccumulatorMapControl->activeAccumulatorBuffer, lineageId); } __inline__ __device__ int findAccumulatorSlot(uint32_t lineageId) { auto map = getActiveAccumulatorMap(); - auto mask = _lineageMapCapacity - 1; + auto mask = _lineageArrayCapacity - 1; auto index = toInt((lineageId * 2654435761u) & mask); - for (int i = 0; i < _lineageMapCapacity; ++i) { + for (int i = 0; i < _lineageArrayCapacity; ++i) { auto slotLineageId = map[index].lineageId; if (slotLineageId == lineageId) { return index; @@ -419,12 +418,16 @@ private: } StatisticsRawData* _data; + + int _lineageArrayCapacity; + OverallStatisticsEntry* _overallStatisticsEntry; - int _lineageMapCapacity; + LineageStatisticsEntry* _lineageStatisticsEntries; + // Lineage map for timestep values LineageMapSlot* _lineageMap; - LineageAccumulatorMapSlot* _lineageAccumulatorMaps[2]; - LineageMapControl* _lineageMapControl; - LineageStatisticsEntry* _lineageCompactData; + // Lineage map for accumulated values (with history => needs migration) + LineageAccumulatorMapControl* _lineageAccumulatorMapControl; + LineageAccumulatorMapSlot* _lineageAccumulatorMaps[2]; }; From a2c87b3d709819f1157db36ee00dd389ea84908b Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 18:50:53 +0200 Subject: [PATCH 16/41] SimulationStatistics simplified, part 2 --- source/EngineInterface/LineageStatistics.h | 5 ++- source/EngineInterface/OverallStatistics.h | 4 +- source/EngineKernels/SimulationStatistics.cuh | 41 ++++++++++--------- .../EngineTests/EvolutionStatisticsTests.cpp | 1 - source/Gui/EvolutionDashboardWindow.cpp | 9 ---- source/Gui/EvolutionDashboardWindow.h | 1 - 6 files changed, 27 insertions(+), 34 deletions(-) diff --git a/source/EngineInterface/LineageStatistics.h b/source/EngineInterface/LineageStatistics.h index 31ef62836..dce4f0b71 100644 --- a/source/EngineInterface/LineageStatistics.h +++ b/source/EngineInterface/LineageStatistics.h @@ -14,8 +14,9 @@ struct LineageStatisticsEntry float sumGenomeNodes = 0; float sumMutationRates = 0; float sumCreatureEnergy = 0; - uint32_t numCreatedCreatures = 0; // Accumulated, never reset - float totalMutations = 0; // Accumulated, never reset + + uint64_t numCreatedCreatures = 0; // Accumulated, never reset + double totalMutations = 0; // Accumulated, never reset }; struct RawLineageStatistics diff --git a/source/EngineInterface/OverallStatistics.h b/source/EngineInterface/OverallStatistics.h index 639034ad7..7ae903a54 100644 --- a/source/EngineInterface/OverallStatistics.h +++ b/source/EngineInterface/OverallStatistics.h @@ -16,7 +16,7 @@ struct OverallStatisticsEntry uint32_t numFluidObjects = 0; uint32_t numCellObjects = 0; uint32_t numActiveLineages = 0; - uint32_t lineageMapOverflow = 0; - uint32_t numCreatedCreatures = 0; // Accumulated, never reset + + uint64_t numCreatedCreatures = 0; // Accumulated, never reset float totalMutations = 0; // Accumulated, never reset }; diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 33d3c84ba..2233df989 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -14,22 +14,25 @@ public: { _lineageArrayCapacity = 1 << 18; CudaMemoryManager::getInstance().acquireMemory(1, _data); + CudaMemoryManager::getInstance().acquireMemory(1, _overallStatisticsEntry); - CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageMap); CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageStatisticsEntries); - CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[0]); - CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[1]); + + 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(_data, 0, sizeof(StatisticsRawData))); 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(LineageMapSlot) * _lineageArrayCapacity)); - CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageMap, sizeof(LineageMapSlot), 0xff, sizeof(uint32_t), _lineageArrayCapacity)); + 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(LineageAccumulatorMapSlot) * _lineageArrayCapacity)); - CHECK_FOR_DEVICE_ERRORS(cudaMemset2D(_lineageAccumulatorMaps[i], sizeof(LineageAccumulatorMapSlot), 0xff, sizeof(uint32_t), _lineageArrayCapacity)); + 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)); } } @@ -37,11 +40,13 @@ public: { CudaMemoryManager::getInstance().freeMemory(_data); CudaMemoryManager::getInstance().freeMemory(_overallStatisticsEntry); - CudaMemoryManager::getInstance().freeMemory(_lineageMap); CudaMemoryManager::getInstance().freeMemory(_lineageStatisticsEntries); + + CudaMemoryManager::getInstance().freeMemory(_lineageMap); + + CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMapControl); CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[0]); CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[1]); - CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMapControl); } __host__ StatisticsRawData getStatistics() @@ -131,7 +136,6 @@ public: overall.numFluidObjects = 0; overall.numCellObjects = 0; overall.numActiveLineages = 0; - overall.lineageMapOverflow = 0; } __inline__ __device__ void incNumSolidObjects() { atomicAdd(&_overallStatisticsEntry->numSolidObjects, 1u); } __inline__ __device__ void incNumFluidObjects() { atomicAdd(&_overallStatisticsEntry->numFluidObjects, 1u); } @@ -181,7 +185,6 @@ public: } index = (index + 1) & mask; } - _overallStatisticsEntry->lineageMapOverflow = 1; return -1; } __inline__ __device__ int findLineageSlot(uint32_t lineageId) const @@ -343,7 +346,7 @@ public: private: static auto constexpr LineageIdEmpty = 0xffffffffu; - struct LineageMapSlot + struct LineageMapEntry { uint32_t lineageId; // LineageIdEmpty = slot is unused uint32_t colorBitset; @@ -355,11 +358,11 @@ private: float sumMutationRates; float sumCreatureEnergy; }; - struct LineageAccumulatorMapSlot + struct LineageAccumulatorMapEntry { uint32_t lineageId; // LineageIdEmpty = slot is unused - uint32_t numCreatedCreatures; - float totalMutations; + uint64_t numCreatedCreatures; + double totalMutations; }; struct LineageAccumulatorMapControl { @@ -375,7 +378,7 @@ private: return result; } - __inline__ __device__ LineageAccumulatorMapSlot* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageAccumulatorMapControl->activeAccumulatorBuffer]; } + __inline__ __device__ LineageAccumulatorMapEntry* getActiveAccumulatorMap() { return _lineageAccumulatorMaps[_lineageAccumulatorMapControl->activeAccumulatorBuffer]; } __inline__ __device__ int insertAccumulatorSlot(uint32_t bufferIndex, uint32_t lineageId) { @@ -419,15 +422,15 @@ private: StatisticsRawData* _data; - int _lineageArrayCapacity; + int _lineageArrayCapacity; // Used for all lineage maps and arrays OverallStatisticsEntry* _overallStatisticsEntry; LineageStatisticsEntry* _lineageStatisticsEntries; // Lineage map for timestep values - LineageMapSlot* _lineageMap; + LineageMapEntry* _lineageMap; // Lineage map for accumulated values (with history => needs migration) LineageAccumulatorMapControl* _lineageAccumulatorMapControl; - LineageAccumulatorMapSlot* _lineageAccumulatorMaps[2]; + LineageAccumulatorMapEntry* _lineageAccumulatorMaps[2]; }; diff --git a/source/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp index b55f6aa51..6a7243885 100644 --- a/source/EngineTests/EvolutionStatisticsTests.cpp +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -24,7 +24,6 @@ TEST_F(EvolutionStatisticsTests, basicCounts) EXPECT_EQ(3u, overall.numCellObjects); EXPECT_EQ(0u, overall.numSolidObjects); EXPECT_EQ(0u, overall.numFluidObjects); - EXPECT_EQ(0u, overall.lineageMapOverflow); EXPECT_FLOAT_EQ(3.0f, overall.sumCreatureCells); EXPECT_FLOAT_EQ(8.0f, overall.sumCreatureGenerations); EXPECT_GT(overall.sumCreatureEnergy, 0.0f); diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 18d2af508..4c1f17499 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -39,8 +39,6 @@ namespace ImColor const PositiveDeltaColor = ImColor(0.19f, 0.77f, 0.55f, 1.0f); ImColor const NegativeDeltaColor = ImColor(0.94f, 0.32f, 0.32f, 1.0f); ImColor const SumSeriesColor = ImColor(0.78f, 0.78f, 0.78f, 1.0f); - ImColor const OverflowWarningColor = ImColor(0.94f, 0.62f, 0.22f, 1.0f); - struct MetricDef { char const* tableHeader; @@ -241,7 +239,6 @@ void EvolutionDashboardWindow::processBackground() auto rawStatistics = _SimulationFacade::get()->getStatisticsRawData(); auto overallStatistics = _SimulationFacade::get()->getOverallStatistics(); _timelineLiveStatistics.update(rawStatistics.timeline, overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); - _lineageMapOverflow = overallStatistics.lineageMapOverflow != 0; auto rawLineageStatistics = _SimulationFacade::get()->getRawLineageStatistics(); LineageSample sample; @@ -594,12 +591,6 @@ void EvolutionDashboardWindow::processFilterBar() ImGui::PopID(); ImGui::SameLine(0, scale(5.0f)); } - if (_lineageMapOverflow) { - ImGui::SameLine(0, scale(20.0f)); - ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)OverflowWarningColor); - AlienGui::Text("Too many lineages: some are not tracked"); - ImGui::PopStyleColor(); - } ImGui::NewLine(); ImGui::Spacing(); } diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index 2f2bb5f5b..6c7684448 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -61,7 +61,6 @@ class EvolutionDashboardWindow : public AlienWindow std::array _metricWindowDeltas = {}; std::array, 4> _headerSparklines = {}; std::array _headerDeltas = {}; - bool _lineageMapOverflow = false; std::array _cellColors = {}; From 85170b0b821cd2c65a05c0688e85378d70433737 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 19:12:35 +0200 Subject: [PATCH 17/41] Refactor, part 1 --- source/EngineImpl/EngineWorker.cpp | 8 +- source/EngineImpl/EngineWorker.h | 6 +- source/EngineImpl/SimulationCudaFacade.cu | 12 +- source/EngineImpl/SimulationCudaFacade.cuh | 10 +- source/EngineImpl/SimulationFacadeImpl.cpp | 8 +- source/EngineImpl/SimulationFacadeImpl.h | 4 +- .../SimulationParametersUpdateService.cu | 2 +- .../SimulationParametersUpdateService.cuh | 4 +- source/EngineImpl/StatisticsKernelsService.cu | 3 - source/EngineInterface/CMakeLists.txt | 2 +- source/EngineInterface/DataPointCollection.h | 2 +- source/EngineInterface/Definitions.h | 2 - .../EngineInterface/LineageHistoryService.cpp | 4 +- .../EngineInterface/LineageHistoryService.h | 2 +- source/EngineInterface/LineageStatistics.h | 2 +- source/EngineInterface/SimulationFacade.h | 5 +- ...atisticsRawData.h => TimelineStatistics.h} | 12 - .../LineageHistoryServiceTests.cpp | 4 +- source/EngineKernels/MaxAgeBalancer.cu | 6 +- source/EngineKernels/MaxAgeBalancer.cuh | 6 +- source/EngineKernels/SimulationStatistics.cuh | 92 +- source/EngineKernels/StatisticsKernels.cu | 43 - source/EngineKernels/StatisticsKernels.cuh | 3 - .../EngineTests/EvolutionStatisticsTests.cpp | 4 +- source/EngineTests/StatisticsTests.cpp | 7 +- source/Gui/BrowserWindow.cpp | 1 - source/Gui/CMakeLists.txt | 6 - source/Gui/Definitions.h | 2 - source/Gui/EvolutionDashboardWindow.cpp | 6 +- source/Gui/HistogramLiveStatistics.cpp | 16 - source/Gui/HistogramLiveStatistics.h | 17 - source/Gui/MainLoopController.cpp | 4 - source/Gui/MainWindow.cpp | 2 - source/Gui/StatisticsWindow.cpp | 859 ------------------ source/Gui/StatisticsWindow.h | 121 --- source/Gui/TableLiveStatistics.cpp | 61 -- source/Gui/TableLiveStatistics.h | 27 - source/Gui/TemporalControlWindow.cpp | 1 - source/Gui/TimelineLiveStatistics.cpp | 1 - source/Gui/TimelineLiveStatistics.h | 2 +- source/PersisterImpl/PersisterWorker.cpp | 3 +- .../SaveDeserializedSimulationResultData.h | 1 - .../SharedDeserializedSimulation.h | 16 +- 43 files changed, 94 insertions(+), 1305 deletions(-) rename source/EngineInterface/{StatisticsRawData.h => TimelineStatistics.h} (88%) delete mode 100644 source/Gui/HistogramLiveStatistics.cpp delete mode 100644 source/Gui/HistogramLiveStatistics.h delete mode 100644 source/Gui/StatisticsWindow.cpp delete mode 100644 source/Gui/StatisticsWindow.h delete mode 100644 source/Gui/TableLiveStatistics.cpp delete mode 100644 source/Gui/TableLiveStatistics.h diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index a5eaac719..ba49dc241 100644 --- a/source/EngineImpl/EngineWorker.cpp +++ b/source/EngineImpl/EngineWorker.cpp @@ -99,9 +99,9 @@ Desc EngineWorker::getInspectedSimulationData(std::vector objectsIds) return DescConverterService::get().convertTOtoDescription(dataTO); } -StatisticsRawData EngineWorker::getStatisticsRawData() const +TimelineStatistics EngineWorker::getTimelineStatistics() const { - return _simulationCudaFacade->getStatisticsRawData(); + return _simulationCudaFacade->getTimelineStatistics(); } StatisticsHistory const& EngineWorker::getStatisticsHistory() const @@ -114,9 +114,9 @@ void EngineWorker::setStatisticsHistory(StatisticsHistoryData const& data) _simulationCudaFacade->setStatisticsHistory(data); } -RawLineageStatistics EngineWorker::getRawLineageStatistics() const +LineageStatistics EngineWorker::getLineageStatistics() const { - return _simulationCudaFacade->getRawLineageStatistics(); + return _simulationCudaFacade->getLineageStatistics(); } OverallStatisticsEntry EngineWorker::getOverallStatistics() const diff --git a/source/EngineImpl/EngineWorker.h b/source/EngineImpl/EngineWorker.h index 5252974ab..9f7034327 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include @@ -58,10 +58,10 @@ class EngineWorker Desc getSimulationData(IntVector2D const& rectUpperLeft, IntVector2D const& rectLowerRight); Desc getSelectedSimulationData(bool includeClusters); Desc getInspectedSimulationData(std::vector objectsIds); - StatisticsRawData getStatisticsRawData() const; + TimelineStatistics getTimelineStatistics() const; StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); - RawLineageStatistics getRawLineageStatistics() const; + LineageStatistics getLineageStatistics() const; OverallStatisticsEntry getOverallStatistics() const; LineageHistory const& getLineageHistory() const; diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index e2e500aa9..bca89588a 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -436,13 +436,13 @@ ArraySizesForTOs _SimulationCudaFacade::estimateCapacityNeededForTO() const return DataAccessKernelsService::get().estimateCapacityNeededForTO(_settings.cudaSettings, getSimulationDataPtrCopy()); } -StatisticsRawData _SimulationCudaFacade::getStatisticsRawData() +TimelineStatistics _SimulationCudaFacade::getTimelineStatistics() { std::lock_guard lock(_mutexForStatistics); if (_statisticsData) { return *_statisticsData; } else { - return StatisticsRawData(); + return TimelineStatistics(); } } @@ -463,7 +463,7 @@ void _SimulationCudaFacade::updateTimestepStatistics() _statisticsData = _cudaSimulationStatistics->getStatistics(); overallStatistics = _overallStatisticsData.value_or(OverallStatisticsEntry()); } - StatisticsService::get().addDataPoint(_statisticsHistory, _statisticsData->timeline, overallStatistics, getCurrentTimestep()); + StatisticsService::get().addDataPoint(_statisticsHistory, *_statisticsData, overallStatistics, getCurrentTimestep()); } void _SimulationCudaFacade::updateEvolutionStatistics() @@ -486,13 +486,13 @@ StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const return _statisticsHistory; } -RawLineageStatistics _SimulationCudaFacade::getRawLineageStatistics() +LineageStatistics _SimulationCudaFacade::getLineageStatistics() { std::lock_guard lock(_mutexForStatistics); if (_lineageStatisticsData) { return *_lineageStatisticsData; } else { - return RawLineageStatistics(); + return LineageStatistics(); } } @@ -865,7 +865,7 @@ void _SimulationCudaFacade::calcTimestepsInternal(uint64_t timesteps, bool force resizeArraysIfNecessary(); } - auto statistics = getStatisticsRawData(); + auto statistics = getTimelineStatistics(); { std::lock_guard lock(_mutexForSimulationParameters); if (SimulationParametersUpdateService::get().updateSimulationParametersAfterTimestep( diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index 1776d78f7..96f2bff4b 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include @@ -87,11 +87,11 @@ public: ArraySizesForTOs estimateCapacityNeededForTO() const; - StatisticsRawData getStatisticsRawData(); + TimelineStatistics getTimelineStatistics(); void updateStatistics(); StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); - RawLineageStatistics getRawLineageStatistics(); + LineageStatistics getLineageStatistics(); OverallStatisticsEntry getOverallStatistics(); LineageHistory const& getLineageHistory() const; @@ -164,9 +164,9 @@ private: mutable std::mutex _mutexForStatistics; std::optional _lastStatisticsUpdateTime; - std::optional _statisticsData; + std::optional _statisticsData; StatisticsHistory _statisticsHistory; - std::optional _lineageStatisticsData; + std::optional _lineageStatisticsData; std::optional _overallStatisticsData; LineageHistory _lineageHistory; LineageHistoryService _lineageHistoryService; diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index e7bb38d28..377e07fe3 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -320,9 +320,9 @@ IntVector2D _SimulationFacadeImpl::getWorldSize() const return _worldSize; } -StatisticsRawData _SimulationFacadeImpl::getStatisticsRawData() const +TimelineStatistics _SimulationFacadeImpl::getTimelineStatistics() const { - return _worker.getStatisticsRawData(); + return _worker.getTimelineStatistics(); } StatisticsHistory const& _SimulationFacadeImpl::getStatisticsHistory() const @@ -335,9 +335,9 @@ void _SimulationFacadeImpl::setStatisticsHistory(StatisticsHistoryData const& da _worker.setStatisticsHistory(data); } -RawLineageStatistics _SimulationFacadeImpl::getRawLineageStatistics() const +LineageStatistics _SimulationFacadeImpl::getLineageStatistics() const { - return _worker.getRawLineageStatistics(); + return _worker.getLineageStatistics(); } OverallStatisticsEntry _SimulationFacadeImpl::getOverallStatistics() const diff --git a/source/EngineImpl/SimulationFacadeImpl.h b/source/EngineImpl/SimulationFacadeImpl.h index 66deebc30..37fa898f3 100644 --- a/source/EngineImpl/SimulationFacadeImpl.h +++ b/source/EngineImpl/SimulationFacadeImpl.h @@ -88,10 +88,10 @@ class _SimulationFacadeImpl : public _SimulationFacade bool updateSelectionIfNecessary() override; IntVector2D getWorldSize() const override; - StatisticsRawData getStatisticsRawData() const override; + TimelineStatistics getTimelineStatistics() const override; StatisticsHistory const& getStatisticsHistory() const override; void setStatisticsHistory(StatisticsHistoryData const& data) override; - RawLineageStatistics getRawLineageStatistics() const override; + LineageStatistics getLineageStatistics() const override; OverallStatisticsEntry getOverallStatistics() const override; LineageHistory const& getLineageHistory() const override; diff --git a/source/EngineImpl/SimulationParametersUpdateService.cu b/source/EngineImpl/SimulationParametersUpdateService.cu index 8d5f0f86d..ea685aac8 100644 --- a/source/EngineImpl/SimulationParametersUpdateService.cu +++ b/source/EngineImpl/SimulationParametersUpdateService.cu @@ -45,7 +45,7 @@ bool SimulationParametersUpdateService::updateSimulationParametersAfterTimestep( MaxAgeBalancer const& maxAgeBalancer, SimulationData const& simulationData, uint64_t timestep, - StatisticsRawData const& statistics) + TimelineStatistics const& statistics) { auto result = false; diff --git a/source/EngineImpl/SimulationParametersUpdateService.cuh b/source/EngineImpl/SimulationParametersUpdateService.cuh index 17625a27c..e860f7b7c 100644 --- a/source/EngineImpl/SimulationParametersUpdateService.cuh +++ b/source/EngineImpl/SimulationParametersUpdateService.cuh @@ -4,7 +4,7 @@ #include #include -#include +#include #include @@ -23,5 +23,5 @@ public: MaxAgeBalancer const& maxAgeBalancer, SimulationData const& simulationData, uint64_t timestep, - StatisticsRawData const& statistics); //returns true if parameters have been changed + TimelineStatistics const& statistics); //returns true if parameters have been changed }; diff --git a/source/EngineImpl/StatisticsKernelsService.cu b/source/EngineImpl/StatisticsKernelsService.cu index 00ca311c0..e60a1199e 100644 --- a/source/EngineImpl/StatisticsKernelsService.cu +++ b/source/EngineImpl/StatisticsKernelsService.cu @@ -11,9 +11,6 @@ void StatisticsKernelsService::updateStatistics(CudaSettings const& gpuSettings, 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); } void StatisticsKernelsService::updateEvolutionStatistics( diff --git a/source/EngineInterface/CMakeLists.txt b/source/EngineInterface/CMakeLists.txt index ece228a8b..5718562fb 100644 --- a/source/EngineInterface/CMakeLists.txt +++ b/source/EngineInterface/CMakeLists.txt @@ -67,7 +67,7 @@ add_library(EngineInterface StatisticsConverterService.h StatisticsHistory.cpp StatisticsHistory.h - StatisticsRawData.h) + TimelineStatistics.h) target_link_libraries(EngineInterface Base) diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index 4af4f6c9f..484622820 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -1,6 +1,6 @@ #pragma once -#include +#include struct DataPoint { diff --git a/source/EngineInterface/Definitions.h b/source/EngineInterface/Definitions.h index 27cb1d7c4..b2ed4c4d9 100644 --- a/source/EngineInterface/Definitions.h +++ b/source/EngineInterface/Definitions.h @@ -29,8 +29,6 @@ class _SimulationFacade; using SimulationFacade = std::shared_ptr<_SimulationFacade>; struct TimelineStatistics; -struct HistogramData; -struct StatisticsRawData; class SpaceCalculator; diff --git a/source/EngineInterface/LineageHistoryService.cpp b/source/EngineInterface/LineageHistoryService.cpp index d4450c4c1..a1f3bd2e3 100644 --- a/source/EngineInterface/LineageHistoryService.cpp +++ b/source/EngineInterface/LineageHistoryService.cpp @@ -5,7 +5,7 @@ #include -void LineageHistoryService::addSample(LineageHistory& history, RawLineageStatistics const& rawStatistics, uint64_t timestep) +void LineageHistoryService::addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, uint64_t timestep) { std::lock_guard lock(history.getMutex()); auto& historyData = history.getDataRef(); @@ -24,7 +24,7 @@ void LineageHistoryService::addSample(LineageHistory& history, RawLineageStatist auto now = std::chrono::system_clock::now(); auto unixEpoch = std::chrono::time_point(); sample.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); - sample.entries = rawStatistics.entries; + sample.entries = lineageStatistics.entries; std::sort(sample.entries.begin(), sample.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); historyData.emplace_back(std::move(sample)); diff --git a/source/EngineInterface/LineageHistoryService.h b/source/EngineInterface/LineageHistoryService.h index 1a026f63e..7bb5e2ccb 100644 --- a/source/EngineInterface/LineageHistoryService.h +++ b/source/EngineInterface/LineageHistoryService.h @@ -8,7 +8,7 @@ class LineageHistoryService { public: - void addSample(LineageHistory& history, RawLineageStatistics const& rawStatistics, uint64_t timestep); + void addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, uint64_t timestep); void reset(); // Merges two consecutive samples: union of the lineage ids, momentary values are averaged diff --git a/source/EngineInterface/LineageStatistics.h b/source/EngineInterface/LineageStatistics.h index dce4f0b71..b404c1ca5 100644 --- a/source/EngineInterface/LineageStatistics.h +++ b/source/EngineInterface/LineageStatistics.h @@ -19,7 +19,7 @@ struct LineageStatisticsEntry double totalMutations = 0; // Accumulated, never reset }; -struct RawLineageStatistics +struct LineageStatistics { std::vector entries; }; diff --git a/source/EngineInterface/SimulationFacade.h b/source/EngineInterface/SimulationFacade.h index 6970dbaf8..2fd57e9ce 100644 --- a/source/EngineInterface/SimulationFacade.h +++ b/source/EngineInterface/SimulationFacade.h @@ -13,6 +13,7 @@ #include "ShallowUpdateSelectionData.h" #include "SimulationParametersUpdateConfig.h" #include "StatisticsHistory.h" +#include "TimelineStatistics.h" class _SimulationFacade { @@ -106,10 +107,10 @@ class _SimulationFacade //************ //* Statistics //************ - virtual StatisticsRawData getStatisticsRawData() const = 0; + virtual TimelineStatistics getTimelineStatistics() const = 0; virtual StatisticsHistory const& getStatisticsHistory() const = 0; virtual void setStatisticsHistory(StatisticsHistoryData const& data) = 0; - virtual RawLineageStatistics getRawLineageStatistics() const = 0; + virtual LineageStatistics getLineageStatistics() const = 0; virtual OverallStatisticsEntry getOverallStatistics() const = 0; virtual LineageHistory const& getLineageHistory() const = 0; diff --git a/source/EngineInterface/StatisticsRawData.h b/source/EngineInterface/TimelineStatistics.h similarity index 88% rename from source/EngineInterface/StatisticsRawData.h rename to source/EngineInterface/TimelineStatistics.h index b7041c1d7..60c23923e 100644 --- a/source/EngineInterface/StatisticsRawData.h +++ b/source/EngineInterface/TimelineStatistics.h @@ -39,18 +39,6 @@ struct TimelineStatistics 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; diff --git a/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp b/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp index f2b9fbaa2..030bc267a 100644 --- a/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp +++ b/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp @@ -12,9 +12,9 @@ class LineageHistoryServiceTests : public ::testing::Test return result; } - static RawLineageStatistics createRawStatistics(std::vector entries) + static LineageStatistics createRawStatistics(std::vector entries) { - RawLineageStatistics result; + LineageStatistics result; result.entries = std::move(entries); return result; } diff --git a/source/EngineKernels/MaxAgeBalancer.cu b/source/EngineKernels/MaxAgeBalancer.cu index c37fb0caa..e65678372 100644 --- a/source/EngineKernels/MaxAgeBalancer.cu +++ b/source/EngineKernels/MaxAgeBalancer.cu @@ -12,7 +12,7 @@ namespace auto constexpr MinReplicatorsLowerValue = 20; } -bool _MaxAgeBalancer::balance(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep) +bool _MaxAgeBalancer::balance(SimulationParameters& parameters, TimelineStatistics const& statistics, uint64_t timestep) { auto result = false; if (parameters.cellAgeLimiterToggle.value && parameters.maxCellAgeBalancerInterval.enabled) { @@ -45,11 +45,11 @@ void _MaxAgeBalancer::initializeIfNecessary(SimulationParameters const& paramete } } -bool _MaxAgeBalancer::doAdaptionIfNecessary(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep) +bool _MaxAgeBalancer::doAdaptionIfNecessary(SimulationParameters& parameters, TimelineStatistics const& statistics, uint64_t timestep) { auto result = false; for (int i = 0; i < MAX_COLORS; ++i) { - _numReplicators[i] += statistics.timeline.timestep.numSelfReplicators[i]; + _numReplicators[i] += statistics.timestep.numSelfReplicators[i]; } ++_numMeasurements; if (timestep - *_lastTimestep > parameters.maxCellAgeBalancerInterval.value) { diff --git a/source/EngineKernels/MaxAgeBalancer.cuh b/source/EngineKernels/MaxAgeBalancer.cuh index b771020d1..5b81614f6 100644 --- a/source/EngineKernels/MaxAgeBalancer.cuh +++ b/source/EngineKernels/MaxAgeBalancer.cuh @@ -5,17 +5,17 @@ #include #include -#include +#include class _MaxAgeBalancer { public: //returns true if parameters have been changed - bool balance(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep); + bool balance(SimulationParameters& parameters, TimelineStatistics const& statistics, uint64_t timestep); private: void initializeIfNecessary(SimulationParameters const& parameters, uint64_t timestep); - bool doAdaptionIfNecessary(SimulationParameters& parameters, StatisticsRawData const& statistics, uint64_t timestep); + bool doAdaptionIfNecessary(SimulationParameters& parameters, TimelineStatistics const& statistics, uint64_t timestep); void startNewMeasurement(uint64_t timestep); void saveLastState(SimulationParameters const& parameters); diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 2233df989..a8ab3d4f7 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -2,7 +2,7 @@ #include #include -#include +#include #include "Base.cuh" #include "CudaMemoryManager.cuh" @@ -13,7 +13,7 @@ public: __host__ void init() { _lineageArrayCapacity = 1 << 18; - CudaMemoryManager::getInstance().acquireMemory(1, _data); + CudaMemoryManager::getInstance().acquireMemory(1, _data); CudaMemoryManager::getInstance().acquireMemory(1, _overallStatisticsEntry); CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageStatisticsEntries); @@ -23,7 +23,7 @@ public: CudaMemoryManager::getInstance().acquireMemory(1, _lineageAccumulatorMapControl); CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[0]); CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[1]); - CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(StatisticsRawData))); + CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(TimelineStatistics))); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_overallStatisticsEntry, 0, sizeof(OverallStatisticsEntry))); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMapControl, 0, sizeof(LineageAccumulatorMapControl))); @@ -49,10 +49,10 @@ public: CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[1]); } - __host__ StatisticsRawData getStatistics() + __host__ TimelineStatistics getStatistics() { - StatisticsRawData result; - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _data, sizeof(StatisticsRawData), cudaMemcpyDeviceToHost)); + TimelineStatistics result; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _data, sizeof(TimelineStatistics), cudaMemcpyDeviceToHost)); return result; } @@ -63,10 +63,10 @@ public: return result; } - __host__ RawLineageStatistics getLineageStatistics() + __host__ LineageStatistics getLineageStatistics() { auto control = getLineageMapControl(); - RawLineageStatistics result; + LineageStatistics result; result.entries.resize(control.numCompactEntries); if (control.numCompactEntries > 0) { CHECK_FOR_DEVICE_ERRORS( @@ -85,37 +85,37 @@ public: __inline__ __device__ void resetTimestepData() { if (threadIdx.x == 0 && blockIdx.x == 0) { - _data->timeline.timestep = TimestepStatistics(); + _data->timestep = TimestepStatistics(); } } - __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__ void incNumObjects(int color) { atomicAdd(&(_data->timestep.numObjects[color]), 1); } + __inline__ __device__ void incNumReplicator(int color) { atomicAdd(&_data->timestep.numSelfReplicators[color], 1); } __inline__ __device__ int getNumReplicators() { auto result = 0; for (int i = 0; i < MAX_COLORS; ++i) { - result += _data->timeline.timestep.numSelfReplicators[i]; + result += _data->timestep.numSelfReplicators[i]; } return result; } - __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__ void incNumViruses(int color) { atomicAdd(&_data->timestep.numViruses[color], 1); } + __inline__ __device__ void incNumFreeCells(int color) { atomicAdd(&_data->timestep.numFreeCells[color], 1); } + __inline__ __device__ void incNumParticles(int color) { atomicAdd(&_data->timestep.numEnergyParticles[color], 1); } + __inline__ __device__ void addEnergy(int color, float valueToAdd) { atomicAdd(&_data->timestep.totalEnergy[color], valueToAdd); } + __inline__ __device__ void addNumObjects(int color, float valueToAdd) { atomicAdd(&_data->timestep.numObjects[color], valueToAdd); } __inline__ __device__ double getSummedNumCells() { auto result = 0.0; for (int i = 0; i < MAX_COLORS; ++i) { - result += toDouble(_data->timeline.timestep.numObjects[i]); + result += toDouble(_data->timestep.numObjects[i]); } return result; } __inline__ __device__ void halveNumConnections() { for (int i = 0; i < MAX_COLORS; ++i) { - _data->timeline.timestep.numFreeCells[i] /= 2; + _data->timestep.numFreeCells[i] /= 2; } } @@ -284,33 +284,33 @@ public: //accumulated statistics __host__ void resetAccumulatedStatistics() { - 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)); + TimelineStatistics hostData; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&hostData, _data, sizeof(TimelineStatistics), cudaMemcpyDeviceToHost)); + hostData.accumulated = AccumulatedStatistics(); + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(_data, &hostData, sizeof(TimelineStatistics), cudaMemcpyHostToDevice)); } - __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 incNumCreatedCells(int color) { alienAtomicAdd64(&_data->accumulated.numCreatedCells[color], uint64_t(1)); } + __inline__ __device__ void incNumCreatedReplicators(int color) { alienAtomicAdd64(&_data->accumulated.numCreatedReplicators[color], uint64_t(1)); } + __inline__ __device__ void incNumAttacks(int color) { alienAtomicAdd64(&_data->accumulated.numAttacks[color], uint64_t(1)); } + __inline__ __device__ void incNumMuscleActivities(int color) { alienAtomicAdd64(&_data->accumulated.numMuscleActivities[color], uint64_t(1)); } + __inline__ __device__ void incNumDefenderActivities(int color) { alienAtomicAdd64(&_data->accumulated.numDefenderActivities[color], uint64_t(1)); } + __inline__ __device__ void incNumDepotActivities(int color) { alienAtomicAdd64(&_data->accumulated.numDepotActivities[color], uint64_t(1)); } __inline__ __device__ void incNumInjectionActivities(int color) { - alienAtomicAdd64(&_data->timeline.accumulated.numInjectionActivities[color], uint64_t(1)); + alienAtomicAdd64(&_data->accumulated.numInjectionActivities[color], uint64_t(1)); } __inline__ __device__ void incNumCompletedInjections(int color) { - alienAtomicAdd64(&_data->timeline.accumulated.numCompletedInjections[color], uint64_t(1)); + alienAtomicAdd64(&_data->accumulated.numCompletedInjections[color], uint64_t(1)); } - __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 incNumGeneratorPulses(int color) { alienAtomicAdd64(&_data->accumulated.numGeneratorPulses[color], uint64_t(1)); } + __inline__ __device__ void incNumNeuronActivities(int color) { alienAtomicAdd64(&_data->accumulated.numNeuronActivities[color], uint64_t(1)); } + __inline__ __device__ void incNumSensorActivities(int color) { alienAtomicAdd64(&_data->accumulated.numSensorActivities[color], uint64_t(1)); } + __inline__ __device__ void incNumSensorMatches(int color) { alienAtomicAdd64(&_data->accumulated.numSensorMatches[color], uint64_t(1)); } + __inline__ __device__ void incNumReconnectorCreated(int color) { alienAtomicAdd64(&_data->accumulated.numReconnectorCreated[color], uint64_t(1)); } + __inline__ __device__ void incNumReconnectorRemoved(int color) { alienAtomicAdd64(&_data->accumulated.numReconnectorRemoved[color], uint64_t(1)); } + __inline__ __device__ void incNumDetonations(int color) { alienAtomicAdd64(&_data->accumulated.numDetonations[color], uint64_t(1)); } __inline__ __device__ void incCreatedCreature(uint32_t lineageId) { @@ -329,20 +329,6 @@ public: } } - //histogram - __inline__ __device__ void resetHistogramData() - { - _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; - } - } - } - __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: static auto constexpr LineageIdEmpty = 0xffffffffu; @@ -420,7 +406,7 @@ private: return -1; } - StatisticsRawData* _data; + TimelineStatistics* _data; int _lineageArrayCapacity; // Used for all lineage maps and arrays diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 450b738cc..3ec1432d4 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -244,46 +244,3 @@ __global__ void cudaFinishLineageAccumulatorGC(SimulationStatistics statistics) statistics.flipAccumulatorBuffers(); } -__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()) { - continue; - } - if (object->type == ObjectType_Cell) { - statistics.maxAge(object->typeData.cell.age); - } else if (object->type == ObjectType_FreeCell) { - statistics.maxAge(object->typeData.freeCell.age); - } - } -} - -__global__ void cudaUpdateHistogramData_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()) { - 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); - } - } -} diff --git a/source/EngineKernels/StatisticsKernels.cuh b/source/EngineKernels/StatisticsKernels.cuh index 16da830cd..7c3a16530 100644 --- a/source/EngineKernels/StatisticsKernels.cuh +++ b/source/EngineKernels/StatisticsKernels.cuh @@ -20,6 +20,3 @@ __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/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp index 6a7243885..c0616e2bc 100644 --- a/source/EngineTests/EvolutionStatisticsTests.cpp +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -28,7 +28,7 @@ TEST_F(EvolutionStatisticsTests, basicCounts) EXPECT_FLOAT_EQ(8.0f, overall.sumCreatureGenerations); EXPECT_GT(overall.sumCreatureEnergy, 0.0f); - auto lineageStatistics = _simulationFacade->getRawLineageStatistics(); + auto lineageStatistics = _simulationFacade->getLineageStatistics(); ASSERT_EQ(2, lineageStatistics.entries.size()); auto findEntry = [&](uint32_t lineageId) -> LineageStatisticsEntry const* { for (auto const& entry : lineageStatistics.entries) { @@ -94,7 +94,7 @@ TEST_F(EvolutionStatisticsTests, accumulatedMutations) EXPECT_GT(overall.sumAccumulatedMutations, 0.0f); EXPECT_GT(overall.totalMutations, 0.0f); - auto lineageStatistics = _simulationFacade->getRawLineageStatistics(); + auto lineageStatistics = _simulationFacade->getLineageStatistics(); ASSERT_EQ(1, lineageStatistics.entries.size()); EXPECT_EQ(42, lineageStatistics.entries.front().lineageId); EXPECT_GT(lineageStatistics.entries.front().totalMutations, 0.0f); diff --git a/source/EngineTests/StatisticsTests.cpp b/source/EngineTests/StatisticsTests.cpp index 1478335ff..9d281b7e8 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 77e89f21a..d75909ab0 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 6feabf321..8f9fe0926 100644 --- a/source/Gui/CMakeLists.txt +++ b/source/Gui/CMakeLists.txt @@ -65,8 +65,6 @@ PUBLIC GuiLogger.cpp GuiLogger.h HelpStrings.h - HistogramLiveStatistics.cpp - HistogramLiveStatistics.h ImageToPatternDialog.cpp ImageToPatternDialog.h InspectionWindow.cpp @@ -159,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 99afc2811..b0ccd7039 100644 --- a/source/Gui/Definitions.h +++ b/source/Gui/Definitions.h @@ -23,8 +23,6 @@ class SpatialControlWindow; class SimulationParametersMainWindow; -class StatisticsWindow; - class EvolutionDashboardWindow; class SimulationInteractionController; diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 4c1f17499..de49e63a7 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -236,11 +236,11 @@ void EvolutionDashboardWindow::processBackground() _lastTimepoint = timepoint; _timeSinceSimStart += toDouble(duration) / 1000; - auto rawStatistics = _SimulationFacade::get()->getStatisticsRawData(); + auto timelineStatistics = _SimulationFacade::get()->getTimelineStatistics(); auto overallStatistics = _SimulationFacade::get()->getOverallStatistics(); - _timelineLiveStatistics.update(rawStatistics.timeline, overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); + _timelineLiveStatistics.update(timelineStatistics, overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); - auto rawLineageStatistics = _SimulationFacade::get()->getRawLineageStatistics(); + auto rawLineageStatistics = _SimulationFacade::get()->getLineageStatistics(); LineageSample sample; sample.time = _timeSinceSimStart; sample.systemClock = _timeSinceSimStart; diff --git a/source/Gui/HistogramLiveStatistics.cpp b/source/Gui/HistogramLiveStatistics.cpp deleted file mode 100644 index 33fc03777..000000000 --- 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 37fad165c..000000000 --- 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/MainLoopController.cpp b/source/Gui/MainLoopController.cpp index 0fb4c0ec2..95cb36b4e 100644 --- a/source/Gui/MainLoopController.cpp +++ b/source/Gui/MainLoopController.cpp @@ -53,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,9 +405,6 @@ void MainLoopController::processMenubar() .selected(SpatialControlWindow::get().isOn()) .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::MenuItem( AlienGui::MenuItemParameters() .name("Simulation parameters") diff --git a/source/Gui/MainWindow.cpp b/source/Gui/MainWindow.cpp index 2514d511b..dd2990d69 100644 --- a/source/Gui/MainWindow.cpp +++ b/source/Gui/MainWindow.cpp @@ -75,7 +75,6 @@ #include "SimulationView.h" #include "SpatialControlWindow.h" #include "StartupCheckService.h" -#include "StatisticsWindow.h" #include "StyleRepository.h" #include "TemporalControlWindow.h" #include "UiController.h" @@ -126,7 +125,6 @@ _MainWindow::_MainWindow() EditorController::get().setup(); SimulationView::get().setup(); SimulationInteractionController::get().setup(); - StatisticsWindow::get().setup(); EvolutionDashboardWindow::get().setup(); TemporalControlWindow::get().setup(); SpatialControlWindow::get().setup(); diff --git a/source/Gui/StatisticsWindow.cpp b/source/Gui/StatisticsWindow.cpp deleted file mode 100644 index 2c1e12c7e..000000000 --- a/source/Gui/StatisticsWindow.cpp +++ /dev/null @@ -1,859 +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(); - auto overallStatistics = _SimulationFacade::get()->getOverallStatistics(); - _histogramLiveStatistics.update(rawStatistics.histogram); - _timelineLiveStatistics.update(rawStatistics.timeline, overallStatistics, _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 37713f068..000000000 --- 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 9c1749795..000000000 --- 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 7346bbca8..000000000 --- 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 5845cfbea..185b346de 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 diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index 239a24eee..6dffa4bf9 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -7,7 +7,6 @@ #include #include -#include std::vector const& TimelineLiveStatistics::getDataPointCollectionHistory() const { diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index ea7c9e8ff..93df73dc7 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -7,7 +7,7 @@ #include #include #include -#include +#include class TimelineLiveStatistics { diff --git a/source/PersisterImpl/PersisterWorker.cpp b/source/PersisterImpl/PersisterWorker.cpp index 357ceb808..907908643 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 1562479aa..953957d7d 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/SharedDeserializedSimulation.h b/source/PersisterInterface/SharedDeserializedSimulation.h index 8663d1bf9..3b40f534f 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>; From f68d6a8203230df8cb1c719ea1395a8c15aa6a77 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 21:11:26 +0200 Subject: [PATCH 18/41] Refactor statistics --- source/EngineImpl/EngineWorker.cpp | 10 -- source/EngineImpl/EngineWorker.h | 3 - source/EngineImpl/SimulationCudaFacade.cu | 47 +------ source/EngineImpl/SimulationCudaFacade.cuh | 9 -- source/EngineImpl/SimulationFacadeImpl.cpp | 5 - source/EngineImpl/SimulationFacadeImpl.h | 1 - .../SimulationParametersUpdateService.cu | 7 +- .../SimulationParametersUpdateService.cuh | 5 +- source/EngineImpl/StatisticsKernelsService.cu | 7 - .../EngineImpl/StatisticsKernelsService.cuh | 2 - source/EngineImpl/StatisticsService.cu | 102 ++++++++++++-- source/EngineImpl/StatisticsService.cuh | 20 +-- source/EngineInterface/CMakeLists.txt | 5 +- .../EngineInterface/DataPointCollection.cpp | 70 ---------- source/EngineInterface/DataPointCollection.h | 42 +----- source/EngineInterface/Definitions.h | 2 - .../EngineInterface/LineageHistoryService.cpp | 88 ------------ .../EngineInterface/LineageHistoryService.h | 23 ---- source/EngineInterface/SimulationFacade.h | 2 - .../EngineInterface/SimulationParameters.cpp | 26 +--- source/EngineInterface/SimulationParameters.h | 7 +- .../StatisticsConverterService.cpp | 125 +----------------- .../StatisticsConverterService.h | 8 +- source/EngineInterface/TimelineStatistics.h | 49 ------- source/EngineInterfaceTests/CMakeLists.txt | 1 - source/EngineKernels/AttackerProcessor.cuh | 2 - source/EngineKernels/CMakeLists.txt | 2 - source/EngineKernels/CellProcessor.cuh | 8 +- source/EngineKernels/ConstructorProcessor.cuh | 2 - source/EngineKernels/Definitions.cuh | 3 - source/EngineKernels/DetonatorProcessor.cuh | 1 - source/EngineKernels/GeneratorProcessor.cuh | 2 - source/EngineKernels/InjectorProcessor.cuh | 1 - source/EngineKernels/MaxAgeBalancer.cu | 111 ---------------- source/EngineKernels/MaxAgeBalancer.cuh | 29 ---- source/EngineKernels/MuscleProcessor.cuh | 6 - source/EngineKernels/ReconnectorProcessor.cuh | 2 - source/EngineKernels/SensorProcessor.cuh | 4 - source/EngineKernels/SimulationStatistics.cuh | 82 ------------ source/EngineKernels/StatisticsKernels.cu | 65 --------- source/EngineKernels/StatisticsKernels.cuh | 4 - source/EngineTests/CMakeLists.txt | 1 + .../LineageHistoryServiceTests.cu} | 28 ++-- source/Gui/EvolutionDashboardWindow.cpp | 30 ++--- source/Gui/EvolutionDashboardWindow.h | 4 +- source/Gui/TemporalControlWindow.cpp | 5 - source/Gui/TimelineLiveStatistics.cpp | 5 +- source/Gui/TimelineLiveStatistics.h | 5 +- 48 files changed, 147 insertions(+), 921 deletions(-) delete mode 100644 source/EngineInterface/LineageHistoryService.cpp delete mode 100644 source/EngineInterface/LineageHistoryService.h delete mode 100644 source/EngineInterface/TimelineStatistics.h delete mode 100644 source/EngineKernels/MaxAgeBalancer.cu delete mode 100644 source/EngineKernels/MaxAgeBalancer.cuh rename source/{EngineInterfaceTests/LineageHistoryServiceTests.cpp => EngineTests/LineageHistoryServiceTests.cu} (79%) diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index ba49dc241..d37db4bca 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); } -TimelineStatistics EngineWorker::getTimelineStatistics() const -{ - return _simulationCudaFacade->getTimelineStatistics(); -} - StatisticsHistory const& EngineWorker::getStatisticsHistory() const { return _simulationCudaFacade->getStatisticsHistory(); @@ -278,7 +273,6 @@ void EngineWorker::setCurrentTimestep(uint64_t value) { EngineWorkerGuard access(this); _simulationCudaFacade->setCurrentTimestep(value); - resetTimeIntervalStatistics(); } SimulationParameters EngineWorker::getSimulationParameters() const @@ -553,10 +547,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 9f7034327..1589281e9 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -24,7 +24,6 @@ #include #include #include -#include #include @@ -58,7 +57,6 @@ class EngineWorker Desc getSimulationData(IntVector2D const& rectUpperLeft, IntVector2D const& rectLowerRight); Desc getSelectedSimulationData(bool includeClusters); Desc getInspectedSimulationData(std::vector objectsIds); - TimelineStatistics getTimelineStatistics() const; StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); LineageStatistics getLineageStatistics() const; @@ -138,7 +136,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 bca89588a..31445d852 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -50,8 +49,6 @@ 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. @@ -81,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); @@ -436,34 +432,9 @@ ArraySizesForTOs _SimulationCudaFacade::estimateCapacityNeededForTO() const return DataAccessKernelsService::get().estimateCapacityNeededForTO(_settings.cudaSettings, getSimulationDataPtrCopy()); } -TimelineStatistics _SimulationCudaFacade::getTimelineStatistics() -{ - std::lock_guard lock(_mutexForStatistics); - if (_statisticsData) { - return *_statisticsData; - } else { - return TimelineStatistics(); - } -} - void _SimulationCudaFacade::updateStatistics() { updateEvolutionStatistics(); - updateTimestepStatistics(); -} - -void _SimulationCudaFacade::updateTimestepStatistics() -{ - StatisticsKernelsService::get().updateStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); - syncAndCheck(); - - OverallStatisticsEntry overallStatistics; - { - std::lock_guard lock(_mutexForStatistics); - _statisticsData = _cudaSimulationStatistics->getStatistics(); - overallStatistics = _overallStatisticsData.value_or(OverallStatisticsEntry()); - } - StatisticsService::get().addDataPoint(_statisticsHistory, *_statisticsData, overallStatistics, getCurrentTimestep()); } void _SimulationCudaFacade::updateEvolutionStatistics() @@ -478,7 +449,8 @@ void _SimulationCudaFacade::updateEvolutionStatistics() _lineageStatisticsData = lineageStatistics; _overallStatisticsData = overallStatistics; } - _lineageHistoryService.addSample(_lineageHistory, lineageStatistics, getCurrentTimestep()); + StatisticsService::get().addSample(_lineageHistory, lineageStatistics, getCurrentTimestep()); + StatisticsService::get().addDataPoint(_statisticsHistory, overallStatistics, getCurrentTimestep()); } StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const @@ -516,11 +488,6 @@ void _SimulationCudaFacade::setStatisticsHistory(StatisticsHistoryData const& da StatisticsService::get().rewriteHistory(_statisticsHistory, data, getCurrentTimestep()); } -void _SimulationCudaFacade::resetTimeIntervalStatistics() -{ - _cudaSimulationStatistics->resetAccumulatedStatistics(); -} - uint64_t _SimulationCudaFacade::getCurrentTimestep() const { std::lock_guard lock(_mutexForSimulationData); @@ -865,11 +832,9 @@ void _SimulationCudaFacade::calcTimestepsInternal(uint64_t timesteps, bool force resizeArraysIfNecessary(); } - auto statistics = getTimelineStatistics(); { 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)); } @@ -877,12 +842,6 @@ void _SimulationCudaFacade::calcTimestepsInternal(uint64_t timesteps, bool force if (getCurrentTimestep() % EvolutionStatisticsUpdateInterval == 0) { updateEvolutionStatistics(); } - - auto now = std::chrono::steady_clock::now(); - if (!_lastStatisticsUpdateTime || now - *_lastStatisticsUpdateTime > StatisticsUpdate) { - _lastStatisticsUpdateTime = now; - updateTimestepStatistics(); - } } if (forceUpdateStatistics) { updateStatistics(); diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index 96f2bff4b..deca4e654 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -22,7 +21,6 @@ #include #include #include -#include #include #include @@ -87,7 +85,6 @@ public: ArraySizesForTOs estimateCapacityNeededForTO() const; - TimelineStatistics getTimelineStatistics(); void updateStatistics(); StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); @@ -95,7 +92,6 @@ public: OverallStatisticsEntry getOverallStatistics(); LineageHistory const& getLineageHistory() const; - void resetTimeIntervalStatistics(); uint64_t getCurrentTimestep() const; void setCurrentTimestep(uint64_t timestep); @@ -128,7 +124,6 @@ public: private: void initCuda(); - void updateTimestepStatistics(); void updateEvolutionStatistics(); void syncAndCheck(); @@ -163,14 +158,10 @@ private: TOProvider _collectionTOProvider; mutable std::mutex _mutexForStatistics; - std::optional _lastStatisticsUpdateTime; - std::optional _statisticsData; StatisticsHistory _statisticsHistory; std::optional _lineageStatisticsData; std::optional _overallStatisticsData; LineageHistory _lineageHistory; - LineageHistoryService _lineageHistoryService; std::shared_ptr _cudaSimulationStatistics; std::shared_ptr _cudaPreviewStatistics; - MaxAgeBalancer _maxAgeBalancer; }; diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index 377e07fe3..7727c438a 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -320,11 +320,6 @@ IntVector2D _SimulationFacadeImpl::getWorldSize() const return _worldSize; } -TimelineStatistics _SimulationFacadeImpl::getTimelineStatistics() const -{ - return _worker.getTimelineStatistics(); -} - StatisticsHistory const& _SimulationFacadeImpl::getStatisticsHistory() const { return _worker.getStatisticsHistory(); diff --git a/source/EngineImpl/SimulationFacadeImpl.h b/source/EngineImpl/SimulationFacadeImpl.h index 37fa898f3..29c6ec0a5 100644 --- a/source/EngineImpl/SimulationFacadeImpl.h +++ b/source/EngineImpl/SimulationFacadeImpl.h @@ -88,7 +88,6 @@ class _SimulationFacadeImpl : public _SimulationFacade bool updateSelectionIfNecessary() override; IntVector2D getWorldSize() const override; - TimelineStatistics getTimelineStatistics() const override; StatisticsHistory const& getStatisticsHistory() const override; void setStatisticsHistory(StatisticsHistoryData const& data) override; LineageStatistics getLineageStatistics() const override; diff --git a/source/EngineImpl/SimulationParametersUpdateService.cu b/source/EngineImpl/SimulationParametersUpdateService.cu index ea685aac8..afdb714ab 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, - TimelineStatistics 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 e860f7b7c..0def1858e 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, - TimelineStatistics 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 e60a1199e..0e36eac07 100644 --- a/source/EngineImpl/StatisticsKernelsService.cu +++ b/source/EngineImpl/StatisticsKernelsService.cu @@ -6,13 +6,6 @@ void StatisticsKernelsService::init() {} void StatisticsKernelsService::shutdown() {} -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); -} - void StatisticsKernelsService::updateEvolutionStatistics( CudaSettings const& gpuSettings, SimulationData const& data, diff --git a/source/EngineImpl/StatisticsKernelsService.cuh b/source/EngineImpl/StatisticsKernelsService.cuh index 6cc60cd4c..db617f277 100644 --- a/source/EngineImpl/StatisticsKernelsService.cuh +++ b/source/EngineImpl/StatisticsKernelsService.cuh @@ -16,8 +16,6 @@ public: void init(); void shutdown(); - void updateStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics); - // 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 updateEvolutionStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics); diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index f9c85e6fb..6a7babaed 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -1,6 +1,9 @@ #include "StatisticsService.cuh" +#include +#include + #include #include @@ -10,11 +13,7 @@ namespace auto constexpr MaxSamples = 1000; } -void StatisticsService::addDataPoint( - StatisticsHistory& history, - TimelineStatistics const& newTimelineStatistics, - OverallStatisticsEntry const& overallStatistics, - uint64_t timestep) +void StatisticsService::addDataPoint(StatisticsHistory& history, OverallStatisticsEntry const& overallStatistics, uint64_t timestep) { std::lock_guard lock(history.getMutex()); auto& historyData = history.getDataRef(); @@ -23,21 +22,19 @@ void StatisticsService::addDataPoint( historyData.clear(); } - if (!_lastTimelineStatistics || historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta / 100 * (_numDataPoints + 1)) { + if (!_lastTimestep || historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta / 100 * (_numDataPoints + 1)) { auto newDataPoint = [&] { - if (!_lastTimelineStatistics && !historyData.empty()) { + if (!_lastTimestep && !historyData.empty()) { - //reuse last entry if no raw statistics is available + //reuse last entry if no statistics is available auto result = historyData.back(); result.time = toDouble(timestep); return result; } else { - return StatisticsConverterService::get().convert( - newTimelineStatistics, overallStatistics, timestep, toDouble(timestep), _lastTimelineStatistics, _lastTimestep); + return StatisticsConverterService::get().convert(overallStatistics, timestep, toDouble(timestep)); } }(); - _lastTimelineStatistics = newTimelineStatistics; _lastTimestep = timestep; _accumulatedDataPoint = _accumulatedDataPoint.has_value() ? *_accumulatedDataPoint + newDataPoint : newDataPoint; ++_numDataPoints; @@ -105,7 +102,6 @@ void StatisticsService::rewriteHistory(StatisticsHistory& history, StatisticsHis { _accumulatedDataPoint.reset(); _numDataPoints = 0; - _lastTimelineStatistics.reset(); _lastTimestep.reset(); if (!newHistoryData.empty()) { _longtermTimestepDelta = max(DefaultTimeStepDelta, (timestep - newHistoryData.front().time) / toDouble(newHistoryData.size())); @@ -116,3 +112,85 @@ void StatisticsService::rewriteHistory(StatisticsHistory& history, StatisticsHis std::lock_guard lock(history.getMutex()); history.getDataRef() = newHistoryData; } + +void StatisticsService::addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, 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(); + _lineageTimestepDelta = DefaultTimeStepDelta; + } + + if (!historyData.empty() && toDouble(timestep) - historyData.back().time < _lineageTimestepDelta) { + return; + } + + LineageSample sample; + sample.time = toDouble(timestep); + auto now = std::chrono::system_clock::now(); + auto unixEpoch = std::chrono::time_point(); + sample.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); + sample.entries = lineageStatistics.entries; + std::sort(sample.entries.begin(), sample.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); + historyData.emplace_back(std::move(sample)); + + if (historyData.size() > MaxSamples) { + LineageHistoryData newData; + newData.reserve(historyData.size() / 2 + 1); + for (size_t i = 0; i < (historyData.size() - 1) / 2; ++i) { + newData.emplace_back(mergeSamples(historyData.at(i * 2), historyData.at(i * 2 + 1))); + } + newData.emplace_back(historyData.back()); + historyData.swap(newData); + + _lineageTimestepDelta *= 2.0; + } +} + +void StatisticsService::reset() +{ + _lineageTimestepDelta = DefaultTimeStepDelta; +} + +LineageSample StatisticsService::mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample) +{ + LineageSample result; + result.time = earlierSample.time; + result.systemClock = (earlierSample.systemClock + laterSample.systemClock) / 2; + result.entries.reserve(earlierSample.entries.size() + laterSample.entries.size()); + + auto mergeEntries = [](LineageStatisticsEntry const& lhs, LineageStatisticsEntry const& rhs) { + LineageStatisticsEntry result; + result.lineageId = lhs.lineageId; + result.colorBitset = lhs.colorBitset | rhs.colorBitset; + result.numCreatures = (lhs.numCreatures + rhs.numCreatures) / 2; + result.numGenomes = (lhs.numGenomes + rhs.numGenomes) / 2; + result.sumCreatureCells = (lhs.sumCreatureCells + rhs.sumCreatureCells) / 2; + result.sumCreatureGenerations = (lhs.sumCreatureGenerations + rhs.sumCreatureGenerations) / 2; + result.sumGenomeNodes = (lhs.sumGenomeNodes + rhs.sumGenomeNodes) / 2; + result.sumMutationRates = (lhs.sumMutationRates + rhs.sumMutationRates) / 2; + result.sumCreatureEnergy = (lhs.sumCreatureEnergy + rhs.sumCreatureEnergy) / 2; + result.numCreatedCreatures = std::max(lhs.numCreatedCreatures, rhs.numCreatedCreatures); + result.totalMutations = std::max(lhs.totalMutations, rhs.totalMutations); + return result; + }; + + auto earlierIter = earlierSample.entries.begin(); + auto laterIter = laterSample.entries.begin(); + while (earlierIter != earlierSample.entries.end() || laterIter != laterSample.entries.end()) { + if (laterIter == laterSample.entries.end() || (earlierIter != earlierSample.entries.end() && earlierIter->lineageId < laterIter->lineageId)) { + result.entries.emplace_back(*earlierIter); + ++earlierIter; + } else if (earlierIter == earlierSample.entries.end() || laterIter->lineageId < earlierIter->lineageId) { + result.entries.emplace_back(*laterIter); + ++laterIter; + } else { + result.entries.emplace_back(mergeEntries(*earlierIter, *laterIter)); + ++earlierIter; + ++laterIter; + } + } + return result; +} diff --git a/source/EngineImpl/StatisticsService.cuh b/source/EngineImpl/StatisticsService.cuh index eb97e1233..cb8a5f0af 100644 --- a/source/EngineImpl/StatisticsService.cuh +++ b/source/EngineImpl/StatisticsService.cuh @@ -1,7 +1,11 @@ +#pragma once + #include #include +#include +#include #include #include @@ -12,22 +16,22 @@ class StatisticsService MAKE_SINGLETON(StatisticsService); public: - void addDataPoint( - StatisticsHistory& history, - TimelineStatistics const& newTimelineStatistics, - OverallStatisticsEntry const& overallStatistics, - uint64_t timestep); + void addDataPoint(StatisticsHistory& history, OverallStatisticsEntry const& overallStatistics, uint64_t timestep); void resetTime(StatisticsHistory& history, uint64_t timestep); void rewriteHistory(StatisticsHistory& history, StatisticsHistoryData const& newHistoryData, uint64_t timestep); + void addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, uint64_t timestep); + void reset(); + private: + LineageSample mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample); + static auto constexpr DefaultTimeStepDelta = 10.0; double _longtermTimestepDelta = DefaultTimeStepDelta; - int _numDataPoints = 0; std::optional _accumulatedDataPoint; - - std::optional _lastTimelineStatistics; std::optional _lastTimestep; + + double _lineageTimestepDelta = DefaultTimeStepDelta; }; diff --git a/source/EngineInterface/CMakeLists.txt b/source/EngineInterface/CMakeLists.txt index 5718562fb..aff034f0e 100644 --- a/source/EngineInterface/CMakeLists.txt +++ b/source/EngineInterface/CMakeLists.txt @@ -28,8 +28,6 @@ add_library(EngineInterface InspectedEntityIds.h LineageHistory.cpp LineageHistory.h - LineageHistoryService.cpp - LineageHistoryService.h LineageStatistics.h LocationHelper.cpp LocationHelper.h @@ -66,8 +64,7 @@ add_library(EngineInterface StatisticsConverterService.cpp StatisticsConverterService.h StatisticsHistory.cpp - StatisticsHistory.h - TimelineStatistics.h) + StatisticsHistory.h) target_link_libraries(EngineInterface Base) diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index a1159347b..a304a8f52 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -1,55 +1,10 @@ #include "DataPointCollection.h" -DataPoint DataPoint::operator+(DataPoint 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; - return result; -} - -DataPoint DataPoint::operator/(double divisor) const -{ - DataPoint result; - for (int i = 0; i < MAX_COLORS; ++i) { - result.values[i] = values[i] / divisor; - } - result.summedValues = summedValues / divisor; - return result; -} - DataPointCollection DataPointCollection::operator+(DataPointCollection const& other) const { DataPointCollection result; result.time = time + other.time; 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.numCreatures = numCreatures + other.numCreatures; result.averageCreatureCells = averageCreatureCells + other.averageCreatureCells; result.averageGenomeNodes = averageGenomeNodes + other.averageGenomeNodes; @@ -70,31 +25,6 @@ DataPointCollection DataPointCollection::operator/(double divisor) const DataPointCollection result; result.time = time / 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.numCreatures = numCreatures / divisor; result.averageCreatureCells = averageCreatureCells / divisor; result.averageGenomeNodes = averageGenomeNodes / divisor; diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index 484622820..38f484b8d 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -1,49 +1,11 @@ #pragma once -#include - -struct DataPoint -{ - double values[MAX_COLORS] = {0, 0, 0, 0, 0, 0, 0}; - double summedValues = 0; - - DataPoint operator+(DataPoint const& other) const; - DataPoint operator/(double divisor) const; -}; - struct DataPointCollection { - double time; //could be a time step or real-time + double time = 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; - - // Evolution dashboard values (not color-resolved) + // Evolution dashboard values double numCreatures = 0; double averageCreatureCells = 0; double averageGenomeNodes = 0; diff --git a/source/EngineInterface/Definitions.h b/source/EngineInterface/Definitions.h index b2ed4c4d9..26b655323 100644 --- a/source/EngineInterface/Definitions.h +++ b/source/EngineInterface/Definitions.h @@ -28,8 +28,6 @@ struct SettingsForSimulation; class _SimulationFacade; using SimulationFacade = std::shared_ptr<_SimulationFacade>; -struct TimelineStatistics; - class SpaceCalculator; class ShapeGenerator; diff --git a/source/EngineInterface/LineageHistoryService.cpp b/source/EngineInterface/LineageHistoryService.cpp deleted file mode 100644 index a1f3bd2e3..000000000 --- a/source/EngineInterface/LineageHistoryService.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "LineageHistoryService.h" - -#include -#include - -#include - -void LineageHistoryService::addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, 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(); - _longtermTimestepDelta = DefaultTimestepDelta; - } - - if (!historyData.empty() && toDouble(timestep) - historyData.back().time < _longtermTimestepDelta) { - return; - } - - LineageSample sample; - sample.time = toDouble(timestep); - auto now = std::chrono::system_clock::now(); - auto unixEpoch = std::chrono::time_point(); - sample.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); - sample.entries = lineageStatistics.entries; - std::sort(sample.entries.begin(), sample.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); - historyData.emplace_back(std::move(sample)); - - if (historyData.size() > MaxSamples) { - LineageHistoryData newData; - newData.reserve(historyData.size() / 2 + 1); - for (size_t i = 0; i < (historyData.size() - 1) / 2; ++i) { - newData.emplace_back(mergeSamples(historyData.at(i * 2), historyData.at(i * 2 + 1))); - } - newData.emplace_back(historyData.back()); - historyData.swap(newData); - - _longtermTimestepDelta *= 2.0; - } -} - -void LineageHistoryService::reset() -{ - _longtermTimestepDelta = DefaultTimestepDelta; -} - -LineageSample LineageHistoryService::mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample) -{ - LineageSample result; - result.time = earlierSample.time; - result.systemClock = (earlierSample.systemClock + laterSample.systemClock) / 2; - result.entries.reserve(earlierSample.entries.size() + laterSample.entries.size()); - - auto mergeEntries = [](LineageStatisticsEntry const& lhs, LineageStatisticsEntry const& rhs) { - LineageStatisticsEntry result; - result.lineageId = lhs.lineageId; - result.colorBitset = lhs.colorBitset | rhs.colorBitset; - result.numCreatures = (lhs.numCreatures + rhs.numCreatures) / 2; - result.numGenomes = (lhs.numGenomes + rhs.numGenomes) / 2; - result.sumCreatureCells = (lhs.sumCreatureCells + rhs.sumCreatureCells) / 2; - result.sumCreatureGenerations = (lhs.sumCreatureGenerations + rhs.sumCreatureGenerations) / 2; - result.sumGenomeNodes = (lhs.sumGenomeNodes + rhs.sumGenomeNodes) / 2; - result.sumMutationRates = (lhs.sumMutationRates + rhs.sumMutationRates) / 2; - result.sumCreatureEnergy = (lhs.sumCreatureEnergy + rhs.sumCreatureEnergy) / 2; - result.numCreatedCreatures = std::max(lhs.numCreatedCreatures, rhs.numCreatedCreatures); - result.totalMutations = std::max(lhs.totalMutations, rhs.totalMutations); - return result; - }; - - auto earlierIter = earlierSample.entries.begin(); - auto laterIter = laterSample.entries.begin(); - while (earlierIter != earlierSample.entries.end() || laterIter != laterSample.entries.end()) { - if (laterIter == laterSample.entries.end() || (earlierIter != earlierSample.entries.end() && earlierIter->lineageId < laterIter->lineageId)) { - result.entries.emplace_back(*earlierIter); - ++earlierIter; - } else if (earlierIter == earlierSample.entries.end() || laterIter->lineageId < earlierIter->lineageId) { - result.entries.emplace_back(*laterIter); - ++laterIter; - } else { - result.entries.emplace_back(mergeEntries(*earlierIter, *laterIter)); - ++earlierIter; - ++laterIter; - } - } - return result; -} diff --git a/source/EngineInterface/LineageHistoryService.h b/source/EngineInterface/LineageHistoryService.h deleted file mode 100644 index 7bb5e2ccb..000000000 --- a/source/EngineInterface/LineageHistoryService.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -#include "LineageHistory.h" -#include "LineageStatistics.h" - -class LineageHistoryService -{ -public: - void addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, uint64_t timestep); - void reset(); - - // Merges two consecutive samples: union of the lineage ids, momentary values are averaged - // and accumulated values are maximized where a lineage is present in both samples. - static LineageSample mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample); - -private: - static auto constexpr MaxSamples = 1000; - static auto constexpr DefaultTimestepDelta = 10.0; - - double _longtermTimestepDelta = DefaultTimestepDelta; -}; diff --git a/source/EngineInterface/SimulationFacade.h b/source/EngineInterface/SimulationFacade.h index 2fd57e9ce..bd24b01a8 100644 --- a/source/EngineInterface/SimulationFacade.h +++ b/source/EngineInterface/SimulationFacade.h @@ -13,7 +13,6 @@ #include "ShallowUpdateSelectionData.h" #include "SimulationParametersUpdateConfig.h" #include "StatisticsHistory.h" -#include "TimelineStatistics.h" class _SimulationFacade { @@ -107,7 +106,6 @@ class _SimulationFacade //************ //* Statistics //************ - virtual TimelineStatistics getTimelineStatistics() const = 0; virtual StatisticsHistory const& getStatisticsHistory() const = 0; virtual void setStatisticsHistory(StatisticsHistoryData const& data) = 0; virtual LineageStatistics getLineageStatistics() const = 0; diff --git a/source/EngineInterface/SimulationParameters.cpp b/source/EngineInterface/SimulationParameters.cpp index eb8f806bc..a12149d89 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 36ec841fb..7caffffef 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 5b8af0e84..0b493efd0 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -4,86 +4,10 @@ #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, OverallStatisticsEntry const& overallStatistics, uint64_t timestep, - double time, - std::optional const& lastData, - std::optional lastTimestep) + double time) { DataPointCollection result; result.time = time; @@ -92,15 +16,6 @@ DataPointCollection StatisticsConverterService::convert( 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 const& overall = overallStatistics; result.numCreatures = toDouble(overall.numCreatures); result.averageCreatureCells = overall.numCreatures > 0 ? toDouble(overall.sumCreatureCells) / overall.numCreatures : 0.0; @@ -115,43 +30,5 @@ DataPointCollection StatisticsConverterService::convert( result.accumCreatedCreatures = toDouble(overall.numCreatedCreatures); result.accumMutations = toDouble(overall.totalMutations); - 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]; - } - 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 fa562e279..cb7b5c533 100644 --- a/source/EngineInterface/StatisticsConverterService.h +++ b/source/EngineInterface/StatisticsConverterService.h @@ -10,11 +10,5 @@ class StatisticsConverterService MAKE_SINGLETON(StatisticsConverterService); public: - DataPointCollection convert( - TimelineStatistics const& newData, - OverallStatisticsEntry const& overallStatistics, - uint64_t timestep, - double time, - std::optional const& lastData, - std::optional lastTimestep); + DataPointCollection convert(OverallStatisticsEntry const& overallStatistics, uint64_t timestep, double time); }; diff --git a/source/EngineInterface/TimelineStatistics.h b/source/EngineInterface/TimelineStatistics.h deleted file mode 100644 index 60c23923e..000000000 --- a/source/EngineInterface/TimelineStatistics.h +++ /dev/null @@ -1,49 +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; -}; - -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/EngineInterfaceTests/CMakeLists.txt b/source/EngineInterfaceTests/CMakeLists.txt index aa5409e3b..68e3effb5 100644 --- a/source/EngineInterfaceTests/CMakeLists.txt +++ b/source/EngineInterfaceTests/CMakeLists.txt @@ -4,7 +4,6 @@ PUBLIC DescValidationServiceTests.cpp GenomeDescEditServiceTests.cpp GenomeDescInfoServiceTests.cpp - LineageHistoryServiceTests.cpp ParametersEditServiceTests.cpp PreviewDescConverterServiceTests.cpp SpecificationFilterServiceTests.cpp diff --git a/source/EngineKernels/AttackerProcessor.cuh b/source/EngineKernels/AttackerProcessor.cuh index 01b6cb3e9..6d41d3d66 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 c889e197f..94a9e2798 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 cc9fa6cfe..9f3d8f044 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 110e6747f..5d9c723f3 100644 --- a/source/EngineKernels/ConstructorProcessor.cuh +++ b/source/EngineKernels/ConstructorProcessor.cuh @@ -638,8 +638,6 @@ __inline__ __device__ Object* ConstructorProcessor::constructCellIntern( } } - statistics.incNumCreatedCells(hostObject->color); - return result; } diff --git a/source/EngineKernels/Definitions.cuh b/source/EngineKernels/Definitions.cuh index d19c60554..2cb0ac69e 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 614c542da..42c5dd229 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/GeneratorProcessor.cuh b/source/EngineKernels/GeneratorProcessor.cuh index d31a199ed..771340377 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 182441e21..871455f0e 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 e65678372..000000000 --- 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, TimelineStatistics 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, TimelineStatistics const& statistics, uint64_t timestep) -{ - auto result = false; - for (int i = 0; i < MAX_COLORS; ++i) { - _numReplicators[i] += statistics.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 5b81614f6..000000000 --- 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, TimelineStatistics const& statistics, uint64_t timestep); - -private: - void initializeIfNecessary(SimulationParameters const& parameters, uint64_t timestep); - bool doAdaptionIfNecessary(SimulationParameters& parameters, TimelineStatistics 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 24e3596df..a114dc04b 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/ReconnectorProcessor.cuh b/source/EngineKernels/ReconnectorProcessor.cuh index bcb469cf4..ed5182062 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 d343be577..4c546e8ec 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 a8ab3d4f7..1a9a7ee95 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -2,7 +2,6 @@ #include #include -#include #include "Base.cuh" #include "CudaMemoryManager.cuh" @@ -13,7 +12,6 @@ public: __host__ void init() { _lineageArrayCapacity = 1 << 18; - CudaMemoryManager::getInstance().acquireMemory(1, _data); CudaMemoryManager::getInstance().acquireMemory(1, _overallStatisticsEntry); CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageStatisticsEntries); @@ -23,7 +21,6 @@ public: CudaMemoryManager::getInstance().acquireMemory(1, _lineageAccumulatorMapControl); CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[0]); CudaMemoryManager::getInstance().acquireMemory(_lineageArrayCapacity, _lineageAccumulatorMaps[1]); - CHECK_FOR_DEVICE_ERRORS(cudaMemset(_data, 0, sizeof(TimelineStatistics))); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_overallStatisticsEntry, 0, sizeof(OverallStatisticsEntry))); CHECK_FOR_DEVICE_ERRORS(cudaMemset(_lineageAccumulatorMapControl, 0, sizeof(LineageAccumulatorMapControl))); @@ -38,7 +35,6 @@ public: __host__ void free() { - CudaMemoryManager::getInstance().freeMemory(_data); CudaMemoryManager::getInstance().freeMemory(_overallStatisticsEntry); CudaMemoryManager::getInstance().freeMemory(_lineageStatisticsEntries); @@ -49,13 +45,6 @@ public: CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[1]); } - __host__ TimelineStatistics getStatistics() - { - TimelineStatistics result; - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _data, sizeof(TimelineStatistics), cudaMemcpyDeviceToHost)); - return result; - } - __host__ OverallStatisticsEntry getOverallStatistics() { OverallStatisticsEntry result; @@ -81,44 +70,6 @@ public: return control.numUsedAccumulatorSlots[control.activeAccumulatorBuffer] > static_cast(_lineageArrayCapacity) / 2; } - //timestep statistics - __inline__ __device__ void resetTimestepData() - { - if (threadIdx.x == 0 && blockIdx.x == 0) { - _data->timestep = TimestepStatistics(); - } - } - - __inline__ __device__ void incNumObjects(int color) { atomicAdd(&(_data->timestep.numObjects[color]), 1); } - __inline__ __device__ void incNumReplicator(int color) { atomicAdd(&_data->timestep.numSelfReplicators[color], 1); } - __inline__ __device__ int getNumReplicators() - { - auto result = 0; - for (int i = 0; i < MAX_COLORS; ++i) { - result += _data->timestep.numSelfReplicators[i]; - } - return result; - } - __inline__ __device__ void incNumViruses(int color) { atomicAdd(&_data->timestep.numViruses[color], 1); } - __inline__ __device__ void incNumFreeCells(int color) { atomicAdd(&_data->timestep.numFreeCells[color], 1); } - __inline__ __device__ void incNumParticles(int color) { atomicAdd(&_data->timestep.numEnergyParticles[color], 1); } - __inline__ __device__ void addEnergy(int color, float valueToAdd) { atomicAdd(&_data->timestep.totalEnergy[color], valueToAdd); } - __inline__ __device__ void addNumObjects(int color, float valueToAdd) { atomicAdd(&_data->timestep.numObjects[color], valueToAdd); } - __inline__ __device__ double getSummedNumCells() - { - auto result = 0.0; - for (int i = 0; i < MAX_COLORS; ++i) { - result += toDouble(_data->timestep.numObjects[i]); - } - return result; - } - __inline__ __device__ void halveNumConnections() - { - for (int i = 0; i < MAX_COLORS; ++i) { - _data->timestep.numFreeCells[i] /= 2; - } - } - //evolution statistics (timestep) __inline__ __device__ void resetEvolutionStatistics() { @@ -281,37 +232,6 @@ public: } __inline__ __device__ void flipAccumulatorBuffers() { _lineageAccumulatorMapControl->activeAccumulatorBuffer = 1 - _lineageAccumulatorMapControl->activeAccumulatorBuffer; } - //accumulated statistics - __host__ void resetAccumulatedStatistics() - { - TimelineStatistics hostData; - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&hostData, _data, sizeof(TimelineStatistics), cudaMemcpyDeviceToHost)); - hostData.accumulated = AccumulatedStatistics(); - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(_data, &hostData, sizeof(TimelineStatistics), cudaMemcpyHostToDevice)); - } - - __inline__ __device__ void incNumCreatedCells(int color) { alienAtomicAdd64(&_data->accumulated.numCreatedCells[color], uint64_t(1)); } - __inline__ __device__ void incNumCreatedReplicators(int color) { alienAtomicAdd64(&_data->accumulated.numCreatedReplicators[color], uint64_t(1)); } - __inline__ __device__ void incNumAttacks(int color) { alienAtomicAdd64(&_data->accumulated.numAttacks[color], uint64_t(1)); } - __inline__ __device__ void incNumMuscleActivities(int color) { alienAtomicAdd64(&_data->accumulated.numMuscleActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumDefenderActivities(int color) { alienAtomicAdd64(&_data->accumulated.numDefenderActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumDepotActivities(int color) { alienAtomicAdd64(&_data->accumulated.numDepotActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumInjectionActivities(int color) - { - alienAtomicAdd64(&_data->accumulated.numInjectionActivities[color], uint64_t(1)); - } - __inline__ __device__ void incNumCompletedInjections(int color) - { - alienAtomicAdd64(&_data->accumulated.numCompletedInjections[color], uint64_t(1)); - } - __inline__ __device__ void incNumGeneratorPulses(int color) { alienAtomicAdd64(&_data->accumulated.numGeneratorPulses[color], uint64_t(1)); } - __inline__ __device__ void incNumNeuronActivities(int color) { alienAtomicAdd64(&_data->accumulated.numNeuronActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumSensorActivities(int color) { alienAtomicAdd64(&_data->accumulated.numSensorActivities[color], uint64_t(1)); } - __inline__ __device__ void incNumSensorMatches(int color) { alienAtomicAdd64(&_data->accumulated.numSensorMatches[color], uint64_t(1)); } - __inline__ __device__ void incNumReconnectorCreated(int color) { alienAtomicAdd64(&_data->accumulated.numReconnectorCreated[color], uint64_t(1)); } - __inline__ __device__ void incNumReconnectorRemoved(int color) { alienAtomicAdd64(&_data->accumulated.numReconnectorRemoved[color], uint64_t(1)); } - __inline__ __device__ void incNumDetonations(int color) { alienAtomicAdd64(&_data->accumulated.numDetonations[color], uint64_t(1)); } - __inline__ __device__ void incCreatedCreature(uint32_t lineageId) { atomicAdd(&_overallStatisticsEntry->numCreatedCreatures, 1u); @@ -406,8 +326,6 @@ private: return -1; } - TimelineStatistics* _data; - int _lineageArrayCapacity; // Used for all lineage maps and arrays OverallStatisticsEntry* _overallStatisticsEntry; diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 3ec1432d4..26be1b4cd 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -1,70 +1,5 @@ #include "StatisticsKernels.cuh" -__global__ void cudaUpdateTimestepStatistics_substep1(SimulationData data, SimulationStatistics statistics) -{ - statistics.resetTimestepData(); -} - -__global__ void cudaUpdateTimestepStatistics_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); - statistics.incNumObjects(object->color); - statistics.addEnergy(object->color, object->getEnergy()); - if (object->type == ObjectType_FreeCell) { - statistics.incNumFreeCells(object->color); - } - //if (object->typeData.cell.cellType == CellType_Constructor && GenomeDecoder::containsSelfReplication(object->typeData.cell.cellTypeData.constructor)) { - // statistics.incNumReplicator(object->color); - // 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& 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); - } - } -} - -__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); - //} - //} - } -} - namespace { __device__ float calcMeanMutationRate(MutationRates const& rates) diff --git a/source/EngineKernels/StatisticsKernels.cuh b/source/EngineKernels/StatisticsKernels.cuh index 7c3a16530..bd617728e 100644 --- a/source/EngineKernels/StatisticsKernels.cuh +++ b/source/EngineKernels/StatisticsKernels.cuh @@ -6,10 +6,6 @@ #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); diff --git a/source/EngineTests/CMakeLists.txt b/source/EngineTests/CMakeLists.txt index 66884d677..addf2e698 100644 --- a/source/EngineTests/CMakeLists.txt +++ b/source/EngineTests/CMakeLists.txt @@ -38,6 +38,7 @@ PUBLIC IntegrationTestFramework.cpp IntegrationTestFramework.h LayerParameterTests.cpp + LineageHistoryServiceTests.cu MemoryTests.cpp MetaMutationTests.cpp MoveNodeSectionMutationTests.cpp diff --git a/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp b/source/EngineTests/LineageHistoryServiceTests.cu similarity index 79% rename from source/EngineInterfaceTests/LineageHistoryServiceTests.cpp rename to source/EngineTests/LineageHistoryServiceTests.cu index 030bc267a..f79de001f 100644 --- a/source/EngineInterfaceTests/LineageHistoryServiceTests.cpp +++ b/source/EngineTests/LineageHistoryServiceTests.cu @@ -1,10 +1,12 @@ #include -#include +#include "StatisticsService.cuh" class LineageHistoryServiceTests : public ::testing::Test { protected: + void SetUp() override { StatisticsService::get().reset(); } + static LineageStatisticsEntry createEntry(uint32_t lineageId) { LineageStatisticsEntry result; @@ -32,7 +34,7 @@ TEST_F(LineageHistoryServiceTests, mergeSamples_disjointIds) laterSample.systemClock = 20; laterSample.entries = {createEntry(2), createEntry(4)}; - auto result = LineageHistoryService::mergeSamples(earlierSample, laterSample); + auto result = StatisticsService::mergeSamples(earlierSample, laterSample); EXPECT_EQ(100, result.time); EXPECT_EQ(15, result.systemClock); @@ -79,7 +81,7 @@ TEST_F(LineageHistoryServiceTests, mergeSamples_commonIds) laterSample.systemClock = 20; laterSample.entries = {entry2}; - auto result = LineageHistoryService::mergeSamples(earlierSample, laterSample); + auto result = StatisticsService::mergeSamples(earlierSample, laterSample); ASSERT_EQ(1, result.entries.size()); auto const& merged = result.entries.front(); @@ -92,16 +94,15 @@ TEST_F(LineageHistoryServiceTests, mergeSamples_commonIds) EXPECT_FLOAT_EQ(400, merged.sumGenomeNodes); EXPECT_FLOAT_EQ(5, merged.sumMutationRates); EXPECT_FLOAT_EQ(2000, merged.sumCreatureEnergy); - EXPECT_EQ(60, merged.numCreatedCreatures); //accumulated values are maximized + EXPECT_EQ(60, merged.numCreatedCreatures); EXPECT_FLOAT_EQ(9, merged.totalMutations); } TEST_F(LineageHistoryServiceTests, addSample_sortsEntriesById) { - LineageHistoryService service; LineageHistory history; - service.addSample(history, createRawStatistics({createEntry(5), createEntry(1), createEntry(3)}), 100); + StatisticsService::get().addSample(history, createRawStatistics({createEntry(5), createEntry(1), createEntry(3)}), 100); auto data = history.getCopiedData(); ASSERT_EQ(1, data.size()); @@ -113,12 +114,11 @@ TEST_F(LineageHistoryServiceTests, addSample_sortsEntriesById) TEST_F(LineageHistoryServiceTests, addSample_skipsSamplesBelowCadence) { - LineageHistoryService service; LineageHistory history; - service.addSample(history, createRawStatistics({createEntry(1)}), 100); - service.addSample(history, createRawStatistics({createEntry(2)}), 105); //below the default cadence of 10 timesteps - service.addSample(history, createRawStatistics({createEntry(3)}), 120); + StatisticsService::get().addSample(history, createRawStatistics({createEntry(1)}), 100); + StatisticsService::get().addSample(history, createRawStatistics({createEntry(2)}), 105); //below the default cadence of 10 timesteps + StatisticsService::get().addSample(history, createRawStatistics({createEntry(3)}), 120); auto data = history.getCopiedData(); ASSERT_EQ(2, data.size()); @@ -128,11 +128,10 @@ TEST_F(LineageHistoryServiceTests, addSample_skipsSamplesBelowCadence) TEST_F(LineageHistoryServiceTests, addSample_clearsHistoryOnTimeRegression) { - LineageHistoryService service; LineageHistory history; - service.addSample(history, createRawStatistics({createEntry(1)}), 1000); - service.addSample(history, createRawStatistics({createEntry(2)}), 100); + StatisticsService::get().addSample(history, createRawStatistics({createEntry(1)}), 1000); + StatisticsService::get().addSample(history, createRawStatistics({createEntry(2)}), 100); auto data = history.getCopiedData(); ASSERT_EQ(1, data.size()); @@ -143,12 +142,11 @@ TEST_F(LineageHistoryServiceTests, addSample_clearsHistoryOnTimeRegression) TEST_F(LineageHistoryServiceTests, addSample_compressesHistory) { - LineageHistoryService service; LineageHistory history; auto timestep = uint64_t(0); for (int i = 0; i < 1100; ++i) { - service.addSample(history, createRawStatistics({createEntry(1)}), timestep); + StatisticsService::get().addSample(history, createRawStatistics({createEntry(1)}), timestep); timestep += 10; } diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index de49e63a7..865892591 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -236,9 +236,8 @@ void EvolutionDashboardWindow::processBackground() _lastTimepoint = timepoint; _timeSinceSimStart += toDouble(duration) / 1000; - auto timelineStatistics = _SimulationFacade::get()->getTimelineStatistics(); auto overallStatistics = _SimulationFacade::get()->getOverallStatistics(); - _timelineLiveStatistics.update(timelineStatistics, overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); + _timelineLiveStatistics.update(overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); auto rawLineageStatistics = _SimulationFacade::get()->getLineageStatistics(); LineageSample sample; @@ -407,23 +406,20 @@ void EvolutionDashboardWindow::updateDisplayData() //header sparklines and deltas from the live statistics auto liveWindowInSeconds = liveGlobalHistory.size() >= 2 ? liveGlobalHistory.back().time - liveGlobalHistory.front().time : 0.0; - for (int cardIndex = 0; cardIndex < 4; ++cardIndex) { + for (int cardIndex = 0; cardIndex < 3; ++cardIndex) { auto& sparkline = _headerSparklines.at(cardIndex); sparkline.clear(); std::vector fullSeries; - if (cardIndex == 1) { + if (cardIndex == 0) { fullSeries = _externalEnergySeries; } else { fullSeries.reserve(liveGlobalHistory.size()); for (auto const& dataPoints : liveGlobalHistory) { switch (cardIndex) { - case 0: - fullSeries.emplace_back(dataPoints.totalEnergy.summedValues); - break; - case 2: + case 1: fullSeries.emplace_back(dataPoints.numLineages); break; - case 3: + case 2: fullSeries.emplace_back(dataPoints.numCreatures); break; } @@ -440,7 +436,7 @@ void EvolutionDashboardWindow::updateDisplayData() void EvolutionDashboardWindow::processHeader() { auto const& style = ImGui::GetStyle(); - auto cardWidth = (ImGui::GetContentRegionAvail().x - 4 * style.ItemSpacing.x) / 6; + auto cardWidth = (ImGui::GetContentRegionAvail().x - 3 * style.ItemSpacing.x) / 5; auto cardHeight = style.WindowPadding.y * 2 + ImGui::GetTextLineHeight() * 3 + StyleRepository::get().getLargeFont()->FontSize + style.ItemSpacing.y * 3 + scale(6.0f); @@ -449,16 +445,13 @@ void EvolutionDashboardWindow::processHeader() processEntitiesCard(cardWidth * 2, cardHeight); ImGui::SameLine(); - auto sumEnergy = lastDataPoints ? lastDataPoints->totalEnergy.summedValues : 0.0; - processKpiCard("SUM ENERGY", formatMetricValue(sumEnergy, 0), toFloat(_headerDeltas.at(0)), 0, cardWidth, cardHeight); - ImGui::SameLine(); auto externalEnergy = !_externalEnergySeries.empty() ? _externalEnergySeries.back() : 0.0; - processKpiCard("EXTERNAL ENERGY", formatMetricValue(externalEnergy, 0), toFloat(_headerDeltas.at(1)), 1, cardWidth, cardHeight); + processKpiCard("EXTERNAL ENERGY", formatMetricValue(externalEnergy, 0), toFloat(_headerDeltas.at(0)), 0, cardWidth, cardHeight); ImGui::SameLine(); auto numLineages = lastDataPoints ? lastDataPoints->numLineages : 0.0; - processKpiCard("LINEAGES", formatMetricValue(numLineages, 0), toFloat(_headerDeltas.at(2)), 2, cardWidth, cardHeight); + processKpiCard("LINEAGES", formatMetricValue(numLineages, 0), toFloat(_headerDeltas.at(1)), 1, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), toFloat(_headerDeltas.at(3)), 3, cardWidth, cardHeight); + processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), toFloat(_headerDeltas.at(2)), 2, cardWidth, cardHeight); } void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width, float height) @@ -513,15 +506,13 @@ void EvolutionDashboardWindow::processEntitiesCard(float width, float height) auto solids = 0.0; auto fluids = 0.0; auto cells = 0.0; - auto energyParticles = 0.0; auto total = 0.0; if (!liveGlobalHistory.empty()) { auto const& dataPoints = liveGlobalHistory.back(); solids = dataPoints.numSolidObjects; fluids = dataPoints.numFluidObjects; cells = dataPoints.numCellObjects; - energyParticles = dataPoints.numEnergyParticles.summedValues; - total = dataPoints.numObjects.summedValues + energyParticles; + total = solids + fluids + cells; } ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, scale(6.0f)); @@ -540,7 +531,6 @@ void EvolutionDashboardWindow::processEntitiesCard(float width, float height) {"Solids", solids}, {"Fluid particles", fluids}, {"Cells", cells}, - {"Energy particles", energyParticles}, }; for (auto const& [subLabel, subValue] : subValues) { ImGui::BeginGroup(); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index 6c7684448..73fc29e92 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -59,8 +59,8 @@ class EvolutionDashboardWindow : public AlienWindow std::vector _plottedLineages; LineageDisplayData _allLineages; std::array _metricWindowDeltas = {}; - std::array, 4> _headerSparklines = {}; - std::array _headerDeltas = {}; + std::array, 3> _headerSparklines = {}; + std::array _headerDeltas = {}; std::array _cellColors = {}; diff --git a/source/Gui/TemporalControlWindow.cpp b/source/Gui/TemporalControlWindow.cpp index 185b346de..b7a0a29b7 100644 --- a/source/Gui/TemporalControlWindow.cpp +++ b/source/Gui/TemporalControlWindow.cpp @@ -251,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 6dffa4bf9..4c86a9e88 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -13,7 +13,7 @@ std::vector const& TimelineLiveStatistics::getDataPointColl return _dataPointCollectionHistory; } -void TimelineLiveStatistics::update(TimelineStatistics const& data, OverallStatisticsEntry const& overallStatistics, uint64_t timestep) +void TimelineLiveStatistics::update(OverallStatisticsEntry const& overallStatistics, uint64_t timestep) { truncate(); @@ -23,9 +23,8 @@ void TimelineLiveStatistics::update(TimelineStatistics const& data, OverallStati _timeSinceSimStart += toDouble(duration) / 1000; - auto newDataPoint = StatisticsConverterService::get().convert(data, overallStatistics, timestep, _timeSinceSimStart, _lastData, _lastTimestep); + auto newDataPoint = StatisticsConverterService::get().convert(overallStatistics, timestep, _timeSinceSimStart); _dataPointCollectionHistory.emplace_back(newDataPoint); - _lastData = data; _lastTimestep = timestep; _lastTimepoint = timepoint; } diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index 93df73dc7..02e4fc81f 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -4,10 +4,8 @@ #include #include -#include #include #include -#include class TimelineLiveStatistics { @@ -15,7 +13,7 @@ class TimelineLiveStatistics static auto constexpr MaxLiveHistory = 240.0f; //in seconds std::vector const& getDataPointCollectionHistory() const; - void update(TimelineStatistics const& statistics, OverallStatisticsEntry const& overallStatistics, uint64_t timestep); + void update(OverallStatisticsEntry const& overallStatistics, uint64_t timestep); private: void truncate(); @@ -26,5 +24,4 @@ class TimelineLiveStatistics std::optional _lastTimestep; std::optional _lastTimepoint; - std::optional _lastData; }; From 71d86b91e006b4b3c6d21a7b5ca2135785a12bbd Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 22:39:33 +0200 Subject: [PATCH 19/41] Merge lineage and overall statistics --- source/EngineImpl/EngineWorker.cpp | 14 +- source/EngineImpl/EngineWorker.h | 8 +- source/EngineImpl/SimulationCudaFacade.cu | 24 +- source/EngineImpl/SimulationCudaFacade.cuh | 12 +- source/EngineImpl/SimulationFacadeImpl.cpp | 12 +- source/EngineImpl/SimulationFacadeImpl.h | 4 +- source/EngineImpl/StatisticsService.cu | 88 +---- source/EngineImpl/StatisticsService.cuh | 13 +- source/EngineInterface/CMakeLists.txt | 5 +- .../EngineInterface/DataPointCollection.cpp | 2 + source/EngineInterface/DataPointCollection.h | 6 + source/EngineInterface/LineageHistory.cpp | 23 -- source/EngineInterface/LineageHistory.h | 29 -- source/EngineInterface/LineageStatistics.h | 25 -- source/EngineInterface/OverallStatistics.h | 22 -- source/EngineInterface/SimulationFacade.h | 8 +- .../StatisticsConverterService.cpp | 6 +- .../StatisticsConverterService.h | 4 +- source/EngineInterface/StatisticsEntry.h | 45 +++ source/EngineKernels/SimulationStatistics.cuh | 19 +- source/EngineTests/CMakeLists.txt | 1 - .../EngineTests/EvolutionStatisticsTests.cpp | 48 ++- .../EngineTests/LineageHistoryServiceTests.cu | 158 --------- source/Gui/EvolutionDashboardWindow.cpp | 42 +-- source/Gui/EvolutionDashboardWindow.h | 6 +- source/Gui/TimelineLiveStatistics.cpp | 2 +- source/Gui/TimelineLiveStatistics.h | 4 +- .../PersisterInterface/SerializerService.cpp | 302 ------------------ 28 files changed, 130 insertions(+), 802 deletions(-) delete mode 100644 source/EngineInterface/LineageHistory.cpp delete mode 100644 source/EngineInterface/LineageHistory.h delete mode 100644 source/EngineInterface/LineageStatistics.h delete mode 100644 source/EngineInterface/OverallStatistics.h create mode 100644 source/EngineInterface/StatisticsEntry.h delete mode 100644 source/EngineTests/LineageHistoryServiceTests.cu diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index d37db4bca..a309cae71 100644 --- a/source/EngineImpl/EngineWorker.cpp +++ b/source/EngineImpl/EngineWorker.cpp @@ -109,19 +109,9 @@ void EngineWorker::setStatisticsHistory(StatisticsHistoryData const& data) _simulationCudaFacade->setStatisticsHistory(data); } -LineageStatistics EngineWorker::getLineageStatistics() const +StatisticsEntry EngineWorker::getOverallStatistics() const { - return _simulationCudaFacade->getLineageStatistics(); -} - -OverallStatisticsEntry EngineWorker::getOverallStatistics() const -{ - return _simulationCudaFacade->getOverallStatistics(); -} - -LineageHistory const& EngineWorker::getLineageHistory() const -{ - return _simulationCudaFacade->getLineageHistory(); + return _simulationCudaFacade->getStatisticsEntry(); } void EngineWorker::addAndSelectSimulationData(Desc&& dataToUpdate) diff --git a/source/EngineImpl/EngineWorker.h b/source/EngineImpl/EngineWorker.h index 1589281e9..c48e4dd43 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -14,9 +14,7 @@ #include #include #include -#include -#include -#include +#include #include #include #include @@ -59,9 +57,7 @@ class EngineWorker Desc getInspectedSimulationData(std::vector objectsIds); StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); - LineageStatistics getLineageStatistics() const; - OverallStatisticsEntry getOverallStatistics() const; - LineageHistory const& getLineageHistory() const; + StatisticsEntry getOverallStatistics() const; void addAndSelectSimulationData(Desc&& dataToUpdate); void setSimulationData(Desc const& dataToUpdate); diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index 31445d852..e87e1dba7 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -442,14 +442,11 @@ void _SimulationCudaFacade::updateEvolutionStatistics() StatisticsKernelsService::get().updateEvolutionStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); syncAndCheck(); - auto lineageStatistics = _cudaSimulationStatistics->getLineageStatistics(); - auto overallStatistics = _cudaSimulationStatistics->getOverallStatistics(); + auto overallStatistics = _cudaSimulationStatistics->getStatisticsEntry(); { std::lock_guard lock(_mutexForStatistics); - _lineageStatisticsData = lineageStatistics; _overallStatisticsData = overallStatistics; } - StatisticsService::get().addSample(_lineageHistory, lineageStatistics, getCurrentTimestep()); StatisticsService::get().addDataPoint(_statisticsHistory, overallStatistics, getCurrentTimestep()); } @@ -458,31 +455,16 @@ StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const return _statisticsHistory; } -LineageStatistics _SimulationCudaFacade::getLineageStatistics() -{ - std::lock_guard lock(_mutexForStatistics); - if (_lineageStatisticsData) { - return *_lineageStatisticsData; - } else { - return LineageStatistics(); - } -} - -OverallStatisticsEntry _SimulationCudaFacade::getOverallStatistics() +StatisticsEntry _SimulationCudaFacade::getStatisticsEntry() { std::lock_guard lock(_mutexForStatistics); if (_overallStatisticsData) { return *_overallStatisticsData; } else { - return OverallStatisticsEntry(); + return StatisticsEntry(); } } -LineageHistory const& _SimulationCudaFacade::getLineageHistory() const -{ - return _lineageHistory; -} - void _SimulationCudaFacade::setStatisticsHistory(StatisticsHistoryData const& data) { StatisticsService::get().rewriteHistory(_statisticsHistory, data, getCurrentTimestep()); diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index deca4e654..04b4c30f4 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -13,9 +13,7 @@ #include #include #include -#include -#include -#include +#include #include #include #include @@ -88,9 +86,7 @@ public: void updateStatistics(); StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); - LineageStatistics getLineageStatistics(); - OverallStatisticsEntry getOverallStatistics(); - LineageHistory const& getLineageHistory() const; + StatisticsEntry getStatisticsEntry(); uint64_t getCurrentTimestep() const; void setCurrentTimestep(uint64_t timestep); @@ -159,9 +155,7 @@ private: mutable std::mutex _mutexForStatistics; StatisticsHistory _statisticsHistory; - std::optional _lineageStatisticsData; - std::optional _overallStatisticsData; - LineageHistory _lineageHistory; + std::optional _overallStatisticsData; std::shared_ptr _cudaSimulationStatistics; std::shared_ptr _cudaPreviewStatistics; }; diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index 7727c438a..82e043ca4 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -330,21 +330,11 @@ void _SimulationFacadeImpl::setStatisticsHistory(StatisticsHistoryData const& da _worker.setStatisticsHistory(data); } -LineageStatistics _SimulationFacadeImpl::getLineageStatistics() const -{ - return _worker.getLineageStatistics(); -} - -OverallStatisticsEntry _SimulationFacadeImpl::getOverallStatistics() const +StatisticsEntry _SimulationFacadeImpl::getStatisticsEntry() const { return _worker.getOverallStatistics(); } -LineageHistory const& _SimulationFacadeImpl::getLineageHistory() const -{ - return _worker.getLineageHistory(); -} - std::optional _SimulationFacadeImpl::getTpsRestriction() const { auto result = _worker.getTpsRestriction(); diff --git a/source/EngineImpl/SimulationFacadeImpl.h b/source/EngineImpl/SimulationFacadeImpl.h index 29c6ec0a5..9cfc0ccea 100644 --- a/source/EngineImpl/SimulationFacadeImpl.h +++ b/source/EngineImpl/SimulationFacadeImpl.h @@ -90,9 +90,7 @@ class _SimulationFacadeImpl : public _SimulationFacade IntVector2D getWorldSize() const override; StatisticsHistory const& getStatisticsHistory() const override; void setStatisticsHistory(StatisticsHistoryData const& data) override; - LineageStatistics getLineageStatistics() const override; - OverallStatisticsEntry getOverallStatistics() const override; - LineageHistory const& getLineageHistory() const override; + StatisticsEntry getStatisticsEntry() const override; std::optional getTpsRestriction() const override; void setTpsRestriction(std::optional const& value) override; diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index 6a7babaed..c4af0a296 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -1,9 +1,6 @@ #include "StatisticsService.cuh" -#include -#include - #include #include @@ -13,13 +10,14 @@ namespace auto constexpr MaxSamples = 1000; } -void StatisticsService::addDataPoint(StatisticsHistory& history, OverallStatisticsEntry const& overallStatistics, 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(); + _longtermTimestepDelta = DefaultTimeStepDelta; } if (!_lastTimestep || historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta / 100 * (_numDataPoints + 1)) { @@ -112,85 +110,3 @@ void StatisticsService::rewriteHistory(StatisticsHistory& history, StatisticsHis std::lock_guard lock(history.getMutex()); history.getDataRef() = newHistoryData; } - -void StatisticsService::addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, 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(); - _lineageTimestepDelta = DefaultTimeStepDelta; - } - - if (!historyData.empty() && toDouble(timestep) - historyData.back().time < _lineageTimestepDelta) { - return; - } - - LineageSample sample; - sample.time = toDouble(timestep); - auto now = std::chrono::system_clock::now(); - auto unixEpoch = std::chrono::time_point(); - sample.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); - sample.entries = lineageStatistics.entries; - std::sort(sample.entries.begin(), sample.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); - historyData.emplace_back(std::move(sample)); - - if (historyData.size() > MaxSamples) { - LineageHistoryData newData; - newData.reserve(historyData.size() / 2 + 1); - for (size_t i = 0; i < (historyData.size() - 1) / 2; ++i) { - newData.emplace_back(mergeSamples(historyData.at(i * 2), historyData.at(i * 2 + 1))); - } - newData.emplace_back(historyData.back()); - historyData.swap(newData); - - _lineageTimestepDelta *= 2.0; - } -} - -void StatisticsService::reset() -{ - _lineageTimestepDelta = DefaultTimeStepDelta; -} - -LineageSample StatisticsService::mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample) -{ - LineageSample result; - result.time = earlierSample.time; - result.systemClock = (earlierSample.systemClock + laterSample.systemClock) / 2; - result.entries.reserve(earlierSample.entries.size() + laterSample.entries.size()); - - auto mergeEntries = [](LineageStatisticsEntry const& lhs, LineageStatisticsEntry const& rhs) { - LineageStatisticsEntry result; - result.lineageId = lhs.lineageId; - result.colorBitset = lhs.colorBitset | rhs.colorBitset; - result.numCreatures = (lhs.numCreatures + rhs.numCreatures) / 2; - result.numGenomes = (lhs.numGenomes + rhs.numGenomes) / 2; - result.sumCreatureCells = (lhs.sumCreatureCells + rhs.sumCreatureCells) / 2; - result.sumCreatureGenerations = (lhs.sumCreatureGenerations + rhs.sumCreatureGenerations) / 2; - result.sumGenomeNodes = (lhs.sumGenomeNodes + rhs.sumGenomeNodes) / 2; - result.sumMutationRates = (lhs.sumMutationRates + rhs.sumMutationRates) / 2; - result.sumCreatureEnergy = (lhs.sumCreatureEnergy + rhs.sumCreatureEnergy) / 2; - result.numCreatedCreatures = std::max(lhs.numCreatedCreatures, rhs.numCreatedCreatures); - result.totalMutations = std::max(lhs.totalMutations, rhs.totalMutations); - return result; - }; - - auto earlierIter = earlierSample.entries.begin(); - auto laterIter = laterSample.entries.begin(); - while (earlierIter != earlierSample.entries.end() || laterIter != laterSample.entries.end()) { - if (laterIter == laterSample.entries.end() || (earlierIter != earlierSample.entries.end() && earlierIter->lineageId < laterIter->lineageId)) { - result.entries.emplace_back(*earlierIter); - ++earlierIter; - } else if (earlierIter == earlierSample.entries.end() || laterIter->lineageId < earlierIter->lineageId) { - result.entries.emplace_back(*laterIter); - ++laterIter; - } else { - result.entries.emplace_back(mergeEntries(*earlierIter, *laterIter)); - ++earlierIter; - ++laterIter; - } - } - return result; -} diff --git a/source/EngineImpl/StatisticsService.cuh b/source/EngineImpl/StatisticsService.cuh index cb8a5f0af..0d65df8f6 100644 --- a/source/EngineImpl/StatisticsService.cuh +++ b/source/EngineImpl/StatisticsService.cuh @@ -4,9 +4,7 @@ #include -#include -#include -#include +#include #include #include @@ -16,22 +14,15 @@ class StatisticsService MAKE_SINGLETON(StatisticsService); public: - void addDataPoint(StatisticsHistory& history, OverallStatisticsEntry const& overallStatistics, 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); - void addSample(LineageHistory& history, LineageStatistics const& lineageStatistics, uint64_t timestep); - void reset(); - private: - LineageSample mergeSamples(LineageSample const& earlierSample, LineageSample const& laterSample); - static auto constexpr DefaultTimeStepDelta = 10.0; double _longtermTimestepDelta = DefaultTimeStepDelta; int _numDataPoints = 0; std::optional _accumulatedDataPoint; std::optional _lastTimestep; - - double _lineageTimestepDelta = DefaultTimeStepDelta; }; diff --git a/source/EngineInterface/CMakeLists.txt b/source/EngineInterface/CMakeLists.txt index aff034f0e..0bc46b416 100644 --- a/source/EngineInterface/CMakeLists.txt +++ b/source/EngineInterface/CMakeLists.txt @@ -26,15 +26,12 @@ add_library(EngineInterface GeometryBuffers.h Ids.h InspectedEntityIds.h - LineageHistory.cpp - LineageHistory.h - LineageStatistics.h LocationHelper.cpp LocationHelper.h NeuralNetWeight.h NumberGenerator.cpp NumberGenerator.h - OverallStatistics.h + StatisticsEntry.h ParametersEditService.cpp ParametersEditService.h ParametersFilter.h diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index a304a8f52..9e13d9377 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -17,6 +17,7 @@ DataPointCollection DataPointCollection::operator+(DataPointCollection const& ot result.numCellObjects = numCellObjects + other.numCellObjects; result.accumCreatedCreatures = accumCreatedCreatures + other.accumCreatedCreatures; result.accumMutations = accumMutations + other.accumMutations; + result.lineageEntries = other.lineageEntries; // take the later snapshot return result; } @@ -37,5 +38,6 @@ DataPointCollection DataPointCollection::operator/(double divisor) const result.numCellObjects = numCellObjects / divisor; result.accumCreatedCreatures = accumCreatedCreatures / divisor; result.accumMutations = accumMutations / divisor; + result.lineageEntries = lineageEntries; // pass through unchanged return result; } diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index 38f484b8d..68b78d5bd 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -1,5 +1,9 @@ #pragma once +#include + +#include "StatisticsEntry.h" + struct DataPointCollection { double time = 0; @@ -19,6 +23,8 @@ struct DataPointCollection double accumCreatedCreatures = 0; // Raw accumulated value; rates are derived GUI-side double accumMutations = 0; // Raw accumulated value; rates are derived GUI-side + std::vector lineageEntries; // Per-lineage statistics, sorted by lineageId + DataPointCollection operator+(DataPointCollection const& other) const; DataPointCollection operator/(double divisor) const; }; diff --git a/source/EngineInterface/LineageHistory.cpp b/source/EngineInterface/LineageHistory.cpp deleted file mode 100644 index 5896d2356..000000000 --- a/source/EngineInterface/LineageHistory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "LineageHistory.h" - -LineageHistoryData LineageHistory::getCopiedData() const -{ - std::lock_guard lock(_mutex); - auto copy = _data; - return copy; -} - -std::mutex& LineageHistory::getMutex() const -{ - return _mutex; -} - -LineageHistoryData& LineageHistory::getDataRef() -{ - return _data; -} - -LineageHistoryData const& LineageHistory::getDataRef() const -{ - return _data; -} diff --git a/source/EngineInterface/LineageHistory.h b/source/EngineInterface/LineageHistory.h deleted file mode 100644 index 6dcaf39d9..000000000 --- a/source/EngineInterface/LineageHistory.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -#include "LineageStatistics.h" - -struct LineageSample -{ - double time = 0; // Could be a time step or real-time - double systemClock = 0; - std::vector entries; // Sorted by lineageId -}; - -using LineageHistoryData = std::vector; - -class LineageHistory -{ -public: - LineageHistoryData getCopiedData() const; - - std::mutex& getMutex() const; - LineageHistoryData& getDataRef(); - LineageHistoryData const& getDataRef() const; - -private: - mutable std::mutex _mutex; - LineageHistoryData _data; -}; diff --git a/source/EngineInterface/LineageStatistics.h b/source/EngineInterface/LineageStatistics.h deleted file mode 100644 index b404c1ca5..000000000 --- a/source/EngineInterface/LineageStatistics.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include - -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 numCreatedCreatures = 0; // Accumulated, never reset - double totalMutations = 0; // Accumulated, never reset -}; - -struct LineageStatistics -{ - std::vector entries; -}; diff --git a/source/EngineInterface/OverallStatistics.h b/source/EngineInterface/OverallStatistics.h deleted file mode 100644 index 7ae903a54..000000000 --- a/source/EngineInterface/OverallStatistics.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#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 numActiveLineages = 0; - - uint64_t numCreatedCreatures = 0; // Accumulated, never reset - float totalMutations = 0; // Accumulated, never reset -}; diff --git a/source/EngineInterface/SimulationFacade.h b/source/EngineInterface/SimulationFacade.h index bd24b01a8..f47d5c0d9 100644 --- a/source/EngineInterface/SimulationFacade.h +++ b/source/EngineInterface/SimulationFacade.h @@ -4,9 +4,7 @@ #include "DataPointCollection.h" #include "Definitions.h" #include "GeometryBuffers.h" -#include "LineageHistory.h" -#include "LineageStatistics.h" -#include "OverallStatistics.h" +#include "StatisticsEntry.h" #include "PreviewDesc.h" #include "SelectionShallowData.h" #include "SettingsForSimulation.h" @@ -108,9 +106,7 @@ class _SimulationFacade //************ virtual StatisticsHistory const& getStatisticsHistory() const = 0; virtual void setStatisticsHistory(StatisticsHistoryData const& data) = 0; - virtual LineageStatistics getLineageStatistics() const = 0; - virtual OverallStatisticsEntry getOverallStatistics() const = 0; - virtual LineageHistory const& getLineageHistory() const = 0; + virtual StatisticsEntry getStatisticsEntry() const = 0; //******************** //* Preview simulation diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index 0b493efd0..96457eba0 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -5,7 +5,7 @@ #include DataPointCollection StatisticsConverterService::convert( - OverallStatisticsEntry const& overallStatistics, + StatisticsEntry const& overallStatistics, uint64_t timestep, double time) { @@ -16,7 +16,7 @@ DataPointCollection StatisticsConverterService::convert( auto unixEpoch = std::chrono::time_point(); result.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); - auto const& overall = overallStatistics; + auto const& overall = overallStatistics.overallEntry; result.numCreatures = toDouble(overall.numCreatures); result.averageCreatureCells = overall.numCreatures > 0 ? toDouble(overall.sumCreatureCells) / overall.numCreatures : 0.0; result.averageGeneration = overall.numCreatures > 0 ? toDouble(overall.sumCreatureGenerations) / overall.numCreatures : 0.0; @@ -30,5 +30,7 @@ DataPointCollection StatisticsConverterService::convert( result.accumCreatedCreatures = toDouble(overall.numCreatedCreatures); result.accumMutations = toDouble(overall.totalMutations); + result.lineageEntries = overallStatistics.entries; + return result; } diff --git a/source/EngineInterface/StatisticsConverterService.h b/source/EngineInterface/StatisticsConverterService.h index cb7b5c533..4c4847576 100644 --- a/source/EngineInterface/StatisticsConverterService.h +++ b/source/EngineInterface/StatisticsConverterService.h @@ -3,12 +3,12 @@ #include #include -#include +#include class StatisticsConverterService { MAKE_SINGLETON(StatisticsConverterService); public: - DataPointCollection convert(OverallStatisticsEntry const& overallStatistics, uint64_t timestep, double time); + DataPointCollection convert(StatisticsEntry const& overallStatistics, uint64_t timestep, double time); }; diff --git a/source/EngineInterface/StatisticsEntry.h b/source/EngineInterface/StatisticsEntry.h new file mode 100644 index 000000000..a45f80465 --- /dev/null +++ b/source/EngineInterface/StatisticsEntry.h @@ -0,0 +1,45 @@ +#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 numActiveLineages = 0; + + uint64_t 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 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/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 1a9a7ee95..96fcfd7d7 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -1,7 +1,8 @@ #pragma once -#include -#include +#include + +#include #include "Base.cuh" #include "CudaMemoryManager.cuh" @@ -35,7 +36,7 @@ public: __host__ void free() { - CudaMemoryManager::getInstance().freeMemory(_overallStatisticsEntry); + CudaMemoryManager::getInstance().freeMemory(_overallStatisticsEntry); CudaMemoryManager::getInstance().freeMemory(_lineageStatisticsEntries); CudaMemoryManager::getInstance().freeMemory(_lineageMap); @@ -45,22 +46,18 @@ public: CudaMemoryManager::getInstance().freeMemory(_lineageAccumulatorMaps[1]); } - __host__ OverallStatisticsEntry getOverallStatistics() + __host__ StatisticsEntry getStatisticsEntry() { - OverallStatisticsEntry result; - CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result, _overallStatisticsEntry, sizeof(OverallStatisticsEntry), cudaMemcpyDeviceToHost)); - return result; - } + StatisticsEntry result; + CHECK_FOR_DEVICE_ERRORS(cudaMemcpy(&result.overallEntry, _overallStatisticsEntry, sizeof(OverallStatisticsEntry), cudaMemcpyDeviceToHost)); - __host__ LineageStatistics getLineageStatistics() - { auto control = getLineageMapControl(); - LineageStatistics result; 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; } diff --git a/source/EngineTests/CMakeLists.txt b/source/EngineTests/CMakeLists.txt index addf2e698..66884d677 100644 --- a/source/EngineTests/CMakeLists.txt +++ b/source/EngineTests/CMakeLists.txt @@ -38,7 +38,6 @@ PUBLIC IntegrationTestFramework.cpp IntegrationTestFramework.h LayerParameterTests.cpp - LineageHistoryServiceTests.cu MemoryTests.cpp MetaMutationTests.cpp MoveNodeSectionMutationTests.cpp diff --git a/source/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp index c0616e2bc..10892671e 100644 --- a/source/EngineTests/EvolutionStatisticsTests.cpp +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -17,21 +17,20 @@ TEST_F(EvolutionStatisticsTests, basicCounts) _simulationFacade->setSimulationData(data); - auto overall = _simulationFacade->getOverallStatistics(); - EXPECT_EQ(2u, overall.numCreatures); - EXPECT_EQ(2u, overall.numGenomes); - EXPECT_EQ(2u, overall.numActiveLineages); - EXPECT_EQ(3u, overall.numCellObjects); - EXPECT_EQ(0u, overall.numSolidObjects); - EXPECT_EQ(0u, overall.numFluidObjects); - EXPECT_FLOAT_EQ(3.0f, overall.sumCreatureCells); - EXPECT_FLOAT_EQ(8.0f, overall.sumCreatureGenerations); - EXPECT_GT(overall.sumCreatureEnergy, 0.0f); - - auto lineageStatistics = _simulationFacade->getLineageStatistics(); - ASSERT_EQ(2, lineageStatistics.entries.size()); + 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 : lineageStatistics.entries) { + for (auto const& entry : entries.entries) { if (entry.lineageId == lineageId) { return &entry; } @@ -66,10 +65,10 @@ TEST_F(EvolutionStatisticsTests, genomeNodesAndMutationRates) auto data = Desc().addCreature({ObjectDesc().id(1)}, CreatureDesc().lineageId(42), genome); _simulationFacade->setSimulationData(data); - auto overall = _simulationFacade->getOverallStatistics(); - EXPECT_EQ(1u, overall.numGenomes); - EXPECT_FLOAT_EQ(toFloat(numNodes), overall.sumGenomeNodes); - EXPECT_FLOAT_EQ(0.01f, overall.sumMutationRates); //mean over 21 probability values: 0.21 / 21 + 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) @@ -90,12 +89,11 @@ TEST_F(EvolutionStatisticsTests, accumulatedMutations) } _simulationFacade->calcTimesteps(1); - auto overall = _simulationFacade->getOverallStatistics(); - EXPECT_GT(overall.sumAccumulatedMutations, 0.0f); - EXPECT_GT(overall.totalMutations, 0.0f); + auto entries = _simulationFacade->getStatisticsEntry(); + EXPECT_GT(entries.overallEntry.sumAccumulatedMutations, 0.0f); + EXPECT_GT(entries.overallEntry.totalMutations, 0.0f); - auto lineageStatistics = _simulationFacade->getLineageStatistics(); - ASSERT_EQ(1, lineageStatistics.entries.size()); - EXPECT_EQ(42, lineageStatistics.entries.front().lineageId); - EXPECT_GT(lineageStatistics.entries.front().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/LineageHistoryServiceTests.cu b/source/EngineTests/LineageHistoryServiceTests.cu deleted file mode 100644 index f79de001f..000000000 --- a/source/EngineTests/LineageHistoryServiceTests.cu +++ /dev/null @@ -1,158 +0,0 @@ -#include - -#include "StatisticsService.cuh" - -class LineageHistoryServiceTests : public ::testing::Test -{ -protected: - void SetUp() override { StatisticsService::get().reset(); } - - static LineageStatisticsEntry createEntry(uint32_t lineageId) - { - LineageStatisticsEntry result; - result.lineageId = lineageId; - return result; - } - - static LineageStatistics createRawStatistics(std::vector entries) - { - LineageStatistics result; - result.entries = std::move(entries); - return result; - } -}; - -TEST_F(LineageHistoryServiceTests, mergeSamples_disjointIds) -{ - LineageSample earlierSample; - earlierSample.time = 100; - earlierSample.systemClock = 10; - earlierSample.entries = {createEntry(1), createEntry(3)}; - - LineageSample laterSample; - laterSample.time = 200; - laterSample.systemClock = 20; - laterSample.entries = {createEntry(2), createEntry(4)}; - - auto result = StatisticsService::mergeSamples(earlierSample, laterSample); - - EXPECT_EQ(100, result.time); - EXPECT_EQ(15, result.systemClock); - ASSERT_EQ(4, result.entries.size()); - EXPECT_EQ(1, result.entries.at(0).lineageId); - EXPECT_EQ(2, result.entries.at(1).lineageId); - EXPECT_EQ(3, result.entries.at(2).lineageId); - EXPECT_EQ(4, result.entries.at(3).lineageId); -} - -TEST_F(LineageHistoryServiceTests, mergeSamples_commonIds) -{ - auto entry1 = createEntry(7); - entry1.colorBitset = 0x01; - entry1.numCreatures = 10; - entry1.numGenomes = 8; - entry1.sumCreatureCells = 100; - entry1.sumCreatureGenerations = 50; - entry1.sumGenomeNodes = 300; - entry1.sumMutationRates = 4; - entry1.sumCreatureEnergy = 1000; - entry1.numCreatedCreatures = 40; - entry1.totalMutations = 5; - - auto entry2 = createEntry(7); - entry2.colorBitset = 0x02; - entry2.numCreatures = 20; - entry2.numGenomes = 12; - entry2.sumCreatureCells = 200; - entry2.sumCreatureGenerations = 70; - entry2.sumGenomeNodes = 500; - entry2.sumMutationRates = 6; - entry2.sumCreatureEnergy = 3000; - entry2.numCreatedCreatures = 60; - entry2.totalMutations = 9; - - LineageSample earlierSample; - earlierSample.time = 100; - earlierSample.systemClock = 10; - earlierSample.entries = {entry1}; - - LineageSample laterSample; - laterSample.time = 200; - laterSample.systemClock = 20; - laterSample.entries = {entry2}; - - auto result = StatisticsService::mergeSamples(earlierSample, laterSample); - - ASSERT_EQ(1, result.entries.size()); - auto const& merged = result.entries.front(); - EXPECT_EQ(7, merged.lineageId); - EXPECT_EQ(0x03, merged.colorBitset); - EXPECT_EQ(15, merged.numCreatures); - EXPECT_EQ(10, merged.numGenomes); - EXPECT_FLOAT_EQ(150, merged.sumCreatureCells); - EXPECT_FLOAT_EQ(60, merged.sumCreatureGenerations); - EXPECT_FLOAT_EQ(400, merged.sumGenomeNodes); - EXPECT_FLOAT_EQ(5, merged.sumMutationRates); - EXPECT_FLOAT_EQ(2000, merged.sumCreatureEnergy); - EXPECT_EQ(60, merged.numCreatedCreatures); - EXPECT_FLOAT_EQ(9, merged.totalMutations); -} - -TEST_F(LineageHistoryServiceTests, addSample_sortsEntriesById) -{ - LineageHistory history; - - StatisticsService::get().addSample(history, createRawStatistics({createEntry(5), createEntry(1), createEntry(3)}), 100); - - auto data = history.getCopiedData(); - ASSERT_EQ(1, data.size()); - ASSERT_EQ(3, data.front().entries.size()); - EXPECT_EQ(1, data.front().entries.at(0).lineageId); - EXPECT_EQ(3, data.front().entries.at(1).lineageId); - EXPECT_EQ(5, data.front().entries.at(2).lineageId); -} - -TEST_F(LineageHistoryServiceTests, addSample_skipsSamplesBelowCadence) -{ - LineageHistory history; - - StatisticsService::get().addSample(history, createRawStatistics({createEntry(1)}), 100); - StatisticsService::get().addSample(history, createRawStatistics({createEntry(2)}), 105); //below the default cadence of 10 timesteps - StatisticsService::get().addSample(history, createRawStatistics({createEntry(3)}), 120); - - auto data = history.getCopiedData(); - ASSERT_EQ(2, data.size()); - EXPECT_EQ(100, data.at(0).time); - EXPECT_EQ(120, data.at(1).time); -} - -TEST_F(LineageHistoryServiceTests, addSample_clearsHistoryOnTimeRegression) -{ - LineageHistory history; - - StatisticsService::get().addSample(history, createRawStatistics({createEntry(1)}), 1000); - StatisticsService::get().addSample(history, createRawStatistics({createEntry(2)}), 100); - - auto data = history.getCopiedData(); - ASSERT_EQ(1, data.size()); - EXPECT_EQ(100, data.front().time); - ASSERT_EQ(1, data.front().entries.size()); - EXPECT_EQ(2, data.front().entries.front().lineageId); -} - -TEST_F(LineageHistoryServiceTests, addSample_compressesHistory) -{ - LineageHistory history; - - auto timestep = uint64_t(0); - for (int i = 0; i < 1100; ++i) { - StatisticsService::get().addSample(history, createRawStatistics({createEntry(1)}), timestep); - timestep += 10; - } - - auto data = history.getCopiedData(); - EXPECT_LE(data.size(), 1001); - for (size_t i = 1; i < data.size(); ++i) { - EXPECT_LT(data.at(i - 1).time, data.at(i).time); - } -} diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 865892591..d04d86ed7 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -175,11 +175,11 @@ namespace } } - LineageStatisticsEntry const* findLineageEntry(LineageSample const& sample, uint32_t lineageId) + LineageStatisticsEntry const* findLineageEntry(DataPointCollection const& sample, uint32_t lineageId) { - auto it = - std::lower_bound(sample.entries.begin(), sample.entries.end(), lineageId, [](auto const& entry, uint32_t id) { return entry.lineageId < id; }); - if (it != sample.entries.end() && it->lineageId == lineageId) { + auto it = std::lower_bound( + sample.lineageEntries.begin(), sample.lineageEntries.end(), lineageId, [](auto const& entry, uint32_t id) { return entry.lineageId < id; }); + if (it != sample.lineageEntries.end() && it->lineageId == lineageId) { return &*it; } return nullptr; @@ -236,20 +236,9 @@ void EvolutionDashboardWindow::processBackground() _lastTimepoint = timepoint; _timeSinceSimStart += toDouble(duration) / 1000; - auto overallStatistics = _SimulationFacade::get()->getOverallStatistics(); + auto overallStatistics = _SimulationFacade::get()->getStatisticsEntry(); _timelineLiveStatistics.update(overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); - auto rawLineageStatistics = _SimulationFacade::get()->getLineageStatistics(); - LineageSample sample; - sample.time = _timeSinceSimStart; - sample.systemClock = _timeSinceSimStart; - sample.entries = std::move(rawLineageStatistics.entries); - std::sort(sample.entries.begin(), sample.entries.end(), [](auto const& lhs, auto const& rhs) { return lhs.lineageId < rhs.lineageId; }); - _liveLineageHistory.emplace_back(std::move(sample)); - while (!_liveLineageHistory.empty() && _liveLineageHistory.back().time - _liveLineageHistory.front().time > MaxLiveHistory + 1.0) { - _liveLineageHistory.erase(_liveLineageHistory.begin()); - } - _externalEnergySeries.emplace_back(toDouble(_SimulationFacade::get()->getSimulationParameters().externalEnergy.value)); auto maxExternalEnergyValues = toInt(std::lround(MaxLiveHistory * 1000 / LiveStatisticsDeltaTime)); while (toInt(_externalEnergySeries.size()) > maxExternalEnergyValues) { @@ -288,7 +277,8 @@ void EvolutionDashboardWindow::updateCellColors() void EvolutionDashboardWindow::updateDisplayData() { //rebuild only when the underlying data or view settings have changed - auto liveBackTime = !_liveLineageHistory.empty() ? _liveLineageHistory.back().time : -1.0; + auto const& liveHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); + auto liveBackTime = !liveHistory.empty() ? liveHistory.back().time : -1.0; auto historyBackTime = -1.0; if (_timelineMode != TimelineMode_RealTime) { auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); @@ -305,10 +295,10 @@ void EvolutionDashboardWindow::updateDisplayData() //table rows from the latest live sample _lineages.clear(); - if (!_liveLineageHistory.empty()) { - auto const& lastSample = _liveLineageHistory.back(); - auto const* previousSample = _liveLineageHistory.size() >= 2 ? &_liveLineageHistory.at(_liveLineageHistory.size() - 2) : nullptr; - for (auto const& entry : lastSample.entries) { + if (!liveHistory.empty()) { + auto const& lastSample = liveHistory.back(); + auto const* previousSample = liveHistory.size() >= 2 ? &liveHistory.at(liveHistory.size() - 2) : nullptr; + for (auto const& entry : lastSample.lineageEntries) { LineageDisplayData lineage; lineage.id = toInt(entry.lineageId); lineage.colorBitset = toInt(entry.colorBitset); @@ -323,18 +313,18 @@ void EvolutionDashboardWindow::updateDisplayData() //data source for the timeline plots auto useTimeAsClock = _timelineMode == TimelineMode_RealTime; - std::vector globalSource; - LineageHistoryData lineageSource; + StatisticsHistoryData globalSource; + StatisticsHistoryData lineageSource; if (_timelineMode == TimelineMode_RealTime) { globalSource = _timelineLiveStatistics.getDataPointCollectionHistory(); - lineageSource = _liveLineageHistory; + lineageSource = globalSource; } else { auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); { std::lock_guard lock(statisticsHistory.getMutex()); globalSource = statisticsHistory.getDataRef(); } - lineageSource = _SimulationFacade::get()->getLineageHistory().getCopiedData(); + lineageSource = globalSource; if (_timelineMode == TimelineMode_LastSteps && !globalSource.empty()) { auto startTime = globalSource.back().time - toDouble(_lastSteps); std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startTime; }); @@ -375,7 +365,7 @@ void EvolutionDashboardWindow::updateDisplayData() for (auto const& selectedId : _selectedLineageIds) { LineageDisplayData lineage; lineage.id = selectedId; - LineageSample const* lastSampleWithEntry = nullptr; + DataPointCollection const* lastSampleWithEntry = nullptr; LineageStatisticsEntry const* lastEntry = nullptr; for (auto const& sample : lineageSource) { auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index 73fc29e92..bdd52868b 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -10,8 +10,7 @@ #include #include -#include -#include +#include #include "AlienWindow.h" #include "Definitions.h" @@ -99,9 +98,8 @@ class EvolutionDashboardWindow : public AlienWindow }; std::optional _lastRebuildKey; - //live statistics + // Live statistics TimelineLiveStatistics _timelineLiveStatistics; - LineageHistoryData _liveLineageHistory; std::vector _externalEnergySeries; double _timeSinceSimStart = 0; //in seconds std::optional _lastTimepoint; diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index 4c86a9e88..88da19e2c 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -13,7 +13,7 @@ std::vector const& TimelineLiveStatistics::getDataPointColl return _dataPointCollectionHistory; } -void TimelineLiveStatistics::update(OverallStatisticsEntry const& overallStatistics, uint64_t timestep) +void TimelineLiveStatistics::update(StatisticsEntry const& overallStatistics, uint64_t timestep) { truncate(); diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index 02e4fc81f..3fe0d61ad 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -5,7 +5,7 @@ #include #include -#include +#include class TimelineLiveStatistics { @@ -13,7 +13,7 @@ class TimelineLiveStatistics static auto constexpr MaxLiveHistory = 240.0f; //in seconds std::vector const& getDataPointCollectionHistory() const; - void update(OverallStatisticsEntry const& overallStatistics, uint64_t timestep); + void update(StatisticsEntry const& overallStatistics, uint64_t timestep); private: void truncate(); diff --git a/source/PersisterInterface/SerializerService.cpp b/source/PersisterInterface/SerializerService.cpp index bfd63c510..03fff405c 100644 --- a/source/PersisterInterface/SerializerService.cpp +++ b/source/PersisterInterface/SerializerService.cpp @@ -2251,315 +2251,13 @@ void SerializerService::deserializeSimulationParameters(SimulationParameters& pa parameters = SettingsParserService::get().decodeSimulationParameters(tree); } -namespace -{ - std::string toString(double value) - { - std::stringstream ss; - ss << std::fixed << std::setprecision(9) << value; - return ss.str(); - } - - struct ColumnDescription - { - std::string name; - bool colorDependent = false; - }; - 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}, - {"Creature count", false}, - {"Average creature cells", false}, - {"Average genome nodes", false}, - {"Creature energy", false}, - {"Average mutation rate", false}, - {"Average generation", false}, - {"Lineage count", false}, - {"Solid objects", false}, - {"Fluid objects", false}, - {"Cell objects", false}, - {"Accumulated created creatures", false}, - {"Accumulated mutations", 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; - } else if (colIndex == 27) { - return &dataPoints.numCreatures; - } else if (colIndex == 28) { - return &dataPoints.averageCreatureCells; - } else if (colIndex == 29) { - return &dataPoints.averageGenomeNodes; - } else if (colIndex == 30) { - return &dataPoints.creatureEnergy; - } else if (colIndex == 31) { - return &dataPoints.averageMutationRate; - } else if (colIndex == 32) { - return &dataPoints.averageGeneration; - } else if (colIndex == 33) { - return &dataPoints.numLineages; - } else if (colIndex == 34) { - return &dataPoints.numSolidObjects; - } else if (colIndex == 35) { - return &dataPoints.numFluidObjects; - } else if (colIndex == 36) { - return &dataPoints.numCellObjects; - } else if (colIndex == 37) { - return &dataPoints.accumCreatedCreatures; - } else if (colIndex == 38) { - return &dataPoints.accumMutations; - } - THROW_NOT_IMPLEMENTED(); - } - - void load(int startIndex, std::vector& serializedData, double& value) - { - if (startIndex < serializedData.size()) { - value = std::stod(serializedData.at(startIndex)); - } - } - - void save(std::vector& serializedData, double& value) - { - serializedData.emplace_back(toString(value)); - } - - void load(int startIndex, std::vector& serializedData, DataPoint& dataPoint) - { - for (int i = 0; i < MAX_COLORS; ++i) { - auto index = startIndex + i; - if (index < serializedData.size()) { - dataPoint.values[i] = std::stod(serializedData.at(index)); - } - } - if (startIndex + MAX_COLORS < serializedData.size()) { - dataPoint.summedValues = std::stod(serializedData.at(startIndex + MAX_COLORS)); - } - } - - void save(std::vector& serializedData, DataPoint& dataPoint) - { - for (int i = 0; i < MAX_COLORS; ++i) { - serializedData.emplace_back(toString(dataPoint.values[i])); - } - 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) - { - 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)); - } - if (std::holds_alternative(data)) { - load(startIndex, serializedData, *std::get(data)); - } - startIndex += colInfo.size; - } - } - - void save(std::vector& serializedData, DataPointCollection& dataPoints) - { - 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; - } - } - - std::string getPrincipalPart(std::string const& colName) - { - 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; - } - } - - std::optional getColumnIndex(std::string const& colName) - { - for (auto const& [index, colDescription] : ColumnDescriptions | boost::adaptors::indexed(0)) { - if (colDescription.name == colName) { - return toInt(index); - } - } - return std::nullopt; - } -} - void SerializerService::serializeStatistics(StatisticsHistoryData const& statistics, std::ostream& stream) const { - //header row - auto writeLabelAllColors = [&stream](auto const& name) { - for (int i = 0; i < MAX_COLORS; ++i) { - if (i != 0) { - stream << ","; - } - stream << name << " (color " << i << ")"; - } - stream << "," << name << " (accumulated)"; - }; - - int index = 0; - for (auto const& [colName, colorDependent] : ColumnDescriptions) { - if (index != 0) { - stream << ", "; - } - if (!colorDependent) { - stream << colName; - } else { - writeLabelAllColors(colName); - } - ++index; - } - stream << std::endl; - - //content - for (auto dataPoints : statistics) { - std::vector entries; - save(entries, dataPoints); - stream << boost::join(entries, ",") << std::endl; - } } void SerializerService::deserializeStatistics(StatisticsHistoryData& statistics, std::istream& stream) const { statistics.clear(); - - std::vector> data; - - // header line - std::string header; - std::getline(stream, header); - - std::vector colNames; - boost::split(colNames, header, boost::is_any_of(",")); - - std::vector colInfos; - for (auto const& colName : colNames) { - auto principalPart = getPrincipalPart(colName); - - 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); - } - } - } - - // data lines - while (std::getline(stream, header)) { - std::vector entries; - boost::split(entries, header, boost::is_any_of(",")); - - DataPointCollection dataPoints; - load(colInfos, entries, dataPoints); - - statistics.emplace_back(dataPoints); - } } bool SerializerService::wrapGenome(Desc& output, GenomeDesc const& input) const From ae4703f05293a6cd174b65b564871365e0ee7ef1 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 23:08:09 +0200 Subject: [PATCH 20/41] DataPointCollection refactored --- source/EngineImpl/StatisticsService.cu | 6 +- .../EngineInterface/DataPointCollection.cpp | 76 ++++++++++++++++--- source/EngineInterface/DataPointCollection.h | 42 +++++++--- .../StatisticsConverterService.cpp | 41 ++++++---- source/Gui/EvolutionDashboardWindow.cpp | 64 ++++++++-------- 5 files changed, 159 insertions(+), 70 deletions(-) diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index c4af0a296..c6e6fd69f 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -24,7 +24,7 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry auto newDataPoint = [&] { if (!_lastTimestep && !historyData.empty()) { - //reuse last entry if no statistics is available + // Reuse last entry if no statistics is available auto result = historyData.back(); result.time = toDouble(timestep); return result; @@ -43,13 +43,13 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry _numDataPoints = 0; _accumulatedDataPoint.reset(); - //remove last entry if timestep has not changed + // 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 + // Compress history after MaxSamples if (historyData.size() > MaxSamples) { std::vector newData; newData.reserve(historyData.size() / 2); diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index 9e13d9377..1cc409594 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -1,10 +1,8 @@ #include "DataPointCollection.h" -DataPointCollection DataPointCollection::operator+(DataPointCollection const& other) const +OverallDataPoint OverallDataPoint::operator+(OverallDataPoint const& other) const { - DataPointCollection result; - result.time = time + other.time; - result.systemClock = systemClock + other.systemClock; + OverallDataPoint result; result.numCreatures = numCreatures + other.numCreatures; result.averageCreatureCells = averageCreatureCells + other.averageCreatureCells; result.averageGenomeNodes = averageGenomeNodes + other.averageGenomeNodes; @@ -17,15 +15,12 @@ DataPointCollection DataPointCollection::operator+(DataPointCollection const& ot result.numCellObjects = numCellObjects + other.numCellObjects; result.accumCreatedCreatures = accumCreatedCreatures + other.accumCreatedCreatures; result.accumMutations = accumMutations + other.accumMutations; - result.lineageEntries = other.lineageEntries; // take the later snapshot return result; } -DataPointCollection DataPointCollection::operator/(double divisor) const +OverallDataPoint OverallDataPoint::operator/(double divisor) const { - DataPointCollection result; - result.time = time / divisor; - result.systemClock = systemClock / divisor; + OverallDataPoint result; result.numCreatures = numCreatures / divisor; result.averageCreatureCells = averageCreatureCells / divisor; result.averageGenomeNodes = averageGenomeNodes / divisor; @@ -38,6 +33,67 @@ DataPointCollection DataPointCollection::operator/(double divisor) const result.numCellObjects = numCellObjects / divisor; result.accumCreatedCreatures = accumCreatedCreatures / divisor; result.accumMutations = accumMutations / divisor; - result.lineageEntries = lineageEntries; // pass through unchanged + return result; +} + +LineageDataPoint LineageDataPoint::operator+(LineageDataPoint const& other) const +{ + LineageDataPoint result; + result.colorBitset = colorBitset | other.colorBitset; + 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.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; +} + +DataPointCollection DataPointCollection::operator+(DataPointCollection const& other) const +{ + DataPointCollection result; + result.time = time + other.time; + result.systemClock = systemClock + other.systemClock; + result.overall = overall + other.overall; + result.lineages = lineages; + for (auto const& [id, point] : other.lineages) { + auto it = result.lineages.find(id); + if (it != result.lineages.end()) { + it->second = it->second + point; + } else { + result.lineages[id] = point; + } + } + return result; +} + +DataPointCollection DataPointCollection::operator/(double divisor) const +{ + DataPointCollection result; + result.time = time / divisor; + result.systemClock = systemClock / divisor; + result.overall = overall / divisor; + for (auto const& [id, point] : lineages) { + result.lineages[id] = point / divisor; + } return result; } diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index 68b78d5bd..e105c1937 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -1,15 +1,10 @@ #pragma once -#include +#include +#include -#include "StatisticsEntry.h" - -struct DataPointCollection +struct OverallDataPoint { - double time = 0; - double systemClock = 0; - - // Evolution dashboard values double numCreatures = 0; double averageCreatureCells = 0; double averageGenomeNodes = 0; @@ -20,10 +15,39 @@ struct DataPointCollection double numSolidObjects = 0; double numFluidObjects = 0; double numCellObjects = 0; + double accumCreatedCreatures = 0; // Raw accumulated value; rates are derived GUI-side double accumMutations = 0; // Raw accumulated value; rates are derived GUI-side - std::vector lineageEntries; // Per-lineage statistics, sorted by lineageId + OverallDataPoint operator+(OverallDataPoint const& other) const; + OverallDataPoint operator/(double divisor) const; +}; + +struct LineageDataPoint +{ + uint32_t colorBitset = 0; + 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 = 0; + double systemClock = 0; + + OverallDataPoint overall; + std::unordered_map lineages; DataPointCollection operator+(DataPointCollection const& other) const; DataPointCollection operator/(double divisor) const; diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index 96457eba0..477d6ef0e 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -16,21 +16,34 @@ DataPointCollection StatisticsConverterService::convert( auto unixEpoch = std::chrono::time_point(); result.systemClock = toDouble(std::chrono::duration_cast(now - unixEpoch).count()); - auto const& overall = overallStatistics.overallEntry; - result.numCreatures = toDouble(overall.numCreatures); - result.averageCreatureCells = overall.numCreatures > 0 ? toDouble(overall.sumCreatureCells) / overall.numCreatures : 0.0; - result.averageGeneration = overall.numCreatures > 0 ? toDouble(overall.sumCreatureGenerations) / overall.numCreatures : 0.0; - result.averageGenomeNodes = overall.numGenomes > 0 ? toDouble(overall.sumGenomeNodes) / overall.numGenomes : 0.0; - result.averageMutationRate = overall.numGenomes > 0 ? toDouble(overall.sumMutationRates) / overall.numGenomes : 0.0; - result.creatureEnergy = toDouble(overall.sumCreatureEnergy); - result.numLineages = toDouble(overall.numActiveLineages); - result.numSolidObjects = toDouble(overall.numSolidObjects); - result.numFluidObjects = toDouble(overall.numFluidObjects); - result.numCellObjects = toDouble(overall.numCellObjects); - result.accumCreatedCreatures = toDouble(overall.numCreatedCreatures); - result.accumMutations = toDouble(overall.totalMutations); + auto const& o = overallStatistics.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.accumCreatedCreatures = toDouble(o.numCreatedCreatures); + result.overall.accumMutations = toDouble(o.totalMutations); - result.lineageEntries = overallStatistics.entries; + for (auto const& entry : overallStatistics.entries) { + LineageDataPoint point; + point.colorBitset = entry.colorBitset; + 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; + } return result; } diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index d04d86ed7..84381b817 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -122,17 +122,17 @@ namespace { switch (metricIndex) { case 0: - return dataPoints.numCreatures; + return dataPoints.overall.numCreatures; case 1: - return dataPoints.averageCreatureCells; + return dataPoints.overall.averageCreatureCells; case 2: - return dataPoints.averageGenomeNodes; + return dataPoints.overall.averageGenomeNodes; case 3: - return dataPoints.creatureEnergy; + return dataPoints.overall.creatureEnergy; case 4: - return dataPoints.averageMutationRate; + return dataPoints.overall.averageMutationRate; case 5: - return dataPoints.averageGeneration; + return dataPoints.overall.averageGeneration; case 6: case 7: { if (!lastDataPoints) { @@ -141,9 +141,9 @@ namespace auto clock = clockFromTime ? dataPoints.time : dataPoints.systemClock; auto lastClock = clockFromTime ? lastDataPoints->time : lastDataPoints->systemClock; if (metricIndex == 6) { - return calcRate(dataPoints.accumCreatedCreatures, lastDataPoints->accumCreatedCreatures, clock, lastClock); + return calcRate(dataPoints.overall.accumCreatedCreatures, lastDataPoints->overall.accumCreatedCreatures, clock, lastClock); } else { - return calcRate(dataPoints.accumMutations, lastDataPoints->accumMutations, clock, lastClock); + return calcRate(dataPoints.overall.accumMutations, lastDataPoints->overall.accumMutations, clock, lastClock); } } default: @@ -151,38 +151,34 @@ namespace } } - double getLineageMetricValue(LineageStatisticsEntry const& entry, LineageStatisticsEntry const* lastEntry, double clock, double lastClock, int metricIndex) + double getLineageMetricValue(LineageDataPoint const& entry, LineageDataPoint const* lastEntry, double clock, double lastClock, int metricIndex) { switch (metricIndex) { case 0: - return toDouble(entry.numCreatures); + return entry.numCreatures; case 1: - return entry.numCreatures > 0 ? toDouble(entry.sumCreatureCells) / entry.numCreatures : 0.0; + return entry.numCreatures > 0 ? entry.sumCreatureCells / entry.numCreatures : 0.0; case 2: - return entry.numGenomes > 0 ? toDouble(entry.sumGenomeNodes) / entry.numGenomes : 0.0; + return entry.numGenomes > 0 ? entry.sumGenomeNodes / entry.numGenomes : 0.0; case 3: - return toDouble(entry.sumCreatureEnergy); + return entry.sumCreatureEnergy; case 4: - return entry.numGenomes > 0 ? toDouble(entry.sumMutationRates) / entry.numGenomes : 0.0; + return entry.numGenomes > 0 ? entry.sumMutationRates / entry.numGenomes : 0.0; case 5: - return entry.numCreatures > 0 ? toDouble(entry.sumCreatureGenerations) / entry.numCreatures : 0.0; + return entry.numCreatures > 0 ? entry.sumCreatureGenerations / entry.numCreatures : 0.0; case 6: - return lastEntry ? calcRate(toDouble(entry.numCreatedCreatures), toDouble(lastEntry->numCreatedCreatures), clock, lastClock) : 0.0; + return lastEntry ? calcRate(entry.numCreatedCreatures, lastEntry->numCreatedCreatures, clock, lastClock) : 0.0; case 7: - return lastEntry ? calcRate(toDouble(entry.totalMutations), toDouble(lastEntry->totalMutations), clock, lastClock) : 0.0; + return lastEntry ? calcRate(entry.totalMutations, lastEntry->totalMutations, clock, lastClock) : 0.0; default: return 0.0; } } - LineageStatisticsEntry const* findLineageEntry(DataPointCollection const& sample, uint32_t lineageId) + LineageDataPoint const* findLineageEntry(DataPointCollection const& sample, uint32_t lineageId) { - auto it = std::lower_bound( - sample.lineageEntries.begin(), sample.lineageEntries.end(), lineageId, [](auto const& entry, uint32_t id) { return entry.lineageId < id; }); - if (it != sample.lineageEntries.end() && it->lineageId == lineageId) { - return &*it; - } - return nullptr; + auto it = sample.lineages.find(lineageId); + return it != sample.lineages.end() ? &it->second : nullptr; } double calcWindowDeltaPercent(std::vector const& series) @@ -298,11 +294,11 @@ void EvolutionDashboardWindow::updateDisplayData() if (!liveHistory.empty()) { auto const& lastSample = liveHistory.back(); auto const* previousSample = liveHistory.size() >= 2 ? &liveHistory.at(liveHistory.size() - 2) : nullptr; - for (auto const& entry : lastSample.lineageEntries) { + for (auto const& [lineageId, entry] : lastSample.lineages) { LineageDisplayData lineage; - lineage.id = toInt(entry.lineageId); + lineage.id = toInt(lineageId); lineage.colorBitset = toInt(entry.colorBitset); - auto const* previousEntry = previousSample ? findLineageEntry(*previousSample, entry.lineageId) : nullptr; + auto const* previousEntry = previousSample ? findLineageEntry(*previousSample, lineageId) : nullptr; for (int i = 0; i < NumMetrics; ++i) { lineage.currentValues.at(i) = getLineageMetricValue(entry, previousEntry, lastSample.time, previousSample ? previousSample->time : 0.0, i); } @@ -366,7 +362,7 @@ void EvolutionDashboardWindow::updateDisplayData() LineageDisplayData lineage; lineage.id = selectedId; DataPointCollection const* lastSampleWithEntry = nullptr; - LineageStatisticsEntry const* lastEntry = nullptr; + LineageDataPoint const* lastEntry = nullptr; for (auto const& sample : lineageSource) { auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); if (!entry) { @@ -407,10 +403,10 @@ void EvolutionDashboardWindow::updateDisplayData() for (auto const& dataPoints : liveGlobalHistory) { switch (cardIndex) { case 1: - fullSeries.emplace_back(dataPoints.numLineages); + fullSeries.emplace_back(dataPoints.overall.numLineages); break; case 2: - fullSeries.emplace_back(dataPoints.numCreatures); + fullSeries.emplace_back(dataPoints.overall.numCreatures); break; } } @@ -438,7 +434,7 @@ void EvolutionDashboardWindow::processHeader() auto externalEnergy = !_externalEnergySeries.empty() ? _externalEnergySeries.back() : 0.0; processKpiCard("EXTERNAL ENERGY", formatMetricValue(externalEnergy, 0), toFloat(_headerDeltas.at(0)), 0, cardWidth, cardHeight); ImGui::SameLine(); - auto numLineages = lastDataPoints ? lastDataPoints->numLineages : 0.0; + auto numLineages = lastDataPoints ? lastDataPoints->overall.numLineages : 0.0; processKpiCard("LINEAGES", formatMetricValue(numLineages, 0), toFloat(_headerDeltas.at(1)), 1, cardWidth, cardHeight); ImGui::SameLine(); processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), toFloat(_headerDeltas.at(2)), 2, cardWidth, cardHeight); @@ -499,9 +495,9 @@ void EvolutionDashboardWindow::processEntitiesCard(float width, float height) auto total = 0.0; if (!liveGlobalHistory.empty()) { auto const& dataPoints = liveGlobalHistory.back(); - solids = dataPoints.numSolidObjects; - fluids = dataPoints.numFluidObjects; - cells = dataPoints.numCellObjects; + solids = dataPoints.overall.numSolidObjects; + fluids = dataPoints.overall.numFluidObjects; + cells = dataPoints.overall.numCellObjects; total = solids + fluids + cells; } From fd34e458a97fc100c991c23e6cf25b83b0cde139 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 23:12:56 +0200 Subject: [PATCH 21/41] compile fixes --- source/EngineInterface/StatisticsEntry.h | 4 ++-- source/EngineKernels/SimulationStatistics.cuh | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/EngineInterface/StatisticsEntry.h b/source/EngineInterface/StatisticsEntry.h index a45f80465..6f9080700 100644 --- a/source/EngineInterface/StatisticsEntry.h +++ b/source/EngineInterface/StatisticsEntry.h @@ -18,8 +18,8 @@ struct OverallStatisticsEntry uint32_t numCellObjects = 0; uint32_t numActiveLineages = 0; - uint64_t numCreatedCreatures = 0; // Accumulated, never reset - float totalMutations = 0; // Accumulated, never reset + unsigned long long numCreatedCreatures = 0; // Accumulated, never reset + float totalMutations = 0; // Accumulated, never reset }; struct LineageStatisticsEntry diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 96fcfd7d7..6b67bed0c 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -231,10 +231,10 @@ public: __inline__ __device__ void incCreatedCreature(uint32_t lineageId) { - atomicAdd(&_overallStatisticsEntry->numCreatedCreatures, 1u); + atomicAdd(&_overallStatisticsEntry->numCreatedCreatures, 1ull); auto slotIndex = findOrInsertAccumulatorSlot(lineageId); if (slotIndex >= 0) { - atomicAdd(&getActiveAccumulatorMap()[slotIndex].numCreatedCreatures, 1u); + atomicAdd(&getActiveAccumulatorMap()[slotIndex].numCreatedCreatures, 1ull); } } __inline__ __device__ void addMutations(uint32_t lineageId, float value) @@ -264,7 +264,7 @@ private: struct LineageAccumulatorMapEntry { uint32_t lineageId; // LineageIdEmpty = slot is unused - uint64_t numCreatedCreatures; + unsigned long long numCreatedCreatures; double totalMutations; }; struct LineageAccumulatorMapControl From 7752df08eb818d855ab9a6243180afd63f13eee2 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Mon, 13 Jul 2026 23:26:05 +0200 Subject: [PATCH 22/41] Renamings --- source/EngineImpl/SimulationCudaFacade.cu | 12 ++++++------ source/EngineImpl/SimulationCudaFacade.cuh | 2 +- source/EngineImpl/StatisticsKernelsService.cu | 2 +- source/EngineImpl/StatisticsKernelsService.cuh | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index e87e1dba7..000b96c17 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -439,15 +439,15 @@ void _SimulationCudaFacade::updateStatistics() void _SimulationCudaFacade::updateEvolutionStatistics() { - StatisticsKernelsService::get().updateEvolutionStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); + StatisticsKernelsService::get().updateStatistics(_settings.cudaSettings, getSimulationDataPtrCopy(), *_cudaSimulationStatistics); syncAndCheck(); - auto overallStatistics = _cudaSimulationStatistics->getStatisticsEntry(); + auto statisticsEntry = _cudaSimulationStatistics->getStatisticsEntry(); { std::lock_guard lock(_mutexForStatistics); - _overallStatisticsData = overallStatistics; + _statisticsEntry = statisticsEntry; } - StatisticsService::get().addDataPoint(_statisticsHistory, overallStatistics, getCurrentTimestep()); + StatisticsService::get().addDataPoint(_statisticsHistory, statisticsEntry, getCurrentTimestep()); } StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const @@ -458,8 +458,8 @@ StatisticsHistory const& _SimulationCudaFacade::getStatisticsHistory() const StatisticsEntry _SimulationCudaFacade::getStatisticsEntry() { std::lock_guard lock(_mutexForStatistics); - if (_overallStatisticsData) { - return *_overallStatisticsData; + if (_statisticsEntry) { + return *_statisticsEntry; } else { return StatisticsEntry(); } diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index 04b4c30f4..211c193eb 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -155,7 +155,7 @@ private: mutable std::mutex _mutexForStatistics; StatisticsHistory _statisticsHistory; - std::optional _overallStatisticsData; + std::optional _statisticsEntry; std::shared_ptr _cudaSimulationStatistics; std::shared_ptr _cudaPreviewStatistics; }; diff --git a/source/EngineImpl/StatisticsKernelsService.cu b/source/EngineImpl/StatisticsKernelsService.cu index 0e36eac07..e854d7850 100644 --- a/source/EngineImpl/StatisticsKernelsService.cu +++ b/source/EngineImpl/StatisticsKernelsService.cu @@ -6,7 +6,7 @@ void StatisticsKernelsService::init() {} void StatisticsKernelsService::shutdown() {} -void StatisticsKernelsService::updateEvolutionStatistics( +void StatisticsKernelsService::updateStatistics( CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics) diff --git a/source/EngineImpl/StatisticsKernelsService.cuh b/source/EngineImpl/StatisticsKernelsService.cuh index db617f277..9051f3332 100644 --- a/source/EngineImpl/StatisticsKernelsService.cuh +++ b/source/EngineImpl/StatisticsKernelsService.cuh @@ -18,7 +18,7 @@ public: // 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 updateEvolutionStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics); + void updateStatistics(CudaSettings const& gpuSettings, SimulationData const& data, SimulationStatistics const& simulationStatistics); private: StatisticsKernelsService() = default; From 712f9e5524e12594181db621a524656e15d79a43 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Tue, 14 Jul 2026 08:25:34 +0200 Subject: [PATCH 23/41] Remove plot scale switcher and drop lineage data from statistics history Co-Authored-By: Claude Fable 5 --- source/EngineImpl/StatisticsService.cu | 10 +-- source/EngineImpl/StatisticsService.cuh | 2 +- .../EngineInterface/DataPointCollection.cpp | 61 ++++--------------- source/EngineInterface/DataPointCollection.h | 16 +++-- .../StatisticsConverterService.cpp | 7 ++- .../StatisticsConverterService.h | 2 +- source/EngineInterface/StatisticsHistory.h | 2 +- source/Gui/EvolutionDashboardWindow.cpp | 57 +++++++++-------- source/Gui/EvolutionDashboardWindow.h | 12 +--- 9 files changed, 69 insertions(+), 100 deletions(-) diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index c6e6fd69f..546968b8e 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -21,7 +21,7 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry } if (!_lastTimestep || historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta / 100 * (_numDataPoints + 1)) { - auto newDataPoint = [&] { + auto newDataPoint = [&]() -> OverallDataPointCollection { if (!_lastTimestep && !historyData.empty()) { // Reuse last entry if no statistics is available @@ -29,7 +29,7 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry result.time = toDouble(timestep); return result; } else { - return StatisticsConverterService::get().convert(overallStatistics, timestep, toDouble(timestep)); + return StatisticsConverterService::get().convert(overallStatistics, timestep, toDouble(timestep)).toOverallDataPointCollection(); } }(); @@ -51,10 +51,10 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry // Compress history after MaxSamples if (historyData.size() > MaxSamples) { - std::vector newData; + StatisticsHistoryData 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; + auto interpolatedDataPoint = (historyData.at(i * 2) + historyData.at(i * 2 + 1)) / 2.0; interpolatedDataPoint.time = historyData.at(i * 2).time; newData.emplace_back(interpolatedDataPoint); } @@ -84,7 +84,7 @@ void StatisticsService::resetTime(StatisticsHistory& history, uint64_t timestep) _longtermTimestepDelta = DefaultTimeStepDelta; } - std::vector newData; + StatisticsHistoryData newData; newData.reserve(data.size()); for (size_t i = 0; i < data.size(); ++i) { if (data.at(i).time < toDouble(timestep)) { diff --git a/source/EngineImpl/StatisticsService.cuh b/source/EngineImpl/StatisticsService.cuh index 0d65df8f6..efe3a07a7 100644 --- a/source/EngineImpl/StatisticsService.cuh +++ b/source/EngineImpl/StatisticsService.cuh @@ -23,6 +23,6 @@ private: double _longtermTimestepDelta = DefaultTimeStepDelta; int _numDataPoints = 0; - std::optional _accumulatedDataPoint; + std::optional _accumulatedDataPoint; std::optional _lastTimestep; }; diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index 1cc409594..900fd6242 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -36,64 +36,29 @@ OverallDataPoint OverallDataPoint::operator/(double divisor) const return result; } -LineageDataPoint LineageDataPoint::operator+(LineageDataPoint const& other) const +OverallDataPointCollection OverallDataPointCollection::operator+(OverallDataPointCollection const& other) const { - LineageDataPoint result; - result.colorBitset = colorBitset | other.colorBitset; - 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.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; -} - -DataPointCollection DataPointCollection::operator+(DataPointCollection const& other) const -{ - DataPointCollection result; + OverallDataPointCollection result; result.time = time + other.time; result.systemClock = systemClock + other.systemClock; result.overall = overall + other.overall; - result.lineages = lineages; - for (auto const& [id, point] : other.lineages) { - auto it = result.lineages.find(id); - if (it != result.lineages.end()) { - it->second = it->second + point; - } else { - result.lineages[id] = point; - } - } return result; } -DataPointCollection DataPointCollection::operator/(double divisor) const +OverallDataPointCollection OverallDataPointCollection::operator/(double divisor) const { - DataPointCollection result; + OverallDataPointCollection result; result.time = time / divisor; result.systemClock = systemClock / divisor; result.overall = overall / divisor; - for (auto const& [id, point] : lineages) { - result.lineages[id] = point / divisor; - } + return result; +} + +OverallDataPointCollection DataPointCollection::toOverallDataPointCollection() const +{ + OverallDataPointCollection result; + result.time = time; + result.systemClock = systemClock; + result.overall = overall; return result; } diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index e105c1937..6a2902e45 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -36,19 +36,27 @@ struct LineageDataPoint double numCreatedCreatures = 0; double totalMutations = 0; +}; + +struct OverallDataPointCollection +{ + double time = 0; + double systemClock = 0; + + OverallDataPoint overall; - LineageDataPoint operator+(LineageDataPoint const& other) const; - LineageDataPoint operator/(double divisor) const; + OverallDataPointCollection operator+(OverallDataPointCollection const& other) const; + OverallDataPointCollection operator/(double divisor) const; }; struct DataPointCollection { double time = 0; + double timestep = 0; double systemClock = 0; OverallDataPoint overall; std::unordered_map lineages; - DataPointCollection operator+(DataPointCollection const& other) const; - DataPointCollection operator/(double divisor) const; + OverallDataPointCollection toOverallDataPointCollection() const; }; diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index 477d6ef0e..04fac2a8b 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -5,18 +5,19 @@ #include DataPointCollection StatisticsConverterService::convert( - StatisticsEntry const& overallStatistics, + StatisticsEntry const& statisticsEntry, uint64_t timestep, 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()); - auto const& o = overallStatistics.overallEntry; + 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; @@ -30,7 +31,7 @@ DataPointCollection StatisticsConverterService::convert( result.overall.accumCreatedCreatures = toDouble(o.numCreatedCreatures); result.overall.accumMutations = toDouble(o.totalMutations); - for (auto const& entry : overallStatistics.entries) { + for (auto const& entry : statisticsEntry.entries) { LineageDataPoint point; point.colorBitset = entry.colorBitset; point.numCreatures = toDouble(entry.numCreatures); diff --git a/source/EngineInterface/StatisticsConverterService.h b/source/EngineInterface/StatisticsConverterService.h index 4c4847576..36479b5ca 100644 --- a/source/EngineInterface/StatisticsConverterService.h +++ b/source/EngineInterface/StatisticsConverterService.h @@ -10,5 +10,5 @@ class StatisticsConverterService MAKE_SINGLETON(StatisticsConverterService); public: - DataPointCollection convert(StatisticsEntry const& overallStatistics, uint64_t timestep, double time); + DataPointCollection convert(StatisticsEntry const& statisticsEntry, uint64_t timestep, double time); }; diff --git a/source/EngineInterface/StatisticsHistory.h b/source/EngineInterface/StatisticsHistory.h index 030739951..bc172677d 100644 --- a/source/EngineInterface/StatisticsHistory.h +++ b/source/EngineInterface/StatisticsHistory.h @@ -5,7 +5,7 @@ #include "DataPointCollection.h" -using StatisticsHistoryData = std::vector; +using StatisticsHistoryData = std::vector; class StatisticsHistory { diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 84381b817..5b0b0b44e 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -118,7 +118,8 @@ namespace return std::max(0.0, (accumValue - lastAccumValue) / deltaClock); //negative deltas occur after accumulated statistics have been reset } - double getGlobalMetricValue(DataPointCollection const& dataPoints, DataPointCollection const* lastDataPoints, bool clockFromTime, int metricIndex) + double + getGlobalMetricValue(OverallDataPointCollection const& dataPoints, OverallDataPointCollection const* lastDataPoints, bool clockFromTime, int metricIndex) { switch (metricIndex) { case 0: @@ -206,7 +207,6 @@ void EvolutionDashboardWindow::initIntern() { _timelinesHeight = GlobalSettings::get().getValue("windows.evolution dashboard.timelines height", scale(DefaultTimelinesHeight)); _timelineMode = GlobalSettings::get().getValue("windows.evolution dashboard.timeline mode", _timelineMode); - _plotScale = GlobalSettings::get().getValue("windows.evolution dashboard.plot scale", _plotScale); _lastSteps = GlobalSettings::get().getValue("windows.evolution dashboard.last steps", _lastSteps); _colorFilter = GlobalSettings::get().getValue("windows.evolution dashboard.color filter", _colorFilter); validateAndCorrect(); @@ -216,7 +216,6 @@ 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.plot scale", _plotScale); GlobalSettings::get().setValue("windows.evolution dashboard.last steps", _lastSteps); GlobalSettings::get().setValue("windows.evolution dashboard.color filter", _colorFilter); } @@ -244,6 +243,9 @@ void EvolutionDashboardWindow::processBackground() void EvolutionDashboardWindow::processIntern() { + if (!_selectedLineageIds.empty() && _timelineMode == TimelineMode_EntireHistory) { + _timelineMode = TimelineMode_LastSteps; + } updateCellColors(); updateDisplayData(); processHeader(); @@ -307,24 +309,23 @@ void EvolutionDashboardWindow::updateDisplayData() std::sort(_lineages.begin(), _lineages.end(), [](auto const& lhs, auto const& rhs) { return lhs.currentValues.at(0) > rhs.currentValues.at(0); }); } - //data source for the timeline plots + //data source for the "all lineages" timeline plots auto useTimeAsClock = _timelineMode == TimelineMode_RealTime; StatisticsHistoryData globalSource; - StatisticsHistoryData lineageSource; if (_timelineMode == TimelineMode_RealTime) { - globalSource = _timelineLiveStatistics.getDataPointCollectionHistory(); - lineageSource = globalSource; + globalSource.reserve(liveHistory.size()); + for (auto const& sample : liveHistory) { + globalSource.emplace_back(sample.toOverallDataPointCollection()); + } } else { auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); { std::lock_guard lock(statisticsHistory.getMutex()); globalSource = statisticsHistory.getDataRef(); } - lineageSource = globalSource; if (_timelineMode == TimelineMode_LastSteps && !globalSource.empty()) { auto startTime = globalSource.back().time - toDouble(_lastSteps); std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startTime; }); - std::erase_if(lineageSource, [&](auto const& sample) { return sample.time < startTime; }); } } @@ -345,31 +346,39 @@ void EvolutionDashboardWindow::updateDisplayData() } auto const& liveGlobalHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); if (!liveGlobalHistory.empty()) { - auto const& lastDataPoints = liveGlobalHistory.back(); - auto const* previousDataPoints = liveGlobalHistory.size() >= 2 ? &liveGlobalHistory.at(liveGlobalHistory.size() - 2) : nullptr; + auto lastDataPoints = liveGlobalHistory.back().toOverallDataPointCollection(); + std::optional previousDataPoints; + if (liveGlobalHistory.size() >= 2) { + previousDataPoints = liveGlobalHistory.at(liveGlobalHistory.size() - 2).toOverallDataPointCollection(); + } for (int i = 0; i < NumMetrics; ++i) { - _allLineages.currentValues.at(i) = getGlobalMetricValue(lastDataPoints, previousDataPoints, true, i); + _allLineages.currentValues.at(i) = getGlobalMetricValue(lastDataPoints, previousDataPoints ? &*previousDataPoints : nullptr, true, i); } } for (int i = 0; i < NumMetrics; ++i) { _metricWindowDeltas.at(i) = calcWindowDeltaPercent(_allLineages.series.at(i)); } - //per-lineage series for the selected lineages + //per-lineage series for the selected lineages (live statistics are the only source of lineage data) _plottedLineages.clear(); if (!_selectedLineageIds.empty()) { + auto startTimestep = _timelineMode == TimelineMode_LastSteps && !liveHistory.empty() ? liveHistory.back().timestep - toDouble(_lastSteps) + : std::numeric_limits::lowest(); for (auto const& selectedId : _selectedLineageIds) { LineageDisplayData lineage; lineage.id = selectedId; DataPointCollection const* lastSampleWithEntry = nullptr; LineageDataPoint const* lastEntry = nullptr; - for (auto const& sample : lineageSource) { + for (auto const& sample : liveHistory) { + if (sample.timestep < startTimestep) { + continue; + } auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); if (!entry) { continue; } lineage.colorBitset = toInt(entry->colorBitset); - lineage.timePoints.emplace_back(sample.time); + lineage.timePoints.emplace_back(useTimeAsClock ? sample.time : sample.timestep); auto clock = useTimeAsClock ? sample.time : sample.systemClock; auto lastClock = lastSampleWithEntry ? (useTimeAsClock ? lastSampleWithEntry->time : lastSampleWithEntry->systemClock) : 0.0; for (int i = 0; i < NumMetrics; ++i) { @@ -661,10 +670,11 @@ void EvolutionDashboardWindow::processTimelineSection() void EvolutionDashboardWindow::processTimelineHeader() { ImGui::Spacing(); - AlienGui::Switcher( - AlienGui::SwitcherParameters().name("Mode").width(260.0f).textWidth(45.0f).values({"Real-time", "Entire history", "Last X steps"}), &_timelineMode); - ImGui::SameLine(0, scale(20.0f)); - AlienGui::Switcher(AlienGui::SwitcherParameters().name("Scale").width(220.0f).textWidth(45.0f).values({"Linear", "Logarithmic"}), &_plotScale); + std::vector modeValues{"Real-time", "Last X steps"}; + if (_selectedLineageIds.empty()) { + modeValues.emplace_back("Entire history"); + } + AlienGui::Switcher(AlienGui::SwitcherParameters().name("Mode").width(260.0f).textWidth(45.0f).values(modeValues), &_timelineMode); if (_timelineMode == TimelineMode_LastSteps) { ImGui::SameLine(0, scale(20.0f)); if (ImGui::BeginChild("##lastSteps", {scale(200.0f), ImGui::GetFrameHeight()})) { @@ -766,12 +776,6 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim ImPlot::SetupAxis(ImAxis_X1, "", showTimeAxis ? ImPlotAxisFlags_None : ImPlotAxisFlags_NoTickLabels); ImPlot::SetupAxis(ImAxis_Y1, "", ImPlotAxisFlags_NoTickLabels); ImPlot::SetupAxisFormat(ImAxis_Y1, ""); - if (_plotScale == PlotScale_Logarithmic) { - ImPlot::SetupAxisScale( - ImAxis_Y1, - [](double value, void* userData) { return log(value * 1000 + 1.0) / log(2.0); }, - [](double value, void* userData) { return (pow(2.0, value) - 1.0) / 1000; }); - } for (auto const* lineage : plottedLineages) { auto count = toInt(lineage->timePoints.size()); if (count < 2) { @@ -808,8 +812,7 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim void EvolutionDashboardWindow::validateAndCorrect() { _timelinesHeight = std::max(scale(100.0f), _timelinesHeight); - _timelineMode = std::clamp(_timelineMode, static_cast(TimelineMode_RealTime), static_cast(TimelineMode_LastSteps)); - _plotScale = std::clamp(_plotScale, static_cast(PlotScale_Linear), static_cast(PlotScale_Logarithmic)); + _timelineMode = std::clamp(_timelineMode, static_cast(TimelineMode_RealTime), static_cast(TimelineMode_EntireHistory)); _lastSteps = std::clamp(_lastSteps, 1000, 1000000000); _colorFilter &= (1 << MAX_COLORS) - 1; if (_colorFilter == 0) { diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index bdd52868b..91eab004d 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -70,19 +70,11 @@ class EvolutionDashboardWindow : public AlienWindow enum TimelineMode_ { TimelineMode_RealTime, - TimelineMode_EntireHistory, - TimelineMode_LastSteps + TimelineMode_LastSteps, + TimelineMode_EntireHistory //only selectable when all lineages are shown }; TimelineMode _timelineMode = TimelineMode_EntireHistory; - using PlotScale = int; - enum PlotScale_ - { - PlotScale_Linear, - PlotScale_Logarithmic - }; - PlotScale _plotScale = PlotScale_Linear; - int _lastSteps = 100000; float _timelinesHeight = 0; From 0e95590bbabbcfa7be59aa9232b39791c74e48b6 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Tue, 14 Jul 2026 08:48:10 +0200 Subject: [PATCH 24/41] Show plots in time horizont --- source/Base/Resources.h | 2 +- source/Gui/EvolutionDashboardWindow.cpp | 91 +++++++++++++++++-------- source/Gui/EvolutionDashboardWindow.h | 7 +- source/Gui/MainLoopController.cpp | 16 ++--- 4 files changed, 78 insertions(+), 38 deletions(-) diff --git a/source/Base/Resources.h b/source/Base/Resources.h index bb521edaf..df4c25155 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/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 5b0b0b44e..71d61b9dc 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -208,6 +208,7 @@ 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); _colorFilter = GlobalSettings::get().getValue("windows.evolution dashboard.color filter", _colorFilter); validateAndCorrect(); } @@ -217,6 +218,7 @@ 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.color filter", _colorFilter); } @@ -285,7 +287,7 @@ void EvolutionDashboardWindow::updateDisplayData() historyBackTime = statisticsHistory.getDataRef().back().time; } } - RebuildKey key{_timelineMode, _lastSteps, liveBackTime, historyBackTime, _selectedLineageIds}; + RebuildKey key{_timelineMode, _lastSteps, _timeHorizon, liveBackTime, historyBackTime, _selectedLineageIds}; if (_lastRebuildKey && *_lastRebuildKey == key) { return; } @@ -317,6 +319,10 @@ void EvolutionDashboardWindow::updateDisplayData() for (auto const& sample : liveHistory) { globalSource.emplace_back(sample.toOverallDataPointCollection()); } + if (!globalSource.empty()) { + auto startTime = globalSource.back().time - toDouble(_timeHorizon); + std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startTime; }); + } } else { auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); { @@ -355,22 +361,24 @@ void EvolutionDashboardWindow::updateDisplayData() _allLineages.currentValues.at(i) = getGlobalMetricValue(lastDataPoints, previousDataPoints ? &*previousDataPoints : nullptr, true, i); } } - for (int i = 0; i < NumMetrics; ++i) { - _metricWindowDeltas.at(i) = calcWindowDeltaPercent(_allLineages.series.at(i)); - } - //per-lineage series for the selected lineages (live statistics are the only source of lineage data) _plottedLineages.clear(); if (!_selectedLineageIds.empty()) { - auto startTimestep = _timelineMode == TimelineMode_LastSteps && !liveHistory.empty() ? liveHistory.back().timestep - toDouble(_lastSteps) - : std::numeric_limits::lowest(); + auto startClock = std::numeric_limits::lowest(); + if (!liveHistory.empty()) { + if (_timelineMode == TimelineMode_RealTime) { + startClock = liveHistory.back().time - toDouble(_timeHorizon); + } else if (_timelineMode == TimelineMode_LastSteps) { + startClock = liveHistory.back().timestep - toDouble(_lastSteps); + } + } for (auto const& selectedId : _selectedLineageIds) { LineageDisplayData lineage; lineage.id = selectedId; DataPointCollection const* lastSampleWithEntry = nullptr; LineageDataPoint const* lastEntry = nullptr; for (auto const& sample : liveHistory) { - if (sample.timestep < startTimestep) { + if ((useTimeAsClock ? sample.time : sample.timestep) < startClock) { continue; } auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); @@ -675,6 +683,16 @@ void EvolutionDashboardWindow::processTimelineHeader() modeValues.emplace_back("Entire history"); } AlienGui::Switcher(AlienGui::SwitcherParameters().name("Mode").width(260.0f).textWidth(45.0f).values(modeValues), &_timelineMode); + if (_timelineMode == TimelineMode_RealTime) { + ImGui::SameLine(0, scale(20.0f)); + if (ImGui::BeginChild("##timeHorizon", {scale(320.0f), ImGui::GetFrameHeight()})) { + AlienGui::SliderFloat( + AlienGui::SliderFloatParameters().name("Time horizon").min(1.0f).max(TimelineLiveStatistics::MaxLiveHistory).format("%.1f s").textWidth(90.0f), + &_timeHorizon); + validateAndCorrect(); + } + ImGui::EndChild(); + } if (_timelineMode == TimelineMode_LastSteps) { ImGui::SameLine(0, scale(20.0f)); if (ImGui::BeginChild("##lastSteps", {scale(200.0f), ImGui::GetFrameHeight()})) { @@ -705,6 +723,15 @@ void EvolutionDashboardWindow::processTimelineHeader() void EvolutionDashboardWindow::processTimelinePlots() { + std::vector plottedLineages; + if (_selectedLineageIds.empty()) { + plottedLineages.emplace_back(&_allLineages); + } else { + for (auto const& lineage : _plottedLineages) { + plottedLineages.emplace_back(&lineage); + } + } + if (ImGui::BeginTable("##plots", 2, ImGuiTableFlags_None)) { ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, scale(PlotLabelColumnWidth)); ImGui::TableSetupColumn("##plot"); @@ -713,33 +740,37 @@ void EvolutionDashboardWindow::processTimelinePlots() ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); AlienGui::Text(Metrics[i].plotName); - ImGui::PushFont(StyleRepository::get().getMediumBoldFont()); - AlienGui::Text(formatMetricValue(_allLineages.currentValues.at(i), Metrics[i].decimals)); - ImGui::PopFont(); - char deltaText[32]; - snprintf(deltaText, sizeof(deltaText), "%+.1f %%", _metricWindowDeltas.at(i)); - ImGui::PushStyleColor(ImGuiCol_Text, _metricWindowDeltas.at(i) >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); - AlienGui::Text(deltaText); - ImGui::PopStyleColor(); + auto compactLayout = plottedLineages.size() > 1; + for (auto const* lineage : plottedLineages) { + ImGui::PushFont(StyleRepository::get().getMediumBoldFont()); + if (lineage->id != -1) { + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)toImColor(_cellColors.at(getFirstColor(lineage->colorBitset)))); + } + AlienGui::Text(formatMetricValue(lineage->currentValues.at(i), Metrics[i].decimals)); + if (lineage->id != -1) { + ImGui::PopStyleColor(); + } + ImGui::PopFont(); + if (compactLayout) { + ImGui::SameLine(); + } + auto delta = calcWindowDeltaPercent(lineage->series.at(i)); + char deltaText[32]; + snprintf(deltaText, sizeof(deltaText), "%+.1f %%", delta); + ImGui::PushStyleColor(ImGuiCol_Text, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + AlienGui::Text(deltaText); + ImGui::PopStyleColor(); + } ImGui::TableSetColumnIndex(1); - processTimelinePlot(i, showTimeAxis); + processTimelinePlot(plottedLineages, i, showTimeAxis); } ImGui::EndTable(); } } -void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTimeAxis) +void EvolutionDashboardWindow::processTimelinePlot(std::vector const& plottedLineages, int metricIndex, bool showTimeAxis) { - std::vector plottedLineages; - if (_selectedLineageIds.empty()) { - plottedLineages.emplace_back(&_allLineages); - } else { - for (auto const& lineage : _plottedLineages) { - plottedLineages.emplace_back(&lineage); - } - } - auto upperBound = 0.0; auto minTime = std::numeric_limits::max(); auto maxTime = std::numeric_limits::lowest(); @@ -761,6 +792,11 @@ void EvolutionDashboardWindow::processTimelinePlot(int metricIndex, bool showTim ImGui::PopStyleColor(); return; } + if (_timelineMode == TimelineMode_RealTime) { + minTime = maxTime - toDouble(_timeHorizon); + } else if (_timelineMode == TimelineMode_LastSteps) { + minTime = maxTime - toDouble(_lastSteps); + } upperBound *= 1.35; ImGui::PushID(metricIndex); @@ -814,6 +850,7 @@ 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, 1000000000); + _timeHorizon = std::clamp(_timeHorizon, 1.0f, TimelineLiveStatistics::MaxLiveHistory); _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 index 91eab004d..ac8ef537e 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -24,6 +24,8 @@ class EvolutionDashboardWindow : public AlienWindow static auto constexpr NumMetrics = 8; private: + struct LineageDisplayData; + EvolutionDashboardWindow(); void initIntern() override; @@ -41,7 +43,7 @@ class EvolutionDashboardWindow : public AlienWindow void processTimelineSection(); void processTimelineHeader(); void processTimelinePlots(); - void processTimelinePlot(int metricIndex, bool showTimeAxis); + void processTimelinePlot(std::vector const& plottedLineages, int metricIndex, bool showTimeAxis); void validateAndCorrect(); @@ -57,7 +59,6 @@ class EvolutionDashboardWindow : public AlienWindow std::vector _lineages; //table rows from the latest sample, sorted by creature count std::vector _plottedLineages; LineageDisplayData _allLineages; - std::array _metricWindowDeltas = {}; std::array, 3> _headerSparklines = {}; std::array _headerDeltas = {}; @@ -76,12 +77,14 @@ class EvolutionDashboardWindow : public AlienWindow TimelineMode _timelineMode = TimelineMode_EntireHistory; int _lastSteps = 100000; + float _timeHorizon = 10.0f; //in seconds float _timelinesHeight = 0; struct RebuildKey { int timelineMode = 0; int lastSteps = 0; + float timeHorizon = 0; double liveBackTime = 0; double historyBackTime = 0; std::set selectedLineageIds; diff --git a/source/Gui/MainLoopController.cpp b/source/Gui/MainLoopController.cpp index 95cb36b4e..8fbbe23b3 100644 --- a/source/Gui/MainLoopController.cpp +++ b/source/Gui/MainLoopController.cpp @@ -405,6 +405,14 @@ void MainLoopController::processMenubar() .selected(SpatialControlWindow::get().isOn()) .closeMenuWhenItemClicked(false), [&] { SpatialControlWindow::get().setOn(!SpatialControlWindow::get().isOn()); }); + AlienGui::MenuItem( + 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") @@ -419,14 +427,6 @@ void MainLoopController::processMenubar() AlienGui::MenuItem( AlienGui::MenuItemParameters().name("Log").keyAlt(true).key(ImGuiKey_6).selected(LogWindow::get().isOn()).closeMenuWhenItemClicked(false), [&] { LogWindow::get().setOn(!LogWindow::get().isOn()); }); - AlienGui::MenuItem( - AlienGui::MenuItemParameters() - .name("Evolution dashboard") - .keyAlt(true) - .key(ImGuiKey_7) - .selected(EvolutionDashboardWindow::get().isOn()) - .closeMenuWhenItemClicked(false), - [&] { EvolutionDashboardWindow::get().setOn(!EvolutionDashboardWindow::get().isOn()); }); AlienGui::EndMenu(); AlienGui::BeginMenu(" " ICON_FA_PEN_ALT " Editor ", _editorMenuOpened); From 36704ac364ac6f2745371e79b8e21889464cf17a Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Tue, 14 Jul 2026 18:16:03 +0200 Subject: [PATCH 25/41] Labels corrected --- source/Gui/EvolutionDashboardWindow.cpp | 77 ++++++++++++++++++------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 71d61b9dc..822ebaf8b 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -22,6 +22,7 @@ namespace { auto constexpr LiveStatisticsDeltaTime = 50; //in millisec auto constexpr MaxLiveHistory = 240.0; //in seconds + auto constexpr RateAveragingInterval = 30.0; //in seconds auto constexpr NumSparklinePoints = 40; auto constexpr SparklineWidth = 90.0f; @@ -182,6 +183,19 @@ namespace return it != sample.lineages.end() ? &it->second : nullptr; } + //most recent sample that is at least RateAveragingInterval older; falls back to the oldest sample + size_t findRateReferenceIndex(std::vector const& history, double time) + { + size_t result = 0; + for (size_t i = 0; i < history.size(); ++i) { + if (history.at(i).time + RateAveragingInterval > time) { + break; + } + result = i; + } + return result; + } + double calcWindowDeltaPercent(std::vector const& series) { if (series.size() < 2 || std::abs(series.front()) < NEAR_ZERO) { @@ -297,14 +311,22 @@ void EvolutionDashboardWindow::updateDisplayData() _lineages.clear(); if (!liveHistory.empty()) { auto const& lastSample = liveHistory.back(); - auto const* previousSample = liveHistory.size() >= 2 ? &liveHistory.at(liveHistory.size() - 2) : nullptr; + auto referenceIndex = findRateReferenceIndex(liveHistory, lastSample.time); for (auto const& [lineageId, entry] : lastSample.lineages) { LineageDisplayData lineage; lineage.id = toInt(lineageId); lineage.colorBitset = toInt(entry.colorBitset); - auto const* previousEntry = previousSample ? findLineageEntry(*previousSample, lineageId) : nullptr; + LineageDataPoint const* referenceEntry = nullptr; + auto referenceTime = 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; + referenceTime = liveHistory.at(sampleIndex).time; + break; + } + } for (int i = 0; i < NumMetrics; ++i) { - lineage.currentValues.at(i) = getLineageMetricValue(entry, previousEntry, lastSample.time, previousSample ? previousSample->time : 0.0, i); + lineage.currentValues.at(i) = getLineageMetricValue(entry, referenceEntry, lastSample.time, referenceTime, i); } _lineages.emplace_back(std::move(lineage)); } @@ -342,23 +364,33 @@ void EvolutionDashboardWindow::updateDisplayData() for (int i = 0; i < NumMetrics; ++i) { _allLineages.series.at(i).clear(); } + size_t globalReferenceIndex = 0; for (size_t sampleIndex = 0; sampleIndex < globalSource.size(); ++sampleIndex) { auto const& dataPoints = globalSource.at(sampleIndex); - auto const* lastDataPoints = sampleIndex > 0 ? &globalSource.at(sampleIndex - 1) : nullptr; + auto clock = useTimeAsClock ? dataPoints.time : dataPoints.systemClock; + while (globalReferenceIndex + 1 < sampleIndex) { + auto const& nextReference = globalSource.at(globalReferenceIndex + 1); + if ((useTimeAsClock ? nextReference.time : nextReference.systemClock) + RateAveragingInterval > clock) { + break; + } + ++globalReferenceIndex; + } + auto const* referenceDataPoints = sampleIndex > 0 ? &globalSource.at(globalReferenceIndex) : nullptr; _allLineages.timePoints.emplace_back(dataPoints.time); for (int i = 0; i < NumMetrics; ++i) { - _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, lastDataPoints, useTimeAsClock, i)); + _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, referenceDataPoints, useTimeAsClock, i)); } } auto const& liveGlobalHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); if (!liveGlobalHistory.empty()) { auto lastDataPoints = liveGlobalHistory.back().toOverallDataPointCollection(); - std::optional previousDataPoints; + std::optional referenceDataPoints; if (liveGlobalHistory.size() >= 2) { - previousDataPoints = liveGlobalHistory.at(liveGlobalHistory.size() - 2).toOverallDataPointCollection(); + auto referenceIndex = findRateReferenceIndex(liveGlobalHistory, liveGlobalHistory.back().time); + referenceDataPoints = liveGlobalHistory.at(referenceIndex).toOverallDataPointCollection(); } for (int i = 0; i < NumMetrics; ++i) { - _allLineages.currentValues.at(i) = getGlobalMetricValue(lastDataPoints, previousDataPoints ? &*previousDataPoints : nullptr, true, i); + _allLineages.currentValues.at(i) = getGlobalMetricValue(lastDataPoints, referenceDataPoints ? &*referenceDataPoints : nullptr, true, i); } } //per-lineage series for the selected lineages (live statistics are the only source of lineage data) @@ -375,8 +407,8 @@ void EvolutionDashboardWindow::updateDisplayData() for (auto const& selectedId : _selectedLineageIds) { LineageDisplayData lineage; lineage.id = selectedId; - DataPointCollection const* lastSampleWithEntry = nullptr; - LineageDataPoint const* lastEntry = nullptr; + std::vector> rateReferences; //clock and entry of the already visited samples + size_t rateReferenceIndex = 0; for (auto const& sample : liveHistory) { if ((useTimeAsClock ? sample.time : sample.timestep) < startClock) { continue; @@ -388,12 +420,15 @@ void EvolutionDashboardWindow::updateDisplayData() lineage.colorBitset = toInt(entry->colorBitset); lineage.timePoints.emplace_back(useTimeAsClock ? sample.time : sample.timestep); auto clock = useTimeAsClock ? sample.time : sample.systemClock; - auto lastClock = lastSampleWithEntry ? (useTimeAsClock ? lastSampleWithEntry->time : lastSampleWithEntry->systemClock) : 0.0; + while (rateReferenceIndex + 1 < rateReferences.size() && rateReferences.at(rateReferenceIndex + 1).first + RateAveragingInterval <= clock) { + ++rateReferenceIndex; + } + auto const* lastEntry = !rateReferences.empty() ? rateReferences.at(rateReferenceIndex).second : nullptr; + auto lastClock = !rateReferences.empty() ? rateReferences.at(rateReferenceIndex).first : 0.0; for (int i = 0; i < NumMetrics; ++i) { lineage.series.at(i).emplace_back(getLineageMetricValue(*entry, lastEntry, clock, lastClock, i)); } - lastSampleWithEntry = &sample; - lastEntry = entry; + rateReferences.emplace_back(clock, entry); } for (auto const& tableLineage : _lineages) { if (tableLineage.id == selectedId) { @@ -449,12 +484,12 @@ void EvolutionDashboardWindow::processHeader() processEntitiesCard(cardWidth * 2, cardHeight); ImGui::SameLine(); auto externalEnergy = !_externalEnergySeries.empty() ? _externalEnergySeries.back() : 0.0; - processKpiCard("EXTERNAL ENERGY", formatMetricValue(externalEnergy, 0), toFloat(_headerDeltas.at(0)), 0, cardWidth, cardHeight); + processKpiCard("External energy", formatMetricValue(externalEnergy, 0), toFloat(_headerDeltas.at(0)), 0, cardWidth, cardHeight); ImGui::SameLine(); auto numLineages = lastDataPoints ? lastDataPoints->overall.numLineages : 0.0; - processKpiCard("LINEAGES", formatMetricValue(numLineages, 0), toFloat(_headerDeltas.at(1)), 1, cardWidth, cardHeight); + processKpiCard("Lineages", formatMetricValue(numLineages, 0), toFloat(_headerDeltas.at(1)), 1, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("CREATURES", formatMetricValue(_allLineages.currentValues.at(0), 0), toFloat(_headerDeltas.at(2)), 2, cardWidth, cardHeight); + processKpiCard("Creatures", formatMetricValue(_allLineages.currentValues.at(0), 0), toFloat(_headerDeltas.at(2)), 2, cardWidth, cardHeight); } void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width, float height) @@ -523,7 +558,7 @@ void EvolutionDashboardWindow::processEntitiesCard(float width, float height) ImGui::PushStyleColor(ImGuiCol_Border, (ImU32)CardBorderColor); if (ImGui::BeginChild("##cardEntities", {width, height}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); - AlienGui::Text("ENTITIES"); + AlienGui::Text("Entities"); ImGui::PopStyleColor(); ImGui::PushFont(StyleRepository::get().getLargeFont()); @@ -687,7 +722,7 @@ void EvolutionDashboardWindow::processTimelineHeader() ImGui::SameLine(0, scale(20.0f)); if (ImGui::BeginChild("##timeHorizon", {scale(320.0f), ImGui::GetFrameHeight()})) { AlienGui::SliderFloat( - AlienGui::SliderFloatParameters().name("Time horizon").min(1.0f).max(TimelineLiveStatistics::MaxLiveHistory).format("%.1f s").textWidth(90.0f), + AlienGui::SliderFloatParameters().name("Time horizon").min(1.0f).max(TimelineLiveStatistics::MaxLiveHistory).format("%.1f s").textWidth(100.0f), &_timeHorizon); validateAndCorrect(); } @@ -695,15 +730,15 @@ void EvolutionDashboardWindow::processTimelineHeader() } if (_timelineMode == TimelineMode_LastSteps) { ImGui::SameLine(0, scale(20.0f)); - if (ImGui::BeginChild("##lastSteps", {scale(200.0f), ImGui::GetFrameHeight()})) { - AlienGui::InputInt(AlienGui::InputIntParameters().name("Steps").textWidth(45.0f), _lastSteps); + if (ImGui::BeginChild("##lastSteps", {scale(320.0f), ImGui::GetFrameHeight()})) { + AlienGui::SliderInt(AlienGui::SliderIntParameters().name("Steps").min(1000).max(1000000000).logarithmic(true).textWidth(100.0f), &_lastSteps); validateAndCorrect(); } ImGui::EndChild(); } ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); - AlienGui::Text("TIMELINE FILTER"); + AlienGui::Text("Timeline filter"); ImGui::PopStyleColor(); ImGui::SameLine(0, scale(12.0f)); if (_selectedLineageIds.empty()) { From f4b2cc8f819f9118683e4980cba2e32e55ee37e0 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Tue, 14 Jul 2026 18:01:20 +0200 Subject: [PATCH 26/41] Fix bogus growth-rate spikes in Evolution Dashboard header cards Percent/min was extrapolated from whatever short window had accumulated since sim start (or reload), producing huge values right after loading a simulation. Now it waits for a full trailing minute of data and only evaluates that minute; live history is also cleared on sim reset so a reload can't be averaged together with the old run. --- source/Gui/EvolutionDashboardWindow.cpp | 74 +++++++++++++++++++------ source/Gui/EvolutionDashboardWindow.h | 5 +- source/Gui/TimelineLiveStatistics.cpp | 12 +++- source/Gui/TimelineLiveStatistics.h | 4 ++ 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 822ebaf8b..96ef4664a 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -204,12 +204,31 @@ namespace return (series.back() / series.front() - 1.0) * 100; } - double calcDeltaPercentPerMinute(std::vector const& series, double windowInSeconds) + auto constexpr DeltaWindowInSeconds = 60.0; + + //growth rate over the trailing minute, based only on samples actually covering that minute (no extrapolation from shorter windows) + std::optional calcDeltaPercentPerMinute(std::vector const& series, std::vector const& timePoints) { - if (windowInSeconds < NEAR_ZERO) { - return 0.0; + if (series.size() != timePoints.size() || series.size() < 2) { + return std::nullopt; + } + auto currentTime = timePoints.back(); + if (currentTime - timePoints.front() < DeltaWindowInSeconds) { + return std::nullopt; //not enough history yet to evaluate a full minute } - return calcWindowDeltaPercent(series) / (windowInSeconds / 60); + auto targetTime = currentTime - DeltaWindowInSeconds; + auto startIt = std::lower_bound(timePoints.begin(), timePoints.end(), targetTime); + if (startIt == timePoints.end()) { + return std::nullopt; + } + auto startIndex = static_cast(std::distance(timePoints.begin(), startIt)); + auto startValue = series.at(startIndex); + auto elapsed = currentTime - timePoints.at(startIndex); + if (elapsed < NEAR_ZERO || std::abs(startValue) < NEAR_ZERO) { + return std::nullopt; + } + auto percentChange = (series.back() / startValue - 1.0) * 100.0; + return percentChange * (60.0 / elapsed); } } @@ -249,11 +268,17 @@ void EvolutionDashboardWindow::processBackground() auto overallStatistics = _SimulationFacade::get()->getStatisticsEntry(); _timelineLiveStatistics.update(overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); + if (_timelineLiveStatistics.wasReset()) { + _externalEnergySeries.clear(); + _externalEnergyTimeSeries.clear(); + } _externalEnergySeries.emplace_back(toDouble(_SimulationFacade::get()->getSimulationParameters().externalEnergy.value)); + _externalEnergyTimeSeries.emplace_back(_timeSinceSimStart); auto maxExternalEnergyValues = toInt(std::lround(MaxLiveHistory * 1000 / LiveStatisticsDeltaTime)); while (toInt(_externalEnergySeries.size()) > maxExternalEnergyValues) { _externalEnergySeries.erase(_externalEnergySeries.begin()); + _externalEnergyTimeSeries.erase(_externalEnergyTimeSeries.begin()); } } @@ -443,15 +468,17 @@ void EvolutionDashboardWindow::updateDisplayData() } //header sparklines and deltas from the live statistics - auto liveWindowInSeconds = liveGlobalHistory.size() >= 2 ? liveGlobalHistory.back().time - liveGlobalHistory.front().time : 0.0; for (int cardIndex = 0; cardIndex < 3; ++cardIndex) { auto& sparkline = _headerSparklines.at(cardIndex); sparkline.clear(); std::vector fullSeries; + std::vector fullTimePoints; if (cardIndex == 0) { fullSeries = _externalEnergySeries; + fullTimePoints = _externalEnergyTimeSeries; } else { fullSeries.reserve(liveGlobalHistory.size()); + fullTimePoints.reserve(liveGlobalHistory.size()); for (auto const& dataPoints : liveGlobalHistory) { switch (cardIndex) { case 1: @@ -461,9 +488,10 @@ void EvolutionDashboardWindow::updateDisplayData() fullSeries.emplace_back(dataPoints.overall.numCreatures); break; } + fullTimePoints.emplace_back(dataPoints.time); } } - _headerDeltas.at(cardIndex) = calcDeltaPercentPerMinute(fullSeries, liveWindowInSeconds); + _headerDeltas.at(cardIndex) = calcDeltaPercentPerMinute(fullSeries, fullTimePoints); auto stride = std::max(size_t(1), fullSeries.size() / NumSparklinePoints); for (size_t i = 0; i < fullSeries.size(); i += stride) { sparkline.emplace_back(fullSeries.at(i)); @@ -484,15 +512,21 @@ void EvolutionDashboardWindow::processHeader() processEntitiesCard(cardWidth * 2, cardHeight); ImGui::SameLine(); auto externalEnergy = !_externalEnergySeries.empty() ? _externalEnergySeries.back() : 0.0; - processKpiCard("External energy", formatMetricValue(externalEnergy, 0), toFloat(_headerDeltas.at(0)), 0, cardWidth, cardHeight); + processKpiCard("External energy", formatMetricValue(externalEnergy, 0), _headerDeltas.at(0), 0, cardWidth, cardHeight); ImGui::SameLine(); auto numLineages = lastDataPoints ? lastDataPoints->overall.numLineages : 0.0; - processKpiCard("Lineages", formatMetricValue(numLineages, 0), toFloat(_headerDeltas.at(1)), 1, cardWidth, cardHeight); + processKpiCard("Lineages", formatMetricValue(numLineages, 0), _headerDeltas.at(1), 1, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("Creatures", formatMetricValue(_allLineages.currentValues.at(0), 0), toFloat(_headerDeltas.at(2)), 2, cardWidth, cardHeight); + processKpiCard("Creatures", formatMetricValue(_allLineages.currentValues.at(0), 0), _headerDeltas.at(2), 2, cardWidth, cardHeight); } -void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width, float height) +void EvolutionDashboardWindow::processKpiCard( + std::string const& label, + std::string const& value, + std::optional delta, + int sparklineIndex, + float width, + float height) { ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, scale(6.0f)); ImGui::PushStyleColor(ImGuiCol_ChildBg, (ImU32)CardBackgroundColor); @@ -506,14 +540,20 @@ void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::str AlienGui::Text(value); ImGui::PopFont(); - char deltaText[32]; - snprintf(deltaText, sizeof(deltaText), "%+.1f %% / min", delta); - ImGui::PushStyleColor(ImGuiCol_Text, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); - AlienGui::Text(deltaText); - ImGui::PopStyleColor(); + if (delta.has_value()) { + char deltaText[32]; + snprintf(deltaText, sizeof(deltaText), "%+.1f %% / min", *delta); + ImGui::PushStyleColor(ImGuiCol_Text, *delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + AlienGui::Text(deltaText); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); + AlienGui::Text("collecting data ..."); + ImGui::PopStyleColor(); + } auto const& sparkline = _headerSparklines.at(sparklineIndex); - if (sparkline.size() >= 2) { + if (delta.has_value() && sparkline.size() >= 2) { ImGui::SetCursorPos({width - scale(SparklineWidth + 12.0f), height - scale(SparklineHeight + 12.0f)}); ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0, 0, 0, 0)); @@ -524,7 +564,7 @@ void EvolutionDashboardWindow::processKpiCard(std::string const& label, std::str ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); auto [minIt, maxIt] = std::minmax_element(sparkline.begin(), sparkline.end()); ImPlot::SetupAxesLimits(0, toDouble(sparkline.size() - 1), *minIt * 0.9, *maxIt * 1.1 + NEAR_ZERO, ImGuiCond_Always); - ImPlot::PushStyleColor(ImPlotCol_Line, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); + ImPlot::PushStyleColor(ImPlotCol_Line, *delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); ImPlot::PlotLine("##", sparkline.data(), toInt(sparkline.size())); ImPlot::PopStyleColor(); ImPlot::EndPlot(); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index ac8ef537e..cfb3c0dba 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -36,7 +36,7 @@ class EvolutionDashboardWindow : public AlienWindow void updateCellColors(); void updateDisplayData(); void processHeader(); - void processKpiCard(std::string const& label, std::string const& value, float delta, int sparklineIndex, float width, float height); + void processKpiCard(std::string const& label, std::string const& value, std::optional delta, int sparklineIndex, float width, float height); void processEntitiesCard(float width, float height); void processFilterBar(); void processLineageTable(); @@ -60,7 +60,7 @@ class EvolutionDashboardWindow : public AlienWindow std::vector _plottedLineages; LineageDisplayData _allLineages; std::array, 3> _headerSparklines = {}; - std::array _headerDeltas = {}; + std::array, 3> _headerDeltas = {}; std::array _cellColors = {}; @@ -96,6 +96,7 @@ class EvolutionDashboardWindow : public AlienWindow // Live statistics TimelineLiveStatistics _timelineLiveStatistics; std::vector _externalEnergySeries; + std::vector _externalEnergyTimeSeries; double _timeSinceSimStart = 0; //in seconds std::optional _lastTimepoint; }; diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index 88da19e2c..0e470259f 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -13,9 +13,19 @@ std::vector const& TimelineLiveStatistics::getDataPointColl return _dataPointCollectionHistory; } +bool TimelineLiveStatistics::wasReset() const +{ + return _wasReset; +} + void TimelineLiveStatistics::update(StatisticsEntry const& overallStatistics, uint64_t timestep) { - truncate(); + _wasReset = _lastTimestep.has_value() && timestep < *_lastTimestep; + if (_wasReset) { + _dataPointCollectionHistory.clear(); + } else { + truncate(); + } auto timepoint = std::chrono::steady_clock::now(); auto duration = diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index 3fe0d61ad..73a6b5037 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -15,6 +15,9 @@ class TimelineLiveStatistics std::vector const& getDataPointCollectionHistory() const; void update(StatisticsEntry const& overallStatistics, uint64_t timestep); + //true for the update() call in which a new simulation (lower timestep than before) was detected + bool wasReset() const; + private: void truncate(); @@ -24,4 +27,5 @@ class TimelineLiveStatistics std::optional _lastTimestep; std::optional _lastTimepoint; + bool _wasReset = false; }; From 4a1fe2c36034990f2989a0fa784b23884353b045 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Tue, 14 Jul 2026 18:12:18 +0200 Subject: [PATCH 27/41] Use SessionId instead of timestep to detect simulation reload --- source/Gui/EvolutionDashboardWindow.cpp | 10 +++++++--- source/Gui/EvolutionDashboardWindow.h | 1 + source/Gui/TimelineLiveStatistics.cpp | 11 +++-------- source/Gui/TimelineLiveStatistics.h | 5 ++--- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 96ef4664a..9cdff1695 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -266,12 +266,16 @@ void EvolutionDashboardWindow::processBackground() _lastTimepoint = timepoint; _timeSinceSimStart += toDouble(duration) / 1000; - auto overallStatistics = _SimulationFacade::get()->getStatisticsEntry(); - _timelineLiveStatistics.update(overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); - if (_timelineLiveStatistics.wasReset()) { + auto sessionId = _SimulationFacade::get()->getSessionId(); + if (_lastSessionId.has_value() && *_lastSessionId != sessionId) { + _timelineLiveStatistics.clear(); _externalEnergySeries.clear(); _externalEnergyTimeSeries.clear(); } + _lastSessionId = sessionId; + + auto overallStatistics = _SimulationFacade::get()->getStatisticsEntry(); + _timelineLiveStatistics.update(overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); _externalEnergySeries.emplace_back(toDouble(_SimulationFacade::get()->getSimulationParameters().externalEnergy.value)); _externalEnergyTimeSeries.emplace_back(_timeSinceSimStart); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index cfb3c0dba..23e8c3fc4 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -98,5 +98,6 @@ class EvolutionDashboardWindow : public AlienWindow std::vector _externalEnergySeries; std::vector _externalEnergyTimeSeries; double _timeSinceSimStart = 0; //in seconds + std::optional _lastSessionId; std::optional _lastTimepoint; }; diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index 0e470259f..5d7735954 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -13,19 +13,14 @@ std::vector const& TimelineLiveStatistics::getDataPointColl return _dataPointCollectionHistory; } -bool TimelineLiveStatistics::wasReset() const +void TimelineLiveStatistics::clear() { - return _wasReset; + _dataPointCollectionHistory.clear(); } void TimelineLiveStatistics::update(StatisticsEntry const& overallStatistics, uint64_t timestep) { - _wasReset = _lastTimestep.has_value() && timestep < *_lastTimestep; - if (_wasReset) { - _dataPointCollectionHistory.clear(); - } else { - truncate(); - } + truncate(); auto timepoint = std::chrono::steady_clock::now(); auto duration = diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index 73a6b5037..84840d152 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -15,8 +15,8 @@ class TimelineLiveStatistics std::vector const& getDataPointCollectionHistory() const; void update(StatisticsEntry const& overallStatistics, uint64_t timestep); - //true for the update() call in which a new simulation (lower timestep than before) was detected - bool wasReset() const; + //discards the accumulated history, e.g. after a different simulation has been loaded + void clear(); private: void truncate(); @@ -27,5 +27,4 @@ class TimelineLiveStatistics std::optional _lastTimestep; std::optional _lastTimepoint; - bool _wasReset = false; }; From e14fa993bac0a45617ea49e3fd012353c1b88641 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Tue, 14 Jul 2026 22:13:40 +0200 Subject: [PATCH 28/41] Mouseover in plots + minor adaptions --- source/EngineImpl/EngineWorker.cpp | 2 +- source/EngineImpl/EngineWorker.h | 2 +- source/EngineImpl/SimulationFacadeImpl.cpp | 2 +- source/Gui/EvolutionDashboardWindow.cpp | 165 ++++++++++++++++----- source/Gui/EvolutionDashboardWindow.h | 18 ++- 5 files changed, 152 insertions(+), 37 deletions(-) diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index a309cae71..bfb7a0305 100644 --- a/source/EngineImpl/EngineWorker.cpp +++ b/source/EngineImpl/EngineWorker.cpp @@ -109,7 +109,7 @@ void EngineWorker::setStatisticsHistory(StatisticsHistoryData const& data) _simulationCudaFacade->setStatisticsHistory(data); } -StatisticsEntry EngineWorker::getOverallStatistics() const +StatisticsEntry EngineWorker::getStatisticsEntry() const { return _simulationCudaFacade->getStatisticsEntry(); } diff --git a/source/EngineImpl/EngineWorker.h b/source/EngineImpl/EngineWorker.h index c48e4dd43..5fa1bc97e 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -57,7 +57,7 @@ class EngineWorker Desc getInspectedSimulationData(std::vector objectsIds); StatisticsHistory const& getStatisticsHistory() const; void setStatisticsHistory(StatisticsHistoryData const& data); - StatisticsEntry getOverallStatistics() const; + StatisticsEntry getStatisticsEntry() const; void addAndSelectSimulationData(Desc&& dataToUpdate); void setSimulationData(Desc const& dataToUpdate); diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index 82e043ca4..19bfd8e05 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -332,7 +332,7 @@ void _SimulationFacadeImpl::setStatisticsHistory(StatisticsHistoryData const& da StatisticsEntry _SimulationFacadeImpl::getStatisticsEntry() const { - return _worker.getOverallStatistics(); + return _worker.getStatisticsEntry(); } std::optional _SimulationFacadeImpl::getTpsRestriction() const diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 9cdff1695..c33827027 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -3,12 +3,15 @@ #include #include #include +#include #include #include #include #include +#include + #include #include #include @@ -58,9 +61,13 @@ namespace {"Mutations /s", "Mutations / s", 1}, }; - ImColor toImColor(uint32_t rgb, float alpha = 1.0f) + ImColor toImColor(uint32_t rgb, float alpha = 1.0f, float brightness = 1.0f) { - return ImColor(toInt((rgb >> 16) & 0xff), toInt((rgb >> 8) & 0xff), toInt(rgb & 0xff), toInt(alpha * 255.0f)); + return ImColor( + toInt(toFloat((rgb >> 16) & 0xff) * brightness), + toInt(toFloat((rgb >> 8) & 0xff) * brightness), + toInt(toFloat(rgb & 0xff) * brightness), + toInt(alpha * 255.0f)); } std::string formatMetricValue(double value, int decimals) @@ -79,6 +86,16 @@ namespace return StringHelper::format(toFloat(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); @@ -196,18 +213,10 @@ namespace return result; } - double calcWindowDeltaPercent(std::vector const& series) - { - if (series.size() < 2 || std::abs(series.front()) < NEAR_ZERO) { - return 0.0; - } - return (series.back() / series.front() - 1.0) * 100; - } - auto constexpr DeltaWindowInSeconds = 60.0; - //growth rate over the trailing minute, based only on samples actually covering that minute (no extrapolation from shorter windows) - std::optional calcDeltaPercentPerMinute(std::vector const& series, std::vector const& timePoints) + //value and elapsed time of the reference sample ~60s in the past; nullopt if that much history isn't available yet + std::optional> findRateWindow(std::vector const& series, std::vector const& timePoints) { if (series.size() != timePoints.size() || series.size() < 2) { return std::nullopt; @@ -222,13 +231,32 @@ namespace return std::nullopt; } auto startIndex = static_cast(std::distance(timePoints.begin(), startIt)); - auto startValue = series.at(startIndex); auto elapsed = currentTime - timePoints.at(startIndex); - if (elapsed < NEAR_ZERO || std::abs(startValue) < NEAR_ZERO) { + if (elapsed < NEAR_ZERO) { return std::nullopt; } - auto percentChange = (series.back() / startValue - 1.0) * 100.0; - return percentChange * (60.0 / elapsed); + return std::make_pair(series.at(startIndex), elapsed); + } + + //growth rate over the trailing minute, based only on samples actually covering that minute (no extrapolation from shorter windows) + std::optional calcDeltaPercentPerMinute(std::vector const& series, std::vector const& timePoints) + { + auto window = findRateWindow(series, timePoints); + if (!window || std::abs(window->first) < NEAR_ZERO) { + return std::nullopt; //percentage is undefined for a near-zero baseline + } + auto percentChange = (series.back() / window->first - 1.0) * 100.0; + return percentChange * (60.0 / window->second); + } + + //absolute change over the trailing minute; used for metrics that can legitimately be zero, where a percentage is undefined + std::optional calcAbsoluteDeltaPerMinute(std::vector const& series, std::vector const& timePoints) + { + auto window = findRateWindow(series, timePoints); + if (!window) { + return std::nullopt; + } + return (series.back() - window->first) * (60.0 / window->second); } } @@ -390,6 +418,7 @@ void EvolutionDashboardWindow::updateDisplayData() _allLineages.id = -1; _allLineages.colorBitset = 0; _allLineages.timePoints.clear(); + _allLineages.systemClockPoints.clear(); for (int i = 0; i < NumMetrics; ++i) { _allLineages.series.at(i).clear(); } @@ -406,6 +435,9 @@ void EvolutionDashboardWindow::updateDisplayData() } auto const* referenceDataPoints = sampleIndex > 0 ? &globalSource.at(globalReferenceIndex) : nullptr; _allLineages.timePoints.emplace_back(dataPoints.time); + if (!useTimeAsClock) { + _allLineages.systemClockPoints.emplace_back(dataPoints.systemClock); + } for (int i = 0; i < NumMetrics; ++i) { _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, referenceDataPoints, useTimeAsClock, i)); } @@ -448,6 +480,9 @@ void EvolutionDashboardWindow::updateDisplayData() } lineage.colorBitset = toInt(entry->colorBitset); lineage.timePoints.emplace_back(useTimeAsClock ? sample.time : sample.timestep); + if (!useTimeAsClock) { + lineage.systemClockPoints.emplace_back(sample.systemClock); + } auto clock = useTimeAsClock ? sample.time : sample.systemClock; while (rateReferenceIndex + 1 < rateReferences.size() && rateReferences.at(rateReferenceIndex + 1).first + RateAveragingInterval <= clock) { ++rateReferenceIndex; @@ -495,7 +530,8 @@ void EvolutionDashboardWindow::updateDisplayData() fullTimePoints.emplace_back(dataPoints.time); } } - _headerDeltas.at(cardIndex) = calcDeltaPercentPerMinute(fullSeries, fullTimePoints); + _headerDeltas.at(cardIndex) = + cardIndex == 0 ? calcAbsoluteDeltaPerMinute(fullSeries, fullTimePoints) : calcDeltaPercentPerMinute(fullSeries, fullTimePoints); auto stride = std::max(size_t(1), fullSeries.size() / NumSparklinePoints); for (size_t i = 0; i < fullSeries.size(); i += stride) { sparkline.emplace_back(fullSeries.at(i)); @@ -516,18 +552,19 @@ void EvolutionDashboardWindow::processHeader() processEntitiesCard(cardWidth * 2, cardHeight); ImGui::SameLine(); auto externalEnergy = !_externalEnergySeries.empty() ? _externalEnergySeries.back() : 0.0; - processKpiCard("External energy", formatMetricValue(externalEnergy, 0), _headerDeltas.at(0), 0, cardWidth, cardHeight); + processKpiCard("External energy", formatMetricValue(externalEnergy, 0), _headerDeltas.at(0), false, 0, cardWidth, cardHeight); ImGui::SameLine(); auto numLineages = lastDataPoints ? lastDataPoints->overall.numLineages : 0.0; - processKpiCard("Lineages", formatMetricValue(numLineages, 0), _headerDeltas.at(1), 1, cardWidth, cardHeight); + processKpiCard("Lineages", formatMetricValue(numLineages, 0), _headerDeltas.at(1), true, 1, cardWidth, cardHeight); ImGui::SameLine(); - processKpiCard("Creatures", formatMetricValue(_allLineages.currentValues.at(0), 0), _headerDeltas.at(2), 2, cardWidth, cardHeight); + processKpiCard("Creatures", formatMetricValue(_allLineages.currentValues.at(0), 0), _headerDeltas.at(2), true, 2, cardWidth, cardHeight); } void EvolutionDashboardWindow::processKpiCard( std::string const& label, std::string const& value, std::optional delta, + bool isPercentage, int sparklineIndex, float width, float height) @@ -546,7 +583,11 @@ void EvolutionDashboardWindow::processKpiCard( if (delta.has_value()) { char deltaText[32]; - snprintf(deltaText, sizeof(deltaText), "%+.1f %% / min", *delta); + if (isPercentage) { + snprintf(deltaText, sizeof(deltaText), "%+.1f %% / min", *delta); + } else { + snprintf(deltaText, sizeof(deltaText), "%s%s / min", *delta >= 0 ? "+" : "-", formatMetricValue(std::abs(*delta), 0).c_str()); + } ImGui::PushStyleColor(ImGuiCol_Text, *delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); AlienGui::Text(deltaText); ImGui::PopStyleColor(); @@ -646,7 +687,7 @@ void EvolutionDashboardWindow::processFilterBar() _colorFilter ^= 1 << i; } auto active = (_colorFilter & (1 << i)) != 0; - auto color = toImColor(_cellColors.at(i), active ? 1.0f : 0.2f); + 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( @@ -815,11 +856,10 @@ void EvolutionDashboardWindow::processTimelinePlots() ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, scale(PlotLabelColumnWidth)); ImGui::TableSetupColumn("##plot"); for (int i = 0; i < NumMetrics; ++i) { - auto showTimeAxis = i == NumMetrics - 1; + auto showTimeAxis = i == NumMetrics - 1 && _timelineMode == TimelineMode_EntireHistory; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); AlienGui::Text(Metrics[i].plotName); - auto compactLayout = plottedLineages.size() > 1; for (auto const* lineage : plottedLineages) { ImGui::PushFont(StyleRepository::get().getMediumBoldFont()); if (lineage->id != -1) { @@ -830,15 +870,6 @@ void EvolutionDashboardWindow::processTimelinePlots() ImGui::PopStyleColor(); } ImGui::PopFont(); - if (compactLayout) { - ImGui::SameLine(); - } - auto delta = calcWindowDeltaPercent(lineage->series.at(i)); - char deltaText[32]; - snprintf(deltaText, sizeof(deltaText), "%+.1f %%", delta); - ImGui::PushStyleColor(ImGuiCol_Text, delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); - AlienGui::Text(deltaText); - ImGui::PopStyleColor(); } ImGui::TableSetColumnIndex(1); @@ -917,6 +948,17 @@ void EvolutionDashboardWindow::processTimelinePlot(std::vectortimePoints.size() >= 2) { + auto const* lineage = plottedLineages.front(); + drawValuesAtMouseCursor( + lineage->series.at(metricIndex), + lineage->timePoints, + lineage->systemClockPoints, + minTime, + maxTime, + upperBound + NEAR_ZERO, + Metrics[metricIndex].decimals); + } ImPlot::EndPlot(); } ImPlot::PopStyleVar(); @@ -924,6 +966,63 @@ void EvolutionDashboardWindow::processTimelinePlot(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; + } + } + mousePos.y = 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(mousePos.y, fracPartDecimals).c_str()); + } else { + snprintf( + label, + sizeof(label), + "Relative time: %s\nValue: %s", + StringHelper::format(toFloat(mousePos.x), 0).c_str(), + formatMetricValue(mousePos.y, 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); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index 23e8c3fc4..90b2c15e4 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -36,7 +36,14 @@ class EvolutionDashboardWindow : public AlienWindow void updateCellColors(); void updateDisplayData(); void processHeader(); - void processKpiCard(std::string const& label, std::string const& value, std::optional delta, int sparklineIndex, float width, float height); + void processKpiCard( + std::string const& label, + std::string const& value, + std::optional delta, + bool isPercentage, + int sparklineIndex, + float width, + float height); void processEntitiesCard(float width, float height); void processFilterBar(); void processLineageTable(); @@ -44,6 +51,14 @@ class EvolutionDashboardWindow : public AlienWindow void processTimelineHeader(); void processTimelinePlots(); void processTimelinePlot(std::vector const& plottedLineages, int metricIndex, bool showTimeAxis); + void drawValuesAtMouseCursor( + std::vector const& series, + std::vector const& timePoints, + std::vector const& systemClockPoints, + double startTime, + double endTime, + double upperBound, + int fracPartDecimals); void validateAndCorrect(); @@ -54,6 +69,7 @@ class EvolutionDashboardWindow : public AlienWindow 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 creature count From 3ec3dc919493e2267418231dde4160c00e5b8fa5 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 15 Jul 2026 08:08:20 +0200 Subject: [PATCH 29/41] Crash in timeline header fixed --- source/Gui/EvolutionDashboardWindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index c33827027..3d3c9a958 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -802,6 +802,7 @@ void EvolutionDashboardWindow::processTimelineHeader() if (_selectedLineageIds.empty()) { modeValues.emplace_back("Entire history"); } + _timelineMode = std::min(toInt(modeValues.size()) - 1, _timelineMode); AlienGui::Switcher(AlienGui::SwitcherParameters().name("Mode").width(260.0f).textWidth(45.0f).values(modeValues), &_timelineMode); if (_timelineMode == TimelineMode_RealTime) { ImGui::SameLine(0, scale(20.0f)); From 71bc767951bfe9caffd83fb4888f947b2e6571b7 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 15 Jul 2026 12:24:56 +0200 Subject: [PATCH 30/41] Fix for TimelineMode_LastSteps --- source/Gui/EvolutionDashboardWindow.cpp | 32 ++++++++++++++++--------- source/Gui/TimelineLiveStatistics.cpp | 17 ++++++++++++- source/Gui/TimelineLiveStatistics.h | 1 + 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 3d3c9a958..617c83220 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -351,7 +351,7 @@ void EvolutionDashboardWindow::updateDisplayData() auto const& liveHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); auto liveBackTime = !liveHistory.empty() ? liveHistory.back().time : -1.0; auto historyBackTime = -1.0; - if (_timelineMode != TimelineMode_RealTime) { + if (_timelineMode == TimelineMode_EntireHistory) { auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); std::lock_guard lock(statisticsHistory.getMutex()); if (!statisticsHistory.getDataRef().empty()) { @@ -402,16 +402,24 @@ void EvolutionDashboardWindow::updateDisplayData() auto startTime = globalSource.back().time - toDouble(_timeHorizon); std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startTime; }); } - } else { - auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); - { - std::lock_guard lock(statisticsHistory.getMutex()); - globalSource = statisticsHistory.getDataRef(); + } else if (_timelineMode == TimelineMode_LastSteps) { + //fed from the same fine-grained live buffer as real-time mode (not the coarse, entire-run history), + //so the plot has enough resolution over the requested step window; the timestep is carried in the + //(otherwise unused for this mode) time field, mirroring how the entire-run history encodes it + globalSource.reserve(liveHistory.size()); + for (auto const& sample : liveHistory) { + auto dataPoints = sample.toOverallDataPointCollection(); + dataPoints.time = sample.timestep; + globalSource.emplace_back(dataPoints); } - if (_timelineMode == TimelineMode_LastSteps && !globalSource.empty()) { - auto startTime = globalSource.back().time - toDouble(_lastSteps); - std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startTime; }); + if (!globalSource.empty()) { + auto startStep = globalSource.back().time - toDouble(_lastSteps); + std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startStep; }); } + } else { + auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); + std::lock_guard lock(statisticsHistory.getMutex()); + globalSource = statisticsHistory.getDataRef(); } //"all lineages" series @@ -817,7 +825,9 @@ void EvolutionDashboardWindow::processTimelineHeader() if (_timelineMode == TimelineMode_LastSteps) { ImGui::SameLine(0, scale(20.0f)); if (ImGui::BeginChild("##lastSteps", {scale(320.0f), ImGui::GetFrameHeight()})) { - AlienGui::SliderInt(AlienGui::SliderIntParameters().name("Steps").min(1000).max(1000000000).logarithmic(true).textWidth(100.0f), &_lastSteps); + AlienGui::SliderInt( + AlienGui::SliderIntParameters().name("Steps").min(1000).max(TimelineLiveStatistics::MaxLiveSteps).logarithmic(true).textWidth(100.0f), + &_lastSteps); validateAndCorrect(); } ImGui::EndChild(); @@ -1028,7 +1038,7 @@ 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, 1000000000); + _lastSteps = std::clamp(_lastSteps, 1000, TimelineLiveStatistics::MaxLiveSteps); _timeHorizon = std::clamp(_timeHorizon, 1.0f, TimelineLiveStatistics::MaxLiveHistory); _colorFilter &= (1 << MAX_COLORS) - 1; if (_colorFilter == 0) { diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index 5d7735954..d18a3fed1 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -8,6 +8,11 @@ #include +namespace +{ + auto constexpr MaxSampleCount = 20000; //hard cap to bound memory when the simulation stalls +} + std::vector const& TimelineLiveStatistics::getDataPointCollectionHistory() const { return _dataPointCollectionHistory; @@ -36,7 +41,17 @@ void TimelineLiveStatistics::update(StatisticsEntry const& overallStatistics, ui 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 X 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 84840d152..669341611 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -11,6 +11,7 @@ class TimelineLiveStatistics { public: static auto constexpr MaxLiveHistory = 240.0f; //in seconds + static auto constexpr MaxLiveSteps = 100000; //max span retained for "Last X steps" mode std::vector const& getDataPointCollectionHistory() const; void update(StatisticsEntry const& overallStatistics, uint64_t timestep); From 26f08875632a7d9e0319214bf9075eaa5935586f Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 15 Jul 2026 17:29:45 +0200 Subject: [PATCH 31/41] Improve dashboard visualization, part 1 --- source/Base/StringHelper.cpp | 7 +- source/Base/StringHelper.h | 1 + source/EngineImpl/StatisticsService.cu | 6 +- source/EngineImpl/StatisticsService.cuh | 2 +- .../EngineInterface/DataPointCollection.cpp | 66 +- source/EngineInterface/DataPointCollection.h | 16 +- .../StatisticsConverterService.cpp | 1 + source/EngineInterface/StatisticsEntry.h | 1 + source/EngineInterface/StatisticsHistory.h | 2 +- source/EngineKernels/SimulationStatistics.cuh | 2 + source/EngineKernels/StatisticsKernels.cu | 9 + source/Gui/EvolutionDashboardWindow.cpp | 609 +++++++----------- source/Gui/EvolutionDashboardWindow.h | 22 +- 13 files changed, 325 insertions(+), 419 deletions(-) diff --git a/source/Base/StringHelper.cpp b/source/Base/StringHelper.cpp index 410a18e62..64dd14f3c 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 += "."; diff --git a/source/Base/StringHelper.h b/source/Base/StringHelper.h index d1ca9fd24..4f06ccd94 100644 --- a/source/Base/StringHelper.h +++ b/source/Base/StringHelper.h @@ -10,6 +10,7 @@ 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); diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index 546968b8e..5224dee64 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -21,15 +21,16 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry } if (!_lastTimestep || historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta / 100 * (_numDataPoints + 1)) { - auto newDataPoint = [&]() -> OverallDataPointCollection { + auto newDataPoint = [&]() -> DataPointCollection { if (!_lastTimestep && !historyData.empty()) { // Reuse last entry if no statistics is available auto result = historyData.back(); result.time = toDouble(timestep); + result.timestep = toDouble(timestep); return result; } else { - return StatisticsConverterService::get().convert(overallStatistics, timestep, toDouble(timestep)).toOverallDataPointCollection(); + return StatisticsConverterService::get().convert(overallStatistics, timestep, toDouble(timestep)); } }(); @@ -56,6 +57,7 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry for (size_t i = 0; i < (historyData.size() - 1) / 2; ++i) { auto interpolatedDataPoint = (historyData.at(i * 2) + historyData.at(i * 2 + 1)) / 2.0; interpolatedDataPoint.time = historyData.at(i * 2).time; + interpolatedDataPoint.timestep = historyData.at(i * 2).timestep; newData.emplace_back(interpolatedDataPoint); } newData.emplace_back(historyData.back()); diff --git a/source/EngineImpl/StatisticsService.cuh b/source/EngineImpl/StatisticsService.cuh index efe3a07a7..0d65df8f6 100644 --- a/source/EngineImpl/StatisticsService.cuh +++ b/source/EngineImpl/StatisticsService.cuh @@ -23,6 +23,6 @@ private: double _longtermTimestepDelta = DefaultTimeStepDelta; int _numDataPoints = 0; - std::optional _accumulatedDataPoint; + std::optional _accumulatedDataPoint; std::optional _lastTimestep; }; diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index 900fd6242..b0d659c55 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -13,6 +13,7 @@ OverallDataPoint OverallDataPoint::operator+(OverallDataPoint const& other) cons 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; @@ -31,34 +32,73 @@ OverallDataPoint OverallDataPoint::operator/(double divisor) const 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; } -OverallDataPointCollection OverallDataPointCollection::operator+(OverallDataPointCollection const& other) const +LineageDataPoint LineageDataPoint::operator+(LineageDataPoint const& other) const { - OverallDataPointCollection result; + LineageDataPoint result; + result.colorBitset = colorBitset | other.colorBitset; + 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.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; +} + +DataPointCollection DataPointCollection::operator+(DataPointCollection const& other) const +{ + DataPointCollection result; result.time = time + other.time; + result.timestep = timestep + other.timestep; result.systemClock = systemClock + other.systemClock; 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; } -OverallDataPointCollection OverallDataPointCollection::operator/(double divisor) const +DataPointCollection DataPointCollection::operator/(double divisor) const { - OverallDataPointCollection result; + DataPointCollection result; result.time = time / divisor; + result.timestep = timestep / divisor; result.systemClock = systemClock / divisor; result.overall = overall / divisor; - return result; -} - -OverallDataPointCollection DataPointCollection::toOverallDataPointCollection() const -{ - OverallDataPointCollection result; - result.time = time; - result.systemClock = systemClock; - result.overall = overall; + 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 6a2902e45..99ee49971 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -15,6 +15,7 @@ struct OverallDataPoint double numSolidObjects = 0; double numFluidObjects = 0; double numCellObjects = 0; + double numEnergyParticles = 0; double accumCreatedCreatures = 0; // Raw accumulated value; rates are derived GUI-side double accumMutations = 0; // Raw accumulated value; rates are derived GUI-side @@ -36,17 +37,9 @@ struct LineageDataPoint double numCreatedCreatures = 0; double totalMutations = 0; -}; - -struct OverallDataPointCollection -{ - double time = 0; - double systemClock = 0; - - OverallDataPoint overall; - OverallDataPointCollection operator+(OverallDataPointCollection const& other) const; - OverallDataPointCollection operator/(double divisor) const; + LineageDataPoint operator+(LineageDataPoint const& other) const; + LineageDataPoint operator/(double divisor) const; }; struct DataPointCollection @@ -58,5 +51,6 @@ struct DataPointCollection OverallDataPoint overall; std::unordered_map lineages; - OverallDataPointCollection toOverallDataPointCollection() const; + DataPointCollection operator+(DataPointCollection const& other) const; + DataPointCollection operator/(double divisor) const; }; diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index 04fac2a8b..b21d70974 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -28,6 +28,7 @@ DataPointCollection StatisticsConverterService::convert( 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); diff --git a/source/EngineInterface/StatisticsEntry.h b/source/EngineInterface/StatisticsEntry.h index 6f9080700..11c75ea3e 100644 --- a/source/EngineInterface/StatisticsEntry.h +++ b/source/EngineInterface/StatisticsEntry.h @@ -16,6 +16,7 @@ struct OverallStatisticsEntry 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 diff --git a/source/EngineInterface/StatisticsHistory.h b/source/EngineInterface/StatisticsHistory.h index bc172677d..030739951 100644 --- a/source/EngineInterface/StatisticsHistory.h +++ b/source/EngineInterface/StatisticsHistory.h @@ -5,7 +5,7 @@ #include "DataPointCollection.h" -using StatisticsHistoryData = std::vector; +using StatisticsHistoryData = std::vector; class StatisticsHistory { diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 6b67bed0c..76243d231 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -83,11 +83,13 @@ public: 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; diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index 26be1b4cd..c4ca58558 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -68,6 +68,15 @@ __global__ void cudaUpdateEvolutionStatistics_substep1(SimulationData data, Simu __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) { + if (particles.at(index)) { + statistics.incNumEnergyParticles(); + } + } + } auto& objects = data.entities.objects; auto const partition = calcSystemThreadPartition(objects.getNumEntries()); diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 617c83220..de77e0bcd 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -24,12 +24,8 @@ namespace { auto constexpr LiveStatisticsDeltaTime = 50; //in millisec - auto constexpr MaxLiveHistory = 240.0; //in seconds auto constexpr RateAveragingInterval = 30.0; //in seconds - auto constexpr NumSparklinePoints = 40; - auto constexpr SparklineWidth = 90.0f; - auto constexpr SparklineHeight = 26.0f; auto constexpr ColorChipSize = 22.0f; auto constexpr SwatchSize = 11.0f; auto constexpr SwatchGap = 3.0f; @@ -40,25 +36,25 @@ namespace ImColor const CardBackgroundColor = ImColor(0.095f, 0.117f, 0.165f, 1.0f); ImColor const CardBorderColor = ImColor(0.165f, 0.196f, 0.270f, 1.0f); - ImColor const PositiveDeltaColor = ImColor(0.19f, 0.77f, 0.55f, 1.0f); - ImColor const NegativeDeltaColor = ImColor(0.94f, 0.32f, 0.32f, 1.0f); - ImColor const SumSeriesColor = ImColor(0.78f, 0.78f, 0.78f, 1.0f); + struct MetricDef { char const* tableHeader; char const* plotName; - int decimals; //-1: scientific notation + int tableDecimals; + int plotDecimals; }; + //the last two metrics are rates: per second in the table, per 1K time steps in the timeline plots MetricDef const Metrics[EvolutionDashboardWindow::NumMetrics] = { - {"Creatures", "Creatures", 0}, - {"Avg cells", "Avg cells", 1}, - {"Avg nodes", "Avg nodes", 1}, - {"Sum energy", "Sum energy", 0}, - {"Avg mut. rate", "Avg mutation rate", -1}, - {"Avg age (gen)", "Avg age (generations)", 0}, - {"Created /s", "Created creatures / s", 2}, - {"Mutations /s", "Mutations / s", 1}, + {"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 age (gen)", "Avg age (generations)", 0, 0}, + {"Created /s", "Created creatures / 1K steps", 2, 2}, + {"Mutations /s", "Mutations / 1K steps", 4, 4}, }; ImColor toImColor(uint32_t rgb, float alpha = 1.0f, float brightness = 1.0f) @@ -70,20 +66,20 @@ namespace 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 + ImColor getPlotColor(int metricIndex, int seriesIndex, int numSeries) + { + auto index = numSeries > 1 ? seriesIndex : metricIndex; + return ImColor(ImPlot::GetColormapColor((index % 21) <= 10 ? (index % 21) : 20 - (index % 21), ImPlotColormap_Cool)); + } + std::string formatMetricValue(double value, int decimals) { - if (decimals == -1) { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%.1e", value); - return buffer; - } - if (value >= 1e6) { - return StringHelper::format(toFloat(value / 1e6), 2) + " M"; + if (std::isinf(value)) { + return "infinity"; } - if (value >= 1e4) { - return StringHelper::format(toFloat(value / 1e3), 0) + " K"; - } - return StringHelper::format(toFloat(value), decimals); + return StringHelper::format(value, decimals); } std::string convertSystemClockToString(double systemClock) @@ -102,16 +98,6 @@ namespace ImGui::TextUnformatted(text.c_str()); } - int getFirstColor(int colorBitset) - { - for (int i = 0; i < MAX_COLORS; ++i) { - if (colorBitset & (1 << i)) { - return i; - } - } - return 0; - } - float drawColorSwatches(ImDrawList* drawList, ImVec2 const& pos, int colorBitset, float lineHeight, std::array const& cellColors) { auto swatchSize = scale(SwatchSize); @@ -127,17 +113,15 @@ namespace return x - pos.x; } - double calcRate(double accumValue, double lastAccumValue, double clock, double lastClock) + double calcRate(double accumValue, double lastAccumValue, double delta) { - auto deltaClock = clock - lastClock; - if (deltaClock <= 0) { + if (delta <= 0) { return 0.0; } - return std::max(0.0, (accumValue - lastAccumValue) / deltaClock); //negative deltas occur after accumulated statistics have been reset + return std::max(0.0, (accumValue - lastAccumValue) / delta); //negative deltas occur after accumulated statistics have been reset } - double - getGlobalMetricValue(OverallDataPointCollection const& dataPoints, OverallDataPointCollection const* lastDataPoints, bool clockFromTime, int metricIndex) + double getGlobalMetricValue(DataPointCollection const& dataPoints, DataPointCollection const* lastDataPoints, bool ratePerKSteps, int metricIndex) { switch (metricIndex) { case 0: @@ -157,12 +141,11 @@ namespace if (!lastDataPoints) { return 0.0; } - auto clock = clockFromTime ? dataPoints.time : dataPoints.systemClock; - auto lastClock = clockFromTime ? lastDataPoints->time : lastDataPoints->systemClock; + auto delta = ratePerKSteps ? (dataPoints.timestep - lastDataPoints->timestep) / 1000.0 : dataPoints.time - lastDataPoints->time; if (metricIndex == 6) { - return calcRate(dataPoints.overall.accumCreatedCreatures, lastDataPoints->overall.accumCreatedCreatures, clock, lastClock); + return calcRate(dataPoints.overall.accumCreatedCreatures, lastDataPoints->overall.accumCreatedCreatures, delta); } else { - return calcRate(dataPoints.overall.accumMutations, lastDataPoints->overall.accumMutations, clock, lastClock); + return calcRate(dataPoints.overall.accumMutations, lastDataPoints->overall.accumMutations, delta); } } default: @@ -170,7 +153,7 @@ namespace } } - double getLineageMetricValue(LineageDataPoint const& entry, LineageDataPoint const* lastEntry, double clock, double lastClock, int metricIndex) + double getLineageMetricValue(LineageDataPoint const& entry, LineageDataPoint const* lastEntry, double rateDelta, int metricIndex) { switch (metricIndex) { case 0: @@ -186,9 +169,9 @@ namespace case 5: return entry.numCreatures > 0 ? entry.sumCreatureGenerations / entry.numCreatures : 0.0; case 6: - return lastEntry ? calcRate(entry.numCreatedCreatures, lastEntry->numCreatedCreatures, clock, lastClock) : 0.0; + return lastEntry ? calcRate(entry.numCreatedCreatures, lastEntry->numCreatedCreatures, rateDelta) : 0.0; case 7: - return lastEntry ? calcRate(entry.totalMutations, lastEntry->totalMutations, clock, lastClock) : 0.0; + return lastEntry ? calcRate(entry.totalMutations, lastEntry->totalMutations, rateDelta) : 0.0; default: return 0.0; } @@ -212,52 +195,6 @@ namespace } return result; } - - auto constexpr DeltaWindowInSeconds = 60.0; - - //value and elapsed time of the reference sample ~60s in the past; nullopt if that much history isn't available yet - std::optional> findRateWindow(std::vector const& series, std::vector const& timePoints) - { - if (series.size() != timePoints.size() || series.size() < 2) { - return std::nullopt; - } - auto currentTime = timePoints.back(); - if (currentTime - timePoints.front() < DeltaWindowInSeconds) { - return std::nullopt; //not enough history yet to evaluate a full minute - } - auto targetTime = currentTime - DeltaWindowInSeconds; - auto startIt = std::lower_bound(timePoints.begin(), timePoints.end(), targetTime); - if (startIt == timePoints.end()) { - return std::nullopt; - } - auto startIndex = static_cast(std::distance(timePoints.begin(), startIt)); - auto elapsed = currentTime - timePoints.at(startIndex); - if (elapsed < NEAR_ZERO) { - return std::nullopt; - } - return std::make_pair(series.at(startIndex), elapsed); - } - - //growth rate over the trailing minute, based only on samples actually covering that minute (no extrapolation from shorter windows) - std::optional calcDeltaPercentPerMinute(std::vector const& series, std::vector const& timePoints) - { - auto window = findRateWindow(series, timePoints); - if (!window || std::abs(window->first) < NEAR_ZERO) { - return std::nullopt; //percentage is undefined for a near-zero baseline - } - auto percentChange = (series.back() / window->first - 1.0) * 100.0; - return percentChange * (60.0 / window->second); - } - - //absolute change over the trailing minute; used for metrics that can legitimately be zero, where a percentage is undefined - std::optional calcAbsoluteDeltaPerMinute(std::vector const& series, std::vector const& timePoints) - { - auto window = findRateWindow(series, timePoints); - if (!window) { - return std::nullopt; - } - return (series.back() - window->first) * (60.0 / window->second); - } } EvolutionDashboardWindow::EvolutionDashboardWindow() @@ -292,33 +229,27 @@ void EvolutionDashboardWindow::processBackground() return; } _lastTimepoint = timepoint; - _timeSinceSimStart += toDouble(duration) / 1000; auto sessionId = _SimulationFacade::get()->getSessionId(); if (_lastSessionId.has_value() && *_lastSessionId != sessionId) { _timelineLiveStatistics.clear(); - _externalEnergySeries.clear(); - _externalEnergyTimeSeries.clear(); } _lastSessionId = sessionId; auto overallStatistics = _SimulationFacade::get()->getStatisticsEntry(); _timelineLiveStatistics.update(overallStatistics, _SimulationFacade::get()->getCurrentTimestep()); - - _externalEnergySeries.emplace_back(toDouble(_SimulationFacade::get()->getSimulationParameters().externalEnergy.value)); - _externalEnergyTimeSeries.emplace_back(_timeSinceSimStart); - auto maxExternalEnergyValues = toInt(std::lround(MaxLiveHistory * 1000 / LiveStatisticsDeltaTime)); - while (toInt(_externalEnergySeries.size()) > maxExternalEnergyValues) { - _externalEnergySeries.erase(_externalEnergySeries.begin()); - _externalEnergyTimeSeries.erase(_externalEnergyTimeSeries.begin()); - } } void EvolutionDashboardWindow::processIntern() { - if (!_selectedLineageIds.empty() && _timelineMode == TimelineMode_EntireHistory) { - _timelineMode = TimelineMode_LastSteps; + //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(); @@ -331,6 +262,10 @@ void EvolutionDashboardWindow::processIntern() AlienGui::MovableHorizontalSeparator(AlienGui::MovableHorizontalSeparatorParameters().additive(false), _timelinesHeight); + //apply selection changes from the table immediately; otherwise the plots show "No data" for one + //frame, which collapses the plot child window and resets its scroll position + updateDisplayData(); + if (ImGui::BeginChild("##timelineSection", {0, 0})) { processTimelineSection(); } @@ -347,233 +282,228 @@ void EvolutionDashboardWindow::updateCellColors() void EvolutionDashboardWindow::updateDisplayData() { - //rebuild only when the underlying data or view settings have changed + updateTableData(); + updatePlotData(); +} + +void EvolutionDashboardWindow::updateTableData() +{ auto const& liveHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); auto liveBackTime = !liveHistory.empty() ? liveHistory.back().time : -1.0; - auto historyBackTime = -1.0; + if (_lastTableBackTime.has_value() && *_lastTableBackTime == liveBackTime) { + return; + } + _lastTableBackTime = liveBackTime; + + //table rows from the latest live sample (rates per second) + _lineages.clear(); + if (liveHistory.empty()) { + return; + } + auto const& lastSample = liveHistory.back(); + auto referenceIndex = findRateReferenceIndex(liveHistory, lastSample.time); + for (auto const& [lineageId, entry] : lastSample.lineages) { + LineageDisplayData lineage; + lineage.id = toInt(lineageId); + lineage.colorBitset = toInt(entry.colorBitset); + LineageDataPoint const* referenceEntry = nullptr; + auto referenceTime = 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; + referenceTime = liveHistory.at(sampleIndex).time; + break; + } + } + for (int i = 0; i < NumMetrics; ++i) { + lineage.currentValues.at(i) = getLineageMetricValue(entry, referenceEntry, lastSample.time - referenceTime, i); + } + _lineages.emplace_back(std::move(lineage)); + } + std::sort(_lineages.begin(), _lineages.end(), [](auto const& lhs, auto const& rhs) { return lhs.currentValues.at(0) > rhs.currentValues.at(0); }); + + //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) = getGlobalMetricValue(lastSample, referenceDataPoints, false, i); + } +} + +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()); - if (!statisticsHistory.getDataRef().empty()) { - historyBackTime = statisticsHistory.getDataRef().back().time; - } + rebuildPlotSeries(statisticsHistory.getDataRef()); + } else { + rebuildPlotSeries(_timelineLiveStatistics.getDataPointCollectionHistory()); } - RebuildKey key{_timelineMode, _lastSteps, _timeHorizon, liveBackTime, historyBackTime, _selectedLineageIds}; +} + +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); - //table rows from the latest live sample - _lineages.clear(); - if (!liveHistory.empty()) { - auto const& lastSample = liveHistory.back(); - auto referenceIndex = findRateReferenceIndex(liveHistory, lastSample.time); - for (auto const& [lineageId, entry] : lastSample.lineages) { - LineageDisplayData lineage; - lineage.id = toInt(lineageId); - lineage.colorBitset = toInt(entry.colorBitset); - LineageDataPoint const* referenceEntry = nullptr; - auto referenceTime = 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; - referenceTime = liveHistory.at(sampleIndex).time; - break; - } - } - for (int i = 0; i < NumMetrics; ++i) { - lineage.currentValues.at(i) = getLineageMetricValue(entry, referenceEntry, lastSample.time, referenceTime, i); - } - _lineages.emplace_back(std::move(lineage)); - } - std::sort(_lineages.begin(), _lineages.end(), [](auto const& lhs, auto const& rhs) { return lhs.currentValues.at(0) > rhs.currentValues.at(0); }); - } + auto useTimeAsX = _timelineMode == TimelineMode_RealTime; + auto getX = [&](DataPointCollection const& sample) { return useTimeAsX ? sample.time : sample.timestep; }; - //data source for the "all lineages" timeline plots - auto useTimeAsClock = _timelineMode == TimelineMode_RealTime; - StatisticsHistoryData globalSource; - if (_timelineMode == TimelineMode_RealTime) { - globalSource.reserve(liveHistory.size()); - for (auto const& sample : liveHistory) { - globalSource.emplace_back(sample.toOverallDataPointCollection()); - } - if (!globalSource.empty()) { - auto startTime = globalSource.back().time - toDouble(_timeHorizon); - std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startTime; }); - } - } else if (_timelineMode == TimelineMode_LastSteps) { - //fed from the same fine-grained live buffer as real-time mode (not the coarse, entire-run history), - //so the plot has enough resolution over the requested step window; the timestep is carried in the - //(otherwise unused for this mode) time field, mirroring how the entire-run history encodes it - globalSource.reserve(liveHistory.size()); - for (auto const& sample : liveHistory) { - auto dataPoints = sample.toOverallDataPointCollection(); - dataPoints.time = sample.timestep; - globalSource.emplace_back(dataPoints); + //reference samples for the rate metrics are selected via a trailing ~30s window; the live buffer carries the + //time since simulation start while the long-term history only provides the system clock + auto getRateWindowClock = [&](DataPointCollection const& sample) { return _timelineMode == TimelineMode_EntireHistory ? sample.systemClock : sample.time; }; + + 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); } - if (!globalSource.empty()) { - auto startStep = globalSource.back().time - toDouble(_lastSteps); - std::erase_if(globalSource, [&](auto const& dataPoints) { return dataPoints.time < startStep; }); + while (firstIndex < source.size() && getX(source.at(firstIndex)) < startX) { + ++firstIndex; } - } else { - auto const& statisticsHistory = _SimulationFacade::get()->getStatisticsHistory(); - std::lock_guard lock(statisticsHistory.getMutex()); - globalSource = statisticsHistory.getDataRef(); } - //"all lineages" series + //"all lineages" series (the current values are maintained by updateTableData) _allLineages.id = -1; - _allLineages.colorBitset = 0; + _allLineages.series = {}; _allLineages.timePoints.clear(); _allLineages.systemClockPoints.clear(); - for (int i = 0; i < NumMetrics; ++i) { - _allLineages.series.at(i).clear(); - } - size_t globalReferenceIndex = 0; - for (size_t sampleIndex = 0; sampleIndex < globalSource.size(); ++sampleIndex) { - auto const& dataPoints = globalSource.at(sampleIndex); - auto clock = useTimeAsClock ? dataPoints.time : dataPoints.systemClock; - while (globalReferenceIndex + 1 < sampleIndex) { - auto const& nextReference = globalSource.at(globalReferenceIndex + 1); - if ((useTimeAsClock ? nextReference.time : nextReference.systemClock) + RateAveragingInterval > clock) { - break; - } + auto globalReferenceIndex = firstIndex; + for (auto sampleIndex = firstIndex; sampleIndex < source.size(); ++sampleIndex) { + auto const& dataPoints = source.at(sampleIndex); + auto rateWindowClock = getRateWindowClock(dataPoints); + while (globalReferenceIndex + 1 < sampleIndex && getRateWindowClock(source.at(globalReferenceIndex + 1)) + RateAveragingInterval <= rateWindowClock) { ++globalReferenceIndex; } - auto const* referenceDataPoints = sampleIndex > 0 ? &globalSource.at(globalReferenceIndex) : nullptr; - _allLineages.timePoints.emplace_back(dataPoints.time); - if (!useTimeAsClock) { + auto const* referenceDataPoints = sampleIndex > firstIndex ? &source.at(globalReferenceIndex) : nullptr; + _allLineages.timePoints.emplace_back(getX(dataPoints)); + if (!useTimeAsX) { _allLineages.systemClockPoints.emplace_back(dataPoints.systemClock); } for (int i = 0; i < NumMetrics; ++i) { - _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, referenceDataPoints, useTimeAsClock, i)); + _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, referenceDataPoints, true, i)); } } - auto const& liveGlobalHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); - if (!liveGlobalHistory.empty()) { - auto lastDataPoints = liveGlobalHistory.back().toOverallDataPointCollection(); - std::optional referenceDataPoints; - if (liveGlobalHistory.size() >= 2) { - auto referenceIndex = findRateReferenceIndex(liveGlobalHistory, liveGlobalHistory.back().time); - referenceDataPoints = liveGlobalHistory.at(referenceIndex).toOverallDataPointCollection(); - } - for (int i = 0; i < NumMetrics; ++i) { - _allLineages.currentValues.at(i) = getGlobalMetricValue(lastDataPoints, referenceDataPoints ? &*referenceDataPoints : nullptr, true, i); - } - } - //per-lineage series for the selected lineages (live statistics are the only source of lineage data) + + //per-lineage series for the selected lineages _plottedLineages.clear(); - if (!_selectedLineageIds.empty()) { - auto startClock = std::numeric_limits::lowest(); - if (!liveHistory.empty()) { - if (_timelineMode == TimelineMode_RealTime) { - startClock = liveHistory.back().time - toDouble(_timeHorizon); - } else if (_timelineMode == TimelineMode_LastSteps) { - startClock = liveHistory.back().timestep - toDouble(_lastSteps); + for (auto const& selectedId : _selectedLineageIds) { + LineageDisplayData lineage; + lineage.id = selectedId; + struct RateReference + { + double windowClock = 0; + double timestep = 0; + LineageDataPoint const* entry = nullptr; + }; + std::vector rateReferences; //already visited samples containing the lineage + size_t rateReferenceIndex = 0; + for (auto sampleIndex = firstIndex; sampleIndex < source.size(); ++sampleIndex) { + auto const& sample = source.at(sampleIndex); + auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); + if (!entry) { + continue; } - } - for (auto const& selectedId : _selectedLineageIds) { - LineageDisplayData lineage; - lineage.id = selectedId; - std::vector> rateReferences; //clock and entry of the already visited samples - size_t rateReferenceIndex = 0; - for (auto const& sample : liveHistory) { - if ((useTimeAsClock ? sample.time : sample.timestep) < startClock) { - continue; - } - auto const* entry = findLineageEntry(sample, toUInt32(selectedId)); - if (!entry) { - continue; - } - lineage.colorBitset = toInt(entry->colorBitset); - lineage.timePoints.emplace_back(useTimeAsClock ? sample.time : sample.timestep); - if (!useTimeAsClock) { - lineage.systemClockPoints.emplace_back(sample.systemClock); - } - auto clock = useTimeAsClock ? sample.time : sample.systemClock; - while (rateReferenceIndex + 1 < rateReferences.size() && rateReferences.at(rateReferenceIndex + 1).first + RateAveragingInterval <= clock) { - ++rateReferenceIndex; - } - auto const* lastEntry = !rateReferences.empty() ? rateReferences.at(rateReferenceIndex).second : nullptr; - auto lastClock = !rateReferences.empty() ? rateReferences.at(rateReferenceIndex).first : 0.0; - for (int i = 0; i < NumMetrics; ++i) { - lineage.series.at(i).emplace_back(getLineageMetricValue(*entry, lastEntry, clock, lastClock, i)); - } - rateReferences.emplace_back(clock, entry); + lineage.colorBitset = toInt(entry->colorBitset); + lineage.timePoints.emplace_back(getX(sample)); + if (!useTimeAsX) { + lineage.systemClockPoints.emplace_back(sample.systemClock); } - for (auto const& tableLineage : _lineages) { - if (tableLineage.id == selectedId) { - lineage.currentValues = tableLineage.currentValues; - if (lineage.colorBitset == 0) { - lineage.colorBitset = tableLineage.colorBitset; - } - } + auto windowClock = getRateWindowClock(sample); + while (rateReferenceIndex + 1 < rateReferences.size() + && rateReferences.at(rateReferenceIndex + 1).windowClock + RateAveragingInterval <= windowClock) { + ++rateReferenceIndex; } - _plottedLineages.emplace_back(std::move(lineage)); + auto const* lastEntry = !rateReferences.empty() ? rateReferences.at(rateReferenceIndex).entry : nullptr; + auto deltaKSteps = !rateReferences.empty() ? (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({windowClock, sample.timestep, entry}); } - } - - //header sparklines and deltas from the live statistics - for (int cardIndex = 0; cardIndex < 3; ++cardIndex) { - auto& sparkline = _headerSparklines.at(cardIndex); - sparkline.clear(); - std::vector fullSeries; - std::vector fullTimePoints; - if (cardIndex == 0) { - fullSeries = _externalEnergySeries; - fullTimePoints = _externalEnergyTimeSeries; - } else { - fullSeries.reserve(liveGlobalHistory.size()); - fullTimePoints.reserve(liveGlobalHistory.size()); - for (auto const& dataPoints : liveGlobalHistory) { - switch (cardIndex) { - case 1: - fullSeries.emplace_back(dataPoints.overall.numLineages); - break; - case 2: - fullSeries.emplace_back(dataPoints.overall.numCreatures); - break; + if (lineage.colorBitset == 0) { + for (auto const& tableLineage : _lineages) { + if (tableLineage.id == selectedId) { + lineage.colorBitset = tableLineage.colorBitset; } - fullTimePoints.emplace_back(dataPoints.time); } } - _headerDeltas.at(cardIndex) = - cardIndex == 0 ? calcAbsoluteDeltaPerMinute(fullSeries, fullTimePoints) : calcDeltaPercentPerMinute(fullSeries, fullTimePoints); - auto stride = std::max(size_t(1), fullSeries.size() / NumSparklinePoints); - for (size_t i = 0; i < fullSeries.size(); i += stride) { - sparkline.emplace_back(fullSeries.at(i)); - } + _plottedLineages.emplace_back(std::move(lineage)); } } void EvolutionDashboardWindow::processHeader() { auto const& style = ImGui::GetStyle(); - auto cardWidth = (ImGui::GetContentRegionAvail().x - 3 * style.ItemSpacing.x) / 5; + 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; - processEntitiesCard(cardWidth * 2, cardHeight); - ImGui::SameLine(); - auto externalEnergy = !_externalEnergySeries.empty() ? _externalEnergySeries.back() : 0.0; - processKpiCard("External energy", formatMetricValue(externalEnergy, 0), _headerDeltas.at(0), false, 0, cardWidth, cardHeight); + 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 energyParticles = lastDataPoints ? lastDataPoints->overall.numEnergyParticles : 0.0; + processCard( + "Entities", + formatMetricValue(solids + fluids + cells + energyParticles, 0), + {{"Solids", formatMetricValue(solids, 0)}, + {"Fluid particles", formatMetricValue(fluids, 0)}, + {"Cells", formatMetricValue(cells, 0)}, + {"Energy particles", formatMetricValue(energyParticles, 0)}}, + cardWidth * 2, + cardHeight); ImGui::SameLine(); + auto numLineages = lastDataPoints ? lastDataPoints->overall.numLineages : 0.0; - processKpiCard("Lineages", formatMetricValue(numLineages, 0), _headerDeltas.at(1), true, 1, cardWidth, cardHeight); + 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), + {{">= 5% creatures", formatMetricValue(toDouble(numLineagesAbove5Percent), 0)}, + {">= 1% creatures", formatMetricValue(toDouble(numLineagesAbove1Percent), 0)}}, + cardWidth, + cardHeight); ImGui::SameLine(); - processKpiCard("Creatures", formatMetricValue(_allLineages.currentValues.at(0), 0), _headerDeltas.at(2), true, 2, cardWidth, cardHeight); + + 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::processKpiCard( +void EvolutionDashboardWindow::processCard( std::string const& label, std::string const& value, - std::optional delta, - bool isPercentage, - int sparklineIndex, + std::vector> const& subValues, float width, float height) { @@ -589,86 +519,12 @@ void EvolutionDashboardWindow::processKpiCard( AlienGui::Text(value); ImGui::PopFont(); - if (delta.has_value()) { - char deltaText[32]; - if (isPercentage) { - snprintf(deltaText, sizeof(deltaText), "%+.1f %% / min", *delta); - } else { - snprintf(deltaText, sizeof(deltaText), "%s%s / min", *delta >= 0 ? "+" : "-", formatMetricValue(std::abs(*delta), 0).c_str()); - } - ImGui::PushStyleColor(ImGuiCol_Text, *delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); - AlienGui::Text(deltaText); - ImGui::PopStyleColor(); - } else { - ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); - AlienGui::Text("collecting data ..."); - ImGui::PopStyleColor(); - } - - auto const& sparkline = _headerSparklines.at(sparklineIndex); - if (delta.has_value() && sparkline.size() >= 2) { - ImGui::SetCursorPos({width - scale(SparklineWidth + 12.0f), height - scale(SparklineHeight + 12.0f)}); - ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(0, 0)); - ImPlot::PushStyleColor(ImPlotCol_FrameBg, (ImU32)ImColor(0, 0, 0, 0)); - ImPlot::PushStyleColor(ImPlotCol_PlotBg, (ImU32)ImColor(0, 0, 0, 0)); - ImPlot::PushStyleColor(ImPlotCol_PlotBorder, (ImU32)ImColor(0, 0, 0, 0)); - if (ImPlot::BeginPlot( - ("##spark" + label).c_str(), {scale(SparklineWidth), scale(SparklineHeight)}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) { - ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations); - auto [minIt, maxIt] = std::minmax_element(sparkline.begin(), sparkline.end()); - ImPlot::SetupAxesLimits(0, toDouble(sparkline.size() - 1), *minIt * 0.9, *maxIt * 1.1 + NEAR_ZERO, ImGuiCond_Always); - ImPlot::PushStyleColor(ImPlotCol_Line, *delta >= 0 ? (ImU32)PositiveDeltaColor : (ImU32)NegativeDeltaColor); - ImPlot::PlotLine("##", sparkline.data(), toInt(sparkline.size())); - ImPlot::PopStyleColor(); - ImPlot::EndPlot(); - } - ImPlot::PopStyleColor(3); - ImPlot::PopStyleVar(); - } - } - ImGui::EndChild(); - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); -} - -void EvolutionDashboardWindow::processEntitiesCard(float width, float height) -{ - auto const& liveGlobalHistory = _timelineLiveStatistics.getDataPointCollectionHistory(); - auto solids = 0.0; - auto fluids = 0.0; - auto cells = 0.0; - auto total = 0.0; - if (!liveGlobalHistory.empty()) { - auto const& dataPoints = liveGlobalHistory.back(); - solids = dataPoints.overall.numSolidObjects; - fluids = dataPoints.overall.numFluidObjects; - cells = dataPoints.overall.numCellObjects; - total = solids + fluids + cells; - } - - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, scale(6.0f)); - ImGui::PushStyleColor(ImGuiCol_ChildBg, (ImU32)CardBackgroundColor); - ImGui::PushStyleColor(ImGuiCol_Border, (ImU32)CardBorderColor); - if (ImGui::BeginChild("##cardEntities", {width, height}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { - ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); - AlienGui::Text("Entities"); - ImGui::PopStyleColor(); - - ImGui::PushFont(StyleRepository::get().getLargeFont()); - AlienGui::Text(formatMetricValue(total, 0)); - ImGui::PopFont(); - - std::pair const subValues[] = { - {"Solids", solids}, - {"Fluid particles", fluids}, - {"Cells", cells}, - }; for (auto const& [subLabel, subValue] : subValues) { ImGui::BeginGroup(); ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); AlienGui::Text(subLabel); ImGui::PopStyleColor(); - AlienGui::Text(formatMetricValue(subValue, 0)); + AlienGui::Text(subValue); ImGui::EndGroup(); ImGui::SameLine(0, scale(18.0f)); } @@ -682,7 +538,7 @@ void EvolutionDashboardWindow::processFilterBar() { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); - AlienGui::Text("Filter by colors"); + AlienGui::Text("Filter by customizations"); ImGui::PopStyleColor(); ImGui::SameLine(0, scale(12.0f)); @@ -748,7 +604,7 @@ void EvolutionDashboardWindow::processLineageTable() 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].decimals)); + rightAlignedText(formatMetricValue(_allLineages.currentValues.at(i), Metrics[i].tableDecimals)); } //lineage rows @@ -786,7 +642,7 @@ void EvolutionDashboardWindow::processLineageTable() AlienGui::Text("Lineage #" + std::to_string(lineage.id)); for (int i = 0; i < NumMetrics; ++i) { ImGui::TableSetColumnIndex(i + 1); - rightAlignedText(formatMetricValue(lineage.currentValues.at(i), Metrics[i].decimals)); + rightAlignedText(formatMetricValue(lineage.currentValues.at(i), Metrics[i].tableDecimals)); } ImGui::PopID(); } @@ -806,11 +662,7 @@ void EvolutionDashboardWindow::processTimelineSection() void EvolutionDashboardWindow::processTimelineHeader() { ImGui::Spacing(); - std::vector modeValues{"Real-time", "Last X steps"}; - if (_selectedLineageIds.empty()) { - modeValues.emplace_back("Entire history"); - } - _timelineMode = std::min(toInt(modeValues.size()) - 1, _timelineMode); + std::vector modeValues{"Real-time", "Last X steps", "Entire history"}; AlienGui::Switcher(AlienGui::SwitcherParameters().name("Mode").width(260.0f).textWidth(45.0f).values(modeValues), &_timelineMode); if (_timelineMode == TimelineMode_RealTime) { ImGui::SameLine(0, scale(20.0f)); @@ -871,16 +723,15 @@ void EvolutionDashboardWindow::processTimelinePlots() ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); AlienGui::Text(Metrics[i].plotName); + auto seriesIndex = 0; for (auto const* lineage : plottedLineages) { ImGui::PushFont(StyleRepository::get().getMediumBoldFont()); - if (lineage->id != -1) { - ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)toImColor(_cellColors.at(getFirstColor(lineage->colorBitset)))); - } - AlienGui::Text(formatMetricValue(lineage->currentValues.at(i), Metrics[i].decimals)); - if (lineage->id != -1) { - ImGui::PopStyleColor(); - } + 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::TableSetColumnIndex(1); @@ -933,20 +784,23 @@ void EvolutionDashboardWindow::processTimelinePlot(std::vectortimePoints.size()); if (count < 2) { + ++seriesIndex; continue; } ImGui::PushID(toInt(lineage->id) + 1); - auto color = lineage->id == -1 ? SumSeriesColor : toImColor(_cellColors.at(getFirstColor(lineage->colorBitset))); + auto color = getPlotColor(metricIndex, seriesIndex, toInt(plottedLineages.size())); auto const& series = lineage->series.at(metricIndex); ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); + ImPlot::PushStyleColor(ImPlotCol_Fill, (ImU32)color); ImPlot::PlotLine("##line", lineage->timePoints.data(), series.data(), count); ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.5f * ImGui::GetStyle().Alpha); ImPlot::PlotShaded("##shaded", lineage->timePoints.data(), series.data(), count); ImPlot::PopStyleVar(); - ImPlot::PopStyleColor(); + ImPlot::PopStyleColor(2); if (ImGui::GetStyle().Alpha == 1.0f) { ImPlot::Annotation( lineage->timePoints.back(), @@ -955,9 +809,10 @@ void EvolutionDashboardWindow::processTimelinePlot(std::vectortimePoints.size() >= 2) { auto const* lineage = plottedLineages.front(); @@ -968,7 +823,7 @@ void EvolutionDashboardWindow::processTimelinePlot(std::vector const& source); void processHeader(); - void processKpiCard( + void processCard( std::string const& label, std::string const& value, - std::optional delta, - bool isPercentage, - int sparklineIndex, + std::vector> const& subValues, float width, float height); - void processEntitiesCard(float width, float height); void processFilterBar(); void processLineageTable(); void processTimelineSection(); @@ -75,8 +75,6 @@ class EvolutionDashboardWindow : public AlienWindow std::vector _lineages; //table rows from the latest sample, sorted by creature count std::vector _plottedLineages; LineageDisplayData _allLineages; - std::array, 3> _headerSparklines = {}; - std::array, 3> _headerDeltas = {}; std::array _cellColors = {}; @@ -88,32 +86,30 @@ class EvolutionDashboardWindow : public AlienWindow { TimelineMode_RealTime, TimelineMode_LastSteps, - TimelineMode_EntireHistory //only selectable when all lineages are shown + TimelineMode_EntireHistory }; TimelineMode _timelineMode = TimelineMode_EntireHistory; int _lastSteps = 100000; float _timeHorizon = 10.0f; //in seconds float _timelinesHeight = 0; + std::optional _lastWindowHeight; struct RebuildKey { int timelineMode = 0; int lastSteps = 0; float timeHorizon = 0; - double liveBackTime = 0; - double historyBackTime = 0; + double sourceBackTime = 0; std::set selectedLineageIds; bool operator==(RebuildKey const&) const = default; }; std::optional _lastRebuildKey; + std::optional _lastTableBackTime; // Live statistics TimelineLiveStatistics _timelineLiveStatistics; - std::vector _externalEnergySeries; - std::vector _externalEnergyTimeSeries; - double _timeSinceSimStart = 0; //in seconds std::optional _lastSessionId; std::optional _lastTimepoint; }; From 46e37558c80906f1d4c84b7a820f89a7ead5d1d9 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 15 Jul 2026 20:06:21 +0200 Subject: [PATCH 32/41] Improve dashboard visualization, part 2 --- source/Base/StringHelper.cpp | 10 ++ source/Base/StringHelper.h | 1 + source/Gui/EvolutionDashboardWindow.cpp | 163 ++++++++++++++++-------- source/Gui/SelectionWindow.cpp | 16 +-- source/Gui/TemporalControlWindow.cpp | 12 +- source/Gui/TimelineLiveStatistics.cpp | 2 +- source/Gui/TimelineLiveStatistics.h | 2 +- 7 files changed, 135 insertions(+), 71 deletions(-) diff --git a/source/Base/StringHelper.cpp b/source/Base/StringHelper.cpp index 64dd14f3c..3218a8d38 100644 --- a/source/Base/StringHelper.cpp +++ b/source/Base/StringHelper.cpp @@ -110,6 +110,16 @@ std::string StringHelper::formatInHex(uint64_t value) return ss.str(); } +std::string StringHelper::formatInThousands(double value) +{ + if (value == 0.0) { + return "0"; + } + std::stringstream ss; + ss << value / 1000 << "K"; + return ss.str(); +} + 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 4f06ccd94..45838518c 100644 --- a/source/Base/StringHelper.h +++ b/source/Base/StringHelper.h @@ -15,6 +15,7 @@ class StringHelper 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" 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/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index de77e0bcd..f5929686d 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -17,6 +17,7 @@ #include #include +#include #include "AlienGui.h" #include "StyleRepository.h" @@ -45,16 +46,16 @@ namespace int plotDecimals; }; - //the last two metrics are rates: per second in the table, per 1K time steps in the timeline plots + //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 age (gen)", "Avg age (generations)", 0, 0}, - {"Created /s", "Created creatures / 1K steps", 2, 2}, - {"Mutations /s", "Mutations / 1K steps", 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) @@ -76,7 +77,10 @@ namespace std::string formatMetricValue(double value, int decimals) { - if (std::isinf(value)) { + if (std::isnan(value)) { + return "-"; + } + if (value >= toDouble(Infinity::value)) { //parameters use FLT_MAX to represent infinity return "infinity"; } return StringHelper::format(value, decimals); @@ -121,7 +125,7 @@ namespace return std::max(0.0, (accumValue - lastAccumValue) / delta); //negative deltas occur after accumulated statistics have been reset } - double getGlobalMetricValue(DataPointCollection const& dataPoints, DataPointCollection const* lastDataPoints, bool ratePerKSteps, int metricIndex) + double getGlobalMetricValue(DataPointCollection const& dataPoints, DataPointCollection const* lastDataPoints, int metricIndex) { switch (metricIndex) { case 0: @@ -139,13 +143,13 @@ namespace case 6: case 7: { if (!lastDataPoints) { - return 0.0; + return std::numeric_limits::quiet_NaN(); } - auto delta = ratePerKSteps ? (dataPoints.timestep - lastDataPoints->timestep) / 1000.0 : dataPoints.time - lastDataPoints->time; + auto deltaKSteps = (dataPoints.timestep - lastDataPoints->timestep) / 1000.0; if (metricIndex == 6) { - return calcRate(dataPoints.overall.accumCreatedCreatures, lastDataPoints->overall.accumCreatedCreatures, delta); + return calcRate(dataPoints.overall.accumCreatedCreatures, lastDataPoints->overall.accumCreatedCreatures, deltaKSteps); } else { - return calcRate(dataPoints.overall.accumMutations, lastDataPoints->overall.accumMutations, delta); + return calcRate(dataPoints.overall.accumMutations, lastDataPoints->overall.accumMutations, deltaKSteps); } } default: @@ -169,9 +173,9 @@ namespace case 5: return entry.numCreatures > 0 ? entry.sumCreatureGenerations / entry.numCreatures : 0.0; case 6: - return lastEntry ? calcRate(entry.numCreatedCreatures, lastEntry->numCreatedCreatures, rateDelta) : 0.0; + return lastEntry ? calcRate(entry.numCreatedCreatures, lastEntry->numCreatedCreatures, rateDelta) : std::numeric_limits::quiet_NaN(); case 7: - return lastEntry ? calcRate(entry.totalMutations, lastEntry->totalMutations, rateDelta) : 0.0; + return lastEntry ? calcRate(entry.totalMutations, lastEntry->totalMutations, rateDelta) : std::numeric_limits::quiet_NaN(); default: return 0.0; } @@ -183,6 +187,11 @@ namespace 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 RateAveragingInterval older; falls back to the oldest sample size_t findRateReferenceIndex(std::vector const& history, double time) { @@ -262,8 +271,7 @@ void EvolutionDashboardWindow::processIntern() AlienGui::MovableHorizontalSeparator(AlienGui::MovableHorizontalSeparatorParameters().additive(false), _timelinesHeight); - //apply selection changes from the table immediately; otherwise the plots show "No data" for one - //frame, which collapses the plot child window and resets its scroll position + //apply selection changes from the table immediately; otherwise the plots are empty for one frame updateDisplayData(); if (ImGui::BeginChild("##timelineSection", {0, 0})) { @@ -295,7 +303,7 @@ void EvolutionDashboardWindow::updateTableData() } _lastTableBackTime = liveBackTime; - //table rows from the latest live sample (rates per second) + //table rows from the latest live sample (rates per 1K time steps) _lineages.clear(); if (liveHistory.empty()) { return; @@ -307,16 +315,16 @@ void EvolutionDashboardWindow::updateTableData() lineage.id = toInt(lineageId); lineage.colorBitset = toInt(entry.colorBitset); LineageDataPoint const* referenceEntry = nullptr; - auto referenceTime = 0.0; + 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; - referenceTime = liveHistory.at(sampleIndex).time; + referenceTimestep = liveHistory.at(sampleIndex).timestep; break; } } for (int i = 0; i < NumMetrics; ++i) { - lineage.currentValues.at(i) = getLineageMetricValue(entry, referenceEntry, lastSample.time - referenceTime, i); + lineage.currentValues.at(i) = getLineageMetricValue(entry, referenceEntry, (lastSample.timestep - referenceTimestep) / 1000.0, i); } _lineages.emplace_back(std::move(lineage)); } @@ -325,7 +333,7 @@ void EvolutionDashboardWindow::updateTableData() //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) = getGlobalMetricValue(lastSample, referenceDataPoints, false, i); + _allLineages.currentValues.at(i) = getGlobalMetricValue(lastSample, referenceDataPoints, i); } } @@ -369,6 +377,11 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector 0) { + --firstIndex; + } } //"all lineages" series (the current values are maintained by updateTableData) @@ -376,20 +389,23 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector firstIndex ? &source.at(globalReferenceIndex) : nullptr; + //rates over references younger than the averaging interval would produce spikes at the left plot border; NaN suppresses those samples + auto referenceIsOldEnough = + globalReferenceIndex < sampleIndex && getRateWindowClock(source.at(globalReferenceIndex)) + RateAveragingInterval <= rateWindowClock; + auto const* referenceDataPoints = referenceIsOldEnough ? &source.at(globalReferenceIndex) : nullptr; _allLineages.timePoints.emplace_back(getX(dataPoints)); if (!useTimeAsX) { _allLineages.systemClockPoints.emplace_back(dataPoints.systemClock); } for (int i = 0; i < NumMetrics; ++i) { - _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, referenceDataPoints, true, i)); + _allLineages.series.at(i).emplace_back(getGlobalMetricValue(dataPoints, referenceDataPoints, i)); } } @@ -404,28 +420,32 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector rateReferences; //already visited samples containing the lineage + std::vector rateReferences; //already visited samples containing the lineage; may also lie before the visible range size_t rateReferenceIndex = 0; - for (auto sampleIndex = firstIndex; sampleIndex < source.size(); ++sampleIndex) { + 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); - lineage.timePoints.emplace_back(getX(sample)); - if (!useTimeAsX) { - lineage.systemClockPoints.emplace_back(sample.systemClock); - } auto windowClock = getRateWindowClock(sample); while (rateReferenceIndex + 1 < rateReferences.size() && rateReferences.at(rateReferenceIndex + 1).windowClock + RateAveragingInterval <= windowClock) { ++rateReferenceIndex; } - auto const* lastEntry = !rateReferences.empty() ? rateReferences.at(rateReferenceIndex).entry : nullptr; - auto deltaKSteps = !rateReferences.empty() ? (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)); + if (sampleIndex >= firstIndex) { + lineage.timePoints.emplace_back(getX(sample)); + if (!useTimeAsX) { + lineage.systemClockPoints.emplace_back(sample.systemClock); + } + //rates over references younger than the averaging interval would produce spikes at the left plot border; NaN suppresses those samples + auto referenceIsOldEnough = !rateReferences.empty() && rateReferences.at(rateReferenceIndex).windowClock + RateAveragingInterval <= windowClock; + 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({windowClock, sample.timestep, entry}); } @@ -453,14 +473,17 @@ void EvolutionDashboardWindow::processHeader() 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)}, - {"Fluid particles", formatMetricValue(fluids, 0)}, + {"Fluids", formatMetricValue(fluids, 0)}, {"Cells", formatMetricValue(cells, 0)}, - {"Energy particles", formatMetricValue(energyParticles, 0)}}, + {"Free cells", formatMetricValue(freeCells, 0)}, + {"Energies", formatMetricValue(energyParticles, 0)}}, cardWidth * 2, cardHeight); ImGui::SameLine(); @@ -481,8 +504,8 @@ void EvolutionDashboardWindow::processHeader() processCard( "Lineages", formatMetricValue(numLineages, 0), - {{">= 5% creatures", formatMetricValue(toDouble(numLineagesAbove5Percent), 0)}, - {">= 1% creatures", formatMetricValue(toDouble(numLineagesAbove1Percent), 0)}}, + {{">= 1% creatures", formatMetricValue(toDouble(numLineagesAbove1Percent), 0)}, + {">= 5% creatures", formatMetricValue(toDouble(numLineagesAbove5Percent), 0)}}, cardWidth, cardHeight); ImGui::SameLine(); @@ -519,14 +542,28 @@ void EvolutionDashboardWindow::processCard( 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::SameLine(0, scale(18.0f)); } } ImGui::EndChild(); @@ -653,6 +690,11 @@ void EvolutionDashboardWindow::processLineageTable() 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(); + if (ImGui::BeginChild("##timelinePlots", {0, 0})) { processTimelinePlots(); } @@ -662,7 +704,7 @@ void EvolutionDashboardWindow::processTimelineSection() void EvolutionDashboardWindow::processTimelineHeader() { ImGui::Spacing(); - std::vector modeValues{"Real-time", "Last X steps", "Entire history"}; + 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); if (_timelineMode == TimelineMode_RealTime) { ImGui::SameLine(0, scale(20.0f)); @@ -715,13 +757,17 @@ void EvolutionDashboardWindow::processTimelinePlots() } } + //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("##label", ImGuiTableColumnFlags_WidthFixed, scale(PlotLabelColumnWidth)); ImGui::TableSetupColumn("##plot"); + ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, scale(PlotLabelColumnWidth)); for (int i = 0; i < NumMetrics; ++i) { auto showTimeAxis = i == NumMetrics - 1 && _timelineMode == TimelineMode_EntireHistory; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); + processTimelinePlot(plottedLineages, i, showTimeAxis); + + ImGui::TableSetColumnIndex(1); AlienGui::Text(Metrics[i].plotName); auto seriesIndex = 0; for (auto const* lineage : plottedLineages) { @@ -733,9 +779,6 @@ void EvolutionDashboardWindow::processTimelinePlots() ImGui::PopFont(); ++seriesIndex; } - - ImGui::TableSetColumnIndex(1); - processTimelinePlot(plottedLineages, i, showTimeAxis); } ImGui::EndTable(); } @@ -755,16 +798,15 @@ void EvolutionDashboardWindow::processTimelinePlot(std::vectortimePoints.front()); maxTime = std::max(maxTime, lineage->timePoints.back()); for (auto const& value : lineage->series.at(metricIndex)) { - upperBound = std::max(upperBound, value); + if (std::isfinite(value)) { + upperBound = std::max(upperBound, value); + } } } if (!hasData) { - ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)Const::TextDecentColor); - AlienGui::Text("No data"); - ImGui::PopStyleColor(); - return; - } - if (_timelineMode == TimelineMode_RealTime) { + minTime = 0.0; + maxTime = 1.0; + } else if (_timelineMode == TimelineMode_RealTime) { minTime = maxTime - toDouble(_timeHorizon); } else if (_timelineMode == TimelineMode_LastSteps) { minTime = maxTime - toDouble(_lastSteps); @@ -782,23 +824,33 @@ void EvolutionDashboardWindow::processTimelinePlot(std::vectortimePoints.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())); - auto const& series = lineage->series.at(metricIndex); ImPlot::PushStyleColor(ImPlotCol_Line, (ImU32)color); ImPlot::PushStyleColor(ImPlotCol_Fill, (ImU32)color); - ImPlot::PlotLine("##line", lineage->timePoints.data(), series.data(), count); + 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(), series.data(), count); + ImPlot::PlotShaded("##shaded", lineage->timePoints.data() + offset, series.data() + offset, count); ImPlot::PopStyleVar(); ImPlot::PopStyleColor(2); if (ImGui::GetStyle().Alpha == 1.0f) { @@ -857,7 +909,8 @@ void EvolutionDashboardWindow::drawValuesAtMouseCursor( break; } } - mousePos.y = std::max(0.0, std::min(upperBound, mousePos.y)); + 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)}); @@ -877,14 +930,14 @@ void EvolutionDashboardWindow::drawValuesAtMouseCursor( "Time step: %s\nTimestamp: %s\nValue: %s", StringHelper::format(toFloat(mousePos.x), 0).c_str(), dateTimeString.c_str(), - formatMetricValue(mousePos.y, fracPartDecimals).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(mousePos.y, fracPartDecimals).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)}); } diff --git a/source/Gui/SelectionWindow.cpp b/source/Gui/SelectionWindow.cpp index 769456762..ca921f580 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/TemporalControlWindow.cpp b/source/Gui/TemporalControlWindow.cpp index b7a0a29b7..9e14e38e7 100644 --- a/source/Gui/TemporalControlWindow.cpp +++ b/source/Gui/TemporalControlWindow.cpp @@ -73,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(); } diff --git a/source/Gui/TimelineLiveStatistics.cpp b/source/Gui/TimelineLiveStatistics.cpp index d18a3fed1..4dc1222d5 100644 --- a/source/Gui/TimelineLiveStatistics.cpp +++ b/source/Gui/TimelineLiveStatistics.cpp @@ -41,7 +41,7 @@ void TimelineLiveStatistics::update(StatisticsEntry const& overallStatistics, ui void TimelineLiveStatistics::truncate() { - //keep enough history to cover both the real-time window (time-based) and the "Last X steps" window + //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) { diff --git a/source/Gui/TimelineLiveStatistics.h b/source/Gui/TimelineLiveStatistics.h index 669341611..1fff81624 100644 --- a/source/Gui/TimelineLiveStatistics.h +++ b/source/Gui/TimelineLiveStatistics.h @@ -11,7 +11,7 @@ class TimelineLiveStatistics { public: static auto constexpr MaxLiveHistory = 240.0f; //in seconds - static auto constexpr MaxLiveSteps = 100000; //max span retained for "Last X steps" mode + static auto constexpr MaxLiveSteps = 100000; //max span retained for "Last time steps" mode std::vector const& getDataPointCollectionHistory() const; void update(StatisticsEntry const& overallStatistics, uint64_t timestep); From f46796da9fbc4a7d996e2f46bb30bb70b413d77b Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 15 Jul 2026 21:13:04 +0200 Subject: [PATCH 33/41] Improve dashboard visualization, part 3 --- source/Base/StringHelper.cpp | 13 ++++- source/Base/StringHelper.h | 2 +- source/Gui/EvolutionDashboardWindow.cpp | 74 ++++++++++++++++++------- source/Gui/EvolutionDashboardWindow.h | 1 + 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/source/Base/StringHelper.cpp b/source/Base/StringHelper.cpp index 3218a8d38..4ebeaa8ff 100644 --- a/source/Base/StringHelper.cpp +++ b/source/Base/StringHelper.cpp @@ -115,9 +115,16 @@ std::string StringHelper::formatInThousands(double value) if (value == 0.0) { return "0"; } - std::stringstream ss; - ss << value / 1000 << "K"; - return ss.str(); + 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) diff --git a/source/Base/StringHelper.h b/source/Base/StringHelper.h index 45838518c..67d152e04 100644 --- a/source/Base/StringHelper.h +++ b/source/Base/StringHelper.h @@ -15,7 +15,7 @@ class StringHelper 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" + 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/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index f5929686d..757bf55e2 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -26,13 +26,15 @@ namespace { auto constexpr LiveStatisticsDeltaTime = 50; //in millisec auto constexpr RateAveragingInterval = 30.0; //in seconds + auto constexpr MinRateTimesteps = 1000.0; //at the start of the history, rates are already shown once this many time steps are covered auto constexpr ColorChipSize = 22.0f; auto constexpr SwatchSize = 11.0f; auto constexpr SwatchGap = 3.0f; auto constexpr PlotLabelColumnWidth = 150.0f; - auto constexpr PlotHeight = 60.0f; - auto constexpr PlotHeightWithAxis = 80.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); @@ -68,11 +70,12 @@ namespace } //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 + //or one per selected lineage if multiple lineages are plotted together; + //stepping through the colormap with a stride coprime to its size makes adjacent rows clearly distinct ImColor getPlotColor(int metricIndex, int seriesIndex, int numSeries) { auto index = numSeries > 1 ? seriesIndex : metricIndex; - return ImColor(ImPlot::GetColormapColor((index % 21) <= 10 ? (index % 21) : 20 - (index % 21), ImPlotColormap_Cool)); + return ImColor(ImPlot::GetColormapColor(index * 2 % 11, ImPlotColormap_Cool)); } std::string formatMetricValue(double value, int decimals) @@ -207,7 +210,7 @@ namespace } EvolutionDashboardWindow::EvolutionDashboardWindow() - : AlienWindow("Evolution Dashboard", "windows.evolution dashboard", false, true) + : AlienWindow("Evolution dashboard", "windows.evolution dashboard", false, true, {scale(700.0f), scale(500.0f)}) {} void EvolutionDashboardWindow::initIntern() @@ -216,6 +219,7 @@ void EvolutionDashboardWindow::initIntern() _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); validateAndCorrect(); } @@ -226,6 +230,7 @@ void EvolutionDashboardWindow::shutdownIntern() 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); } @@ -396,9 +401,12 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector= 1% creatures", formatMetricValue(toDouble(numLineagesAbove1Percent), 0)}, - {">= 5% creatures", formatMetricValue(toDouble(numLineagesAbove5Percent), 0)}}, + {{"> 1% creatures", formatMetricValue(toDouble(numLineagesAbove1Percent), 0)}, + {"> 5% creatures", formatMetricValue(toDouble(numLineagesAbove5Percent), 0)}}, cardWidth, cardHeight); ImGui::SameLine(); @@ -629,6 +640,8 @@ void EvolutionDashboardWindow::processLineageTable() ImGui::TextUnformatted(columnName); } + auto drawList = ImGui::GetWindowDrawList(); + //summary row ImGui::TableNextRow(); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImColor(0.13f, 0.16f, 0.23f, 1.0f)); @@ -638,6 +651,18 @@ void EvolutionDashboardWindow::processLineageTable() _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); @@ -645,7 +670,6 @@ void EvolutionDashboardWindow::processLineageTable() } //lineage rows - auto drawList = ImGui::GetWindowDrawList(); auto maxNumColors = 1; for (auto const& lineage : _lineages) { if (_colorFilter & lineage.colorBitset) { @@ -706,26 +730,37 @@ 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) { - ImGui::SameLine(0, scale(20.0f)); - if (ImGui::BeginChild("##timeHorizon", {scale(320.0f), ImGui::GetFrameHeight()})) { + 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) { - ImGui::SameLine(0, scale(20.0f)); - if (ImGui::BeginChild("##lastSteps", {scale(320.0f), ImGui::GetFrameHeight()})) { + 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"); @@ -820,7 +855,7 @@ void EvolutionDashboardWindow::processTimelinePlot(std::vector(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); _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 index 700cb9ce1..05d015532 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -93,6 +93,7 @@ class EvolutionDashboardWindow : public AlienWindow int _lastSteps = 100000; float _timeHorizon = 10.0f; //in seconds float _timelinesHeight = 0; + float _plotHeight = 60.0f; std::optional _lastWindowHeight; struct RebuildKey From a29189fc0ba0299b9d2eaabfbd7e42aec6b09a82 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 15 Jul 2026 23:01:29 +0200 Subject: [PATCH 34/41] Minor adaptions --- source/Gui/EvolutionDashboardWindow.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 757bf55e2..9f37acb44 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -71,11 +71,11 @@ namespace //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 a stride coprime to its size makes adjacent rows clearly distinct + //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 % 11, ImPlotColormap_Cool)); + return ImColor(ImPlot::GetColormapColor((index / 2 * 3 + index % 2) % 11, ImPlotColormap_Cool)); } std::string formatMetricValue(double value, int decimals) @@ -630,13 +630,19 @@ void EvolutionDashboardWindow::processLineageTable() ImGui::TableSetupColumn(metric.tableHeader, ImGuiTableColumnFlags_WidthFixed, scale(105.0f)); } - //header row with centered labels + //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); auto columnName = ImGui::TableGetColumnName(column); auto textWidth = ImGui::CalcTextSize(columnName).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - textWidth) / 2)); + auto cellMinX = ImGui::GetCursorScreenPos().x; + auto cellMaxX = cellMinX + ImGui::GetContentRegionAvail().x; + auto visibleMinX = std::max(cellMinX, ImGui::GetWindowDrawList()->GetClipRectMin().x); + auto visibleMaxX = std::min(cellMaxX, ImGui::GetWindowDrawList()->GetClipRectMax().x); + auto textPosX = std::clamp((visibleMinX + visibleMaxX - textWidth) / 2, cellMinX, std::max(cellMinX, cellMaxX - textWidth)); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + textPosX - cellMinX); ImGui::TextUnformatted(columnName); } From d47e0fe3378327830ac5ac822b1744d65ba7c01c Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 15 Jul 2026 23:21:10 +0200 Subject: [PATCH 35/41] Make columns in lineage table sortable --- source/Gui/EvolutionDashboardWindow.cpp | 65 ++++++++++++++++++++----- source/Gui/EvolutionDashboardWindow.h | 6 ++- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 9f37acb44..a4a61e381 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -221,6 +221,8 @@ void EvolutionDashboardWindow::initIntern() _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(); } @@ -232,6 +234,8 @@ void EvolutionDashboardWindow::shutdownIntern() 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() @@ -333,7 +337,7 @@ void EvolutionDashboardWindow::updateTableData() } _lineages.emplace_back(std::move(lineage)); } - std::sort(_lineages.begin(), _lineages.end(), [](auto const& lhs, auto const& rhs) { return lhs.currentValues.at(0) > rhs.currentValues.at(0); }); + sortLineages(); //current values for the table summary row DataPointCollection const* referenceDataPoints = liveHistory.size() >= 2 ? &liveHistory.at(referenceIndex) : nullptr; @@ -342,6 +346,29 @@ void EvolutionDashboardWindow::updateTableData() } } +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 @@ -630,24 +657,37 @@ void EvolutionDashboardWindow::processLineageTable() 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); - auto columnName = ImGui::TableGetColumnName(column); - auto textWidth = ImGui::CalcTextSize(columnName).x; - auto cellMinX = ImGui::GetCursorScreenPos().x; - auto cellMaxX = cellMinX + ImGui::GetContentRegionAvail().x; - auto visibleMinX = std::max(cellMinX, ImGui::GetWindowDrawList()->GetClipRectMin().x); - auto visibleMaxX = std::min(cellMaxX, ImGui::GetWindowDrawList()->GetClipRectMax().x); - auto textPosX = std::clamp((visibleMinX + visibleMaxX - textWidth) / 2, cellMinX, std::max(cellMinX, cellMaxX - textWidth)); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + textPosX - cellMinX); - ImGui::TextUnformatted(columnName); + 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(); } - auto drawList = ImGui::GetWindowDrawList(); - //summary row ImGui::TableNextRow(); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImColor(0.13f, 0.16f, 0.23f, 1.0f)); @@ -990,6 +1030,7 @@ void EvolutionDashboardWindow::validateAndCorrect() _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 index 05d015532..2833dfe2c 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -36,6 +36,7 @@ class EvolutionDashboardWindow : public AlienWindow void updateCellColors(); void updateDisplayData(); void updateTableData(); + void sortLineages(); void updatePlotData(); void rebuildPlotSeries(std::vector const& source); void processHeader(); @@ -72,7 +73,7 @@ class EvolutionDashboardWindow : public AlienWindow std::vector systemClockPoints; //only filled when the x-axis represents time steps }; - std::vector _lineages; //table rows from the latest sample, sorted by creature count + std::vector _lineages; //table rows from the latest sample, sorted by the selected table column std::vector _plottedLineages; LineageDisplayData _allLineages; @@ -81,6 +82,9 @@ class EvolutionDashboardWindow : public AlienWindow 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_ { From a178753a10a7533b826b6caa8c4f52ef2e37053e Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Thu, 16 Jul 2026 08:11:35 +0200 Subject: [PATCH 36/41] Colors and labels in spatial control window adapted --- source/Gui/SpatialControlWindow.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/source/Gui/SpatialControlWindow.cpp b/source/Gui/SpatialControlWindow.cpp index 31a9997cb..589949b12 100644 --- a/source/Gui/SpatialControlWindow.cpp +++ b/source/Gui/SpatialControlWindow.cpp @@ -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(); From a2ad476ded1fe9472b4cf1c6cd41b1a2bfd5f546 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Thu, 16 Jul 2026 08:59:58 +0200 Subject: [PATCH 37/41] Separate sampling of lineage statistics --- source/EngineImpl/StatisticsService.cu | 212 ++++++++++++------ source/EngineImpl/StatisticsService.cuh | 29 ++- source/EngineInterface/StatisticsHistory.h | 42 +++- source/Gui/EvolutionDashboardWindow.cpp | 159 +++++++++---- source/Gui/EvolutionDashboardWindow.h | 2 + .../PersisterInterface/SerializerService.cpp | 2 +- 6 files changed, 329 insertions(+), 117 deletions(-) diff --git a/source/EngineImpl/StatisticsService.cu b/source/EngineImpl/StatisticsService.cu index 5224dee64..295a6cc0f 100644 --- a/source/EngineImpl/StatisticsService.cu +++ b/source/EngineImpl/StatisticsService.cu @@ -1,6 +1,9 @@ #include "StatisticsService.cuh" +#include +#include + #include #include @@ -15,100 +18,169 @@ void StatisticsService::addDataPoint(StatisticsHistory& history, StatisticsEntry std::lock_guard lock(history.getMutex()); auto& historyData = history.getDataRef(); - if (!historyData.empty() && historyData.back().time > toDouble(timestep) + NEAR_ZERO) { - historyData.clear(); - _longtermTimestepDelta = DefaultTimeStepDelta; + auto time = toDouble(timestep); + if (!historyData.overall.empty() && historyData.overall.back().time > time + NEAR_ZERO) { + historyData = StatisticsHistoryData(); + _overallState = TimelineState(); + _lineageStates.clear(); } - if (!_lastTimestep || historyData.empty() || toDouble(timestep) - historyData.back().time > _longtermTimestepDelta / 100 * (_numDataPoints + 1)) { - auto newDataPoint = [&]() -> DataPointCollection { - if (!_lastTimestep && !historyData.empty()) { - - // Reuse last entry if no statistics is available - auto result = historyData.back(); - result.time = toDouble(timestep); - result.timestep = toDouble(timestep); - return result; - } else { - return StatisticsConverterService::get().convert(overallStatistics, timestep, toDouble(timestep)); - } - }(); - - _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) { - StatisticsHistoryData newData; - newData.reserve(historyData.size() / 2); - for (size_t i = 0; i < (historyData.size() - 1) / 2; ++i) { - auto interpolatedDataPoint = (historyData.at(i * 2) + historyData.at(i * 2 + 1)) / 2.0; - interpolatedDataPoint.time = historyData.at(i * 2).time; - interpolatedDataPoint.timestep = historyData.at(i * 2).timestep; - 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); - StatisticsHistoryData 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; + _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 0d65df8f6..77ef3b5fd 100644 --- a/source/EngineImpl/StatisticsService.cuh +++ b/source/EngineImpl/StatisticsService.cuh @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include @@ -21,8 +23,29 @@ public: private: static auto constexpr DefaultTimeStepDelta = 10.0; - double _longtermTimestepDelta = DefaultTimeStepDelta; - int _numDataPoints = 0; - std::optional _accumulatedDataPoint; + 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/EngineInterface/StatisticsHistory.h b/source/EngineInterface/StatisticsHistory.h index 030739951..8901ba565 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; + 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/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index a4a61e381..99e7c54a9 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -128,31 +128,44 @@ namespace return std::max(0.0, (accumValue - lastAccumValue) / delta); //negative deltas occur after accumulated statistics have been reset } - double getGlobalMetricValue(DataPointCollection const& dataPoints, DataPointCollection const* lastDataPoints, int metricIndex) + 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 dataPoints.overall.numCreatures; + return data.numCreatures; case 1: - return dataPoints.overall.averageCreatureCells; + return data.averageCreatureCells; case 2: - return dataPoints.overall.averageGenomeNodes; + return data.averageGenomeNodes; case 3: - return dataPoints.overall.creatureEnergy; + return data.creatureEnergy; case 4: - return dataPoints.overall.averageMutationRate; + return data.averageMutationRate; case 5: - return dataPoints.overall.averageGeneration; + return data.averageGeneration; case 6: case 7: { - if (!lastDataPoints) { + if (!referenceSample) { return std::numeric_limits::quiet_NaN(); } - auto deltaKSteps = (dataPoints.timestep - lastDataPoints->timestep) / 1000.0; + auto const& referenceData = overallDataOf(*referenceSample); + auto deltaKSteps = (sample.timestep - referenceSample->timestep) / 1000.0; if (metricIndex == 6) { - return calcRate(dataPoints.overall.accumCreatedCreatures, lastDataPoints->overall.accumCreatedCreatures, deltaKSteps); + return calcRate(data.accumCreatedCreatures, referenceData.accumCreatedCreatures, deltaKSteps); } else { - return calcRate(dataPoints.overall.accumMutations, lastDataPoints->overall.accumMutations, deltaKSteps); + return calcRate(data.accumMutations, referenceData.accumMutations, deltaKSteps); } } default: @@ -184,6 +197,12 @@ namespace } } + 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); @@ -207,6 +226,49 @@ namespace } return result; } + + //builds the x points and metric series of one timeline; reference samples for the rate metrics are selected + //via a trailing ~30s window: the live buffer carries the time since simulation start while the long-term + //history only provides the system clock + template + void buildPlotSeries( + Target& target, + std::vector const& source, + size_t firstIndex, + bool useTimeAsX, + bool useSystemClockForRateWindow, + MetricEvaluator const& evaluateMetric) + { + auto getX = [&](Sample const& sample) { return useTimeAsX ? sample.time : sample.timestep; }; + auto getRateWindowClock = [&](Sample const& sample) { return useSystemClockForRateWindow ? sample.systemClock : sample.time; }; + + 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); + auto rateWindowClock = getRateWindowClock(sample); + while (referenceIndex + 1 < sampleIndex && getRateWindowClock(source.at(referenceIndex + 1)) + RateAveragingInterval <= rateWindowClock) { + ++referenceIndex; + } + //rates over references younger than the averaging interval would produce spikes at the left plot border; NaN suppresses those samples; + //at the start of the timeline the oldest sample serves as reference once it is at least MinRateTimesteps old (like in the table) + auto const& referenceSample = source.at(referenceIndex); + auto referenceIsOldEnough = referenceIndex < sampleIndex + && (getRateWindowClock(referenceSample) + RateAveragingInterval <= rateWindowClock + || (referenceIndex == 0 && referenceSample.timestep + MinRateTimesteps <= 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() @@ -342,7 +404,7 @@ void EvolutionDashboardWindow::updateTableData() //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) = getGlobalMetricValue(lastSample, referenceDataPoints, i); + _allLineages.currentValues.at(i) = getOverallMetricValue(lastSample, referenceDataPoints, i); } } @@ -394,10 +456,6 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector::lowest(); @@ -418,31 +476,7 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector); //per-lineage series for the selected lineages _plottedLineages.clear(); @@ -464,7 +498,7 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vectorcolorBitset); - auto windowClock = getRateWindowClock(sample); + auto windowClock = sample.time; while (rateReferenceIndex + 1 < rateReferences.size() && rateReferences.at(rateReferenceIndex + 1).windowClock + RateAveragingInterval <= windowClock) { ++rateReferenceIndex; @@ -498,6 +532,47 @@ void EvolutionDashboardWindow::rebuildPlotSeries(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, true, 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, true, 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(); diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index 2833dfe2c..7fb3bc242 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -39,6 +39,7 @@ class EvolutionDashboardWindow : public AlienWindow void sortLineages(); void updatePlotData(); void rebuildPlotSeries(std::vector const& source); + void rebuildPlotSeries(StatisticsHistoryData const& source); void processHeader(); void processCard( std::string const& label, @@ -107,6 +108,7 @@ class EvolutionDashboardWindow : public AlienWindow 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; }; diff --git a/source/PersisterInterface/SerializerService.cpp b/source/PersisterInterface/SerializerService.cpp index 03fff405c..944779504 100644 --- a/source/PersisterInterface/SerializerService.cpp +++ b/source/PersisterInterface/SerializerService.cpp @@ -2257,7 +2257,7 @@ void SerializerService::serializeStatistics(StatisticsHistoryData const& statist void SerializerService::deserializeStatistics(StatisticsHistoryData& statistics, std::istream& stream) const { - statistics.clear(); + statistics = StatisticsHistoryData(); } bool SerializerService::wrapGenome(Desc& output, GenomeDesc const& input) const From 65ff4e60ef7761f434f2366686d36c24b6c1aff2 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Thu, 16 Jul 2026 09:11:46 +0200 Subject: [PATCH 38/41] Comment added --- source/EngineInterface/StatisticsHistory.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/EngineInterface/StatisticsHistory.h b/source/EngineInterface/StatisticsHistory.h index 8901ba565..c240b25fe 100644 --- a/source/EngineInterface/StatisticsHistory.h +++ b/source/EngineInterface/StatisticsHistory.h @@ -10,7 +10,7 @@ template struct TimedSample { - double time = 0; + double time = 0; // Time since simulation start double timestep = 0; double systemClock = 0; DataPoint data; From 1d6b2c6b5fc22b770d07deefac05a4060d507b01 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Thu, 16 Jul 2026 19:03:22 +0200 Subject: [PATCH 39/41] Improve dashboard visualization, part 4 --- .../EngineInterface/DataPointCollection.cpp | 2 + source/EngineInterface/DataPointCollection.h | 1 + .../StatisticsConverterService.cpp | 1 + source/EngineInterface/StatisticsEntry.h | 1 + source/EngineKernels/SimulationStatistics.cuh | 13 ++ source/EngineKernels/StatisticsKernels.cu | 1 + .../EngineTests/EvolutionStatisticsTests.cpp | 13 ++ source/Gui/EvolutionDashboardWindow.cpp | 135 ++++++++++++++---- source/Gui/EvolutionDashboardWindow.h | 7 +- source/Gui/GeneEditorWidget.cpp | 2 +- source/Gui/GenomeEditorWindow.cpp | 5 +- source/Gui/GenomeTabEditData.h | 9 +- source/Gui/GenomeTabWidget.cpp | 16 ++- source/Gui/GenomeTabWidget.h | 3 + source/Gui/SpatialControlWindow.cpp | 2 +- 15 files changed, 172 insertions(+), 39 deletions(-) diff --git a/source/EngineInterface/DataPointCollection.cpp b/source/EngineInterface/DataPointCollection.cpp index b0d659c55..654f7b2d3 100644 --- a/source/EngineInterface/DataPointCollection.cpp +++ b/source/EngineInterface/DataPointCollection.cpp @@ -42,6 +42,7 @@ LineageDataPoint LineageDataPoint::operator+(LineageDataPoint const& other) cons { 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; @@ -58,6 +59,7 @@ 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; diff --git a/source/EngineInterface/DataPointCollection.h b/source/EngineInterface/DataPointCollection.h index 99ee49971..88d42118e 100644 --- a/source/EngineInterface/DataPointCollection.h +++ b/source/EngineInterface/DataPointCollection.h @@ -27,6 +27,7 @@ struct OverallDataPoint 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; diff --git a/source/EngineInterface/StatisticsConverterService.cpp b/source/EngineInterface/StatisticsConverterService.cpp index b21d70974..8a2f527e3 100644 --- a/source/EngineInterface/StatisticsConverterService.cpp +++ b/source/EngineInterface/StatisticsConverterService.cpp @@ -35,6 +35,7 @@ DataPointCollection StatisticsConverterService::convert( 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); diff --git a/source/EngineInterface/StatisticsEntry.h b/source/EngineInterface/StatisticsEntry.h index 11c75ea3e..c3581f615 100644 --- a/source/EngineInterface/StatisticsEntry.h +++ b/source/EngineInterface/StatisticsEntry.h @@ -34,6 +34,7 @@ struct LineageStatisticsEntry 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 diff --git a/source/EngineKernels/SimulationStatistics.cuh b/source/EngineKernels/SimulationStatistics.cuh index 76243d231..b0256d5be 100644 --- a/source/EngineKernels/SimulationStatistics.cuh +++ b/source/EngineKernels/SimulationStatistics.cuh @@ -121,6 +121,8 @@ public: slot.sumGenomeNodes = 0; slot.sumMutationRates = 0; slot.sumCreatureEnergy = 0; + slot.maxCreatureGeneration = 0; + slot.representativeCellId = 0; } __inline__ __device__ void resetCompactLineageCounter() { _lineageAccumulatorMapControl->numCompactEntries = 0; } @@ -159,6 +161,14 @@ public: atomicAdd(&slot.numCreatures, 1u); atomicAdd(&slot.sumCreatureCells, toFloat(numCells)); atomicAdd(&slot.sumCreatureGenerations, toFloat(generation)); + atomicMax(&slot.maxCreatureGeneration, generation); + } + __inline__ __device__ void updateLineageRepresentativeCell(int slotIndex, uint32_t generation, uint64_t cellId) + { + auto& slot = _lineageMap[slotIndex]; + if (generation == slot.maxCreatureGeneration) { + atomicCAS(reinterpret_cast(&slot.representativeCellId), 0ull, static_cast(cellId)); + } } __inline__ __device__ void addLineageGenomeData(int slotIndex, float numNodes, float meanMutationRate, uint32_t nodeColorBitset) { @@ -190,6 +200,7 @@ public: 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); @@ -262,6 +273,8 @@ private: float sumGenomeNodes; float sumMutationRates; float sumCreatureEnergy; + uint32_t maxCreatureGeneration; + uint64_t representativeCellId; // Cell of a creature with the highest generation; 0 = not set }; struct LineageAccumulatorMapEntry { diff --git a/source/EngineKernels/StatisticsKernels.cu b/source/EngineKernels/StatisticsKernels.cu index c4ca58558..f890f50f9 100644 --- a/source/EngineKernels/StatisticsKernels.cu +++ b/source/EngineKernels/StatisticsKernels.cu @@ -150,6 +150,7 @@ __global__ void cudaUpdateEvolutionStatistics_substep3(SimulationData data, Simu statistics.addCreatureEnergy(energy); if (slotIndex >= 0) { statistics.addLineageEnergy(slotIndex, energy); + statistics.updateLineageRepresentativeCell(slotIndex, creature->generation, object->id); } } } diff --git a/source/EngineTests/EvolutionStatisticsTests.cpp b/source/EngineTests/EvolutionStatisticsTests.cpp index 10892671e..a3cfcbd17 100644 --- a/source/EngineTests/EvolutionStatisticsTests.cpp +++ b/source/EngineTests/EvolutionStatisticsTests.cpp @@ -53,6 +53,19 @@ TEST_F(EvolutionStatisticsTests, basicCounts) 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(); diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 99e7c54a9..2ffb74f3e 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -16,10 +16,13 @@ #include #include +#include #include #include #include "AlienGui.h" +#include "GenomeEditorWindow.h" +#include "OverlayController.h" #include "StyleRepository.h" namespace @@ -385,6 +388,7 @@ void EvolutionDashboardWindow::updateTableData() 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 @@ -805,8 +809,14 @@ void EvolutionDashboardWindow::processLineageTable() ImGui::PushID(toInt(lineage.id)); ImGui::TableNextRow(); 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)) { + if (ImGui::Selectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap)) { if (ImGui::GetIO().KeyCtrl) { if (selected) { _selectedLineageIds.erase(lineage.id); @@ -818,12 +828,26 @@ void EvolutionDashboardWindow::processLineageTable() _selectedLineageIds.insert(lineage.id); } } - ImGui::SameLine(0, 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)); for (int i = 0; i < NumMetrics; ++i) { ImGui::TableSetColumnIndex(i + 1); + if (i == 0) { + //genome button on the left of the creatures column, shrunk like the pin icon and bottom-aligned within the row + auto cellPos = ImGui::GetCursorPos(); + auto lineHeight = ImGui::GetTextLineHeight(); + ImGui::SetWindowFontScale(0.75f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {scale(3.0f), 0.0f}); + ImGui::SetCursorPosY(cellPos.y + 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::SetCursorPos(cellPos); + } rightAlignedText(formatMetricValue(lineage.currentValues.at(i), Metrics[i].tableDecimals)); } ImGui::PopID(); @@ -832,6 +856,19 @@ void EvolutionDashboardWindow::processLineageTable() } } +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(); @@ -840,10 +877,28 @@ void EvolutionDashboardWindow::processTimelineSection() //x-axis shows a gap on the left for one frame because the series are still trimmed to the old horizon updatePlotData(); - if (ImGui::BeginChild("##timelinePlots", {0, 0})) { - processTimelinePlots(); + 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 + auto pinnedTimeAxis = _timelineMode == TimelineMode_EntireHistory; + auto scrollbarWidth = 0.0f; + if (ImGui::BeginChild("##timelinePlots", {0, pinnedTimeAxis ? -scale(TimeAxisExtraHeight) : 0.0f})) { + processTimelinePlots(plottedLineages); + if (ImGui::GetScrollMaxY() > 0.0f) { + scrollbarWidth = ImGui::GetStyle().ScrollbarSize; + } } ImGui::EndChild(); + if (pinnedTimeAxis) { + processTimeAxis(plottedLineages, scrollbarWidth); + } } void EvolutionDashboardWindow::processTimelineHeader() @@ -902,26 +957,16 @@ void EvolutionDashboardWindow::processTimelineHeader() ImGui::Spacing(); } -void EvolutionDashboardWindow::processTimelinePlots() +void EvolutionDashboardWindow::processTimelinePlots(std::vector const& plottedLineages) { - std::vector plottedLineages; - if (_selectedLineageIds.empty()) { - plottedLineages.emplace_back(&_allLineages); - } else { - for (auto const& lineage : _plottedLineages) { - plottedLineages.emplace_back(&lineage); - } - } - //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) { - auto showTimeAxis = i == NumMetrics - 1 && _timelineMode == TimelineMode_EntireHistory; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - processTimelinePlot(plottedLineages, i, showTimeAxis); + processTimelinePlot(plottedLineages, i); ImGui::TableSetColumnIndex(1); AlienGui::Text(Metrics[i].plotName); @@ -940,7 +985,7 @@ void EvolutionDashboardWindow::processTimelinePlots() } } -void EvolutionDashboardWindow::processTimelinePlot(std::vector const& plottedLineages, int metricIndex, bool showTimeAxis) +void EvolutionDashboardWindow::processTimelinePlot(std::vector const& plottedLineages, int metricIndex) { auto upperBound = 0.0; auto minTime = std::numeric_limits::max(); @@ -976,13 +1021,9 @@ void EvolutionDashboardWindow::processTimelinePlot(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, diff --git a/source/Gui/EvolutionDashboardWindow.h b/source/Gui/EvolutionDashboardWindow.h index 7fb3bc242..ec36b4f8e 100644 --- a/source/Gui/EvolutionDashboardWindow.h +++ b/source/Gui/EvolutionDashboardWindow.h @@ -49,10 +49,12 @@ class EvolutionDashboardWindow : public AlienWindow float height); void processFilterBar(); void processLineageTable(); + void onOpenRepresentativeGenome(uint64_t cellId); void processTimelineSection(); void processTimelineHeader(); - void processTimelinePlots(); - void processTimelinePlot(std::vector const& plottedLineages, int metricIndex, bool showTimeAxis); + 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, @@ -68,6 +70,7 @@ class EvolutionDashboardWindow : public AlienWindow { 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 diff --git a/source/Gui/GeneEditorWidget.cpp b/source/Gui/GeneEditorWidget.cpp index 955487b96..b287a9bb2 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 90df13423..fa1bd86c3 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 32c8cd574..f543a4448 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 7f5ed87d2..5590c2923 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 c77d9053a..bb2d88a05 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/SpatialControlWindow.cpp b/source/Gui/SpatialControlWindow.cpp index 589949b12..9bbcd2d4e 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() From 38e0545d4e70ecb4edfc6b28b7b7063438e035ab Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Thu, 16 Jul 2026 21:16:18 +0200 Subject: [PATCH 40/41] Fix tooltip in dashboard --- source/Gui/EvolutionDashboardWindow.cpp | 37 ++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/source/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index 2ffb74f3e..b8e188af7 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -808,8 +808,25 @@ void EvolutionDashboardWindow::processLineageTable() } ImGui::PushID(toInt(lineage.id)); ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); + //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)); @@ -830,24 +847,6 @@ void EvolutionDashboardWindow::processLineageTable() } for (int i = 0; i < NumMetrics; ++i) { ImGui::TableSetColumnIndex(i + 1); - if (i == 0) { - //genome button on the left of the creatures column, shrunk like the pin icon and bottom-aligned within the row - auto cellPos = ImGui::GetCursorPos(); - auto lineHeight = ImGui::GetTextLineHeight(); - ImGui::SetWindowFontScale(0.75f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {scale(3.0f), 0.0f}); - ImGui::SetCursorPosY(cellPos.y + 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::SetCursorPos(cellPos); - } rightAlignedText(formatMetricValue(lineage.currentValues.at(i), Metrics[i].tableDecimals)); } ImGui::PopID(); From bc22e142649f8873e74f9988c3411e726b3ba23d Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Thu, 16 Jul 2026 22:57:42 +0200 Subject: [PATCH 41/41] Implement serialization of new statistics + minor dashboard corrections --- source/Cli/Main.cpp | 2 +- source/Gui/EvolutionDashboardWindow.cpp | 62 ++-- .../PersisterInterface/SerializerService.cpp | 306 +++++++++++++++++- .../PersisterTests/SerializerServiceTest.cpp | 135 ++++++++ 4 files changed, 460 insertions(+), 45 deletions(-) diff --git a/source/Cli/Main.cpp b/source/Cli/Main.cpp index d5a25ec0e..50f37d773 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/Gui/EvolutionDashboardWindow.cpp b/source/Gui/EvolutionDashboardWindow.cpp index b8e188af7..1d7fca419 100644 --- a/source/Gui/EvolutionDashboardWindow.cpp +++ b/source/Gui/EvolutionDashboardWindow.cpp @@ -27,9 +27,8 @@ namespace { - auto constexpr LiveStatisticsDeltaTime = 50; //in millisec - auto constexpr RateAveragingInterval = 30.0; //in seconds - auto constexpr MinRateTimesteps = 1000.0; //at the start of the history, rates are already shown once this many time steps are covered + 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; @@ -217,12 +216,12 @@ namespace return snprintf(buff, size, "%s", StringHelper::formatInThousands(value).c_str()); } - //most recent sample that is at least RateAveragingInterval older; falls back to the oldest sample - size_t findRateReferenceIndex(std::vector const& history, double time) + //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).time + RateAveragingInterval > time) { + if (history.at(i).timestep + RateAveragingTimesteps > timestep) { break; } result = i; @@ -231,19 +230,11 @@ namespace } //builds the x points and metric series of one timeline; reference samples for the rate metrics are selected - //via a trailing ~30s window: the live buffer carries the time since simulation start while the long-term - //history only provides the system clock + //via a trailing window of RateAveragingTimesteps time steps template - void buildPlotSeries( - Target& target, - std::vector const& source, - size_t firstIndex, - bool useTimeAsX, - bool useSystemClockForRateWindow, - MetricEvaluator const& evaluateMetric) + 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; }; - auto getRateWindowClock = [&](Sample const& sample) { return useSystemClockForRateWindow ? sample.systemClock : sample.time; }; target.series = {}; target.timePoints.clear(); @@ -252,16 +243,12 @@ namespace 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); - auto rateWindowClock = getRateWindowClock(sample); - while (referenceIndex + 1 < sampleIndex && getRateWindowClock(source.at(referenceIndex + 1)) + RateAveragingInterval <= rateWindowClock) { + while (referenceIndex + 1 < sampleIndex && source.at(referenceIndex + 1).timestep + RateAveragingTimesteps <= sample.timestep) { ++referenceIndex; } - //rates over references younger than the averaging interval would produce spikes at the left plot border; NaN suppresses those samples; - //at the start of the timeline the oldest sample serves as reference once it is at least MinRateTimesteps old (like in the table) + //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 - && (getRateWindowClock(referenceSample) + RateAveragingInterval <= rateWindowClock - || (referenceIndex == 0 && referenceSample.timestep + MinRateTimesteps <= sample.timestep)); + auto referenceIsOldEnough = referenceIndex < sampleIndex && referenceSample.timestep + RateAveragingTimesteps <= sample.timestep; auto const* reference = referenceIsOldEnough ? &referenceSample : nullptr; target.timePoints.emplace_back(getX(sample)); if (!useTimeAsX) { @@ -383,7 +370,7 @@ void EvolutionDashboardWindow::updateTableData() return; } auto const& lastSample = liveHistory.back(); - auto referenceIndex = findRateReferenceIndex(liveHistory, lastSample.time); + auto referenceIndex = findRateReferenceIndex(liveHistory, lastSample.timestep); for (auto const& [lineageId, entry] : lastSample.lineages) { LineageDisplayData lineage; lineage.id = toInt(lineageId); @@ -480,7 +467,7 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector); + buildPlotSeries(_allLineages, source, firstIndex, useTimeAsX, getOverallMetricValue); //per-lineage series for the selected lineages _plottedLineages.clear(); @@ -489,7 +476,6 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vectorcolorBitset); - auto windowClock = sample.time; while (rateReferenceIndex + 1 < rateReferences.size() - && rateReferences.at(rateReferenceIndex + 1).windowClock + RateAveragingInterval <= windowClock) { + && rateReferences.at(rateReferenceIndex + 1).timestep + RateAveragingTimesteps <= sample.timestep) { ++rateReferenceIndex; } if (sampleIndex >= firstIndex) { @@ -512,18 +497,16 @@ void EvolutionDashboardWindow::rebuildPlotSeries(std::vector); + buildPlotSeries(_allLineages, source.overall, 0, false, getOverallMetricValue); //per-lineage series for the selected lineages _plottedLineages.clear(); @@ -564,7 +547,7 @@ void EvolutionDashboardWindow::rebuildPlotSeries(StatisticsHistoryData const& so 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, true, getLineageSampleMetricValue); + buildPlotSeries(lineage, timelineIt->second, 0, false, getLineageSampleMetricValue); } if (lineage.colorBitset == 0) { for (auto const& tableLineage : _lineages) { @@ -885,10 +868,13 @@ void EvolutionDashboardWindow::processTimelineSection() } } - //in entire-history mode the time axis is pinned below the scrolling plot area so it stays visible + //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 ? -scale(TimeAxisExtraHeight) : 0.0f})) { + if (ImGui::BeginChild("##timelinePlots", {0, pinnedTimeAxis ? -timeAxisHeight : 0.0f})) { processTimelinePlots(plottedLineages); if (ImGui::GetScrollMaxY() > 0.0f) { scrollbarWidth = ImGui::GetStyle().ScrollbarSize; diff --git a/source/PersisterInterface/SerializerService.cpp b/source/PersisterInterface/SerializerService.cpp index 944779504..157bb14eb 100644 --- a/source/PersisterInterface/SerializerService.cpp +++ b/source/PersisterInterface/SerializerService.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -1898,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); @@ -1934,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; @@ -1966,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; @@ -1974,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; @@ -2251,13 +2255,303 @@ void SerializerService::deserializeSimulationParameters(SimulationParameters& pa parameters = SettingsParserService::get().decodeSimulationParameters(tree); } +/************************************************************************/ +/* Statistics history */ +/************************************************************************/ +namespace +{ + 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 + { + OverallTimeline overall; + std::unordered_map lineages; + }; + + struct OverallColumn + { + 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}, + }; + + struct LineageColumn + { + 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}, + }; + + template + void extractTimingColumns(Timeline& timeline, std::vector const& samples) + { + 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); + } + } + + template + std::vector createSamplesWithTiming(Timeline const& timeline) + { + 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; + } + for (auto&& [sample, value] : std::views::zip(result, timeline.systemClock)) { + sample.systemClock = value; + } + return result; + } + + template + void extractDataColumns(Timeline& timeline, std::vector const& samples, Columns const& columns) + { + 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); + } + } + } + + template + void applyDataColumns(std::vector& samples, Timeline const& timeline, Columns const& columns) + { + for (auto const& [id, column, field] : columns) { + for (auto&& [sample, value] : std::views::zip(samples, timeline.*column)) { + sample.data.*field = value; + } + } + } + + OverallTimeline toTimeline(std::vector const& samples) + { + OverallTimeline result; + extractTimingColumns(result, samples); + extractDataColumns(result, samples, OverallColumns); + return result; + } + + std::vector toSamples(OverallTimeline const& timeline) + { + 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::vector toSamples(LineageTimeline const& timeline) + { + auto result = createSamplesWithTiming(timeline); + applyDataColumns(result, timeline, LineageColumns); + for (auto&& [sample, value] : std::views::zip(result, timeline.colorBitset)) { + sample.data.colorBitset = value; + } + for (auto&& [sample, value] : std::views::zip(result, timeline.representativeCellId)) { + sample.data.representativeCellId = value; + } + return result; + } +} + +namespace cereal +{ + 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); + } + } + SPLIT_SERIALIZATION(OverallTimeline) + + 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); + } + } + SPLIT_SERIALIZATION(LineageTimeline) + + 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::serializeStatistics(StatisticsHistoryData const& statistics, std::ostream& stream) const { + StatisticsTimelines timelines; + timelines.overall = toTimeline(statistics.overall); + for (auto const& [lineageId, samples] : statistics.lineages) { + timelines.lineages.emplace(lineageId, toTimeline(samples)); + } + + zstr::ostream compressedStream(stream); + cereal::PortableBinaryOutputArchive archive(compressedStream); + archive(Const::ProgramVersion); + archive(timelines); + compressedStream.flush(); } 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); + + std::string version; + archive(version); + if (!VersionParserService::get().isVersionValid(version) || VersionParserService::get().isVersionOutdated(version)) { + return; + } + + StatisticsTimelines timelines; + archive(timelines); + + statistics.overall = toSamples(timelines.overall); + for (auto const& [lineageId, timeline] : timelines.lineages) { + statistics.lineages.emplace(lineageId, toSamples(timeline)); + } + } catch (...) { + statistics = StatisticsHistoryData(); + } } bool SerializerService::wrapGenome(Desc& output, GenomeDesc const& input) const diff --git a/source/PersisterTests/SerializerServiceTest.cpp b/source/PersisterTests/SerializerServiceTest.cpp index 3dc76539e..fe9bd7128 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;