@@ -423,7 +423,7 @@ def create_interactive_inputs(ad, ui_params=None, p=None,
423423 linelist = None , bad_bits = 0 , absorption = False ):
424424 data = {"x" : [], "y" : [], "meta" : []}
425425 for ext in ad :
426- input_data , fit1d , _ = get_automated_fit (
426+ input_data , fit1d = get_automated_fit (
427427 ext , ui_params , p = p , linelist = linelist , bad_bits = bad_bits ,
428428 absorption = absorption )
429429 # peak locations and line wavelengths of matched peaks/lines
@@ -486,8 +486,6 @@ class holding parameters for the UI, passed from the primitive's Config
486486 fit1d : a fit_1D object
487487 containing the wavelength solution, plus an "image" attribute that
488488 lists the matched arc line wavelengths
489- acceptable_fit : bool
490- whether this fit is likely to be good
491489 """
492490 input_data = get_all_input_data (
493491 ext , p , ui_params .toDict (), linelist = linelist , bad_bits = bad_bits ,
@@ -499,14 +497,14 @@ class holding parameters for the UI, passed from the primitive's Config
499497 dw = np .diff (init_models [0 ](np .arange (spectrum .size ))).mean ()
500498 kdsigma = fwidth * abs (dw )
501499 k = 1 if kdsigma < 3 else 2
502- fit1d , acceptable_fit = find_solution (
500+ fit1d = find_solution (
503501 init_models , ui_params .toDict (), peaks = peaks ,
504502 peak_weights = weights [ui_params .weighting ],
505503 linelist = input_data ["linelist" ], fwidth = fwidth , kdsigma = kdsigma , k = k ,
506504 bounds_setter = input_data ["bounds_setter" ], filename = ext .filename )
507505
508506 input_data ["fit" ] = fit1d
509- return input_data , fit1d , acceptable_fit
507+ return input_data , fit1d
510508
511509
512510def create_chebyshev (waves , central_wavelength = None , dispersion = None ,
@@ -773,9 +771,9 @@ def find_solution(init_models, config, peaks=None, peak_weights=None,
773771
774772 Returns
775773 -------
776- length-2 tuple
777- A tuple of the best-fit model and a boolean denoting whether or not it
778- is an acceptable fit.
774+ fit_1D:
775+ the best-fit model or the original model, of no acceptable fit is
776+ found. The "image" attribute will be empty if no fit is found .
779777 """
780778 log = logutils .get_logger (__name__ )
781779 min_lines = [int (x ) for x in str (config ["debug_min_lines" ]).split (',' )]
@@ -805,57 +803,91 @@ def find_solution(init_models, config, peaks=None, peak_weights=None,
805803 min_lines_per_fit = min_lines_per_fit ,
806804 k = k , bounds_setter = bounds_setter )
807805
806+ wavemin , wavemax = sorted (model (model .domain ))
807+ nlines_expected = sum (wavemin <= wline <= wavemax for wline in arc_lines )
808+
808809 # We perform a regular least-squares fit to all the matches
809810 # we've made. This allows a high polynomial order to be
810811 # used without the risk of it going off the rails
811812 matched = np .where (matches > - 1 )[0 ]
812813 fit_it = fitting .LinearLSQFitter ()
813- if len (matched ) > 1 : # need at least 2 lines, right?
814- m_init = models .Chebyshev1D (degree = min (config ["order" ], len (matched )- 1 ),
815- domain = domain )
814+
815+ # We allow this continue if we only matched 1 line but only 1 line is
816+ # expected
817+ if len (matched ) > 1 or (len (matched ) * nlines_expected == 1 and len (peaks ) <= 2 ):
818+ m_init = models .Chebyshev1D (
819+ degree = min (config ["order" ], (len (matched ) + 1 ) // 2 ),
820+ domain = domain )
816821 for p , v in zip (model .param_names , model .parameters ):
817822 if p in m_init .param_names :
818823 setattr (m_init , p , v )
819- #bounds_setter(m_init)
824+ model_bounds = bounds_setter (m_init )
825+ if len (matched ) == 1 :
826+ m_init .c1 .fixed = True
820827 #for i in range(len(matched), m_init.degree + 1):
821828 # m_init.fixed[f"c{i}"] = True
822829 matched_peaks = peaks [matched ]
823830 matched_arc_lines = arc_lines [matches [matched ]]
824831 m_final = fit_it (m_init , matched_peaks , matched_arc_lines )
832+ dw = abs (np .diff (m_final (m_final .domain ))[0 ] / np .diff (m_final .domain )[0 ])
825833 #for p, l in zip(matched_peaks, matched_arc_lines):
826834 # print(f"{p:.2f} => {l:.2f}")
827835
828- # We're close to the correct solution, perform a KDFit
829- m_init = models .Chebyshev1D (degree = config ["order" ], domain = domain )
830- for p , v in zip (m_final .param_names , m_final .parameters ):
831- setattr (m_init , p , v )
832- dw = abs (np .diff (m_final (m_final .domain ))[0 ] / np .diff (m_final .domain )[0 ])
833- fit_it = matching .KDTreeFitter (sigma = 2 * abs (dw ), maxsig = 5 ,
834- k = k , method = 'Nelder-Mead' )
835- m_final = fit_it (m_init , peaks , arc_lines , in_weights = peak_weights ,
836- ref_weights = arc_weights )
837- logit (f'{ repr (m_final )} { fit_it .statistic } ' )
838-
839- # And then recalculate the matches
840- match_radius = 4 * fwidth * abs (m_final .c1 ) / len_data # 2*fwidth pixels
841- try :
842- matched = matching .match_sources (m_final (peaks ), arc_lines ,
843- radius = match_radius )
844- incoords , outcoords = zip (* [(peaks [i ], arc_lines [m ])
845- for i , m in enumerate (matched ) if m > - 1 ])
846- # Probably incoords and outcoords as defined here should go to
847- # the interactive fitter, but cull to derive the "best" model
848- fit1d = fit_1D (outcoords , points = incoords , function = "chebyshev" ,
849- order = min (m_final .degree , len (incoords )- 1 ),
850- domain = m_final .domain ,
851- niter = config ["niter" ], sigma_lower = config ["lsigma" ],
852- sigma_upper = config ["hsigma" ])
853- fit1d .image = np .asarray (outcoords )
854- except ValueError :
855- log .warning ("Line-matching failed" )
856- continue
857- nmatched = np .sum (~ fit1d .mask )
858- logit (f"{ filename } { repr (fit1d .model )} { nmatched } { fit1d .rms } " )
836+ if len (matched ) > 1 :
837+ # We're close to the correct solution, perform a KDFit
838+ m_init = models .Chebyshev1D (m_final .degree , domain = domain )
839+ for p , v in zip (m_final .param_names , m_final .parameters ):
840+ setattr (m_init , p , v )
841+ fit_it = matching .KDTreeFitter (sigma = 2 * abs (dw ), maxsig = 5 ,
842+ k = k , method = 'Nelder-Mead' )
843+ m_final = fit_it (m_init , peaks , arc_lines , in_weights = peak_weights ,
844+ ref_weights = arc_weights )
845+ logit (f'{ repr (m_final )} { fit_it .statistic } ' )
846+
847+ # And then recalculate the matches
848+ match_radius = 4 * fwidth * abs (m_final .c1 ) / len_data # 2*fwidth pixels
849+ try :
850+ matched = matching .match_sources (m_final (peaks ), arc_lines ,
851+ radius = match_radius )
852+ incoords , outcoords = zip (* [(peaks [i ], arc_lines [m ])
853+ for i , m in enumerate (matched ) if m > - 1 ])
854+ # Probably incoords and outcoords as defined here should go to
855+ # the interactive fitter, but cull to derive the "best" model
856+ fit1d = fit_1D (outcoords , points = incoords , function = "chebyshev" ,
857+ order = min (m_final .degree , (len (incoords ) + 1 ) // 2 ),
858+ domain = m_final .domain ,
859+ niter = config ["niter" ], sigma_lower = config ["lsigma" ],
860+ sigma_upper = config ["hsigma" ])
861+ fit1d .image = np .asarray (outcoords )
862+ except ValueError :
863+ log .warning ("Line-matching failed" )
864+ continue
865+ nmatched = np .sum (~ fit1d .mask )
866+ logit (f"{ filename } { repr (fit1d .model )} { nmatched } { fit1d .rms } " )
867+
868+ # A posteriori check that the fit is within the bounds
869+ # (LinearLSQFitter does not allow bounds so we can't force
870+ # the model to be bounded). We allow the tolerances to be twice
871+ # as large as they were during the piecewise fit, as this seems
872+ # to work well empirically.
873+ try :
874+ for p , v in zip (fit1d .model .param_names , fit1d .model .parameters ):
875+ bounds = model_bounds .get (p , (- np .inf , np .inf ))
876+ new_bounds = (np .asarray (bounds ) +
877+ np .array ([- 0.5 , 0.5 ]) * np .diff (bounds )[0 ])
878+ assert new_bounds [0 ] <= v <= new_bounds [1 ]
879+ except AssertionError :
880+ for p , v in zip (fit1d .model .param_names , fit1d .model .parameters ):
881+ bounds = model_bounds .get (p , (- np .inf , np .inf ))
882+ new_bounds = (np .asarray (bounds ) +
883+ np .array ([- 0.5 , 0.5 ]) * np .diff (bounds )[0 ])
884+ print (p , v , new_bounds )
885+ continue
886+ else :
887+ # Hack a fit1D object with the model and the single match
888+ fit1d = fit_1D .create_with_model (
889+ m_final , image = matched_arc_lines , points = matched_peaks )
890+ nmatched = 1
859891
860892 # Wavelength solution models need to be monotonic. Make that check.
861893 waves = fit1d .evaluate (np .arange (len_data ))
@@ -870,7 +902,7 @@ def find_solution(init_models, config, peaks=None, peak_weights=None,
870902 # Trial and error suggests this criterion works well
871903 if fit1d .rms < 0.8 / config ["order" ] * fwidth * abs (dw ) and nmatched >= min_matches_required :
872904 #print("RETURNING", fit1d.model.parameters)
873- return fit1d , True
905+ return fit1d
874906
875907 # This seems to be a reasonably ranking for poor models
876908 if nmatched > config ["order" ] + 1 :
@@ -883,13 +915,8 @@ def find_solution(init_models, config, peaks=None, peak_weights=None,
883915
884916 if best_fit1d is None :
885917 # Hack a fit1D object that represents the original model with no fitted lines
886- best_fit1d = fit_1D (np .arange (5 ), function = "chebyshev" , order = 1 ,
887- niter = 0 )
888- best_fit1d ._models = init_models [0 ]
889- best_fit1d .image = np .array ([])
890- best_fit1d .points = np .array ([])
891- best_fit1d .mask = np .array ([], dtype = bool )
892- return best_fit1d , True
918+ best_fit1d = fit_1D .create_with_model (init_models [0 ], [], [])
919+ return best_fit1d
893920
894921
895922def perform_piecewise_fit (model , peaks , arc_lines , pixel_start , kdsigma ,
@@ -1168,6 +1195,8 @@ def update_wcs_with_solution(ext, fit1d, input_data, config):
11681195 if len (incoords ) > 1 :
11691196 inv_rms = np .std (m_inverse (m_final (incoords )) - incoords )
11701197 log .stdinfo (f"Inverse model has rms = { inv_rms :.3f} pixels." )
1198+ else :
1199+ inv_rms = np .nan
11711200 m_final .name = "WAVE" # always WAVE, never AWAV
11721201 m_final .inverse = m_inverse
11731202
@@ -1182,8 +1211,8 @@ def update_wcs_with_solution(ext, fit1d, input_data, config):
11821211 # while I refactor tests
11831212 temptable .add_columns ([[1 ], [m_final .degree ], [domain [0 ]], [domain [1 ]]],
11841213 names = ("ndim" , "degree" , "domain_start" , "domain_end" ))
1185- temptable .add_columns ([[rms ], [input_data ["fwidth" ]]],
1186- names = ("rms" , "fwidth" ))
1214+ temptable .add_columns ([[rms ], [inv_rms ], [ input_data ["fwidth" ]]],
1215+ names = ("rms" , "inv_rms" , " fwidth" ))
11871216 if ext .data .ndim > 1 :
11881217 # TODO: Need to update this from the interactive tool's values
11891218 direction , location = input_data ["location" ].split ()
@@ -1276,14 +1305,15 @@ def create_pdf_plot(input_data, peaks, arc_lines, title="",
12761305 data = input_data ["spectrum" ]
12771306 spacing = 0.01
12781307 vert_align = "bottom"
1279- xmin , xmax = input_data ["init_models" ][0 ].domain
1308+ init_model = input_data ["init_models" ][0 ]
1309+ xmin , xmax = init_model .domain
12801310 pixels = np .arange (xmin , xmax + 1 )
12811311 data_max = data .max ()
12821312 spacing *= data_max
12831313 fig , ax = plt .subplots ()
12841314 ax .plot (pixels , data , 'b-' )
12851315 ax .set_ylim (0 , data_max * 1.1 )
1286- if len (arc_lines ) and np . diff ( arc_lines )[ 0 ] / np . diff ( peaks )[ 0 ] < 0 :
1316+ if len (arc_lines ) and init_model ( xmin ) > init_model ( xmax ) :
12871317 ax .set_xlim (xmax + 1 , xmin - 1 )
12881318 else :
12891319 ax .set_xlim (xmin - 1 , xmax + 1 )
0 commit comments