SDKFrameworks

Tidy3D Integration

Complete guide to integrating OptixLog with Tidy3D cloud FDTD simulations

Tidy3D Integration

Complete guide to integrating OptixLog with Tidy3D, the cloud-based FDTD simulation platform for photonics.

Tidy3D runs simulations in the cloud via the Flexcompute API. OptixLog complements this by tracking your experiments, comparing runs, and organizing results across multiple cloud jobs.


Why Use OptixLog with Tidy3D?

ChallengeOptixLog Solution
Cloud jobs scattered across tasksUnified experiment tracking
Parameter sweeps generate many task IDsAutomatic organization and comparison
Results downloaded to different pathsCentralized visualization
Hard to compare across simulation batchesSide-by-side run comparison
FlexCredit costs hard to trackLog costs alongside results

Installation

pip install tidy3d optixlog matplotlib numpy

Make sure you have both your Tidy3D API key (from Flexcompute) and OptixLog API key configured.


Basic Integration Pattern

Every Tidy3D + OptixLog workflow follows this structure:

import os
import matplotlib
matplotlib.use("agg")
import matplotlib.pyplot as plt
import numpy as np
import tidy3d as td
import tidy3d.web as web
from optixlog import Optixlog

# 1. Initialize OptixLog
client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="Tidy3DSimulations", create_if_not_exists=True)

# 2. Create run with simulation config
run = project.run(
    name="dielectric_cube_simulation",
    config={
        "wavelength": 0.75,
        "resolution": 25,
        "structure": "dielectric_cube",
        "permittivity": 2.0
    }
)

# 3. Define Tidy3D simulation
lda0 = 0.75
freq0 = td.C_0 / lda0
fwidth = freq0 / 10.0
freq_range = td.FreqRange(freq0=freq0, fwidth=fwidth)

sim = td.Simulation(
    size=(4, 3, 3),
    grid_spec=td.GridSpec.auto(min_steps_per_wvl=25),
    structures=[...],
    sources=[...],
    monitors=[...],
    run_time=3e-13
)

# 4. Run on Tidy3D cloud
data = web.run(sim, task_name="optixlog_demo", path="data/data.hdf5")

# 5. Log results to OptixLog
run.log(step=0,
        grid_cells=int(np.prod(sim.grid.num_cells)),
        run_time=sim.run_time)

# 6. Log visualizations
fig = data.plot_field("fields", "Ey", z=0)
run.log_matplotlib("field_distribution", fig.figure)
plt.close()

What to Log

Configuration (once per run)

Store all simulation parameters:

run = project.run(
    name="ring_resonator_tidy3d",
    config={
        # Frequency settings
        "wavelength_um": 1.55,
        "freq0_hz": freq0,
        "fwidth_hz": fwidth,
        
        # Domain
        "sim_size": [4, 4, 4],
        "run_time_s": 2e-13,
        "min_steps_per_wvl": 20,
        
        # Structures
        "ring_radius": 5.0,
        "waveguide_width": 0.5,
        "material_index": 3.4,
        
        # Boundaries
        "boundary_type": "PML",
        "pml_layers": 12
    }
)

Tidy3D Task Info

Log cloud task metadata:

# Upload simulation
task_id = web.upload(sim, task_name="my_simulation")

# Log task info
run.log(step=0,
        task_id=task_id,
        estimated_cost=web.estimate_cost(task_id))

# Run simulation
web.start(task_id)
web.monitor(task_id)

# Log actual cost after completion
run.log(step=1,
        real_cost=web.real_cost(task_id),
        status="completed")

Grid and Solver Info

run.log(step=0,
        num_cells=int(np.prod(sim.grid.num_cells)),
        num_time_steps=sim.num_time_steps,
        dt=float(sim.dt),
        grid_shape=list(sim.grid.num_cells))

Monitor Data

Log flux, field, and mode data:

# After simulation completes
sim_data = web.load(task_id, path="data/sim_data.hdf5")

# Log flux values
flux_data = sim_data["flux_monitor"].flux
run.log(step=1,
        total_flux=float(flux_data.sum()),
        max_flux=float(flux_data.max()))

# Log field statistics
field_data = sim_data["field_monitor"]
Ex = field_data.Ex
run.log(step=2,
        Ex_max=float(np.abs(Ex).max()),
        Ex_mean=float(np.abs(Ex).mean()))

