; docformat = 'rst'

;+
;
; Class to manage a fitted spectrum.
;
; Updated versions of the NASA Ames PAH IR Spectroscopic Database and
; more information can be found at: `www.astrochemistry.org/pahdb <https://www.astrochemistry.org/pahdb>`.
;
; :Examples:
;   Create and destroy an
;   AmesPAHdbIDLSuite_Fitted_Spectrum-instance::
;
;     IDL> fitted = OBJ_NEW('AmesPAHdbIDLSuite_Fitted_Spectrum')
;     IDL> fitted->Set,data
;     IDL> fitted->Plot
;     IDL> OBJ_DESTORY,fitted
;
; :Author:
;   Dr. Christiaan Boersma
;
; :Copyright:
;   BSD licensed
;
; :History:
;   Changes::
;
;     08-26-2025
;     Correct spelling and indentation in GETSIZEDISTRIBUTION. Christiaan
;     Boersma.
;     11-27-2024
;     Medium/large PAH size cutoff set to 70 to align with Python counterpart.
;     Christiaan Boersma.
;     09-19-2024
;     Use trapezium integration in favor of INT_TABULATED. Christiaan Boersma.
;     09-10-2024
;     Added GETAVERAGENUMBEROFCARBONATOMS. Christiaan Boersma.
;     12-13-2023
;     Align TOLARANCE AND ITERATIONS in DESCRIPTION. Christiaan Boersma.
;     11-22-2023
;     Add tolerance and iterations properties and their handling in SET, GET,
;     and DESCRIPTION and add GETTOLERANCE and GETITERATIONs. Christiaan
;     Boersma.
;     11-17-2023
;     Add MEDIUM size bin to GETBREAKDOWN, GETCLASSES, and PLOT. Christiaan
;     Boersma.
;     10-04-2023
;     Allow setting the SMALL threshold in PLOT through keyword-inheritance.
;     Christiaan Boersma.
;     06-02-2023
;     Accommodate UIDs >9999 in PLOT and use Courier font when presenting
;     contributing UIDs. Christiaan Boersma.
;     11-23-2022
;     Add missing OPLOT keyword to PLOT signature. Christiaan
;     Boersma.
;     06-14-2022
;     Use lazy instantiation for getters and keep track of function
;     signatures. Christiaan Boersma.
;     05-03-2022
;     Align integration ranges with Python code in GETERROR.
;     Christiaan Boersma
;     04-28-2022
;     Return piecewise errors in GETERROR and handle piecewise.
;     errors in DESCRIPTION. Christiaan Boersma.
;     04-27-2022
;     Refactored plotting of residual in PLOT and corrected logic
;     flow in SET. Christiaan Boersma.
;     06-23-2021
;     Get sorting right for GETERROR, added error from GETERROR
;     to DESCRIPTION, and call Plot::Init. Christiaan Boersma.
;     04-30-2021
;     Added GETERROR. Christiaan Boersma.
;     03-31-2021
;     Intersect now calls Spectrum::Intersect instead of
;     Data::Intersect. Christiaan Boersma.
;     02-17-2021
;     Overload Intersect to ensure weights are also
;     intersected. Christiaan Boersma.
;     09-04-2020
;     Remove extra comma labels when plotting structures PLOT.
;     Christiaan Boersma.
;     02-10-2020
;     Make sure to remove continuum when calculating norm and
;     chi-squared. Christiaan Boersma.
;     01-25-2019
;     Now writing files as IPAC-tables. Christiaan Boersma.
;     01-23-2017
;     Handle UIDs larger than 999 in PLOT. Christiaan Boersma.
;     04-26-2016
;     Updated GETSIZEDISTRIBUTION to use Rice Rule for determining the
;     number of bins. Christiaan Boersma.
;     04-11-2016
;     Added method GETSIZEDISTRIBUTION. Added SIZEDISTRIBUTION-keyword
;     to PLOT. Christiaan Boersma.
;     03-29-2016
;     Fixed dot-notation for class access in DESCRIPTION. Christiaan Boersma.
;     07-12-2015
;     Added GETMETHOD and its infrastructure. Christiaan Boersma.
;     05-20-2015
;     Fixed missing Struct in SET. Christiaan Boersma.
;     05-03-2015
;     Updated formatting for DESCRIPTION. Christiaan Boersma.
;     04-24-2015
;     Refactored GETRESIDUAL and GETBREAKDOWN to use GETFIT and other
;     small fixes. Christiaan Boersma.
;     02-01-2015
;     First version of the file. Christiaan Boersma.
;-

;+
;  Output spectrum description.
;
;  :Params:
;    Str: out, optional, type="string array"
;      Ouput to Str
;
; :Categories:
;   INFORMATIVE
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::Description,Str

  COMPILE_OPT IDL2

  ON_ERROR,2

  self->AmesPAHdbIDLSuite_Spectrum::Description,Str

  err = self->getError()

  Str = [Str, $
         STRING(FORMAT='(A-12,":",X,A0)', "fit", self->getMethod()), $
         STRING(FORMAT='(A-12,":",X,g-8.3)', "|_tolerance", self->getTolerance()), $
         STRING(FORMAT='(A-12,":",X,I0)', "|_iterations", self->getIterations()), $
         STRING(FORMAT='(A-12,":",X,g-8.3)', "|_norm", self->getNorm()), $
         STRING(FORMAT='(A-12,":",X,g-8.3)', "|_chisquared", self->getChiSquared()), $
         STRING(FORMAT='(A-12,":",X,g-8.3)', "|_error", err.(0))]

  tags = STRLOWCASE(TAG_NAMES(err))

  ntags = N_TAGS(err)
  FOR i = 1L, ntags - 1L DO $
    IF err.(i) GE 0.0D THEN $
      Str = [Str, $
             STRING(FORMAT='(A-12,":",X,g-8.3)', "|_" + tags[i], err.(i))]

  Str = STRJOIN(Str, "!C")

  IF N_PARAMS() GT 0 THEN RETURN

  PRINT,STRJOIN(STRSPLIT(Str, "!C", /EXTRACT, /REGEX), STRING(10B))
END

