Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix[autograd]: add validation for missing frequency-domain monitors #2295

Merged
merged 1 commit into from
Mar 11, 2025
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 @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- New `LobeMeasurer` tool in the `microwave` plugin that locates lobes in antenna patterns and calculates lobe measures like half-power beamwidth and sidelobe level.
- Validation step that raises a `ValueError` when no frequency-domain monitors are present, preventing invalid adjoint runs.

### Changed

Expand Down
37 changes: 26 additions & 11 deletions tests/test_components/test_autograd.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
from tidy3d.components.autograd.derivative_utils import DerivativeInfo
from tidy3d.components.autograd.utils import is_tidy_box
from tidy3d.components.data.data_array import DataArray
from tidy3d.exceptions import AdjointError
from tidy3d.plugins.polyslab import ComplexPolySlab
from tidy3d.web import run, run_async
from tidy3d.web.api.autograd.autograd import MAX_NUM_TRACED_STRUCTURES
from tidy3d.web.api.autograd.utils import FieldMap

from ..utils import SIM_FULL, AssertLogLevel, run_emulated, tracer_arr
Expand Down Expand Up @@ -971,11 +973,8 @@ def objective(*args):
value = postprocess(data)
return value

val, grad = ag.value_and_grad(objective)(params0)
print(val, grad)
assert anp.all(grad != 0.0), "some gradients are 0"

val, grad = ag.value_and_grad(objective)(params0)
assert np.all(np.abs(grad) > 0), "some gradients are 0"


@pytest.mark.parametrize("structure_key, monitor_key", args)
Expand All @@ -996,11 +995,8 @@ def objective(*args):
value = value + postprocess(sim_data)
return value

val, grad = ag.value_and_grad(objective)(params0)
print(val, grad)
assert anp.all(grad != 0.0), "some gradients are 0"

val, grad = ag.value_and_grad(objective)(params0)
assert np.all(np.abs(grad) > 0), "some gradients are 0"


@pytest.mark.parametrize("structure_key", ("custom_med",))
Expand Down Expand Up @@ -1098,8 +1094,6 @@ def catch(*args, **kwargs):
def test_too_many_traced_structures(monkeypatch, use_emulated_run):
"""More traced structures than allowed."""

from tidy3d.web.api.autograd.autograd import MAX_NUM_TRACED_STRUCTURES

monitor_key = "mode"
structure_key = "size_element"
monitor, postprocess = make_monitors()[monitor_key]
Expand All @@ -1121,6 +1115,27 @@ def objective(*args):
ag.grad(objective)(params0)


def test_no_freq_adjoint(monkeypatch, use_emulated_run):
"""No frequency adjoint."""

def objective(args):
structures_traced_dict = make_structures(args)
structures = list(SIM_BASE.structures)

for structure_key in structure_keys_:
structures.append(structures_traced_dict[structure_key])

sim = SIM_BASE.updated_copy(
structures=structures,
monitors=[td.FieldTimeMonitor(size=(0, 0, 0), name="time_monitor_only")],
)
# doesn't need to be a valid objective since this should error when calling web.run
return web.run(sim, task_name="autograd_test", verbose=False)

with pytest.raises(AdjointError, match="No frequency-domain data"):
ag.grad(objective)(params0)


@pytest.mark.parametrize("colocate", [True, False])
@pytest.mark.parametrize("objtype", ["flux", "intensity"])
def test_interp_objectives(use_emulated_run, colocate, objtype):
Expand Down Expand Up @@ -1949,7 +1964,7 @@ def scaled_grad_with_slab_bounds(x: float, y: float, z: float) -> anp.ndarray:
return anp.array([new_poly.slab_bounds[0], new_poly.slab_bounds[1]])