Common Tidy3D Workflows

Basic Simulation with Logging

import os
import matplotlib
matplotlib.use("agg")
import matplotlib.pyplot as plt
import numpy as np
import tidy3d as td
import tidy3d.web as web
from optixlog import Optixlog


def run_dielectric_simulation():
    # Initialize OptixLog
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="Tidy3DExamples", create_if_not_exists=True)
    
    # Parameters
    lda0 = 0.75
    freq0 = td.C_0 / lda0
    fwidth = freq0 / 10.0
    freq_range = td.FreqRange(freq0=freq0, fwidth=fwidth)
    
    run = project.run(
        name="dielectric_cube",
        config={
            "wavelength": lda0,
            "freq0": freq0,
            "fwidth": fwidth,
            "permittivity": 2.0,
            "cube_size": 1.5
        }
    )
    
    # Create structure
    cube = td.Structure(
        geometry=td.Box(center=(0, 0, 0), size=(1.5, 1.5, 1.5)),
        medium=td.Medium(permittivity=2.0)
    )
    
    # Create source
    source = td.PointDipole(
        center=(-1.5, 0, 0),
        source_time=freq_range.to_gaussian_pulse(),
        polarization="Ey"
    )
    
    # Create monitor
    monitor = td.FieldMonitor(
        center=(0, 0, 0),
        size=(td.inf, td.inf, 0),
        freqs=freq_range.freqs(num_points=1),
        name="fields"
    )
    
    # Create simulation
    sim = td.Simulation(
        size=(4, 3, 3),
        grid_spec=td.GridSpec.auto(min_steps_per_wvl=25),
        structures=[cube],
        sources=[source],
        monitors=[monitor],
        run_time=3e-13
    )
    
    # Log grid info
    run.log(step=0,
            grid_cells=int(np.prod(sim.grid.num_cells)),
            num_time_steps=sim.num_time_steps)
    
    # Run simulation
    data = web.run(sim, task_name="optixlog_cube", path="data/cube.hdf5")
    
    # Log results
    run.log(step=1, simulation_completed=True)
    
    # Plot and log field
    ax = data.plot_field("fields", "Ey", z=0)
    run.log_matplotlib("Ey_field", ax.figure)
    plt.close()
    
    print("✅ Simulation complete!")


if __name__ == "__main__":
    run_dielectric_simulation()

Transmission Calculation

import os
import tidy3d as td
import tidy3d.web as web
import numpy as np
from optixlog import Optixlog


def run_transmission_simulation():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="Tidy3DExamples", create_if_not_exists=True)
    
    # Parameters
    freq0 = 2e14
    fwidth = 1e13
    freq_range = td.FreqRange(freq0=freq0, fwidth=fwidth)
    sim_size = [4, 4, 4]
    
    run = project.run(
        name="slab_transmission",
        config={
            "freq0": freq0,
            "fwidth": fwidth,
            "sim_size": sim_size,
            "slab_permittivity": 6.0
        }
    )
    
    # Slab structure
    slab = td.Structure(
        geometry=td.Box(center=[0, 0, 0], size=[td.inf, td.inf, 1]),
        medium=td.Medium(permittivity=6.0)
    )
    
    # Plane wave source
    source = td.PlaneWave(
        center=(0, 0, 1.5),
        direction="-",
        size=(td.inf, td.inf, 0),
        source_time=freq_range.to_gaussian_pulse(),
        pol_angle=np.pi / 2
    )
    
    # Flux monitors
    flux_in = td.FluxMonitor(
        center=(0, 0, 0.8),
        size=(td.inf, td.inf, 0),
        freqs=freq_range.freqs(num_points=50),
        name="flux_in"
    )
    
    flux_out = td.FluxMonitor(
        center=(0, 0, -0.8),
        size=(td.inf, td.inf, 0),
        freqs=freq_range.freqs(num_points=50),
        name="flux_out"
    )
    
    sim = td.Simulation(
        size=sim_size,
        grid_spec=td.GridSpec.auto(min_steps_per_wvl=20),
        structures=[slab],
        sources=[source],
        monitors=[flux_in, flux_out],
        run_time=2 / fwidth,
        boundary_spec=td.BoundarySpec.all_sides(boundary=td.PML())
    )
    
    run.log(step=0, grid_cells=int(np.prod(sim.grid.num_cells)))
    
    # Run
    data = web.run(sim, task_name="transmission", path="data/transmission.hdf5")
    
    # Calculate transmission
    flux_in_data = data["flux_in"].flux
    flux_out_data = data["flux_out"].flux
    transmission = np.abs(flux_out_data / flux_in_data)
    
    run.log(step=1,
            avg_transmission=float(np.mean(transmission)),
            max_transmission=float(np.max(transmission)),
            min_transmission=float(np.min(transmission)))
    
    print(f"📊 Average transmission: {np.mean(transmission):.4f}")