;+
;  Plot the fitted spectrum.
;
;  :Keywords:
;    DistributionSize: in, optional, type=int
;      Toggle to plot the PAH size distribution
;    Residual: in, optional, type=int
;      Toggle to plot the residuals of the fit
;    Size: in, optional, type=int
;      Toggle to plot the size breakdown of the fit
;    Charge: in, optional, type=int
;      Toggle to plot the charge breakdown of the fit
;    Composition: in, optional, type=int
;      Toggle to plot the compositional breakdown of the fit
;    Structures: in, optional, type=int
;      Toggle to plot the chemical structures of the PAHs contributing
;      to the fit
;    Wavelength: in, optional, type=int
;      Whether to set the abscissa units to wavelength
;    Stick: in, optional, type=int
;      Whether to plot the spectrum as sticks
;    Fill: in, optional, type=int
;       Whether to solid-fill the spectrum
;    Legend: in, optional, type=int
;      Whether to show a legend
;    Color: in, optional, type=int
;      Color to plot the spectrum with
;    _EXTRA: in, optional, type=struct
;      Required for IDL's keyword-inheritance mechanism
;
; :Categories:
;   PLOTTING
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::Plot,DistributionSize=DistributionSize,Residual=Residual,Size=Size,Charge=Charge,Composition=Composition,Structures=Structures,Wavelength=Wavelength,Stick=Stick,Fill=Fill,Legend=Legend,Color=Color,Oplot=Oplot,_EXTRA=EXTRA

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF KEYWORD_SET(Structures) THEN BEGIN

     TVLCT,ch1,ch2,ch3,/GET

     LOADCT,0,/SILENT

     idx = ULINDGEN((*self.database).sizes.ngeometries, self.nuids)

     sel =  WHERE((*self.database).data.geometries[idx MOD (*self.database).sizes.ngeometries].uid EQ (*self.uids)[idx / (*self.database).sizes.ngeometries]) MOD (*self.database).sizes.ngeometries

     g = OBJ_NEW('AmesPAHdbIDLSuite_Geometry', Data=(*self.database).data.geometries[sel], $
                                               UIDs=*self.uids)

     g->Diagonalize

     idx = ULINDGEN((*self.database).sizes.nspecies, self.nuids)

     sel = WHERE((*self.database).data.species[idx MOD (*self.database).sizes.nspecies].uid EQ (*self.uids)[idx / (*self.database).sizes.nspecies]) MOD (*self.database).sizes.nspecies

     s = OBJ_NEW('AmesPAHdbIDLSuite_Species', Data=(*self.database).data.species[sel], $
                                              UIDs=*self.uids)

     s->formatFormulae

     f = (s->get()).data.formula

     OBJ_DESTROY,[s]

     a_perc = 100D * (*self.weights).weight / TOTAL((*self.weights).weight)

     f_perc = DBLARR(self.nuids, /NOZERO)

     dx = (*self.grid)[1:*] - *self.grid

     FOR i = 0L, self.nuids - 1 DO BEGIN

        sel = WHERE((*self.data).uid EQ (*self.uids)[i])

        intensity = (*self.data)[sel].intensity

        yavg = (intensity[0:-2] + intensity[1:*]) / 2D

        f_perc[i] = TOTAL(dx * yavg)
     ENDFOR

     f_perc *= 100D / TOTAL(f_perc)

     PLOT,[0,1],[0,1],XRANGE=[0,1],YRANGE=[0,1],XSTYLE=5,YSTYLE=5,/NODATA,POSITION=[0,0,1,1]

     IF !D.NAME EQ 'X' THEN BEGIN

        resolution = 128 * [1, 1]

        nx = !D.X_SIZE / resolution[0]
     ENDIF ELSE BEGIN

        resolution = 300 * [1, 1]

        nx = FLOOR(!D.X_SIZE / (!D.X_PX_CM * 2.54))
     ENDELSE

     FOR i = 0, self.nuids - 1 DO BEGIN

        img = g->Structure((*self.uids)[i], Background=[255, 255, 255], Resolution=resolution)

        IF !D.NAME EQ 'X' THEN BEGIN

           x = resolution[0] * (i MOD nx) & y = !D.Y_SIZE - resolution[1] * (1 + (i / nx))

           TV,img,x,y,/DEVICE,/TRUE

           XYOUTS,x+resolution/2,y+0.15*resolution[1],STRING(FORMAT='(A0,X,"(a:",F4.1,"%;f:",F4.1,"%)")',f[i],a_perc[i], f_perc[i]),ALIGNMENT=0.5,/DEVICE
        ENDIF ELSE BEGIN

           x = !D.X_PX_CM * 2.54 * (i MOD nx) & y = !D.Y_SIZE - (!D.Y_PX_CM * 2.54 * (1 + (i / nx)))

           TV,img,x,y,XSIZE=!D.X_PX_CM*2.54,YSIZE=!D.Y_PX_CM*2.54,/TRUE,/DEVICE

           XYOUTS,x+1.27*!D.X_PX_CM,y+!D.Y_PX_CM*0.38,STRING(FORMAT='(A0,"(a:",F4.1,"%;f:",F4.1,"%)")',f[i],a_perc[i],f_perc[i]),ALIGNMENT=0.5,/DEVICE,CHARSIZE=0.5
        ENDELSE
     ENDFOR

     OBJ_DESTROY,[g]

     TVLCT,ch1,ch2,ch3

     RETURN
  ENDIF

  self->AmesPAHdbIDLSuite_Plot::Setup,Oplot=Oplot,XSIZE=600,YSIZE=400

  IF KEYWORD_SET(DistributionSize) THEN GOTO,SIZEDISTRIBUTION

  x = *self.grid

  nx = N_ELEMENTS(x)

  xunits = self.units.abscissa.str

  xrange = [MAX(x, MIN=xmin), xmin]

  obs_xstdev = (*self.observation).data.xstdev

  IF KEYWORD_SET(Wavelength) THEN BEGIN

     obs_xstdev = 1D4 * obs_xstdev / x^2

     x = 1D4 / x

     xrange = [MIN(x, MAX=xmax), xmax]

     xunits = 'wavelength [!Mm!Xm]'
  ENDIF

  yunits = (*self.observation).units.ordinate.str

  IF yunits NE '' THEN yunits += '!C'

  yunits += self.units.ordinate.str

  fit = self->getFit()

  IF NOT KEYWORD_SET(Oplot) THEN BEGIN

     IF KEYWORD_SET(Residual) THEN self->AmesPAHdbIDLSuite_Plot::Plot,[0,1],[0,1],COLOR=Color,XRANGE=[MIN(x - obs_xstdev), MAX(x + obs_xstdev)],YRANGE=[MIN([(*self.observation).data.y - (*self.observation).data.ystdev, (*self.observation).data.continuum + fit]), MAX([(*self.observation).data.y + (*self.observation).data.ystdev, (*self.observation).data.continuum + fit])],XTICKFORMAT='(A1)',YTITLE=yunits,POSITION=[0.2,0.45,0.95,0.9],/NoData,_EXTRA=EXTRA $
     ELSE IF KEYWORD_SET(Size) OR KEYWORD_SET(Charge) OR KEYWORD_SET(COMPOSITION) THEN self->AmesPAHdbIDLSuite_Plot::Plot,[0,1],[0,1],COLOR=Color,XRANGE=[MIN(x - obs_xstdev), MAX(x + obs_xstdev)],YRANGE=[MIN([(*self.observation).data.y - (*self.observation).data.ystdev, (*self.observation).data.continuum + fit]), MAX([(*self.observation).data.y + (*self.observation).data.ystdev, (*self.observation).data.continuum + fit])],XTITLE=xunits,YTITLE=yunits,POSITION=[0.2,0.2,0.95,0.9],/NoData,_EXTRA=EXTRA $
     ELSE self->AmesPAHdbIDLSuite_Plot::Plot,[0,1],[0,1],COLOR=Color,XRANGE=[MIN(x - obs_xstdev), MAX(x + obs_xstdev)],YRANGE=[MIN([(*self.observation).data.y - (*self.observation).data.ystdev, (*self.observation).data.continuum + fit]), MAX([(*self.observation).data.y + (*self.observation).data.ystdev, (*self.observation).data.continuum + fit])],XTITLE=xunits,YTITLE=yunits,POSITION=[0.4,0.2,0.95,0.9],/NoData,_EXTRA=EXTRA
  ENDIF

  self->AmesPAHdbIDLSuite_Plot::OplotError,x,(*self.observation).data.y,(*self.observation).data.ystdev,obs_xstdev,COLOR=14

  self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.y,LINESTYLE=5,COLOR=14

  self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum,LINESTYLE=2

  self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+fit

  IF NOT KEYWORD_SET(Color) THEN Color = 2

  IF KEYWORD_SET(Size) OR KEYWORD_SET(Charge) OR KEYWORD_SET(COMPOSITION) THEN BEGIN

     classes = self->getClasses(_EXTRA=EXTRA)

     IF KEYWORD_SET(Size) THEN BEGIN

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.small,Stick=Stick,Fill=Fill,COLOR=Color

        XYOUTS,0.25,0.75,'small',COLOR=Color,/Normal

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.medium,Stick=Stick,Fill=Fill,COLOR=Color+1

        XYOUTS,0.25,0.70,'medium',COLOR=Color+1,/Normal

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.large,Stick=Stick,Fill=Fill,COLOR=Color+2

        XYOUTS,0.25,0.65,'large',COLOR=Color+2,/NORMAL
     ENDIF ELSE IF KEYWORD_SET(Charge) THEN BEGIN

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.anion,Stick=Stick,Fill=Fill,COLOR=Color

        XYOUTS,0.25,0.75,'anion',COLOR=Color,/Normal

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.neutral,Stick=Stick,Fill=Fill,COLOR=Color+1

        XYOUTS,0.25,0.70,'neutral',COLOR=Color+1,/Normal

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.cation,Stick=Stick,Fill=Fill,COLOR=Color+2

        XYOUTS,0.25,0.65,'cation',COLOR=Color+2,/Normal
     ENDIF ELSE BEGIN

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.pure,Stick=Stick,Fill=Fill,COLOR=Color

        XYOUTS,0.25,0.75,'pure',COLOR=Color,/Normal

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+classes.nitrogen,Stick=Stick,Fill=Fill,COLOR=Color+1

        XYOUTS,0.25,0.70,'nitrogen',COLOR=Color+1,/Normal
     ENDELSE
  ENDIF ELSE BEGIN

     FOR i = 0L, self.nuids - 1 DO BEGIN

        select = WHERE((*self.data).uid EQ (*self.uids)[i])

        self->AmesPAHdbIDLSuite_Plot::Oplot,x,(*self.observation).data.continuum+(*self.data)[select].intensity,Stick=Stick,Fill=Fill,COLOR=Color+i
     ENDFOR
  ENDELSE

  IF SIZE(Legend, /TYPE) EQ 0 THEN Legend = 1

  IF Legend THEN BEGIN

     self->Description,outs

     self->AmesPAHdbIDLSuite_Plot::Legend,outs
  ENDIF

  IF KEYWORD_SET(Residual) THEN BEGIN

     y = -100D * self->getResidual() / (*self.observation).data.y

     ystdev = 100 * (*self.observation).data.ystdev / (*self.observation).data.y

     self->AmesPAHdbIDLSuite_Plot::Plot,x,y,XRANGE=[MIN(x - obs_xstdev), MAX(x + obs_xstdev)],YRANGE=[MIN(y - ystdev), MAX(y + ystdev)],XTITLE=xunits,YTITLE='residual!C[%]',POSITION=[0.2,0.2,0.95,0.45],/NoData,/NOERASE,_EXTRA=EXTRA

     self->AmesPAHdbIDLSuite_Plot::OplotError,x,0*x,ystdev,obs_xstdev,Color=14

     self->AmesPAHdbIDLSuite_Plot::Oplot,!X.CRANGE,[0,0],Color=14,LINESTYLE=5

     self->AmesPAHdbIDLSuite_Plot::Oplot,x,y,Stick=Stick,Fill=Fill,COLOR=Color
  ENDIF ELSE IF NOT (KEYWORD_SET(Size) OR KEYWORD_SET(Charge) OR KEYWORD_SET(COMPOSITION)) THEN BEGIN

     PLOT,[0,1],[0,1],/NODATA,POSITION=[0.05,0.2,0.25,0.9],XTICKS=1,YTICKS=1,XMINOR=1,YMINOR=1,XTICKFORMAT='(A1)',YTICKFORMAT='(A1)',/NOERASE

     !P.CHARSIZE = 1.25

     XYOUTS,0.055,0.87,STRING(FORMAT='("!11",A-5,4X,A-8,"!3")', 'UID', 'WEIGHT'),/NORMAL

     i = 0

     ypos = 0.87 - 0.03

     WHILE i LT self.nuids AND ypos GT 0.225 DO BEGIN

        XYOUTS,0.055,ypos,STRING(FORMAT='("!11",I5,4X,g-8.3,"!3")', (*self.weights)[i].uid, (*self.weights)[i].weight),COLOR=Color+i++,/NORMAL

        ypos -= 0.025
     ENDWHILE

     IF i NE self.nuids THEN XYOUTS,0.055,ypos,STRING(FORMAT='("!11+",I0,X,"more...!3")', self.nuids - i),/NORMAL
  ENDIF

  GOTO,FINISH

