コード例 #1
0
 def __init__(self, n_max, only_even=True):
   self.n_max = n_max
   self.lgf = log_factorial_generator(n_max*2+2)
   self.dmap = {}
   self.emap = {}
   self.gen_coefs_D()
   self.gen_coefs_E()    
コード例 #2
0
def generate_image(n,l, N=100):
  nmax = max(20,n)
  lfg =  math.log_factorial_generator(nmax)
  #rzfa = math.zernike_2d_radial(n,l,lfg)
  #rap = math.zernike_2d_polynome(n,l,rzfa)
  rap = math.zernike_2d_polynome(n,l)#,rzfa)

  image = flex.vec3_double()

  original=open('original.dat','w')
  count = 0
  for x in range(-N, N+1):
    for y in range(-N, N+1):
      rr = smath.sqrt(x*x+y*y)/N
      if rr>1.0:
        value=0.0
      else:
        tt = smath.atan2(y,x)
        value = rap.f(rr,tt)
        value = value.real
        count = count + 1
      image.append([x+N,y+N,value])
      print>>original, x+N,y+N, value
  original.close()
  return image
コード例 #3
0
ファイル: tst_zernike.py プロジェクト: cctbx/cctbx-playground
def tst_nss_spherical_harmonics():
  N=50
  M=20
  lfg =  math.log_factorial_generator(N)
  nsssphe = math.nss_spherical_harmonics(M+5,50000,lfg)

  a = nsssphe.legendre_lm(8,2)
  b = nsssphe.legendre_lm_pc(8,2)
  for aa,bb in zip(a,b):
    assert abs(aa-bb)<1e-7

  a = nsssphe.legendre_lm(14,3)
  b = nsssphe.legendre_lm_pc(14,3)
  for aa,bb in zip(a,b):
    assert abs(aa-bb)<1e-7

  lm=[ (0,0), (3,3), (4,1) ]

  theta = flex.double(range(100))*3.14/100.0
  phi = flex.double(range(100))*6.28/100.0
  for tt in theta:
    for pp in phi:
      r  = nsssphe.spherical_harmonic(20,10,tt,pp)
      rr = nsssphe.spherical_harmonic_pc(20,10,tt,pp)
      #print tt,pp, r.real, r.imag, 100.0*abs(r.real-rr.real)/(max(abs(r.real),1e-12)) , 100.0*abs(rr.imag-r.imag)/(max(abs(r.imag),1e-12))
      assert 100.0*abs(r.real-rr.real)/(max(abs(r.real),1e-12))<2.0
      assert 100.0*abs(rr.imag-r.imag)/(max(abs(r.imag),1e-12))<2.0
コード例 #4
0
def tst_nss_spherical_harmonics():
  N=50
  M=20
  lfg =  math.log_factorial_generator(N)
  nsssphe = math.nss_spherical_harmonics(M+5,50000,lfg)

  a = nsssphe.legendre_lm(8,2)
  b = nsssphe.legendre_lm_pc(8,2)
  for aa,bb in zip(a,b):
    assert abs(aa-bb)<1e-7

  a = nsssphe.legendre_lm(14,3)
  b = nsssphe.legendre_lm_pc(14,3)
  for aa,bb in zip(a,b):
    assert abs(aa-bb)<1e-7

  lm=[ (0,0), (3,3), (4,1) ]

  theta = flex.double(range(100))*3.14/100.0
  phi = flex.double(range(100))*6.28/100.0
  for tt in theta:
    for pp in phi:
      r  = nsssphe.spherical_harmonic(20,10,tt,pp)
      rr = nsssphe.spherical_harmonic_pc(20,10,tt,pp)
      #print tt,pp, r.real, r.imag, 100.0*abs(r.real-rr.real)/(max(abs(r.real),1e-12)) , 100.0*abs(rr.imag-r.imag)/(max(abs(r.imag),1e-12))
      assert 100.0*abs(r.real-rr.real)/(max(abs(r.real),1e-12))<2.0
      assert 100.0*abs(rr.imag-r.imag)/(max(abs(r.imag),1e-12))<2.0