if __name__ == "__main__":
    run_transmission_simulation()

Mode Solver Integration

from tidy3d.plugins.mode import ModeSolver
import tidy3d as td
from optixlog import Optixlog
import os


def analyze_waveguide_modes():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="ModeSolverExamples", create_if_not_exists=True)
    
    run = project.run(
        name="waveguide_modes",
        config={
            "waveguide_width": 0.5,
            "waveguide_height": 0.22,
            "core_index": 3.48,
            "clad_index": 1.44,
            "wavelength": 1.55,
            "num_modes": 4
        }
    )
    
    # Define waveguide structure
    wg = td.Structure(
        geometry=td.Box(center=(0, 0, 0), size=(0.5, 0.22, td.inf)),
        medium=td.Medium.from_nk(n=3.48, k=0, freq=td.C_0 / 1.55)
    )
    
    sim = td.Simulation(
        size=(2, 2, 0.1),
        grid_spec=td.GridSpec.auto(wavelength=1.55, min_steps_per_wvl=20),
        structures=[wg],
        sources=[],
        monitors=[],
        run_time=1e-12
    )
    
    # Mode solver
    mode_solver = ModeSolver(
        simulation=sim,
        plane=td.Box(center=(0, 0, 0), size=(2, 2, 0)),
        mode_spec=td.ModeSpec(num_modes=4),
        freqs=[td.C_0 / 1.55]
    )
    
    mode_data = mode_solver.solve()
    
    # Log mode effective indices
    for mode_idx in range(4):
        n_eff = mode_data.n_eff.sel(mode_index=mode_idx).values[0]
        run.log(step=mode_idx,
                mode_index=mode_idx,
                n_eff_real=float(n_eff.real),
                n_eff_imag=float(n_eff.imag))
    
    # Plot and log mode profiles
    for mode_idx in range(4):
        fig, ax = plt.subplots()
        mode_data.Ex.sel(mode_index=mode_idx).abs.plot(ax=ax)
        ax.set_title(f"Mode {mode_idx}")
        run.log_matplotlib(f"mode_{mode_idx}_profile", fig)
        plt.close(fig)


if __name__ == "__main__":
    analyze_waveguide_modes()

Parameter Sweeps

Simple Sweep

from optixlog import Optixlog
import tidy3d as td
import tidy3d.web as web
import os


def sweep_permittivity():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="PermittivitySweep", create_if_not_exists=True)
    
    permittivities = [2.0, 4.0, 6.0, 8.0, 12.0]
    
    for eps in permittivities:
        run = project.run(
            name=f"eps_{eps}",
            config={"permittivity": eps}
        )
        
        # Build and run simulation
        cube = td.Structure(
            geometry=td.Box(center=(0, 0, 0), size=(1, 1, 1)),
            medium=td.Medium(permittivity=eps)
        )
        
        # ... rest of simulation setup
        
        # Log results
        run.log(step=0, permittivity=eps, result=computed_result)

Batch Submissions

Tidy3D supports batch submissions for efficiency:

import tidy3d.web as web
from optixlog import Optixlog


def batch_sweep():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="BatchSweep", create_if_not_exists=True)
    
    # Create dictionary of simulations
    sims = {
        f"sim_width_{w}": create_simulation(width=w)
        for w in [0.4, 0.5, 0.6, 0.7, 0.8]
    }
    
    # Run all in batch
    batch_data = web.run(sims, path_dir="data/batch/")
    
    # Log results for each
    for name, data in batch_data.items():
        run = project.run(name=name, config={"width": float(name.split("_")[-1])})
        
        # Extract and log metrics
        flux = data["flux_monitor"].flux
        run.log(step=0, total_flux=float(flux.sum()))

Logging Tidy3D Plots

Built-in Visualization Methods

import matplotlib.pyplot as plt

