Module ctsimu.evaluation.test2D_SW_1

Test 2D-SW-1: Scintillator characteristics

In this scenario, the imaging characteristics of specific scintillators is tested for different X-ray source spectra and two different object materials. Two spherical step wedges are used, one made of aluminum, the other one of iron. Each wedge features nine steps of different thickness. The test compares their gray values (as well as the free beam gray value) for each combination of scintillator, material and spectrum (both monochromatic and polychromatic) to a corresponding ideal image taken with an ideal detector, i.e., one whose gray values correspond linearly to the intensity of incident radiation.

The following table lists all the scenarios that need to be simulated for this test, along with the keyword that needs to be used to correctly identify each projection metadata file for the toolbox.

Keyword Material Spectrum Scintillator
D1_Al_50 Al 50 keV mono ideal
D1_Al_120 Al 120 kV poly ideal
D1_Fe_100 Fe 100 keV mono ideal
D1_Fe_200 Fe 200 kV poly ideal
D2_Al_50 Al 50 keV mono 200 μm CsI
D2_Al_120 Al 120 kV poly 200 μm CsI
D2_Fe_100 Fe 100 keV mono 200 μm CsI
D2_Fe_200 Fe 200 kV poly 200 μm CsI
D3_Al_50 Al 50 keV mono 500 μm CsI
D3_Al_120 Al 120 kV poly 500 μm CsI
D3_Fe_100 Fe 100 keV mono 500 μm CsI
D3_Fe_200 Fe 200 kV poly 500 μm CsI
D4_Al_50 Al 50 keV mono 500 μm Gd2O2S
D4_Al_120 Al 120 kV poly 500 μm Gd2O2S
D4_Fe_100 Fe 100 keV mono 500 μm Gd2O2S
D4_Fe_200 Fe 200 kV poly 500 μm Gd2O2S

An example call for this test scenario looks like this:

from ctsimu.toolbox import Toolbox
Toolbox("2D-SW-1",
    D1_Al_50  = "2D-SW-1_Al_Detector1_50keV-mono_metadata.json",
    D1_Al_120 = "2D-SW-1_Al_Detector1_120kV-poly_metadata.json",
    D1_Fe_100 = "2D-SW-1_Fe_Detector1_100keV-mono_metadata.json",
    D1_Fe_200 = "2D-SW-1_Fe_Detector1_200kV-poly_metadata.json",
    D2_Al_50  = "2D-SW-1_Al_Detector2_50keV-mono_metadata.json",
    D2_Al_120 = "2D-SW-1_Al_Detector2_120kV-poly_metadata.json",
    D2_Fe_100 = "2D-SW-1_Fe_Detector2_100keV-mono_metadata.json",
    D2_Fe_200 = "2D-SW-1_Fe_Detector2_200kV-poly_metadata.json",
    D3_Al_50  = "2D-SW-1_Al_Detector3_50keV-mono_metadata.json",
    D3_Al_120 = "2D-SW-1_Al_Detector3_120kV-poly_metadata.json",
    D3_Fe_100 = "2D-SW-1_Fe_Detector3_100keV-mono_metadata.json",
    D3_Fe_200 = "2D-SW-1_Fe_Detector3_200kV-poly_metadata.json",
    D4_Al_50  = "2D-SW-1_Al_Detector4_50keV-mono_metadata.json",
    D4_Al_120 = "2D-SW-1_Al_Detector4_120kV-poly_metadata.json",
    D4_Fe_100 = "2D-SW-1_Fe_Detector4_100keV-mono_metadata.json",
    D4_Fe_200 = "2D-SW-1_Fe_Detector4_200kV-poly_metadata.json"
)

If only a sub-sample should be tested, it is not necessary to provide all simulation results at once. However, if a scintillator scenario misses its corresponding ideal detector scenario, it cannot be evaluated.

For the evaluation, the ratio of scintillator gray value to ideal detector gray value is calculated for each step of the spherical wedge, as well as for a free beam region. The grey values are measured as the pixel mean within the regions of interest (ROI) given in the table below and illustrated in the following image, defined as rectangles with an upper left corner coordinate (x0, y0) in the pixel coordinate system which is included in the ROI, and a lower right corner coordinate (x1, y1) which is not included anymore in the ROI.

Step x0 y0 x1 y1
1 510 854 530 875
2 510 763 530 784
3 510 672 530 693
4 510 581 530 602
5 510 490 530 511
6 510 399 530 420
7 510 308 530 329
8 510 217 530 238
9 510 126 530 147
free beam 510 35 530 56

Regions of interest on the step wedge

The gray value ratios are compared to reference ratios obtained from particle transport Monte-Carlo simulations. The gray value results of such simulations are subject to stochastic noise. For a given mean Monte-Carlo gray value intensity μscint in a scintillator scenario, and a corresponding gray value intensity for the ideal detector μideal, Gaussian error propagation gives the error u of the Monte-Carlo gray value ratio, given the standard deviations \sigma_\text{scint} and \sigma_\text{ideal} of the grey values inside the ROI under consideration:

u\left(\frac{\mu_\text{scint}}{\mu_\text{ideal}}\right) = \sqrt{\frac{\sigma_\text{scint}^2}{\mu_\text{ideal}^2} + \frac{\sigma_\text{ideal}^2 \mu_\text{scint}^2}{\mu_\text{ideal}^4}}

During the Monte-Carlo simulations, the effects of primary radiation and scatter radiation can be separated. Accordingly, the evaluation provides two sets of results: one where only primary radiation is considered (file names contain primary), and one where both scattered radiation and primary radiation are considered (file names contain scatter). Depending on which kind of software is tested, the correct comparison set should be chosen from the two options.

An evaluation run will produce plots such as the example shown below. These plots display the measured gray value ratios and show them in relation to the results from the Monte-Carlo simulations with their corresponding uncertainty u.

The following example evaluation result plot displays the gray value ratios between detector 2 (200 μm CsI) and detector 1 (the ideal detector). Small crosses represent the gray value ratios calculated from the Monte-Carlo simulations. Their error bars cover the ratio's uncertainty u in both directions. Solid shapes without error bars show the measured ratios from the projections of the simulation software.

2D-SW-1 example evaluation result

Additionally, the following data files are created:

  • 2D-SW-1_all_GV_means.txt

    lists all the mean grey values in one file.

  • 2D-SW-1_all_GV_ratios_to_D1.txt

    lists the grey value ratios for detectors D2, D3 and D4 to the scenarios from detector D1. It is self-evident that the grey value ratios of detector D1 to itself are all 1, but they are listed as well for clarity.

  • 2D-SW-1_all_MCprimary_reference_ratios.txt

    lists all the grey value ratios and their uncertainties u calculated from the Monte-Carlo simulations for the case of only primary radiation simulated (i.e., no scatter radiation taken into account).

  • 2D-SW-1_all_MCscatter_reference_ratios.txt

    lists all the grey value ratios and their uncertainties u calculated from the Monte-Carlo simulations for the case of both primary and scatter radiation simulated.

  • For each sub test, a file such as 2D-SW-1_D1_Al_50_grey_values.txt is generated which contains the ROI information and mean grey values.

Expand source code
# -*- coding: UTF-8 -*-
"""# Test 2D-SW-1: Scintillator characteristics

.. include:: ./test2D_SW_1.md
"""

from ..test import *
from ..helpers import *

import pkgutil
import io

