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
12 changes: 8 additions & 4 deletions src/spotoptim/SpotOptim.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class SpotOptimConfig:
ocba_delta (int): Delta for OCBA.
tensorboard_log (bool): Whether to log to TensorBoard.
tensorboard_path (Optional[str]): Path to TensorBoard logs.
tensorboard_clean (bool): Whether to clean TensorBoard logs.
tensorboard_clean (bool): Whether to clean old TensorBoard logs (the
configured tensorboard_path, or the 'runs' folder if no path is set).
fun_mo2so (Optional[Callable]): Function to convert multi-objective to single-objective.
seed (Optional[int]): Seed for random number generator.
verbose (bool): Whether to print verbose output.
Expand Down Expand Up @@ -360,9 +361,12 @@ class SpotOptim(BaseEstimator):
Path for TensorBoard log files. If None and tensorboard_log
is True, creates a default path: runs/spotoptim_YYYYMMDD_HHMMSS. Defaults to None.
tensorboard_clean (bool, optional):
If True, removes all old TensorBoard log directories from
the 'runs' folder before starting optimization. Use with caution as this permanently
deletes all subdirectories in 'runs'. Defaults to False.
If True, removes old TensorBoard logs before starting optimization
so every run begins with a fresh dashboard. With tensorboard_path
set, the configured directory itself is removed (and re-created
empty by the writer); without a path, all subdirectories of the
default 'runs' folder are removed. Use with caution as this
permanently deletes the affected log directories. Defaults to False.
fun_mo2so (callable, optional):
Function to convert multi-objective values to single-objective.
Takes an array of shape (n_samples, n_objectives) and returns array of shape (n_samples,).
Expand Down
21 changes: 19 additions & 2 deletions src/spotoptim/utils/tensorboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,31 @@


def clean_tensorboard_logs(optimizer: SpotOptimProtocol) -> None:
"""Clean old TensorBoard log directories from the runs folder.
"""Clean old TensorBoard logs if tensorboard_clean is True.

Removes all subdirectories in the 'runs' directory if tensorboard_clean is True.
When ``tensorboard_path`` is set, removes that directory (the writer
re-creates it empty afterwards), so every run starts with a fresh
dashboard. When ``tensorboard_path`` is None, falls back to the legacy
behavior of removing all subdirectories of the default ``runs``
directory.

Args:
optimizer: SpotOptim instance.
"""
if optimizer.tensorboard_clean:
if optimizer.tensorboard_path is not None:
path = optimizer.tensorboard_path
if os.path.isdir(path):
try:
shutil.rmtree(path)
if optimizer.verbose:
print(f"Removed old TensorBoard logs: {path}")
except Exception as e:
if optimizer.verbose:
print(f"Warning: Could not remove {path}: {e}")
elif optimizer.verbose:
print(f"No old TensorBoard logs to clean at '{path}'")
return
runs_dir = "runs"
if os.path.exists(runs_dir) and os.path.isdir(runs_dir):
subdirs = [
Expand Down
63 changes: 50 additions & 13 deletions tests/test_tensorboard_clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,15 @@ def teardown_method(self):
shutil.rmtree("runs")

def test_clean_with_custom_tensorboard_path(self):
"""Test that clean doesn't interfere with custom paths."""
# Create custom directory
"""Test that clean targets the custom path and leaves 'runs' alone."""
# Create custom directory with a stale event file from a prior run
custom_path = "my_logs/experiment_1"
os.makedirs(custom_path, exist_ok=True)
stale_file = os.path.join(custom_path, "events.out.tfevents.stale")
with open(stale_file, "w") as f:
f.write("stale")

# Create some old logs in runs
# Create some old logs in runs (unrelated to the configured path)
os.makedirs("runs/old_log", exist_ok=True)

try:
Expand All @@ -251,22 +254,56 @@ def test_clean_with_custom_tensorboard_path(self):
verbose=False,
)

# Runs directory should be cleaned
if os.path.exists("runs"):
subdirs = [
d
for d in os.listdir("runs")
if os.path.isdir(os.path.join("runs", d))
]
assert len(subdirs) == 0
# Custom directory exists (re-created by the writer) but the
# stale event file from the previous run is gone
assert os.path.isdir(custom_path)
assert not os.path.exists(stale_file)

# Custom directory should still exist
assert os.path.exists(custom_path)
# The unrelated 'runs' directory is untouched
assert os.path.isdir("runs/old_log")

finally:
if os.path.exists("my_logs"):
shutil.rmtree("my_logs")

def test_clean_with_custom_path_without_logging(self):
"""Clean honors a custom path even when logging is disabled."""
custom_path = "my_logs/experiment_2"
os.makedirs(custom_path, exist_ok=True)
with open(os.path.join(custom_path, "events.out.tfevents.stale"), "w") as f:
f.write("stale")

try:
_ = SpotOptim(
fun=lambda X: np.sum(X**2, axis=1),
bounds=[(-5, 5), (-5, 5)],
max_iter=10,
n_initial=5,
tensorboard_path=custom_path,
tensorboard_clean=True,
verbose=False,
)

# Directory removed and not re-created (no writer without logging)
assert not os.path.exists(custom_path)

finally:
if os.path.exists("my_logs"):
shutil.rmtree("my_logs")

def test_clean_with_missing_custom_path(self):
"""Clean handles a non-existent custom path gracefully."""
optimizer = SpotOptim(
fun=lambda X: np.sum(X**2, axis=1),
bounds=[(-5, 5), (-5, 5)],
max_iter=10,
n_initial=5,
tensorboard_path="my_logs/does_not_exist",
tensorboard_clean=True,
verbose=False,
)
assert optimizer.tensorboard_clean is True

def test_clean_with_nested_directories(self):
"""Test that clean handles nested structures correctly."""
# Create nested structure
Expand Down
Loading