diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f059af8302e6b86f6649c2b685d8d9dd49af159c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +.idea/ + +__pycache__/ + +*.png diff --git a/evaluation.py b/evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..3466330d0f18e0a802b8a6e38078c9cdda2537cf --- /dev/null +++ b/evaluation.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Jan 22 13:57:28 2020 + +@author: luna +""" +from helpers import ParticleBinSorter + +class ResultComparer(object): + + def _get_mp_count_error_per_bin(self, allParticles, subParticles, fractionMeasured): + binSorter = ParticleBinSorter() + allParticlesInBins = binSorter.sort_particles_into_bins(allParticles) + subParticlesInBins = binSorter.sort_particles_into_bins(subParticles) + mpCountErrorsPerBin = [] + for allParticleBin, subParticleBin in zip(allParticlesInBins, subParticlesInBins): + mpCountErrorsPerBin.append(self._get_mp_count_error(allParticleBin, subParticleBin, fractionMeasured)) + return binSorter.bins, mpCountErrorsPerBin + + def _get_mp_count_error(self, allParticles, subParticles, fractionMeasured): + numMPOrig = self._get_number_of_MP_particles(allParticles) + numMPEstimate = self._get_number_of_MP_particles(subParticles) / fractionMeasured + + if numMPOrig != 0: + mpCountError = self._get_error_from_values(numMPOrig, numMPEstimate) + elif numMPEstimate == 0: + mpCountError = 0 + else: + raise Exception #> 0 particles in subsample, whereas none in entire sample. This cannot be! + + return mpCountError + + def _get_error_from_values(self, exact, estimate): + assert(exact != 0) + return abs(exact - estimate) / exact + + def _get_number_of_MP_particles(self, particleList): + mpPatterns = ['poly', 'rubber', 'pb', 'pr', 'pg', 'py', 'pv'] + numMPParticles = 0 + + for particle in particleList: + assignment = particle.getParticleAssignment() + for pattern in mpPatterns: + if assignment.lower().find(pattern) != -1: + numMPParticles += 1 + break + + return numMPParticles \ No newline at end of file diff --git a/geometricOperations.py b/geometricOperations.py new file mode 100644 index 0000000000000000000000000000000000000000..d59cb4eea2aedd93755d0ec9e7cd9417c721fbcc --- /dev/null +++ b/geometricOperations.py @@ -0,0 +1,69 @@ +from methods import SubsamplingMethod + +class CrossBoxSelector(SubsamplingMethod): + def __init__(self, particleContainer, desiredFraction:float = 0.1) -> None: + super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction) + self.fraction = desiredFraction + self.filterWidth: float = 1000 + self.filterHeight: float = 1000 + self.numBoxesAcross: int = 3 #either 3 or 5 + + @property + def boxSize(self) -> float: + assert self.get_maximum_achievable_fraction() >= self.fraction + totalBoxArea: float = ((self.filterWidth*self.filterHeight)*self.fraction) + boxArea: float = totalBoxArea / (2*self.numBoxesAcross - 1) + return boxArea**0.5 + + def get_topLeft_of_boxes(self) -> list: + topLeftCorners: list = [] + boxSize = self.boxSize + xStartCoordinates: list = self._get_horizontal_box_starts(boxSize) + yStartCoordinates: list = self._get_vertical_box_starts(boxSize) + middleXCoordinate: float = xStartCoordinates[self.numBoxesAcross//2] + middleYCoordinate: float = yStartCoordinates[self.numBoxesAcross//2] + + for i in range(self.numBoxesAcross): + topLeftCorners.append((middleXCoordinate, yStartCoordinates[i])) + if i != self.numBoxesAcross//2: + topLeftCorners.append((xStartCoordinates[i], middleYCoordinate)) + + return topLeftCorners + + def get_maximum_achievable_fraction(self) -> float: + """ + Returns the maximum achievable fraction, given the desired number of boxes across. + It is with respect to a rectangular filter of width*height, for circular filter this has to be divided by 0.786 + :return float: + """ + maxBoxWidth: float = self.filterWidth / self.numBoxesAcross + maxBoxHeight: float = self.filterHeight / self.numBoxesAcross + numBoxes: int = 2*self.numBoxesAcross - 1 + totalBoxArea: float = numBoxes * (maxBoxWidth * maxBoxHeight) + return totalBoxArea / (self.filterHeight * self.filterWidth) + + def _get_horizontal_box_starts(self, boxSize: float) -> list: + """ + Returns a list of width-values at which the individual boxes start + :param boxSize: + :return list: + """ + return self._get_box_starts(self.filterWidth, boxSize) + + def _get_vertical_box_starts(self, boxSize: float) -> list: + """ + Returns a list of height-values at which the individual boxes start + :param boxSize: + :return list: + """ + return self._get_box_starts(self.filterHeight, boxSize) + + def _get_box_starts(self, filterSize: float, boxSize: float) -> list: + maxBoxSize: float = filterSize / self.numBoxesAcross + assert maxBoxSize >= boxSize + tileStarts: list = [] + for i in range(self.numBoxesAcross): + start: float = i * filterSize / self.numBoxesAcross + (maxBoxSize - boxSize) / 2 + tileStarts.append(start) + + return tileStarts \ No newline at end of file diff --git a/gui/filterView.py b/gui/filterView.py new file mode 100644 index 0000000000000000000000000000000000000000..8e061159599c50013d2e7b9d25851d3832c69436 --- /dev/null +++ b/gui/filterView.py @@ -0,0 +1,87 @@ +from PyQt5 import QtGui, QtWidgets, QtCore + + +class FilterView(QtWidgets.QGraphicsView): + + def __init__(self): + super(FilterView, self).__init__() + self.setWindowTitle('FilterView') + + scene = QtWidgets.QGraphicsScene(self) + scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex) + scene.setBackgroundBrush(QtCore.Qt.darkGray) + self.setScene(scene) + self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) + self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) + self.setRenderHint(QtGui.QPainter.Antialiasing) + + self.filter = FilterGraphItem() + self.scene().addItem(self.filter) + self.measuringBoxes: list = [] + + def update_measure_boxes(self, viewItems: list) -> None: + self._remove_measure_boxes() + for item in viewItems: + self.measuringBoxes.append(item) + self.scene().addItem(item) + + def _remove_measure_boxes(self) -> None: + for item in self.measuringBoxes: + self.scene().removeItem(item) + self.measuringBoxes = [] + + +class FilterGraphItem(QtWidgets.QGraphicsItem): + """ + The Graphical Representation of the actual filter + """ + def __init__(self, filterWidth:float=500, filterHeight:float=500): + super(FilterGraphItem, self).__init__() + self.width = filterWidth + self.height = filterHeight + self.setPos(0, 0) + self.rect = QtCore.QRectF(0, 0, self.width, self.height) + + def boundingRect(self) -> QtCore.QRectF: + return self.rect + + def paint(self, painter: QtGui.QPainter, option, widget) -> None: + painter.setPen(QtCore.Qt.black) + painter.setBrush(QtCore.Qt.white) + painter.drawRect(self.rect) + + painter.setPen(QtCore.Qt.darkGray) + painter.setBrush(QtCore.Qt.lightGray) + painter.drawEllipse(self.rect) + + +class MeasureBoxGraphItem(QtWidgets.QGraphicsItem): + """ + Displays a box in which particles will be measured + """ + def __init__(self, posX:float=50, posY:float=50, width:float=50, height:float=50) -> None: + super(MeasureBoxGraphItem, self).__init__() + self.posX: float = posX + self.posY: float = posY + self.height: float = height + self.width: float = width + self.setPos(posX, posY) + self.rect = QtCore.QRectF(0, 0, self.width, self.height) + self.setToolTip(f'x0: {round(self.posX)}, y0: {round(self.posY)}, \n' + f'x1: {round(self.posX + self.width)}, y1: {round(self.posY + self.height)}') + + def boundingRect(self) -> QtCore.QRectF: + return self.rect + + def paint(self, painter, option, widget) -> None: + painter.setBrush(QtCore.Qt.green) + painter.setPen(QtCore.Qt.darkGreen) + painter.drawRects(self.rect) + + +if __name__ == '__main__': + import sys + app = QtWidgets.QApplication(sys.argv) + filterView = FilterView() + filterView.show() + ret = app.exec_() \ No newline at end of file diff --git a/gui/mainView.py b/gui/mainView.py new file mode 100644 index 0000000000000000000000000000000000000000..ed5b730dd88ef60984737eb769cdc8b158a643b8 --- /dev/null +++ b/gui/mainView.py @@ -0,0 +1,55 @@ +from PyQt5 import QtWidgets +from filterView import FilterView +from measureModes import CrossBoxMode, CrossBoxesControls + + +class MainView(QtWidgets.QWidget): + def __init__(self): + super(MainView, self).__init__() + + self.setWindowTitle('Subsampling Selector') + self.layout: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) + + self.modeSelector: QtWidgets.QComboBox = QtWidgets.QComboBox() + self.measureModes: dict = {} + self.activeMode = None + self.activeModeControl: QtWidgets.QGroupBox = QtWidgets.QGroupBox() + + self.controlGroup = QtWidgets.QGroupBox() + self.controlGroupLayout = QtWidgets.QHBoxLayout() + self.controlGroup.setLayout(self.controlGroupLayout) + + self.controlGroupLayout.addWidget(QtWidgets.QLabel('Select Subsampling Mode:')) + self.controlGroupLayout.addWidget(self.modeSelector) + self.controlGroupLayout.addWidget(self.activeModeControl) + + self.layout.addWidget(self.controlGroup) + self.filterView = FilterView() + self.layout.addWidget(self.filterView) + self._add_measure_modes() + self._switch_to_default_mode() + + def _add_measure_modes(self) -> None: + self.modeSelector.addItem('crossSelection') + self.measureModes['crossSelection'] = CrossBoxMode(self.filterView) + + def _switch_to_default_mode(self) -> None: + modes: list = list(self.measureModes.keys()) + self._activate_mode(modes[0]) + + def _activate_mode(self, modeName: str) -> None: + self.activeModeControl.setParent(None) + self.controlGroupLayout.removeWidget(self.activeModeControl) + + self.activeMode = self.measureModes[modeName] + self.activeModeControl = self.activeMode.get_control_groupBox() + self.controlGroupLayout.insertWidget(2, self.activeModeControl) + + +if __name__ == '__main__': + import sys + app = QtWidgets.QApplication(sys.argv) + subsampling = MainView() + subsampling.show() + ret = app.exec_() \ No newline at end of file diff --git a/gui/measureModes.py b/gui/measureModes.py new file mode 100644 index 0000000000000000000000000000000000000000..04b3c4070e4faeb4cc21312897e3e31a85bdea57 --- /dev/null +++ b/gui/measureModes.py @@ -0,0 +1,91 @@ +from PyQt5 import QtCore, QtWidgets +# import os, sys, inspect +# currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +# parentdir = os.path.dirname(currentdir) +# sys.path.insert(0, parentdir) +# del currentdir, parentdir +from filterView import FilterView, MeasureBoxGraphItem +from geometricOperations import CrossBoxSelector + + +class MeasureMode(QtCore.QObject): + def __init__(self, relatedFilterView: FilterView): + super(MeasureMode, self).__init__() + self.filterView = relatedFilterView + self.uiControls: QtWidgets.QGroupBox = QtWidgets.QGroupBox() + + def get_control_groupBox(self) -> QtWidgets.QGroupBox: + return self.uiControls + + def update_measure_viewItems(self) -> None: + raise NotImplementedError + + +class CrossBoxMode(MeasureMode): + def __init__(self, *args): + super(CrossBoxMode, self).__init__(*args) + self.uiControls = CrossBoxesControls(self) + self.crossBoxGenerator: CrossBoxSelector = CrossBoxSelector(None) + self.update_measure_viewItems() + + def update_measure_viewItems(self) -> None: + self.crossBoxGenerator.filterHeight = self.filterView.filter.height + self.crossBoxGenerator.filterWidth = self.filterView.filter.width + self.crossBoxGenerator.numBoxesAcross = int(self.uiControls.numBoxesSelector.currentText()) + + desiredCoverage: int = self.uiControls.coverageSpinbox.value() + maxCoverage: int = int(self.crossBoxGenerator.get_maximum_achievable_fraction() * 100) + self.uiControls.set_to_max_possible_coverage(maxCoverage) + if desiredCoverage > maxCoverage: + desiredCoverage = maxCoverage + + self.crossBoxGenerator.fraction = desiredCoverage / 100 + + viewItems = [] + topLefts: list = self.crossBoxGenerator.get_topLeft_of_boxes() + boxSize = self.crossBoxGenerator.boxSize + for (x, y) in topLefts: + newBox: MeasureBoxGraphItem = MeasureBoxGraphItem(x, y, boxSize, boxSize) + viewItems.append(newBox) + + self.filterView.update_measure_boxes(viewItems) + + +class CrossBoxesControls(QtWidgets.QGroupBox): + """ + Gives a groupbox with the controls for setting up the cross boxes. + """ + + def __init__(self, measureModeParent: MeasureMode): + super(CrossBoxesControls, self).__init__() + self.setTitle('Cross Box Controls') + self.measureModeParent = measureModeParent + layout = QtWidgets.QHBoxLayout() + self.setLayout(layout) + + layout.addWidget(QtWidgets.QLabel('Number of Boxes across:')) + self.numBoxesSelector = QtWidgets.QComboBox() + self.numBoxesSelector.addItems([str(3), str(5)]) + self.numBoxesSelector.currentTextChanged.connect(self._config_changed) + layout.addWidget(self.numBoxesSelector) + + layout.addStretch() + + layout.addWidget(QtWidgets.QLabel('Desired Coverage (%)')) + self.coverageSpinbox = QtWidgets.QSpinBox() + self.coverageSpinbox.setFixedWidth(50) + self.coverageSpinbox.setMinimum(0) + self.coverageSpinbox.setMaximum(100) + self.coverageSpinbox.setValue(10) + self.coverageSpinbox.valueChanged.connect(self._config_changed) + layout.addWidget(self.coverageSpinbox) + + def _config_changed(self): + self.measureModeParent.update_measure_viewItems() + + def set_to_max_possible_coverage(self, maxCoverage:int): + self.coverageSpinbox.setMaximum(maxCoverage) + if maxCoverage < self.coverageSpinbox.value(): + self.coverageSpinbox.valueChanged.disconnect() + self.coverageSpinbox.setValue(maxCoverage) + self.coverageSpinbox.valueChanged.connect(self._config_changed) \ No newline at end of file diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..c5a977fcc1753bbb1099582b35bf286b8cd1df46 --- /dev/null +++ b/helpers.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Jan 28 19:32:50 2020 + +@author: luna +""" + +class ParticleBinSorter(object): + def __init__(self): + super(ParticleBinSorter, self).__init__() + self.bins = [5, 10, 20, 50, 100, 200, 500] + + def sort_particles_into_bins(self, particleList): + particlesInBins = self._get_empty_bins() + + for particle in particleList: + binIndex = self._get_binIndex_of_particle(particle) + particlesInBins[binIndex].append(particle) + return particlesInBins + + def _get_empty_bins(self): + return [[] for i in range(len(self.bins)+1)] + + def _get_binIndex_of_particle(self, particle): + size = particle.getParticleSize() + binIndex = 0 + for upperLimit in self.bins: + if size <= upperLimit: + break + else: + binIndex += 1 + return binIndex \ No newline at end of file diff --git a/methods.py b/methods.py new file mode 100644 index 0000000000000000000000000000000000000000..46d9a041ea102d3d859a4d8a9c120106e1432386 --- /dev/null +++ b/methods.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Nov 26 17:42:33 2019 + +@author: brandt +""" +import random +import numpy as np +from helpers import ParticleBinSorter + +class SubsamplingMethod(object): + def __init__(self, particleConatainer, desiredFraction: float = 0.2): + super(SubsamplingMethod, self).__init__() + self.particleContainer = particleConatainer + self.fraction = desiredFraction + + def apply_subsampling_method(self): + raise NotImplementedError + + +class RandomSampling(SubsamplingMethod): + #def __init__(self, particleContainer, desiredFraction=0.1): + #super(RandomSampling, self).__init__(particleContainer) + #self.fraction = desiredFraction + + def apply_subsampling_method(self): + numOrigParticles = len(self.particleContainer.particles) + numParticles = self._get_number_of_random_particles(numOrigParticles) + subParticles = random.sample(self.particleContainer.particles, numParticles) + return self.fraction, subParticles + + def _get_number_of_random_particles(self, numTotalParticles): + return np.int(np.ceil(numTotalParticles * self.fraction)) + + +class IvlevaSubsampling(SubsamplingMethod): + def __init__(self, particleContainer, sigma=1.65, mpFraction=0.01, errorMargin=0.1): + super(IvlevaSubsampling, self).__init__(particleContainer) + self.sigma = sigma + self.estimatedMPFraction = mpFraction + self.errorMargin = errorMargin + + def apply_subsampling_method(self): + N = self.particleContainer.getNumberOfParticles() + numParticlesMeasured = self._get_ivleva_fraction(N) + subParticles = random.sample(self.particleContainer.particles, numParticlesMeasured) + fractionMeasured = numParticlesMeasured/N + return fractionMeasured, subParticles + + def _get_ivleva_fraction(self, N): + P = self.estimatedMPFraction + e = P * self.errorMargin + numParticlesMeasured = np.ceil(P*(1 - P) / (e**2/self.sigma**2 + P*(1-P)/N)) + return np.int(numParticlesMeasured) + + +class SizeBinFractioning(SubsamplingMethod): + def __init__(self, particleConatiner, desiredfraction: float = 0.2): + super(SizeBinFractioning, self).__init__(particleConatiner, desiredfraction) + self.sorter: ParticleBinSorter = ParticleBinSorter() + + def apply_subsampling_method(self): + subParticlesPerBin: list = self._get_subParticles_per_bin(self.particleContainer.particles) + subParticles: list = [] + for subParticleList in subParticlesPerBin: + for particle in subParticleList: + subParticles.append(particle) + return self.fraction, subParticles + + def _get_subParticles_per_bin(self, particleList: list): + particlesInBins: list = self.sorter.sort_particles_into_bins(particleList) + subParticlesPerBin: list = [] + for particles in particlesInBins: + numParticlesInBin: int = len(particles) + numSubParticlesPerBin:int = np.int(np.round(numParticlesInBin * self.fraction)) + if numSubParticlesPerBin == 0 and numParticlesInBin > 0: + numSubParticlesPerBin = 1 + + subParticlesInBin: list = random.sample(particles, numSubParticlesPerBin) + subParticlesPerBin.append(subParticlesInBin) + + return subParticlesPerBin + + +class SelectSquaresFromFilter(SubsamplingMethod): + def __init__(self, particleContainer, desiredFraction): + super(SelectSquaresFromFilter, self).__init__(particleContainer) + self.fraction = desiredFraction + diff --git a/subsampling.py b/subsampling.py new file mode 100644 index 0000000000000000000000000000000000000000..2323e2ebf7d78f30409049ef63706eb5a035d780 --- /dev/null +++ b/subsampling.py @@ -0,0 +1,76 @@ +import numpy as np +import matplotlib.pyplot as plt +import time +import sys +sys.path.append("C://Users//xbrjos//Desktop//Python") +from gepard import dataset + +from methods import IvlevaSubsampling, RandomSampling, SizeBinFractioning +from helpers import ParticleBinSorter +from evaluation import ResultComparer + +fname: str = r'C:\Users\xbrjos\Desktop\temp MP\190313_Soil_5_A_50_5_1_50_1\190313_Soil_5_A_50_5_1_50_1.pkl' +# fname: str = r'C:\Users\xbrjos\Desktop\temp MP\190326_MCII_WWTP_SB_50_2\190326_MCII_WWTP_SB_50_2.pkl' +# fname: str = r'C:\Users\xbrjos\Desktop\temp MP\190326_MCII_WWTP_SB_50_1\190326_MCII_WWTP_SB_50_1.pkl' +# fname: str = r'C:\Users\xbrjos\Desktop\temp MP\KWS_CT_3_ds1_all_10_2\KWS_CT_3_ds1_all_10_2.pkl' #legacy convert not working.. +# fname: str = r'C:\Users\xbrjos\Desktop\temp MP\190201_BSB_Stroomi_ds2_R1_R2_50\190201_BSB_Stroomi_ds2_R1_R2_50.pkl' #zvalues image missing, legacy convert fails.. + + +dset = dataset.loadData(fname) +print('loaded dataset') + +pc = dset.particleContainer +origParticles = pc.particles + +resultComparer = ResultComparer() +numOrigMP = resultComparer._get_number_of_MP_particles(origParticles) +print(f'orig particles: {len(origParticles)}, of which are mp: {numOrigMP}') +# ivlevaSampling = IvlevaSubsampling(pc) +# ivlevaFraction, ivlevaParticles = ivlevaSampling.apply_subsampling_method() + +t0 = time.time() +fractions = np.arange(0.05, .55, 0.05) +errors = [] +binErrors = [] +numIterations = 1000 + +for fraction in fractions: + print('random sampling, fraction:', fraction) +# randomSampling = RandomSampling(pc, desiredFraction=fraction) + randomSampling = SizeBinFractioning(pc, fraction) + iterErrors = [] + binIterErrors = [] + for _ in range(numIterations): + randomFraction, randomParticles = randomSampling.apply_subsampling_method() + iterErrors.append(resultComparer._get_mp_count_error(origParticles, randomParticles, randomFraction)) + bins, errorsPerBin = resultComparer._get_mp_count_error_per_bin(origParticles, randomParticles, randomFraction) + binIterErrors.append(errorsPerBin) + + errors.append(round(np.mean(iterErrors)*100)) #from fraction to % + fractionBinErrors = [] + for binIndex in range(len(bins)+1): + binError = round(np.mean([binIterErrors[i][binIndex] for i in range(numIterations)]) * 100) + fractionBinErrors.append(binError) + binErrors.append(fractionBinErrors) + +print('random sampling took', np.round(time.time()-t0, 2), 'seonds') +binLowerLimits = bins.copy() +binLowerLimits.insert(0, 0) +plt.subplot(121) +plt.plot(fractions, errors) +# plt.title(f'Random Sampling, averaged from {numIterations} trials, orig particle count: {len(origParticles)}') +plt.xlabel('Fraction measured') +plt.ylabel('Average error in MP particle count (%)') + +plt.subplot(122) +for fracMeas, curBinErrors in zip(fractions, binErrors): + plt.plot(binLowerLimits, curBinErrors, label=np.round(fracMeas, 1)) +# plt.title('Error in MP count (%) per size bin') +plt.xlabel('particle size') +plt.ylabel('Average error in MP particle count (%)') +plt.legend() +plt.show() + + +# sizeBinSampling = SizeBinFractioning(pc) +# sizeBinParticles = sizeBinSampling.apply_subsampling_method() \ No newline at end of file diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..f63454b1b7c8796da5af9477b854a7e1465a2b09 --- /dev/null +++ b/tests/test_evaluation.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Jan 22 13:58:25 2020 + +@author: luna +""" + +import unittest +import random +import sys +sys.path.append("C://Users//xbrjos//Desktop//Python") +import gepard +from gepard.analysis.particleAndMeasurement import Particle, Measurement + +from evaluation import ResultComparer + +class TestResultComparer(unittest.TestCase): + def setUp(self): + self.resultComparer = ResultComparer() + + def test_get_error_per_bin(self): + def get_full_and_sub_particles(particleSizes, numParticlesPerSizeFull, numParticlesPerSizeSub): + fullParticles = [] + subParticles = [] + for particleSize in particleSizes: + for _ in range(numParticlesPerSizeFull): + mpParticle = self._get_MP_particle() + mpParticle.longSize = mpParticle.shortSize = particleSize + fullParticles.append(mpParticle) + + for _ in range(numParticlesPerSizeSub): + mpParticle = self._get_MP_particle() + mpParticle.longSize = mpParticle.shortSize = particleSize + subParticles.append(mpParticle) + + return fullParticles, subParticles + + binSizes = [5, 10, 20, 50, 100, 200, 500] + particleSizes = [upperLimit - 1 for upperLimit in binSizes] + + numParticlesPerSizeFull = 20 + numParticlesPerSizeSub = 10 + fullParticles, subParticles = get_full_and_sub_particles(particleSizes, numParticlesPerSizeFull, numParticlesPerSizeSub) + + #assume everything was measured + bins, mpCountErrorsPerBin = self.resultComparer._get_mp_count_error_per_bin(fullParticles, subParticles, 1.) + + for binIndex, binError in enumerate(mpCountErrorsPerBin): + if binIndex <= 6: + self.assertEqual(binError, 0.5) + else: #it's the last and largest bin, no particles where added there + self.assertEqual(binError, 0) + + #assume only 50 % was measured + bins, mpCountErrorsPerBin = self.resultComparer._get_mp_count_error_per_bin(fullParticles, subParticles, 0.5) + + for binIndex, binError in enumerate(mpCountErrorsPerBin): + self.assertEqual(binError, 0) + + + def test_get_number_of_MP_particles(self): + mpParticles = self._get_MP_particles(5) + numMPParticles = len(mpParticles) + + nonMPparticles = self._get_non_MP_particles(50) + + allParticles = mpParticles + nonMPparticles + + calculatedNumMPParticles = self.resultComparer._get_number_of_MP_particles(allParticles) + self.assertEqual(numMPParticles, calculatedNumMPParticles) + + def test_get_mp_count_error(self): + mpParticles1 = self._get_MP_particles(20) + nonMPparticles1 = self._get_non_MP_particles(20) + origParticles = mpParticles1 + nonMPparticles1 + + mpParticles2 = self._get_MP_particles(30) + estimateParticles = mpParticles2 + nonMPparticles1 + mpCountError = self.resultComparer._get_mp_count_error(origParticles, estimateParticles, 1.0) + self.assertEqual(mpCountError, 0.5) + + mpParticles2 = self._get_MP_particles(20) + estimateParticles = mpParticles2 + nonMPparticles1 + mpCountError = self.resultComparer._get_mp_count_error(origParticles, estimateParticles, 1.0) + self.assertEqual(mpCountError, 0) + + mpCountError = self.resultComparer._get_mp_count_error(origParticles, estimateParticles, 0.5) + self.assertEqual(mpCountError, 1.0) + + def test_get_error_from_values(self): + exact, estimate = 100, 90 + error = self.resultComparer._get_error_from_values(exact, estimate) + self.assertEqual(error, 0.1) + + exact, estimate = 100, 110 + error = self.resultComparer._get_error_from_values(exact, estimate) + self.assertEqual(error, 0.1) + + exact, estimate = 100, 50 + error = self.resultComparer._get_error_from_values(exact, estimate) + self.assertEqual(error, 0.5) + + exact, estimate = 100, 150 + error = self.resultComparer._get_error_from_values(exact, estimate) + self.assertEqual(error, 0.5) + + def _get_MP_particles(self, numParticles): + mpParticles = [] + for _ in range(numParticles): + mpParticles.append(self._get_MP_particle()) + return mpParticles + + def _get_non_MP_particles(self, numParticles): + nonMPParticles = [] + for _ in range(numParticles): + nonMPParticles.append(self._get_non_MP_particle()) + return nonMPParticles + + def _get_MP_particle(self): + polymerNames = ['Poly (methyl methacrylate', + 'Polyethylene', + 'Silicone rubber', + 'PB15', + 'PY13', + 'PR20'] + polymName = random.sample(polymerNames, 1)[0] + newParticle = Particle() + newMeas = Measurement() + newMeas.setAssignment(polymName) + newParticle.addMeasurement(newMeas) + return newParticle + + def _get_non_MP_particle(self): + newParticle = Particle() + newParticle.addMeasurement(Measurement()) + return newParticle + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_geometricOperations.py b/tests/test_geometricOperations.py new file mode 100644 index 0000000000000000000000000000000000000000..bdcb751e2b376f31cfc9701a6f4faf7edc7d83e5 --- /dev/null +++ b/tests/test_geometricOperations.py @@ -0,0 +1,62 @@ +import unittest +from geometricOperations import CrossBoxSelector + +class TestSelectBoxes(unittest.TestCase): + def setUp(self) -> None: + self.crossBoxSelector = CrossBoxSelector(None) + + def test_get_topLeft_of_boxes(self): + self.crossBoxSelector.filterWidth = self.crossBoxSelector.filterHeight = 100 + self.crossBoxSelector.fraction = 0.1 + + self.crossBoxSelector.numBoxesAcross = 3 + topLeftCorners:list = self.crossBoxSelector.get_topLeft_of_boxes() + self.assertEqual(len(topLeftCorners), 5) + + self.crossBoxSelector.numBoxesAcross = 5 + topLeftCorners = self.crossBoxSelector.get_topLeft_of_boxes() + self.assertEqual(len(topLeftCorners), 9) + + def test_get_tile_topLefts(self): + self.crossBoxSelector.filterHeight = self.crossBoxSelector.filterWidth = 100 + self.crossBoxSelector.numBoxesAcross = 3 + maxBoxSize: float = self.crossBoxSelector.filterWidth/self.crossBoxSelector.numBoxesAcross + horizontalTileStarts: list = self.crossBoxSelector._get_horizontal_box_starts(maxBoxSize) + verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize) + self.assertEqual(horizontalTileStarts, [0, 100 / 3, 2 * 100 / 3]) + self.assertEqual(horizontalTileStarts, verticalTileStarts) + + halfBoxSize: float = maxBoxSize / 2 + horizontalTileStarts: list = self.crossBoxSelector._get_horizontal_box_starts(halfBoxSize) + verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(halfBoxSize) + expectedOffset = (maxBoxSize-halfBoxSize) / 2 + self.assertEqual(horizontalTileStarts, [0 + expectedOffset, 100/3 + expectedOffset, 2*100/3 + expectedOffset]) + self.assertEqual(horizontalTileStarts, verticalTileStarts) + + self.crossBoxSelector.numBoxesAcross = 5 + maxBoxSize = self.crossBoxSelector.filterWidth / self.crossBoxSelector.numBoxesAcross + horizontalTileStarts = self.crossBoxSelector._get_horizontal_box_starts(maxBoxSize) + verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize) + self.assertEqual(horizontalTileStarts, [0, 100/5, 2*100/5, 3*100/5, 4*100/5]) + self.assertEqual(horizontalTileStarts, verticalTileStarts) + + halfBoxSize: float = maxBoxSize / 2 + horizontalTileStarts: list = self.crossBoxSelector._get_horizontal_box_starts(halfBoxSize) + verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(halfBoxSize) + expectedOffset = (maxBoxSize - halfBoxSize) / 2 + self.assertEqual(horizontalTileStarts, [0+expectedOffset, 100/5+expectedOffset, 2*100/5+expectedOffset, 3*100/5+expectedOffset, 4*100/5+expectedOffset]) + self.assertEqual(horizontalTileStarts, verticalTileStarts) + + def test_get_box_size(self) -> None: + self.crossBoxSelector.filterHeight = self.crossBoxSelector.filterWidth = 100 # in pixel + self.crossBoxSelector.fraction = 0.1 + self.crossBoxSelector.numBoxesAcross = 3 + + filterArea: float = self.crossBoxSelector.filterHeight * self.crossBoxSelector.filterWidth + totalBoxArea: float = filterArea*self.crossBoxSelector.fraction + + numBoxes: int = 2*self.crossBoxSelector.numBoxesAcross - 1 + areaPerBox: float = totalBoxArea / numBoxes + boxSize: float = areaPerBox**0.5 + + self.assertEqual(self.crossBoxSelector.boxSize, boxSize) \ No newline at end of file diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 0000000000000000000000000000000000000000..1faef6051461de5fb02d39a0c494a31ab7487127 --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,14 @@ +import unittest +from PyQt5.QtWidgets import QApplication +import sys + +app = QApplication(sys.argv) + + +class TestFilterViewGUI(unittest.TestCase): + def setUp(self) -> None: + pass + + def test_gui(self): + #NEEDS TO BE IMPLEMENTED, too much was refactored to make the old test pass... + self.assertTrue(False) \ No newline at end of file diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..02038a6cb36b7fbc8d676bd690fc424a2e9bb56f --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Jan 28 19:35:04 2020 + +@author: luna +""" + +import unittest +from helpers import ParticleBinSorter +import sys +sys.path.append("C://Users//xbrjos//Desktop//Python") +import gepard +from gepard.analysis.particleAndMeasurement import Particle + +class TestBinSorter(unittest.TestCase): + def setUp(self): + self.sorter = ParticleBinSorter() + self.bins = self.sorter.bins + + def test_sort_particles_into_bins(self): + particleList = [] + for upperBinLimit in self.bins: + newParticle = Particle() + newParticle.longSize = newParticle.shortSize = upperBinLimit - 1 + particleList.append(newParticle) + lastParticle = Particle() + lastParticle.longSize = lastParticle.shortSize = upperBinLimit + 1 + particleList.append(lastParticle) + + particlesInBins = self.sorter.sort_particles_into_bins(particleList) + self.assertEqual(len(particlesInBins), len(self.bins)+1) + for binContent in particlesInBins: + self.assertEqual(len(binContent), 1) + + def test_get_empty_bins(self): + emptyBins = self.sorter._get_empty_bins() + self.assertEqual(len(emptyBins), len(self.bins)+1) + + def test_get_binIndex_of_particle(self): + particle = Particle() + particle.longSize = particle.shortSize = 0 + binIndex = self.sorter._get_binIndex_of_particle(particle) + self.assertEqual(binIndex, 0) + + particle.longSize = particle.shortSize = 5 + binIndex = self.sorter._get_binIndex_of_particle(particle) + self.assertEqual(binIndex, 0) + + particle.longSize = particle.shortSize = 5.01 + binIndex = self.sorter._get_binIndex_of_particle(particle) + self.assertEqual(binIndex, 1) + + particle.longSize = particle.shortSize = 100 + binIndex = self.sorter._get_binIndex_of_particle(particle) + self.assertEqual(binIndex, 4) + + particle.longSize = particle.shortSize = 1000 + binIndex = self.sorter._get_binIndex_of_particle(particle) + self.assertEqual(binIndex, 7) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_methods.py b/tests/test_methods.py new file mode 100644 index 0000000000000000000000000000000000000000..1c2123a5ccbc3e10748a8e8fc04a3476577b0fa6 --- /dev/null +++ b/tests/test_methods.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Jan 21 12:54:58 2020 + +@author: luna +""" + +import unittest +import sys +sys.path.append("C://Users//xbrjos//Desktop//Python") +import numpy as np +import gepard +from gepard.analysis.particleContainer import ParticleContainer +from gepard.analysis.particleAndMeasurement import Particle +from methods import IvlevaSubsampling, RandomSampling, SizeBinFractioning +from helpers import ParticleBinSorter + +def get_default_particle_container(numParticles=1000): + particleContainer = ParticleContainer(None) + particleContainer.initializeParticles(numParticles) + return particleContainer + +class TestIvleva(unittest.TestCase): + def test_get_ivleva_fraction(self): + self.particleContainer = get_default_particle_container() + + ivlevaSampling = IvlevaSubsampling(self.particleContainer, sigma=1.65, mpFraction=0.05, errorMargin=0.1) + numParticles = ivlevaSampling._get_ivleva_fraction(1E6) + self.assertEqual(numParticles, 5147) + + ivlevaSampling = IvlevaSubsampling(self.particleContainer, sigma=1.65, mpFraction=0.005, errorMargin=0.1) + numParticles = ivlevaSampling._get_ivleva_fraction(1E6) + self.assertEqual(numParticles, 51394) + + ivlevaSampling = IvlevaSubsampling(self.particleContainer, sigma=1.65, mpFraction=0.0005, errorMargin=0.1) + numParticles = ivlevaSampling._get_ivleva_fraction(1E6) + self.assertEqual(numParticles, 352428) + + ivlevaSampling = IvlevaSubsampling(self.particleContainer, sigma=1.65, mpFraction=0.05, errorMargin=0.1) + numParticles = ivlevaSampling._get_ivleva_fraction(1000) + ivlevaFraction, ivlevaParticles = ivlevaSampling.apply_subsampling_method() + self.assertEqual(len(ivlevaParticles), numParticles) + + +class TestRandomParticles(unittest.TestCase): + def test_get_number_of_random_particles(self): + randomSampling = RandomSampling(None, desiredFraction=0.1) + numParticles = randomSampling._get_number_of_random_particles(1000) + self.assertEqual(numParticles, 100) + + numParticles = randomSampling._get_number_of_random_particles(10000) + self.assertEqual(numParticles, 1000) + + randomSampling = RandomSampling(None, desiredFraction=0.5) + numParticles = randomSampling._get_number_of_random_particles(1000) + self.assertEqual(numParticles, 500) + + +class TestSizeBinFractioning(unittest.TestCase): + def setUp(self): + self.sizeBinFrac = SizeBinFractioning(None) + sorter = ParticleBinSorter() + sizes = [limit-1 for limit in sorter.bins] + sizes.append(sorter.bins[-1]+1) #the last bin, that goes until infinity + + self.numMPparticlesPerBin = 10 + self.particles = [] + + for size in sizes: + for _ in range(self.numMPparticlesPerBin): + newParticle = Particle() + newParticle.longSize = newParticle.shortSize = size + self.particles.append(newParticle) + + def test_get_num_subParticles_per_bin(self): + for fraction in [0.01, 0.1, 0.2, 0.5, 0.95, 0.99]: + self.sizeBinFrac.fraction = fraction + numParticlesPerBinExpected: int = np.round(self.sizeBinFrac.fraction * self.numMPparticlesPerBin) + if numParticlesPerBinExpected == 0: + numParticlesPerBinExpected = 1 + + subParticlesPerBin: list = self.sizeBinFrac._get_subParticles_per_bin(self.particles) + for subParticles in subParticlesPerBin: + self.assertEqual(len(subParticles), numParticlesPerBinExpected) + + + + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file