Poisson background

To add a Poisson background to a Simulation instance, use

bg = ba.PoissonBackground(seed=0)
simulation.setBackground(bg)

In this case, the output intensity is randomly distributed around the exact value with discrete Poisson statistics. The seed is a required argument: use a fixed value (e.g. seed=0) to reproduce the same noise realization, or a fresh one such as random.randrange(2**32) for an independent realization on each run.

The lower the intensity of the probing beam, the lower the signal-to-noise ratio.

Constant background

To add a constant background to a Simulation instance, use

bg = ba.ConstantBackground(1e3)
simulation.setBackground(bg)

Custom background

A Python object with a background method can transform the complete simulated field. The method receives the original Datafield and returns a new Datafield with the same frame. For example, a specular scan assembled from two exposure regions can use

class StitchedBackground:
    def __init__(self, boundary, left, right):
        self.boundary = boundary
        self.left = left
        self.right = right

    def background(self, field):
        x = np.asarray(field.xCenters())
        added = np.where(x < self.boundary, self.left, self.right)
        values = field.intensities() + added
        return ba.Datafield(field.title(), field.frame(), values.tolist())


bg = StitchedBackground(boundary=0.5*deg, left=20., right=100.)
simulation.setBackground(bg)

The input field provides the original intensities and all output-bin coordinates. A two-dimensional coordinate-dependent background can use

class CustomBackground:
    def background(self, field):
        x = np.asarray(field.xCenters())
        y = np.asarray(field.yCenters())
        values = field.intensities()
        values = values + 5. + 2.*x[np.newaxis, :] + 3.*y[:, np.newaxis]
        return ba.Datafield(field.title(), field.frame(),
                            values.ravel().tolist())

The callback is invoked after the scattering calculation. Its result must preserve the input frame and masked bins and contain finite, nonnegative intensities in all computed bins. This makes it suitable for interpolation of a measured background map without restricting the number of simulation threads.

Special cases

Specular simulation:

Specular simulation also transforms a zero field to obtain the background baseline used by its normalization. For an additive background (B(x)), a simulated reflected intensity (I(x)), and probe beam intensity (I_0), the result is ((I(x) + B(x)) / (I_0 + B(x))).

Depth probe simulation:

Background is not supported.

Examples

Examples/specular/Background.py

Examples/scatter2d/Background.py

Examples/scatter2d/CustomBackground.py

Scattering intensity

Coordinate-dependent background

The first script shows the built-in constant and Poisson backgrounds.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["bornagain>=25,<26"]
# ///
"""
Cylinder form factor in DWBA with background/noise
"""
import random
import bornagain as ba
ba.require_versions("bornagain>=25,<26")
from bornagain import ba_plot as bp, deg, nm


def get_sample():
    """
    A dilute random assembly of cylinders on a substrate.
    """
    substrate_color = (0.28, 0.57, 0.82)
    substrate_mat = ba.RefractiveMaterial("Substrate", substrate_color, 6e-6, 2e-8)
    particle_color = (0.86, 0.24, 0.18)
    particle_mat = ba.RefractiveMaterial("Particle", particle_color, 6e-4, 2e-8)
    particle = ba.Particle(particle_mat, ba.Cylinder(5*nm, 5*nm))

    particle_layer = ba.Layer(ba.Vacuum())
    particle_layer.deposit2D(ba.Dilute2D(0.001, particle))
    substrate_layer = ba.Layer(substrate_mat)

    sample = ba.Sample()
    sample.addLayer(particle_layer)
    sample.addLayer(substrate_layer)
    return sample


def get_simulation(sample, background):
    beam = ba.Beam(1e8, 0.1*nm, 0.2*deg)
    n = 100
    detector = ba.SphericalDetector(n, 0., 2*deg, n, 0., 2*deg)
    simulation = ba.ScatteringSimulation(beam, sample, detector)
    simulation.setBackground(background)
    return simulation


def simulate(background, title):
    sample = get_sample()
    simulation = get_simulation(sample, background)
    result = simulation.simulate()
    result.setTitle(title)
    return result


