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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `custom_source_time` parameter to `ComponentModeler` classes (`ModalComponentModeler` and `TerminalComponentModeler`), allowing specification of custom source time dependence.
- 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.

### Changed
- Improved performance of antenna metrics calculation by utilizing cached wave amplitude calculations instead of recomputing wave amplitudes for each port excitation in the `TerminalComponentModelerData`.
Expand Down
36 changes: 36 additions & 0 deletions tests/test_web/test_tidy3d_stub.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import os
from pathlib import Path

import numpy as np
import pytest
import responses

import tidy3d as td
Expand Down Expand Up @@ -162,6 +164,40 @@ def test_stub_data_lazy_loading(tmp_path):
_ = sim_data.monitor_data


@pytest.mark.parametrize(
"path_builder",
(
lambda tmp_path, name: Path(tmp_path) / name,
lambda tmp_path, name: str(Path(tmp_path) / name),
),
)
def test_stub_pathlike_roundtrip(tmp_path, path_builder):
"""Ensure stub read/write helpers accept pathlib.Path and posixpath inputs."""

# Simulation stub roundtrip
sim = make_sim()
stub = Tidy3dStub(simulation=sim)
sim_path = path_builder(tmp_path, "pathlike_sim.json")
stub.to_file(sim_path)
assert os.path.exists(sim_path)
sim_loaded = Tidy3dStub.from_file(sim_path)
assert sim_loaded == sim

# Simulation data stub roundtrip
sim_data = make_sim_data()
stub_data = Tidy3dStubData(data=sim_data)
data_path = path_builder(tmp_path, "pathlike_data.hdf5")
stub_data.to_file(data_path)
assert os.path.exists(data_path)

data_loaded = Tidy3dStubData.from_file(data_path)
assert data_loaded.simulation == sim_data.simulation

# Postprocess using the same PathLike ensures downstream helpers accept the type
processed = Tidy3dStubData.postprocess(data_path, lazy=True)
assert isinstance(processed, SimulationData)


def test_default_task_name():
sim = make_sim()
stub = Tidy3dStub(simulation=sim)
Expand Down
13 changes: 13 additions & 0 deletions tests/test_web/test_tidy3d_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,16 @@ def mock(*args, **kwargs):
task.get_log(LOG_FNAME)
with open(LOG_FNAME) as f:
assert f.read() == "0.3,5.7"


@responses.activate
def test_get_running_tasks(set_api_key):
responses.add(
responses.GET,
f"{Env.current.web_api_endpoint}/tidy3d/py/tasks",
json={"data": [{"taskId": "1234", "status": "queued"}]},
status=200,
)

tasks = SimulationTask.get_running_tasks()
assert len(tasks) == 1
103 changes: 98 additions & 5 deletions tests/test_web/test_webapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from __future__ import annotations

import os
import posixpath
from concurrent.futures import Future
from os import PathLike
from pathlib import Path
from types import SimpleNamespace

import numpy as np
Expand Down Expand Up @@ -739,8 +742,8 @@ def status(self):
events.append((self.task_id, "status", status))
return status

def download(self, path: str):
events.append((self.task_id, "download", path))
def download(self, path: PathLike):
events.append((self.task_id, "download", str(path)))

monkeypatch.setattr("tidy3d.web.api.container.ThreadPoolExecutor", ImmediateExecutor)
monkeypatch.setattr("tidy3d.web.api.container.time.sleep", lambda *_args, **_kwargs: None)
Expand All @@ -766,7 +769,7 @@ def download(self, path: str):
}

for task_id, _, path in downloads:
assert path == expected_paths[task_id]
assert str(path) == expected_paths[task_id]