if expect_exception:
with pytest.raises(ValueError, match=".*"):
with pytest.raises(ValueError):
check_grads(lambda x: scaled_grad_with_vertices(x, y, z), modes=["rev"])(x)
check_grads(lambda y: scaled_grad_with_vertices(x, y, z), modes=["rev"])(y)
check_grads(lambda z: scaled_grad_with_vertices(x, y, z), modes=["rev"])(z)
Expand Down
39 changes: 22 additions & 17 deletions tidy3d/web/api/autograd/autograd.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from tidy3d.components.autograd.derivative_utils import DerivativeInfo
from tidy3d.components.types import Literal

from ....exceptions import AdjointError
from ...core.s3utils import download_file, upload_file
from ..asynchronous import DEFAULT_DATA_DIR
from ..asynchronous import run_async as run_async_webapi
Expand Down Expand Up @@ -61,16 +62,21 @@ def is_valid_for_autograd(simulation: td.Simulation) -> bool:
if not traced_fields:
return False

# if no frequency-domain data (e.g. only field time monitors), raise an error
if not simulation.freqs_adjoint:
raise AdjointError(
"No frequency-domain data found in simulation, but found traced structures. "
"For an autograd run, you must have at least one frequency-domain monitor."
)

# if too many structures, raise an error
structure_indices = {i for key, i, *_ in traced_fields.keys() if key == "structures"}
num_traced_structures = len(structure_indices)
if num_traced_structures > MAX_NUM_TRACED_STRUCTURES:
msg = (
raise AdjointError(
f"Autograd support is currently limited to {MAX_NUM_TRACED_STRUCTURES} structures with "
f"traced fields. Found {num_traced_structures} structures with traced fields."
)
td.log.error(msg)
raise ValueError(msg)

return True

Expand Down Expand Up @@ -435,7 +441,6 @@ def _run_primitive(
sim_original=sim_original,
aux_data=aux_data,
)

else:
sim_combined.validate_pre_upload()
sim_original = sim_original.updated_copy(simulation_type="autograd_fwd", deep=False)
Expand Down Expand Up @@ -469,17 +474,17 @@ def _run_async_primitive(
) -> dict[str, AutogradFieldMap]:
task_names = sim_fields_dict.keys()

if local_gradient:
sims_combined = {}
for task_name in task_names:
sim_fields = sim_fields_dict[task_name]
sim_original = sims_original[task_name]
sims_combined[task_name] = setup_fwd(
sim_fields=sim_fields,
sim_original=sim_original,
local_gradient=local_gradient,
)
sims_combined = {}
for task_name in task_names:
sim_fields = sim_fields_dict[task_name]
sim_original = sims_original[task_name]
sims_combined[task_name] = setup_fwd(
sim_fields=sim_fields,
sim_original=sim_original,
local_gradient=local_gradient,
)

if local_gradient:
batch_data_combined, _ = _run_async_tidy3d(sims_combined, **run_async_kwargs)

field_map_fwd_dict = {}
Expand All @@ -493,6 +498,8 @@ def _run_async_primitive(
aux_data=aux_data,
)
else:
for sim in sims_combined.values():
sim.validate_pre_upload()
run_async_kwargs["simulation_type"] = "autograd_fwd"
run_async_kwargs["sim_fields_keys_dict"] = {}
for task_name, sim_fields in sim_fields_dict.items():
Expand Down Expand Up @@ -912,15 +919,13 @@ def setup_adj(
plt.show()

if len(sims_adj) > max_num_adjoint_per_fwd:
msg = (
raise AdjointError(
f"Number of adjoint simulations ({len(sims_adj)}) exceeds the maximum allowed "
f"({max_num_adjoint_per_fwd}) per forward simulation. This typically means that "
"there are many frequencies and monitors in the simulation that are being differentiated "
"w.r.t. in the objective function. To proceed, please double-check the simulation "
"setup, increase the 'max_num_adjoint_per_fwd' parameter in the run function, and re-run."
)
td.log.error(msg)
raise ValueError(msg)

return sims_adj

Expand Down