Skip to content

Commit f7f1d1f

Browse files
committed
Second round of comments
1 parent 3afe64f commit f7f1d1f

File tree

10 files changed

+239
-124
lines changed

10 files changed

+239
-124
lines changed

docs/lectures/index.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ Welcome to our lecture series!
1010
fdtd101
1111
fdtd_workshop
1212
inversedesign
13-
small_signal_analysis
1413

1514

1615
.. include:: /lectures/fdtd101.rst
1716
.. include:: /lectures/fdtd_workshop.rst
1817
.. include:: /lectures/inversedesign.rst
19-
.. include:: /lectures/small_signal_analysis.rst

docs/lectures/small_signal_analysis.rst

Lines changed: 0 additions & 89 deletions
This file was deleted.

tests/test_components/test_frequencies.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,57 @@ def test_gaussian_pulse():
254254
assert "'fmax'" in msg and "'fmin'" in msg
255255
assert "conflict" in msg.lower()
256256
assert "exclude" in msg.lower()
257+
258+
259+
def test_sweep_decade():
260+
"""
261+
Test the sweep_decade method for generating logarithmically spaced frequencies.
262+
"""
263+
# Test basic functionality
264+
freq_range = td.FreqRange.from_freq_interval(1e3, 1e6) # 1 kHz to 1 MHz (3 decades)
265+
freqs = freq_range.sweep_decade(10) # 10 points per decade
266+
267+
# Check that frequencies are logarithmically spaced
268+
assert len(freqs) > 0
269+
assert np.isclose(freqs[0], freq_range.fmin)
270+
assert np.isclose(freqs[-1], freq_range.fmax)
271+
272+
# Test with different number of points per decade
273+
freqs_9 = freq_range.sweep_decade(9)
274+
freqs_11 = freq_range.sweep_decade(11)
275+
276+
# More points per decade should result in more total points
277+
assert len(freqs_9) < len(freqs) < len(freqs_11)
278+
279+
# All should span the same range
280+
assert np.isclose(freqs_9[0], freqs_11[0])
281+
assert np.isclose(freqs_9[-1], freqs_11[-1])
282+
assert np.isclose(freqs[0], freqs_11[0])
283+
assert np.isclose(freqs[-1], freqs_11[-1])
284+
285+
286+
def test_sweep_decade_edge_cases():
287+
"""
288+
Test edge cases and error conditions for sweep_decade.
289+
"""
290+
# Test with single decade
291+
freq_range = td.FreqRange.from_freq_interval(1e3, 1e4) # 1 decade
292+
freqs = freq_range.sweep_decade(5)
293+
assert len(freqs) >= 5 # Should have at least 5 points
294+
assert np.isclose(freqs[0], freq_range.fmin)
295+
assert np.isclose(freqs[-1], freq_range.fmax)
296+
297+
# Test error conditions
298+
freq_range = td.FreqRange.from_freq_interval(1e3, 1e6)
299+
300+
# Negative points per decade
301+
with pytest.raises(
302+
ValueError, match="'num_points_per_decade' must be strictly positive, got -1."
303+
):
304+
freq_range.sweep_decade(-1)
305+
306+
# Zero points per decade
307+
with pytest.raises(
308+
ValueError, match="'num_points_per_decade' must be strictly positive, got 0."
309+
):
310+
freq_range.sweep_decade(0)

tests/test_components/test_heat_charge.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,11 @@ def test_heat_charge_bcs_validation(boundary_conditions):
899899

900900
# Invalid ACVoltageSource: infinite voltage
901901
with pytest.raises(pd.ValidationError):
902-
td.VoltageBC(source=td.ACVoltageSource(voltage=np.array([td.inf, 0, 1]), amplitude=1e-2))
902+
td.VoltageBC(
903+
source=td.ACVoltageSource(
904+
voltage=np.array([td.inf, 0, 1]), signal=td.SinusoidalSignal(amplitude=1e-2)
905+
)
906+
)
903907

904908

905909
def test_ssac_freqs_validation():
@@ -937,7 +941,7 @@ def test_ssac_freqs_validation():
937941

