feat: index loaded resource discovery
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
class_name ActionTargetResolver
|
||||
extends RefCounted
|
||||
|
||||
var last_resource_query_stats: Dictionary = {}
|
||||
|
||||
|
||||
func resolve(
|
||||
npc: SimNPC, origin: Vector3, simulation_manager: Node, active_world_adapter: Node
|
||||
@@ -28,10 +30,33 @@ func _resolve_resource(
|
||||
definition: ActionDefinition,
|
||||
simulation_manager: Node,
|
||||
active_world_adapter: Node
|
||||
) -> Dictionary:
|
||||
last_resource_query_stats = {}
|
||||
if (
|
||||
active_world_adapter.has_method("get_resource_candidates_in_radius")
|
||||
and active_world_adapter.has_method("get_resource_query_profile")
|
||||
):
|
||||
return _resolve_resource_spatial(
|
||||
npc, origin, definition, simulation_manager, active_world_adapter
|
||||
)
|
||||
return _resolve_resource_linear(
|
||||
npc, origin, definition, simulation_manager, active_world_adapter
|
||||
)
|
||||
|
||||
|
||||
func _resolve_resource_linear(
|
||||
npc: SimNPC,
|
||||
origin: Vector3,
|
||||
definition: ActionDefinition,
|
||||
simulation_manager: Node,
|
||||
active_world_adapter: Node
|
||||
) -> Dictionary:
|
||||
var best: Dictionary = {}
|
||||
var best_score := INF
|
||||
for candidate in active_world_adapter.get_resource_candidates(definition.resource_action_id):
|
||||
var candidates: Array[Dictionary] = active_world_adapter.get_resource_candidates(
|
||||
definition.resource_action_id
|
||||
)
|
||||
for candidate in candidates:
|
||||
var node_id := StringName(candidate["target_id"])
|
||||
var state: ResourceStateRecord = simulation_manager.get_resource_state(node_id)
|
||||
if state == null or not state.can_npc_use() or not state.is_available_for(npc.id):
|
||||
@@ -41,12 +66,108 @@ func _resolve_resource(
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best = candidate
|
||||
if best.is_empty():
|
||||
last_resource_query_stats = {
|
||||
"mode": "linear",
|
||||
"loaded_candidate_count": candidates.size(),
|
||||
"candidates_inspected": candidates.size(),
|
||||
"range_pass_count": 1,
|
||||
}
|
||||
return _reserve_resource_candidate(best, npc, simulation_manager)
|
||||
|
||||
|
||||
func _resolve_resource_spatial(
|
||||
npc: SimNPC,
|
||||
origin: Vector3,
|
||||
definition: ActionDefinition,
|
||||
simulation_manager: Node,
|
||||
active_world_adapter: Node
|
||||
) -> Dictionary:
|
||||
var profile: Dictionary = active_world_adapter.get_resource_query_profile(
|
||||
definition.resource_action_id, origin
|
||||
)
|
||||
var loaded_count := int(profile.get("candidate_count", 0))
|
||||
if loaded_count == 0:
|
||||
last_resource_query_stats = {
|
||||
"mode": "spatial",
|
||||
"loaded_candidate_count": 0,
|
||||
"candidates_inspected": 0,
|
||||
"range_pass_count": 0,
|
||||
}
|
||||
return {}
|
||||
var target_id := StringName(best["target_id"])
|
||||
|
||||
var best: Dictionary = {}
|
||||
var best_score := INF
|
||||
var best_registration_order := 9223372036854775807
|
||||
var maximum_distance := maxf(float(profile["max_distance"]), 0.0)
|
||||
var radius := minf(maxf(float(profile["initial_radius"]), 0.1), maximum_distance)
|
||||
var previous_radius := -1.0
|
||||
var candidates_inspected := 0
|
||||
var range_pass_count := 0
|
||||
while true:
|
||||
var candidates: Array[Dictionary] = active_world_adapter.get_resource_candidates_in_radius(
|
||||
definition.resource_action_id, origin, radius, previous_radius
|
||||
)
|
||||
range_pass_count += 1
|
||||
candidates_inspected += candidates.size()
|
||||
for candidate in candidates:
|
||||
var node_id := StringName(candidate["target_id"])
|
||||
var state: ResourceStateRecord = simulation_manager.get_resource_state(node_id)
|
||||
if state == null or not state.can_npc_use() or not state.is_available_for(npc.id):
|
||||
continue
|
||||
var position: Vector3 = candidate["position"]
|
||||
var score := score_resource_candidate(npc, origin, position, state)
|
||||
var registration_order := int(candidate["registration_order"])
|
||||
if (
|
||||
score < best_score
|
||||
or (score == best_score and registration_order < best_registration_order)
|
||||
):
|
||||
best_score = score
|
||||
best_registration_order = registration_order
|
||||
best = candidate
|
||||
|
||||
if radius >= maximum_distance:
|
||||
break
|
||||
if (
|
||||
not best.is_empty()
|
||||
and _minimum_resource_score_beyond(npc, radius, profile) > best_score
|
||||
):
|
||||
break
|
||||
previous_radius = radius
|
||||
var next_radius := minf(maximum_distance, radius * 2.0)
|
||||
if next_radius <= radius:
|
||||
break
|
||||
radius = next_radius
|
||||
|
||||
last_resource_query_stats = {
|
||||
"mode": "spatial",
|
||||
"loaded_candidate_count": loaded_count,
|
||||
"candidates_inspected": candidates_inspected,
|
||||
"range_pass_count": range_pass_count,
|
||||
"final_radius": radius,
|
||||
}
|
||||
return _reserve_resource_candidate(best, npc, simulation_manager)
|
||||
|
||||
|
||||
func _minimum_resource_score_beyond(npc: SimNPC, radius: float, profile: Dictionary) -> float:
|
||||
var comfort_overage := maxf(radius - float(profile["max_comfort_distance"]), 0.0)
|
||||
var risk_weight := 20.0 + maxf(100.0 - npc.energy, 0.0) * 0.2
|
||||
return (
|
||||
radius
|
||||
+ comfort_overage * 2.5
|
||||
+ float(profile["min_safety_risk"]) * risk_weight
|
||||
- float(profile["max_discovery_priority"])
|
||||
)
|
||||
|
||||
|
||||
func _reserve_resource_candidate(
|
||||
candidate: Dictionary, npc: SimNPC, simulation_manager: Node
|
||||
) -> Dictionary:
|
||||
if candidate.is_empty():
|
||||
return {}
|
||||
var target_id := StringName(candidate["target_id"])
|
||||
if not simulation_manager.reserve_resource(target_id, npc.id):
|
||||
return {}
|
||||
return best
|
||||
return candidate
|
||||
|
||||
|
||||
func _resolve_activity(
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
class_name LoadedResourceDiscoveryBenchmark
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const WORKLOAD_ID := &"loaded_resource_target_resolution"
|
||||
const RESOURCE_SPACING := 20.0
|
||||
const DEFAULT_QUERY_COUNT := 400
|
||||
const DEFAULT_SAMPLE_COUNT := 7
|
||||
const WARMUP_QUERY_COUNT := 40
|
||||
const SimulationManagerScript := preload("res://simulation/SimulationManager.gd")
|
||||
|
||||
|
||||
class LinearResourceAdapter:
|
||||
extends Node
|
||||
|
||||
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
|
||||
var candidates: Array[Dictionary] = []
|
||||
for node in ResourceNode.get_all():
|
||||
if node.action_id != action_id or node.interaction_point == null:
|
||||
continue
|
||||
(
|
||||
candidates
|
||||
. append(
|
||||
{
|
||||
"target_id": String(node.node_id),
|
||||
"position": node.interaction_point.global_position,
|
||||
"resource_id": String(node.resource_id),
|
||||
"safety_risk": node.safety_risk,
|
||||
"comfort_distance": node.comfort_distance,
|
||||
"discovery_priority": node.discovery_priority,
|
||||
}
|
||||
)
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
func create_fixture(parent: Node, resource_count: int, seed_value: int) -> Dictionary:
|
||||
if parent == null or resource_count <= 0:
|
||||
return {}
|
||||
var fixture_root := Node.new()
|
||||
fixture_root.name = "LoadedResourceDiscoveryFixture"
|
||||
parent.add_child(fixture_root)
|
||||
var resource_root := Node3D.new()
|
||||
resource_root.name = "ResourceNodes"
|
||||
fixture_root.add_child(resource_root)
|
||||
for resource_index in resource_count:
|
||||
resource_root.add_child(_create_resource(resource_index, resource_count))
|
||||
|
||||
var adapter := ActiveWorldAdapter.new()
|
||||
adapter.name = "ActiveWorldAdapter"
|
||||
fixture_root.add_child(adapter)
|
||||
var manager: Node = SimulationManagerScript.new()
|
||||
manager.name = "SimulationManager"
|
||||
manager.debug_logs = false
|
||||
manager.simulation_seed = seed_value
|
||||
manager.active_world_adapter = adapter
|
||||
fixture_root.add_child(manager)
|
||||
manager.set_process(false)
|
||||
manager.register_loaded_resource_nodes()
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.energy = 62.0
|
||||
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
|
||||
return {
|
||||
"root": fixture_root,
|
||||
"resource_root": resource_root,
|
||||
"adapter": adapter,
|
||||
"manager": manager,
|
||||
"npc": npc,
|
||||
"resource_count": resource_count,
|
||||
}
|
||||
|
||||
|
||||
func create_linear_adapter() -> Node:
|
||||
return LinearResourceAdapter.new()
|
||||
|
||||
|
||||
func measure_fixture(
|
||||
fixture: Dictionary,
|
||||
query_count: int = DEFAULT_QUERY_COUNT,
|
||||
sample_count: int = DEFAULT_SAMPLE_COUNT,
|
||||
include_spatial: bool = true
|
||||
) -> Dictionary:
|
||||
if fixture.is_empty() or query_count <= 0 or sample_count <= 0:
|
||||
return {}
|
||||
var adapter: ActiveWorldAdapter = fixture["adapter"]
|
||||
var origins := _build_origins(int(fixture["resource_count"]), query_count)
|
||||
var linear_adapter := create_linear_adapter()
|
||||
(fixture["root"] as Node).add_child(linear_adapter)
|
||||
_run_queries(fixture, linear_adapter, origins, mini(WARMUP_QUERY_COUNT, query_count), false)
|
||||
var linear := _measure_mode(fixture, linear_adapter, origins, sample_count, false)
|
||||
if linear.is_empty():
|
||||
return {}
|
||||
var result := {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"workload_id": String(WORKLOAD_ID),
|
||||
"resource_count": int(fixture["resource_count"]),
|
||||
"query_count": query_count,
|
||||
"sample_count": sample_count,
|
||||
"linear": linear,
|
||||
}
|
||||
if (
|
||||
include_spatial
|
||||
and adapter.has_method("get_resource_candidates_in_radius")
|
||||
and adapter.has_method("get_resource_query_profile")
|
||||
):
|
||||
_run_queries(fixture, adapter, origins, mini(WARMUP_QUERY_COUNT, query_count), false)
|
||||
var spatial := _measure_mode(fixture, adapter, origins, sample_count, true)
|
||||
if spatial.is_empty() or spatial["selected_checksum"] != linear["selected_checksum"]:
|
||||
return {}
|
||||
result["spatial"] = spatial
|
||||
result["speedup"] = (
|
||||
float(linear["usec_per_query_median"])
|
||||
/ maxf(float(spatial["usec_per_query_median"]), 0.0001)
|
||||
)
|
||||
result["inspected_reduction_percent"] = (
|
||||
(
|
||||
1.0
|
||||
- float(spatial["candidates_inspected_average"]) / float(fixture["resource_count"])
|
||||
)
|
||||
* 100.0
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func free_fixture(fixture: Dictionary) -> void:
|
||||
if fixture.has("root") and is_instance_valid(fixture["root"]):
|
||||
(fixture["root"] as Node).free()
|
||||
|
||||
|
||||
func _measure_mode(
|
||||
fixture: Dictionary,
|
||||
query_adapter: Object,
|
||||
origins: Array[Vector3],
|
||||
sample_count: int,
|
||||
spatial: bool
|
||||
) -> Dictionary:
|
||||
var elapsed_samples: Array[int] = []
|
||||
var inspected_samples: Array[float] = []
|
||||
var checksum := ""
|
||||
for _sample_index in sample_count:
|
||||
var sample := _run_queries(fixture, query_adapter, origins, origins.size(), spatial)
|
||||
if sample.is_empty():
|
||||
return {}
|
||||
if checksum.is_empty():
|
||||
checksum = sample["selected_checksum"]
|
||||
elif checksum != sample["selected_checksum"]:
|
||||
return {}
|
||||
elapsed_samples.append(int(sample["elapsed_usec"]))
|
||||
inspected_samples.append(float(sample["candidates_inspected_average"]))
|
||||
elapsed_samples.sort()
|
||||
inspected_samples.sort()
|
||||
var elapsed_median := elapsed_samples[elapsed_samples.size() / 2]
|
||||
return {
|
||||
"elapsed_usec_samples": elapsed_samples,
|
||||
"elapsed_usec_median": elapsed_median,
|
||||
"usec_per_query_median": float(elapsed_median) / float(origins.size()),
|
||||
"queries_per_second_median": float(origins.size()) * 1000000.0 / float(elapsed_median),
|
||||
"candidates_inspected_average": inspected_samples[inspected_samples.size() / 2],
|
||||
"selected_checksum": checksum,
|
||||
}
|
||||
|
||||
|
||||
func _run_queries(
|
||||
fixture: Dictionary,
|
||||
query_adapter: Object,
|
||||
origins: Array[Vector3],
|
||||
query_count: int,
|
||||
spatial: bool
|
||||
) -> Dictionary:
|
||||
var manager: Node = fixture["manager"]
|
||||
var npc: SimNPC = fixture["npc"]
|
||||
var selected_ids := PackedStringArray()
|
||||
var inspected_total := 0
|
||||
var started_usec := Time.get_ticks_usec()
|
||||
for query_index in query_count:
|
||||
var result: Dictionary = manager.target_resolver.resolve(
|
||||
npc, origins[query_index], manager, query_adapter
|
||||
)
|
||||
if result.is_empty():
|
||||
return {}
|
||||
var target_id := StringName(result["target_id"])
|
||||
selected_ids.append(String(target_id))
|
||||
manager.release_resource(target_id, npc.id)
|
||||
if spatial and not manager.target_resolver.last_resource_query_stats.is_empty():
|
||||
inspected_total += int(
|
||||
manager.target_resolver.last_resource_query_stats.get("candidates_inspected", 0)
|
||||
)
|
||||
else:
|
||||
inspected_total += int(fixture["resource_count"])
|
||||
var elapsed_usec := maxi(Time.get_ticks_usec() - started_usec, 1)
|
||||
return {
|
||||
"elapsed_usec": elapsed_usec,
|
||||
"candidates_inspected_average": float(inspected_total) / float(query_count),
|
||||
"selected_checksum": "|".join(selected_ids).sha256_text(),
|
||||
}
|
||||
|
||||
|
||||
func _create_resource(resource_index: int, resource_count: int) -> ResourceNode:
|
||||
var node := ResourceNode.new()
|
||||
node.name = "Resource_%04d" % resource_index
|
||||
node.node_id = StringName("benchmark_resource_%04d" % resource_index)
|
||||
node.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||
node.resource_id = SimulationIds.RESOURCE_FOOD
|
||||
node.initial_amount = 100000.0
|
||||
node.initial_enabled = resource_index % 29 != 0
|
||||
node.safety_risk = float(resource_index % 6) * 0.06
|
||||
node.comfort_distance = 16.0 + float(resource_index % 5) * 5.0
|
||||
node.discovery_priority = float(resource_index % 9) * 0.35
|
||||
node.debug_label_enabled = false
|
||||
node.position = _resource_position(resource_index, resource_count)
|
||||
var interaction_point := Marker3D.new()
|
||||
interaction_point.name = "InteractionPoint"
|
||||
node.add_child(interaction_point)
|
||||
return node
|
||||
|
||||
|
||||
func _build_origins(resource_count: int, query_count: int) -> Array[Vector3]:
|
||||
var origins: Array[Vector3] = []
|
||||
for query_index in query_count:
|
||||
var resource_index := (query_index * 37 + 11) % resource_count
|
||||
var offset := Vector3(
|
||||
float((query_index * 7) % 13) - 6.0, 0.0, float((query_index * 11) % 17) - 8.0
|
||||
)
|
||||
origins.append(_resource_position(resource_index, resource_count) + offset)
|
||||
return origins
|
||||
|
||||
|
||||
func _resource_position(resource_index: int, resource_count: int) -> Vector3:
|
||||
var columns := ceili(sqrt(float(resource_count)))
|
||||
var column := resource_index % columns
|
||||
var row := resource_index / columns
|
||||
var centered_column := float(column) - float(columns - 1) * 0.5
|
||||
var row_count := ceili(float(resource_count) / float(columns))
|
||||
var centered_row := float(row) - float(row_count - 1) * 0.5
|
||||
return Vector3(
|
||||
centered_column * RESOURCE_SPACING,
|
||||
sin(float(resource_index) * 0.37) * 2.5,
|
||||
centered_row * RESOURCE_SPACING
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ccstdqk3xgxnu
|
||||
Reference in New Issue
Block a user