Skip to content

Commit 105d757

Browse files
fix(tidy3d): lazy load scipy to reduce import time (#2543)
1 parent c7f1967 commit 105d757

File tree

11 files changed

+59
-33
lines changed

11 files changed

+59
-33
lines changed

tidy3d/components/dispersion_fitter.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import Optional
66

77
import numpy as np
8-
import scipy
98
from pydantic.v1 import Field, NonNegativeFloat, PositiveFloat, PositiveInt, validator
109
from rich.progress import Progress
1110

@@ -493,6 +492,7 @@ def real_weighted_matrix(self, matrix: ArrayComplex2D) -> ArrayFloat2D:
493492

494493
def iterate_poles(self) -> FastFitterData:
495494
"""Perform a single iteration of the pole-updating procedure."""
495+
from scipy import optimize
496496

497497
def compute_zeros(residues: ArrayComplex1D, d_tilde: float) -> ArrayComplex1D:
498498
"""Compute the zeros from the residues."""
@@ -564,7 +564,7 @@ def compute_zeros(residues: ArrayComplex1D, d_tilde: float) -> ArrayComplex1D:
564564
)
565565

566566
# solve the least squares problem
567-
x_vector = scipy.optimize.lsq_linear(a_matrix_real, b_vector_real).x
567+
x_vector = optimize.lsq_linear(a_matrix_real, b_vector_real).x
568568

569569
# unpack the solution
570570
residues = np.zeros(len(self.poles), dtype=complex)
@@ -594,6 +594,8 @@ def compute_zeros(residues: ArrayComplex1D, d_tilde: float) -> ArrayComplex1D:
594594

595595
def fit_residues(self) -> FastFitterData:
596596
"""Fit residues."""
597+
from scipy import optimize
598+
597599
# build the matrices
598600
if self.optimize_eps_inf:
599601
poly_len = 1
@@ -610,7 +612,7 @@ def fit_residues(self) -> FastFitterData:
610612
# solve the least squares problem
611613
bounds = (-np.inf * np.ones(a_matrix.shape[1]), np.inf * np.ones(a_matrix.shape[1]))
612614
bounds[0][-1] = 1 # eps_inf >= 1
613-
x_vector = scipy.optimize.lsq_linear(a_matrix_real, b_vector_real).x
615+
x_vector = optimize.lsq_linear(a_matrix_real, b_vector_real).x
614616

615617
# unpack the solution
616618
residues = np.zeros(len(self.poles), dtype=complex)
@@ -650,6 +652,7 @@ def iterate_fit(self) -> FastFitterData:
650652

651653
def iterate_passivity(self, passivity_omega: ArrayFloat1D) -> tuple[FastFitterData, int]:
652654
"""Iterate passivity enforcement algorithm."""
655+
from scipy import optimize
653656

654657
size = len(self.real_poles) + 2 * len(self.complex_poles)
655658
constraint_matrix = np.imag(self.pole_matrix_omega(passivity_omega))
@@ -683,7 +686,7 @@ def jac(dx):
683686

