Source code for tidy3d.plugins.dispersion.fit_fast
"""Fit PoleResidue Dispersion models to optical NK data"""from__future__importannotationsfromtypingimportTuple,Optionalimportnumpyasnpfromrich.progressimportProgressfrompydantic.v1importField,validator,PositiveInt,NonNegativeFloat,PositiveFloatimportscipyfrom.fitimportDispersionFitterfrom...logimportlog,get_logging_consolefrom...components.baseimportTidy3dBaseModel,cached_property,skip_if_fields_missingfrom...components.mediumimportPoleResidue,LOSS_CHECK_MIN,LOSS_CHECK_MAX,LOSS_CHECK_NUMfrom...components.typesimportArrayFloat1D,ArrayComplex1D,ArrayFloat2D,ArrayComplex2Dfrom...exceptionsimportValidationError# numerical tolerance for pole relocation for fast fitterTOL=1e-8# numerical cutoff for passivity testingCUTOFF=np.finfo(np.float32).eps# parameters for passivity optimizationPASSIVITY_NUM_ITERS_DEFAULT=50SLSQP_CONSTRAINT_SCALE_DEFAULT=1e35# min value of the rms for default weights calculated based on Re[eps], Im[eps]RMS_MIN=0.1DEFAULT_MAX_POLES=5DEFAULT_NUM_ITERS=20DEFAULT_TOLERANCE_RMS=1e-5# this avoids divide by zero errors with lossless polesSCALE_FACTOR=1.01
[docs]classAdvancedFastFitterParam(Tidy3dBaseModel):"""Advanced fast fitter parameters."""loss_bounds:Tuple[float,float]=Field((0,np.inf),title="Loss bounds",description="Bounds (lower, upper) on Im[eps]. Default corresponds to only passivity. ""A lower bound of 0 or greater ensures passivity. To fit a gain medium without ""additional constraints, use ``loss_bounds=(-np.inf, np.inf)``. ""Increasing the lower bound could help with simulation stability. ""A finite upper bound may be helpful when fitting lossless materials. ""In this case, consider also increasing the weight for fitting the imaginary part.",)weights:Tuple[NonNegativeFloat,NonNegativeFloat]=Field(None,title="Weights",description="Weights (real, imag) in objective function for fitting. The weights ""are applied to the real and imaginary parts of the permittivity epsilon. The weights ""will be rescaled together so they average to 1. If ``None``, the weights are calculated ""according to the typical value of the real and imaginary part, so that the relative error ""in the real and imaginary part of the fit should be comparable. ""More precisely, the RMS value ``rms`` of the real and imaginary parts are ""calculated, and the default weights are 1 / max(``rms``, ``RMS_MIN``). ""Changing this can be helpful if fitting either the real or imaginary part is ""more important than the other.",)show_progress:bool=Field(True,title="Show progress bar",description="Whether to show progress bar during fitter run.",)show_unweighted_rms:bool=Field(False,title="Show unweighted RMS",description="Whether to show unweighted RMS error in addition to the default weighted "'RMS error. Requires ``td.config.logging_level = "INFO"``.',)relaxed:Optional[bool]=Field(None,title="Relaxed",description="Whether to use relaxed fitting algorithm, which ""has better pole relocation properties. If ``None``, will try both original and relaxed ""algorithms.",)smooth:Optional[bool]=Field(None,title="Smooth",description="Whether to use real starting poles, which can help when fitting smooth data. ""If ``None``, will try both real and complex starting poles.",)logspacing:Optional[bool]=Field(None,title="Log spacing",description="Whether to space the poles logarithmically. ""If ``None``, will try both log and linear spacing.",)# more technical parametersnum_iters:PositiveInt=Field(DEFAULT_NUM_ITERS,title="Number of iterations",description="Number of iterations of the fitting algorithm. Make this smaller to ""speed up fitter, or make it larger to try to improve fit.",)passivity_num_iters:PositiveInt=Field(PASSIVITY_NUM_ITERS_DEFAULT,title="Number of loss bounds enforcement iterations",description="Number of loss bounds enforcement iterations of the fitting algorithm. ""Make this smaller to speed up fitter. There will be a warning if this value ""is too small. To fit a gain medium, use the ``loss_bounds`` parameter instead.",)slsqp_constraint_scale:PositiveFloat=Field(SLSQP_CONSTRAINT_SCALE_DEFAULT,title="Scale factor for SLSQP",description="Passivity constraint is weighted relative to fit quality by this factor, ""before running passivity optimization using the SLSQP algorithm. ""There will be a warning if this value is too small.",)@validator("loss_bounds",always=True)def_max_loss_geq_min_loss(cls,val):"""Must have max_loss >= min_loss."""ifval[0]>val[1]:raiseValidationError("The loss lower bound cannot be larger than the loss upper bound.")returnval@validator("weights",always=True)def_weights_average_to_one(cls,val):"""Weights must average to one."""ifvalisNone:returnNoneavg=(val[0]+val[1])/2new_val=(val[0]/avg,val[1]/avg)returnnew_val
classFastFitterData(AdvancedFastFitterParam):"""Data class for internal use while running fitter."""omega:ArrayComplex1D=Field(...,title="Angular frequencies in eV",description="Angular frequencies in eV")eps:ArrayComplex1D=Field(...,title="Permittivity",description="Permittivity to fit")optimize_eps_inf:bool=Field(None,title="Optimize eps_inf",description="Whether to optimize ``eps_inf``.")num_poles:PositiveInt=Field(None,title="Number of poles",description="Number of poles")eps_inf:float=Field(None,title="eps_inf",description="Value of ``eps_inf``.",)poles:Optional[ArrayComplex1D]=Field(None,title="Pole frequencies in eV",description="Pole frequencies in eV")residues:Optional[ArrayComplex1D]=Field(None,title="Residues in eV",description="Residues in eV")passivity_optimized:Optional[bool]=Field(False,title="Passivity optimized",description="Whether the fit was optimized to enforce passivity. If None, ""then passivity optimization did not terminate; ""consider increasing ``AdvancedFastFitterParam.passivity_num_iters``.",)passivity_num_iters_too_small:bool=Field(False,title="Passivity num iters too small",description="If this is True, consider increasing ""``AdvancedFastFitterParam.passivity_num_iters``.",)slsqp_constraint_scale_too_small:bool=Field(False,title="SLSQP constraint scale too small",description="The constraint is rescaled by ``slsqp_constraint_scale`` before ""running passivity optimization using the SLSQP algorithm. If this is ``True``, ""consider increasing ``AdvancedFastFitterParam.slsqp_constraint_scale``.",)@validator("eps_inf",always=True)@skip_if_fields_missing(["optimize_eps_inf"])def_eps_inf_geq_one(cls,val,values):"""Must have eps_inf >= 1 unless it is being optimized. In the latter case, it will be made >= 1 later."""ifvalues["optimize_eps_inf"]isFalseandval<1:raiseValidationError("The value of 'eps_inf' must be at least 1.")returnval@validator("poles",always=True)@skip_if_fields_missing(["logspacing","smooth","num_poles","omega","num_poles"])def_generate_initial_poles(cls,val,values):"""Generate initial poles."""ifvalisnotNone:returnvalif(values.get("logspacing")isNoneorvalues.get("smooth")isNoneorvalues.get("num_poles")isNone):returnNoneomega=values["omega"]num_poles=values["num_poles"]ifvalues["logspacing"]:pole_range=np.logspace(np.log10(min(omega)/SCALE_FACTOR),np.log10(max(omega)*SCALE_FACTOR),num_poles)else:pole_range=np.linspace(min(omega)/SCALE_FACTOR,max(omega)*SCALE_FACTOR,num_poles)ifvalues["smooth"]:poles=-pole_rangeelse:poles=-pole_range/100+1j*pole_rangereturnpoles@validator("residues",always=True)@skip_if_fields_missing(["poles"])def_generate_initial_residues(cls,val,values):"""Generate initial residues."""ifvalisnotNone:returnvalpoles=values.get("poles")ifpolesisNone:returnNonereturnnp.zeros(len(poles))@classmethoddefinitialize(cls,omega:ArrayFloat1D,eps:ArrayComplex1D,eps_inf:float,advanced_param:AdvancedFastFitterParam,)->FastFitterData:"""Construct :class:`.FastFitterData` from :class:`.AdvancedFastFitterParam`."""weights=advanced_param.weightsorcls.get_default_weights(eps)optimize_eps_inf=Noneifeps_infisNoneelseFalsedata=FastFitterData(num_iters=advanced_param.num_iters,passivity_num_iters=advanced_param.passivity_num_iters,slsqp_constraint_scale=advanced_param.slsqp_constraint_scale,loss_bounds=advanced_param.loss_bounds,weights=weights,show_progress=advanced_param.show_progress,show_unweighted_rms=advanced_param.show_unweighted_rms,relaxed=advanced_param.relaxed,smooth=advanced_param.smooth,logspacing=advanced_param.logspacing,omega=omega,eps=eps,optimize_eps_inf=optimize_eps_inf,eps_inf=eps_infor1,)returndata@cached_propertydefreal_poles(self)->ArrayFloat1D:"""The real poles."""returnself.poles[np.isreal(self.poles)]@cached_propertydefcomplex_poles(self)->ArrayFloat1D:"""The complex poles."""returnself.poles[np.iscomplex(self.poles)]@classmethoddefget_default_weights(cls,eps:ArrayComplex1D)->Tuple[float,float]:"""Default weights based on real and imaginary part of eps."""rms=np.array([np.sqrt(np.mean(x**2))forxin(np.real(eps),np.imag(eps))])rms=np.maximum(RMS_MIN,rms)weights=[1/valforvalinrms]average=(weights[0]+weights[1])/2weights=[val/averageforvalinweights]returntuple(weights)@cached_propertydefpole_residue(self)->PoleResidue:"""Corresponding :class:`.PoleResidue` model."""ifself.eps_infisNoneorself.polesisNone:returnNonereturnPoleResidue(eps_inf=self.eps_inf,poles=list(zip(PoleResidue.eV_to_angular_freq(self.poles),PoleResidue.eV_to_angular_freq(self.residues),)),)defevaluate(self,omega:float)->complex:"""Evaluate model at omega in eV."""eps=self.eps_infforpole,resinzip(self.poles,self.residues):eps+=-res/(1j*omega+pole)-np.conj(res)/(1j*omega+np.conj(pole))returneps@cached_propertydefvalues(self)->ArrayComplex1D:"""Evaluate model at sample frequencies."""returnself.evaluate(self.omega)@cached_propertydefloss_in_bounds_violations(self)->ArrayFloat1D:"""Return list of frequencies where model violates loss bounds."""extrema_list=PoleResidue.imag_ep_extrema(zip(self.poles,self.residues))# let's check a big range in addition to the imag_extremarange_omega=np.logspace(LOSS_CHECK_MIN,LOSS_CHECK_MAX,LOSS_CHECK_NUM)omega=np.concatenate((range_omega,extrema_list))loss=self.evaluate(omega).imagbmin,bmax=self.loss_boundsviolation_inds=np.where((loss<bmin)|(loss>bmax))returnomega[violation_inds]@cached_propertydefloss_in_bounds(self)->bool:"""Whether model satisfies loss bounds."""returnlen(self.loss_in_bounds_violations)==0@cached_propertydefsellmeier_passivity(self)->bool:"""Check passivity in the case of marginally stable poles, if loss_bounds[0] >= 0."""ifself.loss_bounds[0]<0:returnTrueforpole,resinzip(self.poles,self.residues):ifnp.real(pole)==0:ifnp.imag(pole)*np.imag(res)>0:returnFalsereturnTrue@cached_propertydefrms_error(self)->float:"""RMS error."""ifself.eps_infisNoneorself.residuesisNone:returnnp.infdiff=self.values-self.epssquare=(self.weights[0]*np.real(diff))**2+(self.weights[1]*np.imag(diff))**2returnnp.sqrt(np.sum(square)/len(self.eps))@cached_propertydefunweighted_rms_error(self)->float:"""RMS error."""ifself.eps_infisNoneorself.residuesisNone:returnnp.infdiff=self.values-self.epssquare=(np.real(diff))**2+(np.imag(diff))**2returnnp.sqrt(np.sum(square)/len(self.eps))defpole_matrix_omega(self,omega:ArrayFloat1D)->ArrayComplex2D:"""A matrix used in the fitting algorithms containing the pole information."""size=len(self.real_poles)+2*len(self.complex_poles)pole_matrix=np.zeros((len(omega),size),dtype=complex)fori,poleinenumerate(self.real_poles):pole_matrix[:,i]=1/(1j*omega+pole)+1/(1j*omega+np.conj(pole))offset=len(self.real_poles)fori,poleinenumerate(self.complex_poles):pole_matrix[:,offset+2*i]=1/(1j*omega+pole)+1/(1j*omega+np.conj(pole))pole_matrix[:,offset+2*i+1]=1j/(1j*omega+pole)-1j/(1j*omega+np.conj(pole))returnpole_matrix@cached_propertydefpole_matrix(self)->ArrayComplex2D:"""A matrix used in the fitting algorithms containing the pole information."""returnself.pole_matrix_omega(self.omega)defreal_weighted_matrix(self,matrix:ArrayComplex2D)->ArrayFloat2D:"""Turn a complex matrix into a weighted real matrix."""returnnp.concatenate((self.weights[0]*np.real(matrix),self.weights[1]*np.imag(matrix)))defiterate_poles(self)->FastFitterData:"""Perform a single iteration of the pole-updating procedure."""defcompute_zeros(residues:ArrayComplex1D,d_tilde:float)->ArrayComplex1D:"""Compute the zeros from the residues."""size=len(self.real_poles)+2*len(self.complex_poles)a_matrix=np.zeros((size,size))b_vector=np.zeros(size)c_vector=np.zeros(size)fori,poleinenumerate(self.real_poles):a_matrix[i,i]=np.real(pole)b_vector[i]=1c_vector[i]=np.real(residues[i])fori,poleinenumerate(self.complex_poles):offset=len(self.real_poles)a_matrix[offset+2*i:offset+2*i+2,offset+2*i:offset+2*i+2]=[[np.real(pole),np.imag(pole)],[-np.imag(pole),np.real(pole)]]b_vector[offset+2*i:offset+2*i+2]=[2,0]c_vector[offset+2*i:offset+2*i+2]=[np.real(residues[i]),np.imag(residues[i]),]zeros,_=np.linalg.eig(a_matrix+np.outer(b_vector,c_vector)/d_tilde)returnzeros.astype(complex)d_tilde=Noneifself.relaxedelse1for_inrange(2ifself.relaxedelse1):# build the matricesifself.optimize_eps_inf:poly_len=1b_vector=np.zeros(len(self.eps),dtype=complex)else:poly_len=0# fixed eps_inf enters into b_vectorb_vector=-self.eps_inf*np.ones(len(self.eps),dtype=complex)a_matrix=np.hstack((self.pole_matrix,np.ones((len(self.omega),poly_len)),-self.eps[:,None]*self.pole_matrix,))ifd_tildeisNone:nontriviality_weight=np.sqrt(np.sum(np.abs(self.eps)**2))/len(self.omega)nontriviality_matrix=np.real(np.sum(self.pole_matrix,axis=0))nontriviality_matrix=np.concatenate((np.zeros(len(nontriviality_matrix)),np.zeros(poly_len),nontriviality_matrix,[len(self.omega)],))nontriviality_matrix*=nontriviality_weighta_matrix=np.hstack((a_matrix,-self.eps[:,None]))else:b_vector+=d_tilde*self.epsa_matrix_real=self.real_weighted_matrix(a_matrix)b_vector_real=self.real_weighted_matrix(b_vector)ifd_tildeisNone:a_matrix_real=np.vstack((a_matrix_real,nontriviality_matrix))b_vector_real=np.concatenate((b_vector_real,[nontriviality_weight*len(self.omega)]))# solve the least squares problemx_vector=scipy.optimize.lsq_linear(a_matrix_real,b_vector_real).x# unpack the solutionresidues=np.zeros(len(self.poles),dtype=complex)size=len(self.real_poles)+2*len(self.complex_poles)offset0=size+poly_lenforiinrange(len(self.real_poles)):residues[i]=x_vector[offset0+i]offset=len(self.real_poles)foriinrange(len(self.complex_poles)):residues[offset+i]=(x_vector[offset0+offset+2*i]+1j*x_vector[offset0+offset+2*i+1])ifd_tildeisNone:d_tilde=x_vector[-1]ifabs(d_tilde)>TOL:breakd_tilde=TOLifd_tilde==0elseTOL*np.sign(d_tilde)new_poles=compute_zeros(residues,d_tilde)# only keep one in each conjugate pairnew_poles=new_poles[np.imag(new_poles)<=0]# impose stability, negative real partnew_poles[np.real(new_poles)>0]=-1j*np.conj(1j*new_poles[np.real(new_poles)>0])# impose minimum decay ratereturnself.updated_copy(poles=new_poles)deffit_residues(self)->FastFitterData:"""Fit residues."""# build the matricesifself.optimize_eps_inf:poly_len=1b_vector=self.epselse:poly_len=0b_vector=self.eps-self.eps_inf*np.ones(len(self.eps))a_matrix=np.hstack((self.pole_matrix,np.ones((len(self.omega),poly_len))))a_matrix_real=self.real_weighted_matrix(a_matrix)b_vector_real=self.real_weighted_matrix(b_vector)# solve the least squares problembounds=(-np.inf*np.ones(a_matrix.shape[1]),np.inf*np.ones(a_matrix.shape[1]))bounds[0][-1]=1# eps_inf >= 1x_vector=scipy.optimize.lsq_linear(a_matrix_real,b_vector_real).x# unpack the solutionresidues=np.zeros(len(self.poles),dtype=complex)foriinrange(len(self.real_poles)):residues[i]=x_vector[i]offset=len(self.real_poles)foriinrange(len(self.complex_poles)):residues[offset+i]=x_vector[offset+2*i]+1j*x_vector[offset+2*i+1]ifself.optimize_eps_inf:returnself.updated_copy(residues=-residues,eps_inf=x_vector[-1])returnself.updated_copy(residues=-residues)defiterate_fit(self)->FastFitterData:"""Perform a single fit to the data and return optimization result. Returns ------- :class:`.FastFitterData` Result of single fit. """model=self.iterate_poles()model=model.fit_residues()returnmodeldefiterate_passivity(self,passivity_omega:ArrayFloat1D)->Tuple[FastFitterData,int]:"""Iterate passivity enforcement algorithm."""size=len(self.real_poles)+2*len(self.complex_poles)constraint_matrix=np.imag(self.pole_matrix_omega(passivity_omega))c_vector=np.imag(self.evaluate(passivity_omega))ifself.loss_bounds[1]!=np.inf:constraint_matrix=np.concatenate((constraint_matrix,-constraint_matrix))c_vector=np.concatenate((c_vector-self.loss_bounds[0],self.loss_bounds[1]-c_vector))a_matrix_real=self.real_weighted_matrix(self.pole_matrix)b_vector_real=self.real_weighted_matrix(self.values-self.eps)h_matrix=a_matrix_real.T@a_matrix_realf_vector=a_matrix_real.T@b_vector_realdefloss(dx):returndx.T@h_matrix@dx/2-f_vector.T@dxdefjac(dx):returndx.T@h_matrix-f_vector.Tcons={"type":"ineq","fun":lambdadx:(c_vector-constraint_matrix@dx)*self.slsqp_constraint_scale,"jac":lambdadx:-constraint_matrix*self.slsqp_constraint_scale,}opt={"disp":False}x0=np.zeros(size)err=np.amin(c_vector-constraint_matrix@x0)result=scipy.optimize.minimize(loss,x0=x0,jac=jac,constraints=cons,method="SLSQP",options=opt)x_vector=result.xerr=np.amin(c_vector-constraint_matrix@x_vector)model=selfifresult.status==0anderr<0:model=self.updated_copy(slsqp_constraint_scale_too_small=True)residues=np.zeros(len(self.poles),dtype=complex)foriinrange(len(self.real_poles)):residues[i]=x_vector[i]offset=len(self.real_poles)foriinrange(len(self.complex_poles)):residues[offset+i]=x_vector[offset+2*i]+1j*x_vector[offset+2*i+1]returnmodel.updated_copy(residues=np.array(self.residues)+residues),result.statusdefenforce_passivity(self,)->FastFitterData:"""Try to enforce loss bounds."""ifself.loss_in_bounds:returnselfmodel=self.updated_copy(passivity_optimized=True)violations=model.loss_in_bounds_violationsrange_omega=np.logspace(LOSS_CHECK_MIN,LOSS_CHECK_MAX,LOSS_CHECK_NUM)violations=np.unique(np.concatenate((violations,range_omega)))# only need one iteration since poles are fixedfor_inrange(self.passivity_num_iters):model,status=model.iterate_passivity(violations)ifmodel.loss_in_boundsorstatus!=0:returnmodelnew_violations=model.loss_in_bounds_violationsviolations=np.unique(np.concatenate((violations,new_violations)))model=model.updated_copy(passivity_num_iters_too_small=True)returnmodel
[docs]classFastDispersionFitter(DispersionFitter):"""Tool for fitting refractive index data to get a dispersive medium described by :class:`.PoleResidue` model."""def_fit_fixed_parameters(self,num_poles_range:Tuple[PositiveInt,PositiveInt],model:FastFitterData)->FastFitterData:deffit_non_passive(model:FastFitterData)->FastFitterData:best_model=modelfor_inrange(model.num_iters):model=model.iterate_fit()if(num_poles_range[0]<=len(model.poles)andlen(model.poles)<=num_poles_range[1]andmodel.rms_error<best_model.rms_error):best_model=modelreturnbest_modelmodel=fit_non_passive(model)ifmodel.eps_inf<1:model=model.updated_copy(eps_inf=max(1,np.mean(np.real(model.eps))),optimize_eps_inf=False)model=fit_non_passive(model)model=model.enforce_passivity()returnmodel
[docs]deffit(self,min_num_poles:PositiveInt=1,max_num_poles:PositiveInt=DEFAULT_MAX_POLES,eps_inf:float=None,tolerance_rms:NonNegativeFloat=DEFAULT_TOLERANCE_RMS,advanced_param:AdvancedFastFitterParam=None,)->Tuple[PoleResidue,float]:"""Fit data using a fast fitting algorithm. Note ---- The algorithm is described in:: B. Gustavsen and A. Semlyen, "Rational approximation of frequency domain responses by vector fitting," IEEE Trans. Power. Deliv. 14, 3 (1999). B. Gustavsen, "Improving the pole relocation properties of vector fitting," IEEE Trans. Power Deliv. 21, 3 (2006). B. Gustavsen, "Enforcing Passivity for Admittance Matrices Approximated by Rational Functions," IEEE Trans. Power Syst. 16, 1 (2001). Note ---- The fit is performed after weighting the real and imaginary parts, so the RMS error is also weighted accordingly. By default, the weights are chosen based on typical values of the data. To change this behavior, use 'AdvancedFastFitterParam.weights'. Parameters ---------- min_num_poles: PositiveInt, optional Minimum number of poles in the model. max_num_poles: PositiveInt, optional Maximum number of poles in the model. eps_inf : float, optional Value of eps_inf to use in fit. If None, then eps_inf is also fit. Note: fitting eps_inf is not guaranteed to yield a global optimum, so the result may occasionally be better with a fixed value of eps_inf. tolerance_rms : float, optional Weighted RMS error below which the fit is successful and the result is returned. advanced_param : :class:`AdvancedFastFitterParam`, optional Advanced parameters for fitting. Returns ------- Tuple[:class:`.PoleResidue`, float] Best fitting result: (dispersive medium, weighted RMS error). """ifmax_num_poles<min_num_poles:raiseValidationError("Dispersion fitter cannot have 'max_num_poles' less than 'min_num_poles'.")omega=PoleResidue.angular_freq_to_eV(PoleResidue.Hz_to_angular_freq(self.freqs[::-1]))eps=self.eps_data[::-1]init_model=FastFitterData.initialize(omega,eps,eps_inf,advanced_paramorAdvancedFastFitterParam())log.info(f"Fitting weights=({init_model.weights[0]:.3g}, "f"{init_model.weights[1]:.3g}).")defmake_configs():configs=[[p]forpinrange(max(min_num_poles//2,1),max_num_poles+1)]forsettingin[init_model.relaxed,init_model.smooth,init_model.logspacing,init_model.optimize_eps_inf,]:ifsettingisNone:configs=[c+[r]forcinconfigsforrin[True,False]]else:configs=[c+[r]forcinconfigsforrin[setting]]returnconfigsbest_model=init_modelwarned_about_passivity_num_iters=Falsewarned_about_slsqp_constraint_scale=Falseconfigs=make_configs()withProgress(console=get_logging_console())asprogress:task=progress.add_task(f"Fitting to weighted RMS of {tolerance_rms}...",total=len(configs),visible=init_model.show_progress,)whilenotprogress.finished:# try different initial pole configurationsfornum_poles,relaxed,smooth,logspacing,optimize_eps_infinconfigs:model=init_model.updated_copy(num_poles=num_poles,relaxed=relaxed,smooth=smooth,logspacing=logspacing,optimize_eps_inf=optimize_eps_inf,)model=self._fit_fixed_parameters((min_num_poles,max_num_poles),model)ifmodel.rms_error<best_model.rms_error:log.debug(f"Fitter: possible improved fit with "f"rms_error={model.rms_error:.3g} found using "f"relaxed={model.relaxed}, "f"smooth={model.smooth}, "f"logspacing={model.logspacing}, "f"optimize_eps_inf={model.optimize_eps_inf}, "f"loss_in_bounds={model.loss_in_bounds}, "f"passivity_optimized={model.passivity_optimized}, "f"sellmeier_passivity={model.sellmeier_passivity}.")ifmodel.loss_in_boundsandmodel.sellmeier_passivity:best_model=modelelse:if(notwarned_about_passivity_num_itersandmodel.passivity_num_iters_too_small):warned_about_passivity_num_iters=Truelog.warning("Did not finish enforcing passivity in dispersion fitter. ""If the fit is not good enough, consider increasing ""'AdvancedFastFitterParam.passivity_num_iters'.")if(notwarned_about_slsqp_constraint_scaleandmodel.slsqp_constraint_scale_too_small):warned_about_slsqp_constraint_scale=Truelog.warning("SLSQP constraint scale may be too small. ""If the fit is not good enough, consider increasing ""'AdvancedFastFitterParam.slsqp_constraint_scale'.")progress.update(task,advance=1,description=f"Best weighted RMS error so far: {best_model.rms_error:.3g}",refresh=True,)# if below tolerance, returnifbest_model.rms_error<tolerance_rms:progress.update(task,completed=len(configs),description=f"Best weighted RMS error: {best_model.rms_error:.3g}",refresh=True,)log.info("Found optimal fit with weighted RMS error %.3g",best_model.rms_error,)ifbest_model.show_unweighted_rms:log.info("Unweighted RMS error %.3g",best_model.unweighted_rms_error,)returnbest_model.pole_residue,best_model.rms_error# if exited loop, did not reach tolerance (warn)progress.update(task,completed=len(configs),description=f"Best weighted RMS error: {best_model.rms_error:.3g}",refresh=True,)log.warning("Unable to fit with weighted RMS error under 'tolerance_rms' of %.3g",tolerance_rms)log.info("Returning best fit with weighted RMS error %.3g",best_model.rms_error)ifbest_model.show_unweighted_rms:log.info("Unweighted RMS error %.3g",best_model.unweighted_rms_error,)returnbest_model.pole_residue,best_model.rms_error