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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Validation for `run_only` field in component modelers to catch duplicate or invalid matrix indices early with clear error messages.
- Introduced a profile-based configuration manager with TOML persistence and runtime overrides exposed via `tidy3d.config`.
- Added support of `os.PathLike` objects as paths like `pathlib.Path` alongside `str` paths in all path-related functions.
- Added configurable local simulation result caching with checksum validation, eviction limits, and per-call overrides across `web.run`, `web.load`, and job workflows.
- Added local simulation result caching to avoid rerunning identical simulations. Enable and configure it through `td.config.local_cache` (size limits, cache directory). Use CLI commands `tidy3d cache {info, list, clear}` to inspect or clear the cache.
- Added `DirectivityMonitorSpec` for automated creation and configuration of directivity radiation monitors in `TerminalComponentModeler`.
- Added multimode support to `WavePort` in the smatrix plugin, allowing multiple modes to be analyzed per port.

Expand Down
46 changes: 46 additions & 0 deletions tests/test_web/test_local_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pytest
import xarray as xr
from autograd.core import defvjp
from click.testing import CliRunner
from rich.console import Console

import tidy3d as td
Expand All @@ -37,6 +38,7 @@
get_cache_entry_dir,
resolve_local_cache,
)
from tidy3d.web.cli.app import tidy3d_cli
from tidy3d.web.core.task_core import BatchTask

common.CONNECTION_RETRY_TIME = 0.1
Expand Down Expand Up @@ -724,6 +726,49 @@ def _test_env_var_overrides(monkeypatch, tmp_path):
manager._reload()


def _test_cache_cli_commands(monkeypatch, tmp_path_factory, basic_simulation):
runner = CliRunner()
cache_dir = tmp_path_factory.mktemp("cli_cache")
artifact_dir = tmp_path_factory.mktemp("cli_cache_artifact")

monkeypatch.setattr(config.local_cache, "enabled", True)
monkeypatch.setattr(config.local_cache, "directory", cache_dir)
monkeypatch.setattr(config.local_cache, "max_entries", 3)
monkeypatch.setattr(config.local_cache, "max_size_gb", 2.5)

cache = resolve_local_cache(use_cache=True)
cache.clear()

artifact = artifact_dir / CACHE_ARTIFACT_NAME
artifact.write_text("payload_cli")
cache.store_result(
_FakeStubData(basic_simulation), f"{MOCK_TASK_ID}-cli", str(artifact), "FDTD"
)

info_result = runner.invoke(tidy3d_cli, ["cache", "info"])
assert info_result.exit_code == 0
assert "Enabled: yes" in info_result.output
assert "Entries: 1" in info_result.output
assert "Max entries: 3" in info_result.output
assert "Max size: 2.50 GB" in info_result.output

list_result = runner.invoke(tidy3d_cli, ["cache", "list"])
assert list_result.exit_code == 0
out = list_result.output
assert "Cache Entry #1" in out
assert "Workflow type: FDTD" in out
assert "File size:" in out

clear_result = runner.invoke(tidy3d_cli, ["cache", "clear"])
assert clear_result.exit_code == 0
assert "Local cache cleared." in clear_result.output
assert len(cache) == 0

list_after = runner.invoke(tidy3d_cli, ["cache", "list"])
assert list_after.exit_code == 0
assert "Cache is empty." in list_after.output


