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
23 changes: 12 additions & 11 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -259,20 +259,25 @@ codebase-map.html
CLAUDE.md
.luarc.json
.mcp.json
/bart26g/.quarto
/bart26g/_manuscript
/bart26g/_freeze
bart26g/index.quarto_ipynb_5
bart26g/index.quarto_ipynb_4
bart26g/index.quarto_ipynb_3
bart26g/index.quarto_ipynb_2
/bart26g/runs
/bart26g/spotoptim_arxiv
bart26g/index.pdf
bart26g/index.tex
bart26g/index.quarto_ipynb_1
bart26g/index.quarto_ipynb_10
bart26g/index.quarto_ipynb_11
bart26g/index.quarto_ipynb_12
bart26g/index.quarto_ipynb_2
bart26g/index.quarto_ipynb_3
bart26g/index.quarto_ipynb_4
bart26g/index.quarto_ipynb_5
bart26g/index.quarto_ipynb_6
bart26g/index.quarto_ipynb_7
bart26g/index.quarto_ipynb_8
bart26g/index.quarto_ipynb_9
bart26g/index.quarto_ipynb_10
bart26g/index.quarto_ipynb_11
bart26g/index.quarto_ipynb_12
bart26g/index.quarto_ipynb_13
bart26g/index.quarto_ipynb_14
bart26g/index.quarto_ipynb_15
Expand All @@ -281,8 +286,6 @@ bart26g/index.quarto_ipynb_17
bart26g/index.quarto_ipynb_18
bart26g/index.quarto_ipynb_19
bart26g/index.quarto_ipynb_20
bart26g/index.pdf
bart26g/index.tex
bart26g/index.quarto_ipynb_21
bart26g/index.quarto_ipynb_22
bart26g/index.quarto_ipynb_23
Expand All @@ -307,6 +310,4 @@ bart26g/index.quarto_ipynb_41
bart26g/index.quarto_ipynb_42
bart26g/index.quarto_ipynb_43
bart26g/index.quarto_ipynb_44
/bart26g/runs
bart26g/index.quarto_ipynb_45
/bart26g/spotoptim_arxiv
54 changes: 46 additions & 8 deletions src/spotoptim/sampling/mm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,7 @@ def plot_mmphi_corrected_vs_n_lhs(
n_step: int = 5,
q_phi: float = 2.0,
p_phi: float = 2.0,
plot_only_corrected: bool = False,
) -> None:
"""Generate LHS designs for varying n and plot the Corrected Morris-Mitchell
Criterion against the standard criterion.
Expand All @@ -1808,6 +1809,11 @@ def plot_mmphi_corrected_vs_n_lhs(
two series are displayed on a shared x-axis with independent y-axes so
their trends can be compared directly.

When ``plot_only_corrected`` is ``True``, the intensive criterion is
omitted: only ``hat_Phi_q`` is plotted on a single y-axis. This is the
preferred form when illustrating the asymptotic size-invariance of the
corrected criterion on its own scale.

The *corrected* criterion is asymptotically size-invariant: for large ``n``
its expected value stabilizes at a finite constant that depends only on the
spatial distribution of the design, not on ``n`` itself. This plot makes
Expand All @@ -1821,42 +1827,74 @@ def plot_mmphi_corrected_vs_n_lhs(
n_step (int): Step size for increasing n. Defaults to 5.
q_phi (float): Exponent q for the Morris-Mitchell criteria. Defaults to 2.0.
p_phi (float): Distance norm p for the Morris-Mitchell criteria. Defaults to 2.0.
plot_only_corrected (bool): If ``True``, plot only the corrected
criterion ``hat_Phi_q`` on a single y-axis and skip the intensive
criterion entirely. Defaults to ``False``, which preserves the
original dual-axis comparison plot.

Returns:
None: Displays a dual-axis plot of ``mmphi_intensive`` and
``mmphi_corrected`` vs. number of samples (n).
None: Displays the plot. When ``plot_only_corrected`` is ``False`` the
figure has dual y-axes showing ``mmphi_intensive`` and
``mmphi_corrected``; when ``True`` only the corrected curve is shown.

Examples:
>>> from spotoptim.sampling.mm import plot_mmphi_corrected_vs_n_lhs
>>> plot_mmphi_corrected_vs_n_lhs(k_dim=3, seed=42, n_min=10, n_max=50, n_step=5, q_phi=2.0, p_phi=2.0)
>>> plot_mmphi_corrected_vs_n_lhs(k_dim=3, seed=42, n_min=10, n_max=50, plot_only_corrected=True)
"""
n_values = list(range(n_min, n_max + 1, n_step))
if not n_values:
print("Warning: n_values list is empty. Check n_min, n_max, and n_step.")
return
mmphi_intensive_results = []
mmphi_corrected_results = []
mmphi_intensive_results: list[float] = []
mmphi_corrected_results: list[float] = []
lhs_sampler = LatinHypercube(d=k_dim, rng=seed)