SIZEDISTRIBUTION:

  sd = self->getSizeDistribution()

  sd.distribution = 100 * sd.distribution / TOTAL(sd.distribution)

  nbins = N_ELEMENTS(sd.size) - 1

  PLOT,[0,1],[0,1],XRANGE=[sd.size[0], sd.size[nbins]],YRANGE=[0, MAX(sd.distribution)],XTITLE='n!LCarbon!N',YTITLE='frequency [%]',POSITION=[0.2,0.2,0.95,0.9],/NODATA

  PLOTS,[sd.size[0],sd.size[0],sd.size[1]],[0,sd.distribution[0],sd.distribution[0]],COLOR=2,THICK=2

  FOR i = 1, nbins - 1 DO PLOTS,[sd.size[i], sd.size[i], sd.size[i+1]],[sd.distribution[i-1],sd.distribution[i],sd.distribution[i]],COLOR=2,THICK=2

  PLOTS,[1,1]*sd.size[nbins],[0,sd.distribution[nbins-1]],COLOR=2,THICK=2

FINISH:

  self->AmesPAHdbIDLSuite_Plot::Restore
END

;+
; Write the fitted spectrum to file as an IPAC-table.
;
; :Params:
;   Filename: in, optional, type=string
;     Output filename
;
; :Categories:
;   OUTPUT
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::Write,Filename

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF N_PARAMS() LT 1 THEN Filename = OBJ_CLASS(self) + '.tbl'

  timestamp = SYSTIME()
  hdr = []
  FXADDPAR,hdr,"DATE",timestamp," Date this file was generated"
  FXADDPAR,hdr,"ORIGIN","NASA Ames Research Center"," Organization generating this file"
  FXADDPAR,hdr,"CREATOR",STRING(FORMAT='("IDL",X,A0,X,"on",X,A0)', !VERSION.RELEASE, !VERSION.OS_NAME)," Software used to create this file"
  FXADDPAR,hdr,"SOFTWARE","AmesPAHdbIDLSuite"," Program used to create this file"
  FXADDPAR,hdr,"AUTHOR","Dr. C. Boersma"," Author of the program"
  FXADDPAR,hdr,"TYPE",OBJ_CLASS(self)," AmesPAHdbIDLSuite data type"

  self->Description,description

  comments = STRSPLIT(description, "!C", /EXTRACT, /REGEX, COUNT=ncomments)

  FOR i = 0L, ncomments - 1 DO FXADDPAR,hdr,"COMMENT",comments[i]

  abscissa = STREGEX(self.units.abscissa.str, '(.*) \[(.*)\]', /SUBEXPR, /EXTRACT)

  ordinate = STREGEX(self.units.ordinate.str, '(.*) \[(.*)\]', /SUBEXPR, /EXTRACT)

  half_abscissa_len = STRLEN(abscissa[1]) / 2
  half_ordinate_len = STRLEN(ordinate[1]) / 2

  fmt1 = '("|",A' + STRING(FORMAT='(I0)', 12 + half_abscissa_len) + ',' + STRING(FORMAT='(I0)', 13 - half_abscissa_len) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)', 12 + half_ordinate_len) + ',' + STRING(FORMAT='(I0)', 13 - half_ordinate_len) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)', 12 + 3) + ',' + STRING(FORMAT='(I0)', 13 - 3) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)',  6 + 3) + ',' + STRING(FORMAT='(I0)',  6 - 3) + 'X,' + $
          '"|")'

  fmt2 = '("|",A' + STRING(FORMAT='(I0)', 12 + 3) + ',' + STRING(FORMAT='(I0)', 13 - 3) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)', 12 + 3) + ',' + STRING(FORMAT='(I0)', 13 - 3) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)', 12 + 3) + ',' + STRING(FORMAT='(I0)', 13 - 3) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)',  6 + 3) + ',' + STRING(FORMAT='(I0)',  6 - 3) + 'X,' + $
          '"|")'

  half_abscissa_len = STRLEN(abscissa[2]) / 2
  half_ordinate_len = STRLEN(ordinate[2]) / 2

  fmt3 = '("|",A' + STRING(FORMAT='(I0)', 12 + half_abscissa_len) + ',' + STRING(FORMAT='(I0)', 13 - half_abscissa_len) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)', 12 + half_ordinate_len) + ',' + STRING(FORMAT='(I0)', 13 - half_ordinate_len) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)', 12 + 3) + ',' + STRING(FORMAT='(I0)', 13 - 3) + 'X,' + $
          '"|",A' + STRING(FORMAT='(I0)',  6 + 3) + ',' + STRING(FORMAT='(I0)',  6 - 3) + 'X,' + $
          '"|")'

  cols = [STRING(FORMAT=fmt1,STRUPCASE(abscissa[1]),STRUPCASE(ordinate[1]),'WEIGHT','UID'), $
          STRING(FORMAT=fmt2,"double","double","double","int"), $
          STRING(FORMAT=fmt3,abscissa[2],ordinate[2],"","")]

  n = N_ELEMENTS(*self.grid)
  intensities = REFORM((*self.data).intensity, n, self.nuids)
  srt = SORT(*self.uids)
  intensities = intensities[*, srt]

  OPENW,funit,Filename,/GET_LUN
  PRINTF,funit,FORMAT='("\",A0)',hdr[0:WHERE(STRPOS(hdr, 'END') EQ 0)]
  PRINTF,funit,STRJOIN(cols, STRING( 10B ))
  n = N_ELEMENTS(*self.grid)

  FOR i = 0L, self.nuids - 1L DO BEGIN
     FOR j = 0L, n - 1L DO PRINTF,funit,FORMAT='(X,F25.6,X,F25.6,X,F25.6,X,I)',(*self.grid)[j],intensities[j,i],(*self.weights)[i].weight,(*self.uids)[i]
  ENDFOR
  CLOSE,funit
  FREE_LUN,funit

  PRINT
  PRINT,"========================================================="
  PRINT,"    WRITTEN IPAC TABLE: ", Filename
  PRINT,"========================================================="
  PRINT
END

;+
;  Sorts the UIDs by their contribution to the fit.
;
; :Params:
;   Weights: out, optional, type="struct array"
;     Return the sorted weights
;
; :Keywords:
;   Flux: in, optional, type=int
;     Whether to sort by flux instead of abundance.
;
; :Categories:
;   SET/GET
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::Sort,Weights,Flux=Flux

  COMPILE_OPT IDL2

  ON_ERROR,2

  Weights = REPLICATE({AmesPAHdbIDLSuite_Weights_S, uid:0L, weight:0D}, self.nuids)

  IF NOT KEYWORD_SET(Flux) THEN Weights.weight = (*self.weights).weight $
  ELSE BEGIN

     dx = (*self.grid)[1:*] - *self.grid

     FOR i = 0L, self.nuids - 1 DO BEGIN

        select = WHERE((*self.data).uid EQ (*self.uids)[i])

        intensity = (*self.data)[select].intensity

        yavg = (intensity[0:-2] + intensity[1:*]) / 2.0

        Weights[i].weight = TOTAL(dx * yavg)
     ENDFOR
  ENDELSE

  Weights.uid = *self.uids

  srt = REVERSE(SORT(Weights.weight))

  Weights = weights[srt]

  *self.weights = (*self.weights)[srt]

  *self.uids = (*self.uids)[srt]
END

;+
; Retrieves the AmesPAHdbIDLSuite_Fitted_Spectrum representation in a
; structure.
;
; :Returns:
;   Structure
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::Get

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self.data) THEN RETURN, 0

  struct = self->AmesPAHdbIDLSuite_Spectrum::Get()

  struct.type = OBJ_CLASS(self)+'_S'

  RETURN,CREATE_STRUCT(struct, 'observation', *self.observation, 'weights', *self.weights, 'norm', self->getNorm(), 'chisquared', self->getChiSquared(), 'method', self.method, 'tolerance', self.tolerance, 'iterations', self.iterations)
END

;+
; Populates the AmesPAHdbIDLSuite_Fitted_Spectrum-instance.
;
; :Params:
;   Struct: in, optional, type=struct
;     Data structure
;
; :Keywords:
;   Type: in, optional, type=string
;     Type of Data
;   Version: in, optional, type=string
;    Versioning information
;   Data: in, optional, type=struct
;     Data structure
;   PAHdb: in, optional, type=pointer
;     Pointer to parsed database file
;   Uids: in, optional, type="long array (1D)"
;     UIDs in Data
;   Model: in, optional, type=string
;     References
;   Units: in, optional, type="AmesPAHdb_Units_S struct"
;     Units
;   Shift: in, optional, type=float
;     Shift
;   Grid: in, optional, type="float array"
;     Grid
;   Profile: in, optional, type=string
;     Profile
;   FWHM: in, optional, type=float
;     FWHM
;   Observation: in, optional, type="AmesPAHdb_Observation_S array"
;     Observation
;   Weights: in, optional, type="struct array"
;     Weights
;   Method: in, optional, type=string
;     Method
;   Tolerance: in, optional, type=double
;     Tolerance
;   Iterations: in, optional, type=long
;     Number of iterations
;
; :Categories:
;   SET/GET
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::Set,Struct,Type=Type,Version=Version,Data=Data,PAHdb=PAHdb,Uids=Uids,Model=Model,Units=Units,Shift=Shift,Grid=Grid,Profile=Profile,FWHM=FWHM,Observation=Observation,Weights=Weights,Method=Method,Tolerance=Tolerance,Iterations=Iterations

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF N_PARAMS() GT 0 THEN BEGIN

     tag = WHERE(TAG_NAMES(Struct) EQ 'TYPE', ntype)

     IF ntype EQ 1 THEN BEGIN

        IF Struct.(tag) EQ OBJ_CLASS(self)+'_S' THEN BEGIN

           IF NOT KEYWORD_SET(Data) THEN self.InvalidateLazy

           IF NOT KEYWORD_SET(Observation) THEN BEGIN

              IF PTR_VALID(self.observation) THEN PTR_FREE,self.observation

              self.observation = PTR_NEW(Struct.observation)
           ENDIF

           IF NOT KEYWORD_SET(Weights) THEN BEGIN

              IF PTR_VALID(self.weights) THEN PTR_FREE,self.weights

              self.weights = PTR_NEW(Struct.weights)
           ENDIF

           IF NOT KEYWORD_SET(Method) THEN self.method = Struct.method

           IF NOT KEYWORD_SET(Tolerance) THEN self.tolerance = Struct.tolerance

           IF NOT KEYWORD_SET(Iterations) THEN self.iterations = Struct.iterations
        ENDIF

        IF KEYWORD_SET(Data) THEN self.InvalidateLazy

        self->AmesPAHdbIDLSuite_Spectrum::Set,Struct,Type=Type,Version=Version,Data=Data,PAHdb=PAHdb,Uids=Uids,Model=Model,Units=Units,Shift=Shift,Grid=Grid,Profile=Profile,FWHM=FWHM
     ENDIF
  ENDIF ELSE BEGIN

     IF KEYWORD_SET(Data) THEN self.InvalidateLazy

     self->AmesPAHdbIDLSuite_Spectrum::Set,Type=Type,Version=Version,Data=Data,PAHdb=PAHdb,Uids=Uids,Model=Model,Units=Units,Shift=Shift,Grid=Grid,Profile=Profile,FWHM=FWHM
  ENDELSE

  IF KEYWORD_SET(Observation) THEN BEGIN

      IF PTR_VALID(self.observation) THEN PTR_FREE,self.observation

      self.observation = PTR_NEW(Observation)
   ENDIF

   IF KEYWORD_SET(Weights) THEN BEGIN

      IF PTR_VALID(self.weights) THEN PTR_FREE,self.weights

      self.weights = PTR_NEW(Weights)
   ENDIF

    IF KEYWORD_SET(Method) THEN self.method = Method

    IF KEYWORD_SET(Tolerance) THEN self.tolerance = Tolerance

    IF KEYWORD_SET(Iterations) THEN self.iterations = Iterations
END

;+
; Retrieves the residual of the fit.
;
; :Returns:
;   float array
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetResidual

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self._lazy.residual) THEN $
    self._lazy.residual = PTR_NEW(((*self.observation).data.y - (*self.observation).data.continuum) - self->getFit())

  RETURN,*self._lazy.residual
END

;+
; Retrieves the total fit.
;
; :Returns:
;   float array
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetFit

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self._lazy.fit) THEN $
    self._lazy.fit = PTR_NEW(TOTAL(REFORM((*self.data).intensity, N_ELEMENTS(*self.grid), self.nuids), 2))

  RETURN,*self._lazy.fit
END

;+
; Retrieves the ordinate values of the observation.
;
; :Returns:
;   float array
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetObservation

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF PTR_VALID(self.observation) THEN RETURN,*self.observation

  RETURN,0
END

;+
;  Retrieves the indices intersection of two sets.
;
; :Returns:
;   long array
;
; :Params:
;   a: in, required, type="long or long array"
;    First set
;   b: in, required, type="long or long array"
;     Second set
;   Count: out, optional, type=long
;     Number in intersection
;
; :Categories:
;   SET OPERATIONS
;
; :Private:
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum__Intersection,a,b,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  min_a = MIN(a, MAX=max_a)

  min_b = Min(b, MAX=max_b)

  min_ab = min_a > min_b

  max_ab = max_a < max_b

  IF ((max_a LT min_ab) AND (min_b GT max_ab)) OR ((max_b LT min_ab) AND (min_a GT max_ab)) THEN BEGIN

     Count = 0

     RETURN, -1
  ENDIF

  r = WHERE((HISTOGRAM([a], MIN=min_ab, MAX=max_ab, REVERSE_INDICES=r_a) NE 0) AND  $
            (HISTOGRAM([b], MIN=min_ab, MAX=max_ab, REVERSE_INDICES=r_b) NE 0), Count)

  IF Count EQ 0 THEN RETURN, -1

  RETURN, TEMPORARY(r) + min_ab
END

;+
;  Retrieves the indices of the intersection of a certain
;  property from a subset of PAHs.
;
; :Params:
;   selector: in, required, type="long array"
;     Indices of the subset to consider
;   property: in, required, type=struct
;     Property structure with a 'UID'-tag
;   Count: in, optional, type=long
;    Number of matches
;
; :Returns:
;   Structure
;
; :Categories:
;   SET Operations
;
; :Private:
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::Select,selector,property,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  intersect = AmesPAHdbIDLSuite_Fitted_Spectrum__Intersection((*self.database).data.species[selector].uid, *self.uids, Count)

  IF Count GT 0 THEN BEGIN

     nitag = N_ELEMENTS(property)

     idx = ULINDGEN(nitag, Count)

     select = WHERE(property[idx MOD nitag].uid EQ intersect[idx / nitag]) MOD nitag

     RETURN,select
  ENDIF

  Count = 0

  RETURN,0
END

;+
; Updates Data and Weigths to the Intersection with UIDs
;
; :Params:
;   UIDs: in, required, type="long or long array"
;     UIDs to consider for Difference
;   Count: out, optional, type=long
;     Number of UIDs
;
; :Categories:
;   SET OPERATIONS
;
; :Private:
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::Intersect,UIDs,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  self->AmesPAHdbIDLSuite_Spectrum::Intersect,UIDs,Count

  IF Count EQ 0 THEN RETURN

  nuids = N_ELEMENTS(UIDs)

  nweights = N_ELEMENTS(*self.weights)

  idx = ULINDGEN(nweights, nuids)

  select = WHERE((*self.weights)[idx MOD nweights].uid EQ UIDs[idx / nweights], nselect) MOD nweights

  *self.weights = (*self.weights)[select]
END

;+
; Retrieves the PAH size distribution of the fitted PAHs.
;
; :Keywords:
;   NBins: in, optional, type=int
;     Set the number of bins to use, otherwise Rice's rule is used.
;   Min: in, optional, type=float
;    Set the minimum size to consider.
;   Max: in, optional, type=float
;    Set the maximum size to consider.
;
; :Returns:
;   Structure
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetSizeDistribution,NBins=nbins,Min=min,Max=max

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self._lazy.sizedistribution) THEN BEGIN

    IF NOT PTR_VALID(self.database) THEN BEGIN
      PRINT
      PRINT,"========================================================="
      PRINT," VALID DATABASE POINTER NEEDED TO GET SIZE DISTRIBUTION  "
      PRINT,"========================================================="
      PRINT
      self.state = 0
      RETURN,0
   ENDIF

    IF NOT PTR_VALID(self.weights) THEN BEGIN
      PRINT
      PRINT,"========================================================="
      PRINT,"                  WEIGHTS ARE NOT SET                    "
      PRINT,"========================================================="
      PRINT
      self.state = 0
      RETURN,0
    ENDIF

    ndata = N_ELEMENTS((*self.database).data.species)

    idx = ULINDGEN(ndata, self.nuids)

    select = WHERE((*self.database).data.species[idx MOD ndata].uid EQ (*self.uids)[idx / ndata]) MOD ndata

    nc = (*self.database).data.species[select].nc

    IF N_ELEMENTS(Min) EQ 0 THEN min = MIN(nc)

    IF NOT KEYWORD_SET(Max) THEN max = MAX(nc)

    ; Rice Rule
    IF NOT KEYWORD_SET(NBins) THEN nbins = CEIL(2 * self.nuids^(1./3))

    size = min + (max - min) * DINDGEN(nbins + 1) / DOUBLE(nbins)

    distribution = DBLARR(nbins)

    idx = VALUE_LOCATE(size, nc)

    left = WHERE(idx LT 0, c)
    IF c GT 0 THEN idx[left] = 0
    right = WHERE(idx GE nbins, c)
    IF c GT 0 THEN idx[right] = nbins - 1

    FOR i = 0, self.nuids - 1 DO distribution[idx[i]] += (*self.weights)[i].weight

    self._lazy.sizedistribution = PTR_NEW({size:size, distribution:distribution})
  ENDIF

  RETURN,*self._lazy.sizedistribution
END

;+
; Retrieves the average number and standard deviation of carbon atoms of the
; fitted PAHs.
;
; :Returns:
;   double array
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetAverageNumberOfCarbonAtoms

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self._lazy.averagenumberofcarbonatoms) THEN BEGIN

    ndata = N_ELEMENTS((*self.database).data.species)

    idx = ULINDGEN(ndata, self.nuids)

    select = WHERE((*self.database).data.species[idx MOD ndata].uid EQ (*self.weights)[idx / ndata].uid) MOD ndata

    nc = (*self.database).data.species[select].nc

    tot = TOTAL((*self.weights).weight)

    avg = TOTAL(nc * (*self.weights).weight) / tot

    var = TOTAL((nc - avg)^2 * (*self.weights).weight) / (self.nuids * tot / (self.nuids - 1D))

    self._lazy.averagenumberofcarbonatoms = PTR_NEW([avg, SQRT(var)])
  END

  RETURN,*self._lazy.averagenumberofcarbonatoms
END

;+
; Retrieves the breakdown of the fitted PAHs.
;
; :Keywords:
;   Small: in, optional, type=int
;     Cut-off size for small PAHs, defaults to 50
;   Medium: in, optional, type=int
;     Cut-off size for medium PAHs, defaults to 70
;   Flux: in, optional, type=int
;     Whether to use the flux to determine the breakdown
;   Absolute: in, optional, type=int
;     Whether to return the absolute breakdown, otherwise normalize
;
; :Returns:
;   Structure
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetBreakdown,Small=Small,Medium=Medium,Flux=Flux,Absolute=Absolute

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF PTR_VALID(self._lazy.breakdown) THEN BEGIN

    signature = ([KEYWORD_SET(Small) ? Small : 0L, $
                  KEYWORD_SET(Medium) ? Medium : 0L, $
                  KEYWORD_SET(Flux), $
                  KEYWORD_SET(Absolute)]).HASHCODE()

    IF signature NE self._lazy.breakdown_sig THEN BEGIN

      PTR_FREE,self._lazy.breakdown

      self._lazy.breakdown_sig = signature
    ENDIF
  ENDIF

  IF NOT PTR_VALID(self._lazy.breakdown) THEN BEGIN

    IF  NOT PTR_VALID(self.database) THEN BEGIN
      PRINT
      PRINT,"========================================================="
      PRINT,"     VALID DATABASE POINTER NEEDED TO GET BREAKDOWN      "
      PRINT,"========================================================="
      PRINT
      self.state = 0
      RETURN,0
    ENDIF

    breakdown = {AmesPAHdb_Breakdown, $
                 anion:0D, $
                 neutral:0D, $
                 cation:0D, $
                 small:0D, $
                 medium:0D, $
                 large:0D, $
                 pure:0D, $
                 nitrogen:0D, $
                 solo:0L, $
                 duo: 0L, $
                 trio: 0L, $
                 quartet: 0L, $
                 quintet: 0L}

    ndata = N_ELEMENTS((*self.database).data.species)

    idx = ULINDGEN(ndata, self.nuids)

    select = WHERE((*self.database).data.species[idx MOD ndata].uid EQ (*self.uids)[idx / ndata]) MOD ndata

    ;; TODO: Is this useful information? Provide number of solos/etc. for the average PAH?
    breakdown.solo = TOTAL((*self.database).data.species[select].nsolo)

    breakdown.duo = TOTAL((*self.database).data.species[select].nduo)

    breakdown.trio = TOTAL((*self.database).data.species[select].ntrio)

    breakdown.quartet = TOTAL((*self.database).data.species[select].nquartet)

    breakdown.quintet = TOTAL((*self.database).data.species[select].nquintet)

    total = 1.0D

    IF KEYWORD_SET(Flux) THEN BEGIN

       IF NOT PTR_VALID(self.grid) THEN BEGIN
          PRINT
          PRINT,"========================================================="
          PRINT,"                    GRID IS NOT SET                      "
          PRINT,"========================================================="
          PRINT
         self.state = 0
         RETURN,0
       ENDIF

       classes = self->getClasses(Small=Small, Medium=Medium)

       ntags = N_TAGS(classes)

       dx = (*self.grid)[1:*] - *self.grid

       IF NOT KEYWORD_SET(Absolute) THEN BEGIN

          y = self->getFit()

          yavg = (y[0:-2] + y[1:*]) / 2.0D

          total = TOTAL(dx * yavg)
       ENDIF

       FOR i = 0, ntags - 1 DO BEGIN

          class = classes.(i)

          yavg = (class[0:-2] + class[1:*]) / 2.0D

          breakdown.(i) = TOTAL(dx * yavg) / total
      ENDFOR
    ENDIF ELSE BEGIN

      IF NOT PTR_VALID(self.weights) THEN BEGIN
         PRINT
         PRINT,"========================================================="
         PRINT,"                  WEIGHTS ARE NOT SET                    "
         PRINT,"========================================================="
         PRINT
         self.state = 0
         RETURN,0
      ENDIF

      IF NOT KEYWORD_SET(Absolute) THEN total = TOTAL((*self.weights).weight)

      select = self->Select(WHERE((*self.database).data.species.charge LT 0), $
                            *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.anion = TOTAL((*self.weights)[select].weight) / total

      select = self->Select(WHERE((*self.database).data.species.charge EQ 0), $
                            *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.neutral = TOTAL((*self.weights)[select].weight) / total

      select = self->Select(WHERE((*self.database).data.species.charge GT 0), $
                            *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.cation = TOTAL((*self.weights)[select].weight) / total

      IF NOT KEYWORD_SET(Small) THEN Small = 50L

      select = self->Select(WHERE((*self.database).data.species.nc LE Small), $
                            *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.small = TOTAL((*self.weights)[select].weight) / total

      IF NOT KEYWORD_SET(Medium) THEN Medium = 70L

      select = self->Select(WHERE((*self.database).data.species.nc GT Small AND $
                                  (*self.database).data.species.nc LE Medium), $
                            *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.medium = TOTAL((*self.weights)[select].weight) / total

      select = self->Select(WHERE((*self.database).data.species.nc GT Medium), $
                            *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.large = TOTAL((*self.weights)[select].weight) / total

      select = self->Select(WHERE((*self.database).data.species.nn EQ 0 AND $
                                  (*self.database).data.species.no EQ 0 AND $
                                  (*self.database).data.species.nmg EQ 0 AND $
                                  (*self.database).data.species.nsi EQ 0 AND $
                                  (*self.database).data.species.nfe EQ 0), $
                                  *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.pure = TOTAL((*self.weights)[select].weight) / total

      select = self->Select(WHERE((*self.database).data.species.nn GT 0), $
                            *self.weights, nselect)

      IF nselect NE 0 THEN breakdown.nitrogen = TOTAL((*self.weights)[select].weight) / total
    ENDELSE

    self._lazy.breakdown = PTR_NEW(breakdown)
  ENDIF

  RETURN,*self._lazy.breakdown
END

;+
; Retrieves the spectra of the different classes of the fitted PAHs.
;
; :Keywords:
;   Small: in, optional, type=int
;     Cut-off size for small PAHs, defaults to 50
;   Medium: in, optional, type=int
;     Cut-off size for medium PAHs defaults to 70
;
; :Returns:
;   Structure
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetClasses,Small=Small,Medium=Medium

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF PTR_VALID(self._lazy.classes) THEN BEGIN

    signature = ([KEYWORD_SET(Small) ? Small : 0L, $
                  KEYWORD_SET(Medium) ? Medium : 0L]).HASHCODE()

    IF signature NE self._lazy.classes_sig THEN BEGIN

      PTR_FREE,self._lazy.classes

      self._lazy.classes_sig = signature
    ENDIF
  ENDIF

  IF NOT PTR_VALID(self._lazy.classes) THEN BEGIN

    IF NOT PTR_VALID(self.database) THEN BEGIN
       PRINT
       PRINT,"========================================================="
       PRINT,"       VALID DATABASE POINTER NEEDED TO GET CLASSES      "
       PRINT,"========================================================="
       PRINT
       self.state = 0
       RETURN,0
    ENDIF

    ngrid = N_ELEMENTS(*self.grid)

    classes = REPLICATE({AmesPAHdb_Classes, $
                         anion:0D, $
                         neutral:0D, $
                         cation:0D, $
                         small:0D, $
                         medium:0D, $
                         large:0D, $
                         pure:0D, $
                         nitrogen:0D}, ngrid)

    select = self->Select(WHERE((*self.database).data.species.charge LT 0), $
                          *self.data, nselect)

    IF nselect NE 0 THEN classes.anion = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    select = self->Select(WHERE((*self.database).data.species.charge EQ 0), $
                          *self.data, nselect)

    IF nselect NE 0 THEN classes.neutral = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    select = self->Select(WHERE((*self.database).data.species.charge GT 0), $
                          *self.data, nselect)

    IF nselect NE 0 THEN classes.cation = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    IF NOT KEYWORD_SET(Small) THEN Small = 50L

    select = self->Select(WHERE((*self.database).data.species.nc LE Small), $
                          *self.data, nselect)

    IF nselect NE 0 THEN classes.small = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    IF NOT KEYWORD_SET(Medium) THEN Medium = 70L

    select = self->Select(WHERE((*self.database).data.species.nc GT Small AND $
                                (*self.database).data.species.nc LE Medium), $
                          *self.data, nselect)

    IF nselect NE 0 THEN classes.medium = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    select = self->Select(WHERE((*self.database).data.species.nc GT Medium), $
                          *self.data, nselect)

    IF nselect NE 0 THEN classes.large = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    select = self->Select(WHERE((*self.database).data.species.nn EQ 0 AND $
                                (*self.database).data.species.no EQ 0 AND $
                                (*self.database).data.species.nmg EQ 0 AND $
                                (*self.database).data.species.nsi EQ 0 AND $
                                (*self.database).data.species.nfe EQ 0), $
                                *self.data, nselect)

    IF nselect NE 0 THEN classes.pure = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    select = self->Select(WHERE((*self.database).data.species.nn GT 0), $
                          *self.data, nselect)

    IF nselect NE 0 THEN classes.nitrogen = TOTAL(REFORM((*self.data)[select].intensity, ngrid, nselect), 2)

    self._lazy.classes = PTR_NEW(classes)
  ENDIF

  RETURN,*self._lazy.classes
END

;+
; Retrieves the weights of the fitted PAHs
;
; :Returns:
;   Structure
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetWeights

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF PTR_VALID(self.weights) THEN RETURN,*self.weights

  RETURN,0
END

;+
; Retrieves the Norm of the fit
;
; :Returns:
;   float
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetNorm

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self._lazy.norm) THEN $
    self._lazy.norm = PTR_NEW(SQRT(TOTAL(((*self.observation).data.y-(*self.observation).data.continuum-self->getFit())^2, /NAN)))

  RETURN,*self._lazy.norm
END

;+
; Retrieves the Chi-squared of the fit.
;
; :Returns:
;   float
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetChiSquared

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self._lazy.chisquared) THEN $
    self._lazy.chisquared = PTR_NEW(TOTAL(((*self.observation).data.y-(*self.observation).data.continuum-self->getFit())^2 / (*self.observation).data.ystdev, /NAN))

  RETURN,*self._lazy.chisquared
END

;+
; Retrieves the error for the fit.
;
; :Returns:
;   Structure
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetError

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self._lazy.error) THEN BEGIN

    srt = SORT(*self.grid)
    x = (*self.grid)[srt]
    y = ((*self.observation).data.y-(*self.observation).data.continuum)[srt]
    r = (ABS(self->getResidual()))[srt]

    tags = ['err',  'e127',  'e112',  'e77', 'e62', 'e33']
    piecewise = CREATE_STRUCT(NAME='AmesPAHdb_Piecewise_Error', $
                              tags, -1.0D, -1.0D, -1.0D, -1.0D, -1.0D, -1.0D)

    range = [[MIN(x, MAX=xmax), xmax], $
             [ 754.0,  855.0], $
             [ 855.0, 1000.0], $
             [1000.0, 1495.0], $
             [1495.0, 1712.0], $
             [2900.0, 3125.0]]

    ntags = N_ELEMENTS(tags)
    FOR i = 0L, ntags - 1L DO BEGIN

      sel = WHERE(x GE range[0, i] AND x LE range[1, i], nsel)

      IF nsel EQ 0 THEN CONTINUE

      dx = (*self.grid)[sel[1:*]] - (*self.grid)[sel]
      yavg = (y[sel[0:-2]] + y[sel[1:*]]) / 2D
      ravg = (r[sel[0:-2]] + r[sel[1:*]]) / 2D

      piecewise.(i) = TOTAL(dx * ravg) / TOTAL(dx * yavg)
    ENDFOR

    self._lazy.error = PTR_NEW(piecewise)
  ENDIF

  RETURN,*self._lazy.error
END

;+
; Retrieves the method used for the fit.
;
; :Returns:
;   string
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetMethod

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,self.method
END

;+
; Retrieves the tolerance used for the fit.
;
; :Returns:
;   double
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetTolerance

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,self.tolerance
END

;+
; Retrieves the number of iterations needed for the fit.
;
; :Returns:
;   long
;
; :Categories:
;   SET/GET
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::GetIterations

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,self.iterations
END

;+
; Invalidate the AmesPAHdbIDLSuite_Fitted_Lazy structure.
;
; :Categories:
;   CLASS
;
; :Private:
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::InvalidateLazy

  COMPILE_OPT IDL2

  ON_ERROR,2

  FOR i = 0L, N_TAGS(self._lazy) - 1L DO BEGIN

    IF PTR_VALID(self._lazy.(i)) THEN PTR_FREE,self._lazy.(i)
   ENDFOR
END

;+
; Clean-up an AmesPAHdbIDLSuite_Fitted_Spectrum-instance
;
; :Categories:
;   CLASS
;
; :Private:
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum::Cleanup

  COMPILE_OPT IDL2

  ON_ERROR,2

  self.InvalidateLazy

  IF PTR_VALID(self.observation) THEN PTR_FREE,self.observation

  IF PTR_VALID(self.weights) THEN PTR_FREE,self.weights

  self->AmesPAHdbIDLSuite_Spectrum::Cleanup
END

;+
; Create an AmesPAHdbIDLSuite_Fitted_Spectrum-instance
;
; :Returns:
;   AmesPAHdbIDLSuite_Fitted_Spectrum-instance
;
; :Params:
;   Struct: in, optional, type=struct
;     Data structure
;
; :Keywords:
;   Type: in, optional, type=string
;     Type of Data
;   Version: in, optional, type=string
;    Versioning information
;   Data: in, optional, type=struct
;     Data structure
;   PAHdb: in, optional, type=pointer
;     Pointer to parsed database file
;   Uids: in, optional, type="long array (1D)"
;     UIDs in Data
;   Model: in, optional, type=string
;     References
;   Units: in, optional, type="AmesPAHdb_Units_S struct"
;     Units
;   Shift: in, optional, type=float
;     Shift
;   Grid: in, optional, type="float array"
;     Grid
;   Profile: in, optional, type=string
;     Profile
;   FWHM: in, optional, type=float
;     FWHM
;   Observation: in, optional, type="AmesPAHdb_Observation_S array"
;     Observation
;   Weights: in, optional, type="struct array"
;     Weights
;   Method: in, optional, type=string
;     Method
;   Tolerance: in, optional, type=double
;     Tolerance
;   Iterations: in, optional, type=long
;     Number of iterations
;
; :Categories:
;   CLASS
;-
FUNCTION AmesPAHdbIDLSuite_Fitted_Spectrum::Init,Struct,Type=Type,Version=Version,Data=Data,PAHdb=PAHdb,Uids=Uids,Model=Model,Units=Units,Shift=Shift,Grid=Grid,Profile=Profile,FWHM=FWHM,Observation=Observation,Weights=Weights,Method=Method,Tolerance=Tolerance,Iterations=Iterations

  COMPILE_OPT IDL2

  ON_ERROR,2

  self.state = self->AmesPAHdbIDLSuite_Plot::Init()

  IF N_PARAMS() GT 0 THEN self->Set,Struct,Type=Type,Version=Version,Data=Data,PAHdb=PAHdb,Uids=Uids,Model=Model,Units=Units,Shift=Shift,Grid=Grid,Profile=Profile,FWHM=FWHM,Observation=Observation,Weights=Weights,Method=Method,Tolerance=Tolerance,Iterations=Iterations $
  ELSE self->Set,Type=Type,Version=Version,Data=Data,PAHdb=PAHdb,Uids=Uids,Model=Model,Units=Units,Shift=Shift,Grid=Grid,Profile=Profile,FWHM=FWHM,Observation=Observation,Weights=Weights,Method=Method,Tolerance=Tolerance,Iterations=Iterations

  RETURN,self.state
END

;+
; Defines the AmesPAHdbIDLSuite_Fitted_Spectrum Class
;
; :Fields:
;   observation: type=pointer
;    Pointer to an observation structure
;   weights: type=pointer
;     Pointer to a weights structure
;   method: type=string
;     Method used for the fit
;   tolerance: type=double
;     Tolerance used for the fit
;   iterations: type=long
;     Number of iterations needed for the fit
;
; :Categories:
;   CLASS
;
; :Private:
;-
PRO AmesPAHdbIDLSuite_Fitted_Spectrum__DEFINE

  COMPILE_OPT IDL2

  ON_ERROR,2

  void = {AmesPAHdbIDLSuite_Fitted_Spectrum, $
          INHERITS AmesPAHdbIDLSuite_Spectrum, $
          observation:PTR_NEW(), $
          weights:PTR_NEW(), $
          method:'', $
          tolerance:0D, $
          iterations:0L, $
          _lazy:{AmesPAHdbIDLSuite_Fitted_Lazy, $
                 residual:PTR_NEW(), $
                 fit:PTR_NEW(), $
                 sizedistribution:PTR_NEW(), $
                 averagenumberofcarbonatoms:PTR_NEW(), $
                 breakdown:PTR_NEW(), $
                 breakdown_sig:0L, $
                 classes:PTR_NEW(), $
                 classes_sig:0L, $
                 norm:PTR_NEW(), $
                 chisquared:PTR_NEW(), $
                 error:PTR_NEW() $
                 }}
END

; END OF amespahdbidlsuite_fitted_spectrum__define.pro