def test_cache_sequential(
monkeypatch, tmp_path, tmp_path_factory, basic_simulation, fake_data, request
):
Expand All @@ -746,3 +791,4 @@ def test_cache_sequential(
_test_store_and_fetch_do_not_iterate(monkeypatch, tmp_path, basic_simulation)
_test_mode_solver_caching(monkeypatch, tmp_path)
_test_verbosity(monkeypatch, basic_simulation)
_test_cache_cli_commands(monkeypatch, tmp_path_factory, basic_simulation)
12 changes: 10 additions & 2 deletions tidy3d/web/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,8 +842,16 @@ def resolve_local_cache(use_cache: Optional[bool] = None) -> Optional[LocalCache
return None

if _CACHE is not None and _CACHE._root != Path(config.local_cache.directory):
log.debug(f"Clearing old cache directory {_CACHE._root}")
_CACHE.clear(hard=True)
old_root = _CACHE._root
new_root = Path(config.local_cache.directory)
log.debug(f"Moving cache directory from {old_root} → {new_root}")
try:
new_root.parent.mkdir(parents=True, exist_ok=True)
if old_root.exists():
shutil.move(old_root, new_root)
except Exception as e:
log.warning(f"Failed to move cache directory: {e}. Delete old cache.")
shutil.rmtree(old_root)

_CACHE = LocalCache(
directory=config.local_cache.directory,
Expand Down
2 changes: 2 additions & 0 deletions tidy3d/web/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
legacy_config_directory,
migrate_legacy_config,
)
from tidy3d.web.cli.cache import cache_group
from tidy3d.web.cli.constants import TIDY3D_DIR
from tidy3d.web.core.constants import HEADER_APIKEY

Expand Down Expand Up @@ -217,3 +218,4 @@ def config_group() -> None:
tidy3d_cli.add_command(convert)
tidy3d_cli.add_command(develop)
tidy3d_cli.add_command(config_group, name="config")
tidy3d_cli.add_command(cache_group)
95 changes: 95 additions & 0 deletions tidy3d/web/cli/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Cache-related CLI commands."""

from __future__ import annotations

from typing import Optional

import click

from tidy3d import config
from tidy3d.web.cache import LocalCache, resolve_local_cache
from tidy3d.web.cache import clear as clear_cache


def _fmt_size(num_bytes: int) -> str:
"""Format bytes into human-readable form."""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if num_bytes < 1024.0 or unit == "TB":
return f"{num_bytes:.2f} {unit}"
num_bytes /= 1024.0
return f"{num_bytes:.2f} B" # fallback, though unreachable


def _get_cache(ensure: bool = True) -> Optional[LocalCache]:
"""Resolve the local cache object, surfacing errors as ClickExceptions."""
try:
cache = resolve_local_cache(use_cache=True)
except Exception as exc: # pragma: no cover - defensive guard
raise click.ClickException(f"Failed to access local cache: {exc}") from exc
if cache is None and ensure:
raise click.ClickException("Local cache is disabled in the current configuration.")
return cache


@click.group(name="cache")
def cache_group() -> None:
"""Inspect or manage the local cache."""


@cache_group.command()
def info() -> None:
"""Display current cache configuration and usage statistics."""

enabled = bool(config.local_cache.enabled)
directory = config.local_cache.directory
max_entries = config.local_cache.max_entries
max_size_gb = config.local_cache.max_size_gb

cache = _get_cache(ensure=False)
entries = 0
total_size = 0
if cache is not None:
stats = cache.sync_stats()
entries = stats.total_entries
total_size = stats.total_size

click.echo(f"Enabled: {'yes' if enabled else 'no'}")
click.echo(f"Directory: {directory}")
click.echo(f"Entries: {entries}")
click.echo(f"Total size: {_fmt_size(total_size)}")
click.echo("Max entries: " + (str(max_entries) if max_entries else "unlimited"))
click.echo(
"Max size: " + (f"{_fmt_size(max_size_gb * 1024**3)}" if max_size_gb else "unlimited")
)


@cache_group.command(name="list")
def list() -> None:
"""List cached entries in a readable, separated format."""

cache = _get_cache()
entries = cache.list()
if not entries:
click.echo("Cache is empty.")
return

def fmt_key(key: str) -> str:
return key.replace("_", " ").capitalize()

for i, entry in enumerate(entries, start=1):
entry.pop("simulation_hash", None)
entry.pop("checksum", None)
entry["file_size"] = _fmt_size(entry["file_size"])

click.echo(f"\n=== Cache Entry #{i} ===")
for k, v in entry.items():
click.echo(f"{fmt_key(k)}: {v}")
click.echo("")


@cache_group.command()
def clear() -> None:
"""Remove all cache contents."""

clear_cache()
click.echo("Local cache cleared.")
Loading