job1_download_idx = next(
i
Expand Down Expand Up @@ -797,8 +800,8 @@ def status(self):
events.append((self.task_id, "status", status))
return status

def download(self, path: str):
events.append((self.task_id, "download", path))
def download(self, path: PathLike):
events.append((self.task_id, "download", str(path)))

monkeypatch.setattr("tidy3d.web.api.container.ThreadPoolExecutor", ImmediateExecutor)
monkeypatch.setattr("tidy3d.web.api.container.time.sleep", lambda *_args, **_kwargs: None)
Expand All @@ -819,6 +822,7 @@ def download(self, path: str):
batch.monitor(download_on_success=True, path_dir=str(tmp_path))

downloads = [event for event in events if event[1] == "download"]

assert downloads == [("task_b_id", "download", os.path.join(str(tmp_path), "task_b_id.hdf5"))]


Expand Down Expand Up @@ -996,3 +1000,92 @@ def test_run_single_offline_eager(monkeypatch, tmp_path):

assert isinstance(sim_data, SimulationData)
assert sim_data.__class__.__name__ == "SimulationData" # no proxy


class FauxPath:
"""Minimal PathLike to exercise __fspath__ support."""

def __init__(self, path: PathLike | str):
self._p = os.fspath(path)

def __fspath__(self) -> str:
return self._p


def _pathlib_builder(tmp_path, name: str):
return Path(tmp_path) / name


def _posix_builder(tmp_path, name: str):
return posixpath.join(tmp_path.as_posix(), name)


def _str_builder(tmp_path, name: str):
return str(Path(tmp_path) / name)


def _fspath_builder(tmp_path, name: str):
return FauxPath(Path(tmp_path) / name)


@pytest.mark.parametrize(
"path_builder",
[_pathlib_builder, _posix_builder, _str_builder, _fspath_builder],
ids=["pathlib.Path", "posixpath_str", "str", "PathLike"],
)
def test_run_single_offline_eager_accepts_pathlikes(monkeypatch, tmp_path, path_builder):
"""run(sim, path=...) accepts any PathLike."""
sim = make_sim()
task_name = "pathlike_single"
out_file = path_builder(tmp_path, "sim.hdf5")

# Patch webapi for offline run and to write to the provided path
apply_common_patches(monkeypatch, tmp_path, taskid_to_sim={task_name: sim})

sim_data = run(sim, task_name=task_name, path=out_file)

# File existed (written via patched load) and types are correct
assert os.path.exists(os.fspath(out_file))
assert isinstance(sim_data, SimulationData)
assert sim_data.simulation == sim


@pytest.mark.parametrize(
"path_builder",
[_pathlib_builder, _posix_builder, _str_builder, _fspath_builder],
ids=["pathlib.Path", "posixpath_str", "str", "PathLike"],
)
def test_job_run_accepts_pathlikes(monkeypatch, tmp_path, path_builder):
"""Job.run(path=...) accepts any PathLike."""
sim = make_sim()
task_name = "job_pathlike"
out_file = path_builder(tmp_path, "job_out.hdf5")

apply_common_patches(monkeypatch, tmp_path, taskid_to_sim={task_name: sim})

j = Job(simulation=sim, task_name=task_name, folder_name=PROJECT_NAME)
_ = j.run(path=out_file)

assert os.path.exists(os.fspath(out_file))


@pytest.mark.parametrize(
"dir_builder",
[_pathlib_builder, _posix_builder, _str_builder, _fspath_builder],
ids=["pathlib.Path", "posixpath_str", "str", "PathLike"],
)
def test_batch_run_accepts_pathlike_dir(monkeypatch, tmp_path, dir_builder):
"""Batch.run(path_dir=...) accepts any PathLike directory location."""
sims = {"A": make_sim(), "B": make_sim()}
out_dir = dir_builder(tmp_path, "batch_out")

# Map task_ids to sims: upload() is patched to return task_name, which for dict input
# corresponds to the dict keys ("A", "B"), so we map those.
apply_common_patches(monkeypatch, tmp_path, taskid_to_sim={"A": sims["A"], "B": sims["B"]})

b = Batch(simulations=sims, folder_name=PROJECT_NAME)
b.run(path_dir=out_dir)

# Directory created and two .hdf5 outputs produced
out_dir_str = os.fspath(out_dir)
assert os.path.isdir(out_dir_str)
Loading