ai-nk-cce 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_nk_cce-0.1.0.dist-info/METADATA +118 -0
- ai_nk_cce-0.1.0.dist-info/RECORD +46 -0
- ai_nk_cce-0.1.0.dist-info/WHEEL +4 -0
- api/__init__.py +0 -0
- api/mpcdf_vllm.py +94 -0
- evals/nk_model.py +277 -0
- model/README.md +64 -0
- model/config/dataset_conv_v1.yml +9 -0
- model/config/dataset_conv_v2_m2.yml +9 -0
- model/config/dataset_conv_v3_m2_assembl_nearest.yml +9 -0
- model/config/dataset_debug.yml +9 -0
- model/config/dataset_v4_int_format.yml +9 -0
- model/config/dataset_v5.yml +9 -0
- model/config/inference.yml +7 -0
- model/config/train.yml +24 -0
- model/config/train_debug.yml +19 -0
- model/config/train_from_checkpoint.yml +24 -0
- model/config/train_from_checkpoint_debug.yml +19 -0
- model/config/train_grpo.yml +30 -0
- model/config/train_grpo_debug.yml +30 -0
- model/config/train_grpo_debug_vllm.yml +32 -0
- model/config.py +54 -0
- model/dataset.py +324 -0
- model/inference.py +51 -0
- model/nk_assistant.py +207 -0
- model/parser.py +70 -0
- model/run_slurm.py +335 -0
- model/score.ipynb +596 -0
- model/scripts/template.slurm +54 -0
- model/scripts/template_rl.slurm +54 -0
- model/train.py +293 -0
- nk_model/__init__.py +0 -0
- nk_model/assembler.py +112 -0
- nk_model/biased_prediction_agent.py +389 -0
- nk_model/dataset.py +434 -0
- nk_model/enums.py +21 -0
- nk_model/landscape_cache.py +149 -0
- nk_model/models.py +172 -0
- nk_model/nk_landscape.py +498 -0
- simulation/hill_climber_simulation.py +211 -0
- simulation/hill_climber_vs_ai_simulation.py +132 -0
- simulation/landscape_selection.py +179 -0
- utils/__init__.py +0 -0
- utils/binary_conversion.py +128 -0
- utils/logging.py +33 -0
- utils/utils.py +51 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import List, Optional, Type
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from src.model.config import ParserConfig
|
|
9
|
+
from src.model.dataset import binary_vectors_within_radius
|
|
10
|
+
from src.model.nk_assistant import NKAssistant, NKAssistantConfig
|
|
11
|
+
from src.nk_model.models import NKParams
|
|
12
|
+
from src.nk_model.nk_landscape import NKLandscape
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StopCondition(Enum):
|
|
18
|
+
MAX_ITERATIONS = "max_iterations"
|
|
19
|
+
REACH_PEAK = "reach_peak"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgentType(Enum):
|
|
23
|
+
HILL_CLIMBER = "hill_climber"
|
|
24
|
+
AI_HILL_CLIMBER = "ai_hill_climber"
|
|
25
|
+
|
|
26
|
+
def agent_class(self) -> Type["HillClimberAgent"]:
|
|
27
|
+
if self == AgentType.HILL_CLIMBER:
|
|
28
|
+
return HillClimberAgent
|
|
29
|
+
elif self == AgentType.AI_HILL_CLIMBER:
|
|
30
|
+
return AIHillClimberAgent
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError(f"Invalid agent type: {self}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HillClimberSimConfig(BaseModel):
|
|
36
|
+
nk_params: NKParams
|
|
37
|
+
hamming_distance: int
|
|
38
|
+
agent_type: AgentType
|
|
39
|
+
start_idxs: List[int]
|
|
40
|
+
agent_start_idx: Optional[int] = None
|
|
41
|
+
use_trajectory: Optional[bool] = False
|
|
42
|
+
stop_condition: StopCondition
|
|
43
|
+
stop_condition_kwargs: dict
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HillClimberAgent:
|
|
47
|
+
def __init__(self, start_idx: int):
|
|
48
|
+
self.trajectory = [start_idx]
|
|
49
|
+
|
|
50
|
+
def _get_current_idx(self) -> int:
|
|
51
|
+
return self.trajectory[-1]
|
|
52
|
+
|
|
53
|
+
def hill_climb(
|
|
54
|
+
self,
|
|
55
|
+
n: int,
|
|
56
|
+
hamming_distance: int,
|
|
57
|
+
) -> int:
|
|
58
|
+
neighbors = binary_vectors_within_radius(
|
|
59
|
+
origin=self._get_current_idx(),
|
|
60
|
+
radius=hamming_distance,
|
|
61
|
+
n_dim=n,
|
|
62
|
+
)
|
|
63
|
+
next_idx = np.random.choice(neighbors)
|
|
64
|
+
|
|
65
|
+
# Only advance if the next index is not in the trajectory
|
|
66
|
+
if next_idx is not self._get_current_idx():
|
|
67
|
+
self.trajectory.append(next_idx)
|
|
68
|
+
return next_idx
|
|
69
|
+
else:
|
|
70
|
+
return self.hill_climb(n, hamming_distance)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AIHillClimberAgent(HillClimberAgent):
|
|
74
|
+
def __init__(self, start_idx: int, use_trajectory: bool):
|
|
75
|
+
super().__init__(start_idx)
|
|
76
|
+
self.use_trajectory = use_trajectory
|
|
77
|
+
assistant_config = NKAssistantConfig(
|
|
78
|
+
parser_config=ParserConfig(include_payoff=True),
|
|
79
|
+
use_mpcdf_vllm=True,
|
|
80
|
+
generation_params={"temperature": 0.7},
|
|
81
|
+
)
|
|
82
|
+
self.assistant = NKAssistant(config=assistant_config)
|
|
83
|
+
|
|
84
|
+
def hill_climb(
|
|
85
|
+
self,
|
|
86
|
+
n: int,
|
|
87
|
+
k: int,
|
|
88
|
+
power_scale: float,
|
|
89
|
+
hamming_distance: int,
|
|
90
|
+
sample_idxs: List[int],
|
|
91
|
+
payoffs: List[float],
|
|
92
|
+
) -> int:
|
|
93
|
+
logger.debug(f"sample_idxs: {sample_idxs}")
|
|
94
|
+
if self.use_trajectory:
|
|
95
|
+
sample_idxs = self.trajectory
|
|
96
|
+
logger.debug(f"Using trajectory: {sample_idxs}")
|
|
97
|
+
else:
|
|
98
|
+
sample_idxs = sample_idxs[-16:]
|
|
99
|
+
extracted_payoffs = [payoffs[idx] for idx in sample_idxs]
|
|
100
|
+
logger.debug(f"extracted_payoffs: {extracted_payoffs}")
|
|
101
|
+
|
|
102
|
+
origin_idx = self._get_current_idx()
|
|
103
|
+
suggestion = self.assistant.suggest(
|
|
104
|
+
n=n,
|
|
105
|
+
k=k,
|
|
106
|
+
power_scale=power_scale,
|
|
107
|
+
hamming_distance=hamming_distance,
|
|
108
|
+
sample_idxs=sample_idxs,
|
|
109
|
+
origin_idx=origin_idx,
|
|
110
|
+
payoffs=extracted_payoffs,
|
|
111
|
+
)
|
|
112
|
+
self.trajectory.append(suggestion)
|
|
113
|
+
return suggestion
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class HillClimberSimulation:
|
|
117
|
+
def __init__(self, config: HillClimberSimConfig):
|
|
118
|
+
landscape = NKLandscape(params=config.nk_params)
|
|
119
|
+
self._initialize(config, landscape)
|
|
120
|
+
|
|
121
|
+
def _initialize(
|
|
122
|
+
self, config: HillClimberSimConfig, landscape: NKLandscape
|
|
123
|
+
):
|
|
124
|
+
"""Initialize shared components of the simulation.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
config: Configuration for the simulation
|
|
128
|
+
landscape: The NK landscape instance to use
|
|
129
|
+
"""
|
|
130
|
+
self.config = config
|
|
131
|
+
self.visited_idxs = set(config.start_idxs)
|
|
132
|
+
self.landscape = landscape
|
|
133
|
+
self.ordered_payoffs = landscape.get_ordered_payoffs()
|
|
134
|
+
|
|
135
|
+
# Initialize the agent
|
|
136
|
+
if (
|
|
137
|
+
config.agent_start_idx is None
|
|
138
|
+
or config.agent_start_idx not in config.start_idxs
|
|
139
|
+
):
|
|
140
|
+
self.agent_start_idx = np.random.choice(config.start_idxs)
|
|
141
|
+
else:
|
|
142
|
+
self.agent_start_idx = config.agent_start_idx
|
|
143
|
+
|
|
144
|
+
if config.agent_type == AgentType.HILL_CLIMBER:
|
|
145
|
+
self.agent = HillClimberAgent(start_idx=self.agent_start_idx)
|
|
146
|
+
elif config.agent_type == AgentType.AI_HILL_CLIMBER:
|
|
147
|
+
self.agent = AIHillClimberAgent(
|
|
148
|
+
start_idx=self.agent_start_idx,
|
|
149
|
+
use_trajectory=config.use_trajectory,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_cached_landscape(
|
|
154
|
+
cls, config: HillClimberSimConfig, landscape_uuid: str
|
|
155
|
+
) -> "HillClimberSimulation":
|
|
156
|
+
"""Create simulation using a cached landscape by UUID.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
config: Configuration for the simulation
|
|
160
|
+
landscape_uuid: UUID of the cached landscape to use
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
HillClimberSimulation instance with the cached landscape
|
|
164
|
+
"""
|
|
165
|
+
instance = cls.__new__(cls)
|
|
166
|
+
landscape = NKLandscape.from_cache(landscape_uuid)
|
|
167
|
+
instance._initialize(config, landscape)
|
|
168
|
+
return instance
|
|
169
|
+
|
|
170
|
+
def run(self):
|
|
171
|
+
while not self._break_hill_climber(self.agent.trajectory):
|
|
172
|
+
if self.config.agent_type == AgentType.HILL_CLIMBER:
|
|
173
|
+
self.agent.hill_climb(
|
|
174
|
+
n=self.config.nk_params.n,
|
|
175
|
+
hamming_distance=self.config.hamming_distance,
|
|
176
|
+
)
|
|
177
|
+
elif self.config.agent_type == AgentType.AI_HILL_CLIMBER:
|
|
178
|
+
self.agent.hill_climb(
|
|
179
|
+
n=self.config.nk_params.n,
|
|
180
|
+
k=self.config.nk_params.k,
|
|
181
|
+
power_scale=self.config.nk_params.power,
|
|
182
|
+
hamming_distance=self.config.hamming_distance,
|
|
183
|
+
sample_idxs=list(self.visited_idxs),
|
|
184
|
+
payoffs=self.ordered_payoffs,
|
|
185
|
+
)
|
|
186
|
+
# Update visited indices
|
|
187
|
+
self.visited_idxs.update(self.agent.trajectory)
|
|
188
|
+
logger.debug(f"visited_idxs: {self.visited_idxs}")
|
|
189
|
+
|
|
190
|
+
return self.agent.trajectory
|
|
191
|
+
|
|
192
|
+
def _break_hill_climber(self, trajectory: List[int]) -> bool:
|
|
193
|
+
if self.config.stop_condition == StopCondition.MAX_ITERATIONS:
|
|
194
|
+
# Break if the number of iterations where reached
|
|
195
|
+
return (
|
|
196
|
+
len(trajectory)
|
|
197
|
+
>= self.config.stop_condition_kwargs["max_iterations"]
|
|
198
|
+
)
|
|
199
|
+
elif self.config.stop_condition == StopCondition.REACH_PEAK:
|
|
200
|
+
# Break if the agent is stuck on a local maximum
|
|
201
|
+
stuck_for_until_break = self.config.stop_condition_kwargs[
|
|
202
|
+
"stuck_for_until_break"
|
|
203
|
+
]
|
|
204
|
+
if len(trajectory) < stuck_for_until_break * 2:
|
|
205
|
+
return False
|
|
206
|
+
if set(trajectory[:-stuck_for_until_break]) >= set(
|
|
207
|
+
trajectory[-stuck_for_until_break:]
|
|
208
|
+
):
|
|
209
|
+
return True
|
|
210
|
+
else:
|
|
211
|
+
return False
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from tqdm.auto import tqdm
|
|
3
|
+
|
|
4
|
+
from src.nk_model.models import NKParams
|
|
5
|
+
from src.nk_model.nk_landscape import NKLandscape
|
|
6
|
+
from src.simulation.hill_climber_simulation import (
|
|
7
|
+
AgentType,
|
|
8
|
+
HillClimberSimConfig,
|
|
9
|
+
HillClimberSimulation,
|
|
10
|
+
StopCondition,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def hill_climber_vs_ai_simulation(
|
|
15
|
+
config: HillClimberSimConfig,
|
|
16
|
+
landscape: NKLandscape,
|
|
17
|
+
):
|
|
18
|
+
if config.agent_start_idx is None:
|
|
19
|
+
config.agent_start_idx = np.random.choice(config.start_idxs)
|
|
20
|
+
|
|
21
|
+
if config.agent_type == AgentType.HILL_CLIMBER:
|
|
22
|
+
hill_climber_config = config
|
|
23
|
+
ai_config = config.model_copy(
|
|
24
|
+
update={"agent_type": AgentType.AI_HILL_CLIMBER}
|
|
25
|
+
)
|
|
26
|
+
elif config.agent_type == AgentType.AI_HILL_CLIMBER:
|
|
27
|
+
ai_config = config
|
|
28
|
+
hill_climber_config = config.model_copy(
|
|
29
|
+
update={"agent_type": AgentType.HILL_CLIMBER}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
hill_climber_simulation = HillClimberSimulation.from_cached_landscape(
|
|
33
|
+
config=hill_climber_config, landscape_uuid=landscape.uuid
|
|
34
|
+
)
|
|
35
|
+
ai_simulation = HillClimberSimulation.from_cached_landscape(
|
|
36
|
+
config=ai_config, landscape_uuid=landscape.uuid
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
hill_climber_trajectory = hill_climber_simulation.run()
|
|
40
|
+
ai_trajectory = ai_simulation.run()
|
|
41
|
+
|
|
42
|
+
return hill_climber_trajectory, ai_trajectory
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def compare_hill_climber_vs_ai(
|
|
46
|
+
nk_params: NKParams,
|
|
47
|
+
hamming_distance: int,
|
|
48
|
+
max_iterations: int,
|
|
49
|
+
repetitions: int,
|
|
50
|
+
n_start_idxs: int = 1,
|
|
51
|
+
):
|
|
52
|
+
hill_climber_payoff_trajectories = []
|
|
53
|
+
ai_payoff_trajectories = []
|
|
54
|
+
for _ in tqdm(range(repetitions), desc="Comparing hill climber vs AI"):
|
|
55
|
+
start_idxs = [
|
|
56
|
+
np.random.choice(list(range(2**nk_params.n)))
|
|
57
|
+
for _ in range(n_start_idxs)
|
|
58
|
+
]
|
|
59
|
+
start_idx = np.random.choice(start_idxs)
|
|
60
|
+
|
|
61
|
+
config = HillClimberSimConfig(
|
|
62
|
+
nk_params=nk_params,
|
|
63
|
+
agent_type=AgentType.HILL_CLIMBER,
|
|
64
|
+
start_idxs=start_idxs,
|
|
65
|
+
agent_start_idx=start_idx,
|
|
66
|
+
use_trajectory=True,
|
|
67
|
+
stop_condition=StopCondition.MAX_ITERATIONS,
|
|
68
|
+
stop_condition_kwargs={"max_iterations": max_iterations},
|
|
69
|
+
hamming_distance=hamming_distance,
|
|
70
|
+
)
|
|
71
|
+
landscape = NKLandscape(params=nk_params)
|
|
72
|
+
payoff_list = landscape.get_ordered_payoffs()
|
|
73
|
+
|
|
74
|
+
hill_climber_trajectory, ai_trajectory = hill_climber_vs_ai_simulation(
|
|
75
|
+
config, landscape
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
hill_climber_payoff_traj = [
|
|
79
|
+
payoff_list[idx] for idx in hill_climber_trajectory
|
|
80
|
+
]
|
|
81
|
+
ai_payoff_traj = [payoff_list[idx] for idx in ai_trajectory]
|
|
82
|
+
|
|
83
|
+
hill_climber_payoff_trajectories.append(hill_climber_payoff_traj)
|
|
84
|
+
ai_payoff_trajectories.append(ai_payoff_traj)
|
|
85
|
+
|
|
86
|
+
return hill_climber_payoff_trajectories, ai_payoff_trajectories
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def average_payoff_trajectories(
|
|
90
|
+
payoff_trajectories: list[list[float]],
|
|
91
|
+
) -> list[float]:
|
|
92
|
+
"""Transform list of payoff trajectories into list of averaged payoffs.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
payoff_trajectories: List of payoff trajectories, where each
|
|
96
|
+
trajectory is a list of payoffs.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of averaged payoffs at each step position.
|
|
100
|
+
"""
|
|
101
|
+
if not payoff_trajectories:
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
max_len = max(len(traj) for traj in payoff_trajectories)
|
|
105
|
+
averaged = []
|
|
106
|
+
|
|
107
|
+
for step_idx in range(max_len):
|
|
108
|
+
step_payoffs = [
|
|
109
|
+
traj[step_idx]
|
|
110
|
+
for traj in payoff_trajectories
|
|
111
|
+
if step_idx < len(traj)
|
|
112
|
+
]
|
|
113
|
+
averaged.append(np.mean(step_payoffs))
|
|
114
|
+
|
|
115
|
+
return averaged
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def average_last_payoff(payoff_trajectories: list[list[float]]) -> float:
|
|
119
|
+
"""Return the average of the last payoff in each trajectory.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
payoff_trajectories: List of payoff trajectories, where each
|
|
123
|
+
trajectory is a list of payoffs.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Average of the last payoff across all trajectories.
|
|
127
|
+
"""
|
|
128
|
+
if not payoff_trajectories:
|
|
129
|
+
return 0.0
|
|
130
|
+
|
|
131
|
+
last_payoffs = [traj[-1] for traj in payoff_trajectories if traj]
|
|
132
|
+
return np.mean(last_payoffs)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Utilities for finding and selecting average performing landscapes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
from src.nk_model.models import NKParams
|
|
12
|
+
from src.nk_model.nk_landscape import NKLandscape
|
|
13
|
+
from src.simulation.hill_climber_simulation import (
|
|
14
|
+
AgentType,
|
|
15
|
+
HillClimberSimConfig,
|
|
16
|
+
HillClimberSimulation,
|
|
17
|
+
StopCondition,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_simulation_with_landscape(
|
|
22
|
+
landscape_uuid: str,
|
|
23
|
+
nk_params: NKParams,
|
|
24
|
+
hamming_distance: int,
|
|
25
|
+
max_iterations: int,
|
|
26
|
+
) -> float:
|
|
27
|
+
"""Run simulation on a specific landscape and return final payoff.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
landscape_uuid: UUID of the cached landscape
|
|
31
|
+
nk_params: NK model parameters
|
|
32
|
+
hamming_distance: Hamming distance for neighbor selection
|
|
33
|
+
max_iterations: Maximum number of iterations
|
|
34
|
+
start_idxs: List of starting indices
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Final payoff from the simulation
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
start_idxs = np.random.choice(list(range(2**nk_params.n)), 1)
|
|
41
|
+
|
|
42
|
+
config = HillClimberSimConfig(
|
|
43
|
+
nk_params=nk_params,
|
|
44
|
+
agent_type=AgentType.HILL_CLIMBER,
|
|
45
|
+
start_idxs=start_idxs,
|
|
46
|
+
stop_condition=StopCondition.MAX_ITERATIONS,
|
|
47
|
+
stop_condition_kwargs={"max_iterations": max_iterations},
|
|
48
|
+
hamming_distance=hamming_distance,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
simulation = HillClimberSimulation.from_cached_landscape(
|
|
52
|
+
config=config, landscape_uuid=landscape_uuid
|
|
53
|
+
)
|
|
54
|
+
trajectory = simulation.run()
|
|
55
|
+
|
|
56
|
+
final_idx = trajectory[-1]
|
|
57
|
+
final_payoff = simulation.ordered_payoffs[final_idx]
|
|
58
|
+
|
|
59
|
+
return final_payoff
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def select_average_landscape(
|
|
63
|
+
results: List[Tuple[str, float]],
|
|
64
|
+
percentile_low: float = 25.0,
|
|
65
|
+
percentile_high: float = 75.0,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Select an average landscape from tested results.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
results: List of tuples (landscape_uuid, average_payoff)
|
|
71
|
+
percentile_low: Lower percentile threshold (default: 25.0)
|
|
72
|
+
percentile_high: Upper percentile threshold (default: 75.0)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
UUID of the selected landscape
|
|
76
|
+
"""
|
|
77
|
+
# Sort by average payoff
|
|
78
|
+
results.sort(key=lambda x: x[1])
|
|
79
|
+
|
|
80
|
+
# Select from percentile range
|
|
81
|
+
start_idx = int((percentile_low / 100.0) * len(results))
|
|
82
|
+
end_idx = int((percentile_high / 100.0) * len(results))
|
|
83
|
+
candidates = results[start_idx:end_idx]
|
|
84
|
+
|
|
85
|
+
# Randomly select one landscape
|
|
86
|
+
selected_idx = np.random.choice(range(len(candidates)))
|
|
87
|
+
return candidates[selected_idx][0]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_average_landscape_for_k(
|
|
91
|
+
nk_params: NKParams,
|
|
92
|
+
num_landscapes: int = 100,
|
|
93
|
+
repetitions: int = 100,
|
|
94
|
+
hamming_distance: int = 1,
|
|
95
|
+
max_iterations: int = 10,
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Find an average landscape for a specific k value.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
nk_params: NK model parameters
|
|
101
|
+
num_landscapes: Number of landscapes to generate and test
|
|
102
|
+
repetitions: Number of repetitions per landscape
|
|
103
|
+
hamming_distance: Hamming distance for neighbor selection
|
|
104
|
+
max_iterations: Maximum number of iterations
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
UUID of the selected average landscape
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Generate landscapes
|
|
111
|
+
landscape_uuids = [
|
|
112
|
+
NKLandscape(params=nk_params).uuid for _ in range(num_landscapes)
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
# Test landscapes and collect results
|
|
116
|
+
results = []
|
|
117
|
+
for uuid in tqdm(landscape_uuids, desc="Testing landscapes"):
|
|
118
|
+
final_payoffs = []
|
|
119
|
+
for _ in range(repetitions):
|
|
120
|
+
payoff = run_simulation_with_landscape(
|
|
121
|
+
landscape_uuid=uuid,
|
|
122
|
+
nk_params=nk_params,
|
|
123
|
+
hamming_distance=hamming_distance,
|
|
124
|
+
max_iterations=max_iterations,
|
|
125
|
+
)
|
|
126
|
+
final_payoffs.append(payoff)
|
|
127
|
+
avg_payoff = np.mean(final_payoffs)
|
|
128
|
+
results.append((uuid, avg_payoff))
|
|
129
|
+
|
|
130
|
+
# Select average landscape
|
|
131
|
+
selected_uuid = select_average_landscape(results)
|
|
132
|
+
|
|
133
|
+
return selected_uuid
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def save_landscape_json(
|
|
137
|
+
landscape_uuid: str,
|
|
138
|
+
output_path: Path,
|
|
139
|
+
n: int,
|
|
140
|
+
k: int,
|
|
141
|
+
add_timestamp: bool = True,
|
|
142
|
+
) -> Path:
|
|
143
|
+
"""Save a landscape to disk as JSON.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
landscape_uuid: UUID of the landscape to save
|
|
147
|
+
output_path: Path where to save the JSON file
|
|
148
|
+
n: Number of components
|
|
149
|
+
k: Number of interactions
|
|
150
|
+
add_timestamp: Whether to add timestamp to filename
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Path to the saved file (with timestamp if added)
|
|
154
|
+
"""
|
|
155
|
+
landscape = NKLandscape.from_cache(landscape_uuid)
|
|
156
|
+
json_data = []
|
|
157
|
+
nk_str = f"({n}, {k})"
|
|
158
|
+
|
|
159
|
+
for item in landscape.items:
|
|
160
|
+
location = "".join([str(int(coord)) for coord in item.coordinates])
|
|
161
|
+
fitness = int(item.payoff)
|
|
162
|
+
|
|
163
|
+
json_data.append(
|
|
164
|
+
{"N_NK": nk_str, "location": location, "fitness": fitness}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Add timestamp to filename if requested
|
|
168
|
+
if add_timestamp:
|
|
169
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
170
|
+
stem = output_path.stem
|
|
171
|
+
suffix = output_path.suffix
|
|
172
|
+
output_path = output_path.parent / f"{stem}_{timestamp}{suffix}"
|
|
173
|
+
|
|
174
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
print(f"Saving landscape to {output_path}")
|
|
176
|
+
with open(output_path, "w") as f:
|
|
177
|
+
json.dump(json_data, f, indent=4)
|
|
178
|
+
|
|
179
|
+
return output_path
|
utils/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Binary conversion utilities for transforming between int, array, and string.
|
|
2
|
+
|
|
3
|
+
This module provides functions to convert between different representations
|
|
4
|
+
of binary states:
|
|
5
|
+
- Integer (0-255): Represents an 8-bit binary number
|
|
6
|
+
- Binary array: NumPy array of shape (8,) with values 0 or 1
|
|
7
|
+
- Binary string: String representation like "0,0,0,0,0,0,0,0"
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from src.utils.utils import BIN_ARRAY
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def int_to_binary_str(value: int, separator: str = ",") -> str:
|
|
16
|
+
"""Convert integer to binary string representation.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
value: Integer value (0-255) to convert
|
|
20
|
+
separator: String separator for binary digits. Defaults to ",".
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Binary string representation, e.g., "0,0,0,0,0,0,0,0"
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> int_to_binary_str(0)
|
|
27
|
+
'0,0,0,0,0,0,0,0'
|
|
28
|
+
>>> int_to_binary_str(255)
|
|
29
|
+
'1,1,1,1,1,1,1,1'
|
|
30
|
+
>>> int_to_binary_str(15)
|
|
31
|
+
'0,0,0,0,1,1,1,1'
|
|
32
|
+
"""
|
|
33
|
+
return separator.join([str(v) for v in BIN_ARRAY[value]])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def binary_str_to_int(binary_str: str, separator: str = ",") -> int:
|
|
37
|
+
"""Convert binary string to integer.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
binary_str: Binary string representation, e.g., "0,0,0,0,0,0,0,0"
|
|
41
|
+
separator: String separator used in binary_str. Defaults to ",".
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Integer value (0-255)
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
>>> binary_str_to_int("0,0,0,0,0,0,0,0")
|
|
48
|
+
0
|
|
49
|
+
>>> binary_str_to_int("1,1,1,1,1,1,1,1")
|
|
50
|
+
255
|
|
51
|
+
>>> binary_str_to_int("0,0,0,0,1,1,1,1")
|
|
52
|
+
15
|
|
53
|
+
"""
|
|
54
|
+
return int(binary_str.replace(separator, ""), 2)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def int_to_binary_array(value: int) -> np.ndarray:
|
|
58
|
+
"""Convert integer to binary array.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
value: Integer value (0-255) to convert
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Binary array of shape (8,) with values 0 or 1
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
>>> int_to_binary_array(0)
|
|
68
|
+
array([0, 0, 0, 0, 0, 0, 0, 0])
|
|
69
|
+
>>> int_to_binary_array(255)
|
|
70
|
+
array([1, 1, 1, 1, 1, 1, 1, 1])
|
|
71
|
+
"""
|
|
72
|
+
return BIN_ARRAY[value].copy()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def binary_array_to_int(binary_array: np.ndarray) -> int:
|
|
76
|
+
"""Convert binary array to integer.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
binary_array: Binary array of shape (8,) with values 0 or 1
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Integer value (0-255)
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
>>> binary_array_to_int(np.array([0, 0, 0, 0, 0, 0, 0, 0]))
|
|
86
|
+
0
|
|
87
|
+
>>> binary_array_to_int(np.array([1, 1, 1, 1, 1, 1, 1, 1]))
|
|
88
|
+
255
|
|
89
|
+
"""
|
|
90
|
+
return int("".join([str(int(v)) for v in binary_array]), 2)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def binary_array_to_str(binary_array: np.ndarray, separator: str = ",") -> str:
|
|
94
|
+
"""Convert binary array to string representation.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
binary_array: Binary array of shape (n,) with values 0 or 1
|
|
98
|
+
separator: String separator for binary digits. Defaults to ",".
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Binary string representation, e.g., "1,0,1,0,1"
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
>>> binary_array_to_str(np.array([1, 0, 1, 0, 1]))
|
|
105
|
+
'1,0,1,0,1'
|
|
106
|
+
>>> binary_array_to_str(np.array([1, 0, 1]), separator=" ")
|
|
107
|
+
'1 0 1'
|
|
108
|
+
"""
|
|
109
|
+
return separator.join([str(int(item)) for item in binary_array])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def binary_str_to_array(binary_str: str, separator: str = ",") -> np.ndarray:
|
|
113
|
+
"""Convert binary string to array.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
binary_str: Binary string representation, e.g., "0,0,0,0,0,0,0,0"
|
|
117
|
+
separator: String separator used in binary_str. Defaults to ",".
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Binary array of shape (n,) with values 0 or 1
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
>>> binary_str_to_array("0,0,0,0,0,0,0,0")
|
|
124
|
+
array([0, 0, 0, 0, 0, 0, 0, 0])
|
|
125
|
+
>>> binary_str_to_array("1,1,1,1,1,1,1,1")
|
|
126
|
+
array([1, 1, 1, 1, 1, 1, 1, 1])
|
|
127
|
+
"""
|
|
128
|
+
return np.array([int(x) for x in binary_str.split(separator)])
|
utils/logging.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
from time import time
|
|
4
|
+
|
|
5
|
+
# logging settings
|
|
6
|
+
logging.basicConfig(
|
|
7
|
+
format=("%(asctime)s %(levelname)-8s %(message)s"),
|
|
8
|
+
# log.INFO for normal run
|
|
9
|
+
level=logging.INFO,
|
|
10
|
+
# log.DEBUG for diagnostics
|
|
11
|
+
# level=logging.DEBUG,
|
|
12
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def log_dec(func):
|
|
17
|
+
@functools.wraps(func)
|
|
18
|
+
def wrapper(*args, **kwargs):
|
|
19
|
+
logger = logging.getLogger(func.__module__)
|
|
20
|
+
try:
|
|
21
|
+
start_time = time()
|
|
22
|
+
logger.info("%-30s started", func.__name__)
|
|
23
|
+
return func(*args, **kwargs)
|
|
24
|
+
except Exception as ex:
|
|
25
|
+
logger.error(f"%-30s raised an exception: {ex}", func.__name__)
|
|
26
|
+
raise ex
|
|
27
|
+
finally:
|
|
28
|
+
duration = time() - start_time
|
|
29
|
+
logger.info(
|
|
30
|
+
"%-30s finished in %s seconds", func.__name__, duration
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return wrapper
|