Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -489,15 +489,20 @@ def rewrite_pareto_flags(y_all_norm):
df.to_csv(obs_csv, sep=';', index=False)


def save_hypervolume_to_file(hvs, iteration):
def save_hypervolume_to_file(hvs, iteration, ref_point):
hv_csv = os.path.join(PROJECT_PATH, "HypervolumePerEvaluation.csv")
os.makedirs(os.path.dirname(hv_csv), exist_ok=True)
write_header = not os.path.exists(hv_csv) or os.path.getsize(hv_csv) == 0
with open(hv_csv, 'a', newline='') as f:
w = csv.writer(f, delimiter=';')
if write_header:
w.writerow(["Hypervolume", "Run"])
w.writerow([hvs[-1], iteration])
w.writerow(["Hypervolume", "Run", "Scale", "ReferencePoint"])
w.writerow([
hvs[-1],
iteration,
"normalized maximize-space [-1,1] per objective",
"[" + ",".join(str(float(value)) for value in ref_point) + "]",
])


def append_meta_weights_row(weights_csv, iteration, optimizer, source_names):
Expand Down Expand Up @@ -595,22 +600,23 @@ def meta_execute(conn, seed, iterations, initial_samples):
print("Initial Sobol X in [0,1]:", x_init, flush=True)

y_rows = []
hvs = []
for i in range(initial_samples):
print(f"---- Initial Sample {i+1}", flush=True)
y_row = objective_function(conn, x_init[i])
y_rows.append(y_row)
append_observation_row(i + 1, 'sampling', y_row, x_init[i])
send_json_line(conn, {"type": "tempCoverage", "value": float(i + 1) / float(max(1, initial_samples))})
y_so_far = np.vstack(y_rows)
hvs.append(float(compute_hypervolume(y_so_far, np.asarray(ref_point, dtype=np.float64))))
save_hypervolume_to_file(hvs, i + 1, ref_point)
send_json_line(conn, {"type": "coverage", "value": float(hvs[-1])})

y_all = np.vstack(y_rows)
x_all = np.asarray(x_init, dtype=np.float64)
rewrite_pareto_flags(y_all)
optimizer.observe(x_all, y_all)

hvs = [float(compute_hypervolume(y_all, np.asarray(ref_point, dtype=np.float64)))]
save_hypervolume_to_file(hvs, 0)
send_json_line(conn, {"type": "coverage", "value": float(hvs[-1])})

# ---- optimization phase (ask/tell against openbo) ----
for it in range(1, iterations + 1):
t0 = time.time()
Expand All @@ -628,7 +634,7 @@ def meta_execute(conn, seed, iterations, initial_samples):
append_observation_row(initial_samples + it, 'optimization', y_row, x_next[0])
rewrite_pareto_flags(y_all)
hvs.append(float(compute_hypervolume(y_all, np.asarray(ref_point, dtype=np.float64))))
save_hypervolume_to_file(hvs, it)
save_hypervolume_to_file(hvs, initial_samples + it, ref_point)
send_json_line(conn, {"type": "coverage", "value": float(hvs[-1])})

send_json_line(conn, {"type": "optimization_finished"})
Expand Down
32 changes: 23 additions & 9 deletions Assets/StreamingAssets/BOData/BayesianOptimization/mobo.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ def objective_function(conn, x_tensor):
return torch.tensor(fs, dtype=torch.double)

# -------------------- data IO --------------------
def generate_initial_data(conn, n_samples):
def generate_initial_data(conn, n_samples, hv_util=None, hvs=None):
global PROJECT_PATH
if n_samples < 1:
raise ValueError("n_samples must be >= 1 for non-warm-start runs.")
Expand Down Expand Up @@ -442,6 +442,12 @@ def generate_initial_data(conn, n_samples):
with open(obs_csv, 'a', newline='') as f:
csv.writer(f, delimiter=';').writerow(row)
send_json_line(conn, {"type": "tempCoverage", "value": float(i+1)/float(max(1,n_samples))})
if hv_util is not None and hvs is not None:
y_so_far = torch.stack(train_obj, dim=0).to(dtype=torch.double)
volume = hv_util.compute(y_so_far[is_non_dominated(y_so_far)])
hvs.append(volume)
save_hypervolume_to_file(hvs, i + 1)
send_json_line(conn, {"type": "coverage", "value": float(volume)})

Y = torch.stack(train_obj, dim=0).to(dtype=torch.double)
# Ensure sampling-only runs (N_ITERATIONS=0) have globally-correct IsPareto flags.
Expand Down Expand Up @@ -616,8 +622,13 @@ def save_hypervolume_to_file(hvs, iteration):
with open(hv_csv, 'a', newline='') as f:
w = csv.writer(f, delimiter=';')
if write_header:
w.writerow(["Hypervolume", "Run"])
w.writerow([hvs[-1], iteration])
w.writerow(["Hypervolume", "Run", "Scale", "ReferencePoint"])
w.writerow([
hvs[-1],
iteration,
"normalized maximize-space [-1,1] per objective",
"[" + ",".join(str(float(value)) for value in as_numpy_array(ref_point)) + "]",
])

# -------------------- main loop --------------------
def mobo_execute(conn, seed, iterations, initial_samples):
Expand All @@ -638,7 +649,9 @@ def mobo_execute(conn, seed, iterations, initial_samples):
if WARM_START:
train_x, train_y = load_data()
else:
train_x, train_y = generate_initial_data(conn, n_samples=initial_samples)
train_x, train_y = generate_initial_data(
conn, n_samples=initial_samples, hv_util=hv_util, hvs=hvs
)