コード例 #5
0
ファイル: test2dzernike.py プロジェクト: wangyufan/2dzernike
def tst_2d_zernike_mom(n, l, filename, h5file, flag):
    rebuilt = open(filename, 'w')
    # image = generate_image()
    # image=ImageToDat("/Users/wyf/Desktop/test11.png")
    image = preprocess_image(h5file, flag)
    # image = ImageToDat('cha.png')

    NP = int(smath.sqrt(image.size()))
    N = int(NP / 2)
    print "=====", NP, N
    grid_2d = math.two_d_grid(N, nmax)
    grid_2d.clean_space(image)
    grid_2d.construct_space_sum()
    zernike_2d_mom = math.two_d_zernike_moments(grid_2d, nmax)
    moments = zernike_2d_mom.moments()
    coefs = flex.real(moments)
    nl_array = math.nl_array(nmax)
    nls = nl_array.nl()
    nl_array.load_coefs(nls, coefs)
    lfg = math.log_factorial_generator(nmax)
    # print nl_array.get_coef(n,l)*2

    for nl, c in zip(nls, moments):
        if (abs(c) < 1e-3):
            c = 0
        print nl, c

    reconst = flex.complex_double(NP**2, 0)
    for nl, c in zip(nls, moments):
        n = nl[0]
        l = nl[1]
        if (l > 0):
            c = c * 2
        #rzfa = math.zernike_2d_radial(n,l,lfg)
        rap = math.zernike_2d_polynome(n, l)  #,rzfa)
        i = 0
        for x in range(0, NP):
            x = x - N
            for y in range(0, NP):
                y = y - N
                rr = smath.sqrt(x * x + y * y) / N
                if rr > 1.0:
                    value = 0.0
                else:
                    tt = smath.atan2(y, x)
                    value = rap.f(rr, tt)
                reconst[i] = reconst[i] + value * c
                i = i + 1

    i = 0
    for x in range(0, NP):
        for y in range(0, NP):
            value = reconst[i].real
            print >> rebuilt, x, y, value
            i = i + 1
    rebuilt.close()
コード例 #6
0
ファイル: plots.py プロジェクト: hattne/dials
def plot_absorption_surface(physical_model):
    """Plot an absorption surface for a physical scaling model."""

    d = {
        "absorption_surface": {
            "data": [],
            "layout": {
                "title": "Absorption correction surface",
                "xaxis": {"domain": [0, 1], "anchor": "y", "title": "theta (degrees)"},
                "yaxis": {"domain": [0, 1], "anchor": "x", "title": "phi (degrees)"},
            },
            "help": absorption_help_msg,
        }
    }

    params = physical_model.components["absorption"].parameters

    order = int(-1.0 + ((1.0 + len(params)) ** 0.5))
    lfg = scitbxmath.log_factorial_generator(2 * order + 1)
    STEPS = 50
    phi = np.linspace(0, 2 * np.pi, 2 * STEPS)
    theta = np.linspace(0, np.pi, STEPS)
    THETA, _ = np.meshgrid(theta, phi)
    lmax = int(-1.0 + ((1.0 + len(params)) ** 0.5))
    Intensity = np.ones(THETA.shape)
    counter = 0
    sqrt2 = pymath.sqrt(2)
    nsssphe = scitbxmath.nss_spherical_harmonics(order, 50000, lfg)
    for l in range(1, lmax + 1):
        for m in range(-l, l + 1):
            for it, t in enumerate(theta):
                for ip, p in enumerate(phi):
                    Ylm = nsssphe.spherical_harmonic(l, abs(m), t, p)
                    if m < 0:
                        r = sqrt2 * ((-1) ** m) * Ylm.imag
                    elif m == 0:
                        assert Ylm.imag == 0.0
                        r = Ylm.real
                    else:
                        r = sqrt2 * ((-1) ** m) * Ylm.real
                    Intensity[ip, it] += params[counter] * r
            counter += 1
    d["absorption_surface"]["data"].append(
        {
            "x": list(theta * 180.0 / np.pi),
            "y": list(phi * 180.0 / np.pi),
            "z": list(Intensity.T.tolist()),
            "type": "heatmap",
            "colorscale": "Viridis",
            "colorbar": {"title": "inverse <br>scale factor"},
            "name": "absorption surface",
            "xaxis": "x",
            "yaxis": "y",
        }
    )
    return d
コード例 #7
0
def tst_log_factorial():
  # Log factorial generator
  N=89
  lfg =  math.log_factorial_generator(N)
  ff = 1.0
  for ii in range(1,N):
    tmp = lfg.log_fact(ii)
    tmp2 = lfg.fact(ii)
    ff = ff*ii
    assert 1e10*abs(tmp2-ff)/ff<1
