Commit d6dc1dec authored by Josef Brandt's avatar Josef Brandt

Merge branch 'ResultGeneration'

parents 064d26eb c509aa9c
......@@ -4,3 +4,5 @@
__pycache__/
*.png
*.res
......@@ -5,12 +5,226 @@ Created on Wed Jan 22 13:57:28 2020
@author: luna
"""
import pickle
import sys
import os
import numpy as np
import matplotlib.pyplot as plt
from helpers import ParticleBinSorter
import methods as meth
import geometricMethods as gmeth
sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset
def get_name_from_directory(dirPath: str) -> str:
return str(os.path.basename(dirPath).split('.')[0])
class TotalResults(object):
methods: list = [meth.RandomSampling, meth.SizeBinFractioning, gmeth.CrossBoxSubSampling,
gmeth.SpiralBoxSubsampling]
measuredFreactions: list = [0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 0.9]
def __init__(self):
super(TotalResults, self).__init__()
self.sampleResults: list = []
def add_sample(self, filePath: str):
"""
Adds a new sampleResult object, if a .pkl file is given and if the sample name is not already present.
:param filePath:
:return:
"""
newResult: SampleResult = None
sampleName: str = get_name_from_directory(filePath)
presentSampleNames: list = [res.sampleName for res in self.sampleResults]
if sampleName not in presentSampleNames:
if os.path.basename(filePath).split('.')[-1] == 'pkl':
newResult = SampleResult(filePath)
self.sampleResults.append(newResult)
return newResult
def update_all(self) -> None:
"""
Updates all samples with all methods and all fractions
:return:
"""
for index, sample in enumerate(self.sampleResults):
sample.load_dataset()
for fraction in self.measuredFreactions:
possibleMethods = self._get_methods_for_fraction(sample.dataset, fraction)
for curMethod in possibleMethods:
# print(f'updating {sample.sampleName} with {curMethod.label} at fraction {fraction}')
sample.update_result_with_method(curMethod)
print(f'processed {index+1} of {len(self.sampleResults)} samples')
def get_error_vs_fraction_data(self, attributes: list = [], methods: list = []) -> dict:
"""
Returns Dict: Key: Method Label, Value: (Dict: Key:Measured Fraction, Value: averaged MPCountError over all samples)
:param attributes: A list of attributes that should be used for filtering the samples. Only samples with an
attribute from within that list are considered.
:return:
"""
result: dict = {}
for sample in self.sampleResults:
sample: SampleResult = sample
if attributes == [] or sample.has_any_attribute(attributes):
for res in sample.results:
res: SubsamplingResult = res
method: meth.SubsamplingMethod = res.method
if methods == [] or method.matches_any_pattern(methods):
label: str = method.label
frac: float = method.fraction
error: float = res.mpCountError
if label not in result.keys():
result[label] = {frac: [error]}
elif frac not in result[label].keys():
result[label][frac] = [error]
else:
result[label][frac].append(error)
for method in result.keys():
methodRes: dict = result[method]
for fraction in methodRes.keys():
methodRes[fraction] = np.mean(methodRes[fraction])
return result
def _get_methods_for_fraction(self, dataset: dataset.DataSet, fraction: float) -> list:
"""
:param fraction: The desired fraction to measure
:return: list of measurement Objects that are applicable
"""
particleContainer = dataset.particleContainer
methods: list = [meth.RandomSampling(particleContainer, fraction),
meth.SizeBinFractioning(particleContainer, fraction)]
boxCreator: gmeth.BoxSelectionCreator = gmeth.BoxSelectionCreator(dataset)
methods += boxCreator.get_crossBoxSubsamplers_for_fraction(fraction)
methods += boxCreator.get_spiralBoxSubsamplers_for_fraction(fraction)
return methods
class SampleResult(object):
"""
An object the actually stores all generated results per sample and can update and report on them.
"""
def __init__(self, filepath: str):
super(SampleResult, self).__init__()
self.filepath: str = filepath
self.dataset: dataset.DataSet = None
self.results: list = []
self.attributes: list = []
@property
def sampleName(self) -> str:
return get_name_from_directory(self.filepath)
def load_dataset(self) -> None:
self.dataset = dataset.loadData(self.filepath)
assert self.dataset is not None
def update_result_with_method(self, method: meth.SubsamplingMethod, force: bool = False) -> None:
"""
Updates result with the given method (contains desiredFraction already)
:param method: The SubsamplingMethod Object
:param force: Wether to force an update. If False, the result is not updated, if it is already present.
:return:
"""
if not self._result_is_already_present(method) or force:
if force:
self._remove_result_of_method(method)
if self.dataset is None:
self.load_dataset()
method.particleContainer = self.dataset.particleContainer
newResult: SubsamplingResult = SubsamplingResult(method)
self.results.append(newResult)
newResult.update()
def set_attribute(self, newAttribute: str) -> None:
"""
Adds a new attribute to the sample, if it does not contain the attribute already
:param newAttribute:
:return:
"""
if not self.has_attribute(newAttribute):
self.attributes.append(newAttribute)
print(f'sample {self.filepath} has now attribute {newAttribute}')
def has_any_attribute(self, listOfAttributes: list) -> bool:
hasAttr: bool = False
for attr in listOfAttributes:
if self.has_attribute(attr):
hasAttr = True
break
return hasAttr
def has_attribute(self, attribute: str) -> bool:
attributes: list = [attr.lower() for attr in self.attributes]
return attribute.lower() in attributes
def _remove_result_of_method(self, method: meth.SubsamplingMethod) -> None:
"""
Removes the specified result from the list
:param method:
:return:
"""
for result in self.results:
if method.equals(result.method):
self.results.remove(result)
def _result_is_already_present(self, method: meth.SubsamplingMethod) -> bool:
"""
Checks, if a result with the given method (method type AND measured fraction) is already present.
:param method: The method object, specifying the subsampling method and the measured fraction
:return:
"""
isPresent: bool = False
for result in self.results:
if method.equals(result.method):
isPresent = True
break
return isPresent
class SubsamplingResult(object):
"""
Stores all interesting results from a subsampling experiment
"""
def __init__(self, subsamplingMethod: meth.SubsamplingMethod):
super(SubsamplingResult, self).__init__()
self.method: meth.SubsamplingMethod = subsamplingMethod
self.fraction = self.method.fraction
self.origParticleCount: int = None
self.subSampledParticleCount: int = None
self.mpCountError: float = None
self.mpCountErrorPerBin: tuple = None
def update(self) -> None:
"""
Updates all results from the method.
:return:
"""
assert self.method.particleContainer is not None
origParticles: list = self.method.particleContainer.particles
self.origParticleCount = len(origParticles)
subParticles: list = self.method.apply_subsampling_method()
self.subSampledParticleCount = len(subParticles)
fraction: float = self.method.fraction
class ResultComparer(object):
self.mpCountError = self._get_mp_count_error(origParticles, subParticles, fraction)
# print(f'{self.origParticleCount} particles, thereof {self.subSampledParticleCount} measured, error: {self.mpCountError}')
self.mpCountErrorPerBin = self._get_mp_count_error_per_bin(origParticles, subParticles, fraction)
# print(f'method {self.method.label} updated, result is {self.mpCountError}')
def _get_mp_count_error_per_bin(self, allParticles, subParticles, fractionMeasured):
def _get_mp_count_error_per_bin(self, allParticles: list, subParticles: list, fractionMeasured: float) -> tuple:
binSorter = ParticleBinSorter()
allParticlesInBins = binSorter.sort_particles_into_bins(allParticles)
subParticlesInBins = binSorter.sort_particles_into_bins(subParticles)
......@@ -19,7 +233,7 @@ class ResultComparer(object):
mpCountErrorsPerBin.append(self._get_mp_count_error(allParticleBin, subParticleBin, fractionMeasured))
return binSorter.bins, mpCountErrorsPerBin
def _get_mp_count_error(self, allParticles, subParticles, fractionMeasured):
def _get_mp_count_error(self, allParticles: list, subParticles: list, fractionMeasured: float) -> float:
numMPOrig = self._get_number_of_MP_particles(allParticles)
numMPEstimate = self._get_number_of_MP_particles(subParticles) / fractionMeasured
......@@ -32,11 +246,11 @@ class ResultComparer(object):
return mpCountError
def _get_error_from_values(self, exact, estimate):
def _get_error_from_values(self, exact: float, estimate: float) -> float:
assert(exact != 0)
return abs(exact - estimate) / exact
def _get_number_of_MP_particles(self, particleList):
def _get_number_of_MP_particles(self, particleList: list) -> int:
mpPatterns = ['poly', 'rubber', 'pb', 'pr', 'pg', 'py', 'pv']
numMPParticles = 0
......@@ -47,4 +261,4 @@ class ResultComparer(object):
numMPParticles += 1
break
return numMPParticles
\ No newline at end of file
return numMPParticles
......@@ -2,8 +2,10 @@ import numpy as np
from itertools import combinations
from methods import SubsamplingMethod
import sys
sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset
import helpers
class BoxSelectionSubsamplingMethod(SubsamplingMethod):
......@@ -12,55 +14,112 @@ class BoxSelectionSubsamplingMethod(SubsamplingMethod):
self.filterDiameter: float = 500
self.offset: tuple = (0, 0)
@property
def label(self) -> str:
raise NotImplementedError
@property
def filterArea(self) -> float:
return np.pi * (self.filterDiameter / 2) ** 2
def apply_subsampling_method(self) -> tuple:
def apply_subsampling_method(self) -> list:
def distanceToCnt(topleft: tuple):
return abs(topleft[0] - cntStart[0]) + abs(topleft[1] - cntStart[1])
subParticles: list = []
topLefts: list = self.get_topLeft_of_boxes()
boxSize = self.boxSize
boxWidthHeight: tuple = (self.boxSize, self.boxSize)
for particle in self.particleContainer.particles:
for topLeft in topLefts:
if box_overlaps_contour(topLeft, (boxSize, boxSize), particle.contour, self.offset):
cntStart: tuple = (particle.contour[0, 0, 0], particle.contour[0, 0, 1])
sortedTopLefts = sorted(topLefts, key=distanceToCnt)
for topLeftXY in sortedTopLefts:
if helpers.box_overlaps_contour(topLeftXY, boxWidthHeight, particle.contour):
subParticles.append(particle)
break
return self.fraction, subParticles
return subParticles
def get_topLeft_of_boxes(self) -> list:
raise NotImplementedError
def _apply_offset_to_toplefts(self, topLefts: list) -> list:
"""
Applies the filter offset to the calculated topLefts of the measure boxes.
:param topLefts:
:return:
"""
newTopLefts: list = []
for topLeft in topLefts:
newTopLefts.append((topLeft[0] + self.offset[0], topLeft[1] + self.offset[1]))
return newTopLefts
def equals(self, otherMethod) -> bool:
raise NotImplementedError
class BoxSelectionCreator(object):
def __init__(self, dataset: dataset.DataSet):
super(BoxSelectionCreator, self).__init__()
self.dataset: dataset.DataSet = dataset
def get_crossBoxSelectors_for_fraction(self, desiredFraction: float) -> list:
def get_crossBoxSubsamplers_for_fraction(self, desiredFraction: float) -> list:
"""
Creates CrossBoxSelectors that fullfil the desired fraction criterium.
Creates CrossBoxSubsamplers that fullfill the desired fraction criterium.
:param desiredFraction:
:return:
:return list of CrossBoxSubsamplers:
"""
crossBoxSelectors = []
offset, diameter, widthHeight = self.get_filterDimensions_from_dataset()
crossBoxSubsamplers = []
offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset)
diameter = helpers.convert_length_to_pixels(self.dataset, diameter)
offset = helpers.convert_length_to_pixels(self.dataset, offset[0]), \
helpers.convert_length_to_pixels(self.dataset, offset[1])
for numBoxesAcross in [3, 5]:
newBoxSelector: CrossBoxSubSampling = CrossBoxSubSampling(self.dataset.particleContainer, desiredFraction)
newBoxSelector.filterDiameter = diameter
newBoxSelector.offset = offset
newBoxSelector.numBoxesAcross = numBoxesAcross
# for numBoxesAcross in [3, 5]:
# newBoxSelector: CrossBoxSelector = CrossBoxSelector(self.dataset.particleContainer, desiredFraction)
# newBoxSelector.filterDiameter = diameter
# newBoxSelector.offset = offset
# newBoxSelector.numBoxesAcross = numBoxesAcross
#
# crossBoxSelectors.append(newBoxSelector)
maxFraction: float = newBoxSelector.get_maximum_achievable_fraction()
if desiredFraction <= maxFraction:
crossBoxSubsamplers.append(newBoxSelector)
return crossBoxSelectors
return crossBoxSubsamplers
def get_spiralBoxSubsamplers_for_fraction(self, desiredFraction: float) -> list:
"""
Creates CrossBoxSubsamplers that fullfill the desired fraction criterium.
:param desiredFraction:
:return list of SpiralBoxSelectors:
"""
spiralBoxSubsamplers = []
offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset)
diameter = helpers.convert_length_to_pixels(self.dataset, diameter)
offset = helpers.convert_length_to_pixels(self.dataset, offset[0]), \
helpers.convert_length_to_pixels(self.dataset, offset[1])
for numBoxes in SpiralBoxSubsampling.possibleBoxNumbers:
newBoxSelector: SpiralBoxSubsampling = SpiralBoxSubsampling(self.dataset.particleContainer, desiredFraction)
newBoxSelector.filterDiameter = diameter
newBoxSelector.offset = offset
newBoxSelector.numBoxes = numBoxes
class CrossBoxSelector(BoxSelectionSubsamplingMethod):
if newBoxSelector.noBoxOverlap:
spiralBoxSubsamplers.append(newBoxSelector)
return spiralBoxSubsamplers
class CrossBoxSubSampling(BoxSelectionSubsamplingMethod):
def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
self.numBoxesAcross: int = 3 # either 3 or 5
super(CrossBoxSubSampling, self).__init__(particleContainer, desiredFraction)
self.numBoxesAcross: int = 3 # either 3 or 5
@property
def label(self) -> str:
return f'Boxes CrossLayout ({self.numBoxesAcross} boxes across)'
@property
def boxSize(self) -> float:
......@@ -69,23 +128,23 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
self.fraction = maxFraction
totalBoxArea: float = self.filterArea * self.fraction
boxArea: float = totalBoxArea / (2*self.numBoxesAcross - 1)
return boxArea**0.5
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]
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:
if i != self.numBoxesAcross // 2:
topLeftCorners.append((xStartCoordinates[i], middleYCoordinate))
return topLeftCorners
return self._apply_offset_to_toplefts(topLeftCorners)
def get_maximum_achievable_fraction(self) -> float:
"""
......@@ -94,17 +153,24 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
:return float:
"""
alpha: float = np.deg2rad(135)
r: float = self.filterDiameter/2
d: float = (self.numBoxesAcross-1) * r / self.numBoxesAcross # 2/3*r for numAcross = 3, 4/5*r numAcross = 5
r: float = self.filterDiameter / 2
d: float = (self.numBoxesAcross - 1) * r / self.numBoxesAcross # 2/3*r for numAcross = 3, 4/5*r numAcross = 5
delta: float = np.arcsin((np.sin(alpha) * d) / r)
gamma: float = np.pi - alpha - delta
longestBoxHalfDiagonal: float = r / np.sin(alpha) * np.sin(gamma)
maxBoxSize: float = 2 * longestBoxHalfDiagonal / np.sqrt(2)
numBoxes: int = 2*self.numBoxesAcross - 1
totalBoxArea: float = numBoxes * (maxBoxSize**2)
numBoxes: int = 2 * self.numBoxesAcross - 1
totalBoxArea: float = numBoxes * (maxBoxSize ** 2)
maxFraction: float = totalBoxArea / self.filterArea
return maxFraction
def equals(self, otherMethod) -> bool:
equals: bool = False
if type(otherMethod) == type(self) and otherMethod.fraction == self.fraction:
if otherMethod.numBoxesAcross == self.numBoxesAcross:
equals = True
return equals
def _get_horizontal_box_starts(self, boxSize: float) -> list:
"""
Returns a list of width-values at which the individual boxes start
......@@ -132,30 +198,41 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
return tileStarts
class SpiralSelector(BoxSelectionSubsamplingMethod):
class SpiralBoxSubsampling(BoxSelectionSubsamplingMethod):
possibleBoxNumbers: list = [7, 10, 15]
def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
super(SpiralSelector, self).__init__(particleContainer, desiredFraction)
self.boxSize: float = 50
self.numBoxes = 20
super(SpiralBoxSubsampling, self).__init__(particleContainer, desiredFraction)
self.numBoxes = 10
@property
def label(self) -> str:
return f'Boxes SpiralLayout ({self.numBoxes} boxes)'
@property
def noBoxOverlap(self) -> bool:
return not self._boxes_are_overlapping(self.get_topLeft_of_boxes())
@property
def boxSize(self) -> float:
totalBoxArea: float = self.filterArea * self.fraction
boxArea: float = totalBoxArea / self.numBoxes
return boxArea ** 0.5
@property
def spiralSlope(self) -> float:
return self.armDistance / (2*np.pi)
return self.armDistance / (2 * np.pi)
@property
def armDistance(self) -> float:
return np.sqrt(2) * self.boxSize
@property
def actuallyCoveredFraction(self) -> float:
return self.numBoxes*self.boxSize**2 / self.filterArea
def get_topLeft_of_boxes(self) -> list:
"""
Calculates the topLeft-points (x, y) of all measure boxes
The method uses an approximation for the spiral and is not purely accurate.
:return list:"""
filterCenter: tuple = self.filterDiameter/2 - self.boxSize/2, self.filterDiameter/2 - self.boxSize/2
filterCenter: tuple = self.filterDiameter / 2 - self.boxSize / 2, self.filterDiameter / 2 - self.boxSize / 2
slope = self.spiralSlope
theta: float = 0
boxDistance = self.boxSize * 1.1
......@@ -164,11 +241,18 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
for i in range(self.numBoxes):
newPoint: tuple = self._get_xy_at_angle(theta, filterCenter)
topLefts.append(newPoint)
theta += boxDistance / (slope * np.sqrt(1 + theta**2))
theta += boxDistance / (slope * np.sqrt(1 + theta ** 2))
boxDistance *= 1.05
topLefts = self._move_and_scale_toplefts(topLefts)
return topLefts
return self._apply_offset_to_toplefts(topLefts)
def equals(self, otherMethod) -> bool:
equals: bool = False
if type(otherMethod) == type(self) and otherMethod.fraction == self.fraction:
if otherMethod.numBoxes == self.numBoxes:
equals = True
return equals
def _move_and_scale_toplefts(self, topLefts: list) -> list:
"""
......@@ -176,11 +260,11 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
This function moves and scales the topLeft-Points so that all measure boxes lie within the filter limits.
:return list:
"""
xCoords: np.ndarray= np.array([float(point[0]) for point in topLefts]) - self.filterDiameter / 2
yCoords: np.ndarray= np.array([float(point[1]) for point in topLefts]) - self.filterDiameter / 2
xCoords: np.ndarray = np.array([float(point[0]) for point in topLefts]) - self.filterDiameter / 2
yCoords: np.ndarray = np.array([float(point[1]) for point in topLefts]) - self.filterDiameter / 2
xCoordsBoxMiddles: np.ndarray= xCoords + self.boxSize/2
yCoordsBoxMiddles: np.ndarray= yCoords + self.boxSize/2
xCoordsBoxMiddles: np.ndarray = xCoords + self.boxSize / 2
yCoordsBoxMiddles: np.ndarray = yCoords + self.boxSize / 2
lastBoxCenter: tuple = (xCoordsBoxMiddles[-1], yCoordsBoxMiddles[-1])
distanceLastCenter: float = np.linalg.norm(lastBoxCenter)
......@@ -193,8 +277,8 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
xCoordsBoxMiddles *= scaleFactor
yCoordsBoxMiddles *= scaleFactor
xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2
yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2
newTopLefts = zip(np.round(xCoords), np.round(yCoords))
return list(tuple(newTopLefts))
......@@ -208,12 +292,12 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
"""
center = np.array(center)
boxSize = self.boxSize
coords: np.ndarray= np.array([[boxCenter[0] - 0.5*boxSize, boxCenter[1] - 0.5*boxSize],
[boxCenter[0] + 0.5*boxSize, boxCenter[1] - 0.5*boxSize],
[boxCenter[0] - 0.5*boxSize, boxCenter[1] + 0.5*boxSize],
[boxCenter[0] + 0.5*boxSize, boxCenter[1] + 0.5*boxSize]])
coords: np.ndarray = np.array([[boxCenter[0] - 0.5 * boxSize, boxCenter[1] - 0.5 * boxSize],
[boxCenter[0] + 0.5 * boxSize, boxCenter[1] - 0.5 * boxSize],
[boxCenter[0] - 0.5 * boxSize, boxCenter[1] + 0.5 * boxSize],
[boxCenter[0] + 0.5 * boxSize, boxCenter[1] + 0.5 * boxSize]])
distances: np.ndarray= np.linalg.norm(coords - center, axis=1)
distances: np.ndarray = np.linalg.norm(coords - center, axis=1)
return np.max(distances)
def _get_xy_at_angle(self, theta: float, centerXY: tuple = (0, 0)) -> tuple:
......
......@@ -4,6 +4,7 @@ sys.path.append("C://Users//xbrjos//Desktop//Python")
import gepard
from gepard import dataset
import helpers
import numpy as np
class FilterView(QtWidgets.QGraphicsView):
......@@ -21,8 +22,6 @@ class FilterView(QtWidgets.QGraphicsView):
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate)
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.drag = None
......@@ -33,28 +32,18 @@ class FilterView(QtWidgets.QGraphicsView):
def update_measure_boxes(self, topLefts: list, boxSize: float) -> None:
self._remove_measure_boxes()