Skip to content

Commit 2315f83

Browse files
marcorudolphflexdaquinteroflex
authored andcommitted
feat(tidy3d): FXC-3004 Container-aware web.run returning lazy results
Co-authored-by: daquinteroflex <dario@flexcompute.com>
1 parent 83d619b commit 2315f83

File tree

12 files changed

+886
-193
lines changed

12 files changed

+886
-193
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
- `CustomCurrentIntegral2D``Custom2DCurrentIntegral`
3535
- Path integral and impedance calculator classes have been refactored and moved from `tidy3d.plugins.microwave` to `tidy3d.components.microwave`. They are now publicly exported via the top-level package `__init__.py`, so you can import them directly, e.g. `from tidy3d import ImpedanceCalculator, AxisAlignedVoltageIntegral, AxisAlignedCurrentIntegral, Custom2DVoltageIntegral, Custom2DCurrentIntegral, Custom2DPathIntegral`.
3636
- `DirectivityMonitor` now forces `far_field_approx` to `True`, which was previously configurable.
37+
- Unified run submission API: `web.run(...)` is now a container-aware wrapper that accepts a single simulation or arbitrarily nested containers (`list`, `tuple`, `dict` values) and returns results in the same shape.
38+
- `web.Batch(ComponentModeler)` and `web.Job(ComponentModeler)` native support
3739

3840
### Fixed
3941
- More robust `Sellmeier` and `Debye` material model, and prevent very large pole parameters in `PoleResidue` material model.

tests/test_web/test_webapi.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import os
5+
from types import SimpleNamespace
56

67
import numpy as np
78
import pytest
@@ -22,7 +23,9 @@
2223
from tidy3d.exceptions import SetupError
2324
from tidy3d.web import common
2425
from tidy3d.web.api.asynchronous import run_async
25-
from tidy3d.web.api.container import Batch, Job
26+
from tidy3d.web.api.container import Batch, Job, WebContainer
27+
from tidy3d.web.api.run import _collect_by_hash, run
28+
from tidy3d.web.api.tidy3d_stub import Tidy3dStubData
2629
from tidy3d.web.api.webapi import (
2730
abort,
2831
delete,
@@ -38,7 +41,6 @@
3841
load_simulation,
3942
monitor,
4043
real_cost,
41-
run,
4244
start,
4345
upload,
4446
)
@@ -55,6 +57,7 @@
5557
EST_FLEX_UNIT = 11.11
5658
FILE_SIZE_GB = 4.0
5759
common.CONNECTION_RETRY_TIME = 0.1
60+
INVALID_TASK_ID = "INVALID_TASK_ID"
5861

5962
task_core_path = "tidy3d.web.core.task_core"
6063
api_path = "tidy3d.web.api.webapi"
@@ -768,13 +771,95 @@ def save_sim_to_path(path: str) -> None:
768771
@responses.activate
769772
def test_load_invalid_task_raises(mock_webapi):
770773
"""Ensure that load() raises TaskNotFoundError for a non-existent task ID."""
771-
fake_id = "INVALID_TASK_ID"
772774