# Simulation structure plot
fig, ax = plt.subplots()
sim.plot_eps(z=0, ax=ax)
run.log_matplotlib("structure_eps", fig)
plt.close(fig)

# 3D structure visualization (save as image)
sim.plot_3d()
plt.savefig("structure_3d.png")
run.log_file("structure_3d", "structure_3d.png", "image/png")

# Field data plot
ax = sim_data.plot_field("field_monitor", "Ey", z=0, val="real")
run.log_matplotlib("Ey_field", ax.figure)
plt.close()

Custom Analysis Plots

import matplotlib.pyplot as plt
import numpy as np

# Transmission spectrum
freqs = sim_data["flux_monitor"].flux.f.values
flux = sim_data["flux_monitor"].flux.values
wavelengths = td.C_0 / freqs * 1e6  # Convert to μm

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(wavelengths, flux, 'b-', linewidth=2)
ax.set_xlabel('Wavelength (μm)')
ax.set_ylabel('Flux')
ax.set_title('Transmission Spectrum')
ax.grid(True, alpha=0.3)

run.log_matplotlib("transmission_spectrum", fig)
plt.close(fig)

Tracking Costs

Log FlexCredit usage for budgeting:

# Before running
task_id = web.upload(sim, task_name="cost_tracking")
estimated = web.estimate_cost(task_id)

run.log(step=0,
        task_id=task_id,
        estimated_flexcredits=estimated)

# Run simulation
web.start(task_id)
web.monitor(task_id)

# After completion
import time
time.sleep(4)  # Wait for cost calculation
real = web.real_cost(task_id)

run.log(step=1,
        actual_flexcredits=real,
        cost_efficiency=estimated / real if real > 0 else 0)

S-Matrix Calculations

from tidy3d.plugins.smatrix import ComponentModeler, Port
from optixlog import Optixlog
import os


def compute_smatrix():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="SMatrixExamples", create_if_not_exists=True)
    
    run = project.run(
        name="coupler_smatrix",
        config={"device": "directional_coupler", "coupling_length": 10.0}
    )
    
    # Define ports and modeler
    # ... setup code
    
    modeler = ComponentModeler(
        simulation=sim,
        ports=[port1, port2, port3, port4],
        freqs=freqs
    )
    
    smatrix = modeler.run()
    
    # Log S-parameters
    for i in range(4):
        for j in range(4):
            s_ij = smatrix.S[i, j]
            run.log(step=i * 4 + j,
                    s_param=f"S{i+1}{j+1}",
                    magnitude=float(np.abs(s_ij)),
                    phase_deg=float(np.angle(s_ij) * 180 / np.pi))

Tips & Best Practices

1. Use matplotlib.use("agg") for headless operation

import matplotlib
matplotlib.use("agg")  # Before importing pyplot!
import matplotlib.pyplot as plt

2. Log task IDs for reference

task_id = web.upload(sim, task_name="my_sim")
run.log(step=0, task_id=task_id)

3. Track computation costs

run.log(step=0,
        estimated_cost=web.estimate_cost(task_id),
        grid_cells=int(np.prod(sim.grid.num_cells)))

4. Store complete simulation configs

run = project.run(
    name="simulation",
    config={
        "frequency": {"freq0": freq0, "fwidth": fwidth},
        "domain": {"size": sim_size, "run_time": run_time},
        "mesh": {"min_steps_per_wvl": 20},
        "boundaries": {"type": "PML", "layers": 12},
        "tidy3d_version": td.__version__
    }
)

5. Close figures after logging

fig, ax = plt.subplots()
# ... create plot
run.log_matplotlib("plot", fig)
plt.close(fig)  # Important!

Troubleshooting

Tidy3D API Key Issues

# Configure Tidy3D API key
import tidy3d.web as web
web.configure("YOUR_TIDY3D_API_KEY")

Large Data Downloads

For simulations with large monitor data:

# Download only specific monitors
sim_data = web.load(task_id, path="data/sim.hdf5")

# Process in chunks if needed
for freq_idx in range(len(freqs)):
    field_slice = sim_data["field_monitor"].Ex.isel(f=freq_idx)
    run.log(step=freq_idx, field_max=float(np.abs(field_slice).max()))

Simulation Divergence

# Log divergence info for debugging
try:
    data = web.run(sim, task_name="test")
except Exception as e:
    run.log(step=0, error=str(e), diverged=True)

Next Steps

On this page