938942
charge_tolerance = td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=1e3, max_iters=400)
939943
ssac_freqs_input = [1e3, 1e4, 1e5]
940-
isothermal_spec = td.IsothermalSteadyChargeDCAnalysis(
944+
isothermal_spec = td.IsothermalSSACAnalysis(
941945
temperature=300,
942946
tolerance_settings=charge_tolerance,
943947
fermi_dirac=True,
@@ -965,7 +969,7 @@ def test_ssac_freqs_validation():
965969
)
966970

967971
# Test that ssac_freqs with ACVoltageSource works
968-
ac_source = td.ACVoltageSource(voltage=[0, 1, 2], amplitude=1e-3)
972+
ac_source = td.ACVoltageSource(voltage=[0, 1, 2], signal=td.SinusoidalSignal(amplitude=1e-3))
969973
sim = td.HeatChargeSimulation(
970974
size=(8, 8, 8),
971975
center=(0, 0, 0),
@@ -1332,10 +1336,14 @@ def createSolid(geometry, name):
13321336
solid_structure_3 = createSolid(solid_box_3, "solid_3")
13331337

13341338
bc_ssac1 = td.VoltageBC(
1335-
source=td.ACVoltageSource(voltage=[0, 1], amplitude=1e-3, name="ac_source1")
1339+
source=td.ACVoltageSource(
1340+
voltage=[0, 1], signal=td.SinusoidalSignal(amplitude=1e-3), name="ac_source1"
1341+
)
13361342
)
13371343
bc_ssac2 = td.VoltageBC(
1338-
source=td.ACVoltageSource(voltage=[0, 1], amplitude=1e-3, name="ac_source2")
1344+
source=td.ACVoltageSource(
1345+
voltage=[0, 1], signal=td.SinusoidalSignal(amplitude=1e-3), name="ac_source2"
1346+
)
13391347
)
13401348

13411349
placement1 = td.StructureStructureInterface(structures=["solid_1", "solid_2"])
@@ -1346,14 +1354,15 @@ def createSolid(geometry, name):
13461354
td.HeatChargeBoundarySpec(condition=bc_ssac2, placement=placement2),
13471355
]
13481356

1349-
with pytest.raises(pd.ValidationError, match="Only a single AC source can be supplied"):
1357+
with pytest.raises(pd.ValidationError, match="Only a single AC source can be supplied."):
13501358
td.HeatChargeSimulation(
13511359
structures=[solid_structure_1, solid_structure_2, solid_structure_3],
13521360
center=(2, 2, 2),
13531361
size=(6, 6, 6),
13541362
boundary_spec=boundary_spec,
13551363
medium=air,
13561364
grid_spec=grid_specs["uniform"],
1365+
analysis_spec=td.IsothermalSSACAnalysis(ssac_freqs=[1e2, 1e3]),
13571366
)
13581367

13591368

@@ -1627,8 +1636,16 @@ def test_charge_simulation(
16271636

16281637
# Two AC sources cannot be defined
16291638
with pytest.raises(pd.ValidationError):
1630-
bc_ssac_n = bc_n.updated_copy(source=td.ACVoltageSource(voltage=[0, 1], amplitude=1e-3))
1631-
bc_ssac_p = bc_p.updated_copy(source=td.ACVoltageSource(voltage=[0, 1], amplitude=1e-3))
1639+
bc_ssac_n = bc_n.updated_copy(
1640+
source=td.ACVoltageSource(
1641+
voltage=[0, 1], signal=td.SinusoidalSignal(amplitude=1e-3)
1642+
)
1643+
)
1644+
bc_ssac_p = bc_p.updated_copy(
1645+
source=td.ACVoltageSource(
1646+
voltage=[0, 1], signal=td.SinusoidalSignal(amplitude=1e-3)
1647+
)
1648+
)
16321649
analysis = sim.analysis_spec.updated_copy(ssac_freqs=[1e2, 1e3])
16331650
sim.updated_copy(boundary_spec=[bc_n_ground, bc_p_ground], analysis_spec=analysis)
16341651

tidy3d/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
from tidy3d.components.microwave.data.monitor_data import (
2121
AntennaMetricsData,
2222
)
23+
from tidy3d.components.spice.analysis.ac import IsothermalSSACAnalysis
2324
from tidy3d.components.spice.analysis.dc import (
2425
ChargeToleranceSpec,
2526
IsothermalSteadyChargeDCAnalysis,
2627
SteadyChargeDCAnalysis,
2728
)
28-
from tidy3d.components.spice.sources.ac import ACVoltageSource
29+
from tidy3d.components.spice.sources.ac import ACSignal, ACVoltageSource, SinusoidalSignal
2930
from tidy3d.components.spice.sources.dc import DCCurrentSource, DCVoltageSource, GroundVoltageSource
3031
from tidy3d.components.spice.sources.types import VoltageSourceType
3132
from tidy3d.components.tcad.analysis.heat_simulation_type import UnsteadyHeatAnalysis, UnsteadySpec
@@ -445,6 +446,7 @@ def set_logging_level(level: str) -> None:
445446
"PML",
446447
"TFSF",
447448
"ABCBoundary",
449+
"ACSignal",
448450
"ACVoltageSource",
449451
"Absorber",
450452
"AbsorberParams",
@@ -613,6 +615,7 @@ def set_logging_level(level: str) -> None:
613615
"IndexedVoltageDataArray",
614616
"InsulatingBC",
615617
"InternalAbsorber",
618+
"IsothermalSSACAnalysis",
616619
"IsothermalSteadyChargeDCAnalysis",
617620
"IsotropicEffectiveDOS",
618621
"KerrNonlinearity",
@@ -694,6 +697,7 @@ def set_logging_level(level: str) -> None:
694697
"SimulationData",
695698
"SimulationDataMap",
696699
"SimulationMap",
700+
"SinusoidalSignal",
697701
"SlotboomBandGapNarrowing",
698702
"SolidMedium",
699703
"SolidSpec",

tidy3d/components/frequencies.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,54 @@ def freqs(self, num_points: int, spacing: str = "uniform_freq") -> NDArray[np.fl
436436
f"Received: {spacing!r}. Please provide a valid spacing type."
437437
)
438438

439+
def sweep_decade(self, num_points_per_decade: int) -> NDArray[np.float64]:
440+
"""
441+
Generate frequencies with logarithmic spacing across decades.
442+
443+
Notes
444+
-----
445+
This method creates a logarithmically spaced frequency sweep.
446+
It is analogous to the SPICE AC analysis command for a decade
447+
sweep: ``.ac dec <num_points_per_decade> <fmin> <fmax>``
448+
449+
Parameters
450+
----------
451+
num_points_per_decade : int
452+
Number of frequency points per decade. Must be strictly positive.
453+
454+
Returns
455+
-------
456+
NDArray[np.float64]
457+
Array of frequencies with logarithmic spacing across decades.
458+
459+
Raises
460+
------
461+
ValueError
462+
If ``num_points_per_decade`` is not positive, or if frequency range is invalid.
463+
464+
Examples
465+
--------
466+
>>> freq_range = td.FreqRange.from_freq_interval(1e3, 1e6) # 1 kHz to 1 MHz
467+
>>> freqs = freq_range.sweep_decade(10) # 10 points per decade
468+
"""
469+
# Input validation
470+
if num_points_per_decade <= 0:
471+
raise ValueError(
472+
f"'num_points_per_decade' must be strictly positive, got {num_points_per_decade}."
473+
)
474+
475+
# Calculate logarithmic range
476+
fstart = np.log10(self.fmin)
477+
fend = np.log10(self.fmax)
478+
num_decades = fend - fstart
479+
480+
# Calculate total number of points
481+
# Add 1 to include the endpoint, ensuring we cover the full range
482+
num_total_points = int(np.round(num_decades * num_points_per_decade)) + 1
483+
484+
# Generate logarithmically spaced frequencies
485+
return np.logspace(fstart, fend, num_total_points)
486+
439487
def wvls(self, num_points: int, spacing: str = "uniform_wvl") -> NDArray[np.float64]:
440488
"""
441489
method ``wvls()`` returns a numpy array of ``num_points`` wavelengths uniformly
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
This class defines standard SPICE electrical_analysis types (electrical simulations configurations).
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import pydantic.v1 as pd
8+
9+
from tidy3d.components.spice.analysis.dc import IsothermalSteadyChargeDCAnalysis
10+
from tidy3d.components.types import ArrayFloat1D
11+
from tidy3d.constants import HERTZ
12+
13+
14+
class IsothermalSSACAnalysis(IsothermalSteadyChargeDCAnalysis):
15+
"""
16+
Configures Isothermal Small-Signal AC (SSAC) analysis parameters for charge simulation.
17+
18+
This analysis class provides a more convenient interface for SSAC analysis by allowing
19+
frequency range specification and automatic frequency sweep generation.
20+
21+
Examples
22+
--------
23+
>>> import tidy3d as td
24+
>>> freq_range = td.FreqRange.from_freq_interval(start_freq=1e3, stop_freq=1e6)
25+
>>> sweep_freqs = freq_range.sweep_decade(num_points_per_decade=10)
26+
>>> ssac_spec = td.IsothermalSSACAnalysis(ssac_freqs=sweep_freqs)
27+
"""
28+
29+
ssac_freqs: ArrayFloat1D = pd.Field(
30+
...,
31+
title="Small Signal AC Frequencies",
32+
description="List of frequencies for small signal AC analysis. "
33+
"At least one 'ACVoltageSource' must be present in the boundary conditions.",
34+
units=HERTZ,
35+
)

0 commit comments

Comments
 (0)