Source code for spam.plotting.particleSizeDistribution

"""
Library of SPAM functions for plotting a particle size distribution
Copyright (C) 2020 SPAM Contributors

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License along with
this program.  If not, see <http://www.gnu.org/licenses/>.
"""


import matplotlib.pyplot as plt
import numpy


[docs] def plotParticleSizeDistribution( inputRadii, range=None, logScaleX=False, logScaleY=False, units="px", bins=256, cumulative=False, cumulativePassing=True, mode="particles", returnValues=False, plot=True, legendNames=None ): """ This functions draws particle size distributions. There are a number of options, which are detailed below. For a typical geotechnical grading curve the following options (not default) should be used: * cumulative=True, * cumulativePassing=True, * mode="mass", * logScaleX=True, * logScaleY=False Parameters ----------- inputRadii : list of particle radii This can be in any units, but if they are not in pixels, you should also set the "units" variable so that the label on the x-axis is correct range : two-component list (optional, default = [min and max] diameters or calculated volumes) Contains range for histogram, top and bottom. logScaleX : Bool (optional, default = False) Log-scale X axis logScaleY : Bool (optional, default = False) Log-scale Y axis units : string (optional, default = "px") Units to write on the x-axis bins : int (optional, default = 256) Number of bins for the histogram, *i.e.,* the number of points cumulative : bool (optional, default = False) Draw a cumulative histogram, or just a regular histogram? cumulativePassing : bool (optional, default = True) If you aked for a cumulative histogram, do you want it in "passing" the sieve mode, or in "retained" on the sieve mode? mode : string (options, default = "massExact") Should the cumulative graphs be based on number of particles, or particle volume/mass? In sieving ones measures mass. Options: * "particles" * "mass" * "volume" -- the same as above returnValues : bool (optional, default=False) return size and count vectors plot : bool (optional, default=True) actually draw a matplotlib graph legendNames : list of strings (optional, default=None) Description of distribution Returns -------- None -- just a matplotlib graph """ def rad2vol(rad): return (4.0 / 3.0) * numpy.pi * (rad**3.0) # detect list of lists with code from: https://stackoverflow.com/questions/5251663/determine-if-a-list-contains-other-lists if not any(isinstance(el, list) for el in inputRadii): inputRadii = [inputRadii] # Determine overall range. Flatten list and *2 for radius -> diameter if range is None: radiiTemp = numpy.array([item for sublist in inputRadii for item in sublist]) range = [2.0 * radiiTemp.min(), 2.0 * radiiTemp.max()] del radiiTemp if plot: fig = plt.figure() ax = fig.add_subplot(1, 1, 1) for n, radii in enumerate(inputRadii): print("\tplotParticleSizeDistribution(): Multiplying radii by two to make diameters") if legendNames is None: name = "" else: name = legendNames[n] # Can't use numpy.histogram directly since we need to count particle size and account for their volume sieves = numpy.linspace(range[0], range[1], bins) counts = numpy.zeros_like(sieves) if mode == "mass" or "volume": masses = numpy.zeros_like(sieves, dtype=float) # Sort radii radii = sorted(radii) # This holds the sive index of the current bin. # Since we're using sorted data, this will increase monotonically currentSieve = 0 for r in radii: d = r * 2.0 if d > sieves[currentSieve]: # print "sieve {} (size={})too small for me (size={})!".format( currentSieve, d, sieves[currentSieve]) while d > sieves[currentSieve] and currentSieve < len(sieves) - 1: currentSieve += 1 # print "\tbump" if currentSieve < len(sieves) - 1: # Add count to bin counts[currentSieve] += 1 # Add to mass to this sieve if mode == "mass" or "volume": masses[currentSieve] += rad2vol(r) if cumulative: if plot: plt.grid() histCumulative = numpy.zeros_like(counts, dtype=float) if mode == "particles": for n, item in enumerate(counts): if n == 0: histCumulative[n] = counts[n] else: histCumulative[n] = counts[n] + histCumulative[n - 1] elif mode == "mass" or "volume": for n, item in enumerate(masses): if n == 0: histCumulative[n] = masses[n] else: histCumulative[n] = masses[n] + histCumulative[n - 1] else: print("\tparticleSizeDistribution.run(): Unknown mode") # Normalise... histCumulative /= float(histCumulative.max()) / 100.0 if cumulativePassing: if plot: (line,) = ax.plot(sieves, histCumulative, label=name) # line, = ax.step(sieves, histCumulative, where='post', label=name) plt.ylabel("% {} passing".format(mode)) if returnValues: returnOutput = [sieves, histCumulative] else: histCumulative = 100 - histCumulative if plot: (line,) = ax.plot(sieves, histCumulative, label=name) # line, = ax.step(sieves, histCumulative, where='post', label=name) # ax.plot( sieves, histCumulative, ) plt.ylabel("% {} retained".format(mode)) if returnValues: returnOutput = [sieves, histCumulative] if plot: plt.xlabel("Sieve Size ({})".format(units)) else: if plot: # line, = ax.plot(sieves+(sieves[1]-sieves[0])/2.0, counts, label=name) (line,) = ax.step(sieves + (sieves[1] - sieves[0]) / 2.0, counts, where="post", label=name) plt.ylabel("Count") plt.xlabel("Particle Size ({})".format(units)) if returnValues: returnOutput = [sieves + (sieves[1] - sieves[0]) / 2.0, counts] if logScaleX and plot: ax.set_xscale("log") if logScaleY and plot: ax.set_yscale("log") if legendNames is not None: plt.legend() if plot: plt.show() if returnValues: return returnOutput
if __name__ == "__main__": import spam.label.toolkit as ltk import tifffile lab = tifffile.imread("./data/M2EA05/M2EA05-quart-01-bin-watershed.tif") radii = ltk.getEquivalentRadii(lab) # Apply pixel size and binning radii = numpy.multiply(radii, 15.0 * 4.0 / 1000.0) plotParticleSizeDistribution( radii, bins=256, units="mm", cumulative=True, cumulativePassing=True, mode="mass", range=[0, 2], # range=None, logScaleX=False, logScaleY=False, )