773775
responses.add(
774776
responses.GET,
775-
f"{Env.current.web_api_endpoint}/tidy3d/tasks/{fake_id}/detail",
777+
f"{Env.current.web_api_endpoint}/tidy3d/tasks/{INVALID_TASK_ID}/detail",
776778
json={"error": "Task not found"},
777779
status=404,
778780
)
779781
with pytest.raises(WebNotFoundError, match="Resource not found"):
780-
load(fake_id)
782+
load(INVALID_TASK_ID, replace_existing=True)
783+
784+
785+
def _fake_load_factory(tmp_root, taskid_to_sim: dict):
786+
def _fake_load(task_id, path="simulation_data.hdf5", lazy=False, **kwargs):
787+
abs_path = path if os.path.isabs(path) else os.path.join(tmp_root, path)
788+
abs_path = os.path.normpath(abs_path)
789+
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
790+
791+
sim_for_this = taskid_to_sim.get(task_id)
792+
793+
log = "- Time step 827 / time 4.13e-14s ( 4 % done), field decay: 0.110e+00"
794+
sim_data = SimulationData(simulation=sim_for_this, data=[], diverged=False, log=log)
795+
796+
sim_data.to_file(abs_path)
797+
return Tidy3dStubData.postprocess(abs_path, lazy=lazy)
798+
799+
return _fake_load
800+
801+
802+
def apply_common_patches(
803+
monkeypatch, tmp_root, *, api_path="tidy3d.web.api.webapi", path_to_sim=None, taskid_to_sim=None
804+
):
805+
"""Patch start/monitor/get_info/estimate_cost/upload/_check_folder/_modesolver_patch/load."""
806+
monkeypatch.setattr(f"{api_path}.start", lambda *a, **k: True)
807+
monkeypatch.setattr(f"{api_path}.monitor", lambda *a, **k: True)
808+
monkeypatch.setattr(f"{api_path}.get_info", lambda *a, **k: SimpleNamespace(status="success"))
809+
monkeypatch.setattr(f"{api_path}.estimate_cost", lambda *a, **k: 0.0)
810+
monkeypatch.setattr(f"{api_path}.upload", lambda *a, **k: k["task_name"])
811+
monkeypatch.setattr(WebContainer, "_check_folder", lambda *a, **k: True)
812+
monkeypatch.setattr(f"{api_path}._modesolver_patch", lambda *_, **__: None, raising=False)
813+
monkeypatch.setattr(
814+
f"{api_path}.load",
815+
_fake_load_factory(tmp_root=str(tmp_root), taskid_to_sim=taskid_to_sim),
816+
raising=False,
817+
)
818+
819+
820+
@responses.activate
821+
def test_run_with_flexible_containers_offline_lazy(monkeypatch, tmp_path):
822+
sim1 = make_sim()
823+
sim2 = sim1.updated_copy(run_time=sim1.run_time / 2)
824+
sim_container = [sim1, {"sim": sim1, "sim2": sim2}, (sim1, [sim2])]
825+
826+
h2sim = _collect_by_hash(sim_container)
827+
task_name = "T"
828+
out_dir = tmp_path / "out"
829+
830+
taskid_to_sim = {f"{task_name}_{h}": s for h, s in h2sim.items()}
831+
832+
apply_common_patches(monkeypatch, tmp_path, taskid_to_sim=taskid_to_sim)
833+
834+
data = run(sim_container, task_name=task_name, folder_name="PROJECT", path=str(out_dir))
835+
836+
assert isinstance(data, list) and len(data) == 3
837+
838+
assert isinstance(data[0], SimulationData)
839+
assert data[0].__class__.__name__ == "SimulationDataProxy"
840+
841+
assert isinstance(data[1], dict)
842+
assert "sim2" in data[1]
843+
assert isinstance(data[1]["sim2"], SimulationData)
844+
assert data[1]["sim2"].__class__.__name__ == "SimulationDataProxy"
845+
846+
assert isinstance(data[2], tuple)
847+
assert data[2][0].__class__.__name__ == "SimulationDataProxy"
848+
assert isinstance(data[2][1], list)
849+
assert data[2][1][0].__class__.__name__ == "SimulationDataProxy"
850+
851+
assert data[0].simulation == sim1
852+
assert data[1]["sim2"].simulation == sim2
853+
854+
855+
@responses.activate
856+
def test_run_single_offline_eager(monkeypatch, tmp_path):
857+
sim = make_sim()
858+
single_file = str(tmp_path / "sim.hdf5")
859+
task_name = "single"
860+
apply_common_patches(monkeypatch, tmp_path, taskid_to_sim={task_name: sim})
861+
862+
sim_data = run(sim, task_name=task_name, path=single_file)
863+
864+
assert isinstance(sim_data, SimulationData)
865+
assert sim_data.__class__.__name__ == "SimulationData" # no proxy

tests/test_web/test_webapi_mode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from tidy3d.plugins.mode import ModeSolver
1313
from tidy3d.web.api.asynchronous import run_async
1414
from tidy3d.web.api.container import Batch, Job
15+
from tidy3d.web.api.run import run
1516
from tidy3d.web.api.webapi import (
1617
abort,
1718
download_json,
@@ -20,7 +21,6 @@
2021
get_reduced_simulation,
2122
get_run_info,
2223
load_simulation,
23-
run,
2424
upload,
2525
)
2626
from tidy3d.web.core.environment import Env

