nimo 2.1.2__tar.gz → 2.1.4__tar.gz
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.
- {nimo-2.1.2 → nimo-2.1.4}/PKG-INFO +3 -1
- {nimo-2.1.2 → nimo-2.1.4}/README.md +1 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/__init__.py +1 -0
- nimo-2.1.4/nimo/ai_tools/ai_tool_llmep.py +346 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/nimo_modules.py +28 -1
- {nimo-2.1.2 → nimo-2.1.4}/nimo/visualization/plot_distribution.py +13 -6
- {nimo-2.1.2 → nimo-2.1.4}/nimo/visualization/plot_history.py +30 -7
- {nimo-2.1.2 → nimo-2.1.4}/nimo/visualization/plot_phase_diagram.py +17 -8
- {nimo-2.1.2 → nimo-2.1.4}/nimo.egg-info/PKG-INFO +3 -1
- {nimo-2.1.2 → nimo-2.1.4}/nimo.egg-info/SOURCES.txt +1 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo.egg-info/requires.txt +1 -0
- {nimo-2.1.2 → nimo-2.1.4}/pyproject.toml +3 -2
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_blox.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_bomp.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_combi.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_doe.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_es.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_nts.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_pdc.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_physbo.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_ptr.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_re.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_rsvm.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_slesa.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/ai_tools/ai_tool_slesa_WAM.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/input_tools/preparation_input_certus.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/input_tools/preparation_input_certus_sc.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/input_tools/preparation_input_combat.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/input_tools/preparation_input_naree.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/input_tools/preparation_input_standard.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/output_tools/analysis_output_combat.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/output_tools/analysis_output_insert.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/output_tools/analysis_output_naree.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/output_tools/analysis_output_sd8.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/output_tools/analysis_output_standard.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo/output_tools/analysis_output_update.py +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo.egg-info/dependency_links.txt +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/nimo.egg-info/top_level.txt +0 -0
- {nimo-2.1.2 → nimo-2.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nimo
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: NIMO package
|
|
5
5
|
Author: NIMO developers
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,7 @@ Requires-Dist: physbo>=3.1.0
|
|
|
11
11
|
Requires-Dist: scikit-learn
|
|
12
12
|
Requires-Dist: scipy
|
|
13
13
|
Requires-Dist: pyDOE3
|
|
14
|
+
Requires-Dist: anthropic
|
|
14
15
|
|
|
15
16
|
# nimo
|
|
16
17
|
|
|
@@ -33,6 +34,7 @@ NIMO is a Python library to realize a closed loop of robotic experiments and art
|
|
|
33
34
|
- scikit-learn
|
|
34
35
|
- scipy
|
|
35
36
|
- pyDOE3
|
|
37
|
+
- anthropic
|
|
36
38
|
|
|
37
39
|
# Install
|
|
38
40
|
|
|
@@ -23,6 +23,7 @@ from .ai_tools import ai_tool_es
|
|
|
23
23
|
from .ai_tools import ai_tool_combi
|
|
24
24
|
from .ai_tools import ai_tool_nts
|
|
25
25
|
from .ai_tools import ai_tool_doe
|
|
26
|
+
from .ai_tools import ai_tool_llmep
|
|
26
27
|
|
|
27
28
|
from .input_tools import preparation_input_standard
|
|
28
29
|
from .input_tools import preparation_input_naree
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import csv
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import anthropic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LLMEP():
|
|
9
|
+
"""Class of LLMEP
|
|
10
|
+
|
|
11
|
+
This class selects the next experimental candidates using a Large Language Model (LLM).
|
|
12
|
+
It is a standalone class with no dependency on ai_tool_re.
|
|
13
|
+
|
|
14
|
+
The user provides a Markdown prompt file that describes the experimental context.
|
|
15
|
+
The candidates CSV and any observed data are injected automatically into the prompt.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, input_file, output_file, num_objectives, num_proposals,
|
|
20
|
+
prompt_file, system_prompt_file, llm_model,
|
|
21
|
+
num_runs, log_file, api_key, max_tokens):
|
|
22
|
+
"""Constructor
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
input_file (str): the file for candidates (CSV with descriptor columns + objective columns)
|
|
26
|
+
output_file (str): the file for proposals output
|
|
27
|
+
num_objectives (int): the number of objective columns (rightmost columns in CSV)
|
|
28
|
+
num_proposals (int): the number of proposals to select
|
|
29
|
+
prompt_file (str): path to the Markdown file describing the experimental context
|
|
30
|
+
system_prompt_file (str): path to a file containing the system prompt.
|
|
31
|
+
llm_model (str): Anthropic model ID to use
|
|
32
|
+
num_runs (int): number of independent LLM selection runs (for majority vote)
|
|
33
|
+
log_file (str): if provided, save the full selection log as Markdown
|
|
34
|
+
api_key (str): Anthropic API key. If None, reads from ANTHROPIC_API_KEY env variable.
|
|
35
|
+
max_tokens (int): maximum number of tokens in the LLM response (default: 8192).
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
self.input_file = input_file
|
|
39
|
+
self.output_file = output_file
|
|
40
|
+
self.num_objectives = num_objectives
|
|
41
|
+
self.num_proposals = num_proposals
|
|
42
|
+
self.prompt_file = prompt_file
|
|
43
|
+
self.system_prompt_file = system_prompt_file
|
|
44
|
+
self.model = llm_model
|
|
45
|
+
self.num_runs = num_runs
|
|
46
|
+
if num_runs == None: self.num_runs = 10
|
|
47
|
+
self.log_file = log_file
|
|
48
|
+
if log_file == None: self.log_file = "log"
|
|
49
|
+
self.client = anthropic.Anthropic(api_key = api_key)
|
|
50
|
+
self.max_tokens = max_tokens
|
|
51
|
+
if max_tokens == None: self.max_tokens = 8192
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
# Data loading (ported from RE.load_data)
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def load_data(self):
|
|
59
|
+
"""Load candidates CSV and split into observed / unobserved rows.
|
|
60
|
+
|
|
61
|
+
t_train is returned as a list of raw strings for compatibility with
|
|
62
|
+
calc_ai's signature, but LLMEP does not use it internally.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
t_train (list[list[str]]): observed objective values (raw strings)
|
|
66
|
+
X_all (list[list[str]]): all descriptor rows (raw strings; numeric, categorical, and text all accepted)
|
|
67
|
+
train_actions (np.ndarray): indices of observed rows
|
|
68
|
+
test_actions (list[int]): indices of unobserved rows
|
|
69
|
+
"""
|
|
70
|
+
with open(self.input_file, 'r') as f:
|
|
71
|
+
reader = csv.reader(f)
|
|
72
|
+
_headers = next(reader)
|
|
73
|
+
raw_rows = list(reader)
|
|
74
|
+
|
|
75
|
+
X_all = []
|
|
76
|
+
t_train = []
|
|
77
|
+
train_actions = []
|
|
78
|
+
test_actions = []
|
|
79
|
+
|
|
80
|
+
for idx, row in enumerate(raw_rows):
|
|
81
|
+
descriptors = row[:-self.num_objectives] # kept as strings (numeric, categorical, text all accepted)
|
|
82
|
+
objectives = row[-self.num_objectives:]
|
|
83
|
+
X_all.append(descriptors)
|
|
84
|
+
# A row is "observed" if all objective cells are non-empty
|
|
85
|
+
if all(v.strip() != '' for v in objectives):
|
|
86
|
+
t_train.append(objectives)
|
|
87
|
+
train_actions.append(idx)
|
|
88
|
+
else:
|
|
89
|
+
test_actions.append(idx)
|
|
90
|
+
|
|
91
|
+
train_actions = np.array(train_actions)
|
|
92
|
+
|
|
93
|
+
return t_train, X_all, train_actions, test_actions
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Internal helpers
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def _load_prompt(self):
|
|
101
|
+
"""Load the user-supplied Markdown prompt file."""
|
|
102
|
+
with open(self.prompt_file, 'r', encoding='utf-8', errors='replace') as f:
|
|
103
|
+
return f.read()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_candidates_text(self, X_all, train_actions, test_actions):
|
|
107
|
+
"""Build a text table of all candidates for injection into the prompt.
|
|
108
|
+
|
|
109
|
+
Descriptor values are kept as raw strings, supporting numeric, categorical, and free-text descriptors.
|
|
110
|
+
Observed rows include their measured objective value; unobserved rows show 'unknown'.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
str: CSV-like text block
|
|
114
|
+
"""
|
|
115
|
+
with open(self.input_file, 'r') as f:
|
|
116
|
+
reader = csv.reader(f)
|
|
117
|
+
headers = next(reader)
|
|
118
|
+
raw_rows = list(reader)
|
|
119
|
+
|
|
120
|
+
descriptor_headers = headers[:-self.num_objectives]
|
|
121
|
+
objective_headers = headers[-self.num_objectives:]
|
|
122
|
+
|
|
123
|
+
lines = ["index," + ",".join(descriptor_headers) + "," + ",".join(objective_headers)]
|
|
124
|
+
for idx, row in enumerate(raw_rows):
|
|
125
|
+
descriptors = row[:-self.num_objectives]
|
|
126
|
+
objectives_str = [v if v.strip() != '' else 'unknown' for v in row[-self.num_objectives:]]
|
|
127
|
+
lines.append(str(idx) + "," + ",".join(descriptors) + "," + ",".join(objectives_str))
|
|
128
|
+
|
|
129
|
+
return "\n".join(lines), objective_headers
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_messages(self, user_prompt_md, candidates_text, objective_headers):
|
|
133
|
+
"""Construct the system prompt and user message for the single LLM call."""
|
|
134
|
+
with open(self.system_prompt_file, 'r', encoding='utf-8') as f:
|
|
135
|
+
system = f.read()
|
|
136
|
+
|
|
137
|
+
obj_cols = ", ".join(f"`{h}`" for h in objective_headers)
|
|
138
|
+
is_are = 'is' if len(objective_headers) == 1 else 'are'
|
|
139
|
+
|
|
140
|
+
user = (
|
|
141
|
+
f"{user_prompt_md}\n"
|
|
142
|
+
"\n---\n"
|
|
143
|
+
"\n## Candidates\n"
|
|
144
|
+
"\nThe following table lists all candidate experimental conditions.\n"
|
|
145
|
+
f"- Rows with a value in {obj_cols} are already observed data points.\n"
|
|
146
|
+
f"- Rows where {obj_cols} {is_are} 'unknown' have not yet been measured.\n"
|
|
147
|
+
"\n```\n"
|
|
148
|
+
f"{candidates_text}\n"
|
|
149
|
+
"```\n"
|
|
150
|
+
"\n---\n"
|
|
151
|
+
"\n## Your Task\n"
|
|
152
|
+
f"\nPerform {self.num_runs} independent selection runs, then aggregate by majority vote.\n"
|
|
153
|
+
"\n### Step 1 Independent Runs\n"
|
|
154
|
+
f"\nFor each run (Run 1 through Run {self.num_runs}):\n"
|
|
155
|
+
"- Choose a **distinct exploration strategy**.\n"
|
|
156
|
+
f"- Select exactly {self.num_proposals} indices from the 'index' column above.\n"
|
|
157
|
+
f"- Only select rows where {obj_cols} {is_are} 'unknown'.\n"
|
|
158
|
+
"- Document the strategy and the selected indices.\n"
|
|
159
|
+
|
|
160
|
+
"\n### Step 2 Majority Vote\n"
|
|
161
|
+
f"\nAfter all {self.num_runs} runs:\n"
|
|
162
|
+
"- Count how many times each index was selected across all runs.\n"
|
|
163
|
+
"- Rank indices by vote count (descending). Break ties by preferring lower index values.\n"
|
|
164
|
+
f"- Select the top {self.num_proposals} indices as the final proposals.\n"
|
|
165
|
+
"- Document the vote counts for all indices that received at least one vote.\n"
|
|
166
|
+
"\n### Output Format\n"
|
|
167
|
+
"\nAfter your reasoning, output **exactly one** JSON object in the following format.\n"
|
|
168
|
+
"This must be the last thing in your response.\n"
|
|
169
|
+
"\n```json\n"
|
|
170
|
+
"{\n"
|
|
171
|
+
' "runs": [\n'
|
|
172
|
+
' {\n'
|
|
173
|
+
' "run": 1,\n'
|
|
174
|
+
' "strategy": "<one-line description>",\n'
|
|
175
|
+
' "selected_indices": [<index1>, <index2>, ...]\n'
|
|
176
|
+
' },\n'
|
|
177
|
+
' ...\n'
|
|
178
|
+
' ],\n'
|
|
179
|
+
' "vote_counts": {\n'
|
|
180
|
+
' "<index>": <count>,\n'
|
|
181
|
+
' ...\n'
|
|
182
|
+
' },\n'
|
|
183
|
+
' "final_selected_indices": [<index1>, <index2>, ...],\n'
|
|
184
|
+
' "reasons": {\n'
|
|
185
|
+
' "<index>": "<one-sentence reason why this composition was selected>",\n'
|
|
186
|
+
' ...\n'
|
|
187
|
+
' }\n'
|
|
188
|
+
"}\n"
|
|
189
|
+
"```\n"
|
|
190
|
+
)
|
|
191
|
+
return [{"role": "user", "content": user}], system
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _parse_result(self, response_text):
|
|
195
|
+
"""Extract the result JSON from the LLM response.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ValueError: if parsing fails
|
|
199
|
+
"""
|
|
200
|
+
# Try last ```json ... ``` block first
|
|
201
|
+
json_blocks = re.findall(r'```json\s*(.*?)\s*```', response_text, re.DOTALL)
|
|
202
|
+
if json_blocks:
|
|
203
|
+
try:
|
|
204
|
+
return json.loads(json_blocks[-1])
|
|
205
|
+
except json.JSONDecodeError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# Fallback: find the last {...} containing final_selected_indices
|
|
209
|
+
for m in reversed(list(re.finditer(r'\{', response_text))):
|
|
210
|
+
candidate = response_text[m.start():]
|
|
211
|
+
depth, end = 0, -1
|
|
212
|
+
for i, ch in enumerate(candidate):
|
|
213
|
+
if ch == '{':
|
|
214
|
+
depth += 1
|
|
215
|
+
elif ch == '}':
|
|
216
|
+
depth -= 1
|
|
217
|
+
if depth == 0:
|
|
218
|
+
end = i
|
|
219
|
+
break
|
|
220
|
+
if end != -1:
|
|
221
|
+
try:
|
|
222
|
+
data = json.loads(candidate[:end + 1])
|
|
223
|
+
if "final_selected_indices" in data:
|
|
224
|
+
return data
|
|
225
|
+
except json.JSONDecodeError:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Could not parse result JSON from LLM response:\n{response_text[:500]}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
# Core selection
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
def calc_ai(self, t_train, X_all, train_actions, test_actions):
|
|
238
|
+
"""Select proposals by delegating all runs + majority vote to the LLM.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
t_train (np.ndarray): observed objective values
|
|
242
|
+
X_all (np.ndarray): all descriptor rows
|
|
243
|
+
train_actions (np.ndarray): indices of observed rows
|
|
244
|
+
test_actions (list[int]): indices of unobserved rows
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
actions (list[int]): selected action indices (length = num_proposals)
|
|
248
|
+
"""
|
|
249
|
+
user_prompt_md = self._load_prompt()
|
|
250
|
+
candidates_text, objective_headers = self._build_candidates_text(X_all, train_actions, test_actions)
|
|
251
|
+
|
|
252
|
+
print(f" [LLMEP] Sending request ({self.num_runs} runs + majority vote) ...")
|
|
253
|
+
|
|
254
|
+
messages, system = self._build_messages(user_prompt_md, candidates_text, objective_headers)
|
|
255
|
+
|
|
256
|
+
response = self.client.messages.create(
|
|
257
|
+
model=self.model,
|
|
258
|
+
max_tokens=self.max_tokens,
|
|
259
|
+
system=system,
|
|
260
|
+
messages=messages,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
response_text = response.content[0].text
|
|
264
|
+
result = self._parse_result(response_text)
|
|
265
|
+
|
|
266
|
+
# Validate: keep only indices that are actually unobserved
|
|
267
|
+
test_actions_set = set(test_actions)
|
|
268
|
+
actions = [int(i) for i in result["final_selected_indices"] if int(i) in test_actions_set]
|
|
269
|
+
reasons = result.get("reasons", {})
|
|
270
|
+
|
|
271
|
+
if len(actions) < self.num_proposals:
|
|
272
|
+
print(f" [LLMEP] Warning: only {len(actions)} valid indices returned "
|
|
273
|
+
f"(expected {self.num_proposals}). Padding with remaining candidates.")
|
|
274
|
+
used = set(actions)
|
|
275
|
+
for a in test_actions:
|
|
276
|
+
if len(actions) >= self.num_proposals:
|
|
277
|
+
break
|
|
278
|
+
if a not in used:
|
|
279
|
+
actions.append(a)
|
|
280
|
+
|
|
281
|
+
actions = actions[:self.num_proposals]
|
|
282
|
+
|
|
283
|
+
if self.log_file:
|
|
284
|
+
log_lines = [
|
|
285
|
+
"# LLMEP Selection Log\n",
|
|
286
|
+
f"Model : {self.model}",
|
|
287
|
+
f"Runs : {self.num_runs}",
|
|
288
|
+
f"Proposals: {self.num_proposals}\n",
|
|
289
|
+
"---\n",
|
|
290
|
+
"## Full LLM Response\n",
|
|
291
|
+
response_text,
|
|
292
|
+
"\n---\n",
|
|
293
|
+
"## Final Selected Indices (Python-validated)\n",
|
|
294
|
+
]
|
|
295
|
+
for rank, action in enumerate(actions, 1):
|
|
296
|
+
reason = reasons.get(str(action), "")
|
|
297
|
+
log_lines.append(f" {rank}. index={action}, descriptors={X_all[action]}, reason={reason}")
|
|
298
|
+
with open(self.log_file, 'w', encoding='utf-8') as f:
|
|
299
|
+
f.write("\n".join(log_lines))
|
|
300
|
+
print(f" [LLMEP] Log saved to {self.log_file}")
|
|
301
|
+
|
|
302
|
+
print(f" [LLMEP] Final proposals: {actions}")
|
|
303
|
+
return actions, reasons
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
# Entry point (ported from RE.select)
|
|
308
|
+
# ------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def select(self):
|
|
311
|
+
"""Run the full selection pipeline and write proposals CSV.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
str: "True" on success
|
|
315
|
+
"""
|
|
316
|
+
print("Start selection of proposals by LLMEP!")
|
|
317
|
+
|
|
318
|
+
t_train, X_all, train_actions, test_actions = self.load_data()
|
|
319
|
+
|
|
320
|
+
actions, reasons = self.calc_ai(
|
|
321
|
+
t_train=t_train, X_all=X_all,
|
|
322
|
+
train_actions=train_actions, test_actions=test_actions,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
print('Proposals')
|
|
326
|
+
|
|
327
|
+
with open(self.input_file, 'r') as f:
|
|
328
|
+
indexes = f.readlines()[0].rstrip('\n').split(',')
|
|
329
|
+
|
|
330
|
+
indexes = ["actions"] + indexes[:-self.num_objectives]
|
|
331
|
+
proposals_all = [indexes]
|
|
332
|
+
|
|
333
|
+
for i, action in enumerate(actions):
|
|
334
|
+
row = [str(action)] + [str(v) for v in X_all[action]]
|
|
335
|
+
proposals_all.append(row)
|
|
336
|
+
print("###")
|
|
337
|
+
print("number =", i + 1)
|
|
338
|
+
print("actions =", action)
|
|
339
|
+
print("proposal =", X_all[action])
|
|
340
|
+
print("###")
|
|
341
|
+
|
|
342
|
+
with open(self.output_file, 'w', newline="") as f:
|
|
343
|
+
csv.writer(f).writerows(proposals_all)
|
|
344
|
+
|
|
345
|
+
print("Finish selection of proposals!")
|
|
346
|
+
return "True"
|
|
@@ -21,7 +21,10 @@ class selection():
|
|
|
21
21
|
combi_ranges = None, spread_elements = None,
|
|
22
22
|
sample_mode = None,
|
|
23
23
|
mode = None, max_iter = None,
|
|
24
|
-
output_res = None, training_res = None
|
|
24
|
+
output_res = None, training_res = None,
|
|
25
|
+
prompt_file = None, system_prompt_file = None,
|
|
26
|
+
llm_model = None, num_runs = None,
|
|
27
|
+
log_file = None, api_key = None, max_tokens = None):
|
|
25
28
|
|
|
26
29
|
"""Constructor
|
|
27
30
|
|
|
@@ -49,6 +52,13 @@ class selection():
|
|
|
49
52
|
max_iter (int): max iteration for DOE
|
|
50
53
|
output_res (str): True or False to output res file
|
|
51
54
|
training_res (str): True or False to training res file
|
|
55
|
+
prompt_file (str): path to the Markdown file describing the experimental context
|
|
56
|
+
system_prompt_file (str): path to a file containing the system prompt.
|
|
57
|
+
llm_model (str): Anthropic model ID to use
|
|
58
|
+
num_runs (int): number of independent LLM selection runs (for majority vote)
|
|
59
|
+
log_file (str): if provided, save the full selection log as Markdown
|
|
60
|
+
api_key (str): Anthropic API key. If None, reads from ANTHROPIC_API_KEY env variable.
|
|
61
|
+
max_tokens (int): maximum number of tokens in the LLM response (default: 8192).
|
|
52
62
|
|
|
53
63
|
"""
|
|
54
64
|
|
|
@@ -80,6 +90,16 @@ class selection():
|
|
|
80
90
|
self.output_res = output_res
|
|
81
91
|
self.training_res = training_res
|
|
82
92
|
|
|
93
|
+
|
|
94
|
+
self.prompt_file = prompt_file
|
|
95
|
+
self.system_prompt_file = system_prompt_file
|
|
96
|
+
self.llm_model = llm_model
|
|
97
|
+
self.num_runs = num_runs
|
|
98
|
+
self.log_file = log_file
|
|
99
|
+
self.api_key = api_key
|
|
100
|
+
self.max_tokens = max_tokens
|
|
101
|
+
|
|
102
|
+
|
|
83
103
|
res = self.module_selection()
|
|
84
104
|
|
|
85
105
|
|
|
@@ -153,6 +173,13 @@ class selection():
|
|
|
153
173
|
res = nimo.ai_tools.ai_tool_doe.DOE(self.input_file, self.output_file,
|
|
154
174
|
self.num_objectives, self.num_proposals, self.mode, self.max_iter).select()
|
|
155
175
|
return res
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if self.method == "LLMEP":
|
|
179
|
+
res = nimo.ai_tools.ai_tool_llmep.LLMEP(self.input_file, self.output_file,
|
|
180
|
+
self.num_objectives, self.num_proposals,
|
|
181
|
+
self.prompt_file, self.system_prompt_file, self.llm_model,
|
|
182
|
+
self.num_runs, self.log_file, self.api_key, self.max_tokens).select()
|
|
156
183
|
|
|
157
184
|
|
|
158
185
|
class preparation_input():
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import csv
|
|
3
|
+
import os
|
|
3
4
|
|
|
4
5
|
import matplotlib.pyplot as plt
|
|
5
6
|
import time
|
|
6
7
|
from mpl_toolkits.mplot3d import Axes3D
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def plot(input_file, num_objectives, fig_folder = None, dpi = None):
|
|
10
|
+
def plot(input_file, num_objectives, fig_folder = None, filename = None, dpi = None):
|
|
10
11
|
"""Loading candidates
|
|
11
12
|
|
|
12
13
|
This function do not depend on robot.
|
|
@@ -17,18 +18,24 @@ def plot(input_file, num_objectives, fig_folder = None, dpi = None):
|
|
|
17
18
|
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
|
-
if fig_folder
|
|
21
|
+
if fig_folder is None:
|
|
21
22
|
fig_path = "./fig"
|
|
22
23
|
else:
|
|
23
24
|
fig_path = fig_folder
|
|
24
25
|
|
|
25
26
|
t_train, X_all, train_actions, test_actions = load_data(input_file, num_objectives)
|
|
26
27
|
|
|
27
|
-
if dpi
|
|
28
|
+
if dpi is None:
|
|
28
29
|
dpi = 72
|
|
29
30
|
|
|
30
31
|
dt_now = time.localtime()
|
|
31
32
|
|
|
33
|
+
if filename is None:
|
|
34
|
+
name = "distribution_" + time.strftime('%y%m%d%H%M%S', dt_now) + ".png"
|
|
35
|
+
else:
|
|
36
|
+
name = filename
|
|
37
|
+
|
|
38
|
+
|
|
32
39
|
if num_objectives == 1:
|
|
33
40
|
|
|
34
41
|
fig = plt.figure()
|
|
@@ -36,7 +43,7 @@ def plot(input_file, num_objectives, fig_folder = None, dpi = None):
|
|
|
36
43
|
plt.hist(t_train)
|
|
37
44
|
plt.xlabel("Objectives")
|
|
38
45
|
plt.ylabel("Counts")
|
|
39
|
-
plt.savefig(
|
|
46
|
+
plt.savefig(os.path.join(fig_path, name), dpi = dpi)
|
|
40
47
|
plt.clf()
|
|
41
48
|
plt.close()
|
|
42
49
|
|
|
@@ -55,7 +62,7 @@ def plot(input_file, num_objectives, fig_folder = None, dpi = None):
|
|
|
55
62
|
plt.scatter(x, y)
|
|
56
63
|
plt.xlabel("Objective 1")
|
|
57
64
|
plt.ylabel("Objective 2")
|
|
58
|
-
plt.savefig(
|
|
65
|
+
plt.savefig(os.path.join(fig_path, name), dpi = dpi)
|
|
59
66
|
plt.clf()
|
|
60
67
|
plt.close()
|
|
61
68
|
|
|
@@ -78,7 +85,7 @@ def plot(input_file, num_objectives, fig_folder = None, dpi = None):
|
|
|
78
85
|
ax.set_xlabel("Objective 1")
|
|
79
86
|
ax.set_ylabel("Objective 2")
|
|
80
87
|
ax.set_zlabel("Objective 3")
|
|
81
|
-
plt.savefig(
|
|
88
|
+
plt.savefig(os.path.join(fig_path, name), dpi = dpi)
|
|
82
89
|
plt.clf()
|
|
83
90
|
plt.close()
|
|
84
91
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import matplotlib.pyplot as plt
|
|
2
2
|
import time
|
|
3
3
|
import collections
|
|
4
|
+
import os
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
def cycle(input_file, num_cycles, fig_folder = None, dpi = None):
|
|
7
|
+
def cycle(input_file, num_cycles, fig_folder = None, filename = None, dpi = None):
|
|
7
8
|
"""Creating the figure of datapoints depending on the cycles
|
|
8
9
|
|
|
9
10
|
This function do not depend on robot.
|
|
@@ -14,12 +15,12 @@ def cycle(input_file, num_cycles, fig_folder = None, dpi = None):
|
|
|
14
15
|
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
|
-
if fig_folder
|
|
18
|
+
if fig_folder is None:
|
|
18
19
|
fig_path = "./fig"
|
|
19
20
|
else:
|
|
20
21
|
fig_path = fig_folder
|
|
21
22
|
|
|
22
|
-
if dpi
|
|
23
|
+
if dpi is None:
|
|
23
24
|
dpi = 72
|
|
24
25
|
|
|
25
26
|
obs_itt = []
|
|
@@ -34,18 +35,28 @@ def cycle(input_file, num_cycles, fig_folder = None, dpi = None):
|
|
|
34
35
|
|
|
35
36
|
for i in range(len(obs_y[0])):
|
|
36
37
|
|
|
38
|
+
if filename is None:
|
|
39
|
+
name = "history_step_" + time.strftime('%y%m%d%H%M%S', dt_now) + "_" + str(i+1)+ ".png"
|
|
40
|
+
else:
|
|
41
|
+
if len(obs_y[0]) == 1: # Use the provided name when the number of objectives is 1
|
|
42
|
+
name = filename
|
|
43
|
+
else:
|
|
44
|
+
root, ext = os.path.splitext(filename)
|
|
45
|
+
name = f"{root}_{i+1}{ext}"
|
|
46
|
+
|
|
47
|
+
|
|
37
48
|
fig = plt.figure()
|
|
38
49
|
|
|
39
50
|
plt.scatter(obs_itt, [r[i] for r in obs_y], alpha=0.7)
|
|
40
51
|
plt.xlim(0, num_cycles)
|
|
41
52
|
plt.xlabel("Cycle")
|
|
42
53
|
plt.ylabel("Objective"+str(i+1))
|
|
43
|
-
plt.savefig(
|
|
54
|
+
plt.savefig(os.path.join(fig_path, name), dpi = dpi)
|
|
44
55
|
plt.clf()
|
|
45
56
|
plt.close()
|
|
46
57
|
|
|
47
58
|
|
|
48
|
-
def best(input_file, num_cycles, fig_folder = None):
|
|
59
|
+
def best(input_file, num_cycles, fig_folder = None, filename = None, dpi = None):
|
|
49
60
|
"""Creating the figure of best datapoints depending on the cycles
|
|
50
61
|
|
|
51
62
|
This function do not depend on robot.
|
|
@@ -56,11 +67,14 @@ def best(input_file, num_cycles, fig_folder = None):
|
|
|
56
67
|
|
|
57
68
|
"""
|
|
58
69
|
|
|
59
|
-
if fig_folder
|
|
70
|
+
if fig_folder is None:
|
|
60
71
|
fig_path = "./fig"
|
|
61
72
|
else:
|
|
62
73
|
fig_path = fig_folder
|
|
63
74
|
|
|
75
|
+
if dpi is None:
|
|
76
|
+
dpi = 72
|
|
77
|
+
|
|
64
78
|
obs_itt = []
|
|
65
79
|
obs_y = []
|
|
66
80
|
|
|
@@ -105,6 +119,15 @@ def best(input_file, num_cycles, fig_folder = None):
|
|
|
105
119
|
best_y.append(max_list[target_index[j]])
|
|
106
120
|
|
|
107
121
|
|
|
122
|
+
if filename is None:
|
|
123
|
+
name = "history_best_" + time.strftime('%y%m%d%H%M%S', dt_now) + "_" + str(i+1)+ ".png"
|
|
124
|
+
else:
|
|
125
|
+
if len(obs_y[0]) == 1: # Use the provided name when the number of objectives is 1
|
|
126
|
+
name = filename
|
|
127
|
+
else:
|
|
128
|
+
root, ext = os.path.splitext(filename)
|
|
129
|
+
name = f"{root}_{i+1}{ext}"
|
|
130
|
+
|
|
108
131
|
fig = plt.figure()
|
|
109
132
|
|
|
110
133
|
plt.scatter(best_itt, best_y)
|
|
@@ -112,6 +135,6 @@ def best(input_file, num_cycles, fig_folder = None):
|
|
|
112
135
|
plt.xlim(0, num_cycles)
|
|
113
136
|
plt.xlabel("Cycle")
|
|
114
137
|
plt.ylabel("Best objective"+str(i+1))
|
|
115
|
-
plt.savefig(
|
|
138
|
+
plt.savefig(os.path.join(fig_path, name), dpi = dpi)
|
|
116
139
|
plt.clf()
|
|
117
140
|
plt.close()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import csv
|
|
3
|
+
import os
|
|
3
4
|
|
|
4
5
|
import matplotlib.pyplot as plt
|
|
5
6
|
import matplotlib.cm as cm
|
|
@@ -10,7 +11,8 @@ import sklearn.semi_supervised
|
|
|
10
11
|
from sklearn.preprocessing import StandardScaler
|
|
11
12
|
from scipy.spatial import distance
|
|
12
13
|
|
|
13
|
-
def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None,
|
|
14
|
+
def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None,
|
|
15
|
+
filename_phase = None, filename_diagram = None, dpi = None):
|
|
14
16
|
"""Loading candidates
|
|
15
17
|
|
|
16
18
|
This function do not depend on robot.
|
|
@@ -20,7 +22,7 @@ def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None
|
|
|
20
22
|
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
|
-
if fig_folder
|
|
25
|
+
if fig_folder is None:
|
|
24
26
|
fig_path = "./fig"
|
|
25
27
|
else:
|
|
26
28
|
fig_path = fig_folder
|
|
@@ -29,10 +31,10 @@ def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None
|
|
|
29
31
|
|
|
30
32
|
LP_algorithm = pdc_estimation
|
|
31
33
|
|
|
32
|
-
if pdc_estimation
|
|
34
|
+
if pdc_estimation is None:
|
|
33
35
|
LP_algorithm = 'LP'
|
|
34
36
|
|
|
35
|
-
if dpi
|
|
37
|
+
if dpi is None:
|
|
36
38
|
dpi = 72
|
|
37
39
|
|
|
38
40
|
#Preparation of data
|
|
@@ -93,6 +95,13 @@ def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None
|
|
|
93
95
|
|
|
94
96
|
dt_now = time.localtime()
|
|
95
97
|
|
|
98
|
+
if filename_phase is None:
|
|
99
|
+
filename_phase = "phase_" + time.strftime('%y%m%d%H%M%S', dt_now) + ".png"
|
|
100
|
+
|
|
101
|
+
if filename_diagram is None:
|
|
102
|
+
filename_diagram = "phase_diagram_" + time.strftime('%y%m%d%H%M%S', dt_now) + ".png"
|
|
103
|
+
|
|
104
|
+
|
|
96
105
|
if len(data_list[0]) == 2:
|
|
97
106
|
|
|
98
107
|
with open(input_file, 'r') as f:
|
|
@@ -110,7 +119,7 @@ def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None
|
|
|
110
119
|
plt.xlabel(header[0])
|
|
111
120
|
plt.ylabel(header[1])
|
|
112
121
|
|
|
113
|
-
plt.savefig(
|
|
122
|
+
plt.savefig(os.path.join(fig_path, filename_phase), dpi = dpi)
|
|
114
123
|
plt.clf()
|
|
115
124
|
plt.close()
|
|
116
125
|
|
|
@@ -126,7 +135,7 @@ def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None
|
|
|
126
135
|
plt.xlabel(header[0])
|
|
127
136
|
plt.ylabel(header[1])
|
|
128
137
|
|
|
129
|
-
plt.savefig(
|
|
138
|
+
plt.savefig(os.path.join(fig_path, filename_diagram), dpi = dpi)
|
|
130
139
|
plt.clf()
|
|
131
140
|
plt.close()
|
|
132
141
|
|
|
@@ -152,7 +161,7 @@ def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None
|
|
|
152
161
|
ax.set_ylabel(header[1])
|
|
153
162
|
ax.set_zlabel(header[2])
|
|
154
163
|
|
|
155
|
-
plt.savefig(
|
|
164
|
+
plt.savefig(os.path.join(fig_path, filename_phase), dpi = dpi)
|
|
156
165
|
plt.clf()
|
|
157
166
|
plt.close()
|
|
158
167
|
|
|
@@ -172,7 +181,7 @@ def plot(input_file, fig_folder = None, pdc_estimation = None, num_phases = None
|
|
|
172
181
|
ax.set_ylabel(header[1])
|
|
173
182
|
ax.set_zlabel(header[2])
|
|
174
183
|
|
|
175
|
-
plt.savefig(
|
|
184
|
+
plt.savefig(os.path.join(fig_path, filename_diagram), dpi = dpi)
|
|
176
185
|
plt.clf()
|
|
177
186
|
plt.close()
|
|
178
187
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nimo
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: NIMO package
|
|
5
5
|
Author: NIMO developers
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,7 @@ Requires-Dist: physbo>=3.1.0
|
|
|
11
11
|
Requires-Dist: scikit-learn
|
|
12
12
|
Requires-Dist: scipy
|
|
13
13
|
Requires-Dist: pyDOE3
|
|
14
|
+
Requires-Dist: anthropic
|
|
14
15
|
|
|
15
16
|
# nimo
|
|
16
17
|
|
|
@@ -33,6 +34,7 @@ NIMO is a Python library to realize a closed loop of robotic experiments and art
|
|
|
33
34
|
- scikit-learn
|
|
34
35
|
- scipy
|
|
35
36
|
- pyDOE3
|
|
37
|
+
- anthropic
|
|
36
38
|
|
|
37
39
|
# Install
|
|
38
40
|
|
|
@@ -7,7 +7,7 @@ packages = ["nimo", "nimo.ai_tools", "nimo.input_tools", "nimo.output_tools", "n
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "nimo"
|
|
10
|
-
version = "2.1.
|
|
10
|
+
version = "2.1.4"
|
|
11
11
|
description = "NIMO package"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [
|
|
@@ -20,5 +20,6 @@ dependencies = [
|
|
|
20
20
|
"physbo>=3.1.0",
|
|
21
21
|
"scikit-learn",
|
|
22
22
|
"scipy",
|
|
23
|
-
"pyDOE3"
|
|
23
|
+
"pyDOE3",
|
|
24
|
+
"anthropic"
|
|
24
25
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|