if __name__ == '__main__':    
    # New seed each run; constant seed reproduces the same noise realization.
    poisson_seed = random.randrange(2**32)
    results = [
        simulate(ba.ConstantBackground(1e0), "Constant background"),
        simulate(ba.PoissonBackground(seed=poisson_seed), "Poisson noise"),
    ]    
    ba.showSample3D(get_sample(), sample_size=120*nm, seed=0)
    bp.plot2d_to_row(results, log_range=5, unit_aspect=1)
    bp.plt.show()
auto/Examples/scatter2d/Background.py

The second script compares a simulation without background to one using a Python-defined background for four detector regions.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["bornagain>=25,<26"]
# ///
"""
Coordinate-dependent custom background in a GISAS simulation.
"""
import bornagain as ba
import numpy as np
ba.require_versions("bornagain>=25,<26")
from bornagain import ba_plot as bp, deg, nm


def get_sample():
    """
    A dilute random assembly of cylinders on a substrate.
    """
    substrate_color = (0.28, 0.57, 0.82)
    substrate_mat = ba.RefractiveMaterial("Substrate", substrate_color, 6e-6, 2e-8)
    particle_color = (0.86, 0.24, 0.18)
    particle_mat = ba.RefractiveMaterial("Particle", particle_color, 6e-4, 2e-8)
    particle = ba.Particle(particle_mat, ba.Cylinder(5*nm, 5*nm))

    particle_layer = ba.Layer(ba.Vacuum())
    particle_layer.deposit2D(ba.Dilute2D(0.001, particle))
    substrate_layer = ba.Layer(substrate_mat)

    sample = ba.Sample()
    sample.addLayer(particle_layer)
    sample.addLayer(substrate_layer)
    return sample


class DetectorRegionBackground:
    """
    Different background models for four detector regions.
    """

    def __init__(self, phi_boundary, alpha_boundary, seed=0):
        self.phi_boundary = phi_boundary
        self.alpha_boundary = alpha_boundary
        self.rng = np.random.default_rng(seed)

    def background(self, field):
        """
        Use two constant backgrounds, Poisson noise, and no background in the
        four quadrants.
        """
        values = field.intensities().copy()
        phi_axis = field.xCenters()
        alpha_axis = field.yCenters()
        for i_alpha, i_phi in np.ndindex(values.shape):
            alpha = alpha_axis[i_alpha]
            phi = phi_axis[i_phi]

            # No background in the lower-left quadrant.
            if alpha < self.alpha_boundary and phi < self.phi_boundary:
                values[i_alpha, i_phi] += 0.

            # Small constant background in the lower-right quadrant.
            elif alpha < self.alpha_boundary and phi >= self.phi_boundary:
                values[i_alpha, i_phi] += 0.03

            # Poisson noise in the upper-left quadrant.
            elif alpha >= self.alpha_boundary and phi < self.phi_boundary:
                intensity = values[i_alpha, i_phi]
                if np.isfinite(intensity):
                    values[i_alpha, i_phi] = self.rng.poisson(intensity)

            # Large constant background in the upper-right quadrant.
            else:
                values[i_alpha, i_phi] += 0.3

        return ba.Datafield(field.title(), field.frame(), values.ravel().tolist())


def get_simulation(sample, background=None):
    beam = ba.Beam(1e6, 0.1*nm, 0.2*deg)
    n = 100
    detector = ba.SphericalDetector(n, 0., 2*deg, n, 0., 2*deg)
    simulation = ba.ScatteringSimulation(beam, sample, detector)
    if background is not None:
        simulation.setBackground(background)
    return simulation


def simulate(sample, background, title):
    simulation = get_simulation(sample, background)
    result = simulation.simulate()
    result.setTitle(title)
    return result


if __name__ == '__main__':
    sample = get_sample()
    background = DetectorRegionBackground(1*deg, 0.63*deg)
    results = [
        simulate(sample, None, "No background"),
        simulate(sample, background, "Custom background"),
    ]
    bp.plot2d_to_row(results, log_range=5, unit_aspect=1)
    bp.plt.show()
auto/Examples/scatter2d/CustomBackground.py