Enhance civilization flow mechanics and adaptive quality control

- Implemented runtime-only adaptive quality controller to optimize simulation performance based on CPU time.
- Added adaptive quality thresholds and recovery cycles to dynamically adjust simulation fidelity.
- Introduced single-step solving method for civilization transitions to improve performance under high loads.
- Updated benchmark scripts to include speed parameter for testing various simulation speeds.
- Added tests for adaptive quality adjustments and their effects on civilization flow calculations.
- Documented results of optimization attempts and their impact on performance metrics.
This commit is contained in:
Kirill
2026-10-08 22:05:08 +03:00
parent 459fbb2a4d
commit 02c9843e1b
11 changed files with 891 additions and 14 deletions
+32 -2
View File
@@ -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 = []
+20
View File
@@ -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:
+5 -5
View File
@@ -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
+33
View File
@@ -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