expected_x_dim = PROBLEM_DIM + (1 if CONTEXT_SETUP is not None else 0)
if train_x.shape[0] != train_y.shape[0]:
Expand All @@ -650,10 +663,11 @@ def mobo_execute(conn, seed, iterations, initial_samples):

mll, model = initialize_model(train_x, train_y)

volume = current_context_hypervolume(hv_util, train_x, train_y)
hvs.append(volume)
save_hypervolume_to_file(hvs, 0)
send_json_line(conn, {"type": "coverage", "value": float(volume)})
if WARM_START:
volume = current_context_hypervolume(hv_util, train_x, train_y)
hvs.append(volume)
save_hypervolume_to_file(hvs, 0)
send_json_line(conn, {"type": "coverage", "value": float(volume)})

for it in range(1, iterations + 1):
t0 = time.time()
Expand All @@ -673,7 +687,7 @@ def mobo_execute(conn, seed, iterations, initial_samples):
volume = current_context_hypervolume(hv_util, train_x, train_y)
hvs.append(volume)
save_xy(train_x, train_y, it)
save_hypervolume_to_file(hvs, it)
save_hypervolume_to_file(hvs, initial_samples + it)
send_json_line(conn, {"type": "coverage", "value": float(volume)})
mll, model = initialize_model(train_x, train_y)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,7 @@ Common files:

MOBO (`mobo.py`, `m >= 2`):
* `ObservationsPerEvaluation.csv` uses `IsPareto`.
* `HypervolumePerEvaluation.csv` stores hypervolume per iteration.
* `HypervolumePerEvaluation.csv` stores hypervolume after every sampling and optimization evaluation. Its `Scale` column records that objectives are normalized to maximize-space `[-1,1]`, and `ReferencePoint` records the default `[-1,...,-1]` reference point.
* Unity `coverage` corresponds to current hypervolume.

Single-objective BO (`bo.py`, `m = 1`):
Expand Down
8 changes: 5 additions & 3 deletions tests/test_meta_mobo_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def test_full_protocol_run_with_sources(self):
types = [m["type"] for m in sent]
self.assertEqual(types.count("parameters"), 4)
self.assertEqual(types.count("tempCoverage"), 2)
self.assertEqual(types.count("coverage"), 3) # after sampling + 2 iterations
self.assertEqual(types.count("coverage"), 4) # after every evaluation
self.assertEqual(types.count("optimization_finished"), 1)
self.assertEqual(types[-1], "optimization_finished")

Expand Down Expand Up @@ -270,8 +270,10 @@ def test_full_protocol_run_with_sources(self):

with open(run_dir / "HypervolumePerEvaluation.csv", newline="") as f:
hv_rows = list(csv.reader(f, delimiter=";"))
self.assertEqual(hv_rows[0], ["Hypervolume", "Run"])
self.assertEqual([r[1] for r in hv_rows[1:]], ["0", "1", "2"])
self.assertEqual(hv_rows[0], ["Hypervolume", "Run", "Scale", "ReferencePoint"])
self.assertEqual([r[1] for r in hv_rows[1:]], ["1", "2", "3", "4"])
self.assertTrue(all(r[2] == "normalized maximize-space [-1,1] per objective" for r in hv_rows[1:]))
self.assertTrue(all(r[3] == "[-1.0,-1.0]" for r in hv_rows[1:]))

with open(run_dir / "ExecutionTimes.csv", newline="") as f:
exec_rows = list(csv.reader(f, delimiter=";"))
Expand Down
11 changes: 6 additions & 5 deletions tests/test_mobo.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,16 +637,17 @@ def test_save_hypervolume_to_file_writes_header_once(self):
mobo = load_mobo_module()
with tempfile.TemporaryDirectory() as tmp:
mobo.PROJECT_PATH = tmp
mobo.ref_point = FakeTensor([-1.0])
mobo.save_hypervolume_to_file([0.1], iteration=0)
mobo.save_hypervolume_to_file([0.2], iteration=1)
hv_csv = pathlib.Path(tmp) / "HypervolumePerEvaluation.csv"
with hv_csv.open() as f:
rows = list(csv.reader(f, delimiter=";"))

self.assertEqual(rows[0], ["Hypervolume", "Run"])
self.assertEqual(rows[0], ["Hypervolume", "Run", "Scale", "ReferencePoint"])
self.assertEqual(len(rows), 3)
self.assertEqual(rows[1], ["0.1", "0"])
self.assertEqual(rows[2], ["0.2", "1"])
self.assertEqual(rows[1], ["0.1", "0", "normalized maximize-space [-1,1] per objective", "[-1.0]"])
self.assertEqual(rows[2], ["0.2", "1", "normalized maximize-space [-1,1] per objective", "[-1.0]"])

def test_mobo_execute_with_simulated_unity_objective_stream(self):
mobo = load_mobo_module()
Expand Down Expand Up @@ -710,8 +711,8 @@ def test_mobo_execute_with_simulated_unity_objective_stream(self):
np.array([[0.6, 0.6], [-0.6, -0.6], [0.0, 0.0]]),
atol=1e-12,
)
# Hypervolume logs: iteration 0 + iteration 1
self.assertEqual(len(hvs), 2)
# Hypervolume logs cover both sampling evaluations and the optimization step.
self.assertEqual(len(hvs), 3)
# Ensure loop sent completion signal.
self.assertTrue(any(m.get("type") == "optimization_finished" for m in out_msgs))

Expand Down