コード例 #8
0
ファイル: tst_zernike.py プロジェクト: cctbx/cctbx-playground
def tst_log_factorial():
  # Log factorial generator
  N=89
  lfg =  math.log_factorial_generator(N)
  ff = 1.0
  for ii in range(1,N):
    tmp = lfg.log_fact(ii)
    tmp2 = lfg.fact(ii)
    ff = ff*ii
    assert 1e10*abs(tmp2-ff)/ff<1
コード例 #9
0
ファイル: plot.py プロジェクト: dials/dials_scratch
def sph_harm_surf(l, m):
    from scitbx import math
    from scitbx.array_family import flex
    import math as pymath

    lfg = math.log_factorial_generator(2 * l + 1)
    nsssphe = math.nss_spherical_harmonics(l, 50000, lfg)

    theta = numpy.linspace(0, 2 * numpy.pi, 2 * STEPS)
    phi = numpy.linspace(0, numpy.pi, STEPS)

    THETA, PHI = numpy.meshgrid(theta, phi)
    R = numpy.cos(PHI**2)
    C = numpy.empty(THETA.shape, dtype=str)

    sqrt2 = pymath.sqrt(2)

    for it, t in enumerate(theta):
        for ip, p in enumerate(phi):
            Ylm = nsssphe.spherical_harmonic(l, abs(m), p, t)
            if m < 0:
                r = sqrt2 * ((-1)**m) * Ylm.imag
            elif m == 0:
                assert Ylm.imag == 0.0
                r = Ylm.real
            else:
                r = sqrt2 * ((-1)**m) * Ylm.real
            R[ip, it] = pymath.fabs(r)
            if r < 0:
                C[ip, it] = "y"
            else:
                C[ip, it] = "b"

    X = R * numpy.sin(PHI) * numpy.cos(THETA)
    Y = R * numpy.sin(PHI) * numpy.sin(THETA)
    Z = R * numpy.cos(PHI)

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1, projection="3d")
    plot = ax.plot_surface(
        X,
        Y,
        Z,
        rstride=1,
        cstride=1,
        facecolors=C,
        linewidth=0,
        antialiased=True,
        alpha=0.5,
    )

    print("Saving %s..." % ("ylm%d%d.png" % (l, m)))
    plt.savefig("ylm%d%d.png" % (l, m))
コード例 #10
0
ファイル: AimlessSurface.py プロジェクト: xia2/xia2
def work():
  from scitbx import math
  from scitbx.array_family import flex
  N=15
  lfg =  math.log_factorial_generator(N)
  nsssphe = math.nss_spherical_harmonics(6,50000,lfg)

  l = 2
  m = 1
  t = 1
  p = 1

  print nsssphe.spherical_harmonic(2, 1, 1, 1)
コード例 #11
0
def work():
    from scitbx import math
    from scitbx.array_family import flex
    N = 15
    lfg = math.log_factorial_generator(N)
    nsssphe = math.nss_spherical_harmonics(6, 50000, lfg)

    l = 2
    m = 1
    t = 1
    p = 1

    print nsssphe.spherical_harmonic(2, 1, 1, 1)
コード例 #12
0
def tst_2d_poly(n,l):
  nmax=max(n,20)
  np=50
  x,y=0.1,0.9
  r,t=smath.sqrt(x*x+y*y),smath.atan2(y,x)
  lfg =  math.log_factorial_generator(nmax)
  #rzfa = math.zernike_2d_radial(n,l,lfg)
  #rap = math.zernike_2d_polynome(n,l,rzfa)
  rap = math.zernike_2d_polynome(n,l)#,rzfa)
  rt_value=rap.f(r,t)
  grid = math.two_d_grid(np, nmax)
  zm2d = math.two_d_zernike_moments(grid, nmax)
  xy_value=zm2d.zernike_poly(n,l,x,y)

  print rt_value, xy_value, abs(rt_value), abs(xy_value)