class Test2D_SW_1_results:
    """ Results for one sub test of the filtering scenario. """

    def __init__(self):
        self.subtestName = ""
        self.longName = ""

        # Grey value means and ratios (per wedge step):
        self.means             = []     # Mean value for each step (measured for simulation software)
        self.ratios_to_D1      = None   # grey value ratios to ideal detector (measured for simulation software)

        # Grey values from Monte-Carlo simuation:
        self.means_rosi_total    = None   # accounting for scatter radiation
        self.means_rosi_primary  = None   # accounting only for primary radiation
        self.means_mcray_total    = None   # accounting for scatter radiation
        self.means_mcray_primary  = None   # accounting only for primary radiation


        # Reference values for Monte-Carlo simulation (calculated in loadReference())
        self.means_mc_total    = None   # accounting for scatter radiation
        self.means_mc_primary  = None   # accounting only for primary radiation
        self.error_mc_total_upper   = None
        self.error_mc_total_lower   = None
        self.error_mc_primary_upper = None
        self.error_mc_primary_lower = None

        self.ratios_to_D1_mc_total = None
        self.ratios_to_D1_mc_primary = None
        self.error_ratios_to_D1_mc_total_upper = None
        self.error_ratios_to_D1_mc_total_lower = None
        self.error_ratios_to_D1_mc_primary_upper = None
        self.error_ratios_to_D1_mc_primary_lower = None

    def loadReference(self, name):
        dataText = pkgutil.get_data(__name__, "data/2D-SW-1_scenario{name}.txt".format(name=name)).decode()
        dataIO = io.StringIO(dataText)
        allData = numpy.loadtxt(dataIO, delimiter='\t')  # ignore free beam

        # --- total radiation
        means_rosi_total  = allData[:,1]
        means_mcray_total = allData[:,4]
        means_total = (means_rosi_total + means_mcray_total) * 0.5
        delta_total = numpy.absolute(means_rosi_total - means_mcray_total)

        stddev_rosi_total  = allData[:,2]
        stddev_mcray_total = allData[:,5]

        #err_total = 0.5*(delta_total + numpy.sqrt(numpy.square(stddev_rosi_total) + numpy.square(stddev_mcray_total)))
        err_total_rosi  = numpy.fmax(stddev_rosi_total,  stddev_mcray_total-delta_total) + 0.5*delta_total
        err_total_mcray = numpy.fmax(stddev_mcray_total, stddev_rosi_total-delta_total)  + 0.5*delta_total
        err_total_upper = numpy.zeros_like(err_total_rosi)
        err_total_lower = numpy.zeros_like(err_total_rosi)
        for i in range(len(err_total_upper)):
            if means_rosi_total[i] > means_total[i]:  # ROSI is upper bound
                err_total_upper[i] = err_total_rosi[i]
                err_total_lower[i] = err_total_mcray[i]
            else:   # McRay is upper bound
                err_total_upper[i] = err_total_mcray[i]
                err_total_lower[i] = err_total_rosi[i]


        # --- primary radiation
        means_rosi_primary  = allData[:,9]
        means_mcray_primary = allData[:,12]
        means_primary = (means_rosi_primary + means_mcray_primary) * 0.5
        delta_primary = numpy.absolute(means_rosi_primary - means_mcray_primary)

        stddev_rosi_primary  = allData[:,10]
        stddev_mcray_primary = allData[:,13]

        #err_primary = 0.5*(delta_primary + numpy.sqrt(numpy.square(stddev_rosi_primary) + numpy.square(stddev_mcray_primary)))
        err_primary_rosi  = numpy.fmax(stddev_rosi_primary,  stddev_mcray_primary-delta_primary) + 0.5*delta_primary
        err_primary_mcray = numpy.fmax(stddev_mcray_primary, stddev_rosi_primary-delta_primary)  + 0.5*delta_primary
        err_primary_upper = numpy.zeros_like(err_primary_rosi)
        err_primary_lower = numpy.zeros_like(err_primary_rosi)
        for i in range(len(err_primary_upper)):
            if means_rosi_primary[i] > means_primary[i]:  # ROSI is upper bound
                err_primary_upper[i] = err_primary_rosi[i]
                err_primary_lower[i] = err_primary_mcray[i]
            else:   # McRay is upper bound
                err_primary_upper[i] = err_primary_mcray[i]
                err_primary_lower[i] = err_primary_rosi[i]


        self.means_rosi_total    = means_rosi_total
        self.means_rosi_primary  = means_rosi_primary
        self.means_mcray_total    = means_mcray_total
        self.means_mcray_primary  = means_mcray_primary

        self.means_mc_total         = means_total
        self.means_mc_primary       = means_primary
        self.error_mc_total_upper   = err_total_upper
        self.error_mc_total_lower   = err_total_lower
        self.error_mc_primary_upper = err_primary_upper
        self.error_mc_primary_lower = err_primary_lower

        dataIO.close()


