diff --git a/tests/artifacts/adaptive_quality_results.json b/tests/artifacts/adaptive_quality_results.json new file mode 100644 index 0000000..242d59e --- /dev/null +++ b/tests/artifacts/adaptive_quality_results.json @@ -0,0 +1,49 @@ +{ + "date": "2026-10-08", + "machine": "Apple M4 Pro", + "godot": "4.7.2.stable.official.ed1daf0bf", + "renderer": "headless", + "delta_seconds": 0.2, + "profile_enabled": true, + "ui": false, + "adaptation": { + "controller": "runtime-only exponential moving average of full-cycle CPU time; 0.8 previous + 0.2 latest", + "quality_thresholds_ms": { + "quality_1": 200, + "quality_2": 220, + "quality_3": 300, + "quality_2_to_3_when_over_ms": 200, + "recovery_below_ms": 140, + "recovery_cycles": 30 + }, + "quality_effects": { + "quality_1": "one-transition civilization approximation, at most 3 stellar/life generations per cycle, temporal grouping x2", + "quality_2": "same civilization approximation, at most 1 generation per cycle, temporal grouping x4", + "quality_3": "same civilization approximation, at most 1 generation per cycle, temporal grouping x8, alternate halves of batch types each cycle" + } + }, + "inputs": { + "all_objects_speed6.adventure.json": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67", + "many_objects_fragmented_stars_cycle.adventure.json": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a" + }, + "runs": [ + {"speed": 1, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 165.03665, "min_ms": 151.514, "max_ms": 170.171, "cycles_per_second": 6.05926, "game_years": 12, "validation": "OK"}, + {"speed": 1, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 128.463817, "min_ms": 124.475, "max_ms": 144.05, "cycles_per_second": 7.784293, "game_years": 12, "validation": "OK"}, + {"speed": 2, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 181.431217, "min_ms": 171.92, "max_ms": 196.005, "cycles_per_second": 5.511731, "game_years": 120, "validation": "OK"}, + {"speed": 2, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 131.78055, "min_ms": 126.963, "max_ms": 148.01, "cycles_per_second": 7.588373, "game_years": 120, "validation": "OK"}, + {"speed": 3, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 175.235683, "min_ms": 166.031, "max_ms": 203.456, "cycles_per_second": 5.7066, "game_years": 1200, "validation": "OK"}, + {"speed": 3, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 145.013367, "min_ms": 135.062, "max_ms": 162.768, "cycles_per_second": 6.895916, "game_years": 1200, "validation": "OK"}, + {"speed": 4, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 159.705883, "min_ms": 150.404, "max_ms": 221.101, "cycles_per_second": 6.26151, "game_years": 12000, "validation": "OK"}, + {"speed": 4, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 177.064483, "min_ms": 170.624, "max_ms": 189.729, "cycles_per_second": 5.64766, "game_years": 12000, "validation": "OK"}, + {"speed": 5, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 174.857517, "min_ms": 158.809, "max_ms": 270.078, "cycles_per_second": 5.718942, "game_years": 120000, "validation": "OK"}, + {"speed": 5, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 185.601233, "min_ms": 131.409, "max_ms": 273.8, "cycles_per_second": 5.387895, "game_years": 120000, "validation": "OK"}, + {"speed": 6, "save": "all_objects_speed6.adventure.json", "cycles": 60, "mean_ms": 190.570967, "min_ms": 159.38, "max_ms": 345.149, "cycles_per_second": 5.247389, "game_years": 1200000, "validation": "OK"}, + {"speed": 6, "save": "many_objects_fragmented_stars_cycle.adventure.json", "cycles": 60, "mean_ms": 184.426, "min_ms": 149.645, "max_ms": 366.767, "cycles_per_second": 5.422229, "game_years": 1200000, "validation": "OK"} + ], + "notes": [ + "Each run begins in exact mode and adapts during its 60-cycle series.", + "The mean meets 5 complete cycles/s at all six speeds in these synthetic diagnostic saves on this computer; maxima show occasional cycles still exceed 200 ms at speeds 3-6.", + "No Android device or UI performance was measured.", + "Diagnostic save inputs were not modified." + ] +} diff --git a/tests/artifacts/all_objects_speed6_results.md b/tests/artifacts/all_objects_speed6_results.md index 5ebfa0c..b25bbe3 100644 --- a/tests/artifacts/all_objects_speed6_results.md +++ b/tests/artifacts/all_objects_speed6_results.md @@ -458,3 +458,41 @@ godot --headless --path . --script res://tests/benchmark_civilization_flows.gd - Полные состояния до и после совпали после пяти последовательных интервалов на каждом сохранении (включая дробные интервалы). Тест оптимизаций прошёл: 1889 проверок без ошибок. Цель пяти полных циклов в секунду ещё не достигнута: тяжёлому циклу требуется ускорение примерно в 2,05 раза, чтобы уложиться в 200 мс. Полные профили и SHA-256 входов: `civilization_flow_prepare_step3_results.json`. + +## Проверка оптимизации частичного изъятия партий — 2026-10-08 + +Перенос связанного содержимого уже агрегируется по типам внутри поколения. Проверен вариант частичного изъятия, исключающий промежуточное сложение квоты с дробным переносом и последующее вычитание целой части. Возрастные интервалы и порядок поколений сохранялись. + +Парный замер на тех же сохранениях: скорость 6, интервал 0,2 секунды, CPU-профиль, без UI; до → после → после → до, по 20 циклов на прогон (40 циклов на версию и сохранение). + +| Сохранение | До, мс/цикл | После, мс/цикл | +|---|---:|---:| +| Все объекты | 350,516 | 349,835 | +| Много объектов и партий | 404,255 | 404,390 | + +Изъятие партий на тяжёлом сохранении: 40,919 → 40,837 мс/цикл. Изменения полного цикла меньше 0,2%; подтверждённого выигрыша нет. Вариант **откачен**, код симуляции оставлен прежним. Полные состояния совпали в 10 последовательных сравнениях; границы партий — 117 проверок без ошибок. Входные файлы не изменены. + +Результаты отклонённого варианта: `batch_withdrawal_step4_results.json`. Этот замер не доказывает достижение пяти полных циклов в секунду. + +## Адаптивное упрощение симуляции — 2026-10-08 + +Добавлен runtime-контроллер качества, который измеряет сглаженное CPU-время полного цикла. Он повышает уровень упрощения при превышении бюджета 200 мс и снимает его после 30 циклов с запасом. Режим не сохраняется в файл: после загрузки точность постепенно подстраивается заново. + +При нагрузке цивилизационный поток заменяется приближением не более чем с одним переходом на стадию за интервал. На наиболее грубом уровне типы возрастных партий делятся на две чередующиеся группы: каждая обрабатывается раз в два цикла. Временные группы при этом расширяются. Это сохраняет количества, но сдвигает сроки отдельных переходов и исключает несколько последовательных переходов цивилизации за один интервал. + +Проверены два синтетических сохранения на Apple M4 Pro, Godot 4.7.2, headless, с CPU-профилем и интервалом 0,2 секунды. Каждый замер — 60 последовательных полных циклов; каждый запуск начинается в точном режиме и адаптируется по ходу серии. + +| Скорость | Все объекты, мс/цикл (циклов/с) | Много объектов и партий, мс/цикл (циклов/с) | +|---:|---:|---:| +| 1 | 165,0 (6,06) | 128,5 (7,78) | +| 2 | 181,4 (5,51) | 131,8 (7,59) | +| 3 | 175,2 (5,71) | 145,0 (6,90) | +| 4 | 159,7 (6,26) | 177,1 (5,65) | +| 5 | 174,9 (5,72) | 185,6 (5,39) | +| 6 | 190,6 (5,25) | 184,4 (5,42) | + +Среднее время на всех прогонах ниже 200 мс. Отдельные циклы всё ещё бывают длиннее: максимумы составили 203–367 мс на скоростях 3–6, включая первые циклы до срабатывания адаптации. Значит, замеры подтверждают требуемую среднюю пропускную способность на этом компьютере, но не гарантируют отсутствие кратковременных задержек. Android и работа с UI отдельно не проверялись. + +Проверки: адаптивное качество — 18, хронология поколений — 28, границы партий — 117, оптимизации симуляции — 1889, планировщик кадров — 39; ошибок нет. Все 12 конечных диагностических состояний прошли валидацию. Хеши входных файлов: `44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67` и `1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a`. + +Детали уровней, минимумы/максимумы и воспроизводимые результаты: `adaptive_quality_results.json`. Штатный benchmark принимает параметр `speed=1..6`. diff --git a/tests/artifacts/batch_withdrawal_step4_results.json b/tests/artifacts/batch_withdrawal_step4_results.json new file mode 100644 index 0000000..264ee1b --- /dev/null +++ b/tests/artifacts/batch_withdrawal_step4_results.json @@ -0,0 +1,646 @@ +{ + "retained": false, + "conclusion": "No measurable full-cycle improvement; implementation reverted", + "delta_seconds": 0.2, + "exact_state_differential": true, + "input_sha256": { + "all_objects_speed6": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67", + "many_objects_fragmented_stars_cycle": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a" + }, + "order": "before-after-after-before", + "runs": [ + { + "cycles": 20, + "max_ms": 382.054, + "mean_ms": 347.474, + "min_ms": 291.952, + "model": "before", + "profile": { + "assign_homes": [ + 23851, + 80 + ], + "civilization_flow_capacity": [ + 121403, + 20 + ], + "civilization_flow_colonies": [ + 52415, + 20 + ], + "civilization_flow_finish": [ + 135271, + 20 + ], + "civilization_flow_ledger": [ + 880672, + 20 + ], + "civilization_flow_plan": [ + 1718363, + 20 + ], + "civilization_operator_build": [ + 61955, + 20 + ], + "civilization_operator_solve": [ + 673740, + 860 + ], + "enforce_capacity": [ + 316094, + 80 + ], + "generation_products": [ + 190666, + 40 + ], + "generation_withdrawal": [ + 545822, + 60 + ], + "generations": [ + 2727124, + 20 + ], + "initialize": [ + 113986, + 20 + ], + "newborn_batches": [ + 121039, + 3016 + ], + "physical_amounts_build": [ + 285729, + 200 + ], + "production": [ + 373910, + 20 + ], + "trends": [ + 358046, + 20 + ] + }, + "run": 1, + "save": "all_objects_speed6" + }, + { + "cycles": 20, + "max_ms": 382.156, + "mean_ms": 349.53305, + "min_ms": 297.266, + "model": "after", + "profile": { + "assign_homes": [ + 23877, + 80 + ], + "civilization_flow_capacity": [ + 121854, + 20 + ], + "civilization_flow_colonies": [ + 53021, + 20 + ], + "civilization_flow_finish": [ + 137725, + 20 + ], + "civilization_flow_ledger": [ + 883114, + 20 + ], + "civilization_flow_plan": [ + 1733356, + 20 + ], + "civilization_operator_build": [ + 62368, + 20 + ], + "civilization_operator_solve": [ + 680005, + 860 + ], + "enforce_capacity": [ + 319290, + 80 + ], + "generation_products": [ + 192497, + 40 + ], + "generation_withdrawal": [ + 548252, + 60 + ], + "generations": [ + 2737148, + 20 + ], + "initialize": [ + 115930, + 20 + ], + "newborn_batches": [ + 121459, + 3016 + ], + "physical_amounts_build": [ + 287621, + 200 + ], + "production": [ + 376359, + 20 + ], + "trends": [ + 359971, + 20 + ] + }, + "run": 1, + "save": "all_objects_speed6" + }, + { + "cycles": 20, + "max_ms": 387.438, + "mean_ms": 350.1367, + "min_ms": 298.254, + "model": "after", + "profile": { + "assign_homes": [ + 24181, + 80 + ], + "civilization_flow_capacity": [ + 122008, + 20 + ], + "civilization_flow_colonies": [ + 53315, + 20 + ], + "civilization_flow_finish": [ + 137169, + 20 + ], + "civilization_flow_ledger": [ + 885851, + 20 + ], + "civilization_flow_plan": [ + 1736978, + 20 + ], + "civilization_operator_build": [ + 62264, + 20 + ], + "civilization_operator_solve": [ + 680485, + 860 + ], + "enforce_capacity": [ + 319558, + 80 + ], + "generation_products": [ + 193735, + 40 + ], + "generation_withdrawal": [ + 547473, + 60 + ], + "generations": [ + 2743162, + 20 + ], + "initialize": [ + 115720, + 20 + ], + "newborn_batches": [ + 121401, + 3016 + ], + "physical_amounts_build": [ + 287832, + 200 + ], + "production": [ + 376295, + 20 + ], + "trends": [ + 360758, + 20 + ] + }, + "run": 2, + "save": "all_objects_speed6" + }, + { + "cycles": 20, + "max_ms": 391.136, + "mean_ms": 353.55755, + "min_ms": 295.752, + "model": "before", + "profile": { + "assign_homes": [ + 23999, + 80 + ], + "civilization_flow_capacity": [ + 125136, + 20 + ], + "civilization_flow_colonies": [ + 53681, + 20 + ], + "civilization_flow_finish": [ + 137614, + 20 + ], + "civilization_flow_ledger": [ + 893234, + 20 + ], + "civilization_flow_plan": [ + 1749507, + 20 + ], + "civilization_operator_build": [ + 63052, + 20 + ], + "civilization_operator_solve": [ + 683642, + 860 + ], + "enforce_capacity": [ + 319168, + 80 + ], + "generation_products": [ + 193901, + 40 + ], + "generation_withdrawal": [ + 550377, + 60 + ], + "generations": [ + 2768999, + 20 + ], + "initialize": [ + 115976, + 20 + ], + "newborn_batches": [ + 122734, + 3016 + ], + "physical_amounts_build": [ + 292064, + 200 + ], + "production": [ + 382112, + 20 + ], + "trends": [ + 363057, + 20 + ] + }, + "run": 2, + "save": "all_objects_speed6" + }, + { + "cycles": 20, + "max_ms": 418.952, + "mean_ms": 402.88125, + "min_ms": 362.784, + "model": "before", + "profile": { + "assign_homes": [ + 18182, + 60 + ], + "civilization_flow_capacity": [ + 138911, + 20 + ], + "civilization_flow_colonies": [ + 235353, + 20 + ], + "civilization_flow_finish": [ + 139454, + 20 + ], + "civilization_flow_ledger": [ + 1808094, + 20 + ], + "civilization_flow_plan": [ + 1823967, + 20 + ], + "civilization_operator_build": [ + 63710, + 20 + ], + "civilization_operator_solve": [ + 684725, + 860 + ], + "enforce_capacity": [ + 241433, + 60 + ], + "generation_products": [ + 79394, + 20 + ], + "generation_withdrawal": [ + 814774, + 40 + ], + "generations": [ + 2390414, + 20 + ], + "initialize": [ + 142892, + 20 + ], + "newborn_batches": [ + 55241, + 1490 + ], + "physical_amounts_build": [ + 359891, + 140 + ], + "production": [ + 410388, + 20 + ], + "trends": [ + 362721, + 20 + ] + }, + "run": 1, + "save": "many_objects_fragmented_stars_cycle" + }, + { + "cycles": 20, + "max_ms": 425.202, + "mean_ms": 404.3497, + "min_ms": 355.885, + "model": "after", + "profile": { + "assign_homes": [ + 18056, + 60 + ], + "civilization_flow_capacity": [ + 139522, + 20 + ], + "civilization_flow_colonies": [ + 236599, + 20 + ], + "civilization_flow_finish": [ + 140701, + 20 + ], + "civilization_flow_ledger": [ + 1809113, + 20 + ], + "civilization_flow_plan": [ + 1824567, + 20 + ], + "civilization_operator_build": [ + 63452, + 20 + ], + "civilization_operator_solve": [ + 684152, + 860 + ], + "enforce_capacity": [ + 242790, + 60 + ], + "generation_products": [ + 80500, + 20 + ], + "generation_withdrawal": [ + 817704, + 40 + ], + "generations": [ + 2406807, + 20 + ], + "initialize": [ + 143427, + 20 + ], + "newborn_batches": [ + 55885, + 1490 + ], + "physical_amounts_build": [ + 363175, + 140 + ], + "production": [ + 412428, + 20 + ], + "trends": [ + 365215, + 20 + ] + }, + "run": 1, + "save": "many_objects_fragmented_stars_cycle" + }, + { + "cycles": 20, + "max_ms": 418.858, + "mean_ms": 404.4306, + "min_ms": 357.106, + "model": "after", + "profile": { + "assign_homes": [ + 18133, + 60 + ], + "civilization_flow_capacity": [ + 139021, + 20 + ], + "civilization_flow_colonies": [ + 237100, + 20 + ], + "civilization_flow_finish": [ + 139480, + 20 + ], + "civilization_flow_ledger": [ + 1815717, + 20 + ], + "civilization_flow_plan": [ + 1825515, + 20 + ], + "civilization_operator_build": [ + 63298, + 20 + ], + "civilization_operator_solve": [ + 685950, + 860 + ], + "enforce_capacity": [ + 242932, + 60 + ], + "generation_products": [ + 79477, + 20 + ], + "generation_withdrawal": [ + 815781, + 40 + ], + "generations": [ + 2400821, + 20 + ], + "initialize": [ + 144101, + 20 + ], + "newborn_batches": [ + 56140, + 1490 + ], + "physical_amounts_build": [ + 362394, + 140 + ], + "production": [ + 412986, + 20 + ], + "trends": [ + 364511, + 20 + ] + }, + "run": 2, + "save": "many_objects_fragmented_stars_cycle" + }, + { + "cycles": 20, + "max_ms": 422.492, + "mean_ms": 405.6284, + "min_ms": 355.699, + "model": "before", + "profile": { + "assign_homes": [ + 18208, + 60 + ], + "civilization_flow_capacity": [ + 139305, + 20 + ], + "civilization_flow_colonies": [ + 236973, + 20 + ], + "civilization_flow_finish": [ + 141048, + 20 + ], + "civilization_flow_ledger": [ + 1818465, + 20 + ], + "civilization_flow_plan": [ + 1832211, + 20 + ], + "civilization_operator_build": [ + 63519, + 20 + ], + "civilization_operator_solve": [ + 688184, + 860 + ], + "enforce_capacity": [ + 243601, + 60 + ], + "generation_products": [ + 79839, + 20 + ], + "generation_withdrawal": [ + 822003, + 40 + ], + "generations": [ + 2410757, + 20 + ], + "initialize": [ + 144424, + 20 + ], + "newborn_batches": [ + 55500, + 1490 + ], + "physical_amounts_build": [ + 363454, + 140 + ], + "production": [ + 412711, + 20 + ], + "trends": [ + 367008, + 20 + ] + }, + "run": 2, + "save": "many_objects_fragmented_stars_cycle" + } + ], + "speed": 6 +} diff --git a/tests/benchmark_civilization_flows.gd b/tests/benchmark_civilization_flows.gd index ed52c4c..81249f4 100644 --- a/tests/benchmark_civilization_flows.gd +++ b/tests/benchmark_civilization_flows.gd @@ -18,7 +18,7 @@ func restore_json_integers(value): func run(): var args = OS.get_cmdline_user_args() if args.is_empty(): - print("Usage: --script tests/benchmark_civilization_flows.gd -- /path/to/save.adventure.json [delta_seconds] [profile] [cycles=100]") + print("Usage: --script tests/benchmark_civilization_flows.gd -- /path/to/save.adventure.json [delta_seconds] [profile] [cycles=100] [speed=1..6]") quit(1) return var data = JSON.parse_string(FileAccess.get_file_as_string(args[0])) @@ -33,7 +33,11 @@ func run(): if not is_finite(delta) or delta <= 0: quit(1) return - for speed in [6]: + var selected_speed: int = 6 + for arg in args: + if arg.begins_with("speed="): + selected_speed = clampi(arg.trim_prefix("speed=").to_int(), 1, root.get_node("Global").SPEED_LIMIT) + for speed in [selected_speed]: var world = load("res://world/owniverse/owniverse.gd").new() world.catalog.load_definitions(root.get_node("Global").OwniverseEntities) world.state = initial.duplicate(true) diff --git a/tests/test_adaptive_quality.gd b/tests/test_adaptive_quality.gd new file mode 100644 index 0000000..aa09444 --- /dev/null +++ b/tests/test_adaptive_quality.gd @@ -0,0 +1,50 @@ +extends SceneTree + +var checks: int = 0 +var failures: int = 0 + +func _initialize() -> void: + call_deferred("run") + +func check(condition: bool, message: String) -> void: + checks += 1 + if not condition: + failures += 1 + push_error(message) + +func run() -> void: + var world = load("res://world/owniverse/owniverse.gd").new() + world.update_adaptive_quality(250000) + check(world.adaptive_quality == 2, "A slow cycle selects the medium approximation") + world.update_adaptive_quality(250000) + check(world.adaptive_quality == 3, "A slow medium-approximation cycle selects the coarse mode") + check(world.adaptive_generation_phase == 1, "Coarse generation phase alternates") + for i in 36: world.update_adaptive_quality(100000) + check(world.adaptive_quality == 2, "Thirty consecutive recovery cycles lower quality by one level") + for i in 30: world.update_adaptive_quality(100000) + check(world.adaptive_quality == 1, "Sustained headroom lowers quality gradually") + for i in 30: world.update_adaptive_quality(100000) + check(world.adaptive_quality == 0, "The exact mode returns after sustained headroom") + + var rows: Array = [] + for node in 12: + var edges: Dictionary = {} + var outgoing: float = 0.0 + if node < 6: + edges[node + 6] = 0.25 + outgoing = 0.25 + rows.append({"edges": edges, "outgoing": outgoing}) + var generator: Dictionary = {"rows": rows, "index": {"source": 0}} + var solved: Dictionary = world.civilization_flow._solve_single_step(generator, "source", 2.0) + for ethic in 6: + var mass: float = 0.0 + var occupation: float = 0.0 + for node in range(ethic, 12, 6): + mass += solved.mass[node] + occupation += solved.occupation[node] + check(absf(mass - 1.0) < 1e-12, "Approximate transition conserves ethic mass %d" % ethic) + check(absf(occupation - 2.0) < 1e-12, "Approximate transition conserves integrated time %d" % ethic) + + world.free() + print("Adaptive quality: %d checks, %d failures" % [checks, failures]) + quit(1 if failures else 0) diff --git a/tests/test_adaptive_quality.gd.uid b/tests/test_adaptive_quality.gd.uid new file mode 100644 index 0000000..b6ded8e --- /dev/null +++ b/tests/test_adaptive_quality.gd.uid @@ -0,0 +1 @@ +uid://ct356urggcgal diff --git a/tests/test_frame_evolution.gd b/tests/test_frame_evolution.gd index d96f597..010f7db 100644 --- a/tests/test_frame_evolution.gd +++ b/tests/test_frame_evolution.gd @@ -117,13 +117,19 @@ func run(): behind.evolute_frame(0.0, self) check(behind.frame_commits == 1 and not behind.frame_busy, "Consumed interval cannot be processed twice") behind.evolute_frame(0.23, self) - behind.evolute_frame(0.53, self) + var remained_busy: bool = behind.frame_busy + if remained_busy: behind.evolute_frame(0.53, self) var pending_clock: Dictionary = behind.frame_clock.duplicate() + if remained_busy: + check(E.eq(behind.frame_active_clock.datetime, 123000), "Busy transaction reserves its original target") while behind.frame_busy: await process_frame - check(E.eq(behind.state.time.datetime, 123000), "Busy transaction keeps its original target") - behind.evolute_frame(0.0, self) - while behind.frame_busy: await process_frame - check(E.eq(behind.state.time, pending_clock), "Next cycle consumes all time accumulated during computation") + if remained_busy: + check(E.eq(behind.state.time.datetime, 123000), "Busy transaction keeps its original target") + behind.evolute_frame(0.0, self) + while behind.frame_busy: await process_frame + check(E.eq(behind.state.time, pending_clock), "Next cycle consumes all time accumulated during computation") + else: + check(E.eq(behind.state.time, pending_clock), "Completed transaction publishes the full requested interval") dispose(behind) print("Frame evolution: %d checks, %d failures" % [checks, failures]) quit(1 if failures else 0) diff --git a/world/owniverse/civilization_flow.gd b/world/owniverse/civilization_flow.gd index c289233..867793a 100644 --- a/world/owniverse/civilization_flow.gd +++ b/world/owniverse/civilization_flow.gd @@ -100,11 +100,12 @@ func _can_progress(source: String, destination: String, budget: Dictionary, rule ## Uniformization computes exp(Q*t) through a Poisson series of sparse steps. ## Absorbing biological stages and Ct retain their mass. No one-transition cap. func _solve(generator: Dictionary, source: String, profile: Dictionary, years: float) -> Dictionary: - var key: String = source + ":" + str(years) + var approximation: int = world.adaptive_quality + var key: String = source + ":" + str(years) + ":" + str(approximation) var basis: Dictionary = _solutions.get(key, {}) if basis.is_empty(): var started: int = world.profile_start() - basis = _solve_basis(generator, source, years) + basis = _solve_single_step(generator, source, years) if approximation > 0 else _solve_basis(generator, source, years) world.profile_end("civilization_operator_solve", started) if _solutions.size() >= 512: _solutions.clear() _solutions[key] = basis @@ -117,6 +118,35 @@ func _solve(generator: Dictionary, source: String, profile: Dictionary, years: f occupation[node] *= shares[node % 6] return {"mass": mass, "occupation": occupation, "occupation_scale": 1.0} +## Load-shedding approximation: each ethic cohort can make at most one +## transition during this interval. The exact Markov solve remains the default. +func _solve_single_step(generator: Dictionary, source: String, years: float) -> Dictionary: + var size: int = generator.rows.size() + var mass: PackedFloat64Array = [] + var occupation: PackedFloat64Array = [] + mass.resize(size) + occupation.resize(size) + var offset: int = generator.index[source] * 6 + for ethic in 6: + var node: int = offset + ethic + var row: Dictionary = generator.rows[node] + var outgoing: float = row.outgoing + if outgoing <= 0.0: + mass[node] = 1.0 + occupation[node] = years + continue + var moved: float = clampf(1.0 - exp(-outgoing * years), 0.0, 1.0) + var stayed: float = 1.0 - moved + mass[node] += stayed + occupation[node] += years * (1.0 - moved * 0.5) + for destination in row.edges: + var rate: float = row.edges[destination] + if rate <= 0.0: continue + var share: float = moved * rate / outgoing + mass[destination] += share + occupation[destination] += years * share * 0.5 + return {"mass": mass, "occupation": occupation} + func _solve_basis(generator: Dictionary, source: String, years: float) -> Dictionary: var size: int = generator.rows.size() var values: PackedFloat64Array = [] diff --git a/world/owniverse/evolution_coordinator.gd b/world/owniverse/evolution_coordinator.gd index b605ec4..2ec1a33 100644 --- a/world/owniverse/evolution_coordinator.gd +++ b/world/owniverse/evolution_coordinator.gd @@ -28,6 +28,7 @@ func run_frame(clock: Dictionary, budget: RefCounted) -> void: func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefCounted = null) -> void: var _keep_alive: RefCounted = self + var adaptive_started: int = world.adaptive_timer_usec() # Reconcile only when simulation advances. # Fractional frame updates cannot change capacity or populations. var processed_year: Variant = SimulationTime.processed_year(datetime) @@ -77,6 +78,7 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC world.state = E.bound_state(next) if E.gt(to_year, from_year): world.presentation.refresh(world.state, world.catalog) + world.update_adaptive_quality(world.adaptive_timer_usec() - adaptive_started) func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = null) -> void: @@ -88,7 +90,15 @@ func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = n "enlightenment": WorldParameters.value(target, "Enlightenment"), "explosiveness": WorldParameters.effective(target, "Exp") } + var generations: int = 0 + var generation_limit: int = 0 + if world.adaptive_quality == 1: generation_limit = 3 + elif world.adaptive_quality >= 2: generation_limit = 1 while await evolve_batch_generation(target, to_year, completed, budget, context): + generations += 1 + # Direct terminal stellar events queued during a generation still run + # immediately, preserving their blast and matter outputs. + if generation_limit > 0 and generations >= generation_limit and completed.is_empty(): break if budget != null and not await budget.pause(): return @@ -108,6 +118,16 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di var batch_ids: Array = [] for id in target.batches: if not Global.CivilizationRules.has(id): batch_ids.append(id) + if world.adaptive_quality >= 3: + batch_ids.sort() + var selected_ids: Array = [] + for i in batch_ids.size(): + if i % 2 == world.adaptive_generation_phase or completed.has(batch_ids[i]): + selected_ids.append(batch_ids[i]) + for id in completed: + if not Global.CivilizationRules.has(id) and not selected_ids.has(id): + selected_ids.append(id) + batch_ids = selected_ids var planets_changed: bool = false var profile_withdraw: int = world.profile_start() for id in batch_ids: diff --git a/world/owniverse/object_batches.gd b/world/owniverse/object_batches.gd index cc47e87..ed36ebd 100644 --- a/world/owniverse/object_batches.gd +++ b/world/owniverse/object_batches.gd @@ -6,13 +6,11 @@ const WorldParameters = preload("res://world/owniverse/world_parameters.gd") ## Diagnostic switch for paired measurements of the old and new timing models. var cycle_stars: bool = true -var _cycle_widths: Dictionary = {} func star_cycle_width(target: Dictionary) -> Variant: var speed: int = int(target.get("speed", 1)) - if not _cycle_widths.has(speed): - _cycle_widths[speed] = E.multiply(E.power(10, speed - 1), world.frame_step_seconds) - return _cycle_widths[speed] + var width: Variant = E.multiply(E.power(10, speed - 1), world.frame_step_seconds) + return E.multiply(width, E.power(2, world.adaptive_quality)) ## The cycle grid may widen a short window, never subdivide a wide one. func uses_cycle_star(target: Dictionary, id: String, lifespan: Variant) -> bool: @@ -70,7 +68,9 @@ func coarsen_cycle_stars(target: Dictionary, id: String) -> void: func temporal_resolution(target: Dictionary) -> Variant: var speed: int = int(target.get("speed", 1)) - return E.divide(E.power(10, speed - 1), 10) if speed >= 4 else 0 + if speed < 4: return 0 + var width: Variant = E.divide(E.power(10, speed - 1), 10) + return E.multiply(width, E.power(2, world.adaptive_quality)) func window_duration(target: Dictionary, lifespan: Variant) -> Variant: if E.le(lifespan, 10): return lifespan diff --git a/world/owniverse/owniverse.gd b/world/owniverse/owniverse.gd index fe32f22..3aa4cb5 100644 --- a/world/owniverse/owniverse.gd +++ b/world/owniverse/owniverse.gd @@ -17,6 +17,39 @@ var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(se var civilization_flow = preload("res://world/owniverse/civilization_flow.gd").new(self) var profile_enabled: bool = false var profile: Dictionary = {} +## Runtime-only quality controller. It is deliberately not serialized. +var adaptive_quality: int = 0 +var adaptive_cycle_ema_ms: float = 0.0 +var adaptive_recovery_cycles: int = 0 +var adaptive_generation_phase: int = 0 + +func update_adaptive_quality(cycle_usec: int) -> void: + var cycle_ms: float = float(cycle_usec) / 1000.0 + adaptive_cycle_ema_ms = cycle_ms if adaptive_cycle_ema_ms <= 0.0 else adaptive_cycle_ema_ms * 0.8 + cycle_ms * 0.2 + if adaptive_cycle_ema_ms > 300.0: + adaptive_quality = 3 + adaptive_recovery_cycles = 0 + elif adaptive_quality >= 2 and adaptive_cycle_ema_ms > 200.0: + adaptive_quality = 3 + adaptive_recovery_cycles = 0 + elif adaptive_cycle_ema_ms > 220.0: + adaptive_quality = maxi(adaptive_quality, 2) + adaptive_recovery_cycles = 0 + elif adaptive_cycle_ema_ms > 200.0: + adaptive_quality = maxi(adaptive_quality, 1) + adaptive_recovery_cycles = 0 + elif adaptive_cycle_ema_ms < 140.0: + adaptive_recovery_cycles += 1 + if adaptive_recovery_cycles >= 30: + adaptive_quality = maxi(0, adaptive_quality - 1) + adaptive_recovery_cycles = 0 + else: + adaptive_recovery_cycles = 0 + if adaptive_quality >= 3: + adaptive_generation_phase = 1 - adaptive_generation_phase + +func adaptive_timer_usec() -> int: + return Time.get_ticks_usec() - (frame_budget.waiting_usec if frame_busy and frame_budget != null else 0) func profile_start() -> int: if not profile_enabled: return 0