tidy3d/web/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
# from .api.asynchronous import run_async # NOTE: we use autograd one now (see below)
1515
# autograd compatible wrappers for run and run_async
16-
from .api.autograd.autograd import run, run_async
16+
from .api.autograd.autograd import run_async
1717
from .api.container import Batch, BatchData, Job
18+
from .api.run import run
1819
from .api.webapi import (
1920
abort,
2021
account,
@@ -29,6 +30,7 @@
2930
load,
3031
load_simulation,
3132
monitor,
33+
postprocess_start,
3234
real_cost,
3335
start,
3436
test,
@@ -59,6 +61,7 @@
5961
"load",
6062
"load_simulation",
6163
"monitor",
64+
"postprocess_start",
6265
"real_cost",
6366
"run",
6467
"run_async",

tidy3d/web/api/asynchronous.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def run_async(
2424
reduce_simulation: Literal["auto", True, False] = "auto",
2525
pay_type: Union[PayType, str] = PayType.AUTO,
2626
priority: Optional[int] = None,
27+
lazy: bool = False,
2728
) -> BatchData:
2829
"""Submits a set of Union[:class:`.Simulation`, :class:`.HeatSimulation`, :class:`.EMESimulation`] objects to server,
2930
starts running, monitors progress, downloads, and loads results as a :class:`.BatchData` object.
@@ -56,6 +57,10 @@ def run_async(
5657
priority: int = None
5758
Priority of the simulation in the Virtual GPU (vGPU) queue (1 = lowest, 10 = highest).
5859
It affects only simulations from vGPU licenses and does not impact simulations using FlexCredits.
60+
lazy : bool = False
61+
Whether to load the actual data (``lazy=False``) or return a proxy that loads
62+
the data when accessed (``lazy=True``).
63+
5964
Returns
6065
------
6166
:class:`BatchData`
@@ -91,6 +96,7 @@ def run_async(
9196
parent_tasks=parent_tasks,
9297
reduce_simulation=reduce_simulation,
9398
pay_type=pay_type,
99+
lazy=lazy,
94100
)
95101

96102
batch_data = batch.run(path_dir=path_dir, priority=priority)

tidy3d/web/api/autograd/autograd.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def run(
106106
reduce_simulation: typing.Literal["auto", True, False] = "auto",
107107
pay_type: typing.Union[PayType, str] = PayType.AUTO,
108108
priority: typing.Optional[int] = None,
109+
lazy: bool = False,
109110
) -> WorkflowDataType:
110111
"""
111112
Submits a :class:`.Simulation` to server, starts running, monitors progress, downloads,
@@ -147,6 +148,10 @@ def run(
147148
Which method to pay for the simulation.
148149
priority: int = None
149150
Task priority for vGPU queue (1=lowest, 10=highest).
151+
lazy : bool = False
152+
Whether to load the actual data (``lazy=False``) or return a proxy that loads
153+
the data when accessed (``lazy=True``).
154+
150155
Returns
151156
-------
152157
Union[:class:`.SimulationData`, :class:`.HeatSimulationData`, :class:`.EMESimulationData`, :class:`.ModalComponentModelerData`, :class:`.TerminalComponentModelerData`]
@@ -237,6 +242,7 @@ def run(
237242
max_num_adjoint_per_fwd=max_num_adjoint_per_fwd,
238243
pay_type=pay_type,
239244
priority=priority,
245+
lazy=lazy,
240246
)
241247

242248
return run_webapi(
@@ -255,6 +261,7 @@ def run(
255261
reduce_simulation=reduce_simulation,
256262
pay_type=pay_type,
257263
priority=priority,
264+
lazy=lazy,
258265
)
259266

260267

@@ -273,6 +280,7 @@ def run_async(
273280
reduce_simulation: typing.Literal["auto", True, False] = "auto",
274281
pay_type: typing.Union[PayType, str] = PayType.AUTO,
275282
priority: typing.Optional[int] = None,
283+
lazy: bool = False,
276284
) -> BatchData:
277285
"""Submits a set of Union[:class:`.Simulation`, :class:`.HeatSimulation`, :class:`.EMESimulation`] objects to server,
278286
starts running, monitors progress, downloads, and loads results as a :class:`.BatchData` object.
@@ -307,6 +315,9 @@ def run_async(
307315
Whether to reduce structures in the simulation to the simulation domain only. Note: currently only implemented for the mode solver.
308316
pay_type: typing.Union[PayType, str] = PayType.AUTO
309317
Specify the payment method.
318+
lazy : bool = False
319+
Whether to load the actual data (``lazy=False``) or return a proxy that loads
320+
the data when accessed (``lazy=True``).
310321
311322
Returns
312323
------
@@ -349,6 +360,7 @@ def run_async(
349360
max_num_adjoint_per_fwd=max_num_adjoint_per_fwd,
350361
pay_type=pay_type,
351362
priority=priority,
363+
lazy=lazy,
352364
)
353365

354366
return run_async_webapi(
@@ -364,6 +376,7 @@ def run_async(
364376
reduce_simulation=reduce_simulation,
365377
pay_type=pay_type,
366378
priority=priority,
379+
lazy=lazy,
367380
)
368381

369382

tidy3d/web/api/autograd/engine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
def parse_run_kwargs(**run_kwargs):
1212
"""Parse the ``run_kwargs`` to extract what should be passed to the ``Job``/``Batch`` init."""
13-
job_fields = [*list(Job._upload_fields), "solver_version", "pay_type"]
13+
job_fields = [*list(Job._upload_fields), "solver_version", "pay_type", "lazy"]
1414
job_init_kwargs = {k: v for k, v in run_kwargs.items() if k in job_fields}
1515
return job_init_kwargs
1616

0 commit comments

Comments
 (0)