class Test2D_SW_1(generalTest):
    """ CTSimU test 2D-SW-1: detector models / scintillators. """

    def __init__(self, resultFileDirectory=".", name=None, rawOutput=False):
        generalTest.__init__(
            self,
            testName="2D-SW-1",
            name=name,
            resultFileDirectory=resultFileDirectory,
            rawOutput=rawOutput)

        self.validSubtests = ["D1_Al_50", "D1_Al_120", "D1_Fe_100", "D1_Fe_200", "D2_Al_50", "D2_Al_120", "D2_Fe_100", "D2_Fe_200", "D3_Al_50", "D3_Al_120", "D3_Fe_100", "D3_Fe_200", "D4_Al_50", "D4_Al_120", "D4_Fe_100", "D4_Fe_200"]

        self.results = len(self.validSubtests)*[None]
        self.currentIndex = None

        self.shrink = 35
        self.leftOffset = 510 - self.shrink
        self.nPixels = 20 + 2*self.shrink

        # Absolute step definitions. Will be shrunk to accept tolerance border,
        # but these absolute definitions are needed for grey value rescaling and clipping.
        self.steps = [
            ImageROI(self.leftOffset, 819, self.leftOffset+self.nPixels, 910),
            ImageROI(self.leftOffset, 728, self.leftOffset+self.nPixels, 819),
            ImageROI(self.leftOffset, 637, self.leftOffset+self.nPixels, 728),
            ImageROI(self.leftOffset, 546, self.leftOffset+self.nPixels, 637),
            ImageROI(self.leftOffset, 455, self.leftOffset+self.nPixels, 546),
            ImageROI(self.leftOffset, 364, self.leftOffset+self.nPixels, 455),
            ImageROI(self.leftOffset, 273, self.leftOffset+self.nPixels, 364),
            ImageROI(self.leftOffset, 182, self.leftOffset+self.nPixels, 273),
            ImageROI(self.leftOffset,  91, self.leftOffset+self.nPixels, 182),
            ImageROI(self.leftOffset,   0, self.leftOffset+self.nPixels,  91)  # free beam
        ]

    def prepare(self):
        """ Preparations before the test will be run with the images from the pipeline. """
        self.prepared = True

    def prepareRun(self, i):
        if i < len(self.subtests):
            if self.subtests[i] in self.validSubtests:
                self.currentIndex = self.validSubtests.index(self.subtests[i])
            else:
                raise Exception("{key} is not a valid subtest identifier for test scenario {test}. Identifiers from the following list are accepted: {valids}.".format(key=self.subtests[i], test=self.testName, valids=self.validSubtests))

            # Load the correct GV reference data into results:
            results = Test2D_SW_1_results()
            results.subtestName = self.subtests[i]

            if self.subtests[i] == "D1_Al_50":
                results.loadReference("07_Al_noFilter_050keV-mono_Ideal")
                results.longName = "Al wedge, 50 keV (mono), ideal detector"
            elif self.subtests[i] == "D1_Al_120":
                results.loadReference("09_Al_noFilter_120kV-poly_Ideal")
                results.longName = "Al wedge, 120 kV (poly), ideal detector"
            elif self.subtests[i] == "D1_Fe_100":
                results.loadReference("11_Fe_noFilter_100kV-mono_Ideal")
                results.longName = "Fe wedge, 100 keV (mono), ideal detector"
            elif self.subtests[i] == "D1_Fe_200":
                results.loadReference("13_Fe_noFilter_200kV-poly_Ideal")
                results.longName = "Fe wedge, 120 kV (poly), ideal detector"

            elif self.subtests[i] == "D2_Al_50":
                results.loadReference("15_Al_noFilter_050keV-mono_200mum-CsI")
                results.longName = "Al wedge, 50 keV (mono), 200 µm CsI"
            elif self.subtests[i] == "D2_Al_120":
                results.loadReference("17_Al_noFilter_120kV-poly_200mum-CsI")
                results.longName = "Al wedge, 120 kV (poly), 200 µm CsI"
            elif self.subtests[i] == "D2_Fe_100":
                results.loadReference("19_Fe_noFilter_100keV-mono_200mum-CsI")
                results.longName = "Fe wedge, 100 keV (mono), 200 µm CsI"
            elif self.subtests[i] == "D2_Fe_200":
                results.loadReference("21_Fe_noFilter_200kV-poly_200mum-CsI")
                results.longName = "Fe wedge, 120 kV (poly), 200 µm CsI"

            elif self.subtests[i] == "D3_Al_50":
                results.loadReference("23_Al_noFilter_050keV-mono_500mum-CsI")
                results.longName = "Al wedge, 50 keV (mono), 500 µm CsI"
            elif self.subtests[i] == "D3_Al_120":
                results.loadReference("25_Al_noFilter_120kV-poly_500mum-CsI")
                results.longName = "Al wedge, 120 kV (poly), 500 µm CsI"
            elif self.subtests[i] == "D3_Fe_100":
                results.loadReference("27_Fe_noFilter_100keV-mono_500mum-CsI")
                results.longName = "Fe wedge, 100 keV (mono), 500 µm CsI"
            elif self.subtests[i] == "D3_Fe_200":
                results.loadReference("29_Fe_noFilter_200kV-poly_500mum-CsI")
                results.longName = "Fe wedge, 120 kV (poly), 500 µm CsI"

            elif self.subtests[i] == "D4_Al_50":
                results.loadReference("31_Al_noFilter_050kV-mono_500mum-GOS")
                results.longName = "Al wedge, 50 keV (mono), 500 µm Gd2O2S"
            elif self.subtests[i] == "D4_Al_120":
                results.loadReference("33_Al_noFilter_120kV-poly_500mum-GOS")
                results.longName = "Al wedge, 120 kV (poly), 500 µm Gd2O2S"
            elif self.subtests[i] == "D4_Fe_100":
                results.loadReference("35_Fe_noFilter_100keV-mono_500mum-GOS")
                results.longName = "Fe wedge, 100 keV (mono), 500 µm Gd2O2S"
            elif self.subtests[i] == "D4_Fe_200":
                results.loadReference("37_Fe_noFilter_200kV-poly_500mum-GOS")
                results.longName = "Fe wedge, 120 kV (poly), 500 µm Gd2O2S"

            self.results[self.currentIndex] = results
        else:
            if len(self.subtests) == 0:
                raise Exception("Please provide keywords that identify which metadata file belongs to which subtest. Test {testname} accepts identifiers from the following list: {valids}.".format(testname=self.testName, valids=self.validSubtests))
            else:
                raise Exception("Number of provided image metadata files exceeds number of test runs ({expected}).".format(expected=len(self.subtests)))

    def run(self, image):
        self.prepare()
        self.prepareRun(self.currentRun)
        i = self.currentIndex
        subtestName = self.subtests[self.currentRun]

        # Grey value summary
        statsText = "# Evaluation of Test {name}, {subname}:\n".format(name=self.name, subname=subtestName)
        statsText += "# {longDesc}\n".format(longDesc=self.results[i].longName)
        statsText += "# \n"
        statsText += "# ROI mean grey value per step\n"
        statsText += "# step\tx0\ty0\tx1\ty1\twidth [px]\theight [px]\tarea [px]\tmean [GV]\n"

        step = 0
        for roi in self.steps:
            step += 1
            smallerROI = copy.deepcopy(roi)
            smallerROI.grow(-self.shrink)
            stats = image.stats(smallerROI)

            statsText += "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:.3f}\n".format(step, smallerROI.x0, smallerROI.y0, smallerROI.x1, smallerROI.y1, stats["width"], stats["height"], stats["area"], stats["mean"])

            self.results[i].means.append(stats["mean"])

        statsFileName = "{dir}/{name}_{subname}_grey_values.txt".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName)
        with open(statsFileName, 'w') as statsFile:
            statsFile.write(statsText)
            statsFile.close()

        self.currentRun += 1
        return image

    def followUp(self):
        # Means
        meansText = "# step"
        for r in range(len(self.results)):
            meansText += "\t{}".format(self.validSubtests[r])

        meansText += "\n"
        for step in range(len(self.steps)):
            meansText += "{}".format(step+1)

            for result in self.results:
                if result is not None:
                    meansText += "\t{:.3f}".format(result.means[step])
                else:
                    meansText += "\t-"

            meansText += "\n"

        meansFileName = "{dir}/{name}_all_GV_means.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(meansFileName, 'w') as meansFile:
            meansFile.write(meansText)
            meansFile.close()


        # Calculate and write ratios to D1 (ideal detector)
        for r in range(len(self.results)):
            result = self.results[r]
            D1result = self.results[r%4]

            if D1result is not None:
                if result is not None:
                    result.ratios_to_D1 = len(self.steps)*[None]
                    result.ratios_to_D1_mc_primary = len(self.steps)*[None]
                    result.ratios_to_D1_mc_total = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_primary_lower = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_primary_upper = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_total_lower = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_total_upper = len(self.steps)*[None]

                    for step in range(len(self.steps)):
                        result.ratios_to_D1[step] = result.means[step] / D1result.means[step]
                        result.ratios_to_D1_mc_primary[step], result.error_ratios_to_D1_mc_primary_upper[step] = divide_and_error(
                                muA = result.means_mc_primary[step],
                                muB = D1result.means_mc_primary[step],
                                errA = result.error_mc_primary_upper[step],
                                errB = D1result.error_mc_primary_upper[step]
                            )
                        result.ratios_to_D1_mc_primary[step], result.error_ratios_to_D1_mc_primary_lower[step] = divide_and_error(
                                muA = result.means_mc_primary[step],
                                muB = D1result.means_mc_primary[step],
                                errA = result.error_mc_primary_lower[step],
                                errB = D1result.error_mc_primary_lower[step]
                            )

                        result.ratios_to_D1_mc_total[step], result.error_ratios_to_D1_mc_total_upper[step] = divide_and_error(
                                muA = result.means_mc_total[step],
                                muB = D1result.means_mc_total[step],
                                errA = result.error_mc_total_upper[step],
                                errB = D1result.error_mc_total_upper[step]
                            )
                        result.ratios_to_D1_mc_total[step], result.error_ratios_to_D1_mc_total_lower[step] = divide_and_error(
                                muA = result.means_mc_total[step],
                                muB = D1result.means_mc_total[step],
                                errA = result.error_mc_total_lower[step],
                                errB = D1result.error_mc_total_lower[step]
                            )

                        if r < 4: # this is an ideal detector, set ratio uncertainty to zero
                            result.error_ratios_to_D1_mc_primary_upper[step] = 0
                            result.error_ratios_to_D1_mc_primary_lower[step] = 0
                            result.error_ratios_to_D1_mc_total_upper[step] = 0
                            result.error_ratios_to_D1_mc_total_lower[step] = 0

        # Measured ratios
        ratiosText = "# step"
        for i in range(len(self.results)):
            ratiosText += "\t{}".format(self.validSubtests[i])

        ratiosText += "\n"
        for step in range(len(self.steps)):
            ratiosText += "{}".format(step+1)
            for r in range(len(self.results)):
                result = self.results[r]
                D1result = self.results[r%4]
                if result is not None:
                    if D1result is not None:
                        ratiosText += "\t{:.5f}".format(result.ratios_to_D1[step])
                    else:
                        ratiosText += "\t-"
                else:
                    ratiosText += "\t-"

            ratiosText += "\n"

        ratiosFileName = "{dir}/{name}_all_GV_ratios_to_Det1.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(ratiosFileName, 'w') as ratiosFile:
            ratiosFile.write(ratiosText)
            ratiosFile.close()


        # Monte-Carlo reference results (only primary radiation accounted for)
        ratiosText = "# step"
        for i in range(len(self.results)):
            ratiosText += "\t{name}\tu_lower({name})\tu_upper({name})".format(name=self.validSubtests[i])

        ratiosText += "\n"
        for step in range(len(self.steps)):
            ratiosText += "{}".format(step+1)
            for r in range(len(self.results)):
                result = self.results[r]
                D1result = self.results[r%4]
                if result is not None:
                    if D1result is not None:
                        ratiosText += "\t{:.5f}".format(result.ratios_to_D1_mc_primary[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_primary_lower[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_primary_upper[step])
                    else:
                        ratiosText += "\t-"
                        ratiosText += "\t-"
                else:
                    ratiosText += "\t-"
                    ratiosText += "\t-"

            ratiosText += "\n"

        ratiosFileName = "{dir}/{name}_all_MCprimary_reference_ratios.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(ratiosFileName, 'w') as ratiosFile:
            ratiosFile.write(ratiosText)
            ratiosFile.close()


        # Monte-Carlo reference results (scatter radiation accounted for)
        ratiosText = "# step"
        for i in range(len(self.results)):
            ratiosText += "\t{name}\tu_lower({name})\tu_upper({name})".format(name=self.validSubtests[i])

        ratiosText += "\n"
        for step in range(len(self.steps)):
            ratiosText += "{}".format(step+1)
            for r in range(len(self.results)):
                result = self.results[r]
                D1result = self.results[r%4]
                if result is not None:
                    if D1result is not None:
                        ratiosText += "\t{:.5f}".format(result.ratios_to_D1_mc_total[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_total_lower[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_total_upper[step])
                    else:
                        ratiosText += "\t-"
                        ratiosText += "\t-"
                else:
                    ratiosText += "\t-"
                    ratiosText += "\t-"

            ratiosText += "\n"

        ratiosFileName = "{dir}/{name}_all_MCtotal_reference_ratios.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(ratiosFileName, 'w') as ratiosFile:
            ratiosFile.write(ratiosText)
            ratiosFile.close()

        self.plotResults()

    def plotResults(self):
        xValues = numpy.linspace(0, len(self.steps), len(self.steps), endpoint=False)
        xLabels = ("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")

        # minor x ticks:
        mxValues = numpy.linspace(0.5, len(self.steps)-0.5, len(self.steps)-1, endpoint=False)

        try:
            import matplotlib
            import matplotlib.pyplot
            from matplotlib.ticker import (MultipleLocator, FormatStrFormatter, AutoMinorLocator)

            matplotlib.use("agg")

            subDetails   = ["Al 50 keV (mono)", "Al 120 kV (poly)", "Fe 100 keV (mono)", "Fe 200 kV (poly)"]
            subDetectors = ["Ideal", "200 µm CsI", "500 µm CsI", "500 µm Gd2O2S"]
            colors = ['#ffb000', '#fe6100', '#dc267f', '#648fff']

            d = 0
            epsilon = numpy.array([-1.5, -0.5, 0.5, 1.5]) * 0.18   # data point spacing
            markers = ["x", "x", "x", "x"]

            for detector in ["Det2", "Det3", "Det4"]:
                d += 1
                subDetector = subDetectors[d]
                dOffset = 4*d

                for mode in ("primary", "total"):
                    fig, ax = matplotlib.pyplot.subplots(nrows=1, ncols=1, figsize=(8, 6))

                    for t in range(0, 4):  # 4 sub-tests per detector
                        subDetail = subDetails[t]

                        r = dOffset + t
                        subtestName = self.results[r].subtestName

                        # Grey value ratios:
                        if mode == "primary":
                            modeDescription = "primary radiation"
                            ax.errorbar(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_primary, xerr=None, yerr=[self.results[r].error_ratios_to_D1_mc_primary_lower, self.results[r].error_ratios_to_D1_mc_primary_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_primary, '_', markersize=11.0, label="{detail}, Monte-Carlo".format(detail=subDetail), color=colors[t])
                        else:
                            modeDescription = "total radiation (scatter+primary)"
                            ax.errorbar(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_total, xerr=None, yerr=[self.results[r].error_ratios_to_D1_mc_total_lower, self.results[r].error_ratios_to_D1_mc_total_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_total, '_', markersize=11.0, label="{detail}, Monte-Carlo".format(detail=subDetail), color=colors[t])

                        ax.plot(xValues+epsilon[t], self.results[r].ratios_to_D1, markers[t], markersize=6.0, label="{detail}, measured".format(detail=subDetail), color=colors[t])

                        """
                        # Grey values:
                        if mode == "primary":
                            modeDescription = "primary radiation"
                            ax.errorbar(xValues+epsilon[t], self.results[r].means_mc_primary, xerr=None, yerr=[self.results[r].error_mc_primary_lower, self.results[r].error_mc_primary_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mc_primary, '_', markersize=7.0, label="{detail}, MC ± 1u".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mcray_primary, 'o', markersize=4.0, label="{detail}, McRay".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_rosi_primary,  'v', markersize=4.0, label="{detail}, ROSI".format(detail=subDetail), color=colors[t])
                        else:
                            modeDescription = "total radiation (scatter+primary)"
                            ax.errorbar(xValues+epsilon[t], self.results[r].means_mc_total, xerr=None, yerr=[self.results[r].error_mc_total_lower, self.results[r].error_mc_total_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mc_total, '_', markersize=7.0, label="{detail}, MC ± 1u".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mcray_total, 'o', markersize=4.0, label="{detail}, McRay".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_rosi_total,  'v', markersize=4.0, label="{detail}, ROSI".format(detail=subDetail), color=colors[t])
                        """


                    ax.set_xlim([-0.5, 9.5])
                    ax.set_xlabel("step")
                    ax.set_ylabel("grey value ratio")
                    ax.set_title(label="2D-SW-1, ratio {detector}/Det1 ({subDet} to ideal), {details}".format(detector=detector, subDet=subDetector, details=modeDescription), loc='left', fontsize=10)

                    ax.set_xticks(xValues, minor=False)
                    ax.set_xticks(mxValues, minor=True)
                    ax.xaxis.set_ticklabels(xLabels)
                    ax.grid(visible=True, which='major', axis='y', color='#d9d9d9', linestyle='dashed')
                    ax.grid(visible=True, which='minor', axis='x', color='#d9d9d9', linestyle='dashed')
                    ax.grid(visible=False, which='major', axis='x')
                    ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=2)

                    ax.tick_params(axis="x", which="major", length=0, color="#ffffff")
                    ax.tick_params(axis="x", which="minor", length=5)

                    fig.tight_layout(pad=2.5)

                    plotFilename = "{dir}/{name}_{detector}_ratios_{mode}.png".format(dir=self.resultFileDirectory, name=self.name, detector=detector, mode=mode)
                    matplotlib.pyplot.savefig(plotFilename)
                    fig.clf()
                    matplotlib.pyplot.close('all')

        except Exception as e:
            log(f"Warning: Error plotting results for test {self.name}, {subtestName} using matplotlib: {e}")

Classes

class Test2D_SW_1 (resultFileDirectory='.', name=None, rawOutput=False)

CTSimU test 2D-SW-1: detector models / scintillators.

Expand source code
class Test2D_SW_1(generalTest):
    """ CTSimU test 2D-SW-1: detector models / scintillators. """

    def __init__(self, resultFileDirectory=".", name=None, rawOutput=False):
        generalTest.__init__(
            self,
            testName="2D-SW-1",
            name=name,
            resultFileDirectory=resultFileDirectory,
            rawOutput=rawOutput)

        self.validSubtests = ["D1_Al_50", "D1_Al_120", "D1_Fe_100", "D1_Fe_200", "D2_Al_50", "D2_Al_120", "D2_Fe_100", "D2_Fe_200", "D3_Al_50", "D3_Al_120", "D3_Fe_100", "D3_Fe_200", "D4_Al_50", "D4_Al_120", "D4_Fe_100", "D4_Fe_200"]

        self.results = len(self.validSubtests)*[None]
        self.currentIndex = None

        self.shrink = 35
        self.leftOffset = 510 - self.shrink
        self.nPixels = 20 + 2*self.shrink

        # Absolute step definitions. Will be shrunk to accept tolerance border,
        # but these absolute definitions are needed for grey value rescaling and clipping.
        self.steps = [
            ImageROI(self.leftOffset, 819, self.leftOffset+self.nPixels, 910),
            ImageROI(self.leftOffset, 728, self.leftOffset+self.nPixels, 819),
            ImageROI(self.leftOffset, 637, self.leftOffset+self.nPixels, 728),
            ImageROI(self.leftOffset, 546, self.leftOffset+self.nPixels, 637),
            ImageROI(self.leftOffset, 455, self.leftOffset+self.nPixels, 546),
            ImageROI(self.leftOffset, 364, self.leftOffset+self.nPixels, 455),
            ImageROI(self.leftOffset, 273, self.leftOffset+self.nPixels, 364),
            ImageROI(self.leftOffset, 182, self.leftOffset+self.nPixels, 273),
            ImageROI(self.leftOffset,  91, self.leftOffset+self.nPixels, 182),
            ImageROI(self.leftOffset,   0, self.leftOffset+self.nPixels,  91)  # free beam
        ]

    def prepare(self):
        """ Preparations before the test will be run with the images from the pipeline. """
        self.prepared = True

    def prepareRun(self, i):
        if i < len(self.subtests):
            if self.subtests[i] in self.validSubtests:
                self.currentIndex = self.validSubtests.index(self.subtests[i])
            else:
                raise Exception("{key} is not a valid subtest identifier for test scenario {test}. Identifiers from the following list are accepted: {valids}.".format(key=self.subtests[i], test=self.testName, valids=self.validSubtests))

            # Load the correct GV reference data into results:
            results = Test2D_SW_1_results()
            results.subtestName = self.subtests[i]

            if self.subtests[i] == "D1_Al_50":
                results.loadReference("07_Al_noFilter_050keV-mono_Ideal")
                results.longName = "Al wedge, 50 keV (mono), ideal detector"
            elif self.subtests[i] == "D1_Al_120":
                results.loadReference("09_Al_noFilter_120kV-poly_Ideal")
                results.longName = "Al wedge, 120 kV (poly), ideal detector"
            elif self.subtests[i] == "D1_Fe_100":
                results.loadReference("11_Fe_noFilter_100kV-mono_Ideal")
                results.longName = "Fe wedge, 100 keV (mono), ideal detector"
            elif self.subtests[i] == "D1_Fe_200":
                results.loadReference("13_Fe_noFilter_200kV-poly_Ideal")
                results.longName = "Fe wedge, 120 kV (poly), ideal detector"

            elif self.subtests[i] == "D2_Al_50":
                results.loadReference("15_Al_noFilter_050keV-mono_200mum-CsI")
                results.longName = "Al wedge, 50 keV (mono), 200 µm CsI"
            elif self.subtests[i] == "D2_Al_120":
                results.loadReference("17_Al_noFilter_120kV-poly_200mum-CsI")
                results.longName = "Al wedge, 120 kV (poly), 200 µm CsI"
            elif self.subtests[i] == "D2_Fe_100":
                results.loadReference("19_Fe_noFilter_100keV-mono_200mum-CsI")
                results.longName = "Fe wedge, 100 keV (mono), 200 µm CsI"
            elif self.subtests[i] == "D2_Fe_200":
                results.loadReference("21_Fe_noFilter_200kV-poly_200mum-CsI")
                results.longName = "Fe wedge, 120 kV (poly), 200 µm CsI"

            elif self.subtests[i] == "D3_Al_50":
                results.loadReference("23_Al_noFilter_050keV-mono_500mum-CsI")
                results.longName = "Al wedge, 50 keV (mono), 500 µm CsI"
            elif self.subtests[i] == "D3_Al_120":
                results.loadReference("25_Al_noFilter_120kV-poly_500mum-CsI")
                results.longName = "Al wedge, 120 kV (poly), 500 µm CsI"
            elif self.subtests[i] == "D3_Fe_100":
                results.loadReference("27_Fe_noFilter_100keV-mono_500mum-CsI")
                results.longName = "Fe wedge, 100 keV (mono), 500 µm CsI"
            elif self.subtests[i] == "D3_Fe_200":
                results.loadReference("29_Fe_noFilter_200kV-poly_500mum-CsI")
                results.longName = "Fe wedge, 120 kV (poly), 500 µm CsI"

            elif self.subtests[i] == "D4_Al_50":
                results.loadReference("31_Al_noFilter_050kV-mono_500mum-GOS")
                results.longName = "Al wedge, 50 keV (mono), 500 µm Gd2O2S"
            elif self.subtests[i] == "D4_Al_120":
                results.loadReference("33_Al_noFilter_120kV-poly_500mum-GOS")
                results.longName = "Al wedge, 120 kV (poly), 500 µm Gd2O2S"
            elif self.subtests[i] == "D4_Fe_100":
                results.loadReference("35_Fe_noFilter_100keV-mono_500mum-GOS")
                results.longName = "Fe wedge, 100 keV (mono), 500 µm Gd2O2S"
            elif self.subtests[i] == "D4_Fe_200":
                results.loadReference("37_Fe_noFilter_200kV-poly_500mum-GOS")
                results.longName = "Fe wedge, 120 kV (poly), 500 µm Gd2O2S"

            self.results[self.currentIndex] = results
        else:
            if len(self.subtests) == 0:
                raise Exception("Please provide keywords that identify which metadata file belongs to which subtest. Test {testname} accepts identifiers from the following list: {valids}.".format(testname=self.testName, valids=self.validSubtests))
            else:
                raise Exception("Number of provided image metadata files exceeds number of test runs ({expected}).".format(expected=len(self.subtests)))

    def run(self, image):
        self.prepare()
        self.prepareRun(self.currentRun)
        i = self.currentIndex
        subtestName = self.subtests[self.currentRun]

        # Grey value summary
        statsText = "# Evaluation of Test {name}, {subname}:\n".format(name=self.name, subname=subtestName)
        statsText += "# {longDesc}\n".format(longDesc=self.results[i].longName)
        statsText += "# \n"
        statsText += "# ROI mean grey value per step\n"
        statsText += "# step\tx0\ty0\tx1\ty1\twidth [px]\theight [px]\tarea [px]\tmean [GV]\n"

        step = 0
        for roi in self.steps:
            step += 1
            smallerROI = copy.deepcopy(roi)
            smallerROI.grow(-self.shrink)
            stats = image.stats(smallerROI)

            statsText += "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:.3f}\n".format(step, smallerROI.x0, smallerROI.y0, smallerROI.x1, smallerROI.y1, stats["width"], stats["height"], stats["area"], stats["mean"])

            self.results[i].means.append(stats["mean"])

        statsFileName = "{dir}/{name}_{subname}_grey_values.txt".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName)
        with open(statsFileName, 'w') as statsFile:
            statsFile.write(statsText)
            statsFile.close()

        self.currentRun += 1
        return image

    def followUp(self):
        # Means
        meansText = "# step"
        for r in range(len(self.results)):
            meansText += "\t{}".format(self.validSubtests[r])

        meansText += "\n"
        for step in range(len(self.steps)):
            meansText += "{}".format(step+1)

            for result in self.results:
                if result is not None:
                    meansText += "\t{:.3f}".format(result.means[step])
                else:
                    meansText += "\t-"

            meansText += "\n"

        meansFileName = "{dir}/{name}_all_GV_means.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(meansFileName, 'w') as meansFile:
            meansFile.write(meansText)
            meansFile.close()


        # Calculate and write ratios to D1 (ideal detector)
        for r in range(len(self.results)):
            result = self.results[r]
            D1result = self.results[r%4]

            if D1result is not None:
                if result is not None:
                    result.ratios_to_D1 = len(self.steps)*[None]
                    result.ratios_to_D1_mc_primary = len(self.steps)*[None]
                    result.ratios_to_D1_mc_total = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_primary_lower = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_primary_upper = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_total_lower = len(self.steps)*[None]
                    result.error_ratios_to_D1_mc_total_upper = len(self.steps)*[None]

                    for step in range(len(self.steps)):
                        result.ratios_to_D1[step] = result.means[step] / D1result.means[step]
                        result.ratios_to_D1_mc_primary[step], result.error_ratios_to_D1_mc_primary_upper[step] = divide_and_error(
                                muA = result.means_mc_primary[step],
                                muB = D1result.means_mc_primary[step],
                                errA = result.error_mc_primary_upper[step],
                                errB = D1result.error_mc_primary_upper[step]
                            )
                        result.ratios_to_D1_mc_primary[step], result.error_ratios_to_D1_mc_primary_lower[step] = divide_and_error(
                                muA = result.means_mc_primary[step],
                                muB = D1result.means_mc_primary[step],
                                errA = result.error_mc_primary_lower[step],
                                errB = D1result.error_mc_primary_lower[step]
                            )

                        result.ratios_to_D1_mc_total[step], result.error_ratios_to_D1_mc_total_upper[step] = divide_and_error(
                                muA = result.means_mc_total[step],
                                muB = D1result.means_mc_total[step],
                                errA = result.error_mc_total_upper[step],
                                errB = D1result.error_mc_total_upper[step]
                            )
                        result.ratios_to_D1_mc_total[step], result.error_ratios_to_D1_mc_total_lower[step] = divide_and_error(
                                muA = result.means_mc_total[step],
                                muB = D1result.means_mc_total[step],
                                errA = result.error_mc_total_lower[step],
                                errB = D1result.error_mc_total_lower[step]
                            )

                        if r < 4: # this is an ideal detector, set ratio uncertainty to zero
                            result.error_ratios_to_D1_mc_primary_upper[step] = 0
                            result.error_ratios_to_D1_mc_primary_lower[step] = 0
                            result.error_ratios_to_D1_mc_total_upper[step] = 0
                            result.error_ratios_to_D1_mc_total_lower[step] = 0

        # Measured ratios
        ratiosText = "# step"
        for i in range(len(self.results)):
            ratiosText += "\t{}".format(self.validSubtests[i])

        ratiosText += "\n"
        for step in range(len(self.steps)):
            ratiosText += "{}".format(step+1)
            for r in range(len(self.results)):
                result = self.results[r]
                D1result = self.results[r%4]
                if result is not None:
                    if D1result is not None:
                        ratiosText += "\t{:.5f}".format(result.ratios_to_D1[step])
                    else:
                        ratiosText += "\t-"
                else:
                    ratiosText += "\t-"

            ratiosText += "\n"

        ratiosFileName = "{dir}/{name}_all_GV_ratios_to_Det1.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(ratiosFileName, 'w') as ratiosFile:
            ratiosFile.write(ratiosText)
            ratiosFile.close()


        # Monte-Carlo reference results (only primary radiation accounted for)
        ratiosText = "# step"
        for i in range(len(self.results)):
            ratiosText += "\t{name}\tu_lower({name})\tu_upper({name})".format(name=self.validSubtests[i])

        ratiosText += "\n"
        for step in range(len(self.steps)):
            ratiosText += "{}".format(step+1)
            for r in range(len(self.results)):
                result = self.results[r]
                D1result = self.results[r%4]
                if result is not None:
                    if D1result is not None:
                        ratiosText += "\t{:.5f}".format(result.ratios_to_D1_mc_primary[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_primary_lower[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_primary_upper[step])
                    else:
                        ratiosText += "\t-"
                        ratiosText += "\t-"
                else:
                    ratiosText += "\t-"
                    ratiosText += "\t-"

            ratiosText += "\n"

        ratiosFileName = "{dir}/{name}_all_MCprimary_reference_ratios.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(ratiosFileName, 'w') as ratiosFile:
            ratiosFile.write(ratiosText)
            ratiosFile.close()


        # Monte-Carlo reference results (scatter radiation accounted for)
        ratiosText = "# step"
        for i in range(len(self.results)):
            ratiosText += "\t{name}\tu_lower({name})\tu_upper({name})".format(name=self.validSubtests[i])

        ratiosText += "\n"
        for step in range(len(self.steps)):
            ratiosText += "{}".format(step+1)
            for r in range(len(self.results)):
                result = self.results[r]
                D1result = self.results[r%4]
                if result is not None:
                    if D1result is not None:
                        ratiosText += "\t{:.5f}".format(result.ratios_to_D1_mc_total[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_total_lower[step])
                        ratiosText += "\t{:.5f}".format(result.error_ratios_to_D1_mc_total_upper[step])
                    else:
                        ratiosText += "\t-"
                        ratiosText += "\t-"
                else:
                    ratiosText += "\t-"
                    ratiosText += "\t-"

            ratiosText += "\n"

        ratiosFileName = "{dir}/{name}_all_MCtotal_reference_ratios.txt".format(dir=self.resultFileDirectory, name=self.name)
        with open(ratiosFileName, 'w') as ratiosFile:
            ratiosFile.write(ratiosText)
            ratiosFile.close()

        self.plotResults()

    def plotResults(self):
        xValues = numpy.linspace(0, len(self.steps), len(self.steps), endpoint=False)
        xLabels = ("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")

        # minor x ticks:
        mxValues = numpy.linspace(0.5, len(self.steps)-0.5, len(self.steps)-1, endpoint=False)

        try:
            import matplotlib
            import matplotlib.pyplot
            from matplotlib.ticker import (MultipleLocator, FormatStrFormatter, AutoMinorLocator)

            matplotlib.use("agg")

            subDetails   = ["Al 50 keV (mono)", "Al 120 kV (poly)", "Fe 100 keV (mono)", "Fe 200 kV (poly)"]
            subDetectors = ["Ideal", "200 µm CsI", "500 µm CsI", "500 µm Gd2O2S"]
            colors = ['#ffb000', '#fe6100', '#dc267f', '#648fff']

            d = 0
            epsilon = numpy.array([-1.5, -0.5, 0.5, 1.5]) * 0.18   # data point spacing
            markers = ["x", "x", "x", "x"]

            for detector in ["Det2", "Det3", "Det4"]:
                d += 1
                subDetector = subDetectors[d]
                dOffset = 4*d

                for mode in ("primary", "total"):
                    fig, ax = matplotlib.pyplot.subplots(nrows=1, ncols=1, figsize=(8, 6))

                    for t in range(0, 4):  # 4 sub-tests per detector
                        subDetail = subDetails[t]

                        r = dOffset + t
                        subtestName = self.results[r].subtestName

                        # Grey value ratios:
                        if mode == "primary":
                            modeDescription = "primary radiation"
                            ax.errorbar(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_primary, xerr=None, yerr=[self.results[r].error_ratios_to_D1_mc_primary_lower, self.results[r].error_ratios_to_D1_mc_primary_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_primary, '_', markersize=11.0, label="{detail}, Monte-Carlo".format(detail=subDetail), color=colors[t])
                        else:
                            modeDescription = "total radiation (scatter+primary)"
                            ax.errorbar(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_total, xerr=None, yerr=[self.results[r].error_ratios_to_D1_mc_total_lower, self.results[r].error_ratios_to_D1_mc_total_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].ratios_to_D1_mc_total, '_', markersize=11.0, label="{detail}, Monte-Carlo".format(detail=subDetail), color=colors[t])

                        ax.plot(xValues+epsilon[t], self.results[r].ratios_to_D1, markers[t], markersize=6.0, label="{detail}, measured".format(detail=subDetail), color=colors[t])

                        """
                        # Grey values:
                        if mode == "primary":
                            modeDescription = "primary radiation"
                            ax.errorbar(xValues+epsilon[t], self.results[r].means_mc_primary, xerr=None, yerr=[self.results[r].error_mc_primary_lower, self.results[r].error_mc_primary_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mc_primary, '_', markersize=7.0, label="{detail}, MC ± 1u".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mcray_primary, 'o', markersize=4.0, label="{detail}, McRay".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_rosi_primary,  'v', markersize=4.0, label="{detail}, ROSI".format(detail=subDetail), color=colors[t])
                        else:
                            modeDescription = "total radiation (scatter+primary)"
                            ax.errorbar(xValues+epsilon[t], self.results[r].means_mc_total, xerr=None, yerr=[self.results[r].error_mc_total_lower, self.results[r].error_mc_total_upper], linewidth=0, elinewidth=2.0, ecolor=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mc_total, '_', markersize=7.0, label="{detail}, MC ± 1u".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_mcray_total, 'o', markersize=4.0, label="{detail}, McRay".format(detail=subDetail), color=colors[t])
                            ax.plot(xValues+epsilon[t], self.results[r].means_rosi_total,  'v', markersize=4.0, label="{detail}, ROSI".format(detail=subDetail), color=colors[t])
                        """


                    ax.set_xlim([-0.5, 9.5])
                    ax.set_xlabel("step")
                    ax.set_ylabel("grey value ratio")
                    ax.set_title(label="2D-SW-1, ratio {detector}/Det1 ({subDet} to ideal), {details}".format(detector=detector, subDet=subDetector, details=modeDescription), loc='left', fontsize=10)

                    ax.set_xticks(xValues, minor=False)
                    ax.set_xticks(mxValues, minor=True)
                    ax.xaxis.set_ticklabels(xLabels)
                    ax.grid(visible=True, which='major', axis='y', color='#d9d9d9', linestyle='dashed')
                    ax.grid(visible=True, which='minor', axis='x', color='#d9d9d9', linestyle='dashed')
                    ax.grid(visible=False, which='major', axis='x')
                    ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=2)

                    ax.tick_params(axis="x", which="major", length=0, color="#ffffff")
                    ax.tick_params(axis="x", which="minor", length=5)

                    fig.tight_layout(pad=2.5)

                    plotFilename = "{dir}/{name}_{detector}_ratios_{mode}.png".format(dir=self.resultFileDirectory, name=self.name, detector=detector, mode=mode)
                    matplotlib.pyplot.savefig(plotFilename)
                    fig.clf()
                    matplotlib.pyplot.close('all')

        except Exception as e:
            log(f"Warning: Error plotting results for test {self.name}, {subtestName} using matplotlib: {e}")

Ancestors

Methods

def prepare(self)

Preparations before the test will be run with the images from the pipeline.

Expand source code
def prepare(self):
    """ Preparations before the test will be run with the images from the pipeline. """
    self.prepared = True
def prepareRun(self, i)
Expand source code
def prepareRun(self, i):
    if i < len(self.subtests):
        if self.subtests[i] in self.validSubtests:
            self.currentIndex = self.validSubtests.index(self.subtests[i])
        else:
            raise Exception("{key} is not a valid subtest identifier for test scenario {test}. Identifiers from the following list are accepted: {valids}.".format(key=self.subtests[i], test=self.testName, valids=self.validSubtests))

        # Load the correct GV reference data into results:
        results = Test2D_SW_1_results()
        results.subtestName = self.subtests[i]

        if self.subtests[i] == "D1_Al_50":
            results.loadReference("07_Al_noFilter_050keV-mono_Ideal")
            results.longName = "Al wedge, 50 keV (mono), ideal detector"
        elif self.subtests[i] == "D1_Al_120":
            results.loadReference("09_Al_noFilter_120kV-poly_Ideal")
            results.longName = "Al wedge, 120 kV (poly), ideal detector"
        elif self.subtests[i] == "D1_Fe_100":
            results.loadReference("11_Fe_noFilter_100kV-mono_Ideal")
            results.longName = "Fe wedge, 100 keV (mono), ideal detector"
        elif self.subtests[i] == "D1_Fe_200":
            results.loadReference("13_Fe_noFilter_200kV-poly_Ideal")
            results.longName = "Fe wedge, 120 kV (poly), ideal detector"

        elif self.subtests[i] == "D2_Al_50":
            results.loadReference("15_Al_noFilter_050keV-mono_200mum-CsI")
            results.longName = "Al wedge, 50 keV (mono), 200 µm CsI"
        elif self.subtests[i] == "D2_Al_120":
            results.loadReference("17_Al_noFilter_120kV-poly_200mum-CsI")
            results.longName = "Al wedge, 120 kV (poly), 200 µm CsI"
        elif self.subtests[i] == "D2_Fe_100":
            results.loadReference("19_Fe_noFilter_100keV-mono_200mum-CsI")
            results.longName = "Fe wedge, 100 keV (mono), 200 µm CsI"
        elif self.subtests[i] == "D2_Fe_200":
            results.loadReference("21_Fe_noFilter_200kV-poly_200mum-CsI")
            results.longName = "Fe wedge, 120 kV (poly), 200 µm CsI"

        elif self.subtests[i] == "D3_Al_50":
            results.loadReference("23_Al_noFilter_050keV-mono_500mum-CsI")
            results.longName = "Al wedge, 50 keV (mono), 500 µm CsI"
        elif self.subtests[i] == "D3_Al_120":
            results.loadReference("25_Al_noFilter_120kV-poly_500mum-CsI")
            results.longName = "Al wedge, 120 kV (poly), 500 µm CsI"
        elif self.subtests[i] == "D3_Fe_100":
            results.loadReference("27_Fe_noFilter_100keV-mono_500mum-CsI")
            results.longName = "Fe wedge, 100 keV (mono), 500 µm CsI"
        elif self.subtests[i] == "D3_Fe_200":
            results.loadReference("29_Fe_noFilter_200kV-poly_500mum-CsI")
            results.longName = "Fe wedge, 120 kV (poly), 500 µm CsI"

        elif self.subtests[i] == "D4_Al_50":
            results.loadReference("31_Al_noFilter_050kV-mono_500mum-GOS")
            results.longName = "Al wedge, 50 keV (mono), 500 µm Gd2O2S"
        elif self.subtests[i] == "D4_Al_120":
            results.loadReference("33_Al_noFilter_120kV-poly_500mum-GOS")
            results.longName = "Al wedge, 120 kV (poly), 500 µm Gd2O2S"
        elif self.subtests[i] == "D4_Fe_100":
            results.loadReference("35_Fe_noFilter_100keV-mono_500mum-GOS")
            results.longName = "Fe wedge, 100 keV (mono), 500 µm Gd2O2S"
        elif self.subtests[i] == "D4_Fe_200":
            results.loadReference("37_Fe_noFilter_200kV-poly_500mum-GOS")
            results.longName = "Fe wedge, 120 kV (poly), 500 µm Gd2O2S"

        self.results[self.currentIndex] = results
    else:
        if len(self.subtests) == 0:
            raise Exception("Please provide keywords that identify which metadata file belongs to which subtest. Test {testname} accepts identifiers from the following list: {valids}.".format(testname=self.testName, valids=self.validSubtests))
        else:
            raise Exception("Number of provided image metadata files exceeds number of test runs ({expected}).".format(expected=len(self.subtests)))

Inherited members

class Test2D_SW_1_results

Results for one sub test of the filtering scenario.

Expand source code
class Test2D_SW_1_results:
    """ Results for one sub test of the filtering scenario. """

    def __init__(self):
        self.subtestName = ""
        self.longName = ""

        # Grey value means and ratios (per wedge step):
        self.means             = []     # Mean value for each step (measured for simulation software)
        self.ratios_to_D1      = None   # grey value ratios to ideal detector (measured for simulation software)

        # Grey values from Monte-Carlo simuation:
        self.means_rosi_total    = None   # accounting for scatter radiation
        self.means_rosi_primary  = None   # accounting only for primary radiation
        self.means_mcray_total    = None   # accounting for scatter radiation
        self.means_mcray_primary  = None   # accounting only for primary radiation


        # Reference values for Monte-Carlo simulation (calculated in loadReference())
        self.means_mc_total    = None   # accounting for scatter radiation
        self.means_mc_primary  = None   # accounting only for primary radiation
        self.error_mc_total_upper   = None
        self.error_mc_total_lower   = None
        self.error_mc_primary_upper = None
        self.error_mc_primary_lower = None

        self.ratios_to_D1_mc_total = None
        self.ratios_to_D1_mc_primary = None
        self.error_ratios_to_D1_mc_total_upper = None
        self.error_ratios_to_D1_mc_total_lower = None
        self.error_ratios_to_D1_mc_primary_upper = None
        self.error_ratios_to_D1_mc_primary_lower = None

    def loadReference(self, name):
        dataText = pkgutil.get_data(__name__, "data/2D-SW-1_scenario{name}.txt".format(name=name)).decode()
        dataIO = io.StringIO(dataText)
        allData = numpy.loadtxt(dataIO, delimiter='\t')  # ignore free beam

        # --- total radiation
        means_rosi_total  = allData[:,1]
        means_mcray_total = allData[:,4]
        means_total = (means_rosi_total + means_mcray_total) * 0.5
        delta_total = numpy.absolute(means_rosi_total - means_mcray_total)

        stddev_rosi_total  = allData[:,2]
        stddev_mcray_total = allData[:,5]

        #err_total = 0.5*(delta_total + numpy.sqrt(numpy.square(stddev_rosi_total) + numpy.square(stddev_mcray_total)))
        err_total_rosi  = numpy.fmax(stddev_rosi_total,  stddev_mcray_total-delta_total) + 0.5*delta_total
        err_total_mcray = numpy.fmax(stddev_mcray_total, stddev_rosi_total-delta_total)  + 0.5*delta_total
        err_total_upper = numpy.zeros_like(err_total_rosi)
        err_total_lower = numpy.zeros_like(err_total_rosi)
        for i in range(len(err_total_upper)):
            if means_rosi_total[i] > means_total[i]:  # ROSI is upper bound
                err_total_upper[i] = err_total_rosi[i]
                err_total_lower[i] = err_total_mcray[i]
            else:   # McRay is upper bound
                err_total_upper[i] = err_total_mcray[i]
                err_total_lower[i] = err_total_rosi[i]


        # --- primary radiation
        means_rosi_primary  = allData[:,9]
        means_mcray_primary = allData[:,12]
        means_primary = (means_rosi_primary + means_mcray_primary) * 0.5
        delta_primary = numpy.absolute(means_rosi_primary - means_mcray_primary)

        stddev_rosi_primary  = allData[:,10]
        stddev_mcray_primary = allData[:,13]

        #err_primary = 0.5*(delta_primary + numpy.sqrt(numpy.square(stddev_rosi_primary) + numpy.square(stddev_mcray_primary)))
        err_primary_rosi  = numpy.fmax(stddev_rosi_primary,  stddev_mcray_primary-delta_primary) + 0.5*delta_primary
        err_primary_mcray = numpy.fmax(stddev_mcray_primary, stddev_rosi_primary-delta_primary)  + 0.5*delta_primary
        err_primary_upper = numpy.zeros_like(err_primary_rosi)
        err_primary_lower = numpy.zeros_like(err_primary_rosi)
        for i in range(len(err_primary_upper)):
            if means_rosi_primary[i] > means_primary[i]:  # ROSI is upper bound
                err_primary_upper[i] = err_primary_rosi[i]
                err_primary_lower[i] = err_primary_mcray[i]
            else:   # McRay is upper bound
                err_primary_upper[i] = err_primary_mcray[i]
                err_primary_lower[i] = err_primary_rosi[i]


        self.means_rosi_total    = means_rosi_total
        self.means_rosi_primary  = means_rosi_primary
        self.means_mcray_total    = means_mcray_total
        self.means_mcray_primary  = means_mcray_primary

        self.means_mc_total         = means_total
        self.means_mc_primary       = means_primary
        self.error_mc_total_upper   = err_total_upper
        self.error_mc_total_lower   = err_total_lower
        self.error_mc_primary_upper = err_primary_upper
        self.error_mc_primary_lower = err_primary_lower

        dataIO.close()

Methods

def loadReference(self, name)
Expand source code
def loadReference(self, name):
    dataText = pkgutil.get_data(__name__, "data/2D-SW-1_scenario{name}.txt".format(name=name)).decode()
    dataIO = io.StringIO(dataText)
    allData = numpy.loadtxt(dataIO, delimiter='\t')  # ignore free beam

    # --- total radiation
    means_rosi_total  = allData[:,1]
    means_mcray_total = allData[:,4]
    means_total = (means_rosi_total + means_mcray_total) * 0.5
    delta_total = numpy.absolute(means_rosi_total - means_mcray_total)

    stddev_rosi_total  = allData[:,2]
    stddev_mcray_total = allData[:,5]

    #err_total = 0.5*(delta_total + numpy.sqrt(numpy.square(stddev_rosi_total) + numpy.square(stddev_mcray_total)))
    err_total_rosi  = numpy.fmax(stddev_rosi_total,  stddev_mcray_total-delta_total) + 0.5*delta_total
    err_total_mcray = numpy.fmax(stddev_mcray_total, stddev_rosi_total-delta_total)  + 0.5*delta_total
    err_total_upper = numpy.zeros_like(err_total_rosi)
    err_total_lower = numpy.zeros_like(err_total_rosi)
    for i in range(len(err_total_upper)):
        if means_rosi_total[i] > means_total[i]:  # ROSI is upper bound
            err_total_upper[i] = err_total_rosi[i]
            err_total_lower[i] = err_total_mcray[i]
        else:   # McRay is upper bound
            err_total_upper[i] = err_total_mcray[i]
            err_total_lower[i] = err_total_rosi[i]


    # --- primary radiation
    means_rosi_primary  = allData[:,9]
    means_mcray_primary = allData[:,12]
    means_primary = (means_rosi_primary + means_mcray_primary) * 0.5
    delta_primary = numpy.absolute(means_rosi_primary - means_mcray_primary)

    stddev_rosi_primary  = allData[:,10]
    stddev_mcray_primary = allData[:,13]

    #err_primary = 0.5*(delta_primary + numpy.sqrt(numpy.square(stddev_rosi_primary) + numpy.square(stddev_mcray_primary)))
    err_primary_rosi  = numpy.fmax(stddev_rosi_primary,  stddev_mcray_primary-delta_primary) + 0.5*delta_primary
    err_primary_mcray = numpy.fmax(stddev_mcray_primary, stddev_rosi_primary-delta_primary)  + 0.5*delta_primary
    err_primary_upper = numpy.zeros_like(err_primary_rosi)
    err_primary_lower = numpy.zeros_like(err_primary_rosi)
    for i in range(len(err_primary_upper)):
        if means_rosi_primary[i] > means_primary[i]:  # ROSI is upper bound
            err_primary_upper[i] = err_primary_rosi[i]
            err_primary_lower[i] = err_primary_mcray[i]
        else:   # McRay is upper bound
            err_primary_upper[i] = err_primary_mcray[i]
            err_primary_lower[i] = err_primary_rosi[i]


    self.means_rosi_total    = means_rosi_total
    self.means_rosi_primary  = means_rosi_primary
    self.means_mcray_total    = means_mcray_total
    self.means_mcray_primary  = means_mcray_primary

    self.means_mc_total         = means_total
    self.means_mc_primary       = means_primary
    self.error_mc_total_upper   = err_total_upper
    self.error_mc_total_lower   = err_total_lower
    self.error_mc_primary_upper = err_primary_upper
    self.error_mc_primary_lower = err_primary_lower

    dataIO.close()