684687
x0 = np.zeros(size)
685688
err = np.amin(c_vector - constraint_matrix @ x0)
686-
result = scipy.optimize.minimize(
689+
result = optimize.minimize(
687690
loss, x0=x0, jac=jac, constraints=cons, method="SLSQP", options=opt
688691
)
689692
x_vector = result.x

tidy3d/components/geometry/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,7 @@ def to_gds(
13861386
import gdstk
13871387

13881388
if not isinstance(cell, gdstk.Cell):
1389-
if "gdstk" in cell.__class__.__name__.lower(): # type: ignore[attr-defined]
1389+
if "gdstk" in cell.__class__.__name__.lower():
13901390
raise Tidy3dImportError(
13911391
"Module 'gdstk' not found. It is required to export shapes to gdstk cells."
13921392
)

tidy3d/components/medium.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import pydantic.v1 as pd
1717
import xarray as xr
1818
from numpy.typing import NDArray
19-
from scipy import signal
2019

2120
from tidy3d.components.material.tcad.heat import ThermalSpecType
2221
from tidy3d.constants import (
@@ -3496,6 +3495,7 @@ def _real_partial_fraction_decomposition(
34963495
``tuple`` is an array of coefficients representing any direct polynomial term.
34973496
34983497
"""
3498+
from scipy import signal
34993499

35003500
if a.ndim != 1 or np.any(np.iscomplex(a)):
35013501
raise ValidationError(

tidy3d/components/mode/derivatives.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
from __future__ import annotations
44

55
import numpy as np
6-
import scipy.sparse as sp
76

87
from tidy3d.constants import EPSILON_0, ETA_0
98

109

1110
def make_dxf(dls, shape, pmc):
1211
"""Forward derivative in x."""
12+
import scipy.sparse as sp
13+
1314
Nx, Ny = shape
1415
if Nx == 1:
1516
return sp.csr_matrix((Ny, Ny))
@@ -23,6 +24,8 @@ def make_dxf(dls, shape, pmc):
2324

2425
def make_dxb(dls, shape, pmc):
2526
"""Backward derivative in x."""
27+
import scipy.sparse as sp
28+
2629
Nx, Ny = shape
2730
if Nx == 1:
2831
return sp.csr_matrix((Ny, Ny))
@@ -38,6 +41,8 @@ def make_dxb(dls, shape, pmc):
3841

3942
def make_dyf(dls, shape, pmc):
4043
"""Forward derivative in y."""
44+
import scipy.sparse as sp
45+
4146
Nx, Ny = shape
4247
if Ny == 1:
4348
return sp.csr_matrix((Nx, Nx))
@@ -51,6 +56,8 @@ def make_dyf(dls, shape, pmc):
5156

5257
def make_dyb(dls, shape, pmc):
5358
"""Backward derivative in y."""
59+
import scipy.sparse as sp
60+
5461
Nx, Ny = shape
5562
if Ny == 1:
5663
return sp.csr_matrix((Nx, Nx))
@@ -82,6 +89,7 @@ def create_s_matrices(omega, shape, npml, dls, eps_tensor, mu_tensor, dmin_pml=(
8289
"""Makes the 'S-matrices'. When dotted with derivative matrices, they add
8390
PML. If dmin_pml is set to False, PML will not be applied on the "bottom"
8491
side of the domain."""
92+
import scipy.sparse as sp
8593

8694
# strip out some information needed
8795
Nx, Ny = shape

tidy3d/components/mode/solver.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
from __future__ import annotations
44

5-
from typing import Optional
5+
from typing import TYPE_CHECKING, Optional
66

77
import numpy as np
8-
import scipy.linalg as linalg
9-
import scipy.sparse as sp
10-
import scipy.sparse.linalg as spl
118

129
from tidy3d.components.base import Tidy3dBaseModel
1310
from tidy3d.components.types import EpsSpecType, ModeSolverType, Numpy
@@ -30,6 +27,10 @@
3027
# Good conductor permittivity cut-off value. Let it be as large as possible so long as not causing overflow in
3128
# double precision. This value is very heuristic.
3229
GOOD_CONDUCTOR_CUT_OFF = 1e70
30+
31+
if TYPE_CHECKING:
32+
from scipy import sparse as sp
33+
3334
# Consider a material to be good conductor if |ep| (or |mu|) > GOOD_CONDUCTOR_THRESHOLD * |pec_val|
3435
GOOD_CONDUCTOR_THRESHOLD = 0.9
3536

@@ -453,6 +454,7 @@ def trim_small_values(cls, mat: sp.csr_matrix, tol: float) -> sp.csr_matrix:
453454
max_element = np.amax(np.abs(mat))
454455
mat.data *= np.logical_or(np.abs(mat.data) / max_element > tol, np.abs(mat.data) > tol)
455456
mat.eliminate_zeros()
457+
return mat
456458

457459
@classmethod
458460
def solver_diagonal(
@@ -468,6 +470,8 @@ def solver_diagonal(
468470
basis_E,
469471
):
470472
"""EM eigenmode solver assuming ``eps`` and ``mu`` are diagonal everywhere."""
473+
import scipy.sparse as sp
474+
import scipy.sparse.linalg as spl
471475

472476
# code associated with these options is included below in case it's useful in the future
473477
enable_preconditioner = False
@@ -685,6 +689,7 @@ def solver_tensorial(
685689
cls, eps, mu, der_mats, num_modes, neff_guess, vec_init, mat_precision, direction
686690
):
687691
"""EM eigenmode solver assuming ``eps`` or ``mu`` have off-diagonal elements."""
692+
import scipy.sparse as sp
688693

689694
mode_solver_type = "tensorial"
690695
N = eps.shape[-1]
@@ -830,6 +835,7 @@ def solver_eigs(
830835
Number of eigenmodes to compute.
831836
guess_value : float, optional
832837
"""
838+
import scipy.sparse.linalg as spl
833839

834840
values, vectors = spl.eigs(
835841
mat, k=num_modes, sigma=guess_value, tol=TOL_EIGS, v0=vec_init, M=M
@@ -868,6 +874,7 @@ def solver_eigs_relative(
868874
Number of eigenmodes to compute.
869875
guess_value : float, optional
870876
"""
877+
import scipy.linalg as linalg
871878

872879
basis, _ = np.linalg.qr(basis_vecs)
873880
mat_basis = np.conj(basis.T) @ mat @ basis
@@ -878,22 +885,25 @@ def solver_eigs_relative(
878885

879886
@classmethod
880887
def isinstance_complex(cls, vec_or_mat, tol=TOL_COMPLEX):
881-
"""Check if a numpy array or scipy csr_matrix has complex component by looking at
888+
"""Check if a numpy array or scipy.sparse.csr_matrix has complex component by looking at
882889
norm(x.imag)/norm(x)>TOL_COMPLEX
883890
884891
Parameters
885892
----------
886893
vec_or_mat : Union[np.ndarray, sp.csr_matrix]
887894
"""
895+
import scipy.sparse.linalg as spl
896+
from scipy.sparse import csr_matrix
888897

889898
if isinstance(vec_or_mat, np.ndarray):
890899
return np.linalg.norm(vec_or_mat.imag) / (np.linalg.norm(vec_or_mat) + fp_eps) > tol
891-
if isinstance(vec_or_mat, sp.csr_matrix):
900+
if isinstance(vec_or_mat, csr_matrix):
892901
mat_norm = spl.norm(vec_or_mat)
893902
mat_imag_norm = spl.norm(vec_or_mat.imag)
894903
return mat_imag_norm / (mat_norm + fp_eps) > tol
895-
896-
raise RuntimeError("Variable type should be either numpy array or scipy csr_matrix.")
904+
raise RuntimeError(
905+
f"Variable type should be either numpy array or scipy.sparse.csr_matrix, got {type(vec_or_mat)}."
906+
)
897907

898908
@classmethod
899909
def type_conversion(cls, vec_or_mat, new_dtype):

tidy3d/material_library/parametric_materials.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,6 @@
1313
from tidy3d.constants import ELECTRON_VOLT, EPSILON_0, HBAR, K_B, KELVIN, Q_e
1414
from tidy3d.log import log
1515

16-
try:
17-
from scipy import integrate
18-
19-
INTEGRATE_AVAILABLE = True
20-
except ImportError:
21-
INTEGRATE_AVAILABLE = False
22-
2316
# default values of the physical parameters for graphene
2417
# scattering rate in eV
2518
GRAPHENE_DEF_GAMMA = 0.00041
@@ -233,6 +226,12 @@ def interband_conductivity(self, freqs: list[float]) -> list[complex]:
233226
List[complex]
234227
The list of corresponding interband conductivities, in S.
235228
"""
229+
try:
230+
from scipy import integrate
231+
232+
INTEGRATE_AVAILABLE = True
233+
except ImportError:
234+
INTEGRATE_AVAILABLE = False
236235

237236
def fermi(E: float) -> float:
238237
"""Fermi distribution."""

tidy3d/plugins/autograd/primitives/interpolate.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import numpy as np
66
from autograd.extend import defvjp, primitive
77
from numpy.typing import NDArray
8-
from scipy.linalg import solve_banded
98

109
from tidy3d.log import log
1110

@@ -369,6 +368,8 @@ def _solve_tridiagonal(lower: NDArray, diag: NDArray, upper: NDArray, rhs: NDArr
369368
np.ndarray
370369
Solution vector
371370
"""
371+
from scipy.linalg import solve_banded
372+
372373
n = diag.size
373374
ab = np.zeros((3, n))
374375
ab[0, 1:] = upper[:-1]

tidy3d/plugins/design/method.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
from __future__ import annotations
44

55
from abc import ABC, abstractmethod
6-
from typing import Any, Callable, Literal, Optional, Union
6+
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union
77

88
import numpy as np
99
import pydantic.v1 as pd
10-
import scipy.stats.qmc as qmc
1110

1211
from tidy3d.components.base import Tidy3dBaseModel
1312
from tidy3d.constants import inf
1413

1514
from .parameter import ParameterAny, ParameterFloat, ParameterInt, ParameterType
1615

17-
DEFAULT_MONTE_CARLO_SAMPLER_TYPE = qmc.LatinHypercube
16+
if TYPE_CHECKING:
17+
from scipy.stats import qmc as qmc_type
1818

1919

2020
class Method(Tidy3dBaseModel, ABC):
@@ -786,14 +786,14 @@ class AbstractMethodRandom(MethodSample, ABC):
786786
)
787787

788788
@abstractmethod
789-
def _get_sampler(self, parameters: tuple[ParameterType, ...]) -> qmc.QMCEngine:
789+
def _get_sampler(self, parameters: tuple[ParameterType, ...]) -> qmc_type.QMCEngine:
790790
"""Sampler for this ``Method`` class. If ``None``, sets a default."""
791791

792792
def _get_run_count(self, parameters: Optional[list] = None) -> int:
793793
"""Return the maximum number of runs for the method based on current method arguments."""
794794
return self.num_points
795795

796-
def sample(self, parameters: tuple[ParameterType, ...], **kwargs) -> dict[str, Any]:
796+
def sample(self, parameters: tuple[ParameterType, ...], **kwargs) -> list[dict[str, Any]]:
797797
"""Defines how the design parameters are sampled on grid."""
798798

799799
sampler = self._get_sampler(parameters)
@@ -823,11 +823,12 @@ class MethodMonteCarlo(AbstractMethodRandom):
823823
>>> method = tdd.MethodMonteCarlo(num_points=20)
824824
"""
825825

826-
def _get_sampler(self, parameters: tuple[ParameterType, ...]) -> qmc.QMCEngine:
826+
def _get_sampler(self, parameters: tuple[ParameterType, ...]) -> qmc_type.QMCEngine:
827827
"""Sampler for this ``Method`` class."""
828+
from scipy.stats import qmc
828829

829830
d = len(parameters)
830-
return DEFAULT_MONTE_CARLO_SAMPLER_TYPE(d=d, seed=self.seed)
831+
return qmc.LatinHypercube(d=d, seed=self.seed)
831832

832833

833834
MethodType = Union[

tidy3d/plugins/dispersion/fit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import numpy as np
1010
import requests
11-
import scipy.optimize as opt
1211
from pydantic.v1 import Field, validator
1312
from rich.progress import Progress
1413

@@ -361,6 +360,7 @@ def _fit_single(
361360
Tuple[:class:`.PoleResidue`, float]
362361
Results of single fit: (dispersive medium, RMS error).
363362
"""
363+
import scipy.optimize as opt
364364

365365
# NOTE: Not used
366366
def constraint(coeffs, _grad=None):

tidy3d/plugins/microwave/lobe_measurer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import numpy as np
99
import pydantic.v1 as pd
1010
from pandas import DataFrame
11-
from scipy.signal import find_peaks, peak_widths
1211

1312
from tidy3d.components.base import Tidy3dBaseModel, cached_property, skip_if_fields_missing
1413
from tidy3d.components.types import ArrayFloat1D, ArrayLike, Ax
@@ -132,6 +131,8 @@ def lobe_measures(self) -> DataFrame:
132131
DataFrame
133132
A DataFrame containing all lobe measures, where rows indicate the lobe index.
134133
"""
134+
from scipy.signal import find_peaks
135+
135136
if self.apply_cyclic_extension:
136137
angle, signal = self.cyclic_extension(self.angle, self.radiation_pattern)
137138
else:
@@ -231,6 +232,8 @@ def _calc_peak_widths(
231232
self, angle: ArrayLike, signal: ArrayLike, peaks: ArrayLike
232233
) -> tuple[ArrayLike, ArrayLike, ArrayLike, ArrayLike]:
233234
"""Get the peak widths in terms of the angular coordinates."""
235+
from scipy.signal import peak_widths
236+
234237
rel_height = 1.0 - self.width_measure
235238
last_element = len(signal) - 1
236239
left_ips = np.zeros_like(peaks)

0 commit comments

Comments
 (0)