Skip to content

Commit 42afa41

Browse files
committed
fix: automatically create batch output directories
1 parent 7b213e1 commit 42afa41

File tree

3 files changed

+63
-9
lines changed

3 files changed

+63
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
### Fixed
1717
- Bug in `LayerRefinementSpec` that refines grids outside the layer region when one in-plane dimension is of size infinity.
1818
- Querying tasks was sometimes erroring unexpectedly.
19+
- Fixed automatic creation of missing output directories.
1920

2021
## [2.8.0] - 2025-03-04
2122

tests/test_web/test_webapi.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Tests webapi and things that depend on it
22

3+
34
import numpy as np
45
import pytest
56
import responses
@@ -564,6 +565,34 @@ def test_batch(mock_webapi, mock_job_status, mock_load, tmp_path):
564565
assert b2.real_cost() == FLEX_UNIT * len(sims)
565566

566567

568+
@responses.activate
569+
def test_create_output_dirs(mock_webapi, tmp_path, monkeypatch):
570+
"""Test that Job and Batch create output directories if they don't exist."""
571+
monkeypatch.setattr(f"{api_path}.load", lambda *args, **kwargs: True)
572+
non_existent_dirs_job = tmp_path / "new/nested/folders/job"
573+
output_file_job = non_existent_dirs_job / "output.hdf5"
574+
575+
assert not non_existent_dirs_job.exists()
576+
577+
sim = make_sim()
578+
job = Job(simulation=sim, task_name=TASK_NAME, folder_name=PROJECT_NAME)
579+
job.run(path=str(output_file_job))
580+
581+
assert non_existent_dirs_job.exists()
582+
assert non_existent_dirs_job.is_dir()
583+
584+
non_existent_dirs_batch = tmp_path / "new/nested/folders/batch"
585+
586+
assert not non_existent_dirs_batch.exists()
587+
588+
sims = {TASK_NAME: make_sim()}
589+
batch = Batch(simulations=sims, folder_name=PROJECT_NAME)
590+
batch.run(path_dir=str(non_existent_dirs_batch))
591+
592+
assert non_existent_dirs_batch.exists()
593+
assert non_existent_dirs_batch.is_dir()
594+
595+
567596
""" Async """
568597

569598

tidy3d/web/api/container.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@
3434
class WebContainer(Tidy3dBaseModel, ABC):
3535
"""Base class for :class:`Job` and :class:`Batch`, technically not used"""
3636

37+
from abc import abstractmethod
38+
3739
@staticmethod
38-
def _check_path_dir(path_dir: str) -> None:
39-
"""Make sure ``path_dir`` exists and create one if not."""
40-
location = os.path.dirname(path_dir)
41-
if len(location) > 0 and not os.path.exists(location):
42-
os.makedirs(location, exist_ok=True)
40+
@abstractmethod
41+
def _check_path_dir(path: str) -> None:
42+
"""Make sure local output directory exists and create it if not."""
43+
pass
4344

4445
@staticmethod
4546
def _check_folder(folder_name: str) -> None:
@@ -205,7 +206,6 @@ def to_file(self, fname: str) -> None:
205206
-------
206207
>>> simulation.to_file(fname='folder/sim.json') # doctest: +SKIP
207208
"""
208-
209209
task_id_cached = self._cached_properties.get("task_id")
210210
self = self.updated_copy(task_id_cached=task_id_cached)
211211
super(Job, self).to_file(fname=fname) # noqa: UP008
@@ -223,7 +223,6 @@ def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType:
223223
Union[:class:`.SimulationData`, :class:`.HeatSimulationData`, :class:`.EMESimulationData`]
224224
Object containing simulation results.
225225
"""
226-
227226
self.upload()
228227
self.start()
229228
self.monitor()
@@ -305,7 +304,7 @@ def download(self, path: str = DEFAULT_DATA_PATH) -> None:
305304
----
306305
To load the data after download, use :meth:`Job.load`.
307306
"""
308-
self._check_path_dir(path_dir=path)
307+
self._check_path_dir(path=path)
309308
web.download(task_id=self.task_id, path=path, verbose=self.verbose)
310309

311310
def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType:
@@ -321,7 +320,7 @@ def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType:
321320
Union[:class:`.SimulationData`, :class:`.HeatSimulationData`, :class:`.EMESimulationData`]
322321
Object containing simulation results.
323322
"""
324-
self._check_path_dir(path_dir=path)
323+
self._check_path_dir(path=path)
325324
data = web.load(task_id=self.task_id, path=path, verbose=self.verbose)
326325
if isinstance(self.simulation, ModeSolver):
327326
self.simulation._patch_data(data=data)
@@ -366,6 +365,19 @@ def estimate_cost(self, verbose: bool = True) -> float:
366365
"""
367366
return web.estimate_cost(self.task_id, verbose=verbose, solver_version=self.solver_version)
368367

368+
@staticmethod
369+
def _check_path_dir(path: str) -> None:
370+
"""Make sure parent directory of ``path`` exists and create it if not.
371+
372+
Parameters
373+
----------
374+
path : str
375+
Path to file to be created (including filename).
376+
"""
377+
parent_dir = os.path.dirname(path)
378+
if len(parent_dir) > 0 and not os.path.exists(parent_dir):
379+
os.makedirs(parent_dir, exist_ok=True)
380+
369381

370382
class BatchData(Tidy3dBaseModel, Mapping):
371383
"""
@@ -1030,3 +1042,15 @@ def estimate_cost(self, verbose: bool = True) -> float:
10301042
console.log("Could not get estimated batch cost!")
10311043

10321044
return batch_cost
1045+
1046+
@staticmethod
1047+
def _check_path_dir(path_dir: str) -> None:
1048+
"""Make sure ``path_dir`` exists and create it if not.
1049+
1050+
Parameters
1051+
----------
1052+
path_dir : str
1053+
Directory path where files will be saved.
1054+
"""
1055+
if len(path_dir) > 0 and not os.path.exists(path_dir):
1056+
os.makedirs(path_dir, exist_ok=True)

0 commit comments

Comments
 (0)