コード例 #13
0
ファイル: plot.py プロジェクト: dials/dials_scratch
def sph_harm_surf(l, m):
  from scitbx import math
  from scitbx.array_family import flex
  import math as pymath
  lfg = math.log_factorial_generator(2 * l + 1)
  nsssphe = math.nss_spherical_harmonics(l, 50000, lfg)

  theta = numpy.linspace(0, 2 * numpy.pi, 2*STEPS)
  phi = numpy.linspace(0, numpy.pi, STEPS)

  THETA, PHI = numpy.meshgrid(theta, phi)
  R = numpy.cos(PHI**2)
  C = numpy.empty(THETA.shape, dtype=str)

  sqrt2 = pymath.sqrt(2)

  for it, t in enumerate(theta):
    for ip, p in enumerate(phi):
      Ylm = nsssphe.spherical_harmonic(l, abs(m), p, t)
      if m < 0:
        r = sqrt2 * ((-1) ** m) * Ylm.imag
      elif m == 0:
        assert(Ylm.imag == 0.0)
        r = Ylm.real
      else:
        r = sqrt2 * ((-1) ** m) * Ylm.real
      R[ip, it] = pymath.fabs(r)
      if r < 0:
        C[ip, it] = 'y'
      else:
        C[ip, it] = 'b'

  X = R * numpy.sin(PHI) * numpy.cos(THETA)
  Y = R * numpy.sin(PHI) * numpy.sin(THETA)
  Z = R * numpy.cos(PHI)

  fig = plt.figure()
  ax = fig.add_subplot(1,1,1, projection='3d')
  plot = ax.plot_surface(
      X, Y, Z, rstride=1, cstride=1, facecolors=C,
      linewidth=0, antialiased=True, alpha=0.5)

  print 'Saving %s...' % ('ylm%d%d.png' % (l, m))
  plt.savefig('ylm%d%d.png' % (l, m))