for n_points in n_values:
if n_points < 2:
print(f"Skipping n={n_points} as it's less than 2.")
mmphi_intensive_results.append(np.nan)
if not plot_only_corrected:
mmphi_intensive_results.append(np.nan)
mmphi_corrected_results.append(np.nan)
continue
try:
X_design = lhs_sampler.random(n=n_points)
phi_intensive, _, _ = mmphi_intensive(X_design, q=q_phi, p=p_phi)
phi_corrected, _, _ = mmphi_corrected(X_design, q=q_phi, p=p_phi)
mmphi_intensive_results.append(phi_intensive)
mmphi_corrected_results.append(phi_corrected)
if not plot_only_corrected:
phi_intensive, _, _ = mmphi_intensive(X_design, q=q_phi, p=p_phi)
mmphi_intensive_results.append(phi_intensive)
except Exception as e:
print(f"Error calculating for n={n_points}: {e}")
mmphi_intensive_results.append(np.nan)
if not plot_only_corrected:
mmphi_intensive_results.append(np.nan)
mmphi_corrected_results.append(np.nan)

fig, ax1 = plt.subplots(figsize=(9, 6))

if plot_only_corrected:
color = "tab:blue"
ax1.set_xlabel("Number of Samples (n)")
ax1.set_ylabel("mmphi_corrected (hat_Phiq)", color=color)
ax1.plot(
n_values,
mmphi_corrected_results,
color=color,
marker="x",
linestyle="--",
label="mmphi_corrected (hat_Phiq)",
)
ax1.tick_params(axis="y", labelcolor=color)
ax1.grid(True, linestyle="--", alpha=0.7)
fig.tight_layout()
plt.title(
f"Corrected Morris-Mitchell Criterion vs. Number of Samples (n)\n"
f"LHS (k={k_dim}, q={q_phi}, p={p_phi})"
)
ax1.legend(loc="best")
plt.show()
return

color = "tab:red"
ax1.set_xlabel("Number of Samples (n)")
ax1.set_ylabel("mmphi_intensive (PhiqI)", color=color)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_mm.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,56 @@ def test_plot_mmphi_corrected_vs_n_lhs_higher_dim(monkeypatch):
plot_mmphi_corrected_vs_n_lhs(k_dim=5, seed=1, n_min=10, n_max=20, n_step=10)


def test_plot_mmphi_corrected_vs_n_lhs_only_corrected(monkeypatch):
"""plot_only_corrected=True draws a single-axis figure with just the corrected curve."""
import matplotlib.pyplot as plt

monkeypatch.setattr(plt, "show", lambda: None)
captured_figs: list = []
original_subplots = plt.subplots

def _capture_subplots(*args, **kwargs):
fig, ax = original_subplots(*args, **kwargs)
captured_figs.append((fig, ax))
return fig, ax

monkeypatch.setattr(plt, "subplots", _capture_subplots)
plot_mmphi_corrected_vs_n_lhs(
k_dim=2, seed=0, n_min=10, n_max=20, n_step=5, plot_only_corrected=True
)
assert captured_figs, "plt.subplots was never called"
fig, ax = captured_figs[-1]
# Single-axis figure: exactly one Axes on the figure.
assert len(fig.axes) == 1
# The single curve plotted is the corrected criterion.
assert len(ax.get_lines()) == 1
(line,) = ax.get_lines()
assert "corrected" in line.get_label().lower()
plt.close(fig)


def test_plot_mmphi_corrected_vs_n_lhs_default_is_dual_axis(monkeypatch):
"""Default plot_only_corrected=False still produces a dual-axis figure."""
import matplotlib.pyplot as plt

monkeypatch.setattr(plt, "show", lambda: None)
captured_figs: list = []
original_subplots = plt.subplots

def _capture_subplots(*args, **kwargs):
fig, ax = original_subplots(*args, **kwargs)
captured_figs.append((fig, ax))
return fig, ax

monkeypatch.setattr(plt, "subplots", _capture_subplots)
plot_mmphi_corrected_vs_n_lhs(k_dim=2, seed=0, n_min=10, n_max=20, n_step=5)
assert captured_figs, "plt.subplots was never called"
fig, _ = captured_figs[-1]
# twinx() adds a second Axes sharing the x-axis.
assert len(fig.axes) == 2
plt.close(fig)


def test_plot_mmphi_corrected_vs_n_lhs_ratio_identity():
"""mmphi_corrected equals mmphi_intensive scaled by (M / n^{1+q/k})^{1/q}.

Expand Down
6 changes: 4 additions & 2 deletions tests/test_termination_criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,10 @@ def slow_sphere(x):

# It should stop roughly around 3 seconds
assert elapsed >= 2.5
# It shouldn't run forever
assert elapsed < 10.0
# It shouldn't run forever. The ceiling is generous because shared CI
# runners can stall the parallel pool teardown by several seconds; the
# assertion's intent is "the loop terminated", not a tight timing budget.
assert elapsed < 20.0

def test_verbose_output_sequential(self, capsys):
"""Test Case 8: Verbose output in sequential mode."""
Expand Down
Loading