コード例 #14
0
ファイル: AimlessSurface.py プロジェクト: hainm/xia2
def evaluate_1degree(ClmList, png_filename):
  from scitbx import math
  from scitbx.array_family import flex
  import math as pymath
  import numpy
  d2r = pymath.pi / 180.0
  order = order_from_nterm(len(ClmList))
  lfg =  math.log_factorial_generator(2 * order + 1)
  nsssphe = math.nss_spherical_harmonics(order,50000,lfg)
  Clm = { }
  idx = 0
  for l in range(1, order+1):
    for m in range(-l, l+1):
      Clm[(l,m)] = ClmList[idx]
      idx += 1

  abscor = numpy.empty((1+180//1, 1+360//1), float, 'C')
  sqrt2 = pymath.sqrt(2)
  for t in range(0, 181, 1):
    for p in range(0, 361, 1):
      a = 1.0
      for l in range(1, order+1):
        for m in range(-l, l+1):
          # Ylm = nsssphe.spherical_harmonic(l, m, t*d2r, p*d2r)
          # Convert from complex to real according to
          # http://en.wikipedia.org/wiki/Spherical_harmonics#Real_form
          Ylm = nsssphe.spherical_harmonic(l, abs(m), t*d2r, p*d2r)
          if m < 0:
            a += Clm[(l,m)] * sqrt2 * ((-1) ** m) * Ylm.imag
          elif m == 0:
            assert(Ylm.imag == 0.0)
            a += Clm[(l,m)] * Ylm.real
          else:
            a += Clm[(l,m)] * sqrt2 * ((-1) ** m) * Ylm.real
      abscor[(t//1, p//1)] = a

  import matplotlib
  matplotlib.use('Agg')
  from matplotlib import pyplot
  plot = pyplot.imshow(abscor)
  pyplot.colorbar()
  pyplot.savefig(png_filename)
  return
コード例 #15
0
ファイル: tst_zernike.py プロジェクト: cctbx/cctbx-playground
def tst_zernike_radial():
  N=50
  M=8
  lfg =  math.log_factorial_generator(N)
  for n in range(M):
    for l in range(n):
      if (n-l)%2==0:
        rzfa = math.zernike_radial(n,l, lfg)
        r = flex.double( flex.double(range(100000))/99999.0)
        a = rzfa.f( r )
        tmp = a*a*r*r
        tmp = flex.sum( tmp )/100000.0
        assert abs(tmp-1)<2e-2
        for nn in range(M):
          for ll in range(nn):
            if (nn-ll)%2==0:
              if (nn!=n):
                if ll==l:
                  rzfb = math.zernike_radial(nn,ll, lfg)
                  b = rzfb.f( r )
                  tmp = a*b*r*r
                  tmp = flex.sum( tmp )/100000.0
                  assert (tmp<1e-2)
コード例 #16
0
def tst_zernike_radial():
  N=50
  M=8
  lfg =  math.log_factorial_generator(N)
  for n in range(M):
    for l in range(n):
      if (n-l)%2==0:
        rzfa = math.zernike_radial(n,l, lfg)
        r = flex.double( flex.double(range(100000))/99999.0)
        a = rzfa.f( r )
        tmp = a*a*r*r
        tmp = flex.sum( tmp )/100000.0
        assert abs(tmp-1)<2e-2
        for nn in range(M):
          for ll in range(nn):
            if (nn-ll)%2==0:
              if (nn!=n):
                if ll==l:
                  rzfb = math.zernike_radial(nn,ll, lfg)
                  b = rzfb.f( r )
                  tmp = a*b*r*r
                  tmp = flex.sum( tmp )/100000.0
                  assert (tmp<1e-2)
コード例 #17
0
def evaluate_1degree(ClmList):
    from scitbx import math
    from scitbx.array_family import flex
    import math as pymath
    import numpy
    d2r = pymath.pi / 180.0
    order = order_from_nterm(len(ClmList))
    lfg = math.log_factorial_generator(2 * order + 1)
    nsssphe = math.nss_spherical_harmonics(order, 50000, lfg)
    Clm = {}
    idx = 0
    for l in range(1, order + 1):
        for m in range(-l, l + 1):
            Clm[(l, m)] = ClmList[idx]
            idx += 1

    abscor = numpy.empty((1 + 180 // 1, 1 + 360 // 1), float, 'C')
    sqrt2 = pymath.sqrt(2)
    for t in range(0, 181, 1):
        for p in range(0, 361, 1):
            a = 1.0
            for l in range(1, order + 1):
                for m in range(-l, l + 1):
                    # Ylm = nsssphe.spherical_harmonic(l, m, t*d2r, p*d2r)
                    # Convert from complex to real according to
                    # http://en.wikipedia.org/wiki/Spherical_harmonics#Real_form
                    Ylm = nsssphe.spherical_harmonic(l, abs(m), t * d2r,
                                                     p * d2r)
                    if m < 0:
                        a += Clm[(l, m)] * sqrt2 * ((-1)**m) * Ylm.imag
                    elif m == 0:
                        assert (Ylm.imag == 0.0)
                        a += Clm[(l, m)] * Ylm.real
                    else:
                        a += Clm[(l, m)] * sqrt2 * ((-1)**m) * Ylm.real
            abscor[(t // 1, p // 1)] = a
    return abscor
コード例 #18
0
def tst_2d_zernike_mom(n,l, N=100, filename=None):
  nmax = max(20,n)
  rebuilt=open('rebuilt.dat','w')
  tt1=time.time()
  if(filename is not None):
    image=read_data(filename)
  else:
    image=generate_image(n,l)

  NP=int(smath.sqrt( image.size() ))
  N=NP/2
  grid_2d = math.two_d_grid(N, nmax)
  grid_2d.clean_space( image )
  grid_2d.construct_space_sum()
  tt2=time.time()
  print "time used: ", tt2-tt1
  zernike_2d_mom = math.two_d_zernike_moments( grid_2d, nmax )

  moments = zernike_2d_mom.moments()

  tt2=time.time()
  print "time used: ", tt2-tt1

  coefs = flex.real( moments )
  nl_array = math.nl_array( nmax )
  nls = nl_array.nl()
  nl_array.load_coefs( nls, coefs )
  lfg =  math.log_factorial_generator(nmax)

  print nl_array.get_coef(n,l)*2

  for nl, c in zip( nls, moments):
    if(abs(c)<1e-3):
      c=0
    print nl, c

  print
  reconst=flex.complex_double(NP**2, 0)
  for nl,c in zip( nls, moments):
    n=nl[0]
    l=nl[1]
    if(l>0):
      c=c*2
    #rzfa = math.zernike_2d_radial(n,l,lfg)
    rap = math.zernike_2d_polynome(n,l) #,rzfa)
    i=0
    for x in range(0,NP):
      x=x-N
      for y in range(0,NP):
        y=y-N
        rr = smath.sqrt(x*x+y*y)/N
        if rr>1.0:
          value=0.0
        else:
          tt = smath.atan2(y,x)
          value = rap.f(rr,tt)
        reconst[i]=reconst[i]+value*c
        i=i+1

  i = 0
  for x in range(0,NP):
    for y in range(0,NP):
      value=reconst[i].real
      if(value>0):
        print>>rebuilt, x,y,image[i][2],value
      i=i+1


  rebuilt.close()
コード例 #19
0
ファイル: plots.py プロジェクト: rjgildea/dials
def plot_absorption_plots(physical_model, reflection_table=None):
    """Make a number of plots to help with the interpretation of the
    absorption correction."""
    # First plot the absorption surface

    d = {
        "absorption_surface": {
            "data": [],
            "layout": {
                "title": "Absorption correction surface",
                "xaxis": {
                    "domain": [0, 1],
                    "anchor": "y",
                    "title": "azimuthal angle (degrees)",
                },
                "yaxis": {
                    "domain": [0, 1],
                    "anchor": "x",
                    "title": "polar angle (degrees)",
                },
            },
            "help": absorption_help_msg,
        }
    }

    params = physical_model.components["absorption"].parameters

    order = int(-1.0 + ((1.0 + len(params)) ** 0.5))
    lfg = scitbxmath.log_factorial_generator(2 * order + 1)
    STEPS = 50
    azimuth_ = np.linspace(0, 2 * np.pi, 2 * STEPS)
    polar_ = np.linspace(0, np.pi, STEPS)
    THETA, _ = np.meshgrid(azimuth_, polar_, indexing="ij")
    lmax = int(-1.0 + ((1.0 + len(params)) ** 0.5))
    Intensity = np.ones(THETA.shape)
    undiffracted_intensity = np.ones(THETA.shape)
    counter = 0
    sqrt2 = math.sqrt(2)
    nsssphe = scitbxmath.nss_spherical_harmonics(order, 50000, lfg)
    for l in range(1, lmax + 1):
        for m in range(-l, l + 1):
            for it, t in enumerate(polar_):
                for ip, p in enumerate(azimuth_):
                    Ylm = nsssphe.spherical_harmonic(l, abs(m), t, p)
                    if m < 0:
                        r = sqrt2 * ((-1) ** m) * Ylm.imag
                    elif m == 0:
                        assert Ylm.imag == 0.0
                        r = Ylm.real
                    else:
                        r = sqrt2 * ((-1) ** m) * Ylm.real
                    Intensity[ip, it] += params[counter] * r
                    # for the undiffracted intensity, we want to add the correction
                    # at each point to the parity conjugate. We can use the fact
                    # that the odd l terms are parity odd, and even are even, to
                    # just calculate the even terms as follows
                    if l % 2 == 0:
                        undiffracted_intensity[ip, it] += params[counter] * r
            counter += 1
    d["absorption_surface"]["data"].append(
        {
            "x": list(azimuth_ * 180.0 / np.pi),
            "y": list(polar_ * 180.0 / np.pi),
            "z": list(Intensity.T.tolist()),
            "type": "heatmap",
            "colorscale": "Viridis",
            "colorbar": {"title": "inverse <br>scale factor"},
            "name": "absorption surface",
            "xaxis": "x",
            "yaxis": "y",
        }
    )

    d["undiffracted_absorption_surface"] = {
        "data": [],
        "layout": {
            "title": "Undiffracted absorption correction",
            "xaxis": {
                "domain": [0, 1],
                "anchor": "y",
                "title": "azimuthal angle (degrees)",
            },
            "yaxis": {
                "domain": [0, 1],
                "anchor": "x",
                "title": "polar angle (degrees)",
            },
        },
        "help": """
This plot shows the calculated relative absorption for a paths travelling
straight through the crystal at a given direction in a crystal-fixed frame of
reference (in spherical coordinates). This gives an indication of the effective
shape of the crystal for absorbing x-rays. In this plot, the pole (polar angle 0)
corresponds to the laboratory x-axis.
""",
    }

    d["undiffracted_absorption_surface"]["data"].append(
        {
            "x": list(azimuth_ * 180.0 / np.pi),
            "y": list(polar_ * 180.0 / np.pi),
            "z": list(undiffracted_intensity.T.tolist()),
            "type": "heatmap",
            "colorscale": "Viridis",
            "colorbar": {"title": "inverse <br>scale factor"},
            "name": "Undiffracted absorption correction",
            "xaxis": "x",
            "yaxis": "y",
        }
    )

    if not reflection_table:
        return d

    # now plot the directions of the scattering vectors

    d["vector_directions"] = {
        "data": [],
        "layout": {
            "title": "Scattering vectors in crystal frame",
            "xaxis": {
                "domain": [0, 1],
                "anchor": "y",
                "title": "azimuthal angle (degrees)",
                "range": [0, 360],
            },
            "yaxis": {
                "domain": [0, 1],
                "anchor": "x",
                "title": "polar angle (degrees)",
                "range": [0, 180],
            },
            "coloraxis": {
                "showscale": False,
            },
        },
        "help": """
This plot shows the scattering vector directions in the crystal reference frame
used to determine the absorption correction. The s0 vectors are plotted in yellow,
the s1 vectors are plotted in teal. This gives an indication of which parts of
the absorption correction surface are sampled when determining the absorption
correction. In this plot, the pole (polar angle 0) corresponds to the laboratory
x-axis.""",
    }

    STEPS = 180  # do one point per degree
    azimuth_ = np.linspace(0, 2 * np.pi, 2 * STEPS)
    polar_ = np.linspace(0, np.pi, STEPS)
    THETA, _ = np.meshgrid(azimuth_, polar_, indexing="ij")
    Intensity = np.full(THETA.shape, np.NAN)

    # note, the s1_lookup, s0_lookup is only calculated for large datasets, so
    # for small datasets we need to calculate again.
    if "s1_lookup" not in physical_model.components["absorption"].data:
        s1_lookup = calc_lookup_index(
            calc_theta_phi(reflection_table["s1c"]), points_per_degree=1
        )
        idx_polar, idx_azimuth = np.divmod(np.unique(s1_lookup), 360)
        Intensity[idx_azimuth, idx_polar] = 1
    else:
        s1_lookup = np.unique(physical_model.components["absorption"].data["s1_lookup"])
        # x is phi, y is theta
        idx_polar, idx_azimuth = np.divmod(s1_lookup, 720)
        idx_polar = idx_polar // 2  # convert from two points per degree to one
        idx_azimuth = idx_azimuth // 2
        Intensity[idx_azimuth, idx_polar] = 1

    d["vector_directions"]["data"].append(
        {
            "x": list(azimuth_ * 180.0 / np.pi),
            "y": list(polar_ * 180.0 / np.pi),
            "z": list(Intensity.T.tolist()),
            "type": "heatmap",
            "colorscale": "Viridis",
            "showscale": False,
            "xaxis": "x",
            "yaxis": "y",
            "zmin": 0,
            "zmax": 2,
        }
    )

    Intensity = np.full(THETA.shape, np.NAN)

    if "s0_lookup" not in physical_model.components["absorption"].data:
        s0_lookup = calc_lookup_index(
            calc_theta_phi(reflection_table["s0c"]), points_per_degree=1
        )
        idx_polar, idx_azimuth = np.divmod(np.unique(s0_lookup), 360)
        Intensity[idx_azimuth, idx_polar] = 2
    else:
        s0_lookup = np.unique(physical_model.components["absorption"].data["s0_lookup"])
        # x is phi, y is theta
        idx_polar, idx_azimuth = np.divmod(s0_lookup, 720)
        idx_polar = idx_polar // 2  # convert from two points per degree to one
        idx_azimuth = idx_azimuth // 2
        Intensity[idx_azimuth, idx_polar] = 2

    d["vector_directions"]["data"].append(
        {
            "x": list(azimuth_ * 180.0 / np.pi),
            "y": list(polar_ * 180.0 / np.pi),
            "z": list(Intensity.T.tolist()),
            "type": "heatmap",
            "colorscale": "Viridis",
            "showscale": False,
            "xaxis": "x",
            "yaxis": "y",
            "zmin": 0,
            "zmax": 2,
        }
    )

    scales = physical_model.components["absorption"].calculate_scales()
    hist = flex.histogram(scales, n_slots=min(100, int(scales.size() * 10)))

    d["absorption_corrections"] = {
        "data": [
            {
                "x": list(hist.slot_centers()),
                "y": list(hist.slots()),
                "type": "bar",
                "name": "Applied absorption corrections",
            },
        ],
        "layout": {
            "title": "Applied absorption corrections",
            "xaxis": {"anchor": "y", "title": "Inverse scale factor"},
            "yaxis": {"anchor": "x", "title": "Number of reflections"},
        },
    }

    return d