From c9d66ba7ffbc432b1676f94ffc79e57d83659f8d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 13 Oct 2020 17:10:35 +0100 Subject: [PATCH 01/99] Update header file --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3eb3089..5eab747 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# erase \ No newline at end of file +# ERASE +This folder contains simulation tools that are under development (and developed) for the ERASE project. This belongs to Ashwin Kumar Rajagopalan (SNSF Early.Postdoc Mobility Fellowship - P2EZP2_191875), Imperial College London (a.rajagopalan@imperial.ac.uk). \ No newline at end of file From 5f263af07647fb51b02b4aa129ac39207cf8ae4a Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 16 Oct 2020 18:13:44 +0100 Subject: [PATCH 02/99] Generate hypothetical materials and obtain SSL loadings --- generateHypotheticalAdsorbents.py | 56 ++++++++++++++++++++++ simulateSSL.py | 77 +++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100755 generateHypotheticalAdsorbents.py create mode 100755 simulateSSL.py diff --git a/generateHypotheticalAdsorbents.py b/generateHypotheticalAdsorbents.py new file mode 100755 index 0000000..605f59e --- /dev/null +++ b/generateHypotheticalAdsorbents.py @@ -0,0 +1,56 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Generates hypothetical sorbents using latin hypercube sampling. The +# sorbents are assumed to exhibit Langmuirian behavior. +# +# Last modified: +# - 2020-10-16, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def generateHypotheticalAdsorbents(): + + import numpy as np + from smt.sampling_methods import LHS + + # Define the number of gases of interest + numberOfGases = 2; + + # Define the number of materials that will be tested for the sensor array + numberOfAdsorbents = 100; + + # Define a range for single site Langmuir isotherm for one sensor material + singleIsothermRange = np.array([[0.0, 10000.0], [0.0, 3e-6],[0, -40000]]) + + adsorbentMaterial = np.zeros((numberOfGases,3,numberOfAdsorbents)) + + + # Generate latin hypercube sampled hypothethical adsorbent materials. + # Isotherm parameters obtained as a matrix with dimensions [ + # numberOfAdsorbents*numberOfGases x 3] + samplingLHS = LHS(xlimits=singleIsothermRange) + allIsothermParameter = samplingLHS(numberOfAdsorbents*numberOfGases); + + # Get the isotherms for each material for the predefined number of gases + isothermCounter = 0; + for ii in range(0,numberOfAdsorbents): + for jj in range(0,numberOfGases): + adsorbentMaterial[jj,:,ii] = allIsothermParameter[isothermCounter,:] + isothermCounter += 1 + + return adsorbentMaterial \ No newline at end of file diff --git a/simulateSSL.py b/simulateSSL.py new file mode 100755 index 0000000..248e8df --- /dev/null +++ b/simulateSSL.py @@ -0,0 +1,77 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Generates the single site Langmuir isotherms for different temperatures +# and concentrations. +# +# Last modified: +# - 2020-10-16, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +# def simulateSSL(): +import numpy as np +import matplotlib.pyplot as plt +import math + +# Import the function generateHypotheticalAdsorbents +from generateHypotheticalAdsorbents import generateHypotheticalAdsorbents + +# Gas constant +Rg = 8.314; # [J/mol K] + +# Define total pressure +pressureTotal = 1.e5 # Pressure [Pa] + +# Define mole fraction +moleFraction = np.linspace(0,1,num=101) # mole fraction [-] + +# Define temperature +temperature = np.linspace(283,323,num=5) # mole fraction [-] + +# Stack up the isotherm parameters for all the materials +adsorbentMaterial = generateHypotheticalAdsorbents() + +# Get the number of gases and number of materials +numberOfGases = adsorbentMaterial.shape[0] +numberOfMaterials = adsorbentMaterial.shape[2] + +equilibriumLoadings = np.zeros((moleFraction.shape[0], + temperature.shape[0],numberOfGases)) + +# Generate the isotherm +for ii in range(numberOfGases): + for jj in range(moleFraction.shape[0]): + for kk in range(temperature.shape[0]): + # Parse out the saturation capacity, equilibrium constant, and + # heat of adsorption + qsat = adsorbentMaterial[ii,0,0] + b0 = adsorbentMaterial[ii,1,0] + delH = adsorbentMaterial[ii,2,0] + # Compute the concentraiton + conc = pressureTotal*moleFraction[jj]/(Rg*temperature[kk]) + # Compute the numerator and denominator for SSL + loadingNum = qsat*b0*math.exp(-delH/(Rg*temperature[kk]))*conc + loadingDen = 1 + b0*math.exp(-delH/(Rg*temperature[kk]))*conc + # Compute the equilibrium loading + equilibriumLoadings[jj,kk,ii] = loadingNum/loadingDen + + +# Plot isotherm +for ii in range(numberOfGases): + for kk in range(temperature.shape[0]): + plt.plot(pressureTotal*moleFraction,equilibriumLoadings[:,kk,ii]) \ No newline at end of file From 1fb96251b17afcedcf16a0be1c502c8627b3714d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 20 Oct 2020 18:15:40 +0100 Subject: [PATCH 03/99] Add new functionalities (commit id, sensor array, datetime) --- generateHypotheticalAdsorbents.py | 53 +++++++++++++++---- getCommitID.py | 36 +++++++++++++ getCurrentDateTime.py | 33 ++++++++++++ simulateSSL.py | 85 +++++++++++++------------------ simulateSensorArray.py | 77 ++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 60 deletions(-) create mode 100755 getCommitID.py create mode 100755 getCurrentDateTime.py create mode 100755 simulateSensorArray.py diff --git a/generateHypotheticalAdsorbents.py b/generateHypotheticalAdsorbents.py index 605f59e..062b38d 100755 --- a/generateHypotheticalAdsorbents.py +++ b/generateHypotheticalAdsorbents.py @@ -13,6 +13,8 @@ # sorbents are assumed to exhibit Langmuirian behavior. # # Last modified: +# - 2020-10-19, AK: Add adsorbent density and molecular weight +# - 2020-10-19, AK: Integrate git commit and save material properties # - 2020-10-16, AK: Initial creation # # Input arguments: @@ -23,22 +25,34 @@ # ############################################################################ -def generateHypotheticalAdsorbents(): +def generateHypotheticalAdsorbents(numberOfGases, numberOfAdsorbents): import numpy as np + from numpy import savez from smt.sampling_methods import LHS + import os # For OS related stuff (make directory, file separator, etc.) - # Define the number of gases of interest - numberOfGases = 2; - - # Define the number of materials that will be tested for the sensor array - numberOfAdsorbents = 100; + # Get the commit ID of the current repository + from getCommitID import getCommitID + gitCommitID = getCommitID() + + # Get the current date and time for saving purposes + from getCurrentDateTime import getCurrentDateTime + simulationDT = getCurrentDateTime() # Define a range for single site Langmuir isotherm for one sensor material - singleIsothermRange = np.array([[0.0, 10000.0], [0.0, 3e-6],[0, -40000]]) + # [qsat [mol/kg] b0 [m3/mol] delH [J/mol]] + singleIsothermRange = np.array([[0.0, 10.0], [0.0, 3e-6],[0, -40000]]) - adsorbentMaterial = np.zeros((numberOfGases,3,numberOfAdsorbents)) + # Define a range for adsorbent densities for all the materials + densityRange = np.array([[500.0, 1500.0]]) + # Define the molecular weight for the gases of interest + # [CO2 N2 O2 SO2 NO2 H2O] - HARD CODED + molecularWeight = np.array([44.01, 28.01, 15.99, 64.07, 46.01, 18.02]) + + # Initialize the output matrix with zeros + adsorbentIsotherm = np.zeros((numberOfGases,3,numberOfAdsorbents)) # Generate latin hypercube sampled hypothethical adsorbent materials. # Isotherm parameters obtained as a matrix with dimensions [ @@ -46,11 +60,28 @@ def generateHypotheticalAdsorbents(): samplingLHS = LHS(xlimits=singleIsothermRange) allIsothermParameter = samplingLHS(numberOfAdsorbents*numberOfGases); + # Also generate adsorbent densities that is latin hypercube sampled + samplingLHS = LHS(xlimits=densityRange) + adsorbentDensity = samplingLHS(numberOfAdsorbents) + # Get the isotherms for each material for the predefined number of gases isothermCounter = 0; for ii in range(0,numberOfAdsorbents): for jj in range(0,numberOfGases): - adsorbentMaterial[jj,:,ii] = allIsothermParameter[isothermCounter,:] + adsorbentIsotherm[jj,:,ii] = allIsothermParameter[isothermCounter,:] isothermCounter += 1 - - return adsorbentMaterial \ No newline at end of file + + # Save the adsorbent isotherm parameters into a native numpy file + # The .npy file is saved in a folder called inputResources (hardcoded) + filePrefix = "isothermParameters" + saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID + ".npz"; + savePath = os.path.join('inputResources',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists('inputResources'): + os.mkdir('inputResources') + + # Save the adsorbent material array + savez (savePath, adsIsotherm = adsorbentIsotherm, + adsDensity = adsorbentDensity, + molWeight = molecularWeight[0:numberOfGases]) \ No newline at end of file diff --git a/getCommitID.py b/getCommitID.py new file mode 100755 index 0000000..e92bb21 --- /dev/null +++ b/getCommitID.py @@ -0,0 +1,36 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Generates a short SHA key of the git commit id in the current branch and +# repository +# +# Last modified: +# - 2020-10-19, AK: Initial creation +# +# Input arguments: +# - N/A +# +# Output arguments: +# - short_sha: Short git commit ID +# +############################################################################ + +def getCommitID(): + # Use gitpython to get the git information of the current repository + import git + repo = git.Repo(search_parent_directories=True) + # Get the simple hashing algorithm tag (SHA) + sha = repo.head.commit.hexsha + # Parse the first six characters of the sha + short_sha = repo.git.rev_parse(sha, short=7) + + # Return the git commit id + return short_sha \ No newline at end of file diff --git a/getCurrentDateTime.py b/getCurrentDateTime.py new file mode 100755 index 0000000..8ca8ec7 --- /dev/null +++ b/getCurrentDateTime.py @@ -0,0 +1,33 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Obtain the current date and time to be used either for saving in file +# name or to enhance traceability of the simulation +# +# Last modified: +# - 2020-10-19, AK: Initial creation +# +# Input arguments: +# - N/A +# +# Output arguments: +# - simulationDT: Current date and time in YYYYmmdd_HHMM format +# +############################################################################ + +def getCurrentDateTime(): + # Get the current date and time for saving purposes + from datetime import datetime + now = datetime.now() + simulationDT = now.strftime("%Y%m%d_%H%M") + + # Return the current date and time + return simulationDT \ No newline at end of file diff --git a/simulateSSL.py b/simulateSSL.py index 248e8df..2122198 100755 --- a/simulateSSL.py +++ b/simulateSSL.py @@ -13,6 +13,8 @@ # and concentrations. # # Last modified: +# - 2020-10-19, AK: Incorporate sorbent density and minor improvements +# - 2020-10-19, AK: Change functionality to work with a single sorbent input # - 2020-10-16, AK: Initial creation # # Input arguments: @@ -23,55 +25,40 @@ # ############################################################################ -# def simulateSSL(): -import numpy as np -import matplotlib.pyplot as plt -import math +def simulateSSL(adsorbentMaterial, adsorbentDensity, pressureTotal, + temperature, moleFraction): + import numpy as np + import math + + # Gas constant + Rg = 8.314; # [J/mol K] -# Import the function generateHypotheticalAdsorbents -from generateHypotheticalAdsorbents import generateHypotheticalAdsorbents + # Get the number of gases + numberOfGases = moleFraction.shape[1] -# Gas constant -Rg = 8.314; # [J/mol K] + if moleFraction.shape[1] != adsorbentMaterial.shape[0]: + raise Exception("The dimensions of the mole fraction and the number of gases in the adsorbent is not consistent!") -# Define total pressure -pressureTotal = 1.e5 # Pressure [Pa] - -# Define mole fraction -moleFraction = np.linspace(0,1,num=101) # mole fraction [-] - -# Define temperature -temperature = np.linspace(283,323,num=5) # mole fraction [-] - -# Stack up the isotherm parameters for all the materials -adsorbentMaterial = generateHypotheticalAdsorbents() - -# Get the number of gases and number of materials -numberOfGases = adsorbentMaterial.shape[0] -numberOfMaterials = adsorbentMaterial.shape[2] - -equilibriumLoadings = np.zeros((moleFraction.shape[0], - temperature.shape[0],numberOfGases)) - -# Generate the isotherm -for ii in range(numberOfGases): - for jj in range(moleFraction.shape[0]): - for kk in range(temperature.shape[0]): - # Parse out the saturation capacity, equilibrium constant, and - # heat of adsorption - qsat = adsorbentMaterial[ii,0,0] - b0 = adsorbentMaterial[ii,1,0] - delH = adsorbentMaterial[ii,2,0] - # Compute the concentraiton - conc = pressureTotal*moleFraction[jj]/(Rg*temperature[kk]) - # Compute the numerator and denominator for SSL - loadingNum = qsat*b0*math.exp(-delH/(Rg*temperature[kk]))*conc - loadingDen = 1 + b0*math.exp(-delH/(Rg*temperature[kk]))*conc - # Compute the equilibrium loading - equilibriumLoadings[jj,kk,ii] = loadingNum/loadingDen - - -# Plot isotherm -for ii in range(numberOfGases): - for kk in range(temperature.shape[0]): - plt.plot(pressureTotal*moleFraction,equilibriumLoadings[:,kk,ii]) \ No newline at end of file + # Initialize the equilibriumLoadings array with zeros + equilibriumLoadings = np.zeros((moleFraction.shape[0], + temperature.shape[0],numberOfGases)) + + # Generate the isotherm + for ii in range(numberOfGases): + for jj in range(moleFraction.shape[0]): + for kk in range(temperature.shape[0]): + # Parse out the saturation capacity, equilibrium constant, and + # heat of adsorption + qsat = adsorbentMaterial[ii,0] + b0 = adsorbentMaterial[ii,1] + delH = adsorbentMaterial[ii,2] + # Compute the concentraiton + conc = pressureTotal*moleFraction[jj,ii]/(Rg*temperature[kk]) + # Compute the numerator and denominator for SSL + loadingNum = adsorbentDensity*qsat*b0*math.exp(-delH/(Rg*temperature[kk]))*conc + loadingDen = 1 + b0*math.exp(-delH/(Rg*temperature[kk]))*conc + # Compute the equilibrium loading + equilibriumLoadings[jj,kk,ii] = loadingNum/loadingDen # [mol/m3] + + # Return the equilibrium loadings + return equilibriumLoadings \ No newline at end of file diff --git a/simulateSensorArray.py b/simulateSensorArray.py new file mode 100755 index 0000000..34e9f16 --- /dev/null +++ b/simulateSensorArray.py @@ -0,0 +1,77 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# +# +# Last modified: +# - 2020-10-20, AK: Obtain sensor array finger print +# - 2020-10-19, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +# def simulateSensorArray(): +import numpy as np +from numpy import load +import os +from simulateSSL import simulateSSL + +# Total pressure of the gas [Pa] +pressureTotal = np.array([1.e5]); + +# Temperature of the gas [K] +# Can be a vector of temperatures +temperature = np.array([298.15]); + +# Mole fraction of the gas [-] +# Can be [jxg], where j is the number of mole fractions for g gases +moleFraction = np.array([[0.6, 0.4]]) + +# Sensor combinations used in the array. This is a [gx1] vector that maps to +# the sorbent/material ID generated using the +# generateHypotheticalAdsorbents.py function +sensorID = np.array([6,10,71]) + +# For now load a given adsorbent isotherm material file +loadFileName = "isothermParameters_20201020_1756_5f263af.npz" +hypoAdsorbentFile = os.path.join('inputResources',loadFileName); + +# Check if the file with the adsorbent properties exist +if os.path.exists(hypoAdsorbentFile): + loadedFileContent = load(hypoAdsorbentFile) + adsorbentIsothermTemp = loadedFileContent['adsIsotherm'] + adsorbentDensityTemp = loadedFileContent['adsDensity'] + molecularWeight = loadedFileContent['molWeight'] +else: + errorString = "Adsorbent property file " + hypoAdsorbentFile + " does not exist." + raise Exception(errorString) + +# Get the equilibrium loading for all the sensors for each gas +# This is a [nxg] matrix where n is the number of sensors and g the number +# of gases +sensorLoadingPerGasVol = np.zeros((sensorID.shape[0],moleFraction.shape[1])) # [mol/m3] +sensorLoadingPerGasMass = np.zeros((sensorID.shape[0],moleFraction.shape[1])) # [mol/kg] +for ii in range(sensorID.shape[0]): + adsorbentID = sensorID[ii] + adsorbentIsotherm = adsorbentIsothermTemp[:,:,adsorbentID] + adsorbentDensity = adsorbentDensityTemp[adsorbentID] + equilibriumLoadings = simulateSSL(adsorbentIsotherm,adsorbentDensity, + pressureTotal,temperature,moleFraction) # [mol/m3] + sensorLoadingPerGasVol[ii,:] = equilibriumLoadings[0,0,:] # [mol/m3] + sensorLoadingPerGasMass[ii,:] = equilibriumLoadings[0,0,:]/adsorbentDensity # [mol/kg] + +# Obtain the sensor finger print # [g of total gas adsorbed/kg of sorbent] +sensorFingerPrint = np.dot(sensorLoadingPerGasMass,molecularWeight) \ No newline at end of file From 782efa3c17e9be354dadb08141998e5b9f92c061 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 21 Oct 2020 18:26:30 +0100 Subject: [PATCH 04/99] First version of concentration estimator --- estimateConcentration.py | 92 ++++++++++++++++++++++++++++++ generateHypotheticalAdsorbents.py | 3 +- generateTrueSensorResponse.py | 46 +++++++++++++++ simulateSensorArray.py | 95 ++++++++++++++----------------- 4 files changed, 181 insertions(+), 55 deletions(-) create mode 100755 estimateConcentration.py create mode 100755 generateTrueSensorResponse.py diff --git a/estimateConcentration.py b/estimateConcentration.py new file mode 100755 index 0000000..2ea1631 --- /dev/null +++ b/estimateConcentration.py @@ -0,0 +1,92 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Estimates the concentration of the gas mixture using the sensor response +# by minimization of the sum of square error between the "true" and the +# "estimated" differences in the change of mass of the sensor array +# +# Last modified: +# - 2020-10-21, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def estimateConcentration(moleFracID,sensorID): + import numpy as np + from generateTrueSensorResponse import generateTrueSensorResponse + from scipy.optimize import basinhopping + + # Total number of sensor elements/gases simulated and generated using + # generateHypotheticalAdsorbents.py function + numberOfAdsorbents = 100; + numberOfGases = 2; + + # Total pressure of the gas [Pa] + pressureTotal = np.array([1.e5]); + + # Temperature of the gas [K] + # Can be a vector of temperatures + temperature = np.array([298.15]); + + # Get the individual sensor reponse for all the given "experimental/test" concentrations + sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,pressureTotal,temperature) + + # True mole fraction index (provide the index corresponding to the true + # experimental mole fraction (0-4, check generateTrueSensorResponse.py) + moleFracID = moleFracID + + # Sensor combinations used in the array. This is a [gx1] vector that maps to + # the sorbent/material ID generated using the + # generateHypotheticalAdsorbents.py function + sensorID = np.array(sensorID) + + # Parse out the true sensor response for a sensor array with n number of + # sensors given by sensorID + arrayTrueResponse = np.zeros(sensorID.shape[0]) + for ii in range(sensorID.shape[0]): + arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii],moleFracID] + + # Pack the input parameters/arguments useful to compute the objective + # function to estimate the mole fraction as a tuple + inputParameters = (arrayTrueResponse, pressureTotal, temperature, sensorID) + + # Minimize an objective function to compute the mole fraction of the feed + # gas to the sensor + initialCondition = np.zeros(numberOfGases) # Initial guess + # Use the basin hopping minimizer to escape local minima when evaluating + # the function. The number of iterations is hard-coded and fixed at 50 + estMoleFraction = basinhopping(concObjectiveFunction, initialCondition, + minimizer_kwargs = {"args": inputParameters}, niter = 50) + return estMoleFraction.x + +# func: concObjectiveFunction, computes the sum of square error for the +# finger print for varying gas concentrations, using the minimize function +def concObjectiveFunction(x, *inputParameters): + import numpy as np + from simulateSensorArray import simulateSensorArray + + # Unpack the tuple that contains the true response, pressure, temperature, + # and the sensor identifiers + arrayTrueResponse, pressureTotal, temperature, sensorID = inputParameters + + # Reshape the mole fraction to a row vector for compatibility + moleFraction = np.array([x]) # This is needed to keep the structure as a row instead of column + + # Compute the sensor reponse for a given mole fraction input + arraySimResponse = simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction) + + # Compute the sum of the error for the sensor array + return sum(np.power(arrayTrueResponse - arraySimResponse,2)) \ No newline at end of file diff --git a/generateHypotheticalAdsorbents.py b/generateHypotheticalAdsorbents.py index 062b38d..b929c7d 100755 --- a/generateHypotheticalAdsorbents.py +++ b/generateHypotheticalAdsorbents.py @@ -25,8 +25,7 @@ # ############################################################################ -def generateHypotheticalAdsorbents(numberOfGases, numberOfAdsorbents): - +def generateHypotheticalAdsorbents(numberOfGases, numberOfAdsorbents): import numpy as np from numpy import savez from smt.sampling_methods import LHS diff --git a/generateTrueSensorResponse.py b/generateTrueSensorResponse.py new file mode 100755 index 0000000..604d1c3 --- /dev/null +++ b/generateTrueSensorResponse.py @@ -0,0 +1,46 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Generates a "true" sensor response for known mole fractions of gas for the +# sorbents that are hypothetically generated. The mole fraction is hard coded +# and should be modified. +# +# Last modified: +# - 2020-10-21, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def generateTrueSensorResponse(numberOfAdsorbents, pressureTotal, temperature): + import numpy as np + from simulateSensorArray import simulateSensorArray + + # Mole fraction of the gas [-] + # Can be [jxg], where j is the number of mole fractions for g gases + moleFraction = np.array([[0.05, 0.95], + [0.15, 0.85], + [0.40, 0.60], + [0.75, 0.25], + [0.90, 0.10]]) + + # Get the individual sensor reponse for all the five "test" concentrations + sensorTrueResponse = np.zeros((numberOfAdsorbents,moleFraction.shape[0])) + for ii in range(numberOfAdsorbents): + for jj in range(moleFraction.shape[0]): + moleFractionTemp = np.array([moleFraction[jj,:]]) # This is needed to keep the structure as a row instead of column + sensorTrueResponse[ii,jj] = simulateSensorArray(np.array([ii]), + pressureTotal, temperature, moleFractionTemp) + return sensorTrueResponse \ No newline at end of file diff --git a/simulateSensorArray.py b/simulateSensorArray.py index 34e9f16..cd4a5aa 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -9,9 +9,12 @@ # Authors: Ashwin Kumar Rajagopalan (AK) # # Purpose: -# +# Generates the response of a sensor array as change in mass due to gas +# sorption at a given pressure, temperature, and mole fraction for +# n sorbents. # # Last modified: +# - 2020-10-21, AK: Cosmetic changes and make it a function # - 2020-10-20, AK: Obtain sensor array finger print # - 2020-10-19, AK: Initial creation # @@ -23,55 +26,41 @@ # ############################################################################ -# def simulateSensorArray(): -import numpy as np -from numpy import load -import os -from simulateSSL import simulateSSL - -# Total pressure of the gas [Pa] -pressureTotal = np.array([1.e5]); - -# Temperature of the gas [K] -# Can be a vector of temperatures -temperature = np.array([298.15]); - -# Mole fraction of the gas [-] -# Can be [jxg], where j is the number of mole fractions for g gases -moleFraction = np.array([[0.6, 0.4]]) - -# Sensor combinations used in the array. This is a [gx1] vector that maps to -# the sorbent/material ID generated using the -# generateHypotheticalAdsorbents.py function -sensorID = np.array([6,10,71]) - -# For now load a given adsorbent isotherm material file -loadFileName = "isothermParameters_20201020_1756_5f263af.npz" -hypoAdsorbentFile = os.path.join('inputResources',loadFileName); - -# Check if the file with the adsorbent properties exist -if os.path.exists(hypoAdsorbentFile): - loadedFileContent = load(hypoAdsorbentFile) - adsorbentIsothermTemp = loadedFileContent['adsIsotherm'] - adsorbentDensityTemp = loadedFileContent['adsDensity'] - molecularWeight = loadedFileContent['molWeight'] -else: - errorString = "Adsorbent property file " + hypoAdsorbentFile + " does not exist." - raise Exception(errorString) - -# Get the equilibrium loading for all the sensors for each gas -# This is a [nxg] matrix where n is the number of sensors and g the number -# of gases -sensorLoadingPerGasVol = np.zeros((sensorID.shape[0],moleFraction.shape[1])) # [mol/m3] -sensorLoadingPerGasMass = np.zeros((sensorID.shape[0],moleFraction.shape[1])) # [mol/kg] -for ii in range(sensorID.shape[0]): - adsorbentID = sensorID[ii] - adsorbentIsotherm = adsorbentIsothermTemp[:,:,adsorbentID] - adsorbentDensity = adsorbentDensityTemp[adsorbentID] - equilibriumLoadings = simulateSSL(adsorbentIsotherm,adsorbentDensity, - pressureTotal,temperature,moleFraction) # [mol/m3] - sensorLoadingPerGasVol[ii,:] = equilibriumLoadings[0,0,:] # [mol/m3] - sensorLoadingPerGasMass[ii,:] = equilibriumLoadings[0,0,:]/adsorbentDensity # [mol/kg] - -# Obtain the sensor finger print # [g of total gas adsorbed/kg of sorbent] -sensorFingerPrint = np.dot(sensorLoadingPerGasMass,molecularWeight) \ No newline at end of file +def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction): + import numpy as np + from numpy import load + import os + from simulateSSL import simulateSSL + + # For now load a given adsorbent isotherm material file + loadFileName = "isothermParameters_20201020_1756_5f263af.npz" + hypoAdsorbentFile = os.path.join('inputResources',loadFileName); + + # Check if the file with the adsorbent properties exist + if os.path.exists(hypoAdsorbentFile): + loadedFileContent = load(hypoAdsorbentFile) + adsorbentIsothermTemp = loadedFileContent['adsIsotherm'] + adsorbentDensityTemp = loadedFileContent['adsDensity'] + molecularWeight = loadedFileContent['molWeight'] + else: + errorString = "Adsorbent property file " + hypoAdsorbentFile + " does not exist." + raise Exception(errorString) + + # Get the equilibrium loading for all the sensors for each gas + # This is a [nxg] matrix where n is the number of sensors and g the number + # of gases + sensorLoadingPerGasVol = np.zeros((sensorID.shape[0],moleFraction.shape[1])) # [mol/m3] + sensorLoadingPerGasMass = np.zeros((sensorID.shape[0],moleFraction.shape[1])) # [mol/kg] + for ii in range(sensorID.shape[0]): + adsorbentID = sensorID[ii] + adsorbentIsotherm = adsorbentIsothermTemp[:,:,adsorbentID] + adsorbentDensity = adsorbentDensityTemp[adsorbentID] + equilibriumLoadings = simulateSSL(adsorbentIsotherm,adsorbentDensity, + pressureTotal,temperature,moleFraction) # [mol/m3] + sensorLoadingPerGasVol[ii,:] = equilibriumLoadings[0,0,:] # [mol/m3] + sensorLoadingPerGasMass[ii,:] = equilibriumLoadings[0,0,:]/adsorbentDensity # [mol/kg] + + # Obtain the sensor finger print # [g of total gas adsorbed/kg of sorbent] + sensorFingerPrint = np.dot(sensorLoadingPerGasMass,molecularWeight) # [g/kg] + + return sensorFingerPrint \ No newline at end of file From 310ccf2983cb6698b5933d518b24927ac4e8c8e8 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 22 Oct 2020 18:15:27 +0100 Subject: [PATCH 05/99] Introduce parametric sensor screening --- estimateConcentration.py | 20 +++--- generateHypotheticalAdsorbents.py | 2 +- generateTrueSensorResponse.py | 16 +++-- screenSensorArray.py | 112 ++++++++++++++++++++++++++++++ simulateSSL.py | 11 +-- simulateSensorArray.py | 4 +- 6 files changed, 145 insertions(+), 20 deletions(-) create mode 100755 screenSensorArray.py diff --git a/estimateConcentration.py b/estimateConcentration.py index 2ea1631..a9b30cc 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,8 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2020-10-22, AK: Change error to relative from absolute, add opt bounds, +# input arguments, and initial guess # - 2020-10-21, AK: Initial creation # # Input arguments: @@ -24,16 +26,16 @@ # ############################################################################ -def estimateConcentration(moleFracID,sensorID): +def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorID): import numpy as np from generateTrueSensorResponse import generateTrueSensorResponse from scipy.optimize import basinhopping # Total number of sensor elements/gases simulated and generated using # generateHypotheticalAdsorbents.py function - numberOfAdsorbents = 100; - numberOfGases = 2; - + # numberOfAdsorbents = 4; + # numberOfGases = 3; + # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -55,7 +57,7 @@ def estimateConcentration(moleFracID,sensorID): # Parse out the true sensor response for a sensor array with n number of # sensors given by sensorID - arrayTrueResponse = np.zeros(sensorID.shape[0]) + arrayTrueResponse = 0.5*np.ones(sensorID.shape[0]) for ii in range(sensorID.shape[0]): arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii],moleFracID] @@ -66,11 +68,13 @@ def estimateConcentration(moleFracID,sensorID): # Minimize an objective function to compute the mole fraction of the feed # gas to the sensor initialCondition = np.zeros(numberOfGases) # Initial guess + optBounds = np.tile([0.,1.], (numberOfGases,1)) # BOunding the mole fractions # Use the basin hopping minimizer to escape local minima when evaluating # the function. The number of iterations is hard-coded and fixed at 50 estMoleFraction = basinhopping(concObjectiveFunction, initialCondition, - minimizer_kwargs = {"args": inputParameters}, niter = 50) - return estMoleFraction.x + minimizer_kwargs = {"args": inputParameters, + "bounds": optBounds}, niter = 50) + return np.concatenate((sensorID,estMoleFraction.x), axis=0) # func: concObjectiveFunction, computes the sum of square error for the # finger print for varying gas concentrations, using the minimize function @@ -89,4 +93,4 @@ def concObjectiveFunction(x, *inputParameters): arraySimResponse = simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction) # Compute the sum of the error for the sensor array - return sum(np.power(arrayTrueResponse - arraySimResponse,2)) \ No newline at end of file + return sum(np.power((arrayTrueResponse - arraySimResponse)/arrayTrueResponse,2)) \ No newline at end of file diff --git a/generateHypotheticalAdsorbents.py b/generateHypotheticalAdsorbents.py index b929c7d..e3ae50e 100755 --- a/generateHypotheticalAdsorbents.py +++ b/generateHypotheticalAdsorbents.py @@ -71,7 +71,7 @@ def generateHypotheticalAdsorbents(numberOfGases, numberOfAdsorbents): isothermCounter += 1 # Save the adsorbent isotherm parameters into a native numpy file - # The .npy file is saved in a folder called inputResources (hardcoded) + # The .npz file is saved in a folder called inputResources (hardcoded) filePrefix = "isothermParameters" saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID + ".npz"; savePath = os.path.join('inputResources',saveFileName) diff --git a/generateTrueSensorResponse.py b/generateTrueSensorResponse.py index 604d1c3..162b914 100755 --- a/generateTrueSensorResponse.py +++ b/generateTrueSensorResponse.py @@ -14,6 +14,7 @@ # and should be modified. # # Last modified: +# - 2020-10-22, AK: Add two/three gases # - 2020-10-21, AK: Initial creation # # Input arguments: @@ -30,11 +31,16 @@ def generateTrueSensorResponse(numberOfAdsorbents, pressureTotal, temperature): # Mole fraction of the gas [-] # Can be [jxg], where j is the number of mole fractions for g gases - moleFraction = np.array([[0.05, 0.95], - [0.15, 0.85], - [0.40, 0.60], - [0.75, 0.25], - [0.90, 0.10]]) + # moleFraction = np.array([[0.05, 0.95], + # [0.15, 0.85], + # [0.40, 0.60], + # [0.75, 0.25], + # [0.90, 0.10]]) + moleFraction = np.array([[0.05, 0.15, 0.80], + [0.15, 0.25, 0.60], + [0.40, 0.35, 0.25], + [0.75, 0.10, 0.15], + [0.90, 0.05, 0.05]]) # Get the individual sensor reponse for all the five "test" concentrations sensorTrueResponse = np.zeros((numberOfAdsorbents,moleFraction.shape[0])) diff --git a/screenSensorArray.py b/screenSensorArray.py new file mode 100755 index 0000000..0b0f544 --- /dev/null +++ b/screenSensorArray.py @@ -0,0 +1,112 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Generates hypothetical sorbents using latin hypercube sampling. The +# sorbents are assumed to exhibit Langmuirian behavior. +# +# Last modified: +# - 2020-10-22, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from numpy import save +import multiprocessing # For parallel processing +from joblib import Parallel, delayed # For parallel processing +from tqdm import tqdm # To track progress of the loop +from estimateConcentration import estimateConcentration +import os + +# Get the commit ID of the current repository +from getCommitID import getCommitID +gitCommitID = getCommitID() + +# Find out the total number of cores available for parallel processing +num_cores = multiprocessing.cpu_count() + +# Total number of sensor elements/gases simulated and generated using +# generateHypotheticalAdsorbents.py function +numberOfAdsorbents = 10; +numberOfGases = 3; + +# "True" gas composition that is exposed to the sensor array (0-4) +# Check generateTrueSensorResponse.py for the actual concentrations +moleFracID = 0 + +##### FOR 1 SORBENT SENSOR ARRAY ##### +# Get the current date and time for saving purposes +from getCurrentDateTime import getCurrentDateTime +simulationDT = getCurrentDateTime() + +# Loop over all the sorbents for a single material sensor +# Using parallel processing to loop through all the materials +arrayConcentration = np.zeros(numberOfAdsorbents) +arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + (numberOfAdsorbents,numberOfGases,moleFracID,[ii]) + for ii in tqdm(range(numberOfAdsorbents))) + +# Convert the output list to a matrix +arrayConcentration = np.array(arrayConcentration) + +# Save the array concentration into a native numpy file +# The .npy file is saved in a folder called simulationResults (hardcoded) +filePrefix = "arrayConcentration" +saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; +savePath = os.path.join('simulationResults',saveFileName) + +# Check if inputResources directory exists or not. If not, create the folder +if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') + +# Save the array ceoncentration obtained from estimateConcentration +save (savePath, arrayConcentration) + +##### FOR 2 SORBENT SENSOR ARRAY ##### +# Get the current date and time for saving purposes +from getCurrentDateTime import getCurrentDateTime +simulationDT = getCurrentDateTime() + +# Loop over all the sorbents for a single material sensor +# Using parallel processing to loop through all the materials +arrayConcentration = np.zeros(numberOfAdsorbents) +for jj in range(numberOfAdsorbents): + arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + (numberOfAdsorbents,numberOfGases,moleFracID,[ii,jj]) + for ii in tqdm(range(jj,numberOfAdsorbents))) + # Convert the output list to a matrix + arrayConcentrationTemp = np.array(arrayConcentrationTemp) + if jj == 0: + arrayConcentration = arrayConcentrationTemp + else: + arrayConcentration = np.append(arrayConcentration,arrayConcentrationTemp, axis=0) + +# Delete entries that use the same materials for both sensors +delRows = np.where(arrayConcentration[:,0] == arrayConcentration[:,1]) +arrayConcentration = np.delete(arrayConcentration,delRows,axis=0) + +# Save the array concentration into a native numpy file +# The .npy file is saved in a folder called simulationResults (hardcoded) +filePrefix = "arrayConcentration" +saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; +savePath = os.path.join('simulationResults',saveFileName) + +# Check if inputResources directory exists or not. If not, create the folder +if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') + +# Save the array ceoncentration obtained from estimateConcentration +save (savePath, arrayConcentration) \ No newline at end of file diff --git a/simulateSSL.py b/simulateSSL.py index 2122198..733ac33 100755 --- a/simulateSSL.py +++ b/simulateSSL.py @@ -13,6 +13,7 @@ # and concentrations. # # Last modified: +# - 2020-10-22, AK: Minor cosmetic changes # - 2020-10-19, AK: Incorporate sorbent density and minor improvements # - 2020-10-19, AK: Change functionality to work with a single sorbent input # - 2020-10-16, AK: Initial creation @@ -25,7 +26,7 @@ # ############################################################################ -def simulateSSL(adsorbentMaterial, adsorbentDensity, pressureTotal, +def simulateSSL(adsorbentIsotherm, adsorbentDensity, pressureTotal, temperature, moleFraction): import numpy as np import math @@ -36,7 +37,7 @@ def simulateSSL(adsorbentMaterial, adsorbentDensity, pressureTotal, # Get the number of gases numberOfGases = moleFraction.shape[1] - if moleFraction.shape[1] != adsorbentMaterial.shape[0]: + if moleFraction.shape[1] != adsorbentIsotherm.shape[0]: raise Exception("The dimensions of the mole fraction and the number of gases in the adsorbent is not consistent!") # Initialize the equilibriumLoadings array with zeros @@ -49,9 +50,9 @@ def simulateSSL(adsorbentMaterial, adsorbentDensity, pressureTotal, for kk in range(temperature.shape[0]): # Parse out the saturation capacity, equilibrium constant, and # heat of adsorption - qsat = adsorbentMaterial[ii,0] - b0 = adsorbentMaterial[ii,1] - delH = adsorbentMaterial[ii,2] + qsat = adsorbentIsotherm[ii,0] + b0 = adsorbentIsotherm[ii,1] + delH = adsorbentIsotherm[ii,2] # Compute the concentraiton conc = pressureTotal*moleFraction[jj,ii]/(Rg*temperature[kk]) # Compute the numerator and denominator for SSL diff --git a/simulateSensorArray.py b/simulateSensorArray.py index cd4a5aa..2af6312 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -14,6 +14,7 @@ # n sorbents. # # Last modified: +# - 2020-10-22, AK: Add two/three gases # - 2020-10-21, AK: Cosmetic changes and make it a function # - 2020-10-20, AK: Obtain sensor array finger print # - 2020-10-19, AK: Initial creation @@ -33,7 +34,8 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction): from simulateSSL import simulateSSL # For now load a given adsorbent isotherm material file - loadFileName = "isothermParameters_20201020_1756_5f263af.npz" + # loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases + loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases hypoAdsorbentFile = os.path.join('inputResources',loadFileName); # Check if the file with the adsorbent properties exist From 6bf5c350e5cf367592e767ba59dda9541f8f9c9c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 23 Oct 2020 18:22:50 +0100 Subject: [PATCH 06/99] Introduce competition --- screenSensorArray.py | 4 ++-- simulateSSL.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/screenSensorArray.py b/screenSensorArray.py index 0b0f544..16328b1 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -40,8 +40,8 @@ # Total number of sensor elements/gases simulated and generated using # generateHypotheticalAdsorbents.py function -numberOfAdsorbents = 10; -numberOfGases = 3; +numberOfAdsorbents = 10 +numberOfGases = 3 # "True" gas composition that is exposed to the sensor array (0-4) # Check generateTrueSensorResponse.py for the actual concentrations diff --git a/simulateSSL.py b/simulateSSL.py index 733ac33..8861751 100755 --- a/simulateSSL.py +++ b/simulateSSL.py @@ -13,6 +13,7 @@ # and concentrations. # # Last modified: +# - 2020-10-23, AK: Replace the noncompetitive to competitive isotherm # - 2020-10-22, AK: Minor cosmetic changes # - 2020-10-19, AK: Incorporate sorbent density and minor improvements # - 2020-10-19, AK: Change functionality to work with a single sorbent input @@ -29,7 +30,6 @@ def simulateSSL(adsorbentIsotherm, adsorbentDensity, pressureTotal, temperature, moleFraction): import numpy as np - import math # Gas constant Rg = 8.314; # [J/mol K] @@ -51,13 +51,17 @@ def simulateSSL(adsorbentIsotherm, adsorbentDensity, pressureTotal, # Parse out the saturation capacity, equilibrium constant, and # heat of adsorption qsat = adsorbentIsotherm[ii,0] - b0 = adsorbentIsotherm[ii,1] - delH = adsorbentIsotherm[ii,2] + b0 = adsorbentIsotherm[:,1] + delH = adsorbentIsotherm[:,2] + b = np.multiply(b0,np.exp(-delH/(Rg*temperature[kk]))) # Compute the concentraiton - conc = pressureTotal*moleFraction[jj,ii]/(Rg*temperature[kk]) + conc = pressureTotal*moleFraction[jj,:]/(Rg*temperature[kk]) + # Compute sum(b*c) for the multicomponet competitive eqbm (den.) + bStarConc = np.multiply(b,conc) + sumbStarConc = np.sum(bStarConc) # Compute the numerator and denominator for SSL - loadingNum = adsorbentDensity*qsat*b0*math.exp(-delH/(Rg*temperature[kk]))*conc - loadingDen = 1 + b0*math.exp(-delH/(Rg*temperature[kk]))*conc + loadingNum = adsorbentDensity*qsat*b[ii]*conc[ii] + loadingDen = 1 + sumbStarConc # Compute the equilibrium loading equilibriumLoadings[jj,kk,ii] = loadingNum/loadingDen # [mol/m3] From 99f63e87039d25f12bc1566bac0f7fae32626736 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 26 Oct 2020 18:53:15 +0000 Subject: [PATCH 07/99] Add plotting capabilities --- plotSSLIsotherm.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100755 plotSSLIsotherm.py diff --git a/plotSSLIsotherm.py b/plotSSLIsotherm.py new file mode 100755 index 0000000..36e18ac --- /dev/null +++ b/plotSSLIsotherm.py @@ -0,0 +1,90 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plots the single site Langmuir isotherms +# +# Last modified: +# - 2020-10-26, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +import os +from numpy import load +from simulateSSL import simulateSSL +import matplotlib +import matplotlib.pyplot as plt + +# Sensor ID to be plotted +sensorID = np.array([19]) + +# Total pressure of the gas [Pa] +pressureTotal = np.array([1.e5]) + +# Temperature of the gas [K] +# Can be a vector of temperatures +temperature = np.array([298.15]) + +# Molefraction +moleFraction = np.array([np.linspace(0,1,101)]) + +# For now load a given adsorbent isotherm material file +# loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases +loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases +hypoAdsorbentFile = os.path.join('inputResources',loadFileName); + +# Check if the file with the adsorbent properties exist +if os.path.exists(hypoAdsorbentFile): + loadedFileContent = load(hypoAdsorbentFile) + adsorbentIsothermTemp = loadedFileContent['adsIsotherm'] + adsorbentDensityTemp = loadedFileContent['adsDensity'] + molecularWeight = loadedFileContent['molWeight'] +else: + errorString = "Adsorbent property file " + hypoAdsorbentFile + " does not exist." + raise Exception(errorString) + +###### TO DO: SERIOUS ISSUE WITH THE ISOTHERM PLOTTING +# Evaluate the isotherms +adsorbentID = sensorID +adsorbentIsotherm = adsorbentIsothermTemp[:,:,adsorbentID] +adsorbentDensity = adsorbentDensityTemp[adsorbentID] +equilibriumLoadings = np.zeros([moleFraction.shape[1],adsorbentIsotherm.shape[0]]) +# Loop through all the gases so that the single component isotherm is +# generated. If not multicomponent genretaed. Additionally, several +# transpose operations are performed to be self-consistent with other codes +for ii in range(adsorbentIsotherm.shape[0]): + equilibriumLoadings[:,ii] = np.squeeze(simulateSSL(adsorbentIsotherm[ii,:,:].T,adsorbentDensity, + pressureTotal,temperature,moleFraction.T))/adsorbentDensity # [mol/m3] + + +# Plot the pure single component isotherm for the n gases +fig, ax = plt.subplots() +font = {'family' : 'Helvetica', + 'weight' : 'normal', + 'size' : 16} +matplotlib.rc('font', **font) +# HARD CODED for 3 gases +ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,0],'r') +ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,1],'b') +ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,2],'g') + + +ax.set(xlabel='Pressure P [bar]', + ylabel='q [mol kg$^{\mathregular{-1}}$]', + xlim = [0, 1], ylim = [0, 10]) +ax.grid() +plt.show() \ No newline at end of file From 4c9af80d65291f806337936d20a2a806fd03ccc2 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 27 Oct 2020 18:01:30 +0000 Subject: [PATCH 08/99] Add further plotting capability --- .gitignore | 157 ++++++++++++++++++++++++++++++++ doubleColumn.mplstyle | 44 +++++++++ plotAdsorbentCharacteristics.py | 104 +++++++++++++++++++++ plotSSLIsotherm.py | 59 ++++++++---- singleColumn.mplstyle | 44 +++++++++ 5 files changed, 392 insertions(+), 16 deletions(-) create mode 100755 .gitignore create mode 100755 doubleColumn.mplstyle create mode 100755 plotAdsorbentCharacteristics.py create mode 100755 singleColumn.mplstyle diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..d1d4543 --- /dev/null +++ b/.gitignore @@ -0,0 +1,157 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Graphic files +*.eps +*.pdf +*.png +*.tif +*.jpg + +# Logs and databases +*.log +*.sql +*.sqlite +#*.bib +*.xlsx +*.txt + +# Python related outputs +*.npy +*.npz + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/doubleColumn.mplstyle b/doubleColumn.mplstyle new file mode 100755 index 0000000..8a5f6bc --- /dev/null +++ b/doubleColumn.mplstyle @@ -0,0 +1,44 @@ +# matplotlib configuration file for the ERASE project +# https://matplotlib.org/3.3.2/tutorials/introductory/customizing.html + +## Figure property +figure.figsize : 7, 3 # width, height in inches +figure.dpi : 600 # dpi +figure.autolayout : true # for labels not being cut out + +## Axes +axes.titlesize : 10 +axes.labelsize : 10 +axes.formatter.limits : -5, 3 + +## Grid +axes.grid : true +grid.color : cccccc +grid.linewidth : 0.5 + +## Lines & Scatter +lines.linewidth : 1.5 +lines.markersize : 4 +scatter.marker: o + +## Ticks +xtick.top : true +ytick.right : true +xtick.labelsize : 8 +ytick.labelsize : 8 + +## Fonts +font.family : arial +font.weight : normal +font.size : 10 + +## Legends +legend.frameon : true +legend.fontsize : 10 +legend.edgecolor : 1 +legend.framealpha : 0.6 + +## Save figure +savefig.dpi : figure +savefig.format : png +savefig.transparent : false \ No newline at end of file diff --git a/plotAdsorbentCharacteristics.py b/plotAdsorbentCharacteristics.py new file mode 100755 index 0000000..a983be5 --- /dev/null +++ b/plotAdsorbentCharacteristics.py @@ -0,0 +1,104 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plots the adsorbent properties for the hypothetical materials +# +# Last modified: +# - 2020-10-27, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import os +from numpy import load +import matplotlib.pyplot as plt +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + +# Flag for saving figure +saveFlag = False + +# Save file extension (png or pdf) +saveFileExtension = ".png" + +# Select the id of gas that is to be plotted +gasID = 0 + +# For now load a given adsorbent isotherm material file +# loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases +loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases +hypoAdsorbentFile = os.path.join('inputResources',loadFileName); + +# Check if the file with the adsorbent properties exist +if os.path.exists(hypoAdsorbentFile): + loadedFileContent = load(hypoAdsorbentFile) + adsorbentIsothermTemp = loadedFileContent['adsIsotherm'] + adsorbentIsotherm = adsorbentIsothermTemp[gasID,:,:] + adsorbentDensity = loadedFileContent['adsDensity'] + molecularWeight = loadedFileContent['molWeight'] +else: + errorString = "Adsorbent property file " + hypoAdsorbentFile + " does not exist." + raise Exception(errorString) + +# Get the commit ID of the current repository +from getCommitID import getCommitID +gitCommitID = getCommitID() + +# Get the current date and time for saving purposes +from getCurrentDateTime import getCurrentDateTime +currentDT = getCurrentDateTime() + +# Git commit id of the loaded isotherm file +gitCommmitID_loadedFile = hypoAdsorbentFile[-11:-4] + +# Plot the pure single component isotherm for the n gases +colorVar = range(1,101) +fig = plt.figure +ax1 = plt.subplot(1,3,1) +s1 = ax1.scatter(adsorbentIsotherm[0,:], adsorbentIsotherm[1,:], c = colorVar, cmap='RdYlBu') +ax1.set(xlabel='$q_\mathregular{sat}$ [mol kg$^{\mathregular{-1}}$]', + ylabel='$b_\mathregular{0}$ [m$^{\mathregular{3}}$ mol$^{\mathregular{-1}}$]', + xlim = [0, 10], ylim = [0, 3e-6]) +ax1.locator_params(axis="x", nbins=4) +ax1.locator_params(axis="y", nbins=4) + +ax2 = plt.subplot(1,3,2) +s2 = ax2.scatter(adsorbentIsotherm[0,:], -adsorbentIsotherm[2,:], c = colorVar, cmap='RdYlBu') +ax2.set(xlabel='$q_\mathregular{sat}$ [mol kg$^{\mathregular{-1}}$]', + ylabel='-$\Delta H$ [J mol$^{\mathregular{-1}}$]', + xlim = [0, 10], ylim = [0, 4e4]) +ax2.locator_params(axis="x", nbins=4) +ax2.locator_params(axis="y", nbins=4) + +ax3 = plt.subplot(1,3,3) +s3 = ax3.scatter(adsorbentIsotherm[0,:], adsorbentDensity, c = colorVar, cmap='RdYlBu') +ax3.set(xlabel='$q_\mathregular{sat}$ [mol kg$^{\mathregular{-1}}$]', + ylabel='$\\rho$ [kg m$^{\mathregular{-3}}$]', + xlim = [0, 10], ylim = [500, 1500]) +ax3.locator_params(axis="x", nbins=4) +ax3.locator_params(axis="y", nbins=4) + +# Save the figure +if saveFlag: + # FileName: PureIsotherm____ + saveFileName = "AdsCharac_" + str(gasID) + "_" + currentDT + "_" + gitCommmitID_loadedFile + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists('simulationFigures'): + os.mkdir('simulationFigures') + plt.savefig (savePath) + +# For the figure to be saved show should appear after the save +plt.show() \ No newline at end of file diff --git a/plotSSLIsotherm.py b/plotSSLIsotherm.py index 36e18ac..bfedc60 100755 --- a/plotSSLIsotherm.py +++ b/plotSSLIsotherm.py @@ -12,6 +12,7 @@ # Plots the single site Langmuir isotherms # # Last modified: +# - 2020-10-27, AK: Further improvements and cosmetic changes # - 2020-10-26, AK: Initial creation # # Input arguments: @@ -26,11 +27,17 @@ import os from numpy import load from simulateSSL import simulateSSL -import matplotlib import matplotlib.pyplot as plt +plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file + +# Flag for saving figure +saveFlag = False + +# Save file extension (png or pdf) +saveFileExtension = ".pdf" # Sensor ID to be plotted -sensorID = np.array([19]) +sensorID = 50 # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]) @@ -59,7 +66,7 @@ ###### TO DO: SERIOUS ISSUE WITH THE ISOTHERM PLOTTING # Evaluate the isotherms -adsorbentID = sensorID +adsorbentID = np.array([sensorID]) # Do this for consistency adsorbentIsotherm = adsorbentIsothermTemp[:,:,adsorbentID] adsorbentDensity = adsorbentDensityTemp[adsorbentID] equilibriumLoadings = np.zeros([moleFraction.shape[1],adsorbentIsotherm.shape[0]]) @@ -70,21 +77,41 @@ equilibriumLoadings[:,ii] = np.squeeze(simulateSSL(adsorbentIsotherm[ii,:,:].T,adsorbentDensity, pressureTotal,temperature,moleFraction.T))/adsorbentDensity # [mol/m3] +# Get the commit ID of the current repository +from getCommitID import getCommitID +gitCommitID = getCommitID() -# Plot the pure single component isotherm for the n gases -fig, ax = plt.subplots() -font = {'family' : 'Helvetica', - 'weight' : 'normal', - 'size' : 16} -matplotlib.rc('font', **font) -# HARD CODED for 3 gases -ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,0],'r') -ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,1],'b') -ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,2],'g') +# Get the current date and time for saving purposes +from getCurrentDateTime import getCurrentDateTime +currentDT = getCurrentDateTime() +# Git commit id of the loaded isotherm file +gitCommmitID_loadedFile = hypoAdsorbentFile[-11:-4] -ax.set(xlabel='Pressure P [bar]', - ylabel='q [mol kg$^{\mathregular{-1}}$]', +# Plot the pure single component isotherm for the n gases +fig = plt.figure +ax = plt.gca() +# HARD CODED for 3 gases +ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,0], + linewidth=1.5,color='r', label = '$g_1$') +ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,1], + linewidth=1.5,color='b', label = '$g_2$') +ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,2], + linewidth=1.5,color='g', label = '$g_3$') +ax.set(xlabel='$P$ [bar]', + ylabel='$q^*$ [mol kg$^{\mathregular{-1}}$]', xlim = [0, 1], ylim = [0, 10]) -ax.grid() +ax.legend() + +# Save the figure +if saveFlag: + # FileName: PureIsotherm____ + saveFileName = "PureIsotherm_" + str(sensorID) + "_" + currentDT + "_" + gitCommmitID_loadedFile + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists('simulationFigures'): + os.mkdir('simulationFigures') + plt.savefig (savePath) + +# For the figure to be saved show should appear after the save plt.show() \ No newline at end of file diff --git a/singleColumn.mplstyle b/singleColumn.mplstyle new file mode 100755 index 0000000..3cdc0e7 --- /dev/null +++ b/singleColumn.mplstyle @@ -0,0 +1,44 @@ +# matplotlib configuration file for the ERASE project +# https://matplotlib.org/3.3.2/tutorials/introductory/customizing.html + +## Figure property +figure.figsize : 3.3, 3 # width, height in inches +figure.dpi : 600 # dpi +figure.autolayout : true # for labels not being cut out + +## Axes +axes.titlesize : 10 +axes.labelsize : 10 +axes.formatter.limits : -5, 3 + +## Grid +axes.grid : true +grid.color : cccccc +grid.linewidth : 0.5 + +## Lines & Scatter +lines.linewidth : 1.5 +lines.markersize : 4 +scatter.marker: o + +## Ticks +xtick.top : true +ytick.right : true +xtick.labelsize : 8 +ytick.labelsize : 8 + +## Fonts +font.family : arial +font.weight : normal +font.size : 10 + +## Legends +legend.frameon : true +legend.fontsize : 10 +legend.edgecolor : 1 +legend.framealpha : 0.6 + +## Save figure +savefig.dpi : figure +savefig.format : png +savefig.transparent : false \ No newline at end of file From 3a07f95f24e99d828072b39c2c8f76a0ec745101 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 27 Oct 2020 18:30:46 +0000 Subject: [PATCH 09/99] Change directory structure --- .../getCommitID.py | 0 .../getCurrentDateTime.py | 0 .../doubleColumn.mplstyle | 0 .../plotAdsorbentCharacteristics.py | 18 ++++++++++-------- .../plotSSLIsotherm.py | 18 ++++++++++-------- .../singleColumn.mplstyle | 0 6 files changed, 20 insertions(+), 16 deletions(-) rename getCommitID.py => auxiliaryFunctions/getCommitID.py (100%) rename getCurrentDateTime.py => auxiliaryFunctions/getCurrentDateTime.py (100%) rename doubleColumn.mplstyle => plotFunctions/doubleColumn.mplstyle (100%) rename plotAdsorbentCharacteristics.py => plotFunctions/plotAdsorbentCharacteristics.py (87%) rename plotSSLIsotherm.py => plotFunctions/plotSSLIsotherm.py (88%) rename singleColumn.mplstyle => plotFunctions/singleColumn.mplstyle (100%) diff --git a/getCommitID.py b/auxiliaryFunctions/getCommitID.py similarity index 100% rename from getCommitID.py rename to auxiliaryFunctions/getCommitID.py diff --git a/getCurrentDateTime.py b/auxiliaryFunctions/getCurrentDateTime.py similarity index 100% rename from getCurrentDateTime.py rename to auxiliaryFunctions/getCurrentDateTime.py diff --git a/doubleColumn.mplstyle b/plotFunctions/doubleColumn.mplstyle similarity index 100% rename from doubleColumn.mplstyle rename to plotFunctions/doubleColumn.mplstyle diff --git a/plotAdsorbentCharacteristics.py b/plotFunctions/plotAdsorbentCharacteristics.py similarity index 87% rename from plotAdsorbentCharacteristics.py rename to plotFunctions/plotAdsorbentCharacteristics.py index a983be5..b34267e 100755 --- a/plotAdsorbentCharacteristics.py +++ b/plotFunctions/plotAdsorbentCharacteristics.py @@ -23,15 +23,17 @@ ############################################################################ import os +import sys +sys.path.append('..') # Add path to search for packages and files from numpy import load import matplotlib.pyplot as plt plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file # Flag for saving figure -saveFlag = False +saveFlag = True # Save file extension (png or pdf) -saveFileExtension = ".png" +saveFileExtension = ".pdf" # Select the id of gas that is to be plotted gasID = 0 @@ -39,7 +41,7 @@ # For now load a given adsorbent isotherm material file # loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases -hypoAdsorbentFile = os.path.join('inputResources',loadFileName); +hypoAdsorbentFile = os.path.join('..','inputResources',loadFileName); # Check if the file with the adsorbent properties exist if os.path.exists(hypoAdsorbentFile): @@ -53,11 +55,11 @@ raise Exception(errorString) # Get the commit ID of the current repository -from getCommitID import getCommitID +from auxiliaryFunctions.getCommitID import getCommitID gitCommitID = getCommitID() # Get the current date and time for saving purposes -from getCurrentDateTime import getCurrentDateTime +from auxiliaryFunctions.getCurrentDateTime import getCurrentDateTime currentDT = getCurrentDateTime() # Git commit id of the loaded isotherm file @@ -94,10 +96,10 @@ if saveFlag: # FileName: PureIsotherm____ saveFileName = "AdsCharac_" + str(gasID) + "_" + currentDT + "_" + gitCommmitID_loadedFile + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('simulationFigures',saveFileName) + savePath = os.path.join('..','simulationFigures',saveFileName) # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists('simulationFigures'): - os.mkdir('simulationFigures') + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) # For the figure to be saved show should appear after the save diff --git a/plotSSLIsotherm.py b/plotFunctions/plotSSLIsotherm.py similarity index 88% rename from plotSSLIsotherm.py rename to plotFunctions/plotSSLIsotherm.py index bfedc60..0910bf2 100755 --- a/plotSSLIsotherm.py +++ b/plotFunctions/plotSSLIsotherm.py @@ -25,16 +25,18 @@ import numpy as np import os +import sys +sys.path.append('..') # Add path to search for packages and files (one level up) from numpy import load from simulateSSL import simulateSSL import matplotlib.pyplot as plt plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file # Flag for saving figure -saveFlag = False +saveFlag = True # Save file extension (png or pdf) -saveFileExtension = ".pdf" +saveFileExtension = ".png" # Sensor ID to be plotted sensorID = 50 @@ -52,7 +54,7 @@ # For now load a given adsorbent isotherm material file # loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases -hypoAdsorbentFile = os.path.join('inputResources',loadFileName); +hypoAdsorbentFile = os.path.join('..','inputResources',loadFileName); # Check if the file with the adsorbent properties exist if os.path.exists(hypoAdsorbentFile): @@ -78,11 +80,11 @@ pressureTotal,temperature,moleFraction.T))/adsorbentDensity # [mol/m3] # Get the commit ID of the current repository -from getCommitID import getCommitID +from auxiliaryFunctions.getCommitID import getCommitID gitCommitID = getCommitID() # Get the current date and time for saving purposes -from getCurrentDateTime import getCurrentDateTime +from auxiliaryFunctions.getCurrentDateTime import getCurrentDateTime currentDT = getCurrentDateTime() # Git commit id of the loaded isotherm file @@ -107,10 +109,10 @@ if saveFlag: # FileName: PureIsotherm____ saveFileName = "PureIsotherm_" + str(sensorID) + "_" + currentDT + "_" + gitCommmitID_loadedFile + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('simulationFigures',saveFileName) + savePath = os.path.join('..','simulationFigures',saveFileName) # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists('simulationFigures'): - os.mkdir('simulationFigures') + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) # For the figure to be saved show should appear after the save diff --git a/singleColumn.mplstyle b/plotFunctions/singleColumn.mplstyle similarity index 100% rename from singleColumn.mplstyle rename to plotFunctions/singleColumn.mplstyle From cc6aa34b1acdda3150bfcf48e536d1678247ae67 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 28 Oct 2020 18:13:44 +0000 Subject: [PATCH 10/99] Add auxiliary functions as a module --- auxiliaryFunctions/__init__.py | 1 + generateHypotheticalAdsorbents.py | 8 ++++---- plotFunctions/plotAdsorbentCharacteristics.py | 12 +++++------- plotFunctions/plotSSLIsotherm.py | 10 ++++------ screenSensorArray.py | 8 ++++---- simulateSensorArray.py | 2 +- 6 files changed, 19 insertions(+), 22 deletions(-) create mode 100755 auxiliaryFunctions/__init__.py diff --git a/auxiliaryFunctions/__init__.py b/auxiliaryFunctions/__init__.py new file mode 100755 index 0000000..4717a6e --- /dev/null +++ b/auxiliaryFunctions/__init__.py @@ -0,0 +1 @@ +# __init__.py from .getCommitID import getCommitID from .getCurrentDateTime import getCurrentDateTime \ No newline at end of file diff --git a/generateHypotheticalAdsorbents.py b/generateHypotheticalAdsorbents.py index e3ae50e..86fdd1f 100755 --- a/generateHypotheticalAdsorbents.py +++ b/generateHypotheticalAdsorbents.py @@ -13,6 +13,7 @@ # sorbents are assumed to exhibit Langmuirian behavior. # # Last modified: +# - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-19, AK: Add adsorbent density and molecular weight # - 2020-10-19, AK: Integrate git commit and save material properties # - 2020-10-16, AK: Initial creation @@ -28,16 +29,15 @@ def generateHypotheticalAdsorbents(numberOfGases, numberOfAdsorbents): import numpy as np from numpy import savez + import auxiliaryFunctions from smt.sampling_methods import LHS import os # For OS related stuff (make directory, file separator, etc.) # Get the commit ID of the current repository - from getCommitID import getCommitID - gitCommitID = getCommitID() + gitCommitID = auxiliaryFunctions.getCommitID() # Get the current date and time for saving purposes - from getCurrentDateTime import getCurrentDateTime - simulationDT = getCurrentDateTime() + simulationDT = auxiliaryFunctions.getCurrentDateTime() # Define a range for single site Langmuir isotherm for one sensor material # [qsat [mol/kg] b0 [m3/mol] delH [J/mol]] diff --git a/plotFunctions/plotAdsorbentCharacteristics.py b/plotFunctions/plotAdsorbentCharacteristics.py index b34267e..f67dc23 100755 --- a/plotFunctions/plotAdsorbentCharacteristics.py +++ b/plotFunctions/plotAdsorbentCharacteristics.py @@ -12,6 +12,7 @@ # Plots the adsorbent properties for the hypothetical materials # # Last modified: +# - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-27, AK: Initial creation # # Input arguments: @@ -23,14 +24,13 @@ ############################################################################ import os -import sys -sys.path.append('..') # Add path to search for packages and files from numpy import load +import auxiliaryFunctions import matplotlib.pyplot as plt plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file # Flag for saving figure -saveFlag = True +saveFlag = False # Save file extension (png or pdf) saveFileExtension = ".pdf" @@ -55,12 +55,10 @@ raise Exception(errorString) # Get the commit ID of the current repository -from auxiliaryFunctions.getCommitID import getCommitID -gitCommitID = getCommitID() +gitCommitID = auxiliaryFunctions.getCommitID() # Get the current date and time for saving purposes -from auxiliaryFunctions.getCurrentDateTime import getCurrentDateTime -currentDT = getCurrentDateTime() +currentDT = auxiliaryFunctions.getCurrentDateTime() # Git commit id of the loaded isotherm file gitCommmitID_loadedFile = hypoAdsorbentFile[-11:-4] diff --git a/plotFunctions/plotSSLIsotherm.py b/plotFunctions/plotSSLIsotherm.py index 0910bf2..83f48bd 100755 --- a/plotFunctions/plotSSLIsotherm.py +++ b/plotFunctions/plotSSLIsotherm.py @@ -12,6 +12,7 @@ # Plots the single site Langmuir isotherms # # Last modified: +# - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-27, AK: Further improvements and cosmetic changes # - 2020-10-26, AK: Initial creation # @@ -25,8 +26,7 @@ import numpy as np import os -import sys -sys.path.append('..') # Add path to search for packages and files (one level up) +import auxiliaryFunctions from numpy import load from simulateSSL import simulateSSL import matplotlib.pyplot as plt @@ -80,12 +80,10 @@ pressureTotal,temperature,moleFraction.T))/adsorbentDensity # [mol/m3] # Get the commit ID of the current repository -from auxiliaryFunctions.getCommitID import getCommitID -gitCommitID = getCommitID() +gitCommitID = auxiliaryFunctions.getCommitID() # Get the current date and time for saving purposes -from auxiliaryFunctions.getCurrentDateTime import getCurrentDateTime -currentDT = getCurrentDateTime() +currentDT = auxiliaryFunctions.getCurrentDateTime() # Git commit id of the loaded isotherm file gitCommmitID_loadedFile = hypoAdsorbentFile[-11:-4] diff --git a/screenSensorArray.py b/screenSensorArray.py index 16328b1..02a2bbf 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -13,6 +13,7 @@ # sorbents are assumed to exhibit Langmuirian behavior. # # Last modified: +# - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-22, AK: Initial creation # # Input arguments: @@ -25,6 +26,7 @@ import numpy as np from numpy import save +import auxiliaryFunctions import multiprocessing # For parallel processing from joblib import Parallel, delayed # For parallel processing from tqdm import tqdm # To track progress of the loop @@ -32,8 +34,7 @@ import os # Get the commit ID of the current repository -from getCommitID import getCommitID -gitCommitID = getCommitID() +gitCommitID = auxiliaryFunctions.getCommitID() # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() @@ -49,8 +50,7 @@ ##### FOR 1 SORBENT SENSOR ARRAY ##### # Get the current date and time for saving purposes -from getCurrentDateTime import getCurrentDateTime -simulationDT = getCurrentDateTime() +simulationDT = auxiliaryFunctions.getCurrentDateTime() # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials diff --git a/simulateSensorArray.py b/simulateSensorArray.py index 2af6312..b57f214 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -32,7 +32,7 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction): from numpy import load import os from simulateSSL import simulateSSL - + # For now load a given adsorbent isotherm material file # loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases From a726870e6b1f35b94b4dfd09bafcc3127d88a9ee Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 28 Oct 2020 19:15:36 +0000 Subject: [PATCH 11/99] Add new plotting and minor save file name changes --- plotFunctions/plotAdsorbentCharacteristics.py | 11 +- plotFunctions/plotConcentrationEstimate.py | 121 ++++++++++++++++++ plotFunctions/plotSSLIsotherm.py | 5 +- screenSensorArray.py | 3 +- 4 files changed, 133 insertions(+), 7 deletions(-) create mode 100755 plotFunctions/plotConcentrationEstimate.py diff --git a/plotFunctions/plotAdsorbentCharacteristics.py b/plotFunctions/plotAdsorbentCharacteristics.py index f67dc23..fd2c91d 100755 --- a/plotFunctions/plotAdsorbentCharacteristics.py +++ b/plotFunctions/plotAdsorbentCharacteristics.py @@ -12,6 +12,7 @@ # Plots the adsorbent properties for the hypothetical materials # # Last modified: +# - 2020-10-28, AK: Minor fix for save file name # - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-27, AK: Initial creation # @@ -63,9 +64,11 @@ # Git commit id of the loaded isotherm file gitCommmitID_loadedFile = hypoAdsorbentFile[-11:-4] -# Plot the pure single component isotherm for the n gases +# Scatter plot for qsat vs b0, delH, and rho colorVar = range(1,101) fig = plt.figure + +# qsat vs b0 ax1 = plt.subplot(1,3,1) s1 = ax1.scatter(adsorbentIsotherm[0,:], adsorbentIsotherm[1,:], c = colorVar, cmap='RdYlBu') ax1.set(xlabel='$q_\mathregular{sat}$ [mol kg$^{\mathregular{-1}}$]', @@ -74,6 +77,7 @@ ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) +# qsat vs delH ax2 = plt.subplot(1,3,2) s2 = ax2.scatter(adsorbentIsotherm[0,:], -adsorbentIsotherm[2,:], c = colorVar, cmap='RdYlBu') ax2.set(xlabel='$q_\mathregular{sat}$ [mol kg$^{\mathregular{-1}}$]', @@ -82,6 +86,7 @@ ax2.locator_params(axis="x", nbins=4) ax2.locator_params(axis="y", nbins=4) +# qsat vs rho ax3 = plt.subplot(1,3,3) s3 = ax3.scatter(adsorbentIsotherm[0,:], adsorbentDensity, c = colorVar, cmap='RdYlBu') ax3.set(xlabel='$q_\mathregular{sat}$ [mol kg$^{\mathregular{-1}}$]', @@ -92,8 +97,8 @@ # Save the figure if saveFlag: - # FileName: PureIsotherm____ - saveFileName = "AdsCharac_" + str(gasID) + "_" + currentDT + "_" + gitCommmitID_loadedFile + "_" + gitCommitID + saveFileExtension + # FileName: AdsCharac____ + saveFileName = "AdsCharac_" + str(gasID) + "_" + currentDT + "_" + gitCommitID + "_" + gitCommmitID_loadedFile + saveFileExtension savePath = os.path.join('..','simulationFigures',saveFileName) # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures')): diff --git a/plotFunctions/plotConcentrationEstimate.py b/plotFunctions/plotConcentrationEstimate.py new file mode 100755 index 0000000..c0fff4b --- /dev/null +++ b/plotFunctions/plotConcentrationEstimate.py @@ -0,0 +1,121 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plots to visualize estimated concentration +# +# Last modified: +# - 2020-10-23, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from numpy import load +import os +import auxiliaryFunctions +import matplotlib.pyplot as plt +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + +# Flag for saving figure +saveFlag = False + +# Save file extension (png or pdf) +saveFileExtension = ".png" + +# Xlimits and Ylimits +xLimits = [0,1] +xLimitsSum = [0,2] +yLimits = [0,40] + +# Number of bins for histogram +nBins = 10 + +# For now load a given adsorbent isotherm material file +# loadFileName = "arrayConcentration_20201026_1239_6bf5c35.npy" # Three gases +loadFileName = "arrayConcentration_20201026_1240_6bf5c35.npy" # Two sensors +simResultsFile = os.path.join('..','simulationResults',loadFileName); + +# Check if the file with the adsorbent properties exist +if os.path.exists(simResultsFile): + resultOutput = load(simResultsFile) + if resultOutput.shape[1] == 4: + resultOutput = np.delete(resultOutput,0,1) + elif resultOutput.shape[1] == 5: + resultOutput = np.delete(resultOutput,[0,1],1) +else: + errorString = "Simulation result file " + simResultsFile + " does not exist." + raise Exception(errorString) + +# Plot the pure single component isotherm for the n gases +fig = plt.figure +# Histogram for gas 1 +ax1 = plt.subplot(1,4,1) +ax1.hist(resultOutput[:,0], bins = nBins, + linewidth=1.5, histtype = 'step', color='r', label = '$g_1$') +ax1.set(xlabel='$y_1$ [-]', + ylabel='$f$ [-]', + xlim = xLimits, ylim = yLimits) +ax1.locator_params(axis="x", nbins=4) +ax1.locator_params(axis="y", nbins=4) + +# Histogram for gas 2 +ax2 = plt.subplot(1,4,2) +ax2.hist(resultOutput[:,1], bins = nBins, + linewidth=1.5, histtype = 'step', color='b', label = '$g_2$') +ax2.set(xlabel='$y_2$ [-]', + xlim = xLimits, ylim = yLimits) +ax2.locator_params(axis="x", nbins=4) +ax2.locator_params(axis="y", nbins=4) + +# Histogram for gas 3 +ax3 = plt.subplot(1,4,3) +ax3.hist(resultOutput[:,2], bins = nBins, + linewidth=1.5, histtype = 'step', color='g', label = '$g_3$') +ax3.set(xlabel='$y_3$ [-]', + xlim = xLimits, ylim = yLimits) +ax3.locator_params(axis="x", nbins=4) +ax3.locator_params(axis="y", nbins=4) + +# Histogram for the sum of mole fraction +ax3 = plt.subplot(1,4,4) +ax3.hist(np.sum(resultOutput,1), bins = nBins, + linewidth=1.5, histtype = 'step', color='k') +ax3.set(xlabel='$\Sigma y$ [-]', + xlim = xLimitsSum, ylim = yLimits) +ax3.locator_params(axis="x", nbins=4) +ax3.locator_params(axis="y", nbins=4) + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Git commit id of the loaded isotherm file +simID_loadedFile = loadFileName[-21:-4] + +# Save the figure +if saveFlag: + # FileName: ConcEstimate___ + saveFileName = "ConcEstimate_" + currentDT + "_" + gitCommitID + "_" + simID_loadedFile + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + +# For the figure to be saved show should appear after the save +plt.show() \ No newline at end of file diff --git a/plotFunctions/plotSSLIsotherm.py b/plotFunctions/plotSSLIsotherm.py index 83f48bd..a646f1f 100755 --- a/plotFunctions/plotSSLIsotherm.py +++ b/plotFunctions/plotSSLIsotherm.py @@ -12,6 +12,7 @@ # Plots the single site Langmuir isotherms # # Last modified: +# - 2020-10-28, AK: Minor fix for save file name # - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-27, AK: Further improvements and cosmetic changes # - 2020-10-26, AK: Initial creation @@ -33,7 +34,7 @@ plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file # Flag for saving figure -saveFlag = True +saveFlag = False # Save file extension (png or pdf) saveFileExtension = ".png" @@ -106,7 +107,7 @@ # Save the figure if saveFlag: # FileName: PureIsotherm____ - saveFileName = "PureIsotherm_" + str(sensorID) + "_" + currentDT + "_" + gitCommmitID_loadedFile + "_" + gitCommitID + saveFileExtension + saveFileName = "PureIsotherm_" + str(sensorID) + "_" + currentDT + "_" + gitCommitID + "_" + gitCommmitID_loadedFile + saveFileExtension savePath = os.path.join('..','simulationFigures',saveFileName) # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures')): diff --git a/screenSensorArray.py b/screenSensorArray.py index 02a2bbf..104dc7e 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -77,8 +77,7 @@ ##### FOR 2 SORBENT SENSOR ARRAY ##### # Get the current date and time for saving purposes -from getCurrentDateTime import getCurrentDateTime -simulationDT = getCurrentDateTime() +simulationDT = auxiliaryFunctions.getCurrentDateTime() # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials From 5c77a621f1f57b9747169ef5552792490ba4aeeb Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 29 Oct 2020 19:13:53 +0000 Subject: [PATCH 12/99] Add 3 sensor sorbent and improve plotting --- plotFunctions/plotConcentrationEstimate.py | 125 +++++++++++------ screenSensorArray.py | 154 +++++++++++++-------- 2 files changed, 180 insertions(+), 99 deletions(-) diff --git a/plotFunctions/plotConcentrationEstimate.py b/plotFunctions/plotConcentrationEstimate.py index c0fff4b..21cd0ce 100755 --- a/plotFunctions/plotConcentrationEstimate.py +++ b/plotFunctions/plotConcentrationEstimate.py @@ -12,6 +12,7 @@ # Plots to visualize estimated concentration # # Last modified: +# - 2020-10-29, AK: Improvements to the plots # - 2020-10-23, AK: Initial creation # # Input arguments: @@ -27,7 +28,7 @@ import os import auxiliaryFunctions import matplotlib.pyplot as plt -plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file +plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file # Flag for saving figure saveFlag = False @@ -35,19 +36,51 @@ # Save file extension (png or pdf) saveFileExtension = ".png" +# Plot zoomed plot +plotZoomDist = False + +# Gas concentration +molFracG1 = 0.05 +molFracG2 = 0.15 +molFracG3 = 1 - molFracG1 - molFracG2 + # Xlimits and Ylimits xLimits = [0,1] +yLimits = [0,100] xLimitsSum = [0,2] -yLimits = [0,40] +yLimitsSum = [0,80] + +xLimitsZ1 = [0,0.1] # Limits for zoom gas 1 +xLimitsZ2 = [0.05,0.25] # Limits for zoom gas 2 +xLimitsZ3 = [0.75,0.85] # Limits for zoom gas 3 +yLimitsZ1 = [0,5] # Limits for zoom gas 1 +yLimitsZ2 = [0,5] # Limits for zoom gas 2 +yLimitsZ3 = [0,5] # Limits for zoom gas 3 +xLimitsSumZS = [0.9,1.1] # Limits for zoom gas sum +yLimitsSumZS = [0,5] # Limits for zoom gas sum -# Number of bins for histogram -nBins = 10 +# Histogram properties +nBins = 100 +rangeX = (xLimits) +rangeXS = (xLimitsSum) +histTypeX = 'stepfilled' +alphaX=0.5 +densityX = False # For now load a given adsorbent isotherm material file -# loadFileName = "arrayConcentration_20201026_1239_6bf5c35.npy" # Three gases loadFileName = "arrayConcentration_20201026_1240_6bf5c35.npy" # Two sensors +# loadFileName = "arrayConcentration_20201029_1227_a726870.npy" # Three sensors simResultsFile = os.path.join('..','simulationResults',loadFileName); +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Git commit id of the loaded isotherm file +simID_loadedFile = loadFileName[-21:-4] + # Check if the file with the adsorbent properties exist if os.path.exists(simResultsFile): resultOutput = load(simResultsFile) @@ -55,6 +88,8 @@ resultOutput = np.delete(resultOutput,0,1) elif resultOutput.shape[1] == 5: resultOutput = np.delete(resultOutput,[0,1],1) + elif resultOutput.shape[1] == 6: + resultOutput = np.delete(resultOutput,[0,1,2],1) else: errorString = "Simulation result file " + simResultsFile + " does not exist." raise Exception(errorString) @@ -62,55 +97,59 @@ # Plot the pure single component isotherm for the n gases fig = plt.figure # Histogram for gas 1 -ax1 = plt.subplot(1,4,1) -ax1.hist(resultOutput[:,0], bins = nBins, - linewidth=1.5, histtype = 'step', color='r', label = '$g_1$') -ax1.set(xlabel='$y_1$ [-]', - ylabel='$f$ [-]', - xlim = xLimits, ylim = yLimits) -ax1.locator_params(axis="x", nbins=4) -ax1.locator_params(axis="y", nbins=4) +ax1 = plt.subplot(1,1,1) +ax1.hist(resultOutput[:,0], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='r', alpha = alphaX, label = '$g_1$') +ax1.axvline(x=molFracG1, linewidth=1, linestyle='dotted', color = 'r', alpha = 0.6) # Histogram for gas 2 -ax2 = plt.subplot(1,4,2) -ax2.hist(resultOutput[:,1], bins = nBins, - linewidth=1.5, histtype = 'step', color='b', label = '$g_2$') -ax2.set(xlabel='$y_2$ [-]', - xlim = xLimits, ylim = yLimits) -ax2.locator_params(axis="x", nbins=4) -ax2.locator_params(axis="y", nbins=4) +# ax2 = plt.subplot(1,4,2) +ax1.hist(resultOutput[:,1], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='b', alpha = alphaX, label = '$g_2$') +ax1.axvline(x=molFracG2, linewidth=1, linestyle='dotted', color = 'b', alpha = 0.6) # Histogram for gas 3 -ax3 = plt.subplot(1,4,3) -ax3.hist(resultOutput[:,2], bins = nBins, - linewidth=1.5, histtype = 'step', color='g', label = '$g_3$') -ax3.set(xlabel='$y_3$ [-]', - xlim = xLimits, ylim = yLimits) -ax3.locator_params(axis="x", nbins=4) -ax3.locator_params(axis="y", nbins=4) - -# Histogram for the sum of mole fraction -ax3 = plt.subplot(1,4,4) -ax3.hist(np.sum(resultOutput,1), bins = nBins, - linewidth=1.5, histtype = 'step', color='k') -ax3.set(xlabel='$\Sigma y$ [-]', - xlim = xLimitsSum, ylim = yLimits) -ax3.locator_params(axis="x", nbins=4) -ax3.locator_params(axis="y", nbins=4) +# ax3 = plt.subplot(1,4,3) +ax1.hist(resultOutput[:,2], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='g', alpha = alphaX, label = '$g_3$') +ax1.axvline(x=molFracG3, linewidth=1, linestyle='dotted', color = 'g', alpha = 0.6) -# Get the commit ID of the current repository -gitCommitID = auxiliaryFunctions.getCommitID() +ax1.locator_params(axis="x", nbins=4) +ax1.locator_params(axis="y", nbins=4) +ax1.set(xlabel='$y_i$ [-]', + ylabel='$f$ [-]', + xlim = xLimits, ylim = yLimits) +ax1.legend() -# Get the current date and time for saving purposes -currentDT = auxiliaryFunctions.getCurrentDateTime() +# Save the figure +if saveFlag: + # FileName: ConcEstimate___ + saveFileName = "ConcEstimate_" + currentDT + "_" + gitCommitID + "_" + simID_loadedFile + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + + +# For the figure to be saved show should appear after the save +plt.show() -# Git commit id of the loaded isotherm file -simID_loadedFile = loadFileName[-21:-4] +# Histogram for the sum of mole fraction +fig = plt.figure +ax2 = plt.subplot(1,1,1) +ax2.hist(np.sum(resultOutput,1), bins = nBins, range = rangeXS, density = densityX, + linewidth=1.5, histtype = histTypeX, color='k', alpha = alphaX) +ax2.axvline(x=1., linewidth=1, linestyle='dotted', color = 'k') +ax2.set(xlabel='$\Sigma y_i$ [-]', + xlim = xLimitsSum, ylim = yLimitsSum) +ax2.locator_params(axis="x", nbins=4) +ax2.locator_params(axis="y", nbins=4) # Save the figure if saveFlag: # FileName: ConcEstimate___ - saveFileName = "ConcEstimate_" + currentDT + "_" + gitCommitID + "_" + simID_loadedFile + saveFileExtension + saveFileName = "ConcEstimateSum_" + currentDT + "_" + gitCommitID + "_" + simID_loadedFile + saveFileExtension savePath = os.path.join('..','simulationFigures',saveFileName) # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures')): diff --git a/screenSensorArray.py b/screenSensorArray.py index 104dc7e..a084f79 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -13,6 +13,7 @@ # sorbents are assumed to exhibit Langmuirian behavior. # # Last modified: +# - 2020-10-29, AK: Add 3 sorbent sensor # - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-22, AK: Initial creation # @@ -33,6 +34,9 @@ from estimateConcentration import estimateConcentration import os +# Number of sensors in the array +numSensors = 3 + # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -48,64 +52,102 @@ # Check generateTrueSensorResponse.py for the actual concentrations moleFracID = 0 -##### FOR 1 SORBENT SENSOR ARRAY ##### -# Get the current date and time for saving purposes -simulationDT = auxiliaryFunctions.getCurrentDateTime() - -# Loop over all the sorbents for a single material sensor -# Using parallel processing to loop through all the materials -arrayConcentration = np.zeros(numberOfAdsorbents) -arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) - (numberOfAdsorbents,numberOfGases,moleFracID,[ii]) - for ii in tqdm(range(numberOfAdsorbents))) - -# Convert the output list to a matrix -arrayConcentration = np.array(arrayConcentration) - -# Save the array concentration into a native numpy file -# The .npy file is saved in a folder called simulationResults (hardcoded) -filePrefix = "arrayConcentration" -saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; -savePath = os.path.join('simulationResults',saveFileName) - -# Check if inputResources directory exists or not. If not, create the folder -if not os.path.exists('simulationResults'): - os.mkdir('simulationResults') - -# Save the array ceoncentration obtained from estimateConcentration -save (savePath, arrayConcentration) - -##### FOR 2 SORBENT SENSOR ARRAY ##### -# Get the current date and time for saving purposes -simulationDT = auxiliaryFunctions.getCurrentDateTime() - -# Loop over all the sorbents for a single material sensor -# Using parallel processing to loop through all the materials -arrayConcentration = np.zeros(numberOfAdsorbents) -for jj in range(numberOfAdsorbents): - arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) - (numberOfAdsorbents,numberOfGases,moleFracID,[ii,jj]) - for ii in tqdm(range(jj,numberOfAdsorbents))) +# Check for number of sensors in the array +if numSensors == 1: + ##### FOR 1 SORBENT SENSOR ARRAY ##### + # Get the current date and time for saving purposes + simulationDT = auxiliaryFunctions.getCurrentDateTime() + + # Loop over all the sorbents for a single material sensor + # Using parallel processing to loop through all the materials + arrayConcentration = np.zeros(numberOfAdsorbents) + arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + (numberOfAdsorbents,numberOfGases,moleFracID,[ii]) + for ii in tqdm(range(numberOfAdsorbents))) + # Convert the output list to a matrix - arrayConcentrationTemp = np.array(arrayConcentrationTemp) - if jj == 0: - arrayConcentration = arrayConcentrationTemp - else: - arrayConcentration = np.append(arrayConcentration,arrayConcentrationTemp, axis=0) + arrayConcentration = np.array(arrayConcentration) -# Delete entries that use the same materials for both sensors -delRows = np.where(arrayConcentration[:,0] == arrayConcentration[:,1]) -arrayConcentration = np.delete(arrayConcentration,delRows,axis=0) + # Save the array concentration into a native numpy file + # The .npy file is saved in a folder called simulationResults (hardcoded) + filePrefix = "arrayConcentration" + saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; + savePath = os.path.join('simulationResults',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') + + # Save the array ceoncentration obtained from estimateConcentration + save (savePath, arrayConcentration) -# Save the array concentration into a native numpy file -# The .npy file is saved in a folder called simulationResults (hardcoded) -filePrefix = "arrayConcentration" -saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; -savePath = os.path.join('simulationResults',saveFileName) +elif numSensors == 2: + ##### FOR 2 SORBENT SENSOR ARRAY ##### + # Get the current date and time for saving purposes + simulationDT = auxiliaryFunctions.getCurrentDateTime() + + # Loop over all the sorbents for a single material sensor + # Using parallel processing to loop through all the materials + arrayConcentration = np.zeros(numberOfAdsorbents) + for jj in range(numberOfAdsorbents-1): + arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + (numberOfAdsorbents,numberOfGases,moleFracID,[ii,jj]) + for ii in tqdm(range(jj+1,numberOfAdsorbents))) + # Convert the output list to a matrix + arrayConcentrationTemp = np.array(arrayConcentrationTemp) + if jj == 0: + arrayConcentration = arrayConcentrationTemp + else: + arrayConcentration = np.append(arrayConcentration,arrayConcentrationTemp, axis=0) + + # Save the array concentration into a native numpy file + # The .npy file is saved in a folder called simulationResults (hardcoded) + filePrefix = "arrayConcentration" + saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; + savePath = os.path.join('simulationResults',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') + + # Save the array ceoncentration obtained from estimateConcentration + save (savePath, arrayConcentration) -# Check if inputResources directory exists or not. If not, create the folder -if not os.path.exists('simulationResults'): - os.mkdir('simulationResults') +elif numSensors == 3: + ##### FOR 3 SORBENT SENSOR ARRAY ##### + # Get the current date and time for saving purposes + simulationDT = auxiliaryFunctions.getCurrentDateTime() + + # Loop over all the sorbents for a single material sensor + # Using parallel processing to loop through all the materials + arrayConcentration = np.zeros(numberOfAdsorbents) -# Save the array ceoncentration obtained from estimateConcentration -save (savePath, arrayConcentration) \ No newline at end of file + for kk in range(numberOfAdsorbents-1): + for jj in range(kk+1,numberOfAdsorbents-1): + arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + (numberOfAdsorbents,numberOfGases,moleFracID,[ii,jj,kk]) + for ii in tqdm(range(jj+1,numberOfAdsorbents))) + # Convert the output list to a matrix + arrayConcentrationTemp = np.array(arrayConcentrationTemp) + if kk == 0 and jj == 1: + arrayConcentration = arrayConcentrationTemp + else: + arrayConcentration = np.append(arrayConcentration,arrayConcentrationTemp, axis=0) + + # Delete entries that use the same materials for all three sensors + # delRows = np.where(np.logical_and(arrayConcentration[:,0] == arrayConcentration[:,1], + # arrayConcentration[:,0] == arrayConcentration[:,2])) + # arrayConcentration = np.delete(arrayConcentration,delRows,axis=0) + + # Save the array concentration into a native numpy file + # The .npy file is saved in a folder called simulationResults (hardcoded) + filePrefix = "arrayConcentration" + saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; + savePath = os.path.join('simulationResults',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') + + # Save the array ceoncentration obtained from estimateConcentration + save (savePath, arrayConcentration) \ No newline at end of file From da1707bcac353ed93c77c3e56f875213888454ae Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 30 Oct 2020 15:36:55 +0000 Subject: [PATCH 13/99] Fix to find number of gases and add zoom plot --- estimateConcentration.py | 4 +- generateTrueSensorResponse.py | 25 +++--- plotFunctions/plotConcentrationEstimate.py | 94 ++++++++++++++++++---- screenSensorArray.py | 4 +- simulateSensorArray.py | 7 +- 5 files changed, 103 insertions(+), 31 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index a9b30cc..55dffc0 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,7 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2020-10-30, AK: Fix to find number of gases # - 2020-10-22, AK: Change error to relative from absolute, add opt bounds, # input arguments, and initial guess # - 2020-10-21, AK: Initial creation @@ -44,7 +45,8 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI temperature = np.array([298.15]); # Get the individual sensor reponse for all the given "experimental/test" concentrations - sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,pressureTotal,temperature) + sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, + pressureTotal,temperature) # True mole fraction index (provide the index corresponding to the true # experimental mole fraction (0-4, check generateTrueSensorResponse.py) diff --git a/generateTrueSensorResponse.py b/generateTrueSensorResponse.py index 162b914..69fd056 100755 --- a/generateTrueSensorResponse.py +++ b/generateTrueSensorResponse.py @@ -14,6 +14,7 @@ # and should be modified. # # Last modified: +# - 2020-10-30, AK: Fix to find number of gases # - 2020-10-22, AK: Add two/three gases # - 2020-10-21, AK: Initial creation # @@ -25,22 +26,24 @@ # ############################################################################ -def generateTrueSensorResponse(numberOfAdsorbents, pressureTotal, temperature): +def generateTrueSensorResponse(numberOfAdsorbents, numberOfGases, pressureTotal, temperature): import numpy as np from simulateSensorArray import simulateSensorArray # Mole fraction of the gas [-] # Can be [jxg], where j is the number of mole fractions for g gases - # moleFraction = np.array([[0.05, 0.95], - # [0.15, 0.85], - # [0.40, 0.60], - # [0.75, 0.25], - # [0.90, 0.10]]) - moleFraction = np.array([[0.05, 0.15, 0.80], - [0.15, 0.25, 0.60], - [0.40, 0.35, 0.25], - [0.75, 0.10, 0.15], - [0.90, 0.05, 0.05]]) + if numberOfGases == 2: + moleFraction = np.array([[0.05, 0.95], + [0.15, 0.85], + [0.40, 0.60], + [0.75, 0.25], + [0.90, 0.10]]) + elif numberOfGases == 3: + moleFraction = np.array([[0.05, 0.15, 0.80], + [0.15, 0.25, 0.60], + [0.40, 0.35, 0.25], + [0.75, 0.10, 0.15], + [0.90, 0.05, 0.05]]) # Get the individual sensor reponse for all the five "test" concentrations sensorTrueResponse = np.zeros((numberOfAdsorbents,moleFraction.shape[0])) diff --git a/plotFunctions/plotConcentrationEstimate.py b/plotFunctions/plotConcentrationEstimate.py index 21cd0ce..709d277 100755 --- a/plotFunctions/plotConcentrationEstimate.py +++ b/plotFunctions/plotConcentrationEstimate.py @@ -12,6 +12,7 @@ # Plots to visualize estimated concentration # # Last modified: +# - 2020-10-30, AK: Add zoomed in version # - 2020-10-29, AK: Improvements to the plots # - 2020-10-23, AK: Initial creation # @@ -46,18 +47,14 @@ # Xlimits and Ylimits xLimits = [0,1] -yLimits = [0,100] -xLimitsSum = [0,2] -yLimitsSum = [0,80] - -xLimitsZ1 = [0,0.1] # Limits for zoom gas 1 -xLimitsZ2 = [0.05,0.25] # Limits for zoom gas 2 -xLimitsZ3 = [0.75,0.85] # Limits for zoom gas 3 -yLimitsZ1 = [0,5] # Limits for zoom gas 1 -yLimitsZ2 = [0,5] # Limits for zoom gas 2 -yLimitsZ3 = [0,5] # Limits for zoom gas 3 -xLimitsSumZS = [0.9,1.1] # Limits for zoom gas sum -yLimitsSumZS = [0,5] # Limits for zoom gas sum +yLimits = [0,6] +xLimitsSum = [0,3] +yLimitsSum = [0,30] + +xLimitsZ = [0,1] # Limits for zoom distribution +yLimitsZ = [0,15] # Limits for zoom distribution +xLimitsSumZ = [0,2] # Limits for zoom gas sum +yLimitsSumZ = [0,15] # Limits for zoom gas sum # Histogram properties nBins = 100 @@ -68,8 +65,9 @@ densityX = False # For now load a given adsorbent isotherm material file -loadFileName = "arrayConcentration_20201026_1240_6bf5c35.npy" # Two sensors -# loadFileName = "arrayConcentration_20201029_1227_a726870.npy" # Three sensors +# loadFileName = "arrayConcentration_20201030_1109_5c77a62.npy" # One sensor +# loadFileName = "arrayConcentration_20201030_0913_5c77a62.npy" # Two sensors +loadFileName = "arrayConcentration_20201029_2328_5c77a62.npy" # Three sensors simResultsFile = os.path.join('..','simulationResults',loadFileName); # Get the commit ID of the current repository @@ -157,4 +155,70 @@ plt.savefig (savePath) # For the figure to be saved show should appear after the save -plt.show() \ No newline at end of file +plt.show() + +if plotZoomDist: + # Plot the pure single component isotherm for the n gases + fig = plt.figure + # Histogram for gas 1 + ax1 = plt.subplot(1,1,1) + ax1.hist(resultOutput[:,0], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='r', alpha = alphaX, label = '$g_1$') + ax1.axvline(x=molFracG1, linewidth=1, linestyle='dotted', color = 'r', alpha = 0.6) + + # Histogram for gas 2 + # ax2 = plt.subplot(1,4,2) + ax1.hist(resultOutput[:,1], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='b', alpha = alphaX, label = '$g_2$') + ax1.axvline(x=molFracG2, linewidth=1, linestyle='dotted', color = 'b', alpha = 0.6) + + # Histogram for gas 3 + # ax3 = plt.subplot(1,4,3) + ax1.hist(resultOutput[:,2], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='g', alpha = alphaX, label = '$g_3$') + ax1.axvline(x=molFracG3, linewidth=1, linestyle='dotted', color = 'g', alpha = 0.6) + + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.set(xlabel='$y_i$ [-]', + ylabel='$f$ [-]', + xlim = xLimitsZ, ylim = yLimitsZ) + ax1.legend() + + # Save the figure + if saveFlag: + # FileName: ConcEstimate___ + saveFileName = "ConcEstimateZoom_" + currentDT + "_" + gitCommitID + "_" + simID_loadedFile + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + + + # For the figure to be saved show should appear after the save + plt.show() + + # Histogram for the sum of mole fraction + fig = plt.figure + ax2 = plt.subplot(1,1,1) + ax2.hist(np.sum(resultOutput,1), bins = nBins, range = rangeXS, density = densityX, + linewidth=1.5, histtype = histTypeX, color='k', alpha = alphaX) + ax2.axvline(x=1., linewidth=1, linestyle='dotted', color = 'k') + ax2.set(xlabel='$\Sigma y_i$ [-]', + xlim = xLimitsSumZ, ylim = yLimitsSumZ) + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + + # Save the figure + if saveFlag: + # FileName: ConcEstimate___ + saveFileName = "ConcEstimateSumZoom_" + currentDT + "_" + gitCommitID + "_" + simID_loadedFile + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + + # For the figure to be saved show should appear after the save + plt.show() \ No newline at end of file diff --git a/screenSensorArray.py b/screenSensorArray.py index a084f79..48f7980 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -35,7 +35,7 @@ import os # Number of sensors in the array -numSensors = 3 +numSensors = 2 # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -46,7 +46,7 @@ # Total number of sensor elements/gases simulated and generated using # generateHypotheticalAdsorbents.py function numberOfAdsorbents = 10 -numberOfGases = 3 +numberOfGases = 2 # "True" gas composition that is exposed to the sensor array (0-4) # Check generateTrueSensorResponse.py for the actual concentrations diff --git a/simulateSensorArray.py b/simulateSensorArray.py index b57f214..bd3df9a 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -14,6 +14,7 @@ # n sorbents. # # Last modified: +# - 2020-10-30, AK: Fix to find number of gases # - 2020-10-22, AK: Add two/three gases # - 2020-10-21, AK: Cosmetic changes and make it a function # - 2020-10-20, AK: Obtain sensor array finger print @@ -34,8 +35,10 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction): from simulateSSL import simulateSSL # For now load a given adsorbent isotherm material file - # loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases - loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases + if moleFraction.shape[1] == 2: + loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases + elif moleFraction.shape[1] == 3: + loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases hypoAdsorbentFile = os.path.join('inputResources',loadFileName); # Check if the file with the adsorbent properties exist From cc08dc445883e6ec7cb57d4802212ddfaf4b741f Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 4 Nov 2020 17:01:00 +0000 Subject: [PATCH 14/99] Improve plotting and analysis capability --- plotFunctions/mapTrueSensorResponse.py | 161 +++++++++++++++++++++ plotFunctions/plotConcentrationEstimate.py | 74 ++++++---- plotFunctions/plotSSLIsotherm.py | 20 ++- 3 files changed, 222 insertions(+), 33 deletions(-) create mode 100755 plotFunctions/mapTrueSensorResponse.py diff --git a/plotFunctions/mapTrueSensorResponse.py b/plotFunctions/mapTrueSensorResponse.py new file mode 100755 index 0000000..f6529c6 --- /dev/null +++ b/plotFunctions/mapTrueSensorResponse.py @@ -0,0 +1,161 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# +# +# Last modified: +# - 2020-10-23, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from generateTrueSensorResponse import generateTrueSensorResponse +import matplotlib.pyplot as plt +import auxiliaryFunctions +from scipy import stats +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file +import os +os.chdir("..") + +# Histogram properties +nBins = 100 +rangeX = ([-10,10]) +histTypeX = 'step' +alphaX=0.5 +densityX = False + + +# Flag for saving figures +saveFlag = False + +# Save file extension (png or pdf) +saveFileExtension = ".png" + +# Sensors to plot +sensorID = range(0,20) + +# Sensor ID for plotting histogram +histSensorID = 16 + +# Total pressure of the gas [Pa] +numberOfGases = 2 + +# Mole fraction to be plotted +moleFraction = np.linspace(0,1,1001) + +# Total pressure of the gas [Pa] +pressureTotal = np.array([1.e5]) + +# Temperature of the gas [K] +# Can be a vector of temperatures +temperature = np.array([298.15]) + +# Total number of sensor elements/gases simulated and generated using +# generateHypotheticalAdsorbents.py function +numberOfAdsorbents = 20 + +sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents, numberOfGases, + pressureTotal, temperature) + +# Parse out sensors that need to be plotted +sensorPlotResponse = np.zeros([len(sensorID),sensorTrueResponse.shape[1]]) +for jj in range(len(sensorID)): + for ii in range(sensorTrueResponse.shape[1]): + sensorPlotResponse[jj,ii] = (sensorTrueResponse[sensorID[jj],ii]) + +# Perform a standard normal variate of the sensor output +meanResponse = np.mean(sensorPlotResponse,axis=1) +stdResponse = np.std(sensorPlotResponse,axis=1) + +sensorSNVResponse = np.zeros([len(sensorID),sensorTrueResponse.shape[1]]) +for jj in range(len(sensorID)): + sensorSNVResponse[jj,:] = (sensorPlotResponse[jj,:] - meanResponse[jj])/stdResponse[jj] + +# Perform a Box-Cpx transformation +lambdaParam = np.zeros(len(sensorID)) +for jj in range(len(sensorID)): + _ , lambdaParam[jj] = stats.boxcox(sensorPlotResponse[jj,:]) + +# Calculate the skewness of the isotherm +skewnessSNVResponse = np.zeros(len(sensorID)) +for jj in range(len(sensorID)): + skewnessNum = np.sum(np.power(sensorSNVResponse[jj,:] - np.mean(sensorSNVResponse[jj,:]),3)) + skewnessDen = (len(sensorSNVResponse[jj,:]))*np.power(np.std(sensorSNVResponse[jj,:]),3) + skewnessSNVResponse[jj] = skewnessNum/skewnessDen + +# Calculate the kurtosis of the isotherm +kurtosisSNVResponse = np.zeros(len(sensorID)) +for jj in range(len(sensorID)): + kurtosisNum = np.sum(np.power(sensorSNVResponse[jj,:] - np.mean(sensorSNVResponse[jj,:]),4)) + kurtosisDen = (len(sensorSNVResponse[jj,:]))*np.power(np.std(sensorSNVResponse[jj,:]),4) + kurtosisSNVResponse[jj] = kurtosisNum/kurtosisDen + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Plot the sensor finger print for different concentrations of gases +fig = plt.figure +ax = plt.subplot(1,2,1) +for ii in range(len(sensorID)): + plotX = np.ones(sensorTrueResponse.shape[1])*sensorID[ii] + plotY = sensorPlotResponse[ii,:] + s1 = ax.scatter(plotX,plotY,c = moleFraction, cmap='RdYlBu') + +ax.set(xlabel='Adsorbent ID [-]', + ylabel='$m_i$ [g kg$^{-1}$]', + xlim = [0, 20], ylim = [0,300]) +ax.locator_params(axis="x", nbins=4) +ax.locator_params(axis="y", nbins=4) +plt.colorbar(s1,ax=ax) + +# Save the figure +if saveFlag: + # FileName: PureIsotherm____ + saveFileName = "SensorMapRaw_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + +# Plot the sensor finger print for different concentrations of gases, but with +# mean centering and normalizing with standard deviation +ax = plt.subplot(1,2,2) +for ii in range(len(sensorID)): + plotX = np.ones(sensorTrueResponse.shape[1])*sensorID[ii] + plotY = sensorSNVResponse[ii,:] + s2 = ax.scatter(plotX,plotY,c = moleFraction, cmap='RdYlBu') +ax.set(xlabel='Adsorbent ID [-]', + ylabel='$\hat{m}_i$ [-]', + xlim = [0, 20], ylim = [-10,5]) +ax.locator_params(axis="x", nbins=4) +ax.locator_params(axis="y", nbins=4) +plt.colorbar(s2,ax=ax) +# Save the figure +if saveFlag: + # FileName: PureIsotherm____ + saveFileName = "SensorMapSNV_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + +plt.show() \ No newline at end of file diff --git a/plotFunctions/plotConcentrationEstimate.py b/plotFunctions/plotConcentrationEstimate.py index 709d277..41ad600 100755 --- a/plotFunctions/plotConcentrationEstimate.py +++ b/plotFunctions/plotConcentrationEstimate.py @@ -12,6 +12,7 @@ # Plots to visualize estimated concentration # # Last modified: +# - 2020-11-04, AK: Improve plotting capability for three gases/sensors # - 2020-10-30, AK: Add zoomed in version # - 2020-10-29, AK: Improvements to the plots # - 2020-10-23, AK: Initial creation @@ -31,6 +32,9 @@ import matplotlib.pyplot as plt plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file +# Number of gases +numberOfGases = 2 + # Flag for saving figure saveFlag = False @@ -41,15 +45,15 @@ plotZoomDist = False # Gas concentration -molFracG1 = 0.05 -molFracG2 = 0.15 +molFracG1 = 0.4 +molFracG2 = 0.6 molFracG3 = 1 - molFracG1 - molFracG2 # Xlimits and Ylimits xLimits = [0,1] -yLimits = [0,6] -xLimitsSum = [0,3] -yLimitsSum = [0,30] +yLimits = [0,300] +xLimitsSum = [0,2] +yLimitsSum = [0,300] xLimitsZ = [0,1] # Limits for zoom distribution yLimitsZ = [0,15] # Limits for zoom distribution @@ -65,9 +69,11 @@ densityX = False # For now load a given adsorbent isotherm material file -# loadFileName = "arrayConcentration_20201030_1109_5c77a62.npy" # One sensor -# loadFileName = "arrayConcentration_20201030_0913_5c77a62.npy" # Two sensors -loadFileName = "arrayConcentration_20201029_2328_5c77a62.npy" # Three sensors +# loadFileName = "arrayConcentration_20201030_1109_5c77a62.npy" # 3 gases, 1 sensor +# loadFileName = "arrayConcentration_20201030_0913_5c77a62.npy" # 3 gases, 2 sensors +# loadFileName = "arrayConcentration_20201029_2328_5c77a62.npy" # 3 gases, 3 sensors +loadFileName = "arrayConcentration_20201030_1731_da1707b.npy" # 2 gases, 2 sensor +# loadFileName = "arrayConcentration_20201102_1423_da1707b.npy" # 2 gases, 1 sensor simResultsFile = os.path.join('..','simulationResults',loadFileName); # Get the commit ID of the current repository @@ -82,12 +88,18 @@ # Check if the file with the adsorbent properties exist if os.path.exists(simResultsFile): resultOutput = load(simResultsFile) - if resultOutput.shape[1] == 4: - resultOutput = np.delete(resultOutput,0,1) - elif resultOutput.shape[1] == 5: - resultOutput = np.delete(resultOutput,[0,1],1) - elif resultOutput.shape[1] == 6: - resultOutput = np.delete(resultOutput,[0,1,2],1) + if numberOfGases == 2: + if resultOutput.shape[1] == 3: + resultOutput = np.delete(resultOutput,[0],1) + elif resultOutput.shape[1] == 4: + resultOutput = np.delete(resultOutput,[0,1],1) + elif numberOfGases == 3: + if resultOutput.shape[1] == 4: + resultOutput = np.delete(resultOutput,0,1) + elif resultOutput.shape[1] == 5: + resultOutput = np.delete(resultOutput,[0,1],1) + elif resultOutput.shape[1] == 6: + resultOutput = np.delete(resultOutput,[0,1,2],1) else: errorString = "Simulation result file " + simResultsFile + " does not exist." raise Exception(errorString) @@ -106,11 +118,12 @@ linewidth=1.5, histtype = histTypeX, color='b', alpha = alphaX, label = '$g_2$') ax1.axvline(x=molFracG2, linewidth=1, linestyle='dotted', color = 'b', alpha = 0.6) -# Histogram for gas 3 -# ax3 = plt.subplot(1,4,3) -ax1.hist(resultOutput[:,2], bins = nBins, range = rangeX, density = densityX, - linewidth=1.5, histtype = histTypeX, color='g', alpha = alphaX, label = '$g_3$') -ax1.axvline(x=molFracG3, linewidth=1, linestyle='dotted', color = 'g', alpha = 0.6) +if numberOfGases == 3: + # Histogram for gas 3 + # ax3 = plt.subplot(1,4,3) + ax1.hist(resultOutput[:,2], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='g', alpha = alphaX, label = '$g_3$') + ax1.axvline(x=molFracG3, linewidth=1, linestyle='dotted', color = 'g', alpha = 0.6) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) @@ -136,9 +149,15 @@ # Histogram for the sum of mole fraction fig = plt.figure ax2 = plt.subplot(1,1,1) -ax2.hist(np.sum(resultOutput,1), bins = nBins, range = rangeXS, density = densityX, - linewidth=1.5, histtype = histTypeX, color='k', alpha = alphaX) -ax2.axvline(x=1., linewidth=1, linestyle='dotted', color = 'k') +if numberOfGases == 2: + ax2.hist(np.sum(resultOutput,1), bins = nBins, range = rangeXS, density = densityX, + linewidth=1.5, histtype = histTypeX, color='k', alpha = alphaX) + ax2.axvline(x=1., linewidth=1, linestyle='dotted', color = 'k') +elif numberOfGases == 3: + ax2.hist(np.sum(resultOutput,1), bins = nBins, range = rangeXS, density = densityX, + linewidth=1.5, histtype = histTypeX, color='k', alpha = alphaX) + ax2.axvline(x=1., linewidth=1, linestyle='dotted', color = 'k') + ax2.set(xlabel='$\Sigma y_i$ [-]', xlim = xLimitsSum, ylim = yLimitsSum) ax2.locator_params(axis="x", nbins=4) @@ -172,11 +191,12 @@ linewidth=1.5, histtype = histTypeX, color='b', alpha = alphaX, label = '$g_2$') ax1.axvline(x=molFracG2, linewidth=1, linestyle='dotted', color = 'b', alpha = 0.6) - # Histogram for gas 3 - # ax3 = plt.subplot(1,4,3) - ax1.hist(resultOutput[:,2], bins = nBins, range = rangeX, density = densityX, - linewidth=1.5, histtype = histTypeX, color='g', alpha = alphaX, label = '$g_3$') - ax1.axvline(x=molFracG3, linewidth=1, linestyle='dotted', color = 'g', alpha = 0.6) + if numberOfGases == 3: + # Histogram for gas 3 + # ax3 = plt.subplot(1,4,3) + ax1.hist(resultOutput[:,2], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='g', alpha = alphaX, label = '$g_3$') + ax1.axvline(x=molFracG3, linewidth=1, linestyle='dotted', color = 'g', alpha = 0.6) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) diff --git a/plotFunctions/plotSSLIsotherm.py b/plotFunctions/plotSSLIsotherm.py index a646f1f..9039743 100755 --- a/plotFunctions/plotSSLIsotherm.py +++ b/plotFunctions/plotSSLIsotherm.py @@ -12,6 +12,7 @@ # Plots the single site Langmuir isotherms # # Last modified: +# - 2020-11-04, AK: Plot 3rd gas # - 2020-10-28, AK: Minor fix for save file name # - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-27, AK: Further improvements and cosmetic changes @@ -40,7 +41,10 @@ saveFileExtension = ".png" # Sensor ID to be plotted -sensorID = 50 +sensorID = 17 + +# Number of gases to determine the hypothetical sorbents roster +numGases = 2 # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]) @@ -53,8 +57,11 @@ moleFraction = np.array([np.linspace(0,1,101)]) # For now load a given adsorbent isotherm material file -# loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases -loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases +if numGases == 2: + loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases +elif numGases == 3: + loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases + hypoAdsorbentFile = os.path.join('..','inputResources',loadFileName); # Check if the file with the adsorbent properties exist @@ -97,11 +104,12 @@ linewidth=1.5,color='r', label = '$g_1$') ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,1], linewidth=1.5,color='b', label = '$g_2$') -ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,2], - linewidth=1.5,color='g', label = '$g_3$') +if numGases == 3: + ax.plot(pressureTotal*moleFraction.T/1.e5, equilibriumLoadings[:,2], + linewidth=1.5,color='g', label = '$g_3$') ax.set(xlabel='$P$ [bar]', ylabel='$q^*$ [mol kg$^{\mathregular{-1}}$]', - xlim = [0, 1], ylim = [0, 10]) + xlim = [0, 1], ylim = [0, 0.01]) ax.legend() # Save the figure From f7e470fd2ccb5027aa4e1fd713584d27338a9bc5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 9 Nov 2020 10:01:15 +0000 Subject: [PATCH 15/99] Algorithm and functionality upgrade for estimator --- estimateConcentration.py | 37 ++-- generateTrueSensorResponse.py | 27 ++- plotFunctions/mapTrueSensorResponse.py | 29 +-- plotFunctions/plotConcentrationEstimate.py | 11 +- plotFunctions/plotOjectiveFunction.py | 199 +++++++++++++++++++++ sensitivityAnalysis.py | 77 ++++++++ 6 files changed, 345 insertions(+), 35 deletions(-) create mode 100755 plotFunctions/plotOjectiveFunction.py create mode 100755 sensitivityAnalysis.py diff --git a/estimateConcentration.py b/estimateConcentration.py index 55dffc0..d6d448e 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,8 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2020-11-09, AK: Changes to initial condition and optimizer bounds +# - 2020-11-05, AK: Introduce keyword argument for custom mole fraction # - 2020-10-30, AK: Fix to find number of gases # - 2020-10-22, AK: Change error to relative from absolute, add opt bounds, # input arguments, and initial guess @@ -27,15 +29,10 @@ # ############################################################################ -def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorID): +def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorID, **kwargs): import numpy as np from generateTrueSensorResponse import generateTrueSensorResponse from scipy.optimize import basinhopping - - # Total number of sensor elements/gases simulated and generated using - # generateHypotheticalAdsorbents.py function - # numberOfAdsorbents = 4; - # numberOfGases = 3; # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -45,12 +42,16 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI temperature = np.array([298.15]); # Get the individual sensor reponse for all the given "experimental/test" concentrations - sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, + if 'moleFraction' in kwargs: + sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, + pressureTotal,temperature, moleFraction = kwargs["moleFraction"]) + moleFracID = 0 # Index becomes a scalar quantity + else: + sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, pressureTotal,temperature) - - # True mole fraction index (provide the index corresponding to the true - # experimental mole fraction (0-4, check generateTrueSensorResponse.py) - moleFracID = moleFracID + # True mole fraction index (provide the index corresponding to the true + # experimental mole fraction (0-4, check generateTrueSensorResponse.py) + moleFracID = moleFracID # Sensor combinations used in the array. This is a [gx1] vector that maps to # the sorbent/material ID generated using the @@ -59,7 +60,7 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI # Parse out the true sensor response for a sensor array with n number of # sensors given by sensorID - arrayTrueResponse = 0.5*np.ones(sensorID.shape[0]) + arrayTrueResponse = np.zeros(sensorID.shape[0]) for ii in range(sensorID.shape[0]): arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii],moleFracID] @@ -69,13 +70,16 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI # Minimize an objective function to compute the mole fraction of the feed # gas to the sensor - initialCondition = np.zeros(numberOfGases) # Initial guess + initialCondition = np.random.uniform(0,1,numberOfGases) # Initial guess optBounds = np.tile([0.,1.], (numberOfGases,1)) # BOunding the mole fractions + optCons = {'type':'eq','fun': lambda x: sum(x) - 1} # Cinstrain the sum to 1. # Use the basin hopping minimizer to escape local minima when evaluating # the function. The number of iterations is hard-coded and fixed at 50 estMoleFraction = basinhopping(concObjectiveFunction, initialCondition, - minimizer_kwargs = {"args": inputParameters, - "bounds": optBounds}, niter = 50) + minimizer_kwargs = {"args": inputParameters, + "bounds": optBounds, + "constraints": optCons}, + niter = 50) return np.concatenate((sensorID,estMoleFraction.x), axis=0) # func: concObjectiveFunction, computes the sum of square error for the @@ -90,7 +94,8 @@ def concObjectiveFunction(x, *inputParameters): # Reshape the mole fraction to a row vector for compatibility moleFraction = np.array([x]) # This is needed to keep the structure as a row instead of column - + # moleFraction = np.array([x,1.-x]).T # This is needed to keep the structure as a row instead of column + # Compute the sensor reponse for a given mole fraction input arraySimResponse = simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction) diff --git a/generateTrueSensorResponse.py b/generateTrueSensorResponse.py index 69fd056..26c37d2 100755 --- a/generateTrueSensorResponse.py +++ b/generateTrueSensorResponse.py @@ -14,6 +14,8 @@ # and should be modified. # # Last modified: +# - 2020-11-09, AK: Introduce custom mole fraction input +# - 2020-11-05, AK: Introduce new case for mole fraction sweep # - 2020-10-30, AK: Fix to find number of gases # - 2020-10-22, AK: Add two/three gases # - 2020-10-21, AK: Initial creation @@ -26,10 +28,11 @@ # ############################################################################ -def generateTrueSensorResponse(numberOfAdsorbents, numberOfGases, pressureTotal, temperature): +def generateTrueSensorResponse(numberOfAdsorbents, numberOfGases, pressureTotal, temperature, **kwargs): import numpy as np + import pdb from simulateSensorArray import simulateSensorArray - + # Mole fraction of the gas [-] # Can be [jxg], where j is the number of mole fractions for g gases if numberOfGases == 2: @@ -43,7 +46,25 @@ def generateTrueSensorResponse(numberOfAdsorbents, numberOfGases, pressureTotal, [0.15, 0.25, 0.60], [0.40, 0.35, 0.25], [0.75, 0.10, 0.15], - [0.90, 0.05, 0.05]]) + [0.90, 0.05, 0.05]]) + # To sweep through the entire mole fraction range for 2 gases + elif numberOfGases == 20000: + moleFraction = np.array([np.linspace(0,1,1001), 1 - np.linspace(0,1,1001)]).T + # To sweep through the entire mole fraction range for 3 gases + elif numberOfGases == 30000: + moleFractionTemp = np.zeros([1001,3]) + num1 = np.random.uniform(0.0,1.0,1001) + num2 = np.random.uniform(0.0,1.0,1001) + num3 = np.random.uniform(0.0,1.0,1001) + sumNum = num1 + num2 + num3 + moleFractionTemp[:,0] = num1/sumNum + moleFractionTemp[:,1] = num2/sumNum + moleFractionTemp[:,2] = num3/sumNum + moleFraction = moleFractionTemp[moleFractionTemp[:,0].argsort()] + + # Check if a custom mole fraction is provided intead of the predefined one + if 'moleFraction' in kwargs: + moleFraction = np.array([kwargs["moleFraction"]]) # Get the individual sensor reponse for all the five "test" concentrations sensorTrueResponse = np.zeros((numberOfAdsorbents,moleFraction.shape[0])) diff --git a/plotFunctions/mapTrueSensorResponse.py b/plotFunctions/mapTrueSensorResponse.py index f6529c6..f0fef09 100755 --- a/plotFunctions/mapTrueSensorResponse.py +++ b/plotFunctions/mapTrueSensorResponse.py @@ -9,9 +9,11 @@ # Authors: Ashwin Kumar Rajagopalan (AK) # # Purpose: -# +# Maps the sensor response as a function of mole fraction and the sorbent +# used # # Last modified: +# - 2020-11-06, AK: Cosmetic changes # - 2020-10-23, AK: Initial creation # # Input arguments: @@ -52,7 +54,7 @@ histSensorID = 16 # Total pressure of the gas [Pa] -numberOfGases = 2 +numberOfGases = 20000 # 20000 does a complete concentraiton sweep for 2/3 gases # Mole fraction to be plotted moleFraction = np.linspace(0,1,1001) @@ -66,8 +68,9 @@ # Total number of sensor elements/gases simulated and generated using # generateHypotheticalAdsorbents.py function -numberOfAdsorbents = 20 +numberOfAdsorbents = 20 +# Generate the true sensor response sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents, numberOfGases, pressureTotal, temperature) @@ -123,16 +126,16 @@ xlim = [0, 20], ylim = [0,300]) ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) -plt.colorbar(s1,ax=ax) +plt.colorbar(s1,ax=ax,label="$y_1$ [-]") # Save the figure if saveFlag: - # FileName: PureIsotherm____ + # FileName: SensorMapRaw__ saveFileName = "SensorMapRaw_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('..','simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder + savePath = os.path.join('simulationFigures',saveFileName) + # Check if simulationFigures directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) + os.mkdir(os.path.join('simulationFigures')) plt.savefig (savePath) # Plot the sensor finger print for different concentrations of gases, but with @@ -147,15 +150,15 @@ xlim = [0, 20], ylim = [-10,5]) ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) -plt.colorbar(s2,ax=ax) +plt.colorbar(s2,ax=ax,label="$y_1$ [-]") # Save the figure if saveFlag: - # FileName: PureIsotherm____ + # FileName: SensorMapSNV__ saveFileName = "SensorMapSNV_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('..','simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder + savePath = os.path.join('simulationFigures',saveFileName) + # Check if simulationFigures directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) + os.mkdir(os.path.join('simulationFigures')) plt.savefig (savePath) plt.show() \ No newline at end of file diff --git a/plotFunctions/plotConcentrationEstimate.py b/plotFunctions/plotConcentrationEstimate.py index 41ad600..00e34a8 100755 --- a/plotFunctions/plotConcentrationEstimate.py +++ b/plotFunctions/plotConcentrationEstimate.py @@ -12,6 +12,7 @@ # Plots to visualize estimated concentration # # Last modified: +# - 2020-11-09, AK: Cosmetic changes # - 2020-11-04, AK: Improve plotting capability for three gases/sensors # - 2020-10-30, AK: Add zoomed in version # - 2020-10-29, AK: Improvements to the plots @@ -45,8 +46,8 @@ plotZoomDist = False # Gas concentration -molFracG1 = 0.4 -molFracG2 = 0.6 +molFracG1 = 0.90 +molFracG2 = 0.10 molFracG3 = 1 - molFracG1 - molFracG2 # Xlimits and Ylimits @@ -72,8 +73,11 @@ # loadFileName = "arrayConcentration_20201030_1109_5c77a62.npy" # 3 gases, 1 sensor # loadFileName = "arrayConcentration_20201030_0913_5c77a62.npy" # 3 gases, 2 sensors # loadFileName = "arrayConcentration_20201029_2328_5c77a62.npy" # 3 gases, 3 sensors -loadFileName = "arrayConcentration_20201030_1731_da1707b.npy" # 2 gases, 2 sensor # loadFileName = "arrayConcentration_20201102_1423_da1707b.npy" # 2 gases, 1 sensor +# loadFileName = "arrayConcentration_20201104_1732_cc08dc4.npy" # 2 gases, 2 sensor [0.15, 0.85] +# loadFileName = "arrayConcentration_20201030_1731_da1707b.npy" # 2 gases, 2 sensor [0.4, 0.6] +# loadFileName = "arrayConcentration_20201104_1842_cc08dc4.npy" # 2 gases, 2 sensor [0.75, 0.25] +loadFileName = "arrayConcentration_20201104_2227_cc08dc4.npy" # 2 gases, 2 sensor [0.75, 0.25] simResultsFile = os.path.join('..','simulationResults',loadFileName); # Get the commit ID of the current repository @@ -88,6 +92,7 @@ # Check if the file with the adsorbent properties exist if os.path.exists(simResultsFile): resultOutput = load(simResultsFile) + actualOutput = resultOutput if numberOfGases == 2: if resultOutput.shape[1] == 3: resultOutput = np.delete(resultOutput,[0],1) diff --git a/plotFunctions/plotOjectiveFunction.py b/plotFunctions/plotOjectiveFunction.py new file mode 100755 index 0000000..9f6f13c --- /dev/null +++ b/plotFunctions/plotOjectiveFunction.py @@ -0,0 +1,199 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plots the objective function used for concentration estimation +# +# Last modified: +# - 2020-11-05, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from kneed import KneeLocator # To compute the knee/elbow of a curve +from generateTrueSensorResponse import generateTrueSensorResponse +from simulateSensorArray import simulateSensorArray +import os +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file +import auxiliaryFunctions + +os.chdir("..") + +# Save flag for figure +saveFlag = False + +# Save file extension (png or pdf) +saveFileExtension = ".png" + +# Total pressure of the gas [Pa] +pressureTotal = np.array([1.e5]); + +# Temperature of the gas [K] +# Can be a vector of temperatures +temperature = np.array([298.15]); + +# Number of Adsorbents +numberOfAdsorbents = 20 + +# Number of Gases +numberOfGases = 2 + +# Mole Fraction of interest +moleFrac = [0.5, 0.5] + +# Sensor ID +sensorID = np.array([17, 15]) + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Simulate the sensor response for all possible concentrations +if numberOfGases == 2: + moleFractionRange = np.array([np.linspace(0,1,10001), 1 - np.linspace(0,1,10001)]).T +elif numberOfGases == 3: + moleFractionRangeTemp = np.zeros([10001,3]) + num1 = np.random.uniform(0.0,1.0,10001) + num2 = np.random.uniform(0.0,1.0,10001) + num3 = np.random.uniform(0.0,1.0,10001) + sumNum = num1 + num2 + num3 + moleFractionRangeTemp[:,0] = num1/sumNum + moleFractionRangeTemp[:,1] = num2/sumNum + moleFractionRangeTemp[:,2] = num3/sumNum + moleFractionRange = moleFractionRangeTemp[moleFractionRangeTemp[:,0].argsort()] + +arraySimResponse = np.zeros([moleFractionRange.shape[0],numberOfGases]) +for ii in range(moleFractionRange.shape[0]): + arraySimResponse[ii,:] = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([moleFractionRange[ii,:]])) + +# Get the individual sensor reponse for all the given "experimental/test" concentrations +sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, + pressureTotal,temperature,moleFraction = moleFrac) +# Parse out the true sensor response for the desired sensors in the array +arrayTrueResponse = np.zeros(sensorID.shape[0]) +for ii in range(sensorID.shape[0]): + arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii]] +arrayTrueResponse = np.tile(arrayTrueResponse,(moleFractionRange.shape[0],1)) + +# Compute the objective function over all the mole fractions +objFunction = np.sum(np.power((arrayTrueResponse - arraySimResponse)/arrayTrueResponse,2),1) + +# Compute the first derivative and the elbow point of sensor 1 +firstDerivative = np.zeros([moleFractionRange.shape[0],numberOfGases]) +firstDerivative[:,0] = np.gradient(arraySimResponse[:,0]) +if all(i >= 0. for i in firstDerivative[:,0]): + slopeDir1 = "increasing" +else: + slopeDir1 = "decreasing" +kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,0], + curve="concave", direction=slopeDir1) +elbowPointS1 = list(kneedle.all_elbows) + +# Compute the first derivative and the elbow point of sensor 2 +firstDerivative[:,1] = np.gradient(arraySimResponse[:,1]) +if all(i >= 0. for i in firstDerivative[:,1]): + slopeDir2 = "increasing" +else: + slopeDir2 = "decreasing" +kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,1], + curve="concave", direction=slopeDir2) +elbowPointS2 = list(kneedle.all_elbows) + +# Plot the sensor response for all the conocentrations and highlight the +# working region +# Obtain coordinates to fill working region +if slopeDir1 == "increasing": + xFill1 = [0,elbowPointS1[0]] +else: + xFill1 = [elbowPointS1[0], 1.0] + +if slopeDir2 == "increasing": + xFill2 = [0,elbowPointS2[0]] +else: + xFill2 = [elbowPointS2[0], 1.0] + +fig = plt.figure +ax = plt.gca() +# Sensor 1 +ax.plot(moleFractionRange[:,0],arraySimResponse[:,0],'r', label = '$s_1$') # Simulated Response +ax.axvline(x=elbowPointS1, linewidth=1, linestyle='dotted', color = 'r') # Elbow point +ax.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='red', alpha=0.15) +# Sensor 2 +ax.plot(moleFractionRange[:,0],arraySimResponse[:,1],'b', label = '$s_2$') # Simulated Response +ax.axvline(x=elbowPointS2, linewidth=1, linestyle='dotted', color = 'b') # Elbow point +ax.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='blue', alpha=0.15) +# Sensor 3 +if numberOfGases == 3: + ax.axhline(y=arrayTrueResponse[0,2], linewidth=1, linestyle='dotted', + color = 'b', label = '$s_2$') + ax.plot(moleFractionRange[:,0],arraySimResponse[:,2],'g') + +ax.locator_params(axis="x", nbins=4) +ax.locator_params(axis="y", nbins=4) +ax.set(xlabel='$y_1$ [-]', + ylabel='$m_i$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) +ax.legend() + +# Save the figure +if saveFlag: + # FileName: PureIsotherm____ + sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') + saveFileName = "SensorResponse_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) +plt.show() + +# Plot the objective function used to evaluate the concentration for individual +# sensors and the total (sum) +fig = plt.figure +ax = plt.gca() +ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,0] + -arraySimResponse[:,0])/arrayTrueResponse[:,0],2),'r', label = '$J_1$') # Error first sensor +ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,1] + -arraySimResponse[:,1])/arrayTrueResponse[:,1],2),'b', label = '$J_2$') # Error second sensor +if numberOfGases == 3: + ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,2] + -arraySimResponse[:,2])/arrayTrueResponse[:,2],2),'g', label = '$J_3$') # Error third sensor +ax.plot(moleFractionRange[:,0],objFunction,'k', label = '$\Sigma J_i$') # Error all sensors +ax.locator_params(axis="x", nbins=4) +ax.locator_params(axis="y", nbins=4) +ax.set(xlabel='$y_1$ [-]', + ylabel='$J$ [-]', + xlim = [0,1.], ylim = [0, None]) +ax.legend() + +# Save the figure +if saveFlag: + # FileName: PureIsotherm____ + sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') + moleFrac = str(moleFrac).replace('[','').replace(']','').replace(' ','').replace(',','-').replace('.','') + saveFileName = "SensorObjFunc_" + sensorText + "_" + moleFrac + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + +plt.show() \ No newline at end of file diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py new file mode 100755 index 0000000..dd14c5e --- /dev/null +++ b/sensitivityAnalysis.py @@ -0,0 +1,77 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Script to perform a sensitivity analysis on the sensor response and +# concentration estimate +# +# Last modified: +# - 2020-11-06, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +import multiprocessing # For parallel processing +from joblib import Parallel, delayed # For parallel processing +from tqdm import tqdm # To track progress of the loop +from estimateConcentration import estimateConcentration + +# Find out the total number of cores available for parallel processing +num_cores = multiprocessing.cpu_count() + +# Number of adsorbents +numberOfAdsorbents = 30 + +# Number of gases +numberOfGases = 2 + +# Sensor combination +sensorID = [17, 15] + +# Custom input mole fraction for gas 1 +meanMoleFracG1 = 0.10 +diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. +numberOfIterations = 50 + +# Custom input mole fraction for gas 2 (for 3 gas system) +meanMoleFracG2 = 0.20 + + +# Generate a uniform distribution of mole fractions +if numberOfGases == 2: + inputMoleFrac = np.zeros([numberOfIterations,2]) + inputMoleFrac[:,0] = np.random.uniform(meanMoleFracG1-diffMoleFracG1, + meanMoleFracG1+diffMoleFracG1, + numberOfIterations) + inputMoleFrac[:,1] = 1. - inputMoleFrac[:,0] +elif numberOfGases == 3: + inputMoleFrac = np.zeros([numberOfIterations,3]) + inputMoleFrac[:,0] = meanMoleFracG1 + inputMoleFrac[:,1] = meanMoleFracG2 + inputMoleFrac[:,2] = 1 - meanMoleFracG1 - meanMoleFracG2 + + +# Loop over all the sorbents for a single material sensor +# Using parallel processing to loop through all the materials +arrayConcentration = np.zeros(numberOfAdsorbents) +arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + (numberOfAdsorbents,numberOfGases,None,sensorID, + moleFraction = inputMoleFrac[ii]) + for ii in tqdm(range(inputMoleFrac.shape[0]))) + +# Convert the output list to a matrix +arrayConcentration = np.array(arrayConcentration) + From 63f34997529aaf6e32d2b60ecfe060324e03fa35 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 9 Nov 2020 13:54:37 +0000 Subject: [PATCH 16/99] Updates to sensitivity analysis --- sensitivityAnalysis.py | 84 ++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index dd14c5e..9c74c4d 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -24,10 +24,19 @@ ############################################################################ import numpy as np +from numpy import savez import multiprocessing # For parallel processing from joblib import Parallel, delayed # For parallel processing +import auxiliaryFunctions +import os from tqdm import tqdm # To track progress of the loop from estimateConcentration import estimateConcentration + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +simulationDT = auxiliaryFunctions.getCurrentDateTime() # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() @@ -39,39 +48,66 @@ numberOfGases = 2 # Sensor combination -sensorID = [17, 15] +sensorID = [17, 16] # Custom input mole fraction for gas 1 -meanMoleFracG1 = 0.10 +meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. -numberOfIterations = 50 +numberOfIterations = 100 # Custom input mole fraction for gas 2 (for 3 gas system) meanMoleFracG2 = 0.20 +# Initialize mean and standard deviation of concentration estimates +meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) +stdConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) -# Generate a uniform distribution of mole fractions -if numberOfGases == 2: - inputMoleFrac = np.zeros([numberOfIterations,2]) - inputMoleFrac[:,0] = np.random.uniform(meanMoleFracG1-diffMoleFracG1, - meanMoleFracG1+diffMoleFracG1, - numberOfIterations) - inputMoleFrac[:,1] = 1. - inputMoleFrac[:,0] -elif numberOfGases == 3: - inputMoleFrac = np.zeros([numberOfIterations,3]) - inputMoleFrac[:,0] = meanMoleFracG1 - inputMoleFrac[:,1] = meanMoleFracG2 - inputMoleFrac[:,2] = 1 - meanMoleFracG1 - meanMoleFracG2 +for ii in range(len(meanMoleFracG1)): + # Generate a uniform distribution of mole fractions + if numberOfGases == 2: + inputMoleFrac = np.zeros([numberOfIterations,2]) + inputMoleFrac[:,0] = np.random.uniform(meanMoleFracG1[ii]-diffMoleFracG1, + meanMoleFracG1[ii]+diffMoleFracG1, + numberOfIterations) + inputMoleFrac[:,1] = 1. - inputMoleFrac[:,0] + elif numberOfGases == 3: + inputMoleFrac = np.zeros([numberOfIterations,3]) + inputMoleFrac[:,0] = meanMoleFracG1 + inputMoleFrac[:,1] = meanMoleFracG2 + inputMoleFrac[:,2] = 1 - meanMoleFracG1 - meanMoleFracG2 -# Loop over all the sorbents for a single material sensor -# Using parallel processing to loop through all the materials -arrayConcentration = np.zeros(numberOfAdsorbents) -arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) - (numberOfAdsorbents,numberOfGases,None,sensorID, - moleFraction = inputMoleFrac[ii]) - for ii in tqdm(range(inputMoleFrac.shape[0]))) + # Loop over all the sorbents for a single material sensor + # Using parallel processing to loop through all the materials + arrayConcentration = np.zeros(numberOfAdsorbents) + arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + (numberOfAdsorbents,numberOfGases,None,sensorID, + moleFraction = inputMoleFrac[ii]) + for ii in tqdm(range(inputMoleFrac.shape[0]))) + + # Convert the output list to a matrix + arrayConcentration = np.array(arrayConcentration) + + # Compute the mean and the standard deviation of the concentration estimates + if numberOfGases == 2 and len(sensorID) == 2: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,2]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,3]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[:,2]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[:,3]) + +# Save the array concentration into a native numpy file +# The .npz file is saved in a folder called simulationResults (hardcoded) +filePrefix = "sensitivityAnalysis" +sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-').replace(',','') +saveFileName = filePrefix + "_" + sensorText + "_" + simulationDT + "_" + gitCommitID; +savePath = os.path.join('simulationResults',saveFileName) -# Convert the output list to a matrix -arrayConcentration = np.array(arrayConcentration) +# Check if simulationResults directory exists or not. If not, create the folder +if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') +# Save the mean, standard deviation, and molefraction array +savez (savePath, numberOfGases = numberOfGases, + moleFractionG1 = meanMoleFracG1, + meanConcEstimate = meanConcEstimate, + stdConcEstimate = stdConcEstimate) \ No newline at end of file From 10f167a0ce72357465d835e308d9665f8f483111 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 Nov 2020 12:26:28 +0000 Subject: [PATCH 17/99] Add measurement noise and improve plotting --- estimateConcentration.py | 31 ++++++++++---- plotFunctions/plotOjectiveFunction.py | 58 +++++++++++++++++++++++---- sensitivityAnalysis.py | 10 ++++- 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index d6d448e..c786059 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,7 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2020-11-10, AK: Add measurement noise to true sensor response # - 2020-11-09, AK: Changes to initial condition and optimizer bounds # - 2020-11-05, AK: Introduce keyword argument for custom mole fraction # - 2020-10-30, AK: Fix to find number of gases @@ -30,6 +31,7 @@ ############################################################################ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorID, **kwargs): + import pdb import numpy as np from generateTrueSensorResponse import generateTrueSensorResponse from scipy.optimize import basinhopping @@ -41,6 +43,11 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI # Can be a vector of temperatures temperature = np.array([298.15]); + # Sensor combinations used in the array. This is a [gx1] vector that maps to + # the sorbent/material ID generated using the + # generateHypotheticalAdsorbents.py function + sensorID = np.array(sensorID) + # Get the individual sensor reponse for all the given "experimental/test" concentrations if 'moleFraction' in kwargs: sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, @@ -51,18 +58,28 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI pressureTotal,temperature) # True mole fraction index (provide the index corresponding to the true # experimental mole fraction (0-4, check generateTrueSensorResponse.py) - moleFracID = moleFracID + moleFracID = moleFracID + + # Add measurement noise for the true measurement if the user wants it + measurementNoise = np.zeros(sensorID.shape[0]) + if 'addMeasurementNoise' in kwargs: + # The mean and the standard deviation of the Gaussian error is an + # input from the user + measurementNoise = np.random.normal(kwargs["addMeasurementNoise"][0], + kwargs["addMeasurementNoise"][1], + sensorID.shape[0]) - # Sensor combinations used in the array. This is a [gx1] vector that maps to - # the sorbent/material ID generated using the - # generateHypotheticalAdsorbents.py function - sensorID = np.array(sensorID) - # Parse out the true sensor response for a sensor array with n number of # sensors given by sensorID arrayTrueResponse = np.zeros(sensorID.shape[0]) for ii in range(sensorID.shape[0]): - arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii],moleFracID] + arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii],moleFracID] + measurementNoise[ii] + + # Replace all negative values to zero (for physical consistency) + # Print if any of the responses are negative + if any(ii<=0. for ii in arrayTrueResponse): + print("Number of negative response: " + str(sum(arrayTrueResponse<0))) + arrayTrueResponse[arrayTrueResponse<0.] = 0. # Pack the input parameters/arguments useful to compute the objective # function to estimate the mole fraction as a tuple diff --git a/plotFunctions/plotOjectiveFunction.py b/plotFunctions/plotOjectiveFunction.py index 9f6f13c..9fd657d 100755 --- a/plotFunctions/plotOjectiveFunction.py +++ b/plotFunctions/plotOjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-11, AK: Cosmetic changes and add standard deviation plot # - 2020-11-05, AK: Initial creation # # Input arguments: @@ -23,12 +24,12 @@ ############################################################################ import numpy as np +from numpy import load from kneed import KneeLocator # To compute the knee/elbow of a curve from generateTrueSensorResponse import generateTrueSensorResponse from simulateSensorArray import simulateSensorArray import os import matplotlib.pyplot as plt -from mpl_toolkits.mplot3d import Axes3D plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file import auxiliaryFunctions @@ -37,6 +38,9 @@ # Save flag for figure saveFlag = False +# Plot flag to show standard deviation of errors +plotStdError = False + # Save file extension (png or pdf) saveFileExtension = ".png" @@ -54,10 +58,10 @@ numberOfGases = 2 # Mole Fraction of interest -moleFrac = [0.5, 0.5] +moleFrac = [0.1, 0.9] # Sensor ID -sensorID = np.array([17, 15]) +sensorID = np.array([16, 7]) # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -155,7 +159,7 @@ # Save the figure if saveFlag: - # FileName: PureIsotherm____ + # FileName: SensorResponse___ sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') saveFileName = "SensorResponse_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('simulationFigures',saveFileName) @@ -186,14 +190,54 @@ # Save the figure if saveFlag: - # FileName: PureIsotherm____ + # FileName: SensorObjFunc____ sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') moleFrac = str(moleFrac).replace('[','').replace(']','').replace(' ','').replace(',','-').replace('.','') saveFileName = "SensorObjFunc_" + sensorText + "_" + moleFrac + "_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder + # Check if simulationFigures directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures')): os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) -plt.show() \ No newline at end of file +plt.show() + +# Plot the objective function used to evaluate the concentration for individual +# sensors and the total (sum) +if plotStdError: + # loadedFile = load("simulationResults/sensitivityAnalysis_17-15_20201109_1033_f7e470f.npz") + # loadedFile = load("simulationResults/sensitivityAnalysis_17-15_20201109_1616_63f3499.npz") + # loadedFile = load("simulationResults/sensitivityAnalysis_6-2_20201109_1208_f7e470f.npz") + loadedFile = load("simulationResults/sensitivityAnalysis_6-2_20201110_0936_63f3499.npz") + # loadedFile = load("simulationResults/sensitivityAnalysis_17-16_20201109_1416_63f3499.npz") + # loadedFile = load("simulationResults/sensitivityAnalysis_17-16_20201109_1938_63f3499.npz") + # loadedFile = load("simulationResults/sensitivityAnalysis_17-6_20201109_1651_63f3499.npz") + moleFractionG1 = loadedFile["moleFractionG1"] + meanConcEstimate = loadedFile["meanConcEstimate"] + stdConcEstimate = loadedFile["stdConcEstimate"] + fig = plt.figure + ax = plt.gca() + ax.semilogy(moleFractionG1,stdConcEstimate[:,0],':ok') # Error first sensor + ax.axvline(x=elbowPointS1, linewidth=1, linestyle='dotted', color = 'r') # Elbow point + ax.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='red', alpha=0.15) + ax.axvline(x=elbowPointS2, linewidth=1, linestyle='dotted', color = 'b') # Elbow point + ax.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='blue', alpha=0.15) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y") + ax.set(xlabel='$y_1$ [-]', + ylabel='$\sigma ({y_1})$ [-]', + xlim = [0,1.], ylim = [1e-10, 1.]) + ax.legend() + + # Save the figure + if saveFlag: + # FileName: SensorObjFunc____> + sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') + saveFileName = "SensorSenAnalStd_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + + plt.show() \ No newline at end of file diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 9c74c4d..cd715df 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,6 +13,7 @@ # concentration estimate # # Last modified: +# - 2020-11-11, AK: Add measurement nosie # - 2020-11-06, AK: Initial creation # # Input arguments: @@ -48,7 +49,7 @@ numberOfGases = 2 # Sensor combination -sensorID = [17, 16] +sensorID = [17, 6] # Custom input mole fraction for gas 1 meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] @@ -58,6 +59,10 @@ # Custom input mole fraction for gas 2 (for 3 gas system) meanMoleFracG2 = 0.20 +# Measurement noise (Guassian noise) +meanError = 0. +stdError = 0.1 + # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) stdConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) @@ -82,7 +87,8 @@ arrayConcentration = np.zeros(numberOfAdsorbents) arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) (numberOfAdsorbents,numberOfGases,None,sensorID, - moleFraction = inputMoleFrac[ii]) + moleFraction = inputMoleFrac[ii], + addMeasurementNoise = [meanError,stdError]) for ii in tqdm(range(inputMoleFrac.shape[0]))) # Convert the output list to a matrix From fe3a19a477e41634aa1b15e96ec1dfbf3aca0afd Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 Nov 2020 14:31:29 +0000 Subject: [PATCH 18/99] Improvements to run in HPC --- estimateConcentration.py | 1 - sensitivityAnalysis.py | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index c786059..e0f49f2 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -31,7 +31,6 @@ ############################################################################ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorID, **kwargs): - import pdb import numpy as np from generateTrueSensorResponse import generateTrueSensorResponse from scipy.optimize import basinhopping diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index cd715df..f069c96 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,6 +13,7 @@ # concentration estimate # # Last modified: +# - 2020-11-11, AK: Improvements to run in HPC # - 2020-11-11, AK: Add measurement nosie # - 2020-11-06, AK: Initial creation # @@ -30,9 +31,17 @@ from joblib import Parallel, delayed # For parallel processing import auxiliaryFunctions import os +import sys from tqdm import tqdm # To track progress of the loop from estimateConcentration import estimateConcentration - +import argparse + +# For atgument parser if run through terminal. Sensor configuration provided +# as an input using --s and sorbent ids separated by a space +# e.g. python sensitivityAnalysis.py --s 6 2 +parser = argparse.ArgumentParser() +parser.add_argument('--s', nargs='+', type=int) + # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -49,10 +58,18 @@ numberOfGases = 2 # Sensor combination -sensorID = [17, 6] +# Check if argument provided (from terminal) +if len(sys.argv)>1: + print("Sensor configuration provided!") + for _, value in parser.parse_args()._get_kwargs(): + sensorID = value +# Use default values +else: + print("Sensor configuration not not provided. Default used!") + sensorID = [6, 2] # Custom input mole fraction for gas 1 -meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] +meanMoleFracG1 = [0.90] diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. numberOfIterations = 100 @@ -61,7 +78,7 @@ # Measurement noise (Guassian noise) meanError = 0. -stdError = 0.1 +stdError = 0. # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) @@ -111,9 +128,12 @@ # Check if simulationResults directory exists or not. If not, create the folder if not os.path.exists('simulationResults'): os.mkdir('simulationResults') + +print(meanConcEstimate[0,0], meanConcEstimate[0,1], + stdConcEstimate[0,0], stdConcEstimate[0,1]) # Save the mean, standard deviation, and molefraction array -savez (savePath, numberOfGases = numberOfGases, - moleFractionG1 = meanMoleFracG1, - meanConcEstimate = meanConcEstimate, - stdConcEstimate = stdConcEstimate) \ No newline at end of file +# savez (savePath, numberOfGases = numberOfGases, +# moleFractionG1 = meanMoleFracG1, +# meanConcEstimate = meanConcEstimate, +# stdConcEstimate = stdConcEstimate) \ No newline at end of file From b3b31de7433bc41e2cbb9395fbd4fb467f77e85d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 Nov 2020 14:47:57 +0000 Subject: [PATCH 19/99] Cosmetic changes --- sensitivityAnalysis.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index f069c96..38da55b 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -69,7 +69,7 @@ sensorID = [6, 2] # Custom input mole fraction for gas 1 -meanMoleFracG1 = [0.90] +meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. numberOfIterations = 100 @@ -77,8 +77,8 @@ meanMoleFracG2 = 0.20 # Measurement noise (Guassian noise) -meanError = 0. -stdError = 0. +meanError = 0. # [g/kg] +stdError = 0.1 # [g/kg] # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) @@ -133,7 +133,7 @@ stdConcEstimate[0,0], stdConcEstimate[0,1]) # Save the mean, standard deviation, and molefraction array -# savez (savePath, numberOfGases = numberOfGases, -# moleFractionG1 = meanMoleFracG1, -# meanConcEstimate = meanConcEstimate, -# stdConcEstimate = stdConcEstimate) \ No newline at end of file +savez (savePath, numberOfGases = numberOfGases, + moleFractionG1 = meanMoleFracG1, + meanConcEstimate = meanConcEstimate, + stdConcEstimate = stdConcEstimate) \ No newline at end of file From 31e39477d6c0166fd8ab9da91131c8bbc1991b88 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 Nov 2020 14:52:27 +0000 Subject: [PATCH 20/99] More info in the output file --- sensitivityAnalysis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 38da55b..93c7a80 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -134,6 +134,9 @@ # Save the mean, standard deviation, and molefraction array savez (savePath, numberOfGases = numberOfGases, - moleFractionG1 = meanMoleFracG1, + numberOfIterations = numberOfIterations, + moleFractionG1 = meanMoleFracG1, + meanError = meanError, + stdError = stdError, meanConcEstimate = meanConcEstimate, stdConcEstimate = stdConcEstimate) \ No newline at end of file From 3a5cc2d356808e8a22c5f658bf526b5acd7c6c7b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 Nov 2020 19:19:02 +0000 Subject: [PATCH 21/99] Change lower bound to eps --- estimateConcentration.py | 5 +++-- sensitivityAnalysis.py | 3 --- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index e0f49f2..922939b 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -74,11 +74,12 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI for ii in range(sensorID.shape[0]): arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii],moleFracID] + measurementNoise[ii] - # Replace all negative values to zero (for physical consistency) + # Replace all negative values to eps (for physical consistency). Set to + # eps to avoid division by zero # Print if any of the responses are negative if any(ii<=0. for ii in arrayTrueResponse): print("Number of negative response: " + str(sum(arrayTrueResponse<0))) - arrayTrueResponse[arrayTrueResponse<0.] = 0. + arrayTrueResponse[arrayTrueResponse<0.] = np.finfo(float).eps # Pack the input parameters/arguments useful to compute the objective # function to estimate the mole fraction as a tuple diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 93c7a80..f81224a 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -128,9 +128,6 @@ # Check if simulationResults directory exists or not. If not, create the folder if not os.path.exists('simulationResults'): os.mkdir('simulationResults') - -print(meanConcEstimate[0,0], meanConcEstimate[0,1], - stdConcEstimate[0,0], stdConcEstimate[0,1]) # Save the mean, standard deviation, and molefraction array savez (savePath, numberOfGases = numberOfGases, From 55eabe0cccc287c149227eedfb3f61a700695a09 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 Nov 2020 20:08:16 +0000 Subject: [PATCH 22/99] Cosmetic changes for 3 sensor 2 gas system --- sensitivityAnalysis.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index f81224a..90ea09c 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -117,6 +117,11 @@ meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,3]) stdConcEstimate[ii,0] = np.std(arrayConcentration[:,2]) stdConcEstimate[ii,1] = np.std(arrayConcentration[:,3]) + elif numberOfGases == 2 and len(sensorID) == 3: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,3]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,4]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[:,3]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[:,4]) # Save the array concentration into a native numpy file # The .npz file is saved in a folder called simulationResults (hardcoded) From 9e06c373b5a5240558c2f6e3919de799fb80d1ad Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 11 Nov 2020 14:18:42 +0000 Subject: [PATCH 23/99] Add multipler error to true measurement --- estimateConcentration.py | 11 ++++++++++- sensitivityAnalysis.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index 922939b..310397e 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,7 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2020-11-11, AK: Add multiplier error to true sensor response # - 2020-11-10, AK: Add measurement noise to true sensor response # - 2020-11-09, AK: Changes to initial condition and optimizer bounds # - 2020-11-05, AK: Introduce keyword argument for custom mole fraction @@ -68,11 +69,19 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI kwargs["addMeasurementNoise"][1], sensorID.shape[0]) + # Add a multiplier error for the true measurement if the user wants it + multiplierError = np.ones(sensorID.shape[0]) + if 'multiplierError' in kwargs: + # The mean and the standard deviation of the Gaussian error is an + # input from the user + multiplierError = kwargs["multiplierError"] + # Parse out the true sensor response for a sensor array with n number of # sensors given by sensorID arrayTrueResponse = np.zeros(sensorID.shape[0]) for ii in range(sensorID.shape[0]): - arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii],moleFracID] + measurementNoise[ii] + arrayTrueResponse[ii] = multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] + + measurementNoise[ii] # Replace all negative values to eps (for physical consistency). Set to # eps to avoid division by zero diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 90ea09c..3182823 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,8 +13,9 @@ # concentration estimate # # Last modified: -# - 2020-11-11, AK: Improvements to run in HPC -# - 2020-11-11, AK: Add measurement nosie +# - 2020-11-11, AK: Add multipler nosie +# - 2020-11-10, AK: Improvements to run in HPC +# - 2020-11-10, AK: Add measurement nosie # - 2020-11-06, AK: Initial creation # # Input arguments: @@ -76,6 +77,9 @@ # Custom input mole fraction for gas 2 (for 3 gas system) meanMoleFracG2 = 0.20 +# Multipler error for the sensor measurement +multiplierError = [1., 1.] + # Measurement noise (Guassian noise) meanError = 0. # [g/kg] stdError = 0.1 # [g/kg] @@ -105,6 +109,7 @@ arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) (numberOfAdsorbents,numberOfGases,None,sensorID, moleFraction = inputMoleFrac[ii], + multiplierError = multiplierError, addMeasurementNoise = [meanError,stdError]) for ii in tqdm(range(inputMoleFrac.shape[0]))) @@ -138,6 +143,7 @@ savez (savePath, numberOfGases = numberOfGases, numberOfIterations = numberOfIterations, moleFractionG1 = meanMoleFracG1, + multiplierError = multiplierError, meanError = meanError, stdError = stdError, meanConcEstimate = meanConcEstimate, From bd0e6bc5f9fe8b83be6c746346a136d043d0e2ef Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 11 Nov 2020 17:48:54 +0000 Subject: [PATCH 24/99] Bug fix for multiplier error --- estimateConcentration.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index 310397e..07f4b24 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -82,7 +82,7 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI for ii in range(sensorID.shape[0]): arrayTrueResponse[ii] = multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] + measurementNoise[ii] - + # Replace all negative values to eps (for physical consistency). Set to # eps to avoid division by zero # Print if any of the responses are negative @@ -92,7 +92,8 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI # Pack the input parameters/arguments useful to compute the objective # function to estimate the mole fraction as a tuple - inputParameters = (arrayTrueResponse, pressureTotal, temperature, sensorID) + inputParameters = (arrayTrueResponse, pressureTotal, temperature, + sensorID, multiplierError) # Minimize an objective function to compute the mole fraction of the feed # gas to the sensor @@ -116,14 +117,14 @@ def concObjectiveFunction(x, *inputParameters): # Unpack the tuple that contains the true response, pressure, temperature, # and the sensor identifiers - arrayTrueResponse, pressureTotal, temperature, sensorID = inputParameters + arrayTrueResponse, pressureTotal, temperature, sensorID, multiplierError = inputParameters # Reshape the mole fraction to a row vector for compatibility moleFraction = np.array([x]) # This is needed to keep the structure as a row instead of column # moleFraction = np.array([x,1.-x]).T # This is needed to keep the structure as a row instead of column # Compute the sensor reponse for a given mole fraction input - arraySimResponse = simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction) - + arraySimResponse = simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction) * multiplierError + # Compute the sum of the error for the sensor array return sum(np.power((arrayTrueResponse - arraySimResponse)/arrayTrueResponse,2)) \ No newline at end of file From 68f00ffc0d468eda1f0f4338112176eda831a6cb Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 12 Nov 2020 09:53:29 +0000 Subject: [PATCH 25/99] Bug fix for multipler error --- estimateConcentration.py | 5 +++-- sensitivityAnalysis.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index 07f4b24..131790d 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,7 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2020-11-12, AK: Bug fix for multipler error # - 2020-11-11, AK: Add multiplier error to true sensor response # - 2020-11-10, AK: Add measurement noise to true sensor response # - 2020-11-09, AK: Changes to initial condition and optimizer bounds @@ -74,8 +75,8 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI if 'multiplierError' in kwargs: # The mean and the standard deviation of the Gaussian error is an # input from the user - multiplierError = kwargs["multiplierError"] - + multiplierErrorTemp = kwargs["multiplierError"] + multiplierError[0:len(multiplierErrorTemp)] = multiplierErrorTemp # Parse out the true sensor response for a sensor array with n number of # sensors given by sensorID arrayTrueResponse = np.zeros(sensorID.shape[0]) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 3182823..5bf3a03 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,6 +13,7 @@ # concentration estimate # # Last modified: +# - 2020-11-12, AK: Bug fix for multipler error # - 2020-11-11, AK: Add multipler nosie # - 2020-11-10, AK: Improvements to run in HPC # - 2020-11-10, AK: Add measurement nosie @@ -127,6 +128,11 @@ meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,4]) stdConcEstimate[ii,0] = np.std(arrayConcentration[:,3]) stdConcEstimate[ii,1] = np.std(arrayConcentration[:,4]) + elif numberOfGases == 2 and len(sensorID) == 4: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,4]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,5]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[:,4]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[:,5]) # Save the array concentration into a native numpy file # The .npz file is saved in a folder called simulationResults (hardcoded) From 6850b06dbd16eff53a378a10e324503b7a751217 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 12 Nov 2020 20:03:24 +0000 Subject: [PATCH 26/99] Crazy indentation bug fix in estimateConcentration --- estimateConcentration.py | 6 +++--- sensitivityAnalysis.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index 131790d..e0855da 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -81,9 +81,9 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI # sensors given by sensorID arrayTrueResponse = np.zeros(sensorID.shape[0]) for ii in range(sensorID.shape[0]): - arrayTrueResponse[ii] = multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] - + measurementNoise[ii] - + arrayTrueResponse[ii] = (multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] + + measurementNoise[ii]) + # Replace all negative values to eps (for physical consistency). Set to # eps to avoid division by zero # Print if any of the responses are negative diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 5bf3a03..b620323 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -71,7 +71,7 @@ sensorID = [6, 2] # Custom input mole fraction for gas 1 -meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] +meanMoleFracG1 = [0.90] diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. numberOfIterations = 100 From 6e37aea00b59d80f8a80c02d4e9e4b274842e051 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 12 Nov 2020 20:04:12 +0000 Subject: [PATCH 27/99] Merge conflict --- estimateConcentration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index e0855da..443f91b 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -83,7 +83,7 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI for ii in range(sensorID.shape[0]): arrayTrueResponse[ii] = (multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] + measurementNoise[ii]) - + # Replace all negative values to eps (for physical consistency). Set to # eps to avoid division by zero # Print if any of the responses are negative From 7b9a716d715975b93b618f7af8c922b855f0bd68 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 12 Nov 2020 20:07:59 +0000 Subject: [PATCH 28/99] Stupid stash merge conflict --- estimateConcentration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index 443f91b..e0855da 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -83,7 +83,7 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI for ii in range(sensorID.shape[0]): arrayTrueResponse[ii] = (multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] + measurementNoise[ii]) - + # Replace all negative values to eps (for physical consistency). Set to # eps to avoid division by zero # Print if any of the responses are negative From ae510f96fbdf3dd1f440a72caa021887183f862d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 13 Nov 2020 11:09:05 +0000 Subject: [PATCH 29/99] Bug fixes --- plotFunctions/plotOjectiveFunction.py | 121 +++++++++++++++++++------- sensitivityAnalysis.py | 59 ++++++++++--- 2 files changed, 137 insertions(+), 43 deletions(-) diff --git a/plotFunctions/plotOjectiveFunction.py b/plotFunctions/plotOjectiveFunction.py index 9fd657d..8298269 100755 --- a/plotFunctions/plotOjectiveFunction.py +++ b/plotFunctions/plotOjectiveFunction.py @@ -39,7 +39,10 @@ saveFlag = False # Plot flag to show standard deviation of errors -plotStdError = False +plotStdError = True +plotRaw = True +plotBH = False +plotNoise = True # Save file extension (png or pdf) saveFileExtension = ".png" @@ -60,8 +63,11 @@ # Mole Fraction of interest moleFrac = [0.1, 0.9] +# Multiplier Error +multiplierError = [1., 1.] + # Sensor ID -sensorID = np.array([16, 7]) +sensorID = np.array([6, 2]) # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -86,7 +92,7 @@ arraySimResponse = np.zeros([moleFractionRange.shape[0],numberOfGases]) for ii in range(moleFractionRange.shape[0]): arraySimResponse[ii,:] = simulateSensorArray(sensorID, pressureTotal, - temperature, np.array([moleFractionRange[ii,:]])) + temperature, np.array([moleFractionRange[ii,:]])) * multiplierError # Get the individual sensor reponse for all the given "experimental/test" concentrations sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, @@ -94,7 +100,7 @@ # Parse out the true sensor response for the desired sensors in the array arrayTrueResponse = np.zeros(sensorID.shape[0]) for ii in range(sensorID.shape[0]): - arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii]] + arrayTrueResponse[ii] = sensorTrueResponse[sensorID[ii]]*multiplierError[ii] arrayTrueResponse = np.tile(arrayTrueResponse,(moleFractionRange.shape[0],1)) # Compute the objective function over all the mole fractions @@ -137,13 +143,11 @@ fig = plt.figure ax = plt.gca() # Sensor 1 -ax.plot(moleFractionRange[:,0],arraySimResponse[:,0],'r', label = '$s_1$') # Simulated Response -ax.axvline(x=elbowPointS1, linewidth=1, linestyle='dotted', color = 'r') # Elbow point -ax.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='red', alpha=0.15) +ax.plot(moleFractionRange[:,0],arraySimResponse[:,0],color ='#1DBDE6', label = '$s_1$') # Simulated Response +ax.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='#1DBDE6', alpha=0.25) # Sensor 2 -ax.plot(moleFractionRange[:,0],arraySimResponse[:,1],'b', label = '$s_2$') # Simulated Response -ax.axvline(x=elbowPointS2, linewidth=1, linestyle='dotted', color = 'b') # Elbow point -ax.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='blue', alpha=0.15) +ax.plot(moleFractionRange[:,0],arraySimResponse[:,1], color = '#F1515E', label = '$s_2$') # Simulated Response +ax.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='#F1515E', alpha=0.25) # Sensor 3 if numberOfGases == 3: ax.axhline(y=arrayTrueResponse[0,2], linewidth=1, linestyle='dotted', @@ -174,9 +178,9 @@ fig = plt.figure ax = plt.gca() ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,0] - -arraySimResponse[:,0])/arrayTrueResponse[:,0],2),'r', label = '$J_1$') # Error first sensor + -arraySimResponse[:,0])/arrayTrueResponse[:,0],2), color = '#1DBDE6', label = '$J_1$') # Error first sensor ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,1] - -arraySimResponse[:,1])/arrayTrueResponse[:,1],2),'b', label = '$J_2$') # Error second sensor + -arraySimResponse[:,1])/arrayTrueResponse[:,1],2), color = '#F1515E', label = '$J_2$') # Error second sensor if numberOfGases == 3: ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,2] -arraySimResponse[:,2])/arrayTrueResponse[:,2],2),'g', label = '$J_3$') # Error third sensor @@ -205,29 +209,82 @@ # Plot the objective function used to evaluate the concentration for individual # sensors and the total (sum) if plotStdError: - # loadedFile = load("simulationResults/sensitivityAnalysis_17-15_20201109_1033_f7e470f.npz") - # loadedFile = load("simulationResults/sensitivityAnalysis_17-15_20201109_1616_63f3499.npz") - # loadedFile = load("simulationResults/sensitivityAnalysis_6-2_20201109_1208_f7e470f.npz") - loadedFile = load("simulationResults/sensitivityAnalysis_6-2_20201110_0936_63f3499.npz") - # loadedFile = load("simulationResults/sensitivityAnalysis_17-16_20201109_1416_63f3499.npz") - # loadedFile = load("simulationResults/sensitivityAnalysis_17-16_20201109_1938_63f3499.npz") - # loadedFile = load("simulationResults/sensitivityAnalysis_17-6_20201109_1651_63f3499.npz") - moleFractionG1 = loadedFile["moleFractionG1"] - meanConcEstimate = loadedFile["meanConcEstimate"] - stdConcEstimate = loadedFile["stdConcEstimate"] + if set(sensorID) == set([17,15]): + loadedFileRaw = load("simulationResults/sensitivityAnalysis_17-15_20201109_1033_f7e470f.npz") + loadedFileBH = load("simulationResults/sensitivityAnalysis_17-15_20201109_1616_63f3499.npz") + loadedFileNoise = load("simulationResults/sensitivityAnalysis_17-15_20201110_1458_31e3947.npz") + elif set(sensorID) == set([6,2]): + loadedFileRaw = load("simulationResults/sensitivityAnalysis_6-2_20201109_1208_f7e470f.npz") + loadedFileBH = load("simulationResults/sensitivityAnalysis_6-2_20201110_0936_63f3499.npz") + loadedFileNoise = load("simulationResults/sensitivityAnalysis_6-2_20201110_1458_31e3947.npz") + elif set(sensorID) == set([17,16]): + loadedFileRaw = load("simulationResults/sensitivityAnalysis_17-16_20201109_1416_63f3499.npz") + loadedFileBH = load("simulationResults/sensitivityAnalysis_17-16_20201109_1938_63f3499.npz") + loadedFileNoise = load("simulationResults/sensitivityAnalysis_17-16_20201110_1458_31e3947.npz") + elif set(sensorID) == set([17,6]): + loadedFileRaw = load("simulationResults/sensitivityAnalysis_17-6_20201109_1651_63f3499.npz") + loadedFileBH = load("simulationResults/sensitivityAnalysis_17-6_20201110_1205_63f3499.npz") + loadedFileNoise = load("simulationResults/sensitivityAnalysis_17-6_20201110_1458_31e3947.npz") + + # Parse raw data (no noise, default basin hopping iterations (50)) + if plotRaw: + moleFractionG1_Raw = loadedFileRaw["moleFractionG1"] + meanConcEstimate_Raw = loadedFileRaw["meanConcEstimate"] + stdConcEstimate_Raw = loadedFileRaw["stdConcEstimate"] + + # Parse data with higher number of iterations for BH (250) + if plotBH: + moleFractionG1_BH = loadedFileBH["moleFractionG1"] + meanConcEstimate_BH = loadedFileBH["meanConcEstimate"] + stdConcEstimate_BH = loadedFileBH["stdConcEstimate"] + + # Parse data with noise and default iterations for BH (50) + if plotNoise: + moleFractionG1_Noise = loadedFileNoise["moleFractionG1"] + meanConcEstimate_Noise = loadedFileNoise["meanConcEstimate"] + stdConcEstimate_Noise = loadedFileNoise["stdConcEstimate"] + + os.chdir("plotFunctions") + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + os.chdir("..") + fig = plt.figure - ax = plt.gca() - ax.semilogy(moleFractionG1,stdConcEstimate[:,0],':ok') # Error first sensor - ax.axvline(x=elbowPointS1, linewidth=1, linestyle='dotted', color = 'r') # Elbow point - ax.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='red', alpha=0.15) - ax.axvline(x=elbowPointS2, linewidth=1, linestyle='dotted', color = 'b') # Elbow point - ax.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='blue', alpha=0.15) - ax.locator_params(axis="x", nbins=4) - ax.locator_params(axis="y") - ax.set(xlabel='$y_1$ [-]', + ax1 = plt.subplot(1,2,1) + ax2 = plt.subplot(1,2,2) + + ax1.semilogy(np.linspace(0,1,100), np.linspace(0,1,100), + linewidth = 1, linestyle = '--', color = '#adb5bd') + if plotRaw: + ax1.semilogy(moleFractionG1_Raw,meanConcEstimate_Raw[:,0], marker='o', + linestyle='None', color='#4f772d', label = 'Reference') # Raw + ax2.semilogy(moleFractionG1_Raw,stdConcEstimate_Raw[:,0], marker='o', + linewidth = 1, linestyle = ':', color='#4f772d', label = 'Reference') # Raw + if plotBH: + ax1.semilogy(moleFractionG1_BH,meanConcEstimate_BH[:,0], marker='o', + linestyle='None', color='#90a955', label = 'More Iterations') # Basin hopping + ax2.semilogy(moleFractionG1_BH,stdConcEstimate_BH[:,0], marker='o', + linewidth = 1, linestyle = ':', color='#90a955', label = 'More Iterations') # Basin hopping + if plotNoise: + ax1.semilogy(moleFractionG1_Noise,meanConcEstimate_Noise[:,0], marker='o', + linestyle='None', color='#90a955', label = 'With Noise') # Noise + ax2.semilogy(moleFractionG1_Noise,stdConcEstimate_Noise[:,0], marker='o', + linewidth = 1, linestyle = ':', color='#90a955', label = 'With Noise') # Noise + + ax2.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='#1DBDE6', alpha=0.25) + ax2.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='#F1515E', alpha=0.25) + + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y") + ax1.set(xlabel='True $y_1$ [-]', + ylabel='Estimated $y_1$ [-]', + xlim = [0,1.], ylim = [1e-4, 1.]) + + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y") + ax2.set(xlabel='$y_1$ [-]', ylabel='$\sigma ({y_1})$ [-]', xlim = [0,1.], ylim = [1e-10, 1.]) - ax.legend() + ax2.legend() # Save the figure if saveFlag: diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index b620323..c1c7b24 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -38,12 +38,22 @@ from estimateConcentration import estimateConcentration import argparse -# For atgument parser if run through terminal. Sensor configuration provided +# For argument parser if run through terminal. Sensor configuration provided # as an input using --s and sorbent ids separated by a space # e.g. python sensitivityAnalysis.py --s 6 2 parser = argparse.ArgumentParser() parser.add_argument('--s', nargs='+', type=int) +# # Noise configuration provided as an input using --e and mean and standard +# # deviation of the Gaussian noise is separated by white space +# # e.g. python sensitivityAnalysis.py --e 0.0 0.1 +# parser.add_argument('--e', nargs='+', type=float) + +# # separated by a white space +# # Multiplier error provided as an input using --m and multiplier error is +# # e.g. python sensitivityAnalysis.py --e 0.0 0.1 +# parser.add_argument('--m', nargs='+', type=float) + # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -62,29 +72,56 @@ # Sensor combination # Check if argument provided (from terminal) if len(sys.argv)>1: - print("Sensor configuration provided!") + print("\nSensor configuration is provided!") for _, value in parser.parse_args()._get_kwargs(): sensorID = value # Use default values else: - print("Sensor configuration not not provided. Default used!") + print("\nSensor configuration is not provided. Default used!") sensorID = [6, 2] + +# # Measurement noise information +# # Check if argument provided (from terminal) for Guassian noise +# if len(sys.argv)>1: +# print("\nMeasurement noise information is provided!") +# for _, value in parser.parse_args()._get_kwargs(): +# noiseInfo = value +# meanError = nosieInfo[0] # [g/kg] +# stdError = nosieInfo[1] # [g/kg] +# # Use default values +# else: +# print("\nMeasurement noise information is not provided!. Default used!") +# # Measurement noise (Guassian noise) +# meanError = 0. # [g/kg] +# stdError = 0.1 # [g/kg] + +# # Multipler error combination +# # Check if argument provided (from terminal) +# if len(sys.argv)>1: +# print("\Multipler error information is provided!") +# for _, value in parser.parse_args()._get_kwargs(): +# multiplierError = value +# # Use default values +# else: +# print("\nMultipler error information is not provided. Default used!") +# # Multipler error for the sensor measurement +# multiplierError = [5., 1.] + +# Measurement noise (Guassian noise) +meanError = 0. # [g/kg] +stdError = 0.1 # [g/kg] + +# Multipler error for the sensor measurement +multiplierError = [1., 1.] # Custom input mole fraction for gas 1 -meanMoleFracG1 = [0.90] +meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. numberOfIterations = 100 # Custom input mole fraction for gas 2 (for 3 gas system) meanMoleFracG2 = 0.20 -# Multipler error for the sensor measurement -multiplierError = [1., 1.] - -# Measurement noise (Guassian noise) -meanError = 0. # [g/kg] -stdError = 0.1 # [g/kg] - # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) stdConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) From d6512745eade8a794cc4d6100f11154edb073f63 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 13 Nov 2020 11:56:19 +0000 Subject: [PATCH 30/99] More bug fixes due to merge conflict --- plotFunctions/plotConcentrationEstimate.py | 16 ++- sensitivityAnalysis.py | 132 ++++++++++----------- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/plotFunctions/plotConcentrationEstimate.py b/plotFunctions/plotConcentrationEstimate.py index 00e34a8..72e7035 100755 --- a/plotFunctions/plotConcentrationEstimate.py +++ b/plotFunctions/plotConcentrationEstimate.py @@ -12,6 +12,7 @@ # Plots to visualize estimated concentration # # Last modified: +# - 2020-11-09, AK: Add functionality for .npz file # - 2020-11-09, AK: Cosmetic changes # - 2020-11-04, AK: Improve plotting capability for three gases/sensors # - 2020-10-30, AK: Add zoomed in version @@ -77,7 +78,8 @@ # loadFileName = "arrayConcentration_20201104_1732_cc08dc4.npy" # 2 gases, 2 sensor [0.15, 0.85] # loadFileName = "arrayConcentration_20201030_1731_da1707b.npy" # 2 gases, 2 sensor [0.4, 0.6] # loadFileName = "arrayConcentration_20201104_1842_cc08dc4.npy" # 2 gases, 2 sensor [0.75, 0.25] -loadFileName = "arrayConcentration_20201104_2227_cc08dc4.npy" # 2 gases, 2 sensor [0.75, 0.25] +# loadFileName = "arrayConcentration_20201104_2227_cc08dc4.npy" # 2 gases, 2 sensor [0.75, 0.25] +loadFileName = "sensitivityAnalysis_17-15_20201112_1755_68f00ff.npz" simResultsFile = os.path.join('..','simulationResults',loadFileName); # Get the commit ID of the current repository @@ -89,9 +91,19 @@ # Git commit id of the loaded isotherm file simID_loadedFile = loadFileName[-21:-4] +# Check if the file is npz and parse out the necessary information +loadFileExt = loadFileName[-3:] +if loadFileExt == 'npz': + fileNPZ = True +else: + fileNPZ = False + # Check if the file with the adsorbent properties exist if os.path.exists(simResultsFile): - resultOutput = load(simResultsFile) + if fileNPZ: + resultOutput = load(simResultsFile)["arrayConcentration"] + else: + resultOutput = load(simResultsFile) actualOutput = resultOutput if numberOfGases == 2: if resultOutput.shape[1] == 3: diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index c1c7b24..e04595d 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -38,22 +38,12 @@ from estimateConcentration import estimateConcentration import argparse -# For argument parser if run through terminal. Sensor configuration provided +# For atgument parser if run through terminal. Sensor configuration provided # as an input using --s and sorbent ids separated by a space # e.g. python sensitivityAnalysis.py --s 6 2 parser = argparse.ArgumentParser() parser.add_argument('--s', nargs='+', type=int) -# # Noise configuration provided as an input using --e and mean and standard -# # deviation of the Gaussian noise is separated by white space -# # e.g. python sensitivityAnalysis.py --e 0.0 0.1 -# parser.add_argument('--e', nargs='+', type=float) - -# # separated by a white space -# # Multiplier error provided as an input using --m and multiplier error is -# # e.g. python sensitivityAnalysis.py --e 0.0 0.1 -# parser.add_argument('--m', nargs='+', type=float) - # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -63,6 +53,9 @@ # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() +# Check if multiple concentrations are to be simualated +multipleConcentrationFlag = False + # Number of adsorbents numberOfAdsorbents = 30 @@ -72,50 +65,29 @@ # Sensor combination # Check if argument provided (from terminal) if len(sys.argv)>1: - print("\nSensor configuration is provided!") + print("Sensor configuration provided!") for _, value in parser.parse_args()._get_kwargs(): sensorID = value # Use default values else: - print("\nSensor configuration is not provided. Default used!") + print("\nSensor configuration not not provided. Default used!") sensorID = [6, 2] - -# # Measurement noise information -# # Check if argument provided (from terminal) for Guassian noise -# if len(sys.argv)>1: -# print("\nMeasurement noise information is provided!") -# for _, value in parser.parse_args()._get_kwargs(): -# noiseInfo = value -# meanError = nosieInfo[0] # [g/kg] -# stdError = nosieInfo[1] # [g/kg] -# # Use default values -# else: -# print("\nMeasurement noise information is not provided!. Default used!") -# # Measurement noise (Guassian noise) -# meanError = 0. # [g/kg] -# stdError = 0.1 # [g/kg] - -# # Multipler error combination -# # Check if argument provided (from terminal) -# if len(sys.argv)>1: -# print("\Multipler error information is provided!") -# for _, value in parser.parse_args()._get_kwargs(): -# multiplierError = value -# # Use default values -# else: -# print("\nMultipler error information is not provided. Default used!") -# # Multipler error for the sensor measurement -# multiplierError = [5., 1.] - + # Measurement noise (Guassian noise) meanError = 0. # [g/kg] stdError = 0.1 # [g/kg] # Multipler error for the sensor measurement -multiplierError = [1., 1.] +multiplierError = [1., 5.] # Custom input mole fraction for gas 1 -meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] +# meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] +meanMoleFracG1 = [0.90] +if not multipleConcentrationFlag: + if len(meanMoleFracG1)>1: + errorString = "When multipleConcentrationFlag is inactive only one concentration should be provided." + raise Exception(errorString) + diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. numberOfIterations = 100 @@ -154,23 +126,31 @@ # Convert the output list to a matrix arrayConcentration = np.array(arrayConcentration) - # Compute the mean and the standard deviation of the concentration estimates - if numberOfGases == 2 and len(sensorID) == 2: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,2]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,3]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[:,2]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[:,3]) - elif numberOfGases == 2 and len(sensorID) == 3: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,3]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,4]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[:,3]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[:,4]) - elif numberOfGases == 2 and len(sensorID) == 4: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,4]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,5]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[:,4]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[:,5]) + # Get the concentraiton mean and standard deviation for multiple concentrations + # For single concentration, get the true sensor response + if multipleConcentrationFlag: + # Compute the mean and the standard deviation of the concentration estimates + if numberOfGases == 2 and len(sensorID) == 2: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,2]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,3]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[:,2]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[:,3]) + elif numberOfGases == 2 and len(sensorID) == 3: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,3]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,4]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[:,3]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[:,4]) + elif numberOfGases == 2 and len(sensorID) == 4: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,4]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,5]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[:,4]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[:,5]) + +# Check if simulationResults directory exists or not. If not, create the folder +if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') + # Save the array concentration into a native numpy file # The .npz file is saved in a folder called simulationResults (hardcoded) filePrefix = "sensitivityAnalysis" @@ -178,16 +158,24 @@ saveFileName = filePrefix + "_" + sensorText + "_" + simulationDT + "_" + gitCommitID; savePath = os.path.join('simulationResults',saveFileName) -# Check if simulationResults directory exists or not. If not, create the folder -if not os.path.exists('simulationResults'): - os.mkdir('simulationResults') - -# Save the mean, standard deviation, and molefraction array -savez (savePath, numberOfGases = numberOfGases, - numberOfIterations = numberOfIterations, - moleFractionG1 = meanMoleFracG1, - multiplierError = multiplierError, - meanError = meanError, - stdError = stdError, - meanConcEstimate = meanConcEstimate, - stdConcEstimate = stdConcEstimate) \ No newline at end of file +# Save mean and std. for multiple concentraiton and array concentration +# array for single concentration +if multipleConcentrationFlag: + # Save the mean, standard deviation, and molefraction array + savez (savePath, numberOfGases = numberOfGases, + numberOfIterations = numberOfIterations, + moleFractionG1 = meanMoleFracG1, + multiplierError = multiplierError, + meanError = meanError, + stdError = stdError, + meanConcEstimate = meanConcEstimate, + stdConcEstimate = stdConcEstimate) +else: + # Save the array concentration output + savez (savePath, numberOfGases = numberOfGases, + numberOfIterations = numberOfIterations, + moleFractionG1 = meanMoleFracG1, + multiplierError = multiplierError, + meanError = meanError, + stdError = stdError, + arrayConcentration = arrayConcentration) \ No newline at end of file From c9b2a41c157e0f8008824a7e2e13bf0bf917038e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 13 Nov 2020 14:41:35 +0000 Subject: [PATCH 31/99] Save arrayConcentration to the output --- sensitivityAnalysis.py | 86 ++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index e04595d..844f050 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,6 +13,7 @@ # concentration estimate # # Last modified: +# - 2020-11-12, AK: Save arrayConcentration in output # - 2020-11-12, AK: Bug fix for multipler error # - 2020-11-11, AK: Add multipler nosie # - 2020-11-10, AK: Improvements to run in HPC @@ -53,9 +54,6 @@ # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() -# Check if multiple concentrations are to be simualated -multipleConcentrationFlag = False - # Number of adsorbents numberOfAdsorbents = 30 @@ -78,15 +76,11 @@ stdError = 0.1 # [g/kg] # Multipler error for the sensor measurement -multiplierError = [1., 5.] +multiplierError = [1., 1.] # Custom input mole fraction for gas 1 -# meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] -meanMoleFracG1 = [0.90] -if not multipleConcentrationFlag: - if len(meanMoleFracG1)>1: - errorString = "When multipleConcentrationFlag is inactive only one concentration should be provided." - raise Exception(errorString) +meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] +# meanMoleFracG1 = [0.90] diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. numberOfIterations = 100 @@ -98,6 +92,10 @@ meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) stdConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) +# Initialize the arrayConcentration matrix +arrayConcentration = np.zeros([len(meanMoleFracG1),numberOfIterations, + numberOfGases+len(sensorID)]) + for ii in range(len(meanMoleFracG1)): # Generate a uniform distribution of mole fractions if numberOfGases == 2: @@ -115,8 +113,7 @@ # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials - arrayConcentration = np.zeros(numberOfAdsorbents) - arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) (numberOfAdsorbents,numberOfGases,None,sensorID, moleFraction = inputMoleFrac[ii], multiplierError = multiplierError, @@ -124,27 +121,26 @@ for ii in tqdm(range(inputMoleFrac.shape[0]))) # Convert the output list to a matrix - arrayConcentration = np.array(arrayConcentration) + arrayConcentration[ii,:,:] = np.array(arrayConcentrationTemp) # Get the concentraiton mean and standard deviation for multiple concentrations # For single concentration, get the true sensor response - if multipleConcentrationFlag: - # Compute the mean and the standard deviation of the concentration estimates - if numberOfGases == 2 and len(sensorID) == 2: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,2]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,3]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[:,2]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[:,3]) - elif numberOfGases == 2 and len(sensorID) == 3: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,3]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,4]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[:,3]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[:,4]) - elif numberOfGases == 2 and len(sensorID) == 4: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[:,4]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[:,5]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[:,4]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[:,5]) + # Compute the mean and the standard deviation of the concentration estimates + if numberOfGases == 2 and len(sensorID) == 2: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[ii,:,2]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[ii,:,3]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[ii,:,2]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[ii,:,3]) + elif numberOfGases == 2 and len(sensorID) == 3: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[ii,:,3]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[ii,:,4]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[ii,:,3]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[ii,:,4]) + elif numberOfGases == 2 and len(sensorID) == 4: + meanConcEstimate[ii,0] = np.mean(arrayConcentration[ii,:,4]) + meanConcEstimate[ii,1] = np.mean(arrayConcentration[ii,:,5]) + stdConcEstimate[ii,0] = np.std(arrayConcentration[ii,:,4]) + stdConcEstimate[ii,1] = np.std(arrayConcentration[ii,:,5]) # Check if simulationResults directory exists or not. If not, create the folder @@ -160,22 +156,14 @@ # Save mean and std. for multiple concentraiton and array concentration # array for single concentration -if multipleConcentrationFlag: - # Save the mean, standard deviation, and molefraction array - savez (savePath, numberOfGases = numberOfGases, - numberOfIterations = numberOfIterations, - moleFractionG1 = meanMoleFracG1, - multiplierError = multiplierError, - meanError = meanError, - stdError = stdError, - meanConcEstimate = meanConcEstimate, - stdConcEstimate = stdConcEstimate) -else: - # Save the array concentration output - savez (savePath, numberOfGases = numberOfGases, - numberOfIterations = numberOfIterations, - moleFractionG1 = meanMoleFracG1, - multiplierError = multiplierError, - meanError = meanError, - stdError = stdError, - arrayConcentration = arrayConcentration) \ No newline at end of file + +# Save the mean, standard deviation, and molefraction array +savez (savePath, numberOfGases = numberOfGases, + numberOfIterations = numberOfIterations, + moleFractionG1 = meanMoleFracG1, + multiplierError = multiplierError, + meanError = meanError, + stdError = stdError, + meanConcEstimate = meanConcEstimate, + stdConcEstimate = stdConcEstimate, + arrayConcentration = arrayConcentration) \ No newline at end of file From 590b843d2c1bd3c06c171fb89e8d8d17446063db Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 17 Nov 2020 19:48:38 +0000 Subject: [PATCH 32/99] Addition/modification to plotting scripts --- plotFunctions/plotConcentrationEstimate.py | 11 +- plotFunctions/plotConcentrationViolinPlots.py | 169 +++++++++++++++ plotFunctions/plotOjectiveFunction.py | 195 ++++-------------- 3 files changed, 220 insertions(+), 155 deletions(-) create mode 100755 plotFunctions/plotConcentrationViolinPlots.py diff --git a/plotFunctions/plotConcentrationEstimate.py b/plotFunctions/plotConcentrationEstimate.py index 72e7035..f8215ef 100755 --- a/plotFunctions/plotConcentrationEstimate.py +++ b/plotFunctions/plotConcentrationEstimate.py @@ -12,6 +12,7 @@ # Plots to visualize estimated concentration # # Last modified: +# - 2020-11-17, AK: Cosmetic changes # - 2020-11-09, AK: Add functionality for .npz file # - 2020-11-09, AK: Cosmetic changes # - 2020-11-04, AK: Improve plotting capability for three gases/sensors @@ -46,6 +47,9 @@ # Plot zoomed plot plotZoomDist = False +# Mole frac ID (use this if only .npz is used - sensitivity studies) +moleFracID = 0 + # Gas concentration molFracG1 = 0.90 molFracG2 = 0.10 @@ -53,7 +57,7 @@ # Xlimits and Ylimits xLimits = [0,1] -yLimits = [0,300] +yLimits = [0,500] xLimitsSum = [0,2] yLimitsSum = [0,300] @@ -79,7 +83,7 @@ # loadFileName = "arrayConcentration_20201030_1731_da1707b.npy" # 2 gases, 2 sensor [0.4, 0.6] # loadFileName = "arrayConcentration_20201104_1842_cc08dc4.npy" # 2 gases, 2 sensor [0.75, 0.25] # loadFileName = "arrayConcentration_20201104_2227_cc08dc4.npy" # 2 gases, 2 sensor [0.75, 0.25] -loadFileName = "sensitivityAnalysis_17-15_20201112_1755_68f00ff.npz" +loadFileName = "sensitivityAnalysis_17-16_20201114_2032_c9b2a41.npz" simResultsFile = os.path.join('..','simulationResults',loadFileName); # Get the commit ID of the current repository @@ -101,10 +105,9 @@ # Check if the file with the adsorbent properties exist if os.path.exists(simResultsFile): if fileNPZ: - resultOutput = load(simResultsFile)["arrayConcentration"] + resultOutput = load(simResultsFile)["arrayConcentration"][moleFracID,:,:] else: resultOutput = load(simResultsFile) - actualOutput = resultOutput if numberOfGases == 2: if resultOutput.shape[1] == 3: resultOutput = np.delete(resultOutput,[0],1) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py new file mode 100755 index 0000000..87cc4fc --- /dev/null +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -0,0 +1,169 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plots to visualize different sensor responses +# +# Last modified: +# - 2020-11-13, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ +import pdb +import numpy as np +from numpy import load +import os +import pandas as pd +import seaborn as sns +import auxiliaryFunctions +import matplotlib.pyplot as plt +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Save file extension (png or pdf) +saveFileExtension = ".png" + +# Save flag +saveFlag = False + +# Flag for comparison +flagComparison = True + +# Mole fraction ID +moleFracID = 3 +meanMolFrac = [0.001,0.01,0.1,0.25,0.5,0.75,0.90] + +# Y limits for the plot +Y_LIMITS = [1e-4,1.] +scaleLog = True + +# Legend First +legendText = ["Without Noise", "With Noise"] +legendFlag = True + +# Sensor ID +sensorText = ["6/2", "17/16", "17/16", "17/6"] + +# Initialize x, y, and type for the plotting +concatenatedX = [] +concatenatedY = [] +concatenatedType = [] + +# File to be loaded for the left of violin plot +loadFileName = ["sensitivityAnalysis_6-2_20201113_1450_c9b2a41.npz", + "sensitivityAnalysis_6-2_20201116_1806_c9b2a41.npz"] +saveFileSensorText = [17, 16] + +if flagComparison and len(loadFileName) != 2: + errorString = "When flagComparison is True, only two files can be loaded for comparison." + raise Exception(errorString) + +# Loop through the different files to generate the violin plot +for kk in range(len(loadFileName)): + # Initialize x, y, and type for the local loop + xVar = [] + y1Var = [] + y2Var = [] + typeVar = [] + + simResultsFile = os.path.join('..','simulationResults',loadFileName[kk]); + resultOutput = load(simResultsFile)["arrayConcentration"] + numberOfGases = load(simResultsFile)["numberOfGases"] + moleFrac = load(simResultsFile)["moleFractionG1"] + + # For the cases where there are two gases + if numberOfGases == 2: + for ii in range(resultOutput.shape[0]): + # Loop through all the molefractions for comparison + if flagComparison: + y1Var = np.concatenate((y1Var,resultOutput[ii,:,2])) # y1 + y2Var = np.concatenate((y2Var,resultOutput[ii,:,3])) # y2 + xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,2])) # x (true mole fraction) + # Parse the data only for the desired mole fraction + else: + if ii == moleFracID: + y1Var = np.concatenate((y1Var,resultOutput[ii,:,2])) # y1 + y2Var = np.concatenate((y2Var,resultOutput[ii,:,3])) # y2 + xVar = xVar + ([sensorText[kk]] * len(resultOutput[ii,:,2])) + # Generate type for comparison + if flagComparison: + typeVar = [legendText[kk]] * len(y1Var) # Type - string + + # Concatenate all the data to form a data frame with x, y, and type + concatenatedX = concatenatedX + xVar + concatenatedY = np.concatenate((concatenatedY,y1Var)) + concatenatedType = concatenatedType + typeVar + + # Reinitialize all the loaded values to empty variable + simResultsFile = [] + resultOutput = [] + numberOfGases = [] + moleFrac = [] + +# Generate data frame +# Inclue data type for comparison +if flagComparison: + df = pd.DataFrame({'x':concatenatedX, + 'y':concatenatedY, + 'dataType':concatenatedType}) +else: + df = pd.DataFrame({'x':concatenatedX, + 'y':concatenatedY}) + +# Plot the figure +sns.set(style="ticks", palette="pastel", color_codes=True) +fig = plt.figure +# Histogram for gas 1 +ax1 = plt.subplot(1,1,1) +# Draw a nested violinplot for easier comparison +if flagComparison: + if scaleLog: + ax1.set_yscale('log') + sns.violinplot(data=df, x="x", y="y", hue="dataType", inner = "box", + split=True, linewidth=1, palette={"Without Noise": 'r', + "With Noise": 'g'}, + scale='width') + ax1.set(xlabel='$y_1$ [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) + plt.legend(loc='upper left') + for kk in range(len(meanMolFrac)): + ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#c0c0c0') + if not legendFlag: + plt.legend([],[], frameon=False) + ax1.locator_params(axis="y", nbins=4) +# Draw violin plot for compaison of different sensors +else: + sns.violinplot(data=df, x="x", y="y", inner = "box", linewidth=1, + scale='width', palette = ['g', 'g', 'b', 'y']) + ax1.set(xlabel='Sensor ID [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) + for kk in range(len(meanMolFrac)): + ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#646464') + ax1.locator_params(axis="y", nbins=4) +# Save the figure +if saveFlag: + # FileName: SensorViolinPlot____> + saveFileSensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-').replace(',','') + saveFileName = "SensorViolinPlot_" + saveFileSensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + +plt.show() \ No newline at end of file diff --git a/plotFunctions/plotOjectiveFunction.py b/plotFunctions/plotOjectiveFunction.py index 8298269..961f8eb 100755 --- a/plotFunctions/plotOjectiveFunction.py +++ b/plotFunctions/plotOjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-17, AK: Multisensor plotting capability # - 2020-11-11, AK: Cosmetic changes and add standard deviation plot # - 2020-11-05, AK: Initial creation # @@ -39,7 +40,7 @@ saveFlag = False # Plot flag to show standard deviation of errors -plotStdError = True +plotStdError = False plotRaw = True plotBH = False plotNoise = True @@ -47,6 +48,9 @@ # Save file extension (png or pdf) saveFileExtension = ".png" +# Plotting colors +colorsForPlot = ["ff499e","d264b6","a480cf","779be7","49b6ff"] + # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -64,10 +68,13 @@ moleFrac = [0.1, 0.9] # Multiplier Error -multiplierError = [1., 1.] +multiplierError = [1., 1., 1.] # Sensor ID -sensorID = np.array([6, 2]) +sensorID = np.array([17,15,16]) + +# Acceptable SNR +signalToNoise = 25*0.1 # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -89,7 +96,7 @@ moleFractionRangeTemp[:,2] = num3/sumNum moleFractionRange = moleFractionRangeTemp[moleFractionRangeTemp[:,0].argsort()] -arraySimResponse = np.zeros([moleFractionRange.shape[0],numberOfGases]) +arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) for ii in range(moleFractionRange.shape[0]): arraySimResponse[ii,:] = simulateSensorArray(sensorID, pressureTotal, temperature, np.array([moleFractionRange[ii,:]])) * multiplierError @@ -106,54 +113,36 @@ # Compute the objective function over all the mole fractions objFunction = np.sum(np.power((arrayTrueResponse - arraySimResponse)/arrayTrueResponse,2),1) -# Compute the first derivative and the elbow point of sensor 1 -firstDerivative = np.zeros([moleFractionRange.shape[0],numberOfGases]) -firstDerivative[:,0] = np.gradient(arraySimResponse[:,0]) -if all(i >= 0. for i in firstDerivative[:,0]): - slopeDir1 = "increasing" -else: - slopeDir1 = "decreasing" -kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,0], - curve="concave", direction=slopeDir1) -elbowPointS1 = list(kneedle.all_elbows) - -# Compute the first derivative and the elbow point of sensor 2 -firstDerivative[:,1] = np.gradient(arraySimResponse[:,1]) -if all(i >= 0. for i in firstDerivative[:,1]): - slopeDir2 = "increasing" -else: - slopeDir2 = "decreasing" -kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,1], - curve="concave", direction=slopeDir2) -elbowPointS2 = list(kneedle.all_elbows) - -# Plot the sensor response for all the conocentrations and highlight the -# working region -# Obtain coordinates to fill working region -if slopeDir1 == "increasing": - xFill1 = [0,elbowPointS1[0]] -else: - xFill1 = [elbowPointS1[0], 1.0] - -if slopeDir2 == "increasing": - xFill2 = [0,elbowPointS2[0]] -else: - xFill2 = [elbowPointS2[0], 1.0] +# Compute the first derivative, elbow point, and the fill regions for all +# sensors +xFill = np.zeros([arraySimResponse.shape[1],2]) +# Loop through all sensors +for kk in range(arraySimResponse.shape[1]): + firstDerivative = np.zeros([arraySimResponse.shape[0],1]) + firstDerivative[:,0] = np.gradient(arraySimResponse[:,kk]) + if all(i >= 0. for i in firstDerivative[:,0]): + slopeDir = "increasing" + else: + slopeDir = "decreasing" + kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,kk], + curve="concave", direction=slopeDir) + elbowPoint = list(kneedle.all_elbows) + + # Plot the sensor response for all the conocentrations and highlight the + # working region + # Obtain coordinates to fill working region + if slopeDir == "increasing": + xFill[kk,:] = [0,elbowPoint[0]] + else: + xFill[kk,:] = [elbowPoint[0], 1.0] fig = plt.figure ax = plt.gca() -# Sensor 1 -ax.plot(moleFractionRange[:,0],arraySimResponse[:,0],color ='#1DBDE6', label = '$s_1$') # Simulated Response -ax.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='#1DBDE6', alpha=0.25) -# Sensor 2 -ax.plot(moleFractionRange[:,0],arraySimResponse[:,1], color = '#F1515E', label = '$s_2$') # Simulated Response -ax.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='#F1515E', alpha=0.25) -# Sensor 3 -if numberOfGases == 3: - ax.axhline(y=arrayTrueResponse[0,2], linewidth=1, linestyle='dotted', - color = 'b', label = '$s_2$') - ax.plot(moleFractionRange[:,0],arraySimResponse[:,2],'g') - +# Loop through all sensors +for kk in range(arraySimResponse.shape[1]): + ax.plot(moleFractionRange[:,0],arraySimResponse[:,kk],color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response + ax.fill_between(xFill[kk,:],1.1*np.max(arraySimResponse), facecolor='#'+colorsForPlot[kk], alpha=0.25) +ax.fill_between([0.,1.],[signalToNoise,signalToNoise], facecolor='#4a5759', alpha=0.25) ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) ax.set(xlabel='$y_1$ [-]', @@ -177,14 +166,11 @@ # sensors and the total (sum) fig = plt.figure ax = plt.gca() -ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,0] - -arraySimResponse[:,0])/arrayTrueResponse[:,0],2), color = '#1DBDE6', label = '$J_1$') # Error first sensor -ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,1] - -arraySimResponse[:,1])/arrayTrueResponse[:,1],2), color = '#F1515E', label = '$J_2$') # Error second sensor -if numberOfGases == 3: - ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,2] - -arraySimResponse[:,2])/arrayTrueResponse[:,2],2),'g', label = '$J_3$') # Error third sensor -ax.plot(moleFractionRange[:,0],objFunction,'k', label = '$\Sigma J_i$') # Error all sensors +for kk in range(arraySimResponse.shape[1]): + ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,kk] + -arraySimResponse[:,kk])/arrayTrueResponse[:,kk],2), + color='#'+colorsForPlot[kk], label = '$J_'+str(kk+1)+'$') +ax.plot(moleFractionRange[:,0],objFunction,color='#'+colorsForPlot[-1], label = '$\Sigma J_i$') # Error all sensors ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) ax.set(xlabel='$y_1$ [-]', @@ -204,97 +190,4 @@ os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) -plt.show() - -# Plot the objective function used to evaluate the concentration for individual -# sensors and the total (sum) -if plotStdError: - if set(sensorID) == set([17,15]): - loadedFileRaw = load("simulationResults/sensitivityAnalysis_17-15_20201109_1033_f7e470f.npz") - loadedFileBH = load("simulationResults/sensitivityAnalysis_17-15_20201109_1616_63f3499.npz") - loadedFileNoise = load("simulationResults/sensitivityAnalysis_17-15_20201110_1458_31e3947.npz") - elif set(sensorID) == set([6,2]): - loadedFileRaw = load("simulationResults/sensitivityAnalysis_6-2_20201109_1208_f7e470f.npz") - loadedFileBH = load("simulationResults/sensitivityAnalysis_6-2_20201110_0936_63f3499.npz") - loadedFileNoise = load("simulationResults/sensitivityAnalysis_6-2_20201110_1458_31e3947.npz") - elif set(sensorID) == set([17,16]): - loadedFileRaw = load("simulationResults/sensitivityAnalysis_17-16_20201109_1416_63f3499.npz") - loadedFileBH = load("simulationResults/sensitivityAnalysis_17-16_20201109_1938_63f3499.npz") - loadedFileNoise = load("simulationResults/sensitivityAnalysis_17-16_20201110_1458_31e3947.npz") - elif set(sensorID) == set([17,6]): - loadedFileRaw = load("simulationResults/sensitivityAnalysis_17-6_20201109_1651_63f3499.npz") - loadedFileBH = load("simulationResults/sensitivityAnalysis_17-6_20201110_1205_63f3499.npz") - loadedFileNoise = load("simulationResults/sensitivityAnalysis_17-6_20201110_1458_31e3947.npz") - - # Parse raw data (no noise, default basin hopping iterations (50)) - if plotRaw: - moleFractionG1_Raw = loadedFileRaw["moleFractionG1"] - meanConcEstimate_Raw = loadedFileRaw["meanConcEstimate"] - stdConcEstimate_Raw = loadedFileRaw["stdConcEstimate"] - - # Parse data with higher number of iterations for BH (250) - if plotBH: - moleFractionG1_BH = loadedFileBH["moleFractionG1"] - meanConcEstimate_BH = loadedFileBH["meanConcEstimate"] - stdConcEstimate_BH = loadedFileBH["stdConcEstimate"] - - # Parse data with noise and default iterations for BH (50) - if plotNoise: - moleFractionG1_Noise = loadedFileNoise["moleFractionG1"] - meanConcEstimate_Noise = loadedFileNoise["meanConcEstimate"] - stdConcEstimate_Noise = loadedFileNoise["stdConcEstimate"] - - os.chdir("plotFunctions") - plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file - os.chdir("..") - - fig = plt.figure - ax1 = plt.subplot(1,2,1) - ax2 = plt.subplot(1,2,2) - - ax1.semilogy(np.linspace(0,1,100), np.linspace(0,1,100), - linewidth = 1, linestyle = '--', color = '#adb5bd') - if plotRaw: - ax1.semilogy(moleFractionG1_Raw,meanConcEstimate_Raw[:,0], marker='o', - linestyle='None', color='#4f772d', label = 'Reference') # Raw - ax2.semilogy(moleFractionG1_Raw,stdConcEstimate_Raw[:,0], marker='o', - linewidth = 1, linestyle = ':', color='#4f772d', label = 'Reference') # Raw - if plotBH: - ax1.semilogy(moleFractionG1_BH,meanConcEstimate_BH[:,0], marker='o', - linestyle='None', color='#90a955', label = 'More Iterations') # Basin hopping - ax2.semilogy(moleFractionG1_BH,stdConcEstimate_BH[:,0], marker='o', - linewidth = 1, linestyle = ':', color='#90a955', label = 'More Iterations') # Basin hopping - if plotNoise: - ax1.semilogy(moleFractionG1_Noise,meanConcEstimate_Noise[:,0], marker='o', - linestyle='None', color='#90a955', label = 'With Noise') # Noise - ax2.semilogy(moleFractionG1_Noise,stdConcEstimate_Noise[:,0], marker='o', - linewidth = 1, linestyle = ':', color='#90a955', label = 'With Noise') # Noise - - ax2.fill_between(xFill1,1.1*np.max(arraySimResponse), facecolor='#1DBDE6', alpha=0.25) - ax2.fill_between(xFill2,1.1*np.max(arraySimResponse), facecolor='#F1515E', alpha=0.25) - - ax1.locator_params(axis="x", nbins=4) - ax1.locator_params(axis="y") - ax1.set(xlabel='True $y_1$ [-]', - ylabel='Estimated $y_1$ [-]', - xlim = [0,1.], ylim = [1e-4, 1.]) - - ax2.locator_params(axis="x", nbins=4) - ax2.locator_params(axis="y") - ax2.set(xlabel='$y_1$ [-]', - ylabel='$\sigma ({y_1})$ [-]', - xlim = [0,1.], ylim = [1e-10, 1.]) - ax2.legend() - - # Save the figure - if saveFlag: - # FileName: SensorObjFunc____> - sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') - saveFileName = "SensorSenAnalStd_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) - - plt.show() \ No newline at end of file +plt.show() \ No newline at end of file From 9da4094158792b5fa792b8000c140572011bc106 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 17 Nov 2020 19:49:49 +0000 Subject: [PATCH 33/99] Fix typo in file name --- .../{plotOjectiveFunction.py => plotObjectiveFunction.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plotFunctions/{plotOjectiveFunction.py => plotObjectiveFunction.py} (100%) diff --git a/plotFunctions/plotOjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py similarity index 100% rename from plotFunctions/plotOjectiveFunction.py rename to plotFunctions/plotObjectiveFunction.py From 4ca6f7bbb355cac49a29642075147e1a7aea1bcf Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 18 Nov 2020 20:11:58 +0000 Subject: [PATCH 34/99] Changes to data reconciliation and add new plots --- plotFunctions/plotConcentrationViolinPlots.py | 120 ++++++++++++------ 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py index 87cc4fc..237b5eb 100755 --- a/plotFunctions/plotConcentrationViolinPlots.py +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -12,6 +12,7 @@ # Plots to visualize different sensor responses # # Last modified: +# - 2020-11-18, AK: Changes to data reconciliation and new plots # - 2020-11-13, AK: Initial creation # # Input arguments: @@ -44,22 +45,26 @@ saveFlag = False # Flag for comparison -flagComparison = True +flagComparison = False + +# Color combo +colorTemp = ["eac435","345995","03cea4","fb4d3d","ca1551"] +colorForPlot = ["#" + counter for counter in colorTemp] # Mole fraction ID moleFracID = 3 meanMolFrac = [0.001,0.01,0.1,0.25,0.5,0.75,0.90] # Y limits for the plot -Y_LIMITS = [1e-4,1.] +Y_LIMITS = [None,None] scaleLog = True # Legend First legendText = ["Without Noise", "With Noise"] -legendFlag = True +legendFlag = False # Sensor ID -sensorText = ["6/2", "17/16", "17/16", "17/6"] +sensorText = ["17/15/16", "17/15", "17/15/2", "17/6"] # Initialize x, y, and type for the plotting concatenatedX = [] @@ -67,8 +72,9 @@ concatenatedType = [] # File to be loaded for the left of violin plot -loadFileName = ["sensitivityAnalysis_6-2_20201113_1450_c9b2a41.npz", - "sensitivityAnalysis_6-2_20201116_1806_c9b2a41.npz"] +loadFileName = ["sensitivityAnalysis_17-15-16_20201117_1135_c9b2a41.npz", + "sensitivityAnalysis_17-15_20201113_1450_c9b2a41.npz", + "sensitivityAnalysis_17-15-2_20201117_1135_c9b2a41.npz"] saveFileSensorText = [17, 16] if flagComparison and len(loadFileName) != 2: @@ -87,21 +93,20 @@ resultOutput = load(simResultsFile)["arrayConcentration"] numberOfGases = load(simResultsFile)["numberOfGases"] moleFrac = load(simResultsFile)["moleFractionG1"] - + # For the cases where there are two gases if numberOfGases == 2: + # Loop through all the molefractions for ii in range(resultOutput.shape[0]): - # Loop through all the molefractions for comparison - if flagComparison: - y1Var = np.concatenate((y1Var,resultOutput[ii,:,2])) # y1 - y2Var = np.concatenate((y2Var,resultOutput[ii,:,3])) # y2 - xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,2])) # x (true mole fraction) - # Parse the data only for the desired mole fraction - else: - if ii == moleFracID: - y1Var = np.concatenate((y1Var,resultOutput[ii,:,2])) # y1 - y2Var = np.concatenate((y2Var,resultOutput[ii,:,3])) # y2 - xVar = xVar + ([sensorText[kk]] * len(resultOutput[ii,:,2])) + if resultOutput.shape[2] == 4: + counterInd = 0 + elif resultOutput.shape[2] == 5: + counterInd = 1 + y1Var = np.concatenate((y1Var,resultOutput[ii,:,counterInd+2])) # y1 + y2Var = np.concatenate((y2Var,resultOutput[ii,:,counterInd+3])) # y2 + xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) + if not flagComparison: + typeVar = typeVar+[sensorText[kk]] * len(resultOutput[ii,:,counterInd+2]) # Generate type for comparison if flagComparison: typeVar = [legendText[kk]] * len(y1Var) # Type - string @@ -117,44 +122,51 @@ numberOfGases = [] moleFrac = [] -# Generate data frame -# Inclue data type for comparison -if flagComparison: - df = pd.DataFrame({'x':concatenatedX, - 'y':concatenatedY, - 'dataType':concatenatedType}) -else: - df = pd.DataFrame({'x':concatenatedX, - 'y':concatenatedY}) - +# Generate panda data frame +# x = molefraction (true) +# y = molefraction (estimated) +# dataType = either sensor id/comparison type +df = pd.DataFrame({'x':concatenatedX, + 'y':concatenatedY, + 'dataType':concatenatedType}) + +# Compute the mean, standard deviation, and the quantiles for each +meanData = df.groupby(['dataType','x'], as_index=False).mean() +stdData = df.groupby(['dataType','x'], as_index=False).std() +maxData = df.groupby(['dataType','x'], as_index=False).max() +minData = df.groupby(['dataType','x'], as_index=False).min() +rangeData = (df.groupby(['dataType','x'], as_index=False).agg(np.ptp)) +Q1Data = df.groupby(['dataType','x'], as_index=False).quantile(0.25) +Q3Data = df.groupby(['dataType','x'], as_index=False).quantile(0.75) +# Coefficient of variation +cvData = stdData.copy() +cvData['y'] = stdData['y']/meanData['y'] + # Plot the figure sns.set(style="ticks", palette="pastel", color_codes=True) fig = plt.figure -# Histogram for gas 1 ax1 = plt.subplot(1,1,1) # Draw a nested violinplot for easier comparison if flagComparison: if scaleLog: ax1.set_yscale('log') sns.violinplot(data=df, x="x", y="y", hue="dataType", inner = "box", - split=True, linewidth=1, palette={"Without Noise": 'r', - "With Noise": 'g'}, + split=True, linewidth=1, palette={legendText[0]: colorForPlot[0], + legendText[1]: colorForPlot[1]}, scale='width') ax1.set(xlabel='$y_1$ [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) plt.legend(loc='upper left') - for kk in range(len(meanMolFrac)): - ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#c0c0c0') if not legendFlag: plt.legend([],[], frameon=False) - ax1.locator_params(axis="y", nbins=4) # Draw violin plot for compaison of different sensors else: - sns.violinplot(data=df, x="x", y="y", inner = "box", linewidth=1, - scale='width', palette = ['g', 'g', 'b', 'y']) + sns.violinplot(data=df[df.x == str(meanMolFrac[moleFracID])], + x="dataType", y="y", inner = "box", linewidth=1, + scale='width', palette = colorForPlot[0:len(loadFileName)]) ax1.set(xlabel='Sensor ID [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) - for kk in range(len(meanMolFrac)): - ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#646464') - ax1.locator_params(axis="y", nbins=4) +for kk in range(len(meanMolFrac)): + ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#c0c0c0') +ax1.locator_params(axis="y", nbins=4) # Save the figure if saveFlag: # FileName: SensorViolinPlot____> @@ -165,5 +177,35 @@ if not os.path.exists(os.path.join('..','simulationFigures')): os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) +plt.show() +# Plot quantities of interest +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file +fig = plt.figure +# Standard deviation +ax1 = plt.subplot(1,2,1) +stdData["x"] = pd.to_numeric(stdData["x"], downcast="float") +sns.lineplot(data=stdData, x='x', y='y', hue='dataType', style='dataType', + dashes = False, markers = ['o']*len(loadFileName), + palette = colorForPlot[0:len(loadFileName)]) +ax1.set(xlabel='$y_1$ [-]', + ylabel='$\sigma (\hat{y}_i)$ [-]', + xlim = [0.,1.], ylim = [0.,None]) +ax1.locator_params(axis="x", nbins=4) +ax1.locator_params(axis="y", nbins=4) +plt.legend(loc='best') + +# Range +ax2 = plt.subplot(1,2,2) +cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") +sns.lineplot(data=cvData, x='x', y='y', hue='dataType', style='dataType', + dashes = False, markers = ['o']*len(loadFileName), + palette = colorForPlot[0:len(loadFileName)]) +ax2.set(xlabel='$y_1$ [-]', + ylabel='$CV (\hat{y}_i)$ [-]', + xlim = [0.,1.], ylim = [1e-5,1.]) +ax2.locator_params(axis="x", nbins=4) +ax2.set_yscale('log') +if not legendFlag: + plt.legend([],[], frameon=False) plt.show() \ No newline at end of file From b445f9d07d7f65f32fa74ca340326eb47ead4a58 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 19 Nov 2020 15:05:57 +0000 Subject: [PATCH 35/99] Modify for three gas system --- sensitivityAnalysis.py | 63 ++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 844f050..ecb8c0d 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,6 +13,7 @@ # concentration estimate # # Last modified: +# - 2020-11-19, AK: Modify for three gas system # - 2020-11-12, AK: Save arrayConcentration in output # - 2020-11-12, AK: Bug fix for multipler error # - 2020-11-11, AK: Add multipler nosie @@ -58,7 +59,7 @@ numberOfAdsorbents = 30 # Number of gases -numberOfGases = 2 +numberOfGases = 3 # Sensor combination # Check if argument provided (from terminal) @@ -69,7 +70,7 @@ # Use default values else: print("\nSensor configuration not not provided. Default used!") - sensorID = [6, 2] + sensorID = [6, 2, 1] # Measurement noise (Guassian noise) meanError = 0. # [g/kg] @@ -79,14 +80,21 @@ multiplierError = [1., 1.] # Custom input mole fraction for gas 1 -meanMoleFracG1 = [0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90] -# meanMoleFracG1 = [0.90] - +meanMoleFracG1 = np.array([0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90]) diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. -numberOfIterations = 100 -# Custom input mole fraction for gas 2 (for 3 gas system) -meanMoleFracG2 = 0.20 +# Number of iterations for the estimator +numberOfIterations = 1 + +# Custom input mole fraction for gas 3 (for 3 gas system) +# Mole fraction for the third gas is fixed. The mole fraction of the other +# two gases is then constrained as y1 + y2 = 1 - y3 +meanMoleFracG3 = 0.25 + +# Remove elements from array that do no meet the mole fraction sum +# constraint for 3 gas system +if numberOfGases == 3: + meanMoleFracG1 = meanMoleFracG1[meanMoleFracG1 <= 1-meanMoleFracG3] # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) @@ -96,6 +104,7 @@ arrayConcentration = np.zeros([len(meanMoleFracG1),numberOfIterations, numberOfGases+len(sensorID)]) +# Loop through all mole fractions for ii in range(len(meanMoleFracG1)): # Generate a uniform distribution of mole fractions if numberOfGases == 2: @@ -106,10 +115,11 @@ inputMoleFrac[:,1] = 1. - inputMoleFrac[:,0] elif numberOfGases == 3: inputMoleFrac = np.zeros([numberOfIterations,3]) - inputMoleFrac[:,0] = meanMoleFracG1 - inputMoleFrac[:,1] = meanMoleFracG2 - inputMoleFrac[:,2] = 1 - meanMoleFracG1 - meanMoleFracG2 - + inputMoleFrac[:,0] = np.random.uniform(meanMoleFracG1[ii]-diffMoleFracG1, + meanMoleFracG1[ii]+diffMoleFracG1, + numberOfIterations) # y1 is variable + inputMoleFrac[:,2] = meanMoleFracG3 # y3 is fixed to a constant + inputMoleFrac[:,1] = 1. - inputMoleFrac[:,2] - inputMoleFrac[:,0] # y2 from mass balance # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials @@ -121,27 +131,7 @@ for ii in tqdm(range(inputMoleFrac.shape[0]))) # Convert the output list to a matrix - arrayConcentration[ii,:,:] = np.array(arrayConcentrationTemp) - - # Get the concentraiton mean and standard deviation for multiple concentrations - # For single concentration, get the true sensor response - # Compute the mean and the standard deviation of the concentration estimates - if numberOfGases == 2 and len(sensorID) == 2: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[ii,:,2]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[ii,:,3]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[ii,:,2]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[ii,:,3]) - elif numberOfGases == 2 and len(sensorID) == 3: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[ii,:,3]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[ii,:,4]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[ii,:,3]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[ii,:,4]) - elif numberOfGases == 2 and len(sensorID) == 4: - meanConcEstimate[ii,0] = np.mean(arrayConcentration[ii,:,4]) - meanConcEstimate[ii,1] = np.mean(arrayConcentration[ii,:,5]) - stdConcEstimate[ii,0] = np.std(arrayConcentration[ii,:,4]) - stdConcEstimate[ii,1] = np.std(arrayConcentration[ii,:,5]) - + arrayConcentration[ii,:,:] = np.array(arrayConcentrationTemp) # Check if simulationResults directory exists or not. If not, create the folder if not os.path.exists('simulationResults'): @@ -154,16 +144,11 @@ saveFileName = filePrefix + "_" + sensorText + "_" + simulationDT + "_" + gitCommitID; savePath = os.path.join('simulationResults',saveFileName) -# Save mean and std. for multiple concentraiton and array concentration -# array for single concentration - -# Save the mean, standard deviation, and molefraction array +# Save the results as an array savez (savePath, numberOfGases = numberOfGases, numberOfIterations = numberOfIterations, moleFractionG1 = meanMoleFracG1, multiplierError = multiplierError, meanError = meanError, stdError = stdError, - meanConcEstimate = meanConcEstimate, - stdConcEstimate = stdConcEstimate, arrayConcentration = arrayConcentration) \ No newline at end of file From fc5b32711d0b5c7421633f77868cbd4d53e3e5fd Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 19 Nov 2020 15:07:44 +0000 Subject: [PATCH 36/99] Multigas plotting capability --- plotFunctions/plotConcentrationViolinPlots.py | 30 ++--- plotFunctions/plotObjectiveFunction.py | 107 ++++++++++-------- 2 files changed, 77 insertions(+), 60 deletions(-) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py index 237b5eb..62b0c41 100755 --- a/plotFunctions/plotConcentrationViolinPlots.py +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -52,7 +52,7 @@ colorForPlot = ["#" + counter for counter in colorTemp] # Mole fraction ID -moleFracID = 3 +moleFracID = 0 meanMolFrac = [0.001,0.01,0.1,0.25,0.5,0.75,0.90] # Y limits for the plot @@ -64,7 +64,7 @@ legendFlag = False # Sensor ID -sensorText = ["17/15/16", "17/15", "17/15/2", "17/6"] +sensorText = ["1/1", "1/5", "1/10", "17/16"] # Initialize x, y, and type for the plotting concatenatedX = [] @@ -72,9 +72,9 @@ concatenatedType = [] # File to be loaded for the left of violin plot -loadFileName = ["sensitivityAnalysis_17-15-16_20201117_1135_c9b2a41.npz", - "sensitivityAnalysis_17-15_20201113_1450_c9b2a41.npz", - "sensitivityAnalysis_17-15-2_20201117_1135_c9b2a41.npz"] +loadFileName = ["sensitivityAnalysis_6-2_20201113_1450_c9b2a41.npz", + "sensitivityAnalysis_6-2_20201116_1104_c9b2a41.npz", + "sensitivityAnalysis_6-2_20201116_1806_c9b2a41.npz"] saveFileSensorText = [17, 16] if flagComparison and len(loadFileName) != 2: @@ -131,10 +131,10 @@ 'dataType':concatenatedType}) # Compute the mean, standard deviation, and the quantiles for each -meanData = df.groupby(['dataType','x'], as_index=False).mean() -stdData = df.groupby(['dataType','x'], as_index=False).std() -maxData = df.groupby(['dataType','x'], as_index=False).max() -minData = df.groupby(['dataType','x'], as_index=False).min() +meanData = df.groupby(['dataType','x'], as_index=False, sort=False).mean() +stdData = df.groupby(['dataType','x'], as_index=False, sort=False).std() +maxData = df.groupby(['dataType','x'], as_index=False, sort=False).max() +minData = df.groupby(['dataType','x'], as_index=False, sort=False).min() rangeData = (df.groupby(['dataType','x'], as_index=False).agg(np.ptp)) Q1Data = df.groupby(['dataType','x'], as_index=False).quantile(0.25) Q3Data = df.groupby(['dataType','x'], as_index=False).quantile(0.75) @@ -164,8 +164,12 @@ x="dataType", y="y", inner = "box", linewidth=1, scale='width', palette = colorForPlot[0:len(loadFileName)]) ax1.set(xlabel='Sensor ID [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) -for kk in range(len(meanMolFrac)): - ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#c0c0c0') +if flagComparison: + for kk in range(len(meanMolFrac)): + ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#c0c0c0') +else: + ax1.axhline(meanMolFrac[moleFracID], linestyle=':', linewidth=1, color = '#c0c0c0') + ax1.locator_params(axis="y", nbins=4) # Save the figure if saveFlag: @@ -190,9 +194,9 @@ palette = colorForPlot[0:len(loadFileName)]) ax1.set(xlabel='$y_1$ [-]', ylabel='$\sigma (\hat{y}_i)$ [-]', - xlim = [0.,1.], ylim = [0.,None]) + xlim = [0.,1.], ylim = [1e-6,1.]) +ax1.set_yscale('log') ax1.locator_params(axis="x", nbins=4) -ax1.locator_params(axis="y", nbins=4) plt.legend(loc='best') # Range diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index 961f8eb..f342cdf 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-19, AK: Multigas plotting capability # - 2020-11-17, AK: Multisensor plotting capability # - 2020-11-11, AK: Cosmetic changes and add standard deviation plot # - 2020-11-05, AK: Initial creation @@ -30,7 +31,9 @@ from generateTrueSensorResponse import generateTrueSensorResponse from simulateSensorArray import simulateSensorArray import os +import matplotlib as mpl import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file import auxiliaryFunctions @@ -51,6 +54,9 @@ # Plotting colors colorsForPlot = ["ff499e","d264b6","a480cf","779be7","49b6ff"] +# Number of molefractions +numMolFrac= 1001 + # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -62,7 +68,10 @@ numberOfAdsorbents = 20 # Number of Gases -numberOfGases = 2 +numberOfGases = 3 + +# Third gas mole fraction +thirdGasMoleFrac = 0.25 # Mole Fraction of interest moleFrac = [0.1, 0.9] @@ -71,7 +80,7 @@ multiplierError = [1., 1., 1.] # Sensor ID -sensorID = np.array([17,15,16]) +sensorID = np.array([5,6,8]) # Acceptable SNR signalToNoise = 25*0.1 @@ -84,17 +93,12 @@ # Simulate the sensor response for all possible concentrations if numberOfGases == 2: - moleFractionRange = np.array([np.linspace(0,1,10001), 1 - np.linspace(0,1,10001)]).T + moleFractionRange = np.array([np.linspace(0,1,numMolFrac), 1 - np.linspace(0,1,numMolFrac)]).T elif numberOfGases == 3: - moleFractionRangeTemp = np.zeros([10001,3]) - num1 = np.random.uniform(0.0,1.0,10001) - num2 = np.random.uniform(0.0,1.0,10001) - num3 = np.random.uniform(0.0,1.0,10001) - sumNum = num1 + num2 + num3 - moleFractionRangeTemp[:,0] = num1/sumNum - moleFractionRangeTemp[:,1] = num2/sumNum - moleFractionRangeTemp[:,2] = num3/sumNum - moleFractionRange = moleFractionRangeTemp[moleFractionRangeTemp[:,0].argsort()] + remainingMoleFrac = 1. - thirdGasMoleFrac + moleFractionRange = np.array([np.linspace(0,remainingMoleFrac,numMolFrac), + remainingMoleFrac - np.linspace(0,remainingMoleFrac,numMolFrac), + np.tile(thirdGasMoleFrac,numMolFrac)]).T arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) for ii in range(moleFractionRange.shape[0]): @@ -114,7 +118,8 @@ objFunction = np.sum(np.power((arrayTrueResponse - arraySimResponse)/arrayTrueResponse,2),1) # Compute the first derivative, elbow point, and the fill regions for all -# sensors +# sensors for 2 gases + xFill = np.zeros([arraySimResponse.shape[1],2]) # Loop through all sensors for kk in range(arraySimResponse.shape[1]): @@ -125,16 +130,19 @@ else: slopeDir = "decreasing" kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,kk], - curve="concave", direction=slopeDir) + curve="concave", direction=slopeDir) elbowPoint = list(kneedle.all_elbows) - + # Plot the sensor response for all the conocentrations and highlight the # working region # Obtain coordinates to fill working region if slopeDir == "increasing": xFill[kk,:] = [0,elbowPoint[0]] else: - xFill[kk,:] = [elbowPoint[0], 1.0] + if numberOfGases == 2: + xFill[kk,:] = [elbowPoint[0], 1.0] + elif numberOfGases == 3: + xFill[kk,:] = [elbowPoint[0], 1.-thirdGasMoleFrac] fig = plt.figure ax = plt.gca() @@ -142,12 +150,17 @@ for kk in range(arraySimResponse.shape[1]): ax.plot(moleFractionRange[:,0],arraySimResponse[:,kk],color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response ax.fill_between(xFill[kk,:],1.1*np.max(arraySimResponse), facecolor='#'+colorsForPlot[kk], alpha=0.25) -ax.fill_between([0.,1.],[signalToNoise,signalToNoise], facecolor='#4a5759', alpha=0.25) + if numberOfGases == 2: + ax.fill_between([0.,1.],signalToNoise, facecolor='#4a5759', alpha=0.25) + elif numberOfGases == 3: + mpl.rcParams['hatch.linewidth'] = 0.1 + ax.fill_between([0.,1.-thirdGasMoleFrac],signalToNoise, facecolor='#4a5759', alpha=0.25) + ax.fill_between([1.-thirdGasMoleFrac,1.],1.1*np.max(arraySimResponse), facecolor='#555b6e', alpha=0.25, hatch = 'x') +ax.set(xlabel='$y_1$ [-]', + ylabel='$m_i$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) -ax.set(xlabel='$y_1$ [-]', - ylabel='$m_i$ [g kg$^{-1}$]', - xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) ax.legend() # Save the figure @@ -164,30 +177,30 @@ # Plot the objective function used to evaluate the concentration for individual # sensors and the total (sum) -fig = plt.figure -ax = plt.gca() -for kk in range(arraySimResponse.shape[1]): - ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,kk] - -arraySimResponse[:,kk])/arrayTrueResponse[:,kk],2), - color='#'+colorsForPlot[kk], label = '$J_'+str(kk+1)+'$') -ax.plot(moleFractionRange[:,0],objFunction,color='#'+colorsForPlot[-1], label = '$\Sigma J_i$') # Error all sensors -ax.locator_params(axis="x", nbins=4) -ax.locator_params(axis="y", nbins=4) -ax.set(xlabel='$y_1$ [-]', - ylabel='$J$ [-]', - xlim = [0,1.], ylim = [0, None]) -ax.legend() - -# Save the figure -if saveFlag: - # FileName: SensorObjFunc____ - sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') - moleFrac = str(moleFrac).replace('[','').replace(']','').replace(' ','').replace(',','-').replace('.','') - saveFileName = "SensorObjFunc_" + sensorText + "_" + moleFrac + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('simulationFigures',saveFileName) - # Check if simulationFigures directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) - -plt.show() \ No newline at end of file +if numberOfGases == 2: + fig = plt.figure + ax = plt.gca() + for kk in range(arraySimResponse.shape[1]): + ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,kk] + -arraySimResponse[:,kk])/arrayTrueResponse[:,kk],2), + color='#'+colorsForPlot[kk], label = '$J_'+str(kk+1)+'$') + ax.plot(moleFractionRange[:,0],objFunction,color='#'+colorsForPlot[-1], label = '$\Sigma J_i$') # Error all sensors + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + ax.set(xlabel='$y_1$ [-]', + ylabel='$J$ [-]', + xlim = [0,1.], ylim = [0, None]) + ax.legend() + + # Save the figure + if saveFlag: + # FileName: SensorObjFunc____ + sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') + moleFrac = str(moleFrac).replace('[','').replace(']','').replace(' ','').replace(',','-').replace('.','') + saveFileName = "SensorObjFunc_" + sensorText + "_" + moleFrac + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if simulationFigures directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + plt.show() \ No newline at end of file From 4e0cb3150dd917132580de96bf6463f4f30d5bf5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 19 Nov 2020 18:13:24 +0000 Subject: [PATCH 37/99] Add 3 gas knee calculator --- plotFunctions/plotObjectiveFunction.py | 44 ++++++++++++++++++++------ sensitivityAnalysis.py | 1 + 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index f342cdf..163cef4 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-19, AK: Add 3 gas knee calculator # - 2020-11-19, AK: Multigas plotting capability # - 2020-11-17, AK: Multisensor plotting capability # - 2020-11-11, AK: Cosmetic changes and add standard deviation plot @@ -71,7 +72,7 @@ numberOfGases = 3 # Third gas mole fraction -thirdGasMoleFrac = 0.25 +thirdGasMoleFrac = 0.5 # Mole Fraction of interest moleFrac = [0.1, 0.9] @@ -80,7 +81,7 @@ multiplierError = [1., 1., 1.] # Sensor ID -sensorID = np.array([5,6,8]) +sensorID = np.array([18,6,8]) # Acceptable SNR signalToNoise = 25*0.1 @@ -125,24 +126,47 @@ for kk in range(arraySimResponse.shape[1]): firstDerivative = np.zeros([arraySimResponse.shape[0],1]) firstDerivative[:,0] = np.gradient(arraySimResponse[:,kk]) + secondDerivative = np.zeros([firstDerivative.shape[0],1]) + secondDerivative[:,0] = np.gradient(firstDerivative[:,0]) + # Get the sign of the first derivative for increasing/decreasing if all(i >= 0. for i in firstDerivative[:,0]): slopeDir = "increasing" - else: + elif all(i < 0. for i in firstDerivative[:,0]): slopeDir = "decreasing" + else: + print("Dangerous! I should not be here!!!") + # Get the sign of the second derivative for concavity/convexity + if all(i >= 0. for i in secondDerivative[:,0]): + secondDerDir = "convex" + elif all(i < 0. for i in secondDerivative[:,0]): + secondDerDir = "concave" + else: + print("Dangerous! I should not be here!!!") + + kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,kk], - curve="concave", direction=slopeDir) + curve=secondDerDir, direction=slopeDir) elbowPoint = list(kneedle.all_elbows) # Plot the sensor response for all the conocentrations and highlight the # working region # Obtain coordinates to fill working region - if slopeDir == "increasing": - xFill[kk,:] = [0,elbowPoint[0]] + if secondDerDir == "concave": + if slopeDir == "increasing": + xFill[kk,:] = [0,elbowPoint[0]] + else: + if numberOfGases == 2: + xFill[kk,:] = [elbowPoint[0], 1.0] + elif numberOfGases == 3: + xFill[kk,:] = [elbowPoint[0], 1.-thirdGasMoleFrac] + elif secondDerDir == "convex": + if slopeDir == "increasing": + if numberOfGases == 3: + xFill[kk,:] = [elbowPoint[0],1.-thirdGasMoleFrac] + else: + xFill[kk,:] = [0,elbowPoint[0]] else: - if numberOfGases == 2: - xFill[kk,:] = [elbowPoint[0], 1.0] - elif numberOfGases == 3: - xFill[kk,:] = [elbowPoint[0], 1.-thirdGasMoleFrac] + print("Dangerous! I should not be here!!!") fig = plt.figure ax = plt.gca() diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index ecb8c0d..337f527 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -148,6 +148,7 @@ savez (savePath, numberOfGases = numberOfGases, numberOfIterations = numberOfIterations, moleFractionG1 = meanMoleFracG1, + meanMoleFracG3 = meanMoleFracG3, multiplierError = multiplierError, meanError = meanError, stdError = stdError, From c7ae4cf807e5e3e7b1717aaacee5c794010ec405 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 20 Nov 2020 19:20:24 +0000 Subject: [PATCH 38/99] Introduce 3 gas system visualization --- plotFunctions/plotConcentrationViolinPlots.py | 75 ++++-- plotFunctions/plotObjectiveFunction.py | 215 +++++++++++------- 2 files changed, 179 insertions(+), 111 deletions(-) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py index 62b0c41..64d697d 100755 --- a/plotFunctions/plotConcentrationViolinPlots.py +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -12,6 +12,7 @@ # Plots to visualize different sensor responses # # Last modified: +# - 2020-11-20, AK: Introduce 3 gas capability # - 2020-11-18, AK: Changes to data reconciliation and new plots # - 2020-11-13, AK: Initial creation # @@ -68,13 +69,15 @@ # Initialize x, y, and type for the plotting concatenatedX = [] -concatenatedY = [] +concatenatedY1 = [] +concatenatedY2 = [] +concatenatedY3 = [] concatenatedType = [] # File to be loaded for the left of violin plot -loadFileName = ["sensitivityAnalysis_6-2_20201113_1450_c9b2a41.npz", - "sensitivityAnalysis_6-2_20201116_1104_c9b2a41.npz", - "sensitivityAnalysis_6-2_20201116_1806_c9b2a41.npz"] +loadFileName = ["sensitivityAnalysis_0-6-8_20201119_1516_fc5b327.npz", + "sensitivityAnalysis_1-6-8_20201119_1516_fc5b327.npz", + "sensitivityAnalysis_2-6-8_20201119_1516_fc5b327.npz"] saveFileSensorText = [17, 16] if flagComparison and len(loadFileName) != 2: @@ -87,6 +90,7 @@ xVar = [] y1Var = [] y2Var = [] + y3Var = [] typeVar = [] simResultsFile = os.path.join('..','simulationResults',loadFileName[kk]); @@ -94,42 +98,60 @@ numberOfGases = load(simResultsFile)["numberOfGases"] moleFrac = load(simResultsFile)["moleFractionG1"] - # For the cases where there are two gases - if numberOfGases == 2: - # Loop through all the molefractions - for ii in range(resultOutput.shape[0]): + # Loop through all the molefractions + for ii in range(resultOutput.shape[0]): + # For the cases where there are two gases + if numberOfGases == 2: if resultOutput.shape[2] == 4: counterInd = 0 elif resultOutput.shape[2] == 5: counterInd = 1 - y1Var = np.concatenate((y1Var,resultOutput[ii,:,counterInd+2])) # y1 - y2Var = np.concatenate((y2Var,resultOutput[ii,:,counterInd+3])) # y2 - xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) - if not flagComparison: - typeVar = typeVar+[sensorText[kk]] * len(resultOutput[ii,:,counterInd+2]) + # For the cases where there are two gases + elif numberOfGases == 3: + if resultOutput.shape[2] == 6: + counterInd = 1 + + y1Var = np.concatenate((y1Var,resultOutput[ii,:,counterInd+2])) # y1 + y2Var = np.concatenate((y2Var,resultOutput[ii,:,counterInd+3])) # y2 + if numberOfGases == 3: + y3Var = np.concatenate((y3Var,resultOutput[ii,:,counterInd+4])) # y3 + xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) + if not flagComparison: + typeVar = typeVar+[sensorText[kk]] * len(resultOutput[ii,:,counterInd+2]) # Generate type for comparison if flagComparison: typeVar = [legendText[kk]] * len(y1Var) # Type - string # Concatenate all the data to form a data frame with x, y, and type concatenatedX = concatenatedX + xVar - concatenatedY = np.concatenate((concatenatedY,y1Var)) + concatenatedY1 = np.concatenate((concatenatedY1,y1Var)) + concatenatedY2 = np.concatenate((concatenatedY2,y2Var)) + if numberOfGases == 3: + concatenatedY3 = np.concatenate((concatenatedY3,y3Var)) concatenatedType = concatenatedType + typeVar # Reinitialize all the loaded values to empty variable simResultsFile = [] resultOutput = [] - numberOfGases = [] moleFrac = [] # Generate panda data frame # x = molefraction (true) # y = molefraction (estimated) # dataType = either sensor id/comparison type -df = pd.DataFrame({'x':concatenatedX, - 'y':concatenatedY, - 'dataType':concatenatedType}) +if numberOfGases == 2: + df = pd.DataFrame({'x':concatenatedX, + 'y1':concatenatedY1, + 'y2':concatenatedY2, + 'dataType':concatenatedType}) +elif numberOfGases == 3: + df = pd.DataFrame({'x':concatenatedX, + 'y1':concatenatedY1, + 'y2':concatenatedY2, + 'y3':concatenatedY3, + 'dataType':concatenatedType}) + # Compute the mean, standard deviation, and the quantiles for each meanData = df.groupby(['dataType','x'], as_index=False, sort=False).mean() stdData = df.groupby(['dataType','x'], as_index=False, sort=False).std() @@ -140,7 +162,10 @@ Q3Data = df.groupby(['dataType','x'], as_index=False).quantile(0.75) # Coefficient of variation cvData = stdData.copy() -cvData['y'] = stdData['y']/meanData['y'] +cvData['y1'] = stdData['y1']/meanData['y1'] +cvData['y2'] = stdData['y2']/meanData['y2'] +if numberOfGases == 3: + cvData['y3'] = stdData['y3']/meanData['y3'] # Plot the figure sns.set(style="ticks", palette="pastel", color_codes=True) @@ -150,7 +175,7 @@ if flagComparison: if scaleLog: ax1.set_yscale('log') - sns.violinplot(data=df, x="x", y="y", hue="dataType", inner = "box", + sns.violinplot(data=df, x="x", y="y1", hue="dataType", inner = "box", split=True, linewidth=1, palette={legendText[0]: colorForPlot[0], legendText[1]: colorForPlot[1]}, scale='width') @@ -161,7 +186,7 @@ # Draw violin plot for compaison of different sensors else: sns.violinplot(data=df[df.x == str(meanMolFrac[moleFracID])], - x="dataType", y="y", inner = "box", linewidth=1, + x="dataType", y="y1", inner = "box", linewidth=1, scale='width', palette = colorForPlot[0:len(loadFileName)]) ax1.set(xlabel='Sensor ID [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) if flagComparison: @@ -189,11 +214,11 @@ # Standard deviation ax1 = plt.subplot(1,2,1) stdData["x"] = pd.to_numeric(stdData["x"], downcast="float") -sns.lineplot(data=stdData, x='x', y='y', hue='dataType', style='dataType', +sns.lineplot(data=stdData, x='x', y='y1', hue='dataType', style='dataType', dashes = False, markers = ['o']*len(loadFileName), palette = colorForPlot[0:len(loadFileName)]) ax1.set(xlabel='$y_1$ [-]', - ylabel='$\sigma (\hat{y}_i)$ [-]', + ylabel='$\sigma (\hat{y}_1)$ [-]', xlim = [0.,1.], ylim = [1e-6,1.]) ax1.set_yscale('log') ax1.locator_params(axis="x", nbins=4) @@ -202,11 +227,11 @@ # Range ax2 = plt.subplot(1,2,2) cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") -sns.lineplot(data=cvData, x='x', y='y', hue='dataType', style='dataType', +sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', dashes = False, markers = ['o']*len(loadFileName), palette = colorForPlot[0:len(loadFileName)]) ax2.set(xlabel='$y_1$ [-]', - ylabel='$CV (\hat{y}_i)$ [-]', + ylabel='$CV (\hat{y}_1)$ [-]', xlim = [0.,1.], ylim = [1e-5,1.]) ax2.locator_params(axis="x", nbins=4) ax2.set_yscale('log') diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index 163cef4..a24f4ba 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-20, AK: Introduce ternary plots # - 2020-11-19, AK: Add 3 gas knee calculator # - 2020-11-19, AK: Multigas plotting capability # - 2020-11-17, AK: Multisensor plotting capability @@ -32,10 +33,12 @@ from generateTrueSensorResponse import generateTrueSensorResponse from simulateSensorArray import simulateSensorArray import os +import pandas as pd +import plotly.express as px import matplotlib as mpl import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D -plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file import auxiliaryFunctions os.chdir("..") @@ -43,12 +46,6 @@ # Save flag for figure saveFlag = False -# Plot flag to show standard deviation of errors -plotStdError = False -plotRaw = True -plotBH = False -plotNoise = True - # Save file extension (png or pdf) saveFileExtension = ".png" @@ -72,7 +69,8 @@ numberOfGases = 3 # Third gas mole fraction -thirdGasMoleFrac = 0.5 +thirdGasMoleFrac = 0.25 +fixOneGas = False # Flag that says if third composition is fixed # Mole Fraction of interest moleFrac = [0.1, 0.9] @@ -81,7 +79,7 @@ multiplierError = [1., 1., 1.] # Sensor ID -sensorID = np.array([18,6,8]) +sensorID = np.array([0,6,8]) # Acceptable SNR signalToNoise = 25*0.1 @@ -96,11 +94,15 @@ if numberOfGases == 2: moleFractionRange = np.array([np.linspace(0,1,numMolFrac), 1 - np.linspace(0,1,numMolFrac)]).T elif numberOfGases == 3: - remainingMoleFrac = 1. - thirdGasMoleFrac - moleFractionRange = np.array([np.linspace(0,remainingMoleFrac,numMolFrac), + if fixOneGas: + remainingMoleFrac = 1. - thirdGasMoleFrac + moleFractionRange = np.array([np.linspace(0,remainingMoleFrac,numMolFrac), remainingMoleFrac - np.linspace(0,remainingMoleFrac,numMolFrac), np.tile(thirdGasMoleFrac,numMolFrac)]).T - + else: + moleFractionRangeTemp = np.random.dirichlet((1,1,1),numMolFrac) + moleFractionRange = moleFractionRangeTemp[moleFractionRangeTemp[:,0].argsort()] + arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) for ii in range(moleFractionRange.shape[0]): arraySimResponse[ii,:] = simulateSensorArray(sensorID, pressureTotal, @@ -120,84 +122,125 @@ # Compute the first derivative, elbow point, and the fill regions for all # sensors for 2 gases +if numberOfGases == 2 or (numberOfGases == 3 and fixOneGas == True): + xFill = np.zeros([arraySimResponse.shape[1],2]) + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + firstDerivative = np.zeros([arraySimResponse.shape[0],1]) + firstDerivative[:,0] = np.gradient(arraySimResponse[:,kk]) + secondDerivative = np.zeros([firstDerivative.shape[0],1]) + secondDerivative[:,0] = np.gradient(firstDerivative[:,0]) + # Get the sign of the first derivative for increasing/decreasing + if all(i >= 0. for i in firstDerivative[:,0]): + slopeDir = "increasing" + elif all(i < 0. for i in firstDerivative[:,0]): + slopeDir = "decreasing" + else: + print("Dangerous! I should not be here!!!") + # Get the sign of the second derivative for concavity/convexity + if all(i >= 0. for i in secondDerivative[:,0]): + secondDerDir = "convex" + elif all(i < 0. for i in secondDerivative[:,0]): + secondDerDir = "concave" + else: + print("Dangerous! I should not be here!!!") + + + kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,kk], + curve=secondDerDir, direction=slopeDir) + elbowPoint = list(kneedle.all_elbows) + + # Plot the sensor response for all the conocentrations and highlight the + # working region + # Obtain coordinates to fill working region + if secondDerDir == "concave": + if slopeDir == "increasing": + xFill[kk,:] = [0,elbowPoint[0]] + else: + if numberOfGases == 2: + xFill[kk,:] = [elbowPoint[0], 1.0] + elif numberOfGases == 3: + if fixOneGas: + xFill[kk,:] = [elbowPoint[0], 1.-thirdGasMoleFrac] + elif secondDerDir == "convex": + if slopeDir == "increasing": + if numberOfGases == 3: + if fixOneGas: + xFill[kk,:] = [elbowPoint[0],1.-thirdGasMoleFrac] + else: + xFill[kk,:] = [0,elbowPoint[0]] + else: + print("Dangerous! I should not be here!!!") -xFill = np.zeros([arraySimResponse.shape[1],2]) -# Loop through all sensors -for kk in range(arraySimResponse.shape[1]): - firstDerivative = np.zeros([arraySimResponse.shape[0],1]) - firstDerivative[:,0] = np.gradient(arraySimResponse[:,kk]) - secondDerivative = np.zeros([firstDerivative.shape[0],1]) - secondDerivative[:,0] = np.gradient(firstDerivative[:,0]) - # Get the sign of the first derivative for increasing/decreasing - if all(i >= 0. for i in firstDerivative[:,0]): - slopeDir = "increasing" - elif all(i < 0. for i in firstDerivative[:,0]): - slopeDir = "decreasing" - else: - print("Dangerous! I should not be here!!!") - # Get the sign of the second derivative for concavity/convexity - if all(i >= 0. for i in secondDerivative[:,0]): - secondDerDir = "convex" - elif all(i < 0. for i in secondDerivative[:,0]): - secondDerDir = "concave" - else: - print("Dangerous! I should not be here!!!") +if numberOfGases == 2 or (numberOfGases == 3 and fixOneGas == True): + fig = plt.figure + ax = plt.gca() + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + ax.plot(moleFractionRange[:,0],arraySimResponse[:,kk],color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response + ax.fill_between(xFill[kk,:],1.1*np.max(arraySimResponse), facecolor='#'+colorsForPlot[kk], alpha=0.25) + if numberOfGases == 2: + ax.fill_between([0.,1.],signalToNoise, facecolor='#4a5759', alpha=0.25) + elif numberOfGases == 3: + if fixOneGas: + mpl.rcParams['hatch.linewidth'] = 0.1 + ax.fill_between([0.,1.-thirdGasMoleFrac],signalToNoise, facecolor='#4a5759', alpha=0.25) + ax.fill_between([1.-thirdGasMoleFrac,1.],1.1*np.max(arraySimResponse), facecolor='#555b6e', alpha=0.25, hatch = 'x') + ax.set(xlabel='$y_1$ [-]', + ylabel='$m_i$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + ax.legend() + + # Save the figure + if saveFlag: + # FileName: SensorResponse___ + sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') + saveFileName = "SensorResponse_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + plt.show() +# Make ternanry/ternary equivalent plots +if numberOfGases == 3 and fixOneGas == False: + fig = plt.figure() + for ii in range(len(sensorID)): + ax = plt.subplot(1,3,ii+1) + s1 = ax.scatter(moleFractionRange[:,0],moleFractionRange[:,1], + c = arraySimResponse[:,ii], s = 2, + alpha=0.75, cmap = "PuOr", + vmin = 0.9*np.min(arraySimResponse[:,ii]), + vmax = 1.1*np.max(arraySimResponse[:,ii])) + ax.plot([0.,0.25],[0.25,0.],linewidth = 1, linestyle = ':', color = 'k') + ax.plot([0.,0.50],[0.50,0.],linewidth = 1, linestyle = ':', color = 'k') + ax.plot([0.,0.75],[0.75,0.],linewidth = 1, linestyle = ':', color = 'k') + ax.plot([0.,1.],[1.,0.],linewidth = 1, linestyle = ':', color = 'k') + ax.set(xlabel='$y_1$ [-]', + ylabel='$y_2$ [-]', + xlim = [0,1.], ylim = [0, 1.]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + if ii == 3: + cbar = plt.colorbar(s1,ax=ax,label="$m_i$ [g kg$^{-1}$]") + cbar.ax.locator_params(nbins=4) + plt.show() - kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,kk], - curve=secondDerDir, direction=slopeDir) - elbowPoint = list(kneedle.all_elbows) + # TERNARY PLOT - WIP + for ii in range(len(sensorID)): + df = pd.DataFrame({'y1':moleFractionRange[:,0], + 'y2':moleFractionRange[:,1], + 'y3':moleFractionRange[:,2], + 'sensorResponse':arraySimResponse[:,ii]}) - # Plot the sensor response for all the conocentrations and highlight the - # working region - # Obtain coordinates to fill working region - if secondDerDir == "concave": - if slopeDir == "increasing": - xFill[kk,:] = [0,elbowPoint[0]] - else: - if numberOfGases == 2: - xFill[kk,:] = [elbowPoint[0], 1.0] - elif numberOfGases == 3: - xFill[kk,:] = [elbowPoint[0], 1.-thirdGasMoleFrac] - elif secondDerDir == "convex": - if slopeDir == "increasing": - if numberOfGases == 3: - xFill[kk,:] = [elbowPoint[0],1.-thirdGasMoleFrac] - else: - xFill[kk,:] = [0,elbowPoint[0]] - else: - print("Dangerous! I should not be here!!!") - -fig = plt.figure -ax = plt.gca() -# Loop through all sensors -for kk in range(arraySimResponse.shape[1]): - ax.plot(moleFractionRange[:,0],arraySimResponse[:,kk],color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response - ax.fill_between(xFill[kk,:],1.1*np.max(arraySimResponse), facecolor='#'+colorsForPlot[kk], alpha=0.25) - if numberOfGases == 2: - ax.fill_between([0.,1.],signalToNoise, facecolor='#4a5759', alpha=0.25) - elif numberOfGases == 3: - mpl.rcParams['hatch.linewidth'] = 0.1 - ax.fill_between([0.,1.-thirdGasMoleFrac],signalToNoise, facecolor='#4a5759', alpha=0.25) - ax.fill_between([1.-thirdGasMoleFrac,1.],1.1*np.max(arraySimResponse), facecolor='#555b6e', alpha=0.25, hatch = 'x') -ax.set(xlabel='$y_1$ [-]', - ylabel='$m_i$ [g kg$^{-1}$]', - xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) -ax.locator_params(axis="x", nbins=4) -ax.locator_params(axis="y", nbins=4) -ax.legend() - -# Save the figure -if saveFlag: - # FileName: SensorResponse___ - sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') - saveFileName = "SensorResponse_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) -plt.show() + fig = px.scatter_ternary(df, a = 'y1', + b = 'y2', + c = 'y3', + color = 'sensorResponse') + fig.show(renderer="png") # Plot the objective function used to evaluate the concentration for individual # sensors and the total (sum) From f1610c04d995c6deaecb20b50f84b5a1a76ede93 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 23 Nov 2020 16:16:44 +0000 Subject: [PATCH 39/99] Plotting improvements for 3 gas system --- plotFunctions/plotConcentrationViolinPlots.py | 29 +++-- plotFunctions/plotObjectiveFunction.py | 105 +++++++++++------- 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py index 64d697d..2f6d471 100755 --- a/plotFunctions/plotConcentrationViolinPlots.py +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -12,6 +12,7 @@ # Plots to visualize different sensor responses # # Last modified: +# - 2020-11-23, AK: Add standard deviation/CV plotting # - 2020-11-20, AK: Introduce 3 gas capability # - 2020-11-18, AK: Changes to data reconciliation and new plots # - 2020-11-13, AK: Initial creation @@ -53,7 +54,7 @@ colorForPlot = ["#" + counter for counter in colorTemp] # Mole fraction ID -moleFracID = 0 +moleFracID = 6 meanMolFrac = [0.001,0.01,0.1,0.25,0.5,0.75,0.90] # Y limits for the plot @@ -65,7 +66,7 @@ legendFlag = False # Sensor ID -sensorText = ["1/1", "1/5", "1/10", "17/16"] +sensorText = ["17/15", "17/15/16", "17/15/6"] # Initialize x, y, and type for the plotting concatenatedX = [] @@ -75,10 +76,10 @@ concatenatedType = [] # File to be loaded for the left of violin plot -loadFileName = ["sensitivityAnalysis_0-6-8_20201119_1516_fc5b327.npz", - "sensitivityAnalysis_1-6-8_20201119_1516_fc5b327.npz", - "sensitivityAnalysis_2-6-8_20201119_1516_fc5b327.npz"] -saveFileSensorText = [17, 16] +loadFileName = ["sensitivityAnalysis_17-15_20201113_1450_c9b2a41.npz", + "sensitivityAnalysis_17-15-16_20201117_1135_c9b2a41.npz", + "sensitivityAnalysis_17-15-6_20201117_1135_c9b2a41.npz"] +saveFileSensorText = [17,15,16,6] if flagComparison and len(loadFileName) != 2: errorString = "When flagComparison is True, only two files can be loaded for comparison." @@ -222,9 +223,12 @@ xlim = [0.,1.], ylim = [1e-6,1.]) ax1.set_yscale('log') ax1.locator_params(axis="x", nbins=4) -plt.legend(loc='best') +if len(loadFileName) > 1: + plt.legend(loc='best') +else: + plt.legend([],[], frameon=False) -# Range +# CV ax2 = plt.subplot(1,2,2) cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', @@ -237,4 +241,13 @@ ax2.set_yscale('log') if not legendFlag: plt.legend([],[], frameon=False) +if saveFlag: + # FileName: SensorViolinPlot____> + saveFileSensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-').replace(',','') + saveFileName = "SensorStdCV_" + saveFileSensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) plt.show() \ No newline at end of file diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index a24f4ba..d9d6f51 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-23, AK: Change ternary plots # - 2020-11-20, AK: Introduce ternary plots # - 2020-11-19, AK: Add 3 gas knee calculator # - 2020-11-19, AK: Multigas plotting capability @@ -33,8 +34,9 @@ from generateTrueSensorResponse import generateTrueSensorResponse from simulateSensorArray import simulateSensorArray import os +from sklearn.cluster import KMeans import pandas as pd -import plotly.express as px +import ternary import matplotlib as mpl import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D @@ -53,7 +55,7 @@ colorsForPlot = ["ff499e","d264b6","a480cf","779be7","49b6ff"] # Number of molefractions -numMolFrac= 1001 +numMolFrac= 10001 # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -229,45 +231,64 @@ cbar.ax.locator_params(nbins=4) plt.show() - # TERNARY PLOT - WIP + # Loop through all the materials in the array for ii in range(len(sensorID)): - df = pd.DataFrame({'y1':moleFractionRange[:,0], - 'y2':moleFractionRange[:,1], - 'y3':moleFractionRange[:,2], - 'sensorResponse':arraySimResponse[:,ii]}) - - fig = px.scatter_ternary(df, a = 'y1', - b = 'y2', - c = 'y3', - color = 'sensorResponse') - fig.show(renderer="png") + # Reshape the response for k-means clustering + reshapedArraySimResponse = np.reshape(arraySimResponse[:,ii],[-1,1]) + # Obtain the group of the sensor (sensitive/non sensitive) + predictionGroup = KMeans(n_clusters=2,random_state=None).fit_predict(reshapedArraySimResponse) + + # Plot raw response in a ternary plot + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + tax.scatter(moleFractionRange, marker='o', s=2, c=arraySimResponse[:,ii], + vmax=max(arraySimResponse[:,ii]), colorbar=True, + colormap=plt.cm.PuOr, cmap=plt.cm.PuOr, + cbarlabel = '$m_i$ [g kg$^{-1}$]') + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + if saveFlag: + # FileName: SensorResponse___ + sensorText = str(sensorID[ii]).replace('[','').replace(']','').replace(' ','-') + saveFileName = "SensorResponse_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + tax.show() -# Plot the objective function used to evaluate the concentration for individual -# sensors and the total (sum) -if numberOfGases == 2: - fig = plt.figure - ax = plt.gca() - for kk in range(arraySimResponse.shape[1]): - ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,kk] - -arraySimResponse[:,kk])/arrayTrueResponse[:,kk],2), - color='#'+colorsForPlot[kk], label = '$J_'+str(kk+1)+'$') - ax.plot(moleFractionRange[:,0],objFunction,color='#'+colorsForPlot[-1], label = '$\Sigma J_i$') # Error all sensors - ax.locator_params(axis="x", nbins=4) - ax.locator_params(axis="y", nbins=4) - ax.set(xlabel='$y_1$ [-]', - ylabel='$J$ [-]', - xlim = [0,1.], ylim = [0, None]) - ax.legend() - - # Save the figure - if saveFlag: - # FileName: SensorObjFunc____ - sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-') - moleFrac = str(moleFrac).replace('[','').replace(']','').replace(' ','').replace(',','-').replace('.','') - saveFileName = "SensorObjFunc_" + sensorText + "_" + moleFrac + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('simulationFigures',saveFileName) - # Check if simulationFigures directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) - plt.show() \ No newline at end of file + # Plot prediceted group in a ternary plot + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + tax.scatter(moleFractionRange, marker='o', s=2, c=predictionGroup, + colormap=plt.cm.RdYlGn, cmap=plt.cm.RdYlGn, + cbarlabel = '$m_i$ [g kg$^{-1}$]') + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + if saveFlag: + # FileName: SensorResponse___ + sensorText = str(sensorID[ii]).replace('[','').replace(']','').replace(' ','-') + saveFileName = "SensorRegion_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + tax.show() \ No newline at end of file From d8be111d161a9941a47ab0f81f3843e36628c727 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 23 Nov 2020 16:57:18 +0000 Subject: [PATCH 40/99] Fix for 3 gas mole fraction --- sensitivityAnalysis.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 337f527..a1091ef 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,6 +13,7 @@ # concentration estimate # # Last modified: +# - 2020-11-23, AK: Fix for 3 gas mole fraction # - 2020-11-19, AK: Modify for three gas system # - 2020-11-12, AK: Save arrayConcentration in output # - 2020-11-12, AK: Bug fix for multipler error @@ -77,14 +78,14 @@ stdError = 0.1 # [g/kg] # Multipler error for the sensor measurement -multiplierError = [1., 1.] +multiplierError = [1., 1., 1.] # Custom input mole fraction for gas 1 meanMoleFracG1 = np.array([0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90]) diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. # Number of iterations for the estimator -numberOfIterations = 1 +numberOfIterations = 1000 # Custom input mole fraction for gas 3 (for 3 gas system) # Mole fraction for the third gas is fixed. The mole fraction of the other @@ -118,8 +119,10 @@ inputMoleFrac[:,0] = np.random.uniform(meanMoleFracG1[ii]-diffMoleFracG1, meanMoleFracG1[ii]+diffMoleFracG1, numberOfIterations) # y1 is variable - inputMoleFrac[:,2] = meanMoleFracG3 # y3 is fixed to a constant - inputMoleFrac[:,1] = 1. - inputMoleFrac[:,2] - inputMoleFrac[:,0] # y2 from mass balance + tempMoleFrac = (np.random.dirichlet((1,1),numberOfIterations) + *(1.-meanMoleFracG1[ii])) + inputMoleFrac[:,1] = tempMoleFrac[:,0] + inputMoleFrac[:,2] = tempMoleFrac[:,1] # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials From 39829f8df8461ef64da221805c20589e6d1039d1 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 23 Nov 2020 17:51:27 +0000 Subject: [PATCH 41/99] Bug fixes --- sensitivityAnalysis.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index a1091ef..c296f9c 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -87,16 +87,6 @@ # Number of iterations for the estimator numberOfIterations = 1000 -# Custom input mole fraction for gas 3 (for 3 gas system) -# Mole fraction for the third gas is fixed. The mole fraction of the other -# two gases is then constrained as y1 + y2 = 1 - y3 -meanMoleFracG3 = 0.25 - -# Remove elements from array that do no meet the mole fraction sum -# constraint for 3 gas system -if numberOfGases == 3: - meanMoleFracG1 = meanMoleFracG1[meanMoleFracG1 <= 1-meanMoleFracG3] - # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) stdConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) @@ -119,6 +109,7 @@ inputMoleFrac[:,0] = np.random.uniform(meanMoleFracG1[ii]-diffMoleFracG1, meanMoleFracG1[ii]+diffMoleFracG1, numberOfIterations) # y1 is variable + # Generate random numbers between 0 and 1 and scaled to get y2 and y3 tempMoleFrac = (np.random.dirichlet((1,1),numberOfIterations) *(1.-meanMoleFracG1[ii])) inputMoleFrac[:,1] = tempMoleFrac[:,0] @@ -151,7 +142,6 @@ savez (savePath, numberOfGases = numberOfGases, numberOfIterations = numberOfIterations, moleFractionG1 = meanMoleFracG1, - meanMoleFracG3 = meanMoleFracG3, multiplierError = multiplierError, meanError = meanError, stdError = stdError, From 694df39a8b0d766027652d5916f800d7c6bcdc4e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 25 Nov 2020 18:34:45 +0000 Subject: [PATCH 42/99] More fix for 3 gas mole fraction --- sensitivityAnalysis.py | 45 +++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index c296f9c..f3c17b7 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -83,9 +83,18 @@ # Custom input mole fraction for gas 1 meanMoleFracG1 = np.array([0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90]) diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. +# For three gases generate the input concentration from a drichlet distribution +if numberOfGases == 3: + inputMoleFracALL = np.array([[0.05, 0.15, 0.80], + [0.15, 0.25, 0.60], + [0.40, 0.35, 0.25], + [0.50, 0.30, 0.20], + [0.75, 0.10, 0.15], + [0.90, 0.05, 0.05], + [0.999, 0.0009, 0.0001]]) # Number of iterations for the estimator -numberOfIterations = 1000 +numberOfIterations = 2 # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) @@ -106,14 +115,9 @@ inputMoleFrac[:,1] = 1. - inputMoleFrac[:,0] elif numberOfGases == 3: inputMoleFrac = np.zeros([numberOfIterations,3]) - inputMoleFrac[:,0] = np.random.uniform(meanMoleFracG1[ii]-diffMoleFracG1, - meanMoleFracG1[ii]+diffMoleFracG1, - numberOfIterations) # y1 is variable - # Generate random numbers between 0 and 1 and scaled to get y2 and y3 - tempMoleFrac = (np.random.dirichlet((1,1),numberOfIterations) - *(1.-meanMoleFracG1[ii])) - inputMoleFrac[:,1] = tempMoleFrac[:,0] - inputMoleFrac[:,2] = tempMoleFrac[:,1] + inputMoleFrac[:,0] = inputMoleFracALL[ii,0] + inputMoleFrac[:,1] = inputMoleFracALL[ii,1] + inputMoleFrac[:,2] = inputMoleFracALL[ii,2] # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials @@ -139,10 +143,19 @@ savePath = os.path.join('simulationResults',saveFileName) # Save the results as an array -savez (savePath, numberOfGases = numberOfGases, - numberOfIterations = numberOfIterations, - moleFractionG1 = meanMoleFracG1, - multiplierError = multiplierError, - meanError = meanError, - stdError = stdError, - arrayConcentration = arrayConcentration) \ No newline at end of file +if numberOfGases == 2: + savez (savePath, numberOfGases = numberOfGases, + numberOfIterations = numberOfIterations, + trueMoleFrac = meanMoleFracG1, + multiplierError = multiplierError, + meanError = meanError, + stdError = stdError, + arrayConcentration = arrayConcentration) +if numberOfGases == 3: + savez (savePath, numberOfGases = numberOfGases, + numberOfIterations = numberOfIterations, + trueMoleFrac = inputMoleFracALL, + multiplierError = multiplierError, + meanError = meanError, + stdError = stdError, + arrayConcentration = arrayConcentration) \ No newline at end of file From dfac751dab1af87b4e16dbf1b479dd19e452a933 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 25 Nov 2020 18:35:35 +0000 Subject: [PATCH 43/99] Fix for 3 gas system --- plotFunctions/plotObjectiveFunction.py | 109 ++++++++++++++++++------- 1 file changed, 78 insertions(+), 31 deletions(-) diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index d9d6f51..f5e85a7 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-24, AK: Fix for 3 gas system # - 2020-11-23, AK: Change ternary plots # - 2020-11-20, AK: Introduce ternary plots # - 2020-11-19, AK: Add 3 gas knee calculator @@ -37,6 +38,7 @@ from sklearn.cluster import KMeans import pandas as pd import ternary +import scipy import matplotlib as mpl import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D @@ -53,6 +55,8 @@ # Plotting colors colorsForPlot = ["ff499e","d264b6","a480cf","779be7","49b6ff"] +colorGroup = ["#f94144","#43aa8b"] +colorIntersection = ["ff595e","ffca3a","8ac926","1982c4","6a4c93"] # Number of molefractions numMolFrac= 10001 @@ -72,7 +76,6 @@ # Third gas mole fraction thirdGasMoleFrac = 0.25 -fixOneGas = False # Flag that says if third composition is fixed # Mole Fraction of interest moleFrac = [0.1, 0.9] @@ -96,14 +99,8 @@ if numberOfGases == 2: moleFractionRange = np.array([np.linspace(0,1,numMolFrac), 1 - np.linspace(0,1,numMolFrac)]).T elif numberOfGases == 3: - if fixOneGas: - remainingMoleFrac = 1. - thirdGasMoleFrac - moleFractionRange = np.array([np.linspace(0,remainingMoleFrac,numMolFrac), - remainingMoleFrac - np.linspace(0,remainingMoleFrac,numMolFrac), - np.tile(thirdGasMoleFrac,numMolFrac)]).T - else: - moleFractionRangeTemp = np.random.dirichlet((1,1,1),numMolFrac) - moleFractionRange = moleFractionRangeTemp[moleFractionRangeTemp[:,0].argsort()] + moleFractionRangeTemp = np.random.dirichlet((1,1,1),numMolFrac) + moleFractionRange = moleFractionRangeTemp[moleFractionRangeTemp[:,0].argsort()] arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) for ii in range(moleFractionRange.shape[0]): @@ -124,7 +121,7 @@ # Compute the first derivative, elbow point, and the fill regions for all # sensors for 2 gases -if numberOfGases == 2 or (numberOfGases == 3 and fixOneGas == True): +if numberOfGases == 2: xFill = np.zeros([arraySimResponse.shape[1],2]) # Loop through all sensors for kk in range(arraySimResponse.shape[1]): @@ -159,11 +156,7 @@ if slopeDir == "increasing": xFill[kk,:] = [0,elbowPoint[0]] else: - if numberOfGases == 2: - xFill[kk,:] = [elbowPoint[0], 1.0] - elif numberOfGases == 3: - if fixOneGas: - xFill[kk,:] = [elbowPoint[0], 1.-thirdGasMoleFrac] + xFill[kk,:] = [elbowPoint[0], 1.0] elif secondDerDir == "convex": if slopeDir == "increasing": if numberOfGases == 3: @@ -174,20 +167,14 @@ else: print("Dangerous! I should not be here!!!") -if numberOfGases == 2 or (numberOfGases == 3 and fixOneGas == True): +if numberOfGases == 2: fig = plt.figure ax = plt.gca() # Loop through all sensors for kk in range(arraySimResponse.shape[1]): ax.plot(moleFractionRange[:,0],arraySimResponse[:,kk],color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response ax.fill_between(xFill[kk,:],1.1*np.max(arraySimResponse), facecolor='#'+colorsForPlot[kk], alpha=0.25) - if numberOfGases == 2: - ax.fill_between([0.,1.],signalToNoise, facecolor='#4a5759', alpha=0.25) - elif numberOfGases == 3: - if fixOneGas: - mpl.rcParams['hatch.linewidth'] = 0.1 - ax.fill_between([0.,1.-thirdGasMoleFrac],signalToNoise, facecolor='#4a5759', alpha=0.25) - ax.fill_between([1.-thirdGasMoleFrac,1.],1.1*np.max(arraySimResponse), facecolor='#555b6e', alpha=0.25, hatch = 'x') + ax.fill_between([0.,1.],signalToNoise, facecolor='#4a5759', alpha=0.25) ax.set(xlabel='$y_1$ [-]', ylabel='$m_i$ [g kg$^{-1}$]', xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) @@ -208,7 +195,7 @@ plt.show() # Make ternanry/ternary equivalent plots -if numberOfGases == 3 and fixOneGas == False: +if numberOfGases == 3: fig = plt.figure() for ii in range(len(sensorID)): ax = plt.subplot(1,3,ii+1) @@ -232,20 +219,43 @@ plt.show() # Loop through all the materials in the array + sensitiveGroup = {} + skewnessResponse = np.zeros(len(sensorID)) for ii in range(len(sensorID)): + # Key for dictionary entry + tempDictKey = 's_{}'.format(ii) # Reshape the response for k-means clustering reshapedArraySimResponse = np.reshape(arraySimResponse[:,ii],[-1,1]) + # Get the skewness of the sensor resposne + skewnessResponse = scipy.stats.skew(reshapedArraySimResponse) + # Perform k-means clustering to separate out the data + kMeansStruct = KMeans(n_clusters=2,random_state=None).fit(reshapedArraySimResponse) # Obtain the group of the sensor (sensitive/non sensitive) - predictionGroup = KMeans(n_clusters=2,random_state=None).fit_predict(reshapedArraySimResponse) - + predictionGroup = kMeansStruct.predict(reshapedArraySimResponse) + if kMeansStruct.cluster_centers_[0] <= kMeansStruct.cluster_centers_[1]: + if skewnessResponse < 0.0: + sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==0,:] + colorGroup = ["#43aa8b","#f94144"] + else: + sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==1,:] + colorGroup = ["#f94144","#43aa8b"] + + else: + if skewnessResponse < 0.0: + sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==1,:] + colorGroup = ["#f94144","#43aa8b"] + else: + sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==0,:] + colorGroup = ["#43aa8b","#f94144"] + # Plot raw response in a ternary plot fig, tax = ternary.figure(scale=1) fig.set_size_inches(4,3.3) tax.boundary(linewidth=1.0) tax.gridlines(multiple=.2, color="gray") tax.scatter(moleFractionRange, marker='o', s=2, c=arraySimResponse[:,ii], - vmax=max(arraySimResponse[:,ii]), colorbar=True, - colormap=plt.cm.PuOr, cmap=plt.cm.PuOr, + vmin=min(arraySimResponse[:,ii]),vmax=max(arraySimResponse[:,ii]), + colorbar=True,colormap=plt.cm.PuOr, cmap=plt.cm.PuOr, cbarlabel = '$m_i$ [g kg$^{-1}$]') tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) @@ -266,13 +276,14 @@ plt.savefig (savePath) tax.show() - # Plot prediceted group in a ternary plot + # Plot prediceted group in a ternary plot + customColorMap = mpl.colors.ListedColormap(colorGroup) fig, tax = ternary.figure(scale=1) fig.set_size_inches(4,3.3) tax.boundary(linewidth=1.0) tax.gridlines(multiple=.2, color="gray") tax.scatter(moleFractionRange, marker='o', s=2, c=predictionGroup, - colormap=plt.cm.RdYlGn, cmap=plt.cm.RdYlGn, + colormap=customColorMap, cmap=customColorMap, cbarlabel = '$m_i$ [g kg$^{-1}$]') tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) @@ -291,4 +302,40 @@ if not os.path.exists(os.path.join('..','simulationFigures')): os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) - tax.show() \ No newline at end of file + tax.show() + + # Histogram for the sensor response + fig = plt.figure + ax = plt.subplot(1,1,1) + ax.hist(arraySimResponse[:,ii], bins = 100, + linewidth=1.5, histtype = 'stepfilled', color='k', alpha = 0.25) + plt.show() + + # Plot the region of sensitivity in a ternary plot overlayed for all the + # sensors + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + for ii in range(len(sensitiveGroup)): + tempDictKey = 's_{}'.format(ii) + tax.scatter(sensitiveGroup[tempDictKey], marker='o', s=2, + color = '#'+colorIntersection[ii], alpha = 0.15) + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + if saveFlag: + # FileName: SensorResponse___ + sensorText = str(sensorID[ii]).replace('[','').replace(']','').replace(' ','-') + saveFileName = "SensorRegion_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + tax.show() \ No newline at end of file From f67b01868a8a8badc6d422522b400b9a0a4b38d4 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 25 Nov 2020 18:43:02 +0000 Subject: [PATCH 44/99] Small fix for number of iterations --- sensitivityAnalysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index f3c17b7..b0b4b92 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -13,6 +13,7 @@ # concentration estimate # # Last modified: +# - 2020-11-24, AK: More fix for 3 gas mole fraction # - 2020-11-23, AK: Fix for 3 gas mole fraction # - 2020-11-19, AK: Modify for three gas system # - 2020-11-12, AK: Save arrayConcentration in output @@ -94,7 +95,7 @@ [0.999, 0.0009, 0.0001]]) # Number of iterations for the estimator -numberOfIterations = 2 +numberOfIterations = 1000 # Initialize mean and standard deviation of concentration estimates meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) From 79cf4e3ffe3d23e1a4435ad10dd80327d6dffcc7 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 26 Nov 2020 17:26:26 +0000 Subject: [PATCH 45/99] Parallel processing fix --- sensitivityAnalysis.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index b0b4b92..6dc03be 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -12,7 +12,8 @@ # Script to perform a sensitivity analysis on the sensor response and # concentration estimate # -# Last modified: +# Last modified:] +# - 2020-11-26, AK: Parallel processing fix # - 2020-11-24, AK: More fix for 3 gas mole fraction # - 2020-11-23, AK: Fix for 3 gas mole fraction # - 2020-11-19, AK: Modify for three gas system @@ -86,13 +87,12 @@ diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. # For three gases generate the input concentration from a drichlet distribution if numberOfGases == 3: - inputMoleFracALL = np.array([[0.05, 0.15, 0.80], - [0.15, 0.25, 0.60], - [0.40, 0.35, 0.25], - [0.50, 0.30, 0.20], - [0.75, 0.10, 0.15], - [0.90, 0.05, 0.05], - [0.999, 0.0009, 0.0001]]) + inputMoleFracALL = np.array([[0.00, 0.20, 0.80], + [0.15, 0.20, 0.65], + [0.30, 0.20, 0.50], + [0.45, 0.20, 0.35], + [0.60, 0.20, 0.20], + [0.80, 0.20, 0.00]]) # Number of iterations for the estimator numberOfIterations = 1000 @@ -122,7 +122,7 @@ # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials - arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + arrayConcentrationTemp = Parallel(n_jobs=num_cores)(delayed(estimateConcentration) (numberOfAdsorbents,numberOfGases,None,sensorID, moleFraction = inputMoleFrac[ii], multiplierError = multiplierError, From cb4e47bed3de9af4c9f4cea4760c5423e740e6e3 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 26 Nov 2020 17:55:35 +0000 Subject: [PATCH 46/99] Bug fix --- sensitivityAnalysis.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index 6dc03be..cd64cfb 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -85,6 +85,7 @@ # Custom input mole fraction for gas 1 meanMoleFracG1 = np.array([0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90]) diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. +numberOfMoleFrac = len(meanMoleFracG1) # For three gases generate the input concentration from a drichlet distribution if numberOfGases == 3: inputMoleFracALL = np.array([[0.00, 0.20, 0.80], @@ -92,7 +93,8 @@ [0.30, 0.20, 0.50], [0.45, 0.20, 0.35], [0.60, 0.20, 0.20], - [0.80, 0.20, 0.00]]) + [0.80, 0.20, 0.00]]) + numberOfMoleFrac = inputMoleFracALL.shape[0] # Number of iterations for the estimator numberOfIterations = 1000 @@ -106,7 +108,7 @@ numberOfGases+len(sensorID)]) # Loop through all mole fractions -for ii in range(len(meanMoleFracG1)): +for ii in range(numberOfMoleFrac): # Generate a uniform distribution of mole fractions if numberOfGases == 2: inputMoleFrac = np.zeros([numberOfIterations,2]) From 50a3ed7113aedfcefd2d950e59025dd660d886cd Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 26 Nov 2020 18:54:33 +0000 Subject: [PATCH 47/99] More bug fix --- sensitivityAnalysis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index cd64cfb..b0dae8a 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -100,11 +100,11 @@ numberOfIterations = 1000 # Initialize mean and standard deviation of concentration estimates -meanConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) -stdConcEstimate = np.zeros([len(meanMoleFracG1),numberOfGases]) +meanConcEstimate = np.zeros([numberOfMoleFrac,numberOfGases]) +stdConcEstimate = np.zeros([numberOfMoleFrac,numberOfGases]) # Initialize the arrayConcentration matrix -arrayConcentration = np.zeros([len(meanMoleFracG1),numberOfIterations, +arrayConcentration = np.zeros([numberOfMoleFrac,numberOfIterations, numberOfGases+len(sensorID)]) # Loop through all mole fractions From 80abbb554125536b843565715cd48914f34dd3e2 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 27 Nov 2020 18:19:16 +0000 Subject: [PATCH 48/99] Major modification for 3 gas system --- plotFunctions/plotConcentrationViolinPlots.py | 274 +++++++++++------- plotFunctions/plotObjectiveFunction.py | 130 ++++----- 2 files changed, 230 insertions(+), 174 deletions(-) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py index 2f6d471..a23b9e1 100755 --- a/plotFunctions/plotConcentrationViolinPlots.py +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -12,6 +12,7 @@ # Plots to visualize different sensor responses # # Last modified: +# - 2020-11-27, AK: Major modification for 3 gas system # - 2020-11-23, AK: Add standard deviation/CV plotting # - 2020-11-20, AK: Introduce 3 gas capability # - 2020-11-18, AK: Changes to data reconciliation and new plots @@ -54,8 +55,8 @@ colorForPlot = ["#" + counter for counter in colorTemp] # Mole fraction ID -moleFracID = 6 -meanMolFrac = [0.001,0.01,0.1,0.25,0.5,0.75,0.90] +moleFracID = 3 +meanMolFrac = [0.00,0.2,0.35,0.5,0.75,0.9] # Y limits for the plot Y_LIMITS = [None,None] @@ -66,19 +67,20 @@ legendFlag = False # Sensor ID -sensorText = ["17/15", "17/15/16", "17/15/6"] +sensorText = ["12/13/14", "3/4/1", "2/6/8", "0/1/2"] # Initialize x, y, and type for the plotting concatenatedX = [] +concatenatedX2 = [] +concatenatedX3 = [] concatenatedY1 = [] concatenatedY2 = [] concatenatedY3 = [] concatenatedType = [] # File to be loaded for the left of violin plot -loadFileName = ["sensitivityAnalysis_17-15_20201113_1450_c9b2a41.npz", - "sensitivityAnalysis_17-15-16_20201117_1135_c9b2a41.npz", - "sensitivityAnalysis_17-15-6_20201117_1135_c9b2a41.npz"] +loadFileName = ["sensitivityAnalysis_0-6-5_20201127_1217_50a3ed7.npz", + "sensitivityAnalysis_3-4-1_20201127_1220_50a3ed7.npz"] saveFileSensorText = [17,15,16,6] if flagComparison and len(loadFileName) != 2: @@ -89,6 +91,8 @@ for kk in range(len(loadFileName)): # Initialize x, y, and type for the local loop xVar = [] + x2Var = [] + x3Var = [] y1Var = [] y2Var = [] y3Var = [] @@ -97,8 +101,14 @@ simResultsFile = os.path.join('..','simulationResults',loadFileName[kk]); resultOutput = load(simResultsFile)["arrayConcentration"] numberOfGases = load(simResultsFile)["numberOfGases"] - moleFrac = load(simResultsFile)["moleFractionG1"] - + if numberOfGases == 2: + moleFrac = load(simResultsFile)["trueMoleFrac"] + elif numberOfGases == 3: + moleFracTemp = load(simResultsFile)["trueMoleFrac"] + moleFrac = moleFracTemp[:,0] + moleFrac2 = moleFracTemp[:,1] + moleFrac3 = moleFracTemp[:,2] + # Loop through all the molefractions for ii in range(resultOutput.shape[0]): # For the cases where there are two gases @@ -117,6 +127,9 @@ if numberOfGases == 3: y3Var = np.concatenate((y3Var,resultOutput[ii,:,counterInd+4])) # y3 xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) + if numberOfGases == 3: + x2Var = x2Var + ([str(moleFrac2[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) + x3Var = x3Var + ([str(moleFrac3[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) if not flagComparison: typeVar = typeVar+[sensorText[kk]] * len(resultOutput[ii,:,counterInd+2]) # Generate type for comparison @@ -129,6 +142,9 @@ concatenatedY2 = np.concatenate((concatenatedY2,y2Var)) if numberOfGases == 3: concatenatedY3 = np.concatenate((concatenatedY3,y3Var)) + concatenatedX2 = concatenatedX2 + x2Var + concatenatedX3 = concatenatedX3 + x3Var + concatenatedType = concatenatedType + typeVar # Reinitialize all the loaded values to empty variable @@ -145,109 +161,163 @@ 'y1':concatenatedY1, 'y2':concatenatedY2, 'dataType':concatenatedType}) + # Compute the mean and standard deviation + meanData = df.groupby(['dataType','x'], as_index=False, sort=False).mean() + stdData = df.groupby(['dataType','x'], as_index=False, sort=False).std() + # Coefficient of variation + cvData = stdData.copy() + cvData['y1'] = stdData['y1']/meanData['y1'] + elif numberOfGases == 3: df = pd.DataFrame({'x':concatenatedX, + 'x2':concatenatedX2, + 'x3':concatenatedX3, 'y1':concatenatedY1, 'y2':concatenatedY2, 'y3':concatenatedY3, 'dataType':concatenatedType}) + # Compute the mean and standard deviation + meanData = df.groupby(['dataType','x','x2','x3'], as_index=False, sort=False).mean() + stdData = df.groupby(['dataType','x','x2','x3'], as_index=False, sort=False).std() + # Coefficient of variation + cvData = stdData.copy() + cvData['y1'] = stdData['y1']/meanData['y1'] + cvData['y2'] = stdData['y2']/meanData['y2'] + cvData['y3'] = stdData['y3']/meanData['y3'] +# Plot quantities of interest +# Two gases +if numberOfGases == 2: + # Plot the figure + sns.set(style="ticks", palette="pastel", color_codes=True) + fig = plt.figure + ax1 = plt.subplot(1,1,1) + # Draw a nested violinplot for easier comparison + if flagComparison: + if scaleLog: + ax1.set_yscale('log') + sns.violinplot(data=df, x="x", y="y1", hue="dataType", inner = "box", + split=True, linewidth=1, palette={legendText[0]: colorForPlot[0], + legendText[1]: colorForPlot[1]}, + scale='width') + ax1.set(xlabel='$y_1$ [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) + plt.legend(loc='upper left') + if not legendFlag: + plt.legend([],[], frameon=False) + # Draw violin plot for compaison of different sensors + else: + sns.violinplot(data=df[df.x == str(meanMolFrac[moleFracID])], + x="dataType", y="y1", inner = "box", linewidth=1, + scale='width', palette = colorForPlot[0:len(loadFileName)]) + ax1.set(xlabel='Sensor ID [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) + if flagComparison: + for kk in range(len(meanMolFrac)): + ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#c0c0c0') + else: + ax1.axhline(meanMolFrac[moleFracID], linestyle=':', linewidth=1, color = '#c0c0c0') + ax1.locator_params(axis="y", nbins=4) + # Save the figure + if saveFlag: + # FileName: SensorViolinPlot____> + saveFileSensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-').replace(',','') + saveFileName = "SensorViolinPlot_" + saveFileSensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + plt.show() + + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + # Standard deviation + ax1 = plt.subplot(1,2,1) + stdData["x"] = pd.to_numeric(stdData["x"], downcast="float") + sns.lineplot(data=stdData, x='x', y='y1', hue='dataType', style='dataType', + dashes = False, markers = ['o']*len(loadFileName), + palette = colorForPlot[0:len(loadFileName)]) + ax1.set(xlabel='$y_1$ [-]', + ylabel='$\sigma (\hat{y}_1)$ [-]', + xlim = [0.,1.], ylim = [1e-6,1.]) + ax1.set_yscale('log') + ax1.locator_params(axis="x", nbins=4) + if len(loadFileName) > 1: + plt.legend(loc='best') + else: + plt.legend([],[], frameon=False) -# Compute the mean, standard deviation, and the quantiles for each -meanData = df.groupby(['dataType','x'], as_index=False, sort=False).mean() -stdData = df.groupby(['dataType','x'], as_index=False, sort=False).std() -maxData = df.groupby(['dataType','x'], as_index=False, sort=False).max() -minData = df.groupby(['dataType','x'], as_index=False, sort=False).min() -rangeData = (df.groupby(['dataType','x'], as_index=False).agg(np.ptp)) -Q1Data = df.groupby(['dataType','x'], as_index=False).quantile(0.25) -Q3Data = df.groupby(['dataType','x'], as_index=False).quantile(0.75) -# Coefficient of variation -cvData = stdData.copy() -cvData['y1'] = stdData['y1']/meanData['y1'] -cvData['y2'] = stdData['y2']/meanData['y2'] -if numberOfGases == 3: - cvData['y3'] = stdData['y3']/meanData['y3'] + # CV + ax2 = plt.subplot(1,2,2) + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = False, markers = ['o']*len(loadFileName), + palette = colorForPlot[0:len(loadFileName)]) + ax2.set(xlabel='$y_1$ [-]', + ylabel='$CV (\hat{y}_1)$ [-]', + xlim = [0.,1.], ylim = [1e-5,1.]) + ax2.locator_params(axis="x", nbins=4) + ax2.set_yscale('log') + if not legendFlag: + plt.legend([],[], frameon=False) + if saveFlag: + # FileName: SensorViolinPlot____> + saveFileSensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-').replace(',','') + saveFileName = "SensorStdCV_" + saveFileSensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + plt.show() -# Plot the figure -sns.set(style="ticks", palette="pastel", color_codes=True) -fig = plt.figure -ax1 = plt.subplot(1,1,1) -# Draw a nested violinplot for easier comparison -if flagComparison: - if scaleLog: - ax1.set_yscale('log') - sns.violinplot(data=df, x="x", y="y1", hue="dataType", inner = "box", - split=True, linewidth=1, palette={legendText[0]: colorForPlot[0], - legendText[1]: colorForPlot[1]}, - scale='width') - ax1.set(xlabel='$y_1$ [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) - plt.legend(loc='upper left') +# Three gases +if numberOfGases == 3: + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax1 = plt.subplot(1,3,1) + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = False, markers = ['o']*len(loadFileName), + palette = colorForPlot[0:len(loadFileName)]) + ax1.set(xlabel='$y_1$ [-]', + ylabel='$CV (\hat{y}_1)$ [-]', + xlim = [0.,1.], ylim = [1e-5,1.]) + ax1.locator_params(axis="x", nbins=4) + ax1.set_yscale('log') + if not legendFlag: + plt.legend([],[], frameon=False) + ax2 = plt.subplot(1,3,2) + cvData["x2"] = pd.to_numeric(cvData["x2"], downcast="float") + sns.lineplot(data=cvData, x='x2', y='y2', hue='dataType', style='dataType', + dashes = False, markers = ['o']*len(loadFileName), + palette = colorForPlot[0:len(loadFileName)]) + ax2.set(xlabel='$y_2$ [-]', + ylabel='$CV (\hat{y}_2)$ [-]', + xlim = [0.,1.], ylim = [1e-5,1.]) + ax2.locator_params(axis="x", nbins=4) + ax2.set_yscale('log') + if not legendFlag: + plt.legend([],[], frameon=False) + ax3 = plt.subplot(1,3,3) + cvData["x3"] = pd.to_numeric(cvData["x3"], downcast="float") + sns.lineplot(data=cvData, x='x3', y='y3', hue='dataType', style='dataType', + dashes = False, markers = ['o']*len(loadFileName), + palette = colorForPlot[0:len(loadFileName)]) + ax3.set(xlabel='$y_1$ [-]', + ylabel='$CV (\hat{y}_3)$ [-]', + xlim = [0.,1.], ylim = [1e-5,1.]) + ax3.locator_params(axis="x", nbins=4) + ax3.set_yscale('log') if not legendFlag: plt.legend([],[], frameon=False) -# Draw violin plot for compaison of different sensors -else: - sns.violinplot(data=df[df.x == str(meanMolFrac[moleFracID])], - x="dataType", y="y1", inner = "box", linewidth=1, - scale='width', palette = colorForPlot[0:len(loadFileName)]) - ax1.set(xlabel='Sensor ID [-]', ylabel='${\hat{y}_1}$ [-]', ylim = Y_LIMITS) -if flagComparison: - for kk in range(len(meanMolFrac)): - ax1.axhline(meanMolFrac[kk], linestyle=':', linewidth=1, color = '#c0c0c0') -else: - ax1.axhline(meanMolFrac[moleFracID], linestyle=':', linewidth=1, color = '#c0c0c0') - -ax1.locator_params(axis="y", nbins=4) -# Save the figure -if saveFlag: - # FileName: SensorViolinPlot____> - saveFileSensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-').replace(',','') - saveFileName = "SensorViolinPlot_" + saveFileSensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('..','simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) -plt.show() -# Plot quantities of interest -plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file -fig = plt.figure -# Standard deviation -ax1 = plt.subplot(1,2,1) -stdData["x"] = pd.to_numeric(stdData["x"], downcast="float") -sns.lineplot(data=stdData, x='x', y='y1', hue='dataType', style='dataType', - dashes = False, markers = ['o']*len(loadFileName), - palette = colorForPlot[0:len(loadFileName)]) -ax1.set(xlabel='$y_1$ [-]', - ylabel='$\sigma (\hat{y}_1)$ [-]', - xlim = [0.,1.], ylim = [1e-6,1.]) -ax1.set_yscale('log') -ax1.locator_params(axis="x", nbins=4) -if len(loadFileName) > 1: - plt.legend(loc='best') -else: - plt.legend([],[], frameon=False) - -# CV -ax2 = plt.subplot(1,2,2) -cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") -sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', - dashes = False, markers = ['o']*len(loadFileName), - palette = colorForPlot[0:len(loadFileName)]) -ax2.set(xlabel='$y_1$ [-]', - ylabel='$CV (\hat{y}_1)$ [-]', - xlim = [0.,1.], ylim = [1e-5,1.]) -ax2.locator_params(axis="x", nbins=4) -ax2.set_yscale('log') -if not legendFlag: - plt.legend([],[], frameon=False) -if saveFlag: - # FileName: SensorViolinPlot____> - saveFileSensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-').replace(',','') - saveFileName = "SensorStdCV_" + saveFileSensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('..','simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) -plt.show() \ No newline at end of file + if saveFlag: + # FileName: SensorViolinPlot____> + saveFileSensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-').replace(',','') + saveFileName = "SensorStdCV_" + saveFileSensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + plt.show() \ No newline at end of file diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index f5e85a7..a0922aa 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -59,7 +59,7 @@ colorIntersection = ["ff595e","ffca3a","8ac926","1982c4","6a4c93"] # Number of molefractions -numMolFrac= 10001 +numMolFrac= 1001 # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -69,13 +69,22 @@ temperature = np.array([298.15]); # Number of Adsorbents -numberOfAdsorbents = 20 +numberOfAdsorbents = 30 # Number of Gases numberOfGases = 3 - -# Third gas mole fraction -thirdGasMoleFrac = 0.25 +# Experimental mole fraction for 3 gases +if numberOfGases == 3: + inputMoleFracALL = np.array([[0.05, 0.95, 0.00], + [0.20, 0.80, 0.00], + [0.35, 0.65, 0.00], + [0.50, 0.50, 0.00], + [0.75, 0.25, 0.00], + [0.90, 0.10, 0.00]]) + # Fix one gas + fixOneGas = True + # Third gas mole fraction + thirdGasMoleFrac = 0.00 # Mole Fraction of interest moleFrac = [0.1, 0.9] @@ -84,7 +93,7 @@ multiplierError = [1., 1., 1.] # Sensor ID -sensorID = np.array([0,6,8]) +sensorID = np.array([0,4,1]) # Acceptable SNR signalToNoise = 25*0.1 @@ -101,6 +110,11 @@ elif numberOfGases == 3: moleFractionRangeTemp = np.random.dirichlet((1,1,1),numMolFrac) moleFractionRange = moleFractionRangeTemp[moleFractionRangeTemp[:,0].argsort()] + if fixOneGas: + remainingMoleFrac = 1. - thirdGasMoleFrac + moleFractionRange = np.array([np.linspace(0,remainingMoleFrac,numMolFrac), + remainingMoleFrac - np.linspace(0,remainingMoleFrac,numMolFrac), + np.tile(thirdGasMoleFrac,numMolFrac)]).T arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) for ii in range(moleFractionRange.shape[0]): @@ -121,7 +135,7 @@ # Compute the first derivative, elbow point, and the fill regions for all # sensors for 2 gases -if numberOfGases == 2: +if numberOfGases == 2 or (numberOfGases == 3 and fixOneGas == True): xFill = np.zeros([arraySimResponse.shape[1],2]) # Loop through all sensors for kk in range(arraySimResponse.shape[1]): @@ -149,14 +163,16 @@ curve=secondDerDir, direction=slopeDir) elbowPoint = list(kneedle.all_elbows) - # Plot the sensor response for all the conocentrations and highlight the - # working region # Obtain coordinates to fill working region if secondDerDir == "concave": if slopeDir == "increasing": xFill[kk,:] = [0,elbowPoint[0]] else: - xFill[kk,:] = [elbowPoint[0], 1.0] + if numberOfGases == 2: + xFill[kk,:] = [elbowPoint[0], 1.0] + elif numberOfGases == 3: + if fixOneGas: + xFill[kk,:] = [elbowPoint[0], 1.-thirdGasMoleFrac] elif secondDerDir == "convex": if slopeDir == "increasing": if numberOfGases == 3: @@ -167,20 +183,40 @@ else: print("Dangerous! I should not be here!!!") -if numberOfGases == 2: - fig = plt.figure - ax = plt.gca() +# Start plotting +if numberOfGases == 2 or (numberOfGases == 3 and fixOneGas == True): + if numberOfGases == 2: + fig = plt.figure + ax = plt.gca() + elif numberOfGases == 3: + fig = plt.figure + ax = plt.subplot(1,2,1) + ax2 = plt.subplot(1,2,2) # Loop through all sensors for kk in range(arraySimResponse.shape[1]): ax.plot(moleFractionRange[:,0],arraySimResponse[:,kk],color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response ax.fill_between(xFill[kk,:],1.1*np.max(arraySimResponse), facecolor='#'+colorsForPlot[kk], alpha=0.25) - ax.fill_between([0.,1.],signalToNoise, facecolor='#4a5759', alpha=0.25) + if numberOfGases == 2: + ax.fill_between([0.,1.],signalToNoise, facecolor='#4a5759', alpha=0.25) + elif numberOfGases == 3: + mpl.rcParams['hatch.linewidth'] = 0.1 + ax.fill_between([0.,1.-thirdGasMoleFrac],signalToNoise, facecolor='#4a5759', alpha=0.25) + ax.fill_between([1.-thirdGasMoleFrac,1.],1.1*np.max(arraySimResponse), facecolor='#555b6e', alpha=0.25, hatch = 'x') + ax2.plot(moleFractionRange[:,1],arraySimResponse[:,kk],color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response + ax.set(xlabel='$y_1$ [-]', ylabel='$m_i$ [g kg$^{-1}$]', xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) ax.legend() + if numberOfGases == 3: + ax2.set(xlabel='$y_2$ [-]', + ylabel='$m_i$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 1.1*np.max(arraySimResponse)]) + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + ax2.legend() # Save the figure if saveFlag: @@ -195,31 +231,10 @@ plt.show() # Make ternanry/ternary equivalent plots -if numberOfGases == 3: - fig = plt.figure() - for ii in range(len(sensorID)): - ax = plt.subplot(1,3,ii+1) - s1 = ax.scatter(moleFractionRange[:,0],moleFractionRange[:,1], - c = arraySimResponse[:,ii], s = 2, - alpha=0.75, cmap = "PuOr", - vmin = 0.9*np.min(arraySimResponse[:,ii]), - vmax = 1.1*np.max(arraySimResponse[:,ii])) - ax.plot([0.,0.25],[0.25,0.],linewidth = 1, linestyle = ':', color = 'k') - ax.plot([0.,0.50],[0.50,0.],linewidth = 1, linestyle = ':', color = 'k') - ax.plot([0.,0.75],[0.75,0.],linewidth = 1, linestyle = ':', color = 'k') - ax.plot([0.,1.],[1.,0.],linewidth = 1, linestyle = ':', color = 'k') - ax.set(xlabel='$y_1$ [-]', - ylabel='$y_2$ [-]', - xlim = [0,1.], ylim = [0, 1.]) - ax.locator_params(axis="x", nbins=4) - ax.locator_params(axis="y", nbins=4) - if ii == 3: - cbar = plt.colorbar(s1,ax=ax,label="$m_i$ [g kg$^{-1}$]") - cbar.ax.locator_params(nbins=4) - plt.show() - +if numberOfGases == 3 and fixOneGas == False: # Loop through all the materials in the array sensitiveGroup = {} + sensitiveResponse = {} skewnessResponse = np.zeros(len(sensorID)) for ii in range(len(sensorID)): # Key for dictionary entry @@ -235,17 +250,21 @@ if kMeansStruct.cluster_centers_[0] <= kMeansStruct.cluster_centers_[1]: if skewnessResponse < 0.0: sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==0,:] + sensitiveResponse[tempDictKey] = reshapedArraySimResponse[predictionGroup==0,:] colorGroup = ["#43aa8b","#f94144"] else: sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==1,:] + sensitiveResponse[tempDictKey] = reshapedArraySimResponse[predictionGroup==1,:] colorGroup = ["#f94144","#43aa8b"] else: if skewnessResponse < 0.0: sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==1,:] + sensitiveResponse[tempDictKey] = reshapedArraySimResponse[predictionGroup==1,:] colorGroup = ["#f94144","#43aa8b"] else: sensitiveGroup[tempDictKey] = moleFractionRange[predictionGroup==0,:] + sensitiveResponse[tempDictKey] = reshapedArraySimResponse[predictionGroup==0,:] colorGroup = ["#43aa8b","#f94144"] # Plot raw response in a ternary plot @@ -276,41 +295,6 @@ plt.savefig (savePath) tax.show() - # Plot prediceted group in a ternary plot - customColorMap = mpl.colors.ListedColormap(colorGroup) - fig, tax = ternary.figure(scale=1) - fig.set_size_inches(4,3.3) - tax.boundary(linewidth=1.0) - tax.gridlines(multiple=.2, color="gray") - tax.scatter(moleFractionRange, marker='o', s=2, c=predictionGroup, - colormap=customColorMap, cmap=customColorMap, - cbarlabel = '$m_i$ [g kg$^{-1}$]') - tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) - tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) - tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) - tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", - offset=0.035,clockwise=True,fontsize=10) - tax.clear_matplotlib_ticks() - tax._redraw_labels() - plt.axis('off') - if saveFlag: - # FileName: SensorResponse___ - sensorText = str(sensorID[ii]).replace('[','').replace(']','').replace(' ','-') - saveFileName = "SensorRegion_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('simulationFigures',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) - tax.show() - - # Histogram for the sensor response - fig = plt.figure - ax = plt.subplot(1,1,1) - ax.hist(arraySimResponse[:,ii], bins = 100, - linewidth=1.5, histtype = 'stepfilled', color='k', alpha = 0.25) - plt.show() - # Plot the region of sensitivity in a ternary plot overlayed for all the # sensors fig, tax = ternary.figure(scale=1) @@ -321,6 +305,8 @@ tempDictKey = 's_{}'.format(ii) tax.scatter(sensitiveGroup[tempDictKey], marker='o', s=2, color = '#'+colorIntersection[ii], alpha = 0.15) + tax.scatter(inputMoleFracALL, marker='o', s=20, + color = 'k') tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) From f05b2c03c6f49e94b173f804b170d8438df1f1e9 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 4 Dec 2020 12:01:12 +0000 Subject: [PATCH 49/99] Rough version of sensor response --- plotFunctions/analyzeSensorResponse.py | 644 +++++++++++++++++++++++++ 1 file changed, 644 insertions(+) create mode 100755 plotFunctions/analyzeSensorResponse.py diff --git a/plotFunctions/analyzeSensorResponse.py b/plotFunctions/analyzeSensorResponse.py new file mode 100755 index 0000000..60e4a82 --- /dev/null +++ b/plotFunctions/analyzeSensorResponse.py @@ -0,0 +1,644 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# +# +# Last modified: +# - 2020-11-05, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ +import pdb +import numpy as np +from numpy import load +from generateTrueSensorResponse import generateTrueSensorResponse +from simulateSensorArray import simulateSensorArray +import os +import pandas as pd +import ternary +import matplotlib as mpl +import matplotlib.pyplot as plt +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file +import auxiliaryFunctions + +os.chdir("..") + +# Save flag for figure +saveFlag = False + +# Save file extension (png or pdf) +saveFileExtension = ".png" + +# Plotting colors +colorsForPlot = ["eac435","345995","03cea4","fb4d3d","ca1551"] + +# Number of molefractions +numMolFrac= 101 + +# Total pressure of the gas [Pa] +pressureTotal = np.array([1.e5]); + +# Temperature of the gas [K] +# Can be a vector of temperatures +temperature = np.array([298.15]); + +# Number of Adsorbents +numberOfAdsorbents = 20 + +# Number of Gases +numberOfGases = 2 + +# Multiplier Error +multiplierError = [1., 1.] + +# Sensor ID +sensorID = np.array([6,2]) + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Simulate the sensor response for all possible concentrations +moleFractionRange = {} +moleFractionRange_y1 = {} +moleFractionRange_y2 = {} +# Two gases +if numberOfGases == 2: + moleFractionRange['y3_0'] = np.array([np.linspace(0,1,numMolFrac), 1 - np.linspace(0,1,numMolFrac)]).T +# Three gases - done by fixing one gas and going through combinations of +# y1 and y2 +elif numberOfGases == 3: + thirdGasMoleFrac = np.linspace(0,1.,numMolFrac) + for ii in range(len(thirdGasMoleFrac)): + tempDictKey = 'y3_{}'.format(ii) + remainingMoleFrac = 1. - thirdGasMoleFrac[ii] + moleFractionRange[tempDictKey] = np.array([np.linspace(0,remainingMoleFrac,numMolFrac), + remainingMoleFrac - np.linspace(0,remainingMoleFrac,numMolFrac), + np.tile(thirdGasMoleFrac[ii],numMolFrac)]).T + + secondGasMoleFrac = np.linspace(0,1.,numMolFrac) + for ii in range(len(secondGasMoleFrac)): + tempDictKey = 'y2_{}'.format(ii) + remainingMoleFrac = 1. - secondGasMoleFrac[ii] + moleFractionRange_y2[tempDictKey] = np.array([np.linspace(0,remainingMoleFrac,numMolFrac), + np.tile(secondGasMoleFrac[ii],numMolFrac), + remainingMoleFrac - np.linspace(0,remainingMoleFrac,numMolFrac)]).T + + firstGasMoleFrac = np.linspace(0,1.,numMolFrac) + for ii in range(len(firstGasMoleFrac)): + tempDictKey = 'y1_{}'.format(ii) + remainingMoleFrac = 1. - firstGasMoleFrac[ii] + moleFractionRange_y1[tempDictKey] = np.array([np.tile(firstGasMoleFrac[ii],numMolFrac), + np.linspace(0,remainingMoleFrac,numMolFrac), + remainingMoleFrac - np.linspace(0,remainingMoleFrac,numMolFrac)]).T + +# Get the simulated sensor response, condition number and the derivative of +# the mole fraction wrt the sensor response +arraySimResponse = {} +firstDerivativeSimResponse_y1 = {} +firstDerivativeSimResponse_y2 = {} +conditionNumber_y1 = {} +conditionNumber_y2 = {} +# Loop through all variables +for ii in range(len(moleFractionRange)): + tempDictKey = 'y3_{}'.format(ii) + # Temporary variable + moleFractionRangeTemp = moleFractionRange[tempDictKey] + arraySimResponseTemp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + firstDerivativeSimResponse_y1Temp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + firstDerivativeSimResponse_y2Temp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + conditionNumber_y1Temp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + conditionNumber_y2Temp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + # Loop through all mole fractions (y1 and y2) to get sensor response + for kk in range(moleFractionRangeTemp.shape[0]): + arraySimResponseTemp[kk,:] = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([moleFractionRangeTemp[kk,:]])) * multiplierError + + # Loop through all sensor responses to get the derivative and the condition + # number + for jj in range(arraySimResponseTemp.shape[1]): + firstDerivativeSimResponse_y1Temp[:,jj] = np.gradient(moleFractionRangeTemp[:,0], + arraySimResponseTemp[:,jj]) + # Condition number + conditionNumber_y1Temp[:,jj] = np.abs(np.multiply(np.divide(firstDerivativeSimResponse_y1Temp[:,jj], + moleFractionRangeTemp[:,0]), + arraySimResponseTemp[:,jj])) + firstDerivativeSimResponse_y2Temp[:,jj] = np.gradient(moleFractionRangeTemp[:,1], + arraySimResponseTemp[:,jj]) + # Condition number + conditionNumber_y2Temp[:,jj] = np.abs(np.multiply(np.divide(firstDerivativeSimResponse_y2Temp[:,jj], + moleFractionRangeTemp[:,1]), + arraySimResponseTemp[:,jj])) + # Save the sensor responseand the derivative to a dictionary + arraySimResponse[tempDictKey] = arraySimResponseTemp + firstDerivativeSimResponse_y1[tempDictKey] = firstDerivativeSimResponse_y1Temp + firstDerivativeSimResponse_y2[tempDictKey] = firstDerivativeSimResponse_y2Temp + conditionNumber_y1[tempDictKey] = conditionNumber_y1Temp + conditionNumber_y2[tempDictKey] = conditionNumber_y2Temp + arraySimResponseTemp = [] + firstDerivativeSimResponse_y1Temp = [] + firstDerivativeSimResponse_y2Temp = [] + conditionNumber_y1Temp = [] + conditionNumber_y2Temp = [] + +# For three gases +if numberOfGases == 3: + arraySimResponse_y2 = {} + firstDerivativeSimResponse_y31 = {} + firstDerivativeSimResponse_y3 = {} + for ii in range(len(moleFractionRange_y2)): + tempDictKey = 'y2_{}'.format(ii) + # Temporary variable + moleFractionRangeTemp = moleFractionRange_y2[tempDictKey] + arraySimResponseTemp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + firstDerivativeSimResponse_y31Temp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + firstDerivativeSimResponse_y3Temp = np.zeros([moleFractionRangeTemp.shape[0], + sensorID.shape[0]]) + + # Loop through all mole fractions (y1 and y2) to get sensor response + for kk in range(moleFractionRangeTemp.shape[0]): + arraySimResponseTemp[kk,:] = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([moleFractionRangeTemp[kk,:]])) * multiplierError + + # Loop through all sensor responses to get the derivative + for jj in range(arraySimResponseTemp.shape[1]): + firstDerivativeSimResponse_y31Temp[:,jj] = np.gradient(moleFractionRangeTemp[:,0],arraySimResponseTemp[:,jj]) + + firstDerivativeSimResponse_y3Temp[:,jj] = np.gradient(moleFractionRangeTemp[:,2], + arraySimResponseTemp[:,jj]) + + # Save the sensor responseand the derivative to a dictionary + arraySimResponse_y2[tempDictKey] = arraySimResponseTemp + firstDerivativeSimResponse_y31[tempDictKey] = firstDerivativeSimResponse_y31Temp + firstDerivativeSimResponse_y3[tempDictKey] = firstDerivativeSimResponse_y3Temp + arraySimResponseTemp = [] + firstDerivativeSimResponse_y31Temp = [] + firstDerivativeSimResponse_y3Temp = [] + + + # arraySimResponse_y1 = {} + # firstDerivativeSimResponse_y12 = {} + # firstDerivativeSimResponse_y13 = {} + # for ii in range(len(moleFractionRange_y1)): + # tempDictKey = 'y1_{}'.format(ii) + # # Temporary variable + # moleFractionRangeTemp = moleFractionRange_y1[tempDictKey] + # arraySimResponseTemp = np.zeros([moleFractionRangeTemp.shape[0], + # sensorID.shape[0]]) + # firstDerivativeSimResponse_y12Temp = np.zeros([moleFractionRangeTemp.shape[0], + # sensorID.shape[0]]) + # firstDerivativeSimResponse_y13Temp = np.zeros([moleFractionRangeTemp.shape[0], + # sensorID.shape[0]]) + + # # Loop through all mole fractions (y1 and y2) to get sensor response + # for kk in range(moleFractionRangeTemp.shape[0]): + # arraySimResponseTemp[kk,:] = simulateSensorArray(sensorID, pressureTotal, + # temperature, np.array([moleFractionRangeTemp[kk,:]])) * multiplierError + + # # Loop through all sensor responses to get the derivative + # for jj in range(arraySimResponseTemp.shape[1]): + # firstDerivativeSimResponse_y12Temp[:,jj] = np.gradient(moleFractionRangeTemp[:,1], + # arraySimResponseTemp[:,jj]) + + # firstDerivativeSimResponse_y13Temp[:,jj] = np.gradient(moleFractionRangeTemp[:,2], + # arraySimResponseTemp[:,jj]) + + # # Save the sensor responseand the derivative to a dictionary + # arraySimResponse_y1[tempDictKey] = arraySimResponseTemp + # firstDerivativeSimResponse_y12[tempDictKey] = firstDerivativeSimResponse_y12Temp + # firstDerivativeSimResponse_y13[tempDictKey] = firstDerivativeSimResponse_y13Temp + # arraySimResponseTemp = [] + # firstDerivativeSimResponse_y12Temp = [] + # firstDerivativeSimResponse_y13Temp = [] + +# Plot the sensor response and the derivative +# Two gases +if numberOfGases == 2: + fig = plt.figure + # Parse out mole fraction and sensor response + arraySimResponseTemp = arraySimResponse['y3_0'] + moleFractionRangeTemp = moleFractionRange['y3_0'] + firstDerivativeSimResponse_y1Temp = firstDerivativeSimResponse_y1['y3_0'] + conditionNumber_y1Temp = conditionNumber_y1['y3_0'] + + ax1 = plt.subplot(1,3,1) + # Loop through all sensors and plot the sensor response + for kk in range(arraySimResponseTemp.shape[1]): + ax1.plot(arraySimResponseTemp[:,kk],moleFractionRangeTemp[:,0], + color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response + ax1.set(ylabel='$y_1$ [-]', + xlabel='$m_i$ [g kg$^{-1}$]', + ylim = [0,1], xlim = [0, 1.1*np.max(arraySimResponseTemp)]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.legend() + + ax2 = plt.subplot(1,3,2) + # Loop through all sensors and plot the sensor derivative + for kk in range(arraySimResponseTemp.shape[1]): + ax2.plot(arraySimResponseTemp[:,kk],conditionNumber_y1Temp[:,kk], + color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response + ax2.set(xlabel='$m_i$ [g kg$^{-1}$]', + ylabel='$\chi$ [-]', + xlim = [0,1.1*np.max(arraySimResponseTemp)]) + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + ax2.legend() + + ax3 = plt.subplot(1,3,3) + # Loop through all sensors and plot the sensor derivative + for kk in range(arraySimResponseTemp.shape[1]): + ax3.plot(moleFractionRangeTemp[:,0],conditionNumber_y1Temp[:,kk], + color='#'+colorsForPlot[kk], label = '$s_'+str(kk+1)+'$') # Simulated Response + ax3.set(xlabel='$y_1$ [-]', + ylabel='$\chi$ [-]', + xlim = [0,1]) + ax3.locator_params(axis="x", nbins=4) + ax3.locator_params(axis="y", nbins=4) + ax3.legend() + + plt.show() + +# Three gases +elif numberOfGases == 3: + # Sensor response - y3 + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + currentMin = np.Inf + currentMax = -np.Inf + for ii in range(len(moleFractionRange)-1): + tempDictKey = 'y3_{}'.format(ii) + arraySimResponseTemp = arraySimResponse[tempDictKey] + responseMin = min(arraySimResponseTemp) + responseMax = max(arraySimResponseTemp) + currentMin = min([currentMin,responseMin]) + currentMax = max([currentMax,responseMax]) + + for ii in range(len(moleFractionRange)-1): + tempDictKey = 'y3_{}'.format(ii) + moleFractionRangeTemp = moleFractionRange[tempDictKey] + arraySimResponseTemp = arraySimResponse[tempDictKey] + if ii == len(moleFractionRange)-2: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=arraySimResponseTemp, + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + colorbar = True, colormap=plt.cm.PuOr) + else: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=arraySimResponseTemp, + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + moleFractionRangeTemp = [] + arraySimResponseTemp = [] + + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + tax.show() + + # Sensor response - y2 + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + currentMin = np.Inf + currentMax = -np.Inf + for ii in range(len(moleFractionRange_y2)-1): + tempDictKey = 'y2_{}'.format(ii) + arraySimResponseTemp = arraySimResponse_y2[tempDictKey] + responseMin = min(arraySimResponseTemp) + responseMax = max(arraySimResponseTemp) + currentMin = min([currentMin,responseMin]) + currentMax = max([currentMax,responseMax]) + + for ii in range(len(moleFractionRange)-1): + tempDictKey = 'y2_{}'.format(ii) + moleFractionRangeTemp = moleFractionRange_y2[tempDictKey] + arraySimResponseTemp = arraySimResponse_y2[tempDictKey] + if ii == len(moleFractionRange)-2: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=arraySimResponseTemp, + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + colorbar = True, colormap=plt.cm.PuOr) + else: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=arraySimResponseTemp, + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + moleFractionRangeTemp = [] + arraySimResponseTemp = [] + + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + tax.show() + + # # Sensor response - y2 + # fig, tax = ternary.figure(scale=1) + # fig.set_size_inches(4,3.3) + # tax.boundary(linewidth=1.0) + # tax.gridlines(multiple=.2, color="gray") + # currentMin = np.Inf + # currentMax = -np.Inf + # for ii in range(len(moleFractionRange_y1)-1): + # tempDictKey = 'y1_{}'.format(ii) + # arraySimResponseTemp = arraySimResponse_y1[tempDictKey] + # responseMin = min(arraySimResponseTemp) + # responseMax = max(arraySimResponseTemp) + # currentMin = min([currentMin,responseMin]) + # currentMax = max([currentMax,responseMax]) + + # for ii in range(len(moleFractionRange)-1): + # tempDictKey = 'y1_{}'.format(ii) + # moleFractionRangeTemp = moleFractionRange_y1[tempDictKey] + # arraySimResponseTemp = arraySimResponse_y1[tempDictKey] + # if ii == len(moleFractionRange)-2: + # tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=arraySimResponseTemp, + # vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + # colorbar = True, colormap=plt.cm.PuOr) + # else: + # tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=arraySimResponseTemp, + # vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + # moleFractionRangeTemp = [] + # arraySimResponseTemp = [] + + # tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + # tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + # tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + # tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + # offset=0.035,clockwise=True,fontsize=10) + # tax.clear_matplotlib_ticks() + # tax._redraw_labels() + # plt.axis('off') + # tax.show() + + # Derivative - dy1/dm + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + currentMin = np.Inf + currentMax = -np.Inf + for ii in range(len(moleFractionRange)-1): + tempDictKey = 'y3_{}'.format(ii) + firstDerivativeSimResponse_y1Temp = firstDerivativeSimResponse_y1[tempDictKey] + firstDerivativeMin = min(firstDerivativeSimResponse_y1Temp[:,0]) + firstDerivativeMax = max(firstDerivativeSimResponse_y1Temp[:,0]) + currentMin = min([currentMin,firstDerivativeMin]) + currentMax = max([currentMax,firstDerivativeMax]) + + for ii in range(len(moleFractionRange)-1): + tempDictKey = 'y3_{}'.format(ii) + moleFractionRangeTemp = moleFractionRange[tempDictKey] + firstDerivativeSimResponse_y1Temp = firstDerivativeSimResponse_y1[tempDictKey] + if ii == len(moleFractionRange)-2: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y1Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + colorbar = True, colormap=plt.cm.PuOr) + else: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y1Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + moleFractionRangeTemp = [] + firstDerivativeSimResponse_y1Temp = [] + + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + tax.show() + + # Derivative - dy2/dm + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + currentMin = np.Inf + currentMax = -np.Inf + for ii in range(len(moleFractionRange)-1): + tempDictKey = 'y3_{}'.format(ii) + firstDerivativeSimResponse_y2Temp = firstDerivativeSimResponse_y2[tempDictKey] + firstDerivativeMin = min(firstDerivativeSimResponse_y2Temp[:,0]) + firstDerivativeMax = max(firstDerivativeSimResponse_y2Temp[:,0]) + currentMin = min([currentMin,firstDerivativeMin]) + currentMax = max([currentMax,firstDerivativeMax]) + + for ii in range(len(moleFractionRange)-1): + tempDictKey = 'y3_{}'.format(ii) + moleFractionRangeTemp = moleFractionRange[tempDictKey] + firstDerivativeSimResponse_y2Temp = firstDerivativeSimResponse_y2[tempDictKey] + if ii == len(moleFractionRange)-2: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y2Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + colorbar = True, colormap=plt.cm.PuOr) + else: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y2Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + moleFractionRangeTemp = [] + firstDerivativeSimResponse_y2Temp = [] + + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + tax.show() + + # Derivative - dy3/dm + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + currentMin = np.Inf + currentMax = -np.Inf + for ii in range(len(moleFractionRange_y2)-1): + tempDictKey = 'y2_{}'.format(ii) + firstDerivativeSimResponse_y3Temp = firstDerivativeSimResponse_y3[tempDictKey] + firstDerivativeMin = min(firstDerivativeSimResponse_y3Temp[:,0]) + firstDerivativeMax = max(firstDerivativeSimResponse_y3Temp[:,0]) + currentMin = min([currentMin,firstDerivativeMin]) + currentMax = max([currentMax,firstDerivativeMax]) + + for ii in range(len(moleFractionRange_y2)-1): + tempDictKey = 'y2_{}'.format(ii) + moleFractionRangeTemp = moleFractionRange_y2[tempDictKey] + firstDerivativeSimResponse_y3Temp = firstDerivativeSimResponse_y3[tempDictKey] + if ii == len(moleFractionRange)-2: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y3Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + colorbar = True, colormap=plt.cm.PuOr) + else: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y3Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + moleFractionRangeTemp = [] + firstDerivativeSimResponse_y3Temp = [] + + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + tax.show() + + # Derivative - dy3/dm + fig, tax = ternary.figure(scale=1) + fig.set_size_inches(4,3.3) + tax.boundary(linewidth=1.0) + tax.gridlines(multiple=.2, color="gray") + currentMin = np.Inf + currentMax = -np.Inf + for ii in range(len(moleFractionRange_y2)-1): + tempDictKey = 'y2_{}'.format(ii) + firstDerivativeSimResponse_y31Temp = firstDerivativeSimResponse_y31[tempDictKey] + firstDerivativeMin = min(firstDerivativeSimResponse_y31Temp[:,0]) + firstDerivativeMax = max(firstDerivativeSimResponse_y31Temp[:,0]) + currentMin = min([currentMin,firstDerivativeMin]) + currentMax = max([currentMax,firstDerivativeMax]) + + for ii in range(len(moleFractionRange_y2)-1): + tempDictKey = 'y2_{}'.format(ii) + moleFractionRangeTemp = moleFractionRange_y2[tempDictKey] + firstDerivativeSimResponse_y31Temp = firstDerivativeSimResponse_y31[tempDictKey] + if ii == len(moleFractionRange)-2: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y31Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + colorbar = True, colormap=plt.cm.PuOr) + else: + tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y31Temp[:,0], + vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + moleFractionRangeTemp = [] + firstDerivativeSimResponse_y31Temp = [] + + tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + offset=0.035,clockwise=True,fontsize=10) + tax.clear_matplotlib_ticks() + tax._redraw_labels() + plt.axis('off') + tax.show() + + # # Derivative - dy3/dm + # fig, tax = ternary.figure(scale=1) + # fig.set_size_inches(4,3.3) + # tax.boundary(linewidth=1.0) + # tax.gridlines(multiple=.2, color="gray") + # currentMin = np.Inf + # currentMax = -np.Inf + # for ii in range(len(moleFractionRange_y1)-1): + # tempDictKey = 'y1_{}'.format(ii) + # firstDerivativeSimResponse_y12Temp = firstDerivativeSimResponse_y12[tempDictKey] + # firstDerivativeMin = min(firstDerivativeSimResponse_y12Temp[:,0]) + # firstDerivativeMax = max(firstDerivativeSimResponse_y12Temp[:,0]) + # currentMin = min([currentMin,firstDerivativeMin]) + # currentMax = max([currentMax,firstDerivativeMax]) + + # for ii in range(len(moleFractionRange_y1)-1): + # tempDictKey = 'y1_{}'.format(ii) + # moleFractionRangeTemp = moleFractionRange_y1[tempDictKey] + # firstDerivativeSimResponse_y12Temp = firstDerivativeSimResponse_y12[tempDictKey] + # if ii == len(moleFractionRange)-2: + # tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y12Temp[:,0], + # vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + # colorbar = True, colormap=plt.cm.PuOr) + # else: + # tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y12Temp[:,0], + # vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + # moleFractionRangeTemp = [] + # firstDerivativeSimResponse_y12Temp = [] + + # tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + # tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + # tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + # tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + # offset=0.035,clockwise=True,fontsize=10) + # tax.clear_matplotlib_ticks() + # tax._redraw_labels() + # plt.axis('off') + # tax.show() + + # # Derivative - dy3/dm + # fig, tax = ternary.figure(scale=1) + # fig.set_size_inches(4,3.3) + # tax.boundary(linewidth=1.0) + # tax.gridlines(multiple=.2, color="gray") + # currentMin = np.Inf + # currentMax = -np.Inf + # for ii in range(len(moleFractionRange_y1)-1): + # tempDictKey = 'y1_{}'.format(ii) + # firstDerivativeSimResponse_y13Temp = firstDerivativeSimResponse_y13[tempDictKey] + # firstDerivativeMin = min(firstDerivativeSimResponse_y13Temp[:,0]) + # firstDerivativeMax = max(firstDerivativeSimResponse_y13Temp[:,0]) + # currentMin = min([currentMin,firstDerivativeMin]) + # currentMax = max([currentMax,firstDerivativeMax]) + + # for ii in range(len(moleFractionRange_y1)-1): + # tempDictKey = 'y1_{}'.format(ii) + # moleFractionRangeTemp = moleFractionRange_y1[tempDictKey] + # firstDerivativeSimResponse_y13Temp = firstDerivativeSimResponse_y13[tempDictKey] + # if ii == len(moleFractionRange)-2: + # tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y13Temp[:,0], + # vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr, + # colorbar = True, colormap=plt.cm.PuOr) + # else: + # tax.scatter(moleFractionRangeTemp, marker='o', s=2, c=firstDerivativeSimResponse_y13Temp[:,0], + # vmin=currentMin,vmax=currentMax,cmap=plt.cm.PuOr) + + # moleFractionRangeTemp = [] + # firstDerivativeSimResponse_y13Temp = [] + + # tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) + # tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) + # tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) + # tax.ticks(axis='lbr', linewidth=1, multiple=0.2, tick_formats="%.1f", + # offset=0.035,clockwise=True,fontsize=10) + # tax.clear_matplotlib_ticks() + # tax._redraw_labels() + # plt.axis('off') + # tax.show() \ No newline at end of file From b5a994fe3154bcce26ef52a2dce7094cc89f23ab Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 8 Jan 2021 18:28:06 +0000 Subject: [PATCH 50/99] Commit after a long time! Major changes (hopefully not critical) --- plotFunctions/plotConcentrationViolinPlots.py | 21 +-- plotFunctions/plotObjectiveFunction.py | 121 ++++++++++++++---- 2 files changed, 108 insertions(+), 34 deletions(-) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py index a23b9e1..5b10ac8 100755 --- a/plotFunctions/plotConcentrationViolinPlots.py +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -67,7 +67,7 @@ legendFlag = False # Sensor ID -sensorText = ["12/13/14", "3/4/1", "2/6/8", "0/1/2"] +sensorText = ["y1", "y2", "y3", "0/1/2"] # Initialize x, y, and type for the plotting concatenatedX = [] @@ -79,9 +79,10 @@ concatenatedType = [] # File to be loaded for the left of violin plot -loadFileName = ["sensitivityAnalysis_0-6-5_20201127_1217_50a3ed7.npz", - "sensitivityAnalysis_3-4-1_20201127_1220_50a3ed7.npz"] -saveFileSensorText = [17,15,16,6] +loadFileName = ["sensitivityAnalysis_3-4-1_20201204_1710_50a3ed7.npz", + "sensitivityAnalysis_3-4-1_20201204_1831_50a3ed7.npz"] + +saveFileSensorText = [3,4,1] if flagComparison and len(loadFileName) != 2: errorString = "When flagComparison is True, only two files can be loaded for comparison." @@ -146,7 +147,7 @@ concatenatedX3 = concatenatedX3 + x3Var concatenatedType = concatenatedType + typeVar - + # Reinitialize all the loaded values to empty variable simResultsFile = [] resultOutput = [] @@ -254,7 +255,7 @@ palette = colorForPlot[0:len(loadFileName)]) ax2.set(xlabel='$y_1$ [-]', ylabel='$CV (\hat{y}_1)$ [-]', - xlim = [0.,1.], ylim = [1e-5,1.]) + xlim = [0.,1.], ylim = [1e-5,None]) ax2.locator_params(axis="x", nbins=4) ax2.set_yscale('log') if not legendFlag: @@ -281,7 +282,7 @@ palette = colorForPlot[0:len(loadFileName)]) ax1.set(xlabel='$y_1$ [-]', ylabel='$CV (\hat{y}_1)$ [-]', - xlim = [0.,1.], ylim = [1e-5,1.]) + xlim = [0.,1.], ylim = [1e-8,100.]) ax1.locator_params(axis="x", nbins=4) ax1.set_yscale('log') if not legendFlag: @@ -293,7 +294,7 @@ palette = colorForPlot[0:len(loadFileName)]) ax2.set(xlabel='$y_2$ [-]', ylabel='$CV (\hat{y}_2)$ [-]', - xlim = [0.,1.], ylim = [1e-5,1.]) + xlim = [0.,1.], ylim = [1e-8,100.]) ax2.locator_params(axis="x", nbins=4) ax2.set_yscale('log') if not legendFlag: @@ -303,9 +304,9 @@ sns.lineplot(data=cvData, x='x3', y='y3', hue='dataType', style='dataType', dashes = False, markers = ['o']*len(loadFileName), palette = colorForPlot[0:len(loadFileName)]) - ax3.set(xlabel='$y_1$ [-]', + ax3.set(xlabel='$y_3$ [-]', ylabel='$CV (\hat{y}_3)$ [-]', - xlim = [0.,1.], ylim = [1e-5,1.]) + xlim = [-0.1,1], ylim = [1e-8,100.]) ax3.locator_params(axis="x", nbins=4) ax3.set_yscale('log') if not legendFlag: diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index a0922aa..8783076 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2020-11-27, AK: More plotting fix for 3 gas system # - 2020-11-24, AK: Fix for 3 gas system # - 2020-11-23, AK: Change ternary plots # - 2020-11-20, AK: Introduce ternary plots @@ -28,7 +29,7 @@ # # ############################################################################ - +import pdb import numpy as np from numpy import load from kneed import KneeLocator # To compute the knee/elbow of a curve @@ -55,6 +56,7 @@ # Plotting colors colorsForPlot = ["ff499e","d264b6","a480cf","779be7","49b6ff"] +colorsForPlot = ["eac435","345995","03cea4","fb4d3d","ca1551"] colorGroup = ["#f94144","#43aa8b"] colorIntersection = ["ff595e","ffca3a","8ac926","1982c4","6a4c93"] @@ -69,31 +71,49 @@ temperature = np.array([298.15]); # Number of Adsorbents -numberOfAdsorbents = 30 +numberOfAdsorbents = 20 # Number of Gases -numberOfGases = 3 +numberOfGases = 2 + # Experimental mole fraction for 3 gases if numberOfGases == 3: - inputMoleFracALL = np.array([[0.05, 0.95, 0.00], - [0.20, 0.80, 0.00], - [0.35, 0.65, 0.00], - [0.50, 0.50, 0.00], - [0.75, 0.25, 0.00], - [0.90, 0.10, 0.00]]) + inputMoleFracALL = np.array([[0.01, 0.99, 0.00], + [0.10, 0.90, 0.00], + [0.25, 0.75, 0.00], + [0.50, 0.50, 0.00], + [0.75, 0.25, 0.00], + [0.90, 0.10, 0.00], + [0.99, 0.01, 0.00]]) + # inputMoleFracALL1 = np.array([[0.01, 0.59, 0.40], + # [0.10, 0.50, 0.40], + # [0.20, 0.40, 0.40], + # [0.30, 0.30, 0.40], + # [0.40, 0.20, 0.40], + # [0.50, 0.10, 0.40], + # [0.59, 0.01, 0.40]]) + inputMoleFracALL1 = np.array([[0.01, 0.19, 0.80], + [0.05, 0.15, 0.80], + [0.10, 0.10, 0.80], + [0.15, 0.05, 0.80], + [0.19, 0.01, 0.80]]) + inputMoleFracALL2 = np.array([[0.01, 0.09, 0.90], + [0.05, 0.05, 0.90], + [0.09, 0.01, 0.90]]) + # Fix one gas - fixOneGas = True + fixOneGas = False # Third gas mole fraction - thirdGasMoleFrac = 0.00 + thirdGasMoleFrac = 0. # Mole Fraction of interest -moleFrac = [0.1, 0.9] +moleFrac = [0.3, 0.7] # Multiplier Error -multiplierError = [1., 1., 1.] +multiplierError = [1, 1.] # Sensor ID -sensorID = np.array([0,4,1]) +sensorID = np.array([6,5]) # Acceptable SNR signalToNoise = 25*0.1 @@ -138,22 +158,26 @@ if numberOfGases == 2 or (numberOfGases == 3 and fixOneGas == True): xFill = np.zeros([arraySimResponse.shape[1],2]) # Loop through all sensors + firstDerivative = np.zeros([arraySimResponse.shape[0],arraySimResponse.shape[1]]) + firstDerivativeSimResponse_y1 = np.zeros([moleFractionRange.shape[0],arraySimResponse.shape[1]]) + firstDerivativeSimResponse_y2 = np.zeros([moleFractionRange.shape[0],arraySimResponse.shape[1]]) + secondDerivative = np.zeros([firstDerivative.shape[0],firstDerivative.shape[1]]) for kk in range(arraySimResponse.shape[1]): - firstDerivative = np.zeros([arraySimResponse.shape[0],1]) - firstDerivative[:,0] = np.gradient(arraySimResponse[:,kk]) - secondDerivative = np.zeros([firstDerivative.shape[0],1]) - secondDerivative[:,0] = np.gradient(firstDerivative[:,0]) + firstDerivative[:,kk] = np.gradient(arraySimResponse[:,kk],moleFractionRange[1,0]-moleFractionRange[0,0]) + secondDerivative[:,kk] = np.gradient(firstDerivative[:,kk],moleFractionRange[1,0]-moleFractionRange[0,0]) + firstDerivativeSimResponse_y1[:,kk] = np.gradient(moleFractionRange[:,0],arraySimResponse[:,kk]) + firstDerivativeSimResponse_y2[:,kk] = np.gradient(moleFractionRange[:,1],arraySimResponse[:,kk]) # Get the sign of the first derivative for increasing/decreasing - if all(i >= 0. for i in firstDerivative[:,0]): + if all(i >= 0. for i in firstDerivative[:,kk]): slopeDir = "increasing" - elif all(i < 0. for i in firstDerivative[:,0]): + elif all(i < 0. for i in firstDerivative[:,kk]): slopeDir = "decreasing" else: print("Dangerous! I should not be here!!!") # Get the sign of the second derivative for concavity/convexity - if all(i >= 0. for i in secondDerivative[:,0]): + if all(i >= 0. for i in secondDerivative[:,kk]): secondDerDir = "convex" - elif all(i < 0. for i in secondDerivative[:,0]): + elif all(i < 0. for i in secondDerivative[:,kk]): secondDerDir = "concave" else: print("Dangerous! I should not be here!!!") @@ -162,7 +186,7 @@ kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,kk], curve=secondDerDir, direction=slopeDir) elbowPoint = list(kneedle.all_elbows) - + # Obtain coordinates to fill working region if secondDerDir == "concave": if slopeDir == "increasing": @@ -305,6 +329,12 @@ tempDictKey = 's_{}'.format(ii) tax.scatter(sensitiveGroup[tempDictKey], marker='o', s=2, color = '#'+colorIntersection[ii], alpha = 0.15) + # tax.scatter(inputMoleFracALL, marker='o', s=20, + # color = '#'+colorsForPlot[0]) + # tax.scatter(inputMoleFracALL1, marker='o', s=20, + # color = '#'+colorsForPlot[1]) + # tax.scatter(inputMoleFracALL2, marker='o', s=20, + # color = '#'+colorsForPlot[2]) tax.scatter(inputMoleFracALL, marker='o', s=20, color = 'k') tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) @@ -324,4 +354,47 @@ if not os.path.exists(os.path.join('..','simulationFigures')): os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) - tax.show() \ No newline at end of file + tax.show() + +# Plot the objective function used to evaluate the concentration for individual +# sensors and the total (sum) +if numberOfGases == 3 and fixOneGas == True: + fig = plt.figure + ax = plt.subplot(1,3,1) + for kk in range(arraySimResponse.shape[1]): + ax.plot(moleFractionRange[:,0],np.power((arrayTrueResponse[:,kk] + -arraySimResponse[:,kk])/arrayTrueResponse[:,kk],2), + color='#'+colorsForPlot[kk], label = '$J_'+str(kk+1)+'$') + ax.plot(moleFractionRange[:,0],objFunction,color='#'+colorsForPlot[-1], label = '$\Sigma J_i$') # Error all sensors + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + ax.set(xlabel='$y_1$ [-]', + ylabel='$J$ [-]', + xlim = [0,1.], ylim = [0, 5]) + ax.legend() + + ax = plt.subplot(1,3,2) + for kk in range(arraySimResponse.shape[1]): + ax.plot(moleFractionRange[:,1],np.power((arrayTrueResponse[:,kk] + -arraySimResponse[:,kk])/arrayTrueResponse[:,kk],2), + color='#'+colorsForPlot[kk], label = '$J_'+str(kk+1)+'$') + ax.plot(moleFractionRange[:,1],objFunction,color='#'+colorsForPlot[-1], label = '$\Sigma J_i$') # Error all sensors + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + ax.set(xlabel='$y_2$ [-]', + ylabel='$J$ [-]', + xlim = [0,1.], ylim = [0, 5]) + + ax = plt.subplot(1,3,3) + for kk in range(arraySimResponse.shape[1]): + ax.plot(moleFractionRange[:,2],np.power((arrayTrueResponse[:,kk] + -arraySimResponse[:,kk])/arrayTrueResponse[:,kk],2), + color='#'+colorsForPlot[kk], label = '$J_'+str(kk+1)+'$') + ax.plot(moleFractionRange[:,2],objFunction,color='#'+colorsForPlot[-1], label = '$\Sigma J_i$') # Error all sensors + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + ax.set(xlabel='$y_3$ [-]', + ylabel='$J$ [-]', + xlim = [0,1.], ylim = [0, None]) + + plt.show() \ No newline at end of file From 48a38a6f9f55be6a19db4ba6094da14180cbc4dc Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 8 Jan 2021 18:38:19 +0000 Subject: [PATCH 51/99] Plot function for constrained objective --- plotFunctions/plotConstrainedObjective.py | 245 ++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 plotFunctions/plotConstrainedObjective.py diff --git a/plotFunctions/plotConstrainedObjective.py b/plotFunctions/plotConstrainedObjective.py new file mode 100644 index 0000000..2a7a284 --- /dev/null +++ b/plotFunctions/plotConstrainedObjective.py @@ -0,0 +1,245 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plots the objective function used for concentration estimation for a mole +# fraction grid. The constraint of mass conservation will show up as a line +# in the plot. The concept\visualization is similar to the one of Lagrange +# multipliers. +# +# Last modified: +# - 2021-01-08, AK: Code improvements +# - 2020-12-18, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ +def plotConstrainedObjective(): + import numpy as np + import os + import matplotlib.pyplot as plt + from mpl_toolkits.mplot3d import Axes3D + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + import auxiliaryFunctions + + os.chdir("..") + + # Save flag for figure + saveFlag = False + + # Number of molefractions + numMolFrac= 1001 + numMesh = 21 + + # Total pressure of the gas [Pa] + pressureTotal = np.array([1.e5]); + + # Temperature of the gas [K] + # Can be a vector of temperatures + temperature = np.array([298.15]); + + # Number of Adsorbents + numberOfAdsorbents = 20 + + # Number of Gases + numberOfGases = 3 + + # Mole Fraction of interest + moleFrac = np.array([[0.2,0.8,0.0], [0.6,0.4,0.0]]) + + + # Multiplier Error + multiplierError = [1,1,1] + + # Sensor ID + sensorID = np.array([3,4,1]) + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() + + # For two gases + if numberOfGases == 2: + # Obtain the objective functino for a mole fraction grid (constraint + # shown as a line int the plot) - similar to Lagrange multiplier + objFunction = np.zeros([moleFrac.shape[0],numMesh,numMesh]) + for ii in range(moleFrac.shape[0]): + moleFracTemp = moleFrac[ii,:] + x1m,x2m,objFunctionTemp = functionForMoleFrac(numberOfAdsorbents,numberOfGases,pressureTotal, + temperature,sensorID,moleFracTemp,numMolFrac, + numMesh,multiplierError) + objFunction[ii,:,:] = objFunctionTemp[:,:] + if ii == 0: + minObj = np.max([-np.inf,np.min(objFunction[ii,:,:])]) + maxObj = np.min([np.inf,np.max(objFunction[ii,:,:])]) + else: + minObj = np.max([minObj,np.min(objFunction[ii,:,:])]) + maxObj = np.min([maxObj,np.max(objFunction[ii,:,:])]) + + + fig = plt.figure + ax = plt.subplot(1,2,1) + # Plot obbjective function for first mole fraction + plt.contourf(x1m,x2m,objFunction[0,:,:],levels = np.linspace(minObj,maxObj,200),cmap='RdYlBu') + plt.colorbar() + plt.plot([0.0,1.0],[1.0,0.0],linestyle = ':', linewidth = 1, color = 'k') + ax.set(xlabel='$y_1$ [-]', + ylabel='$y_2$ [-]', + xlim = [0,1], ylim = [0, 1.]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + + ax = plt.subplot(1,2,2) + # Plot obbjective function for second mole fraction + plt.contourf(x1m,x2m,objFunction[1,:,:],levels = np.linspace(minObj,maxObj,200),cmap='RdYlBu') + plt.colorbar() + plt.plot([0.0,1.0],[1.0,0.0],linestyle = ':', linewidth = 1, color = 'k') + ax.set(xlabel='$y_1$ [-]', + ylabel='$y_2$ [-]', + xlim = [0,1], ylim = [0, 1.]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + + # For three gases + elif numberOfGases == 3: + # Obtain the objective functino for a mole fraction grid (constraint + # shown as a line int the plot) - similar to Lagrange multiplier + objFunction = np.zeros([moleFrac.shape[0],numMesh,numMesh]) + for ii in range(moleFrac.shape[0]): + moleFracTemp = moleFrac[ii,:] + x1m,x2m,x3m,objFunctionTemp = functionForMoleFrac(numberOfAdsorbents,numberOfGases,pressureTotal, + temperature,sensorID,moleFracTemp,numMolFrac, + numMesh,multiplierError) + plotInd = np.where(x1m[:,0]==moleFracTemp[2])[0][0] + objFunction[ii,:,:] = objFunctionTemp[:,:,plotInd] + if ii == 0: + minObj = np.max([-np.inf,np.min(objFunction[ii,:,:])]) + maxObj = np.min([np.inf,np.max(objFunction[ii,:,:])]) + else: + minObj = np.max([minObj,np.min(objFunction[ii,:,:])]) + maxObj = np.min([maxObj,np.max(objFunction[ii,:,:])]) + + # Plot obbjective function stacked on top of each other + fig = plt.figure() + ax = fig.gca(projection='3d') + for ii in range(moleFrac.shape[0]): + ax.contourf(x1m[:,:,plotInd],x2m[:,:,plotInd], + objFunction[ii,:,:],zdir='z',offset = ii, + levels = np.linspace(minObj,maxObj,30), + cmap='RdYlBu') + + # Set 3D-axis-limits: + ax.set_xlim3d(0,1) + ax.set_ylim3d(0,1) + ax.set_zlim3d(0,moleFrac.shape[0]) + plt.show() + + fig = plt.figure + ax = plt.subplot(1,2,1) + # Plot obbjective function for first mole fraction + plt.contourf(x1m[:,:,0],x2m[:,:,0],objFunction[0,:,:],levels = np.linspace(minObj,maxObj,200),cmap='RdYlBu') + plt.colorbar() + plt.plot([0.0,1-moleFrac[0,2]],[1-moleFrac[0,2],0.0],linestyle = ':', linewidth = 1, color = 'k') + ax.set(xlabel='$y_1$ [-]', + ylabel='$y_2$ [-]', + xlim = [0,1-moleFrac[0,2]], ylim = [0., 1.-moleFrac[0,2]]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + + ax = plt.subplot(1,2,2) + # Plot obbjective function for second mole fraction + plt.contourf(x1m[:,:,0],x2m[:,:,0],objFunction[1,:,:],levels = np.linspace(minObj,maxObj,200),cmap='RdYlBu') + plt.colorbar() + plt.plot([0.0,1-moleFrac[1,2]],[1-moleFrac[1,2],0.0],linestyle = ':', linewidth = 1, color = 'k') + ax.set(xlabel='$y_1$ [-]', + ylabel='$y_2$ [-]', + xlim = [0,1-moleFrac[1,2]], ylim = [0, 1-moleFrac[1,2]]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + +# Function to evaluate the objective function for a given mole fraction +def functionForMoleFrac(numberOfAdsorbents,numberOfGases,pressureTotal, + temperature,sensorID,moleFrac,numMolFrac, + numMesh,multiplierError): + import numpy as np + from generateTrueSensorResponse import generateTrueSensorResponse + from simulateSensorArray import simulateSensorArray + # Simulate the sensor response for all possible concentrations + if numberOfGases == 2: + x1 = np.linspace(0,1,numMesh) + x2 = np.linspace(0,1,numMesh) + x1m, x2m = np.meshgrid(x1, x2, sparse=False, indexing='ij') + arraySimResponse = np.zeros([len(sensorID),numMesh,numMesh]) + elif numberOfGases == 3: + x1 = np.linspace(0,1,numMesh) + x2 = np.linspace(0,1,numMesh) + x3 = np.linspace(0,1,numMesh) + x1m, x2m, x3m = np.meshgrid(x1, x2, x3, sparse=False, indexing='ij') + arraySimResponse = np.zeros([len(sensorID),numMesh,numMesh,numMesh]) + + # Get simulated sensor response + if numberOfGases == 2: + for ii in range(numMesh): + for jj in range(numMesh): + arraySimResponseTemp = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([[x1m[ii,jj],x2m[ii,jj]]])) * multiplierError + for kk in range(len(sensorID)): + arraySimResponse[kk,ii,jj] = arraySimResponseTemp[kk] + elif numberOfGases == 3: + for ii in range(numMesh): + for jj in range(numMesh): + for kk in range(numMesh): + arraySimResponseTemp = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([[x1m[ii,jj,kk],x2m[ii,jj,kk],x3m[ii,jj,kk]]])) * multiplierError + for ll in range(len(sensorID)): + arraySimResponse[ll,ii,jj,kk] = arraySimResponseTemp[ll] + + + # Get the individual sensor reponse for all the given "experimental/test" concentrations + sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, + pressureTotal,temperature,moleFraction = moleFrac) + # Parse out the true sensor response for the desired sensors in the array + arrayTrueResponseTemp = np.zeros(len(sensorID)) + for ii in range(len(sensorID)): + arrayTrueResponseTemp[ii] = sensorTrueResponse[sensorID[ii]]*multiplierError[ii] + if numberOfGases == 2: + arrayTrueResponse = np.zeros([len(sensorID),numMesh,numMesh]) + for kk in range(len(sensorID)): + arrayTrueResponse[kk,:,:] = np.tile(arrayTrueResponseTemp[kk],(numMesh,numMesh)) + elif numberOfGases == 3: + arrayTrueResponse = np.zeros([len(sensorID),numMesh,numMesh,numMesh]) + for ll in range(len(sensorID)): + arrayTrueResponse[ll,:,:,:] = np.tile(arrayTrueResponseTemp[ll],(numMesh,numMesh,numMesh)) + + # Compute the objective function over all the mole fractions + if numberOfGases == 2: + objFunction = np.zeros([numMesh,numMesh]) + for kk in range(len(sensorID)): + objFunctionTemp = np.power(np.divide(arrayTrueResponse[kk,:,:] - arraySimResponse[kk,:,:],arrayTrueResponse[kk,:,:]),2) + objFunction += objFunctionTemp + objFunctionTemp = [] + elif numberOfGases == 3: + objFunction = np.zeros([numMesh,numMesh,numMesh]) + for kk in range(len(sensorID)): + objFunctionTemp = np.power(np.divide(arrayTrueResponse[kk,:,:,:] - arraySimResponse[kk,:,:,:],arrayTrueResponse[kk,:,:,:]),2) + objFunction += objFunctionTemp + objFunctionTemp = [] + + # Function return + if numberOfGases == 2: + return x1m,x2m,objFunction + elif numberOfGases == 3: + return x1m,x2m,x3m,objFunction \ No newline at end of file From 1d23291cb37ffd775c2683b07074561c8562a194 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 12 Jan 2021 20:23:15 +0000 Subject: [PATCH 52/99] Cosmetic changes in plot functions --- plotFunctions/plotConcentrationViolinPlots.py | 10 +++-- plotFunctions/plotObjectiveFunction.py | 45 +++++++------------ 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/plotFunctions/plotConcentrationViolinPlots.py b/plotFunctions/plotConcentrationViolinPlots.py index 5b10ac8..65d496c 100755 --- a/plotFunctions/plotConcentrationViolinPlots.py +++ b/plotFunctions/plotConcentrationViolinPlots.py @@ -12,6 +12,7 @@ # Plots to visualize different sensor responses # # Last modified: +# - 2021-01-11, AK: Cosmetic changes # - 2020-11-27, AK: Major modification for 3 gas system # - 2020-11-23, AK: Add standard deviation/CV plotting # - 2020-11-20, AK: Introduce 3 gas capability @@ -79,8 +80,9 @@ concatenatedType = [] # File to be loaded for the left of violin plot -loadFileName = ["sensitivityAnalysis_3-4-1_20201204_1710_50a3ed7.npz", - "sensitivityAnalysis_3-4-1_20201204_1831_50a3ed7.npz"] +loadFileName = ["sensitivityAnalysis_6-12-55_20210112_1859_50a3ed7.npz", + "sensitivityAnalysis_6-12-55_20210112_1924_50a3ed7.npz", + "sensitivityAnalysis_6-12-55_20210112_1939_50a3ed7.npz"] saveFileSensorText = [3,4,1] @@ -239,7 +241,7 @@ palette = colorForPlot[0:len(loadFileName)]) ax1.set(xlabel='$y_1$ [-]', ylabel='$\sigma (\hat{y}_1)$ [-]', - xlim = [0.,1.], ylim = [1e-6,1.]) + xlim = [0.,1.], ylim = [1e-10,1.]) ax1.set_yscale('log') ax1.locator_params(axis="x", nbins=4) if len(loadFileName) > 1: @@ -255,7 +257,7 @@ palette = colorForPlot[0:len(loadFileName)]) ax2.set(xlabel='$y_1$ [-]', ylabel='$CV (\hat{y}_1)$ [-]', - xlim = [0.,1.], ylim = [1e-5,None]) + xlim = [0.,1.], ylim = [1e-10,None]) ax2.locator_params(axis="x", nbins=4) ax2.set_yscale('log') if not legendFlag: diff --git a/plotFunctions/plotObjectiveFunction.py b/plotFunctions/plotObjectiveFunction.py index 8783076..4145a2d 100755 --- a/plotFunctions/plotObjectiveFunction.py +++ b/plotFunctions/plotObjectiveFunction.py @@ -12,6 +12,7 @@ # Plots the objective function used for concentration estimation # # Last modified: +# - 2021-01-11, AK: Cosmetic changes # - 2020-11-27, AK: More plotting fix for 3 gas system # - 2020-11-24, AK: Fix for 3 gas system # - 2020-11-23, AK: Change ternary plots @@ -61,7 +62,7 @@ colorIntersection = ["ff595e","ffca3a","8ac926","1982c4","6a4c93"] # Number of molefractions -numMolFrac= 1001 +numMolFrac= 10001 # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -71,10 +72,10 @@ temperature = np.array([298.15]); # Number of Adsorbents -numberOfAdsorbents = 20 +numberOfAdsorbents = 60 # Number of Gases -numberOfGases = 2 +numberOfGases = 3 # Experimental mole fraction for 3 gases if numberOfGases == 3: @@ -85,21 +86,6 @@ [0.75, 0.25, 0.00], [0.90, 0.10, 0.00], [0.99, 0.01, 0.00]]) - # inputMoleFracALL1 = np.array([[0.01, 0.59, 0.40], - # [0.10, 0.50, 0.40], - # [0.20, 0.40, 0.40], - # [0.30, 0.30, 0.40], - # [0.40, 0.20, 0.40], - # [0.50, 0.10, 0.40], - # [0.59, 0.01, 0.40]]) - inputMoleFracALL1 = np.array([[0.01, 0.19, 0.80], - [0.05, 0.15, 0.80], - [0.10, 0.10, 0.80], - [0.15, 0.05, 0.80], - [0.19, 0.01, 0.80]]) - inputMoleFracALL2 = np.array([[0.01, 0.09, 0.90], - [0.05, 0.05, 0.90], - [0.09, 0.01, 0.90]]) # Fix one gas fixOneGas = False @@ -110,10 +96,11 @@ moleFrac = [0.3, 0.7] # Multiplier Error -multiplierError = [1, 1.] +multiplierError = [1, 1.,1.] # Sensor ID -sensorID = np.array([6,5]) +sensorID = np.array([6,12,55]) +saveFileSensorText = [6,12,55] # Acceptable SNR signalToNoise = 25*0.1 @@ -329,14 +316,14 @@ tempDictKey = 's_{}'.format(ii) tax.scatter(sensitiveGroup[tempDictKey], marker='o', s=2, color = '#'+colorIntersection[ii], alpha = 0.15) - # tax.scatter(inputMoleFracALL, marker='o', s=20, - # color = '#'+colorsForPlot[0]) - # tax.scatter(inputMoleFracALL1, marker='o', s=20, - # color = '#'+colorsForPlot[1]) - # tax.scatter(inputMoleFracALL2, marker='o', s=20, - # color = '#'+colorsForPlot[2]) tax.scatter(inputMoleFracALL, marker='o', s=20, - color = 'k') + color = '#'+colorsForPlot[0]) + inputMoleFracALL[:,[1,2]]=inputMoleFracALL[:,[2,1]] + tax.scatter(inputMoleFracALL, marker='o', s=20, + color = '#'+colorsForPlot[1]) + inputMoleFracALL[:,[0,1]]=inputMoleFracALL[:,[1,0]] + tax.scatter(inputMoleFracALL, marker='o', s=20, + color = '#'+colorsForPlot[2]) tax.left_axis_label("$y_2$ [-]",offset=0.20,fontsize=10) tax.right_axis_label("$y_1$ [-]",offset=0.20,fontsize=10) tax.bottom_axis_label("$y_3$ [-]",offset=0.20,fontsize=10) @@ -346,8 +333,8 @@ tax._redraw_labels() plt.axis('off') if saveFlag: - # FileName: SensorResponse___ - sensorText = str(sensorID[ii]).replace('[','').replace(']','').replace(' ','-') + # FileName: SensorRegion___ + sensorText = str(saveFileSensorText).replace('[','').replace(']','').replace(' ','-') saveFileName = "SensorRegion_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('simulationFigures',saveFileName) # Check if inputResources directory exists or not. If not, create the folder From 9d4df4a5b8ab163e2e5c2839e51aa43adb2b85ec Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 20 Jan 2021 11:06:29 +0000 Subject: [PATCH 53/99] First version of the full sensor model with kinetics --- simulateFullModel.py | 190 +++++++++++++++++++++++++++++++++++++++++ simulateSensorArray.py | 12 ++- 2 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 simulateFullModel.py diff --git a/simulateFullModel.py b/simulateFullModel.py new file mode 100644 index 0000000..8033f49 --- /dev/null +++ b/simulateFullModel.py @@ -0,0 +1,190 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Simulates the sensor chamber as a CSTR incorporating kinetic effects +# +# Last modified: +# - 2021-01-19, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def simulateFullModel(**kwargs): + import numpy as np + from scipy.integrate import solve_ivp + from simulateSensorArray import simulateSensorArray + import matplotlib.pyplot as plt + import os + + # Sensor ID + if 'sensorID' in kwargs: + sensorID = np.array(kwargs["sensorID"]) + else: + sensorID = np.array([6]) + + # Kinetic rate constants [/s] + if 'rateConstant' in kwargs: + rateConstant = np.array(kwargs["rateConstant"]) + else: + # Equilibrium process + rateConstant = np.array([0.01,0.01]) + + # Feed flow rate [m3/s] + if 'flowIn' in kwargs: + flowIn = np.array(kwargs["flowIn"]) + else: + # Equilibrium process + flowIn = np.array([5e-7]) + + # Feed Mole Fraction [-] + if 'feedMoleFrac' in kwargs: + feedMoleFrac = np.array(kwargs["feedMoleFrac"]) + else: + # Equilibrium process + feedMoleFrac = np.array([1.,0.]) + + # Initial Gas Mole Fraction [-] + if 'initMoleFrac' in kwargs: + initMoleFrac = np.array(kwargs["initMoleFrac"]) + else: + # Equilibrium process + initMoleFrac = np.array([0.,1.]) + + if (len(feedMoleFrac) != len(initMoleFrac) + or len(feedMoleFrac) != len(rateConstant)): + raise Exception("The dimensions of the mole fraction or rate constant and the number of gases in the adsorbent is not consistent!") + else: + numberOfGases = len(feedMoleFrac) + + # Time span for integration [s] + timeInt = (0.0,1000) + + # Total pressure of the gas [Pa] + pressureTotal = np.array([1.e5]); + + # Temperature of the gas [K] + # Can be a vector of temperatures + temperature = np.array([298.15]); + + # Volume of sorbent material [m3] + volSorbent = 5e-7 + + # Volume of gas chamber (dead volume) [m3] + volGas = 5e-7 + + + # Compute the initial sensor loading [mol/m3] @ initMoleFrac + _ , sensorLoadingPerGasVol = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([initMoleFrac]), + fullModel = True) + + inputParameters = (sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, + pressureTotal, temperature, volSorbent, volGas) + + # Prepare initial conditions vector + initialConditions = np.zeros([2*numberOfGases]) + initialConditions[0:numberOfGases-1] = initMoleFrac[0:numberOfGases-1] # Gas mole fraction + initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading + initialConditions[2*numberOfGases-1] = pressureTotal # Outlet pressure the same as inlet pressure + + # Solve the system of ordinary differential equations + outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='BDF', args = inputParameters) + + + os.chdir("plotFunctions") + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax = plt.subplot(1,3,1) + ax.plot(outputSol.t, outputSol.y[0,:], + linewidth=1.5,color='r') + ax.set(xlabel='$t$ [s]', + ylabel='$y$ [-]', + xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[0,:])]) + ax = plt.subplot(1,3,2) + ax.plot(outputSol.t, outputSol.y[1,:], + linewidth=1.5,color='r') + ax.set(xlabel='$t$ [s]', + ylabel='$q_1$ [mol m$^{\mathregular{-3}}$]', + xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[1,:])]) + ax = plt.subplot(1,3,3) + ax.plot(outputSol.t, outputSol.y[2,:], + linewidth=1.5,color='b') + ax.set(xlabel='$t$ [s]', + ylabel='$q_1$ [mol m$^{\mathregular{-3}}$]', + xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[2,:])]) + plt.show() + + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax = plt.subplot(1,2,1) + ax.plot(outputSol.t, outputSol.y[3,:], + linewidth=1.5,color='r') + ax.set(xlabel='$t$ [s]', + ylabel='$P$ [Pa]', + xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[3,:])]) + + ax = plt.subplot(1,2,2) + ax.plot(outputSol.t, outputSol.y[3,:]*(flowIn/temperature/8.314), + linewidth=1.5,color='k') + ax.plot(outputSol.t, outputSol.y[3,:]*outputSol.y[0,:]*(flowIn/temperature/8.314), + linewidth=1.5,color='r') + ax.plot(outputSol.t, outputSol.y[3,:]*(1.-outputSol.y[0,:])*(flowIn/temperature/8.314), + linewidth=1.5,color='b') + ax.set(xlabel='$t$ [s]', + ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', + xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[3,:])*(flowIn/temperature/8.314)]) + plt.show() + + os.chdir("..") + return outputSol + +def solveSorptionEquation(t, f, *inputParameters): + import numpy as np + from simulateSensorArray import simulateSensorArray + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Unpack the tuple of input parameters zsed to solve equations + sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, pressureTotal, temperature, volSorbent, volGas = inputParameters + + # Initialize the derivatives to zero + df = np.zeros([2*numberOfGases]) + + # Compute the equilbirium loading at the current gas composition + currentGasComposition = np.concatenate((f[0:numberOfGases-1], + np.array([1.-np.sum(f[0:numberOfGases-1])]))) + _ , sensorLoadingPerGasVol = simulateSensorArray(sensorID, f[2*numberOfGases-1], + temperature, np.array([currentGasComposition]), + fullModel = True) + + # Linear driving force model (derivative of solid phase loadings) + df[numberOfGases-1:2*numberOfGases-1] = np.multiply(rateConstant,(sensorLoadingPerGasVol-f[numberOfGases-1:2*numberOfGases-1])) + + # Total mass balance + term1 = 1/volGas + term2 = ((flowIn*pressureTotal) - (flowIn*f[2*numberOfGases-1]) + - ((volSorbent*(Rg*temperature))*(np.sum(df[numberOfGases-1:2*numberOfGases-1])))) + df[2*numberOfGases-1] = term1*term2 + + # Component mass balance + term1 = 1/volGas + for ii in range(numberOfGases-1): + term2 = (flowIn*(pressureTotal*feedMoleFrac[ii] - f[2*numberOfGases-1]*f[ii]) + - (volSorbent*(Rg*temperature))*df[ii+numberOfGases-1]) + df[ii] = (term1*term2 - f[ii]*df[2*numberOfGases-1])/f[2*numberOfGases-1] + + return df \ No newline at end of file diff --git a/simulateSensorArray.py b/simulateSensorArray.py index bd3df9a..fa32039 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -14,6 +14,7 @@ # n sorbents. # # Last modified: +# - 2021-01-19, AK: Add flag for full model # - 2020-10-30, AK: Fix to find number of gases # - 2020-10-22, AK: Add two/three gases # - 2020-10-21, AK: Cosmetic changes and make it a function @@ -28,7 +29,7 @@ # ############################################################################ -def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction): +def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction, **kwargs): import numpy as np from numpy import load import os @@ -68,4 +69,11 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction): # Obtain the sensor finger print # [g of total gas adsorbed/kg of sorbent] sensorFingerPrint = np.dot(sensorLoadingPerGasMass,molecularWeight) # [g/kg] - return sensorFingerPrint \ No newline at end of file + # Flag to check if simulation full model or not + if 'fullModel' in kwargs: + if kwargs["fullModel"]: + return sensorFingerPrint, sensorLoadingPerGasVol + else: + return sensorFingerPrint + else: + return sensorFingerPrint \ No newline at end of file From 44c54dd3d119fd40db817015dcc491de119688a7 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 20 Jan 2021 17:20:38 +0000 Subject: [PATCH 54/99] Change structure details for full model and add a file to include inert gas in existing materials --- auxiliaryFunctions/addInertGasToMaterials.py | 80 +++++++++++++++++ simulateFullModel.py | 91 ++++++-------------- simulateSensorArray.py | 33 ++++--- 3 files changed, 130 insertions(+), 74 deletions(-) create mode 100644 auxiliaryFunctions/addInertGasToMaterials.py diff --git a/auxiliaryFunctions/addInertGasToMaterials.py b/auxiliaryFunctions/addInertGasToMaterials.py new file mode 100644 index 0000000..195aaa4 --- /dev/null +++ b/auxiliaryFunctions/addInertGasToMaterials.py @@ -0,0 +1,80 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Adds an inert gas to the exisitng material property matrix for an n gas +# system. This is done to simulate the sensor array with a full model. The +# inert is used to clean the sensor array +# +# Last modified: +# - 2021-01-20, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def addInertGasToMaterials(): + import numpy as np + from numpy import load + from numpy import savez + import os + import auxiliaryFunctions + + # Specify the number of gases + numberOfGases = 2 + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Get the current date and time for saving purposes + simulationDT = auxiliaryFunctions.getCurrentDateTime() + + # For now load a given adsorbent isotherm material file + if numberOfGases == 2: + loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases + elif numberOfGases == 3: + loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases + hypoAdsorbentFile = os.path.join('../inputResources',loadFileName); + + # Check if the file with the adsorbent properties exist + if os.path.exists(hypoAdsorbentFile): + loadedFileContent = load(hypoAdsorbentFile) + adsorbentIsothermTemp = loadedFileContent['adsIsotherm'] + adsorbentDensity = loadedFileContent['adsDensity'] + molecularWeightTemp = loadedFileContent['molWeight'] + else: + errorString = "Adsorbent property file " + hypoAdsorbentFile + " does not exist." + raise Exception(errorString) + + # Create adsorent isotherm matrix with the addition of an intert gas + adsorbentIsotherm = np.zeros([numberOfGases+1,3,adsorbentIsothermTemp.shape[2]]) + adsorbentIsotherm[0:numberOfGases,:,:] = adsorbentIsothermTemp + + # Add the moleuclar weight of the inert (assumed to be helium) + molecularWeight = np.concatenate((molecularWeightTemp,np.array([4.00]))) + + # Save the adsorbent isotherm parameters into a native numpy file + # The .npz file is saved in a folder called inputResources (hardcoded) + filePrefix = "isothermParameters" + saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID + ".npz"; + savePath = os.path.join('../inputResources',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists('../inputResources'): + os.mkdir('../inputResources') + + # Save the adsorbent material array + savez (savePath, adsIsotherm = adsorbentIsotherm, + adsDensity = adsorbentDensity, + molWeight = molecularWeight) \ No newline at end of file diff --git a/simulateFullModel.py b/simulateFullModel.py index 8033f49..c531650 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -4,7 +4,7 @@ # Multifunctional Nanomaterials Laboratory # # Project: ERASE -# Year: 2020 +# Year: 2021 # Python: Python 3.7 # Authors: Ashwin Kumar Rajagopalan (AK) # @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-01-20, AK: Cosmetic changes # - 2021-01-19, AK: Initial creation # # Input arguments: @@ -25,43 +26,44 @@ def simulateFullModel(**kwargs): import numpy as np from scipy.integrate import solve_ivp - from simulateSensorArray import simulateSensorArray - import matplotlib.pyplot as plt - import os + from simulateSensorArray import simulateSensorArray # Sensor ID if 'sensorID' in kwargs: sensorID = np.array(kwargs["sensorID"]) else: - sensorID = np.array([6]) + sensorID = np.array([3]) # Kinetic rate constants [/s] if 'rateConstant' in kwargs: rateConstant = np.array(kwargs["rateConstant"]) else: - # Equilibrium process - rateConstant = np.array([0.01,0.01]) + rateConstant = np.array([0.1,0.1,0.1]) # Feed flow rate [m3/s] if 'flowIn' in kwargs: flowIn = np.array(kwargs["flowIn"]) else: - # Equilibrium process flowIn = np.array([5e-7]) # Feed Mole Fraction [-] if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) else: - # Equilibrium process - feedMoleFrac = np.array([1.,0.]) + feedMoleFrac = np.array([1.,0.,0.]) # Initial Gas Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) else: # Equilibrium process - initMoleFrac = np.array([0.,1.]) + initMoleFrac = np.array([10000.,10000.,10000.]) + + # Time span for integration [tuple with t0 and tf] + if 'timeInt' in kwargs: + timeInt = kwargs["timeInt"] + else: + timeInt = (0.0,30000) if (len(feedMoleFrac) != len(initMoleFrac) or len(feedMoleFrac) != len(rateConstant)): @@ -69,9 +71,6 @@ def simulateFullModel(**kwargs): else: numberOfGases = len(feedMoleFrac) - # Time span for integration [s] - timeInt = (0.0,1000) - # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); @@ -85,12 +84,12 @@ def simulateFullModel(**kwargs): # Volume of gas chamber (dead volume) [m3] volGas = 5e-7 - # Compute the initial sensor loading [mol/m3] @ initMoleFrac _ , sensorLoadingPerGasVol = simulateSensorArray(sensorID, pressureTotal, temperature, np.array([initMoleFrac]), fullModel = True) + # Prepare tuple of input parameters for the ode solver inputParameters = (sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, pressureTotal, temperature, volSorbent, volGas) @@ -101,56 +100,19 @@ def simulateFullModel(**kwargs): initialConditions[2*numberOfGases-1] = pressureTotal # Outlet pressure the same as inlet pressure # Solve the system of ordinary differential equations - outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='BDF', args = inputParameters) - - - os.chdir("plotFunctions") - plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file - fig = plt.figure - ax = plt.subplot(1,3,1) - ax.plot(outputSol.t, outputSol.y[0,:], - linewidth=1.5,color='r') - ax.set(xlabel='$t$ [s]', - ylabel='$y$ [-]', - xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[0,:])]) - ax = plt.subplot(1,3,2) - ax.plot(outputSol.t, outputSol.y[1,:], - linewidth=1.5,color='r') - ax.set(xlabel='$t$ [s]', - ylabel='$q_1$ [mol m$^{\mathregular{-3}}$]', - xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[1,:])]) - ax = plt.subplot(1,3,3) - ax.plot(outputSol.t, outputSol.y[2,:], - linewidth=1.5,color='b') - ax.set(xlabel='$t$ [s]', - ylabel='$q_1$ [mol m$^{\mathregular{-3}}$]', - xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[2,:])]) - plt.show() + # Stiff solver used for the problem: BDF or Radau + outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='BDF', + args = inputParameters) - plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file - fig = plt.figure - ax = plt.subplot(1,2,1) - ax.plot(outputSol.t, outputSol.y[3,:], - linewidth=1.5,color='r') - ax.set(xlabel='$t$ [s]', - ylabel='$P$ [Pa]', - xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[3,:])]) - - ax = plt.subplot(1,2,2) - ax.plot(outputSol.t, outputSol.y[3,:]*(flowIn/temperature/8.314), - linewidth=1.5,color='k') - ax.plot(outputSol.t, outputSol.y[3,:]*outputSol.y[0,:]*(flowIn/temperature/8.314), - linewidth=1.5,color='r') - ax.plot(outputSol.t, outputSol.y[3,:]*(1.-outputSol.y[0,:])*(flowIn/temperature/8.314), - linewidth=1.5,color='b') - ax.set(xlabel='$t$ [s]', - ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', - xlim = timeInt, ylim = [0, 1.1*np.max(outputSol.y[3,:])*(flowIn/temperature/8.314)]) - plt.show() + # Parse out the time and the output matrix + timeSim = outputSol.t + resultMat = outputSol.y - os.chdir("..") - return outputSol + # Return time and the output matrix + return timeSim, resultMat +# func: solveSorptionEquation +# Solves the system of ODEs to evaluate the gas composition, loadings, and pressure def solveSorptionEquation(t, f, *inputParameters): import numpy as np from simulateSensorArray import simulateSensorArray @@ -158,7 +120,7 @@ def solveSorptionEquation(t, f, *inputParameters): # Gas constant Rg = 8.314; # [J/mol K] - # Unpack the tuple of input parameters zsed to solve equations + # Unpack the tuple of input parameters used to solve equations sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero @@ -186,5 +148,6 @@ def solveSorptionEquation(t, f, *inputParameters): term2 = (flowIn*(pressureTotal*feedMoleFrac[ii] - f[2*numberOfGases-1]*f[ii]) - (volSorbent*(Rg*temperature))*df[ii+numberOfGases-1]) df[ii] = (term1*term2 - f[ii]*df[2*numberOfGases-1])/f[2*numberOfGases-1] - + + # Return the derivatives for the solver return df \ No newline at end of file diff --git a/simulateSensorArray.py b/simulateSensorArray.py index fa32039..dce8516 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -14,6 +14,7 @@ # n sorbents. # # Last modified: +# - 2021-01-20, AK: Structure change and add material file for full model # - 2021-01-19, AK: Add flag for full model # - 2020-10-30, AK: Fix to find number of gases # - 2020-10-22, AK: Add two/three gases @@ -35,11 +36,26 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction, **kw import os from simulateSSL import simulateSSL - # For now load a given adsorbent isotherm material file - if moleFraction.shape[1] == 2: - loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases - elif moleFraction.shape[1] == 3: - loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases + # Flag to check if simulation full model or not + if 'fullModel' in kwargs: + if kwargs["fullModel"]: + flagFullModel = True + else: + flagFullModel = False + else: + flagFullModel = False + + # Load a given adsorbent isotherm material file based on full model flag + if flagFullModel: + if moleFraction.shape[1] == 2: + loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases + elif moleFraction.shape[1] == 3: + loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases + else: + if moleFraction.shape[1] == 2: + loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases + elif moleFraction.shape[1] == 3: + loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases hypoAdsorbentFile = os.path.join('inputResources',loadFileName); # Check if the file with the adsorbent properties exist @@ -70,10 +86,7 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction, **kw sensorFingerPrint = np.dot(sensorLoadingPerGasMass,molecularWeight) # [g/kg] # Flag to check if simulation full model or not - if 'fullModel' in kwargs: - if kwargs["fullModel"]: - return sensorFingerPrint, sensorLoadingPerGasVol - else: - return sensorFingerPrint + if flagFullModel: + return sensorFingerPrint, sensorLoadingPerGasVol else: return sensorFingerPrint \ No newline at end of file From fb57143f14f77f15b2c13dfe37b160b847de12d2 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 20 Jan 2021 17:22:08 +0000 Subject: [PATCH 55/99] Minor fix --- auxiliaryFunctions/addInertGasToMaterials.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/auxiliaryFunctions/addInertGasToMaterials.py b/auxiliaryFunctions/addInertGasToMaterials.py index 195aaa4..0480bd4 100644 --- a/auxiliaryFunctions/addInertGasToMaterials.py +++ b/auxiliaryFunctions/addInertGasToMaterials.py @@ -24,16 +24,13 @@ # ############################################################################ -def addInertGasToMaterials(): +def addInertGasToMaterials(numberOfGases): import numpy as np from numpy import load from numpy import savez import os import auxiliaryFunctions - # Specify the number of gases - numberOfGases = 2 - # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() From bc84fed94105c10797b447eb50d67ee8b5d521f6 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 20 Jan 2021 17:28:18 +0000 Subject: [PATCH 56/99] Fix for full model load file --- simulateSensorArray.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/simulateSensorArray.py b/simulateSensorArray.py index dce8516..0a023a8 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -47,10 +47,11 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction, **kw # Load a given adsorbent isotherm material file based on full model flag if flagFullModel: - if moleFraction.shape[1] == 2: - loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases - elif moleFraction.shape[1] == 3: - loadFileName = "isothermParameters_20201022_1056_782efa3.npz" # Three gases + if moleFraction.shape[1] == 3: + loadFileName = "isothermParameters_20210120_1722_fb57143.npz" # Two gases + Inert + elif moleFraction.shape[1] == 4: + print('yes') + loadFileName = "isothermParameters_20210120_1724_fb57143.npz" # Three gases + Inert else: if moleFraction.shape[1] == 2: loadFileName = "isothermParameters_20201020_1756_5f263af.npz" # Two gases From 431fe88087991aa4371fcb2d4b0be8adac788113 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 21 Jan 2021 10:02:13 +0000 Subject: [PATCH 57/99] Cosmetic changes and add wrapper function for full model --- sensorFullModelWrapper.py | 89 +++++++++++++++++++++++ simulateFullModel.py | 148 ++++++++++++++++++++++++++++++++++---- simulateSensorArray.py | 2 +- 3 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 sensorFullModelWrapper.py diff --git a/sensorFullModelWrapper.py b/sensorFullModelWrapper.py new file mode 100644 index 0000000..e415c06 --- /dev/null +++ b/sensorFullModelWrapper.py @@ -0,0 +1,89 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Simulates the sensor chamber as a CSTR incorporating kinetic effects +# +# Last modified: +# - 2021-01-20, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import auxiliaryFunctions +from simulateFullModel import simulateFullModel +from tqdm import tqdm # To track progress of the loop +import numpy as np +import os +import matplotlib.pyplot as plt + +# Save settings +saveFlag = False +saveFileExtension = ".png" +saveText = "rateConstant" +colorsForPlot = ["1DBDE6","52A2C4","6D95B3","A27A91","CA6678","F1515E"] + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Define the variable to be looped +loopVariable = ([0.0001,0.0001,0.0001], + [0.0005,0.0005,0.0005], + [0.001,0.001,0.001], + [0.005,0.005,0.005], + [0.01,0.01,0.01], + [10000.0,10000.0,10000.0]) + +# Define a dictionary +outputStruct = {} + +# Loop over all rate constants +for ii in tqdm(range(len(loopVariable))): + # Call the full model with a given rate constant + timeSim, _ , sensorFingerPrint, inputParameters = simulateFullModel(rateConstant = loopVariable[ii]) + outputStruct[ii] = {'timeSim':timeSim, + 'sensorFingerPrint':sensorFingerPrint, + 'inputParameters':inputParameters} + +# Plot the sensor finger print +os.chdir("plotFunctions") +plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file +fig = plt.figure +ax = plt.subplot(1,1,1) +for ii in range(len(loopVariable)): + timeTemp = outputStruct[ii]["timeSim"] + fingerPrintTemp = outputStruct[ii]["sensorFingerPrint"] + ax.plot(timeTemp, fingerPrintTemp, + linewidth=1.5,color="#"+colorsForPlot[ii], + label = str(loopVariable[ii][0])) +ax.set(xlabel='$t$ [s]', + ylabel='$m_i$ [g kg$^{\mathregular{-1}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 150]) +ax.locator_params(axis="x", nbins=4) +ax.locator_params(axis="y", nbins=4) +# Save the figure +if saveFlag: + # FileName: fullModelWrapper___ + saveFileName = "fullModelWrapper_" + saveText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName.replace('[','').replace(']','')) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) +plt.show() +os.chdir("..") \ No newline at end of file diff --git a/simulateFullModel.py b/simulateFullModel.py index c531650..d14f567 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-01-20, AK: Change to output time and plot function # - 2021-01-20, AK: Cosmetic changes # - 2021-01-19, AK: Initial creation # @@ -27,18 +28,28 @@ def simulateFullModel(**kwargs): import numpy as np from scipy.integrate import solve_ivp from simulateSensorArray import simulateSensorArray + import auxiliaryFunctions + + # Plot flag + plotFlag = False + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() # Sensor ID if 'sensorID' in kwargs: sensorID = np.array(kwargs["sensorID"]) else: - sensorID = np.array([3]) + sensorID = np.array([6]) # Kinetic rate constants [/s] if 'rateConstant' in kwargs: rateConstant = np.array(kwargs["rateConstant"]) else: - rateConstant = np.array([0.1,0.1,0.1]) + rateConstant = np.array([10000.0,10000.0,10000.0]) # Feed flow rate [m3/s] if 'flowIn' in kwargs: @@ -57,13 +68,13 @@ def simulateFullModel(**kwargs): initMoleFrac = np.array(kwargs["initMoleFrac"]) else: # Equilibrium process - initMoleFrac = np.array([10000.,10000.,10000.]) + initMoleFrac = np.array([0.,0.,1.]) # Time span for integration [tuple with t0 and tf] if 'timeInt' in kwargs: timeInt = kwargs["timeInt"] else: - timeInt = (0.0,30000) + timeInt = (0.0,2000) if (len(feedMoleFrac) != len(initMoleFrac) or len(feedMoleFrac) != len(rateConstant)): @@ -85,9 +96,9 @@ def simulateFullModel(**kwargs): volGas = 5e-7 # Compute the initial sensor loading [mol/m3] @ initMoleFrac - _ , sensorLoadingPerGasVol = simulateSensorArray(sensorID, pressureTotal, - temperature, np.array([initMoleFrac]), - fullModel = True) + sensorLoadingPerGasVol, adsorbentDensity, molecularWeight = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([initMoleFrac]), + fullModel = True) # Prepare tuple of input parameters for the ode solver inputParameters = (sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, @@ -101,15 +112,28 @@ def simulateFullModel(**kwargs): # Solve the system of ordinary differential equations # Stiff solver used for the problem: BDF or Radau - outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='BDF', + # The output is print out every second + outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='Radau', + t_eval = np.linspace(timeInt[0],timeInt[1],int(timeInt[1]-timeInt[0])), args = inputParameters) # Parse out the time and the output matrix timeSim = outputSol.t resultMat = outputSol.y + # Compute the time resolved sensor response + sensorFingerPrint = np.zeros([len(timeSim)]) + for ii in range(len(timeSim)): + loadingTemp = resultMat[numberOfGases-1:2*numberOfGases-1,ii] + sensorFingerPrint[ii] = np.dot(loadingTemp,molecularWeight)/adsorbentDensity + + # Call the plotting function + if plotFlag: + plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, + gitCommitID, currentDT) + # Return time and the output matrix - return timeSim, resultMat + return timeSim, resultMat, sensorFingerPrint, inputParameters # func: solveSorptionEquation # Solves the system of ODEs to evaluate the gas composition, loadings, and pressure @@ -129,9 +153,9 @@ def solveSorptionEquation(t, f, *inputParameters): # Compute the equilbirium loading at the current gas composition currentGasComposition = np.concatenate((f[0:numberOfGases-1], np.array([1.-np.sum(f[0:numberOfGases-1])]))) - _ , sensorLoadingPerGasVol = simulateSensorArray(sensorID, f[2*numberOfGases-1], - temperature, np.array([currentGasComposition]), - fullModel = True) + sensorLoadingPerGasVol, _ , _ = simulateSensorArray(sensorID, f[2*numberOfGases-1], + temperature, np.array([currentGasComposition]), + fullModel = True) # Linear driving force model (derivative of solid phase loadings) df[numberOfGases-1:2*numberOfGases-1] = np.multiply(rateConstant,(sensorLoadingPerGasVol-f[numberOfGases-1:2*numberOfGases-1])) @@ -150,4 +174,102 @@ def solveSorptionEquation(t, f, *inputParameters): df[ii] = (term1*term2 - f[ii]*df[2*numberOfGases-1])/f[2*numberOfGases-1] # Return the derivatives for the solver - return df \ No newline at end of file + return df + +# func: plotFullModelResult +# Plots the model output for the conditions simulated locally +def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, + gitCommitID, currentDT): + import numpy as np + import os + import matplotlib.pyplot as plt + + # Save settings + saveFlag = False + saveFileExtension = ".png" + + # Unpack the tuple of input parameters used to solve equations + sensorID, _, _, flowIn, _, _, temperature, _, _ = inputParameters + + os.chdir("plotFunctions") + if resultMat.shape[0] == 6: + # Plot the solid phase compositions + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax = plt.subplot(1,3,1) + ax.plot(timeSim, resultMat[2,:], + linewidth=1.5,color='r') + ax.set(xlabel='$t$ [s]', + ylabel='$q_1$ [mol m$^{\mathregular{-3}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[2,:])]) + + ax = plt.subplot(1,3,2) + ax.plot(timeSim, resultMat[3,:], + linewidth=1.5,color='b') + ax.set(xlabel='$t$ [s]', + ylabel='$q_2$ [mol m$^{\mathregular{-3}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[3,:])]) + + ax = plt.subplot(1,3,3) + ax.plot(timeSim, resultMat[4,:], + linewidth=1.5,color='g') + ax.set(xlabel='$t$ [s]', + ylabel='$q_3$ [mol m$^{\mathregular{-3}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[4,:])]) + + # Save the figure + if saveFlag: + # FileName: solidLoadingFM___ + saveFileName = "solidLoadingFM_" + str(sensorID) + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName.replace('[','').replace(']','')) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + + plt.show() + + # Plot the pressure drop and the flow rate + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax = plt.subplot(1,2,1) + ax.plot(timeSim, resultMat[5,:], + linewidth=1.5,color='r') + ax.set(xlabel='$t$ [s]', + ylabel='$P$ [Pa]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])]) + ax = plt.subplot(1,2,2) + ax.plot(timeSim, resultMat[5,:]*(flowIn/temperature/8.314), + linewidth=1.5,color='k') + ax.plot(timeSim, resultMat[5,:]*resultMat[0,:]*(flowIn/temperature/8.314), + linewidth=1.5,color='r') + ax.plot(timeSim, resultMat[5,:]*(resultMat[1,:])*(flowIn/temperature/8.314), + linewidth=1.5,color='b') + ax.plot(timeSim, resultMat[5,:]*(1-resultMat[0,:]-resultMat[1,:])*(flowIn/temperature/8.314), + linewidth=1.5,color='g') + ax.set(xlabel='$t$ [s]', + ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])*(flowIn/temperature/8.314)]) + plt.show() + + # Plot the sensor finger print + plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax = plt.subplot(1,1,1) + ax.plot(timeSim, sensorFingerPrint, + linewidth=1.5,color='k') + ax.set(xlabel='$t$ [s]', + ylabel='$m_i$ [g kg$^{\mathregular{-1}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(sensorFingerPrint)]) + # Save the figure + if saveFlag: + # FileName: SensorFingerPrintFM___ + saveFileName = "SensorFingerPrintFM_" + str(sensorID) + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName.replace('[','').replace(']','')) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) + plt.show() + + os.chdir("..") \ No newline at end of file diff --git a/simulateSensorArray.py b/simulateSensorArray.py index 0a023a8..4ba1d6d 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -88,6 +88,6 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction, **kw # Flag to check if simulation full model or not if flagFullModel: - return sensorFingerPrint, sensorLoadingPerGasVol + return sensorLoadingPerGasVol, adsorbentDensity, molecularWeight else: return sensorFingerPrint \ No newline at end of file From 1eee70022dec8b2ffbab927cfdb54e1ee29c7bcb Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 21 Jan 2021 10:03:53 +0000 Subject: [PATCH 58/99] Minor chnage to the header --- sensorFullModelWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sensorFullModelWrapper.py b/sensorFullModelWrapper.py index e415c06..93b6435 100644 --- a/sensorFullModelWrapper.py +++ b/sensorFullModelWrapper.py @@ -9,7 +9,8 @@ # Authors: Ashwin Kumar Rajagopalan (AK) # # Purpose: -# Simulates the sensor chamber as a CSTR incorporating kinetic effects +# Wrapper function for sensor full model to check the impact of different +# variables # # Last modified: # - 2021-01-20, AK: Initial creation From 4a54a4d338fe5af99df39a95edc145f81a4acb04 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 21 Jan 2021 20:02:59 +0000 Subject: [PATCH 59/99] Add concentration estimator for full model --- estimateConcentration.py | 91 +++++++++++++------ fullModelConcentrationEstimatorWrapper.py | 103 ++++++++++++++++++++++ sensorFullModelWrapper.py | 9 +- simulateFullModel.py | 19 ++-- simulateSensorArray.py | 1 - 5 files changed, 183 insertions(+), 40 deletions(-) create mode 100644 fullModelConcentrationEstimatorWrapper.py diff --git a/estimateConcentration.py b/estimateConcentration.py index e0855da..3505f4e 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,7 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2021-01-21, AK: Add full model functionality # - 2020-11-12, AK: Bug fix for multipler error # - 2020-11-11, AK: Add multiplier error to true sensor response # - 2020-11-10, AK: Add measurement noise to true sensor response @@ -38,29 +39,32 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI from scipy.optimize import basinhopping # Total pressure of the gas [Pa] - pressureTotal = np.array([1.e5]); + if 'pressureTotal' in kwargs: + pressureTotal = np.array(kwargs["pressureTotal"]); + else: + pressureTotal = np.array([1.e5]); # Temperature of the gas [K] # Can be a vector of temperatures - temperature = np.array([298.15]); + if 'temperature' in kwargs: + temperature = np.array(kwargs["temperature"]); + else: + temperature = np.array([298.15]); # Sensor combinations used in the array. This is a [gx1] vector that maps to # the sorbent/material ID generated using the # generateHypotheticalAdsorbents.py function sensorID = np.array(sensorID) - + # Get the individual sensor reponse for all the given "experimental/test" concentrations - if 'moleFraction' in kwargs: - sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, - pressureTotal,temperature, moleFraction = kwargs["moleFraction"]) - moleFracID = 0 # Index becomes a scalar quantity + if 'fullModel' in kwargs: + if kwargs["fullModel"]: + fullModelFlag = True + else: + fullModelFlag = False else: - sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, - pressureTotal,temperature) - # True mole fraction index (provide the index corresponding to the true - # experimental mole fraction (0-4, check generateTrueSensorResponse.py) - moleFracID = moleFracID - + fullModelFlag = False + # Add measurement noise for the true measurement if the user wants it measurementNoise = np.zeros(sensorID.shape[0]) if 'addMeasurementNoise' in kwargs: @@ -69,20 +73,53 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI measurementNoise = np.random.normal(kwargs["addMeasurementNoise"][0], kwargs["addMeasurementNoise"][1], sensorID.shape[0]) - - # Add a multiplier error for the true measurement if the user wants it - multiplierError = np.ones(sensorID.shape[0]) - if 'multiplierError' in kwargs: - # The mean and the standard deviation of the Gaussian error is an - # input from the user - multiplierErrorTemp = kwargs["multiplierError"] - multiplierError[0:len(multiplierErrorTemp)] = multiplierErrorTemp - # Parse out the true sensor response for a sensor array with n number of - # sensors given by sensorID - arrayTrueResponse = np.zeros(sensorID.shape[0]) - for ii in range(sensorID.shape[0]): - arrayTrueResponse[ii] = (multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] - + measurementNoise[ii]) + + # Check if it is for full model or not + # Full model condition + if fullModelFlag: + # Parse out the true sensor response from the input (time resolved) + # and add measurement noise if asked for. There is no multipler error for + # full model simualtions + # Note that using the full model here is only for comparison purposes + # When kinetics are available the other estimateConcentration function + # should be used + if 'fullModelResponse' in kwargs: + fullModelResponse = kwargs["fullModelResponse"] + multiplierError = np.ones(sensorID.shape[0]) # Always set to 1. + arrayTrueResponse = np.zeros(sensorID.shape[0]) + for ii in range(sensorID.shape[0]): + arrayTrueResponse[ii] = (multiplierError[ii]*fullModelResponse[ii] + + measurementNoise[ii]) + else: + errorString = "Sensor response from full model not available. You should not be here!" + raise Exception(errorString) + # Equilibrium condition + else: + # Get the individual sensor reponse for all the given "experimental/test" concentrations + if 'moleFraction' in kwargs: + sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, + pressureTotal,temperature, moleFraction = kwargs["moleFraction"]) + moleFracID = 0 # Index becomes a scalar quantity + else: + sensorTrueResponse = generateTrueSensorResponse(numberOfAdsorbents,numberOfGases, + pressureTotal,temperature) + # True mole fraction index (provide the index corresponding to the true + # experimental mole fraction (0-4, check generateTrueSensorResponse.py) + moleFracID = moleFracID + + # Add a multiplier error for the true measurement if the user wants it + multiplierError = np.ones(sensorID.shape[0]) + if 'multiplierError' in kwargs: + # The mean and the standard deviation of the Gaussian error is an + # input from the user + multiplierErrorTemp = kwargs["multiplierError"] + multiplierError[0:len(multiplierErrorTemp)] = multiplierErrorTemp + # Parse out the true sensor response for a sensor array with n number of + # sensors given by sensorID + arrayTrueResponse = np.zeros(sensorID.shape[0]) + for ii in range(sensorID.shape[0]): + arrayTrueResponse[ii] = (multiplierError[ii]*sensorTrueResponse[sensorID[ii],moleFracID] + + measurementNoise[ii]) # Replace all negative values to eps (for physical consistency). Set to # eps to avoid division by zero diff --git a/fullModelConcentrationEstimatorWrapper.py b/fullModelConcentrationEstimatorWrapper.py new file mode 100644 index 0000000..528524f --- /dev/null +++ b/fullModelConcentrationEstimatorWrapper.py @@ -0,0 +1,103 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Wrapper function to estimate the concentration given a time-resolved +# measurement from the full model +# +# Last modified: +# - 2021-01-21, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import auxiliaryFunctions +from simulateFullModel import simulateFullModel +from estimateConcentration import estimateConcentration +from tqdm import tqdm # To track progress of the loop +import numpy as np +from joblib import Parallel, delayed # For parallel processing +import multiprocessing +import os +from numpy import savez + +# Find out the total number of cores available for parallel processing +num_cores = multiprocessing.cpu_count() + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Sensor ID +sensorID = [6,2] # [-] + +# Number of Gases +numberOfGases = 2 # [-] + +# Rate Constant +rateConstant = ([0.1,1.0,10000.0], + [0.1,1.0,10000.0]) # [1/s] + +# Feed mole fraction +feedMoleFrac = [0.5,0.5,0.0] # äö[-] + +# Time span for integration [tuple with t0 and tf] [s] +timeInt = (0,500) # [s] + +# Loop over all rate constants +outputStruct = {} +for ii in tqdm(range(len(sensorID))): + # Call the full model with a given rate constant + timeSim, _ , sensorFingerPrint, inputParameters = simulateFullModel(sensorID = sensorID[ii], + rateConstant = rateConstant[ii], + feedMoleFrac = feedMoleFrac, + timeInt = timeInt) + outputStruct[ii] = {'timeSim':timeSim, + 'sensorFingerPrint':sensorFingerPrint, + 'inputParameters':inputParameters} # Check simulateFullModel.py for entries + + +# Prepare time-resolved sendor finger print +timeSim = [] +timeSim = outputStruct[0]["timeSim"] +sensorFingerPrint = np.zeros([len(timeSim),len(sensorID)]) +arrayConcentration = np.zeros([len(timeSim),numberOfGases+len(sensorID)]) +for ii in range(len(sensorID)): + sensorFingerPrint[:,ii] = outputStruct[ii]["sensorFingerPrint"] + +# Loop over all time instants and estimate the gas composition +arrayConcentrationTemp = Parallel(n_jobs=num_cores)(delayed(estimateConcentration) + (None,numberOfGases,None,sensorID, + fullModel = True, + fullModelResponse = sensorFingerPrint[ii,:]) + for ii in tqdm(range(len(timeSim)))) + +arrayConcentration = np.array(arrayConcentrationTemp) + +# Check if simulationResults directory exists or not. If not, create the folder +if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') + +# Save the array concentration into a native numpy file +# The .npz file is saved in a folder called simulationResults (hardcoded) +filePrefix = "fullModelConcentrationEstimate" +sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-').replace(',','') +saveFileName = filePrefix + "_" + sensorText + "_" + currentDT + "_" + gitCommitID; +savePath = os.path.join('simulationResults',saveFileName) + +savez (savePath, outputStruct = outputStruct, + arrayConcentration = arrayConcentration) \ No newline at end of file diff --git a/sensorFullModelWrapper.py b/sensorFullModelWrapper.py index 93b6435..3442b50 100644 --- a/sensorFullModelWrapper.py +++ b/sensorFullModelWrapper.py @@ -43,12 +43,9 @@ currentDT = auxiliaryFunctions.getCurrentDateTime() # Define the variable to be looped -loopVariable = ([0.0001,0.0001,0.0001], - [0.0005,0.0005,0.0005], - [0.001,0.001,0.001], - [0.005,0.005,0.005], - [0.01,0.01,0.01], - [10000.0,10000.0,10000.0]) +# This has to be a be a tuple. For on condition write the values followed by a +# comma to say its a tuple +loopVariable = ([0.001,0.001,0.001],) # Define a dictionary outputStruct = {} diff --git a/simulateFullModel.py b/simulateFullModel.py index d14f567..5c59413 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-01-21, AK: Cosmetic changes # - 2021-01-20, AK: Change to output time and plot function # - 2021-01-20, AK: Cosmetic changes # - 2021-01-19, AK: Initial creation @@ -41,7 +42,7 @@ def simulateFullModel(**kwargs): # Sensor ID if 'sensorID' in kwargs: - sensorID = np.array(kwargs["sensorID"]) + sensorID = np.array([kwargs["sensorID"]]) else: sensorID = np.array([6]) @@ -61,7 +62,7 @@ def simulateFullModel(**kwargs): if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) else: - feedMoleFrac = np.array([1.,0.,0.]) + feedMoleFrac = np.array([1.0,0.,0.]) # Initial Gas Mole Fraction [-] if 'initMoleFrac' in kwargs: @@ -83,11 +84,17 @@ def simulateFullModel(**kwargs): numberOfGases = len(feedMoleFrac) # Total pressure of the gas [Pa] - pressureTotal = np.array([1.e5]); - + if 'pressureTotal' in kwargs: + pressureTotal = np.array(kwargs["pressureTotal"]); + else: + pressureTotal = np.array([1.e5]); + # Temperature of the gas [K] # Can be a vector of temperatures - temperature = np.array([298.15]); + if 'temperature' in kwargs: + temperature = np.array(kwargs["temperature"]); + else: + temperature = np.array([298.15]); # Volume of sorbent material [m3] volSorbent = 5e-7 @@ -114,7 +121,7 @@ def simulateFullModel(**kwargs): # Stiff solver used for the problem: BDF or Radau # The output is print out every second outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='Radau', - t_eval = np.linspace(timeInt[0],timeInt[1],int(timeInt[1]-timeInt[0])), + t_eval = np.linspace(timeInt[0],timeInt[1],int((timeInt[1]-timeInt[0])/5)), args = inputParameters) # Parse out the time and the output matrix diff --git a/simulateSensorArray.py b/simulateSensorArray.py index 4ba1d6d..a3d9ed1 100755 --- a/simulateSensorArray.py +++ b/simulateSensorArray.py @@ -50,7 +50,6 @@ def simulateSensorArray(sensorID, pressureTotal, temperature, moleFraction, **kw if moleFraction.shape[1] == 3: loadFileName = "isothermParameters_20210120_1722_fb57143.npz" # Two gases + Inert elif moleFraction.shape[1] == 4: - print('yes') loadFileName = "isothermParameters_20210120_1724_fb57143.npz" # Three gases + Inert else: if moleFraction.shape[1] == 2: From f113cf49e6556be3c6052cd3ba54c188167c83fc Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 22 Jan 2021 12:56:54 +0000 Subject: [PATCH 60/99] Small fixes --- simulateFullModel.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/simulateFullModel.py b/simulateFullModel.py index 5c59413..d172d74 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -62,7 +62,7 @@ def simulateFullModel(**kwargs): if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) else: - feedMoleFrac = np.array([1.0,0.,0.]) + feedMoleFrac = np.array([1.,0.,0.]) # Initial Gas Mole Fraction [-] if 'initMoleFrac' in kwargs: @@ -236,27 +236,36 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, plt.show() - # Plot the pressure drop and the flow rate + # Plot the pressure drop, the flow rate, and the mole fraction plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file fig = plt.figure - ax = plt.subplot(1,2,1) + ax = plt.subplot(1,3,1) ax.plot(timeSim, resultMat[5,:], linewidth=1.5,color='r') ax.set(xlabel='$t$ [s]', ylabel='$P$ [Pa]', xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])]) - ax = plt.subplot(1,2,2) - ax.plot(timeSim, resultMat[5,:]*(flowIn/temperature/8.314), + + ax = plt.subplot(1,3,2) + ax.plot(timeSim, flowIn*resultMat[5,:]/(temperature*8.314), linewidth=1.5,color='k') - ax.plot(timeSim, resultMat[5,:]*resultMat[0,:]*(flowIn/temperature/8.314), + ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[0,:]/(temperature*8.314), linewidth=1.5,color='r') - ax.plot(timeSim, resultMat[5,:]*(resultMat[1,:])*(flowIn/temperature/8.314), + ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[1,:]/(temperature*8.314), linewidth=1.5,color='b') - ax.plot(timeSim, resultMat[5,:]*(1-resultMat[0,:]-resultMat[1,:])*(flowIn/temperature/8.314), + ax.plot(timeSim, flowIn*resultMat[5,:]*(1-resultMat[0,:]-resultMat[1,:])/(temperature*8.314), linewidth=1.5,color='g') ax.set(xlabel='$t$ [s]', ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])*(flowIn/temperature/8.314)]) + + ax = plt.subplot(1,3,3) + ax.plot(timeSim, resultMat[0,:],linewidth=1.5,color='r') + ax.plot(timeSim, resultMat[1,:],linewidth=1.5,color='b') + ax.plot(timeSim, 1-resultMat[0,:]-resultMat[1,:],linewidth=1.5,color='g') + ax.set(xlabel='$t$ [s]', + ylabel='$y$ [-]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.]) plt.show() # Plot the sensor finger print From 9a0f480733095992fa766f503576b2ea58274d66 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 22 Jan 2021 13:05:27 +0000 Subject: [PATCH 61/99] Cosmetic changes --- fullModelConcentrationEstimatorWrapper.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/fullModelConcentrationEstimatorWrapper.py b/fullModelConcentrationEstimatorWrapper.py index 528524f..a1c15af 100644 --- a/fullModelConcentrationEstimatorWrapper.py +++ b/fullModelConcentrationEstimatorWrapper.py @@ -49,15 +49,18 @@ numberOfGases = 2 # [-] # Rate Constant -rateConstant = ([0.1,1.0,10000.0], - [0.1,1.0,10000.0]) # [1/s] +rateConstant = ([10000.0,10000.0,10000.0], + [10000.0,10000.0,10000.0]) # [1/s] # Feed mole fraction -feedMoleFrac = [0.5,0.5,0.0] # äö[-] +feedMoleFrac = [1.0,0.0,0.0] # [-] # Time span for integration [tuple with t0 and tf] [s] timeInt = (0,500) # [s] +# Volumetric flow rate [m3/s] +flowIn = 5e-7 # [s] + # Loop over all rate constants outputStruct = {} for ii in tqdm(range(len(sensorID))): @@ -65,7 +68,8 @@ timeSim, _ , sensorFingerPrint, inputParameters = simulateFullModel(sensorID = sensorID[ii], rateConstant = rateConstant[ii], feedMoleFrac = feedMoleFrac, - timeInt = timeInt) + timeInt = timeInt, + flowIn = flowIn) outputStruct[ii] = {'timeSim':timeSim, 'sensorFingerPrint':sensorFingerPrint, 'inputParameters':inputParameters} # Check simulateFullModel.py for entries @@ -85,7 +89,7 @@ fullModel = True, fullModelResponse = sensorFingerPrint[ii,:]) for ii in tqdm(range(len(timeSim)))) - +# Convert the list to array arrayConcentration = np.array(arrayConcentrationTemp) # Check if simulationResults directory exists or not. If not, create the folder @@ -98,6 +102,5 @@ sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-').replace(',','') saveFileName = filePrefix + "_" + sensorText + "_" + currentDT + "_" + gitCommitID; savePath = os.path.join('simulationResults',saveFileName) - savez (savePath, outputStruct = outputStruct, arrayConcentration = arrayConcentration) \ No newline at end of file From c9730a17d1d10c2ce2a0d972aed97b3fb1a13b05 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 25 Jan 2021 11:27:00 +0000 Subject: [PATCH 62/99] Add zero response to avoid division by zero (eps) --- estimateConcentration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index 3505f4e..04c3d4c 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -14,6 +14,7 @@ # "estimated" differences in the change of mass of the sensor array # # Last modified: +# - 2021-01-25, AK: Add zero response to avoid division by zero (IMP!) # - 2021-01-21, AK: Add full model functionality # - 2020-11-12, AK: Bug fix for multipler error # - 2020-11-11, AK: Add multiplier error to true sensor response @@ -126,7 +127,7 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI # Print if any of the responses are negative if any(ii<=0. for ii in arrayTrueResponse): print("Number of negative response: " + str(sum(arrayTrueResponse<0))) - arrayTrueResponse[arrayTrueResponse<0.] = np.finfo(float).eps + arrayTrueResponse[arrayTrueResponse<=0.] = np.finfo(float).eps # Pack the input parameters/arguments useful to compute the objective # function to estimate the mole fraction as a tuple From f25a54976878692c9fdacec9de1d1c6438c1e73b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 25 Jan 2021 18:17:32 +0000 Subject: [PATCH 63/99] Integrate full model concentration estimator to existing framework --- estimateConcentrationFullModel.py | 147 ++++++++++++++++++++++ fullModelConcentrationEstimatorWrapper.py | 88 ++++++++++--- simulateFullModel.py | 11 +- 3 files changed, 223 insertions(+), 23 deletions(-) create mode 100644 estimateConcentrationFullModel.py diff --git a/estimateConcentrationFullModel.py b/estimateConcentrationFullModel.py new file mode 100644 index 0000000..b36f44a --- /dev/null +++ b/estimateConcentrationFullModel.py @@ -0,0 +1,147 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Estimates the concentration of the gas mixture using the sensor response +# by minimization of the sum of square error between the "true" and the +# "estimated" differences in the change of mass of the sensor array as a +# function of time. Here the full model instead of equilibrium characteristics +# is used +# +# Last modified: +# - 2021-01-25, AK: Modify the objective function from eqbm method +# - 2021-01-22, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def estimateConcentrationFullModel(numberOfGases, sensorID, **kwargs): + import numpy as np + from generateTrueSensorResponse import generateTrueSensorResponse + from scipy.optimize import basinhopping + + # Total pressure of the gas [Pa] + if 'pressureTotal' in kwargs: + pressureTotal = np.array(kwargs["pressureTotal"]); + else: + pressureTotal = np.array([1.e5]); + + # Temperature of the gas [K] + # Can be a vector of temperatures + if 'temperature' in kwargs: + temperature = np.array(kwargs["temperature"]); + else: + temperature = np.array([298.15]); + + # Sensor combinations used in the array. This is a [gx1] vector that maps to + # the sorbent/material ID generated using the + # generateHypotheticalAdsorbents.py function + sensorID = np.array(sensorID) + + # Parse out the true sensor response from the input (time resolved) + if 'fullModelResponse' in kwargs: + arrayTrueResponse = kwargs["fullModelResponse"] + else: + errorString = "Sensor response from full model not available. You should not be here!" + raise Exception(errorString) + + # Parse out the rate constant for the materials from the input + if 'rateConstant' in kwargs: + rateConstant = kwargs["rateConstant"] + else: + errorString = "Rate constant for materials not available. You should not be here!" + raise Exception(errorString) + + # Parse out the time of integration from the input + if 'timeInt' in kwargs: + timeInt = kwargs["timeInt"] + else: + errorString = "Integration time for the model not available. You should not be here!" + raise Exception(errorString) + + # Parse out the feed flow rate from the input + if 'flowIn' in kwargs: + flowIn = kwargs["flowIn"] + else: + errorString = "Feed flow rate not available. You should not be here!" + raise Exception(errorString) + + # Replace all negative values to eps (for physical consistency). Set to + # eps to avoid division by zero + # Print if any of the responses are negative + if (arrayTrueResponse<=0.).any(): + print("Number of zero/negative response: " + str(np.sum(arrayTrueResponse<=0.))) + arrayTrueResponse[arrayTrueResponse<=0.] = np.finfo(float).eps + + # Pack the input parameters/arguments useful to compute the objective + # function to estimate the mole fraction as a tuple + inputParameters = (arrayTrueResponse, flowIn, sensorID, rateConstant, timeInt) + + # Minimize an objective function to compute the mole fraction of the feed + # gas to the sensor + initialCondition = np.random.uniform(0,1,numberOfGases+1) # Initial guess + optBounds = np.tile([0.,1.], (numberOfGases+1,1)) # BOunding the mole fractions + optCons = {'type':'eq','fun': lambda x: sum(x) - 1} # Cinstrain the sum to 1. + # Use the basin hopping minimizer to escape local minima when evaluating + # the function. The number of iterations is hard-coded and fixed at 50 + estMoleFraction = basinhopping(concObjectiveFunction, initialCondition, + minimizer_kwargs = {"args": inputParameters, + "bounds": optBounds, + "constraints": optCons}, + niter = 50) + return np.concatenate((sensorID,estMoleFraction.x), axis=0) + +# func: concObjectiveFunction, computes the sum of square error for the +# finger print for varying gas concentrations, using the minimize function +def concObjectiveFunction(x, *inputParameters): + import numpy as np + from simulateSensorArray import simulateSensorArray + from simulateFullModel import simulateFullModel + + # Unpack the tuple that contains the true response, sensor identifiers, + # rate constant and time of integration + # IMPORTANT: Pressure and Temperature not considered here!!! + arrayTrueResponse, flowIn, sensorID, rateConstant, timeInt = inputParameters + + # Reshape the mole fraction to a row vector for compatibility + moleFraction = np.array(x) # This is needed to keep the structure as a row instead of column + + # Compute the sensor reponse for a given mole fraction input + # Calls the full sensor model for each material + outputStruct = {} + for ii in range(len(sensorID)): + timeSim , _ , sensorFingerPrint, _ = simulateFullModel(sensorID = sensorID[ii], + rateConstant = rateConstant[ii], + feedMoleFrac = moleFraction, + timeInt = timeInt, + flowIn = flowIn) + outputStruct[ii] = {'timeSim':timeSim, + 'sensorFingerPrint':sensorFingerPrint} # Check simulateFullModel.py for entries + + # Prepare the simulated response at a given mole fraction to compute the + # objective function + timeSim = [] + timeSim = outputStruct[0]["timeSim"] # Need for array initialization + arraySimResponse = np.zeros([len(timeSim),len(sensorID)]) + for ii in range(len(sensorID)): + arraySimResponse[:,ii] = outputStruct[ii]["sensorFingerPrint"] + + # Prepare the objective function for the optimizer + relError = np.divide((arrayTrueResponse - arraySimResponse), arrayTrueResponse) + relErrorPow2 = np.power(relError, 2) # Square of relative error + objFunction = np.sum(relErrorPow2) # Sum over all the sensors and times + + # Return the sum of squares of relative errors + return objFunction \ No newline at end of file diff --git a/fullModelConcentrationEstimatorWrapper.py b/fullModelConcentrationEstimatorWrapper.py index a1c15af..b0f4843 100644 --- a/fullModelConcentrationEstimatorWrapper.py +++ b/fullModelConcentrationEstimatorWrapper.py @@ -13,6 +13,7 @@ # measurement from the full model # # Last modified: +# - 2021-01-25, AK: Integrate full model concentration estimator # - 2021-01-21, AK: Initial creation # # Input arguments: @@ -26,12 +27,15 @@ import auxiliaryFunctions from simulateFullModel import simulateFullModel from estimateConcentration import estimateConcentration +from estimateConcentrationFullModel import estimateConcentrationFullModel from tqdm import tqdm # To track progress of the loop import numpy as np +from numpy import savez from joblib import Parallel, delayed # For parallel processing import multiprocessing import os -from numpy import savez +import time +import socket # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() @@ -42,6 +46,10 @@ # Get the current date and time for saving purposes currentDT = auxiliaryFunctions.getCurrentDateTime() +# Flag to determine whether concentration estimated accounted for kinetics +# (False) or not (True) +flagIndTime = True + # Sensor ID sensorID = [6,2] # [-] @@ -49,14 +57,14 @@ numberOfGases = 2 # [-] # Rate Constant -rateConstant = ([10000.0,10000.0,10000.0], - [10000.0,10000.0,10000.0]) # [1/s] +rateConstant = ([.01,.01,10000.0], + [.01,.01,10000.0]) # [1/s] # Feed mole fraction -feedMoleFrac = [1.0,0.0,0.0] # [-] +feedMoleFrac = [0.1,0.9,0.0] # [-] # Time span for integration [tuple with t0 and tf] [s] -timeInt = (0,500) # [s] +timeInt = (0,35) # [s] # Volumetric flow rate [m3/s] flowIn = 5e-7 # [s] @@ -73,24 +81,65 @@ outputStruct[ii] = {'timeSim':timeSim, 'sensorFingerPrint':sensorFingerPrint, 'inputParameters':inputParameters} # Check simulateFullModel.py for entries - - + # Prepare time-resolved sendor finger print timeSim = [] timeSim = outputStruct[0]["timeSim"] sensorFingerPrint = np.zeros([len(timeSim),len(sensorID)]) -arrayConcentration = np.zeros([len(timeSim),numberOfGases+len(sensorID)]) for ii in range(len(sensorID)): sensorFingerPrint[:,ii] = outputStruct[ii]["sensorFingerPrint"] -# Loop over all time instants and estimate the gas composition -arrayConcentrationTemp = Parallel(n_jobs=num_cores)(delayed(estimateConcentration) - (None,numberOfGases,None,sensorID, - fullModel = True, - fullModelResponse = sensorFingerPrint[ii,:]) - for ii in tqdm(range(len(timeSim)))) -# Convert the list to array -arrayConcentration = np.array(arrayConcentrationTemp) +# flagIndTime - If true, each time instant evaluated without knowledge of +# actual kinetics in the estimate of the concentration (accounted only in the +# generation of the true sensor response above) - This would lead to a +# scenario of concentrations evaluated at each time instant +if flagIndTime: + # Start time for time elapsed + startTime = time.time() + # Initialize output matrix + arrayConcentration = np.zeros([len(timeSim),numberOfGases+len(sensorID)]) + # Loop over all time instants and estimate the gas composition + arrayConcentrationTemp = Parallel(n_jobs=num_cores)(delayed(estimateConcentration) + (None,numberOfGases,None,sensorID, + fullModel = True, + fullModelResponse = sensorFingerPrint[ii,:]) + for ii in tqdm(range(len(timeSim)))) + # Convert the list to array + arrayConcentration = np.array(arrayConcentrationTemp) + # Stop time for time elapsed + stopTime = time.time() + # Time elapsed [s] + timeElapsed = stopTime - startTime +# flagIndTime - If false, only one concentration estimate obtained. The full +# model is simulated in concentration estimator accounting for the kinetics. +else: + # Start time for time elapsed + startTime = time.time() + # Initialize output matrix + arrayConcentration = np.zeros([len(timeSim)-1,numberOfGases+1+len(sensorID)]) + # Loop over all time instants and estimate the gas composition + # The concentration is estimated for all the time instants in timeSim. + # This simulates using the full model as and when a new measurement is + # available. This assumes that the estimator has knowledge of the past + # measurements + # The first time instant is not simulated. This is fixed by adding a dummy + # row after the simulations are over + arrayConcentrationTemp = Parallel(n_jobs=num_cores)(delayed(estimateConcentrationFullModel) + (numberOfGases,sensorID, + fullModelResponse = sensorFingerPrint[0:ii+1,:], + rateConstant = rateConstant, + timeInt = (0,timeSim[ii+1]),flowIn = flowIn) + for ii in tqdm(range(len(timeSim)-1))) + + # Convert the list to array + arrayConcentration = np.array(arrayConcentrationTemp) + # Add dummy row to be consistent (intialized to init condition) + firstRow = np.concatenate((np.array(sensorID), inputParameters[5])) + arrayConcentration = np.vstack([firstRow,arrayConcentration]) + # Stop time for time elapsed + stopTime = time.time() + # Time elapsed [s] + timeElapsed = stopTime - startTime # Check if simulationResults directory exists or not. If not, create the folder if not os.path.exists('simulationResults'): @@ -102,5 +151,8 @@ sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-').replace(',','') saveFileName = filePrefix + "_" + sensorText + "_" + currentDT + "_" + gitCommitID; savePath = os.path.join('simulationResults',saveFileName) -savez (savePath, outputStruct = outputStruct, - arrayConcentration = arrayConcentration) \ No newline at end of file +savez (savePath, outputStruct = outputStruct, # True response + arrayConcentration = arrayConcentration, # Estimated response + eqbmModelFlag = flagIndTime, # Flag to tell whether eqbm. or full model used + timeElapsed = timeElapsed, # Time elapsed for the simulation + hostName = socket.gethostname()) # Hostname of the computer \ No newline at end of file diff --git a/simulateFullModel.py b/simulateFullModel.py index d172d74..78bf158 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-01-25, AK: Change the time interval definition # - 2021-01-21, AK: Cosmetic changes # - 2021-01-20, AK: Change to output time and plot function # - 2021-01-20, AK: Cosmetic changes @@ -109,7 +110,7 @@ def simulateFullModel(**kwargs): # Prepare tuple of input parameters for the ode solver inputParameters = (sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, - pressureTotal, temperature, volSorbent, volGas) + initMoleFrac, pressureTotal, temperature, volSorbent, volGas) # Prepare initial conditions vector initialConditions = np.zeros([2*numberOfGases]) @@ -119,9 +120,9 @@ def simulateFullModel(**kwargs): # Solve the system of ordinary differential equations # Stiff solver used for the problem: BDF or Radau - # The output is print out every second + # The output is print out every 5 s outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='Radau', - t_eval = np.linspace(timeInt[0],timeInt[1],int((timeInt[1]-timeInt[0])/5)), + t_eval = np.arange(timeInt[0],timeInt[1],5), args = inputParameters) # Parse out the time and the output matrix @@ -152,7 +153,7 @@ def solveSorptionEquation(t, f, *inputParameters): Rg = 8.314; # [J/mol K] # Unpack the tuple of input parameters used to solve equations - sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, pressureTotal, temperature, volSorbent, volGas = inputParameters + sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero df = np.zeros([2*numberOfGases]) @@ -196,7 +197,7 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, saveFileExtension = ".png" # Unpack the tuple of input parameters used to solve equations - sensorID, _, _, flowIn, _, _, temperature, _, _ = inputParameters + sensorID, _, _, flowIn, _, _, _, temperature, _, _ = inputParameters os.chdir("plotFunctions") if resultMat.shape[0] == 6: From 848451b113b2293f9bdfea15761f50b0e9bc513a Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 26 Jan 2021 12:25:19 +0000 Subject: [PATCH 64/99] Add noise to raw data for full model --- estimateConcentration.py | 2 +- fullModelConcentrationEstimatorWrapper.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/estimateConcentration.py b/estimateConcentration.py index 04c3d4c..412b04e 100755 --- a/estimateConcentration.py +++ b/estimateConcentration.py @@ -126,7 +126,7 @@ def estimateConcentration(numberOfAdsorbents, numberOfGases, moleFracID, sensorI # eps to avoid division by zero # Print if any of the responses are negative if any(ii<=0. for ii in arrayTrueResponse): - print("Number of negative response: " + str(sum(arrayTrueResponse<0))) + print("Number of negative response: " + str(sum(arrayTrueResponse<=0))) arrayTrueResponse[arrayTrueResponse<=0.] = np.finfo(float).eps # Pack the input parameters/arguments useful to compute the objective diff --git a/fullModelConcentrationEstimatorWrapper.py b/fullModelConcentrationEstimatorWrapper.py index b0f4843..448165b 100644 --- a/fullModelConcentrationEstimatorWrapper.py +++ b/fullModelConcentrationEstimatorWrapper.py @@ -13,6 +13,7 @@ # measurement from the full model # # Last modified: +# - 2021-01-26, AK: Add noise to true measurement # - 2021-01-25, AK: Integrate full model concentration estimator # - 2021-01-21, AK: Initial creation # @@ -64,11 +65,15 @@ feedMoleFrac = [0.1,0.9,0.0] # [-] # Time span for integration [tuple with t0 and tf] [s] -timeInt = (0,35) # [s] +timeInt = (0,1000) # [s] # Volumetric flow rate [m3/s] flowIn = 5e-7 # [s] +# Measurement noise characteristics +meanNoise = 0.0 # [g/kg] +stdNoise = 0.1 # [g/kg] + # Loop over all rate constants outputStruct = {} for ii in tqdm(range(len(sensorID))): @@ -78,8 +83,17 @@ feedMoleFrac = feedMoleFrac, timeInt = timeInt, flowIn = flowIn) + + # Generate the noise data for all time instants and all materials + measurementNoise = np.random.normal(meanNoise, stdNoise, len(timeSim)) + sensorFingerPrintRaw = sensorFingerPrint # Raw sensor finger print + # Add measurement noise to the sensor finger print + sensorFingerPrint = sensorFingerPrint + measurementNoise + outputStruct[ii] = {'timeSim':timeSim, 'sensorFingerPrint':sensorFingerPrint, + 'sensorFingerPrintRaw':sensorFingerPrintRaw, # Without noise + 'measurementNoise':measurementNoise, 'inputParameters':inputParameters} # Check simulateFullModel.py for entries # Prepare time-resolved sendor finger print @@ -154,5 +168,6 @@ savez (savePath, outputStruct = outputStruct, # True response arrayConcentration = arrayConcentration, # Estimated response eqbmModelFlag = flagIndTime, # Flag to tell whether eqbm. or full model used + noiseCharacteristics = [meanNoise, stdNoise], # Noise characteristics timeElapsed = timeElapsed, # Time elapsed for the simulation hostName = socket.gethostname()) # Hostname of the computer \ No newline at end of file From 2cc67985fbd094ef572b208598ec4398a80bf174 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 26 Jan 2021 18:32:36 +0000 Subject: [PATCH 65/99] Plot function for concentration estimation comparison --- plotFunctions/plotConcentrationFullModel.py | 123 ++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 plotFunctions/plotConcentrationFullModel.py diff --git a/plotFunctions/plotConcentrationFullModel.py b/plotFunctions/plotConcentrationFullModel.py new file mode 100644 index 0000000..b7830f7 --- /dev/null +++ b/plotFunctions/plotConcentrationFullModel.py @@ -0,0 +1,123 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plotting function to compare the concentration estimates obtained from using +# the full model and one from assuming instantaneous equilibrium (non kinetics) +# +# Last modified: +# - 2021-01-26, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from numpy import load +import os +import auxiliaryFunctions +import matplotlib.pyplot as plt +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +# Save file extension (png or pdf) +saveFileExtension = ".png" + +# Save flag +saveFlag = True + +# Color combo +colorForMat = ["ff006e","3a86ff"] +colorForConc = ["fe7f2d","619b8a","233d4d"] + +# Y limits for the plot +X_LIMITS = [0.,1000.] +scaleLog = True + +# Legend First +legendText = ["Without Noise", "With Noise"] +legendFlag = False + +# File name for equilibrium model and full model estimates +loadFileName_E = "fullModelConcentrationEstimate_6-2_20210126_1810_848451b.npz" # Eqbm +loadFileName_F = "fullModelConcentrationEstimate_6-2_20210126_1434_848451b.npz" # Full model + +# Parse out equilbirum results file +simResultsFile = os.path.join('..','simulationResults',loadFileName_E); +loadedFile_E = load(simResultsFile, allow_pickle=True) +concentrationEstimate_E = loadedFile_E["arrayConcentration"] + +# Parse out full model results file +simResultsFile = os.path.join('..','simulationResults',loadFileName_F); +loadedFile_F = load(simResultsFile, allow_pickle=True) +concentrationEstimate_F = loadedFile_F["arrayConcentration"] + +# Parse out true responses (this should be the same for both eqbm and full +# model (here loaded from eqbm) +trueResponseStruct = loadedFile_F["outputStruct"].item() +# Parse out time +timeSim = [] +timeSim = trueResponseStruct[0]["timeSim"] +# Parse out feed mole fraction +feedMoleFrac = trueResponseStruct[0]["inputParameters"][4] +# Parse out true sensor finger print +sensorFingerPrint = np.zeros([len(timeSim),len(trueResponseStruct)]) +for ii in range(len(trueResponseStruct)): + sensorFingerPrint[:,ii] = trueResponseStruct[ii]["sensorFingerPrint"] + +# Points that will be taken for sampling (for plots) +lenSampling = 6 +fig = plt.figure +# Plot the true sensor response (using the full model) +ax = plt.subplot(1,2,1) +ax.plot(timeSim[0:len(timeSim):lenSampling], + sensorFingerPrint[0:len(timeSim):lenSampling,0], + marker = 'o', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color='#'+colorForMat[0], label = str(trueResponseStruct[0]["inputParameters"][0]).replace('[','').replace(']','')) +ax.plot(timeSim[0:len(timeSim):lenSampling], + sensorFingerPrint[0:len(timeSim):lenSampling,1], + marker = 'o', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color='#'+colorForMat[1], label = str(trueResponseStruct[1]["inputParameters"][0]).replace('[','').replace(']','')) +ax.locator_params(axis="x", nbins=4) +ax.locator_params(axis="y", nbins=4) +ax.set(xlabel='$t$ [s]', + ylabel='$m_i$ [g kg$^{\mathregular{-1}}$]', + xlim = X_LIMITS, ylim = [0, 30]) +ax.legend() + +# Plot the evolution of the gas composition with respect to time +ax = plt.subplot(1,2,2) +ax.plot([timeSim[0],timeSim[-1]], [feedMoleFrac[0],feedMoleFrac[0]], + linestyle = 'dashed', linewidth = 1., + color='#'+colorForConc[2]) +ax.plot(timeSim[0:len(timeSim):lenSampling], + concentrationEstimate_E[0:len(timeSim):lenSampling,2], + marker = 'v', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color='#'+colorForConc[0], label = 'Equilibrium') +ax.plot(timeSim[0:len(timeSim):lenSampling], + concentrationEstimate_F[0:len(timeSim):lenSampling,2], + marker = 'o', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color='#'+colorForConc[1], label = 'Full Model') +ax.locator_params(axis="x", nbins=4) +ax.locator_params(axis="y", nbins=4) +ax.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = X_LIMITS, ylim = [0, 0.2]) +ax.legend() +plt.show() \ No newline at end of file From 9861c482de351b163dde2766b2dc4f97460571c6 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 27 Jan 2021 15:36:30 +0000 Subject: [PATCH 66/99] Add volSorbent and volGas as inputs --- sensorFullModelWrapper.py | 2 +- simulateFullModel.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/sensorFullModelWrapper.py b/sensorFullModelWrapper.py index 3442b50..bc9da01 100644 --- a/sensorFullModelWrapper.py +++ b/sensorFullModelWrapper.py @@ -50,7 +50,7 @@ # Define a dictionary outputStruct = {} -# Loop over all rate constants +# Loop over all the individual elements of the loop variable for ii in tqdm(range(len(loopVariable))): # Call the full model with a given rate constant timeSim, _ , sensorFingerPrint, inputParameters = simulateFullModel(rateConstant = loopVariable[ii]) diff --git a/simulateFullModel.py b/simulateFullModel.py index 78bf158..c3acdb2 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-01-27, AK: Add volSorbent and volGas to inputs # - 2021-01-25, AK: Change the time interval definition # - 2021-01-21, AK: Cosmetic changes # - 2021-01-20, AK: Change to output time and plot function @@ -77,6 +78,18 @@ def simulateFullModel(**kwargs): timeInt = kwargs["timeInt"] else: timeInt = (0.0,2000) + + # Volume of sorbent material [m3] + if 'volSorbent' in kwargs: + volSorbent = kwargs["volSorbent"] + else: + volSorbent = 5e-7 + + # Volume of gas chamber (dead volume) [m3] + if 'volGas' in kwargs: + volGas = kwargs["volGas"] + else: + volGas = 5e-7 if (len(feedMoleFrac) != len(initMoleFrac) or len(feedMoleFrac) != len(rateConstant)): @@ -96,12 +109,6 @@ def simulateFullModel(**kwargs): temperature = np.array(kwargs["temperature"]); else: temperature = np.array([298.15]); - - # Volume of sorbent material [m3] - volSorbent = 5e-7 - - # Volume of gas chamber (dead volume) [m3] - volGas = 5e-7 # Compute the initial sensor loading [mol/m3] @ initMoleFrac sensorLoadingPerGasVol, adsorbentDensity, molecularWeight = simulateSensorArray(sensorID, pressureTotal, From 1c2a04a4e7d395ffddc6a6ad22f0d92924cbacb1 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sat, 30 Jan 2021 10:56:55 +0000 Subject: [PATCH 67/99] Add constant pressure model (check needed) --- simulateFullModel.py | 122 ++++++++++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 30 deletions(-) diff --git a/simulateFullModel.py b/simulateFullModel.py index c3acdb2..9ee6e67 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-01-30, AK: Add constant pressure model # - 2021-01-27, AK: Add volSorbent and volGas to inputs # - 2021-01-25, AK: Change the time interval definition # - 2021-01-21, AK: Cosmetic changes @@ -41,6 +42,13 @@ def simulateFullModel(**kwargs): # Get the current date and time for saving purposes currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Model flag (constant pressure or constant flow rate) + # Default is constant pressure + if 'sensorID' in kwargs: + modelConstF = kwargs["modelConstF"] + else: + modelConstF = False # Sensor ID if 'sensorID' in kwargs: @@ -52,7 +60,7 @@ def simulateFullModel(**kwargs): if 'rateConstant' in kwargs: rateConstant = np.array(kwargs["rateConstant"]) else: - rateConstant = np.array([10000.0,10000.0,10000.0]) + rateConstant = np.array([.01,.01,.01]) # Feed flow rate [m3/s] if 'flowIn' in kwargs: @@ -118,20 +126,31 @@ def simulateFullModel(**kwargs): # Prepare tuple of input parameters for the ode solver inputParameters = (sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, initMoleFrac, pressureTotal, temperature, volSorbent, volGas) - - # Prepare initial conditions vector - initialConditions = np.zeros([2*numberOfGases]) - initialConditions[0:numberOfGases-1] = initMoleFrac[0:numberOfGases-1] # Gas mole fraction - initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading - initialConditions[2*numberOfGases-1] = pressureTotal # Outlet pressure the same as inlet pressure - + # Solve the system of ordinary differential equations # Stiff solver used for the problem: BDF or Radau # The output is print out every 5 s - outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, method='Radau', - t_eval = np.arange(timeInt[0],timeInt[1],5), - args = inputParameters) - + # Solves the model assuming constant flow rate at the inlet and outlet + if modelConstF: + # Prepare initial conditions vector + initialConditions = np.zeros([2*numberOfGases]) + initialConditions[0:numberOfGases-1] = initMoleFrac[0:numberOfGases-1] # Gas mole fraction + initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading + initialConditions[2*numberOfGases-1] = pressureTotal # Outlet pressure the same as inlet pressure + outputSol = solve_ivp(solveSorptionEquationConstF, timeInt, initialConditions, + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), + args = inputParameters) + + # Solves the model assuming constant/negligible pressure across the sensor + else: + # Prepare initial conditions vector + initialConditions = np.zeros([2*numberOfGases]) + initialConditions[0:numberOfGases-1] = initMoleFrac[0:numberOfGases-1] # Gas mole fraction + initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading + outputSol = solve_ivp(solveSorptionEquationConstP, timeInt, initialConditions, + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), + args = inputParameters) + # Parse out the time and the output matrix timeSim = outputSol.t resultMat = outputSol.y @@ -145,14 +164,14 @@ def simulateFullModel(**kwargs): # Call the plotting function if plotFlag: plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, - gitCommitID, currentDT) + gitCommitID, currentDT, modelConstF) # Return time and the output matrix return timeSim, resultMat, sensorFingerPrint, inputParameters -# func: solveSorptionEquation +# func: solveSorptionEquationConstF - Constant flow rate model # Solves the system of ODEs to evaluate the gas composition, loadings, and pressure -def solveSorptionEquation(t, f, *inputParameters): +def solveSorptionEquationConstF(t, f, *inputParameters): import numpy as np from simulateSensorArray import simulateSensorArray @@ -176,6 +195,7 @@ def solveSorptionEquation(t, f, *inputParameters): df[numberOfGases-1:2*numberOfGases-1] = np.multiply(rateConstant,(sensorLoadingPerGasVol-f[numberOfGases-1:2*numberOfGases-1])) # Total mass balance + # Assumes constant flow rate, so pressure evalauted term1 = 1/volGas term2 = ((flowIn*pressureTotal) - (flowIn*f[2*numberOfGases-1]) - ((volSorbent*(Rg*temperature))*(np.sum(df[numberOfGases-1:2*numberOfGases-1])))) @@ -191,10 +211,50 @@ def solveSorptionEquation(t, f, *inputParameters): # Return the derivatives for the solver return df +# func: solveSorptionEquationConstP - Constant pressure model +# Solves the system of ODEs to evaluate the gas composition and loadings +def solveSorptionEquationConstP(t, f, *inputParameters): + import numpy as np + from simulateSensorArray import simulateSensorArray + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Unpack the tuple of input parameters used to solve equations + sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters + + # Initialize the derivatives to zero + df = np.zeros([2*numberOfGases]) + + # Compute the equilbirium loading at the current gas composition + currentGasComposition = np.concatenate((f[0:numberOfGases-1], + np.array([1.-np.sum(f[0:numberOfGases-1])]))) + sensorLoadingPerGasVol, _ , _ = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([currentGasComposition]), + fullModel = True) + + # Linear driving force model (derivative of solid phase loadings) + df[numberOfGases-1:2*numberOfGases-1] = np.multiply(rateConstant,(sensorLoadingPerGasVol-f[numberOfGases-1:2*numberOfGases-1])) + + # Total mass balance + # Assumes constant pressure, so flow rate evalauted + flowOut = flowIn - ((volSorbent*(Rg*temperature)/pressureTotal)*(np.sum(df[numberOfGases-1:2*numberOfGases-1]))) + + # Component mass balance + term1 = 1/volGas + for ii in range(numberOfGases-1): + term2 = ((flowIn*feedMoleFrac[ii] - flowOut*f[ii]) + - (volSorbent*(Rg*temperature)/pressureTotal)*df[ii+numberOfGases-1]) + df[ii] = term1*term2 + + # Return the derivatives for the solver + return df + + # func: plotFullModelResult # Plots the model output for the conditions simulated locally def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, - gitCommitID, currentDT): + gitCommitID, currentDT, modelConstF): import numpy as np import os import matplotlib.pyplot as plt @@ -248,25 +308,27 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file fig = plt.figure ax = plt.subplot(1,3,1) - ax.plot(timeSim, resultMat[5,:], - linewidth=1.5,color='r') + if modelConstF: + ax.plot(timeSim, resultMat[5,:], + linewidth=1.5,color='r') ax.set(xlabel='$t$ [s]', ylabel='$P$ [Pa]', xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])]) ax = plt.subplot(1,3,2) - ax.plot(timeSim, flowIn*resultMat[5,:]/(temperature*8.314), - linewidth=1.5,color='k') - ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[0,:]/(temperature*8.314), - linewidth=1.5,color='r') - ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[1,:]/(temperature*8.314), - linewidth=1.5,color='b') - ax.plot(timeSim, flowIn*resultMat[5,:]*(1-resultMat[0,:]-resultMat[1,:])/(temperature*8.314), - linewidth=1.5,color='g') - ax.set(xlabel='$t$ [s]', - ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', - xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])*(flowIn/temperature/8.314)]) - + if modelConstF: + ax.plot(timeSim, flowIn*resultMat[5,:]/(temperature*8.314), + linewidth=1.5,color='k') + ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[0,:]/(temperature*8.314), + linewidth=1.5,color='r') + ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[1,:]/(temperature*8.314), + linewidth=1.5,color='b') + ax.plot(timeSim, flowIn*resultMat[5,:]*(1-resultMat[0,:]-resultMat[1,:])/(temperature*8.314), + linewidth=1.5,color='g') + ax.set(xlabel='$t$ [s]', + ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])*(flowIn/temperature/8.314)]) + ax = plt.subplot(1,3,3) ax.plot(timeSim, resultMat[0,:],linewidth=1.5,color='r') ax.plot(timeSim, resultMat[1,:],linewidth=1.5,color='b') From 62e8c3bb827b2c50be0dc6cbbfa1747df9ec01b4 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sat, 30 Jan 2021 11:05:54 +0000 Subject: [PATCH 68/99] Minor bug fixes --- simulateFullModel.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/simulateFullModel.py b/simulateFullModel.py index 9ee6e67..3560532 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -45,7 +45,7 @@ def simulateFullModel(**kwargs): # Model flag (constant pressure or constant flow rate) # Default is constant pressure - if 'sensorID' in kwargs: + if 'modelConstF' in kwargs: modelConstF = kwargs["modelConstF"] else: modelConstF = False @@ -144,7 +144,7 @@ def simulateFullModel(**kwargs): # Solves the model assuming constant/negligible pressure across the sensor else: # Prepare initial conditions vector - initialConditions = np.zeros([2*numberOfGases]) + initialConditions = np.zeros([2*numberOfGases-1]) initialConditions[0:numberOfGases-1] = initMoleFrac[0:numberOfGases-1] # Gas mole fraction initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading outputSol = solve_ivp(solveSorptionEquationConstP, timeInt, initialConditions, @@ -224,7 +224,7 @@ def solveSorptionEquationConstP(t, f, *inputParameters): sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero - df = np.zeros([2*numberOfGases]) + df = np.zeros([2*numberOfGases-1]) # Compute the equilbirium loading at the current gas composition currentGasComposition = np.concatenate((f[0:numberOfGases-1], @@ -267,7 +267,7 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, sensorID, _, _, flowIn, _, _, _, temperature, _, _ = inputParameters os.chdir("plotFunctions") - if resultMat.shape[0] == 6: + if resultMat.shape[0] == 5 or resultMat.shape[0] == 6: # Plot the solid phase compositions plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file fig = plt.figure @@ -311,9 +311,9 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, if modelConstF: ax.plot(timeSim, resultMat[5,:], linewidth=1.5,color='r') - ax.set(xlabel='$t$ [s]', - ylabel='$P$ [Pa]', - xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])]) + ax.set(xlabel='$t$ [s]', + ylabel='$P$ [Pa]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])]) ax = plt.subplot(1,3,2) if modelConstF: From 5125cd826657a56d5e4e69f07891b7f8ef073dc9 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 2 Feb 2021 16:36:46 +0000 Subject: [PATCH 69/99] Add flow rate to output --- fullModelConcentrationEstimatorWrapper.py | 5 ++ simulateFullModel.py | 73 ++++++++++++++++------- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/fullModelConcentrationEstimatorWrapper.py b/fullModelConcentrationEstimatorWrapper.py index 448165b..7caec65 100644 --- a/fullModelConcentrationEstimatorWrapper.py +++ b/fullModelConcentrationEstimatorWrapper.py @@ -51,6 +51,11 @@ # (False) or not (True) flagIndTime = True +# Flag to determine whether constant pressure or constant flow rate model to +# be used +# (False) or not (True) +flagIndTime = True + # Sensor ID sensorID = [6,2] # [-] diff --git a/simulateFullModel.py b/simulateFullModel.py index 3560532..df35254 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-02-02, AK: Add flow rate to output # - 2021-01-30, AK: Add constant pressure model # - 2021-01-27, AK: Add volSorbent and volGas to inputs # - 2021-01-25, AK: Change the time interval definition @@ -141,6 +142,12 @@ def simulateFullModel(**kwargs): method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), args = inputParameters) + # Flow out vector in output + flowOutVec = flowIn * np.ones(len(outputSol.t)) # Constant flow rate + + # Parse out the output matrix and add flow rate + resultMat = np.row_stack((outputSol.y,flowOutVec)) + # Solves the model assuming constant/negligible pressure across the sensor else: # Prepare initial conditions vector @@ -151,16 +158,27 @@ def simulateFullModel(**kwargs): method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), args = inputParameters) - # Parse out the time and the output matrix + # Presure vector in output + pressureVec = pressureTotal * np.ones(len(outputSol.t)) # Constant pressure + + # Compute the outlet flow rate + dqdt = np.gradient(outputSol.y[numberOfGases-1:2*numberOfGases-1,:], + outputSol.t, axis=1) # Compute gradient of loading + sum_dqdt = np.sum(dqdt, axis=0) # Time resolved sum of gradient + flowOut = flowIn - ((volSorbent*(8.314*temperature)/pressureTotal)*(sum_dqdt)) + + # Parse out the output matrix and add flow rate + resultMat = np.row_stack((outputSol.y,pressureVec,flowOut)) + + # Parse out the time timeSim = outputSol.t - resultMat = outputSol.y # Compute the time resolved sensor response sensorFingerPrint = np.zeros([len(timeSim)]) for ii in range(len(timeSim)): loadingTemp = resultMat[numberOfGases-1:2*numberOfGases-1,ii] sensorFingerPrint[ii] = np.dot(loadingTemp,molecularWeight)/adsorbentDensity - + # Call the plotting function if plotFlag: plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, @@ -267,7 +285,7 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, sensorID, _, _, flowIn, _, _, _, temperature, _, _ = inputParameters os.chdir("plotFunctions") - if resultMat.shape[0] == 5 or resultMat.shape[0] == 6: + if resultMat.shape[0] == 7: # Plot the solid phase compositions plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file fig = plt.figure @@ -308,26 +326,35 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file fig = plt.figure ax = plt.subplot(1,3,1) - if modelConstF: - ax.plot(timeSim, resultMat[5,:], - linewidth=1.5,color='r') - ax.set(xlabel='$t$ [s]', - ylabel='$P$ [Pa]', - xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])]) - + ax.plot(timeSim, resultMat[5,:], + linewidth=1.5,color='r') + ax.set_xlabel('$t$ [s]') + ax.set_ylabel('$P$ [Pa]', color='r') + ax.tick_params(axis='y', labelcolor='r') + ax.set(xlim = [timeSim[0], timeSim[-1]], + ylim = [0, 1.1*np.max(resultMat[5,:])]) + + ax2 = plt.twinx() + ax2.plot(timeSim, resultMat[6,:], + linewidth=1.5,color='b') + ax2.set_ylabel('$F$ [m$^{\mathregular{3}}$ s$^{\mathregular{-1}}$]', color='b') + ax2.tick_params(axis='y', labelcolor='b') + ax2.set(xlim = [timeSim[0], timeSim[-1]], + ylim = [0, 1.1*np.max(resultMat[6,:])]) + ax = plt.subplot(1,3,2) - if modelConstF: - ax.plot(timeSim, flowIn*resultMat[5,:]/(temperature*8.314), - linewidth=1.5,color='k') - ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[0,:]/(temperature*8.314), - linewidth=1.5,color='r') - ax.plot(timeSim, flowIn*resultMat[5,:]*resultMat[1,:]/(temperature*8.314), - linewidth=1.5,color='b') - ax.plot(timeSim, flowIn*resultMat[5,:]*(1-resultMat[0,:]-resultMat[1,:])/(temperature*8.314), - linewidth=1.5,color='g') - ax.set(xlabel='$t$ [s]', - ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', - xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])*(flowIn/temperature/8.314)]) + ax.plot(timeSim, resultMat[5,:]*resultMat[6,:]/(temperature*8.314), + linewidth=1.5,color='k') + ax.plot(timeSim, resultMat[5,:]*resultMat[6,:]*resultMat[0,:]/(temperature*8.314), + linewidth=1.5,color='r') + ax.plot(timeSim, resultMat[5,:]*resultMat[6,:]*resultMat[1,:]/(temperature*8.314), + linewidth=1.5,color='b') + ax.plot(timeSim, resultMat[5,:]*resultMat[6,:]*(1-resultMat[0,:]-resultMat[1,:])/(temperature*8.314), + linewidth=1.5,color='g') + ax.set(xlabel='$t$ [s]', + ylabel='$Q$ [mol s$^{\mathregular{-1}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[5,:])*np.max(resultMat[6,:]) + /temperature/8.314]) ax = plt.subplot(1,3,3) ax.plot(timeSim, resultMat[0,:],linewidth=1.5,color='r') From a6a91389b90a9c6d163964cf3ba8369242d3bcf8 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 3 Feb 2021 18:56:34 +0000 Subject: [PATCH 70/99] Add total volume to full model and add adsorbent density to inputParameters --- fullModelConcentrationEstimatorWrapper.py | 3 ++- sensorFullModelWrapper.py | 17 +++++++++++------ simulateFullModel.py | 21 ++++++++++++++++----- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/fullModelConcentrationEstimatorWrapper.py b/fullModelConcentrationEstimatorWrapper.py index 7caec65..03699d4 100644 --- a/fullModelConcentrationEstimatorWrapper.py +++ b/fullModelConcentrationEstimatorWrapper.py @@ -13,6 +13,7 @@ # measurement from the full model # # Last modified: +# - 2021-02-03, AK: Change inputParameters (add adsorbent density) # - 2021-01-26, AK: Add noise to true measurement # - 2021-01-25, AK: Integrate full model concentration estimator # - 2021-01-21, AK: Initial creation @@ -153,7 +154,7 @@ # Convert the list to array arrayConcentration = np.array(arrayConcentrationTemp) # Add dummy row to be consistent (intialized to init condition) - firstRow = np.concatenate((np.array(sensorID), inputParameters[5])) + firstRow = np.concatenate((np.array(sensorID), inputParameters[6])) arrayConcentration = np.vstack([firstRow,arrayConcentration]) # Stop time for time elapsed stopTime = time.time() diff --git a/sensorFullModelWrapper.py b/sensorFullModelWrapper.py index bc9da01..b9df7e5 100644 --- a/sensorFullModelWrapper.py +++ b/sensorFullModelWrapper.py @@ -13,6 +13,7 @@ # variables # # Last modified: +# - 2021-02-03, AK: Change output plot response to absolute values # - 2021-01-20, AK: Initial creation # # Input arguments: @@ -45,7 +46,8 @@ # Define the variable to be looped # This has to be a be a tuple. For on condition write the values followed by a # comma to say its a tuple -loopVariable = ([0.001,0.001,0.001],) +volTotal = 5e-7 +loopVariable = (0.001,0.25,0.5,0.75,0.9) # Define a dictionary outputStruct = {} @@ -53,7 +55,8 @@ # Loop over all the individual elements of the loop variable for ii in tqdm(range(len(loopVariable))): # Call the full model with a given rate constant - timeSim, _ , sensorFingerPrint, inputParameters = simulateFullModel(rateConstant = loopVariable[ii]) + timeSim, _ , sensorFingerPrint, inputParameters = simulateFullModel(volTotal = volTotal, + voidFrac = loopVariable[ii]) outputStruct[ii] = {'timeSim':timeSim, 'sensorFingerPrint':sensorFingerPrint, 'inputParameters':inputParameters} @@ -65,13 +68,15 @@ ax = plt.subplot(1,1,1) for ii in range(len(loopVariable)): timeTemp = outputStruct[ii]["timeSim"] - fingerPrintTemp = outputStruct[ii]["sensorFingerPrint"] + fingerPrintTemp = (outputStruct[ii]["sensorFingerPrint"] + *outputStruct[ii]["inputParameters"][1] + *outputStruct[ii]["inputParameters"][9]) # Compute true response [g] ax.plot(timeTemp, fingerPrintTemp, linewidth=1.5,color="#"+colorsForPlot[ii], - label = str(loopVariable[ii][0])) + label = str(loopVariable[ii])) ax.set(xlabel='$t$ [s]', - ylabel='$m_i$ [g kg$^{\mathregular{-1}}$]', - xlim = [timeSim[0], timeSim[-1]], ylim = [0, 150]) + ylabel='$m_i$ [g]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, None]) ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) # Save the figure diff --git a/simulateFullModel.py b/simulateFullModel.py index df35254..ecf020f 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-02-03, AK: Add total volume and void fraction # - 2021-02-02, AK: Add flow rate to output # - 2021-01-30, AK: Add constant pressure model # - 2021-01-27, AK: Add volSorbent and volGas to inputs @@ -99,6 +100,16 @@ def simulateFullModel(**kwargs): volGas = kwargs["volGas"] else: volGas = 5e-7 + + # Total volume of the system [m3] + if 'volTotal' in kwargs: + volTotal = kwargs["volTotal"] + if 'voidFrac' in kwargs: + voidFrac = kwargs["voidFrac"] + else: + raise Exception("You should provide void fraction if you provide total volume!") + volGas = voidFrac * volTotal # Volume of gas chamber (dead volume) [m3] + volSorbent = (1-voidFrac) * volTotal # Volume of solid sorbent [m3] if (len(feedMoleFrac) != len(initMoleFrac) or len(feedMoleFrac) != len(rateConstant)): @@ -125,8 +136,8 @@ def simulateFullModel(**kwargs): fullModel = True) # Prepare tuple of input parameters for the ode solver - inputParameters = (sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, - initMoleFrac, pressureTotal, temperature, volSorbent, volGas) + inputParameters = (sensorID, adsorbentDensity, rateConstant, numberOfGases, flowIn, + feedMoleFrac, initMoleFrac, pressureTotal, temperature, volSorbent, volGas) # Solve the system of ordinary differential equations # Stiff solver used for the problem: BDF or Radau @@ -197,7 +208,7 @@ def solveSorptionEquationConstF(t, f, *inputParameters): Rg = 8.314; # [J/mol K] # Unpack the tuple of input parameters used to solve equations - sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters + sensorID, _ , rateConstant, numberOfGases, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero df = np.zeros([2*numberOfGases]) @@ -239,7 +250,7 @@ def solveSorptionEquationConstP(t, f, *inputParameters): Rg = 8.314; # [J/mol K] # Unpack the tuple of input parameters used to solve equations - sensorID, rateConstant, numberOfGases, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters + sensorID, _ , rateConstant, numberOfGases, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero df = np.zeros([2*numberOfGases-1]) @@ -282,7 +293,7 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, saveFileExtension = ".png" # Unpack the tuple of input parameters used to solve equations - sensorID, _, _, flowIn, _, _, _, temperature, _, _ = inputParameters + sensorID, _ , _ , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters os.chdir("plotFunctions") if resultMat.shape[0] == 7: From 156d5facbeeaa20abf8c255a9ba28655ad732aa3 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 11 Feb 2021 12:36:24 +0000 Subject: [PATCH 71/99] Fix for parallel computing --- screenSensorArray.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/screenSensorArray.py b/screenSensorArray.py index 48f7980..4b4dd2d 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -13,6 +13,7 @@ # sorbents are assumed to exhibit Langmuirian behavior. # # Last modified: +# - 2021-02-11, AK: Fix for parallel computing # - 2020-10-29, AK: Add 3 sorbent sensor # - 2020-10-28, AK: Add auxiliary functions as a module # - 2020-10-22, AK: Initial creation @@ -61,7 +62,7 @@ # Loop over all the sorbents for a single material sensor # Using parallel processing to loop through all the materials arrayConcentration = np.zeros(numberOfAdsorbents) - arrayConcentration = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + arrayConcentration = Parallel(n_jobs=num_cores)(delayed(estimateConcentration) (numberOfAdsorbents,numberOfGases,moleFracID,[ii]) for ii in tqdm(range(numberOfAdsorbents))) @@ -90,7 +91,7 @@ # Using parallel processing to loop through all the materials arrayConcentration = np.zeros(numberOfAdsorbents) for jj in range(numberOfAdsorbents-1): - arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + arrayConcentrationTemp = Parallel(n_jobs=num_cores)(delayed(estimateConcentration) (numberOfAdsorbents,numberOfGases,moleFracID,[ii,jj]) for ii in tqdm(range(jj+1,numberOfAdsorbents))) # Convert the output list to a matrix @@ -124,7 +125,7 @@ for kk in range(numberOfAdsorbents-1): for jj in range(kk+1,numberOfAdsorbents-1): - arrayConcentrationTemp = Parallel(n_jobs=num_cores, prefer="threads")(delayed(estimateConcentration) + arrayConcentrationTemp = Parallel(n_jobs=num_cores)(delayed(estimateConcentration) (numberOfAdsorbents,numberOfGases,moleFracID,[ii,jj,kk]) for ii in tqdm(range(jj+1,numberOfAdsorbents))) # Convert the output list to a matrix From 0472efb96e198bcd43ab9394facc098bd6e21f32 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 11 Feb 2021 12:50:59 +0000 Subject: [PATCH 72/99] Minor fix for sensor array screening --- screenSensorArray.py | 50 +++++++++++--------------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/screenSensorArray.py b/screenSensorArray.py index 4b4dd2d..4916be9 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -13,6 +13,7 @@ # sorbents are assumed to exhibit Langmuirian behavior. # # Last modified: +# - 2021-02-11, AK: Minor fixes # - 2021-02-11, AK: Fix for parallel computing # - 2020-10-29, AK: Add 3 sorbent sensor # - 2020-10-28, AK: Add auxiliary functions as a module @@ -27,7 +28,7 @@ ############################################################################ import numpy as np -from numpy import save +from numpy import savez import auxiliaryFunctions import multiprocessing # For parallel processing from joblib import Parallel, delayed # For parallel processing @@ -36,7 +37,7 @@ import os # Number of sensors in the array -numSensors = 2 +numSensors = 1 # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -46,12 +47,12 @@ # Total number of sensor elements/gases simulated and generated using # generateHypotheticalAdsorbents.py function -numberOfAdsorbents = 10 +numberOfAdsorbents = 20 numberOfGases = 2 # "True" gas composition that is exposed to the sensor array (0-4) # Check generateTrueSensorResponse.py for the actual concentrations -moleFracID = 0 +moleFracID = 1 # Check for number of sensors in the array if numSensors == 1: @@ -79,9 +80,6 @@ if not os.path.exists('simulationResults'): os.mkdir('simulationResults') - # Save the array ceoncentration obtained from estimateConcentration - save (savePath, arrayConcentration) - elif numSensors == 2: ##### FOR 2 SORBENT SENSOR ARRAY ##### # Get the current date and time for saving purposes @@ -100,19 +98,6 @@ arrayConcentration = arrayConcentrationTemp else: arrayConcentration = np.append(arrayConcentration,arrayConcentrationTemp, axis=0) - - # Save the array concentration into a native numpy file - # The .npy file is saved in a folder called simulationResults (hardcoded) - filePrefix = "arrayConcentration" - saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; - savePath = os.path.join('simulationResults',saveFileName) - - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists('simulationResults'): - os.mkdir('simulationResults') - - # Save the array ceoncentration obtained from estimateConcentration - save (savePath, arrayConcentration) elif numSensors == 3: ##### FOR 3 SORBENT SENSOR ARRAY ##### @@ -134,21 +119,10 @@ arrayConcentration = arrayConcentrationTemp else: arrayConcentration = np.append(arrayConcentration,arrayConcentrationTemp, axis=0) - - # Delete entries that use the same materials for all three sensors - # delRows = np.where(np.logical_and(arrayConcentration[:,0] == arrayConcentration[:,1], - # arrayConcentration[:,0] == arrayConcentration[:,2])) - # arrayConcentration = np.delete(arrayConcentration,delRows,axis=0) - - # Save the array concentration into a native numpy file - # The .npy file is saved in a folder called simulationResults (hardcoded) - filePrefix = "arrayConcentration" - saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; - savePath = os.path.join('simulationResults',saveFileName) - - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists('simulationResults'): - os.mkdir('simulationResults') - - # Save the array ceoncentration obtained from estimateConcentration - save (savePath, arrayConcentration) \ No newline at end of file + +# Save the array ceoncentration obtained from estimateConcentration +savez (savePath, arrayConcentration = arrayConcentration, # Estimated Concentration + numberOfAdsorbents = numberOfAdsorbents, # Estimated response + numberOfGases = numberOfGases, # Flag to gases to be sensed + numSensors = numSensors, # Number of sensors in the array + moleFracID = moleFracID) # Mole fraction \ No newline at end of file From b02f8c309123f527820058b786ec928c5a1725df Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 11 Feb 2021 13:00:51 +0000 Subject: [PATCH 73/99] More fixes --- screenSensorArray.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/screenSensorArray.py b/screenSensorArray.py index 4916be9..024801c 100755 --- a/screenSensorArray.py +++ b/screenSensorArray.py @@ -70,16 +70,6 @@ # Convert the output list to a matrix arrayConcentration = np.array(arrayConcentration) - # Save the array concentration into a native numpy file - # The .npy file is saved in a folder called simulationResults (hardcoded) - filePrefix = "arrayConcentration" - saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; - savePath = os.path.join('simulationResults',saveFileName) - - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists('simulationResults'): - os.mkdir('simulationResults') - elif numSensors == 2: ##### FOR 2 SORBENT SENSOR ARRAY ##### # Get the current date and time for saving purposes @@ -119,6 +109,16 @@ arrayConcentration = arrayConcentrationTemp else: arrayConcentration = np.append(arrayConcentration,arrayConcentrationTemp, axis=0) + +# Save the array concentration into a native numpy file +# The .npy file is saved in a folder called simulationResults (hardcoded) +filePrefix = "arrayConcentration" +saveFileName = filePrefix + "_" + simulationDT + "_" + gitCommitID; +savePath = os.path.join('simulationResults',saveFileName) + +# Check if inputResources directory exists or not. If not, create the folder +if not os.path.exists('simulationResults'): + os.mkdir('simulationResults') # Save the array ceoncentration obtained from estimateConcentration savez (savePath, arrayConcentration = arrayConcentration, # Estimated Concentration From 46d4ab5c6d82a9ce2e28c5813da492fdf08a8e97 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 12 Feb 2021 11:39:21 +0000 Subject: [PATCH 74/99] New plot function for figures of the simulation manuscript --- plotFunctions/plotsForArticle_Simulation.py | 167 ++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 plotFunctions/plotsForArticle_Simulation.py diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py new file mode 100644 index 0000000..9063a16 --- /dev/null +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -0,0 +1,167 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Plots for the simulation manuscript +# +# Last modified: +# - 2021-02-11, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def plotsForArticle_Simulation(**kwargs): + import auxiliaryFunctions + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Flag for saving figure + if 'saveFlag' in kwargs: + if kwargs["saveFlag"]: + saveFlag = kwargs["saveFlag"] + else: + saveFlag = False + + # Save file extension (png or pdf) + if 'saveFileExtension' in kwargs: + if kwargs["saveFileExtension"]: + saveFileExtension = kwargs["saveFileExtension"] + else: + saveFileExtension = ".png" + + # If sensor array plot needs to be plotted + if 'sensorArray' in kwargs: + if kwargs["sensorArray"]: + plotForArticle_SensorArray(gitCommitID, currentDT, + saveFlag, saveFileExtension) +# fun: plotForArticle_SensorArray +# Plots the histogram of gas compositions for a one and two material +# sensor array +def plotForArticle_SensorArray(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + from numpy import load + import os + import matplotlib.pyplot as plt + plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file + # For now load a given adsorbent isotherm material file + loadFileName = ("arrayConcentration_20210211_1818_b02f8c3.npz", # 1 material w/o constraint + "arrayConcentration_20210212_1055_b02f8c3.npz", # 2 material w/o constraint + "arrayConcentration_20210212_1050_b02f8c3.npz", # 1 material with constraint + "arrayConcentration_20210211_1822_b02f8c3.npz") # 2 material with constraint + + # Git commit id of the loaded isotherm file + simID_loadedFile = loadFileName[0][-11:-4] + + # Loop through the two files to get the histogram + for ii in range(len(loadFileName)): + # Create the file name with the path to be loaded + simResultsFile = os.path.join('..','simulationResults',loadFileName[ii]); + + # Check if the file with the adsorbent properties exist + if os.path.exists(simResultsFile): + resultOutput = load(simResultsFile)["arrayConcentration"][:,:] + if resultOutput.shape[1] == 3: + resultOutput = np.delete(resultOutput,[0],1) + elif resultOutput.shape[1] == 4: + resultOutput = np.delete(resultOutput,[0,1],1) + else: + errorString = "Simulation result file " + simResultsFile + " does not exist." + raise Exception(errorString) + + # Gas concentration + molFracG1 = 0.05 + molFracG2 = 0.95 + + # Xlimits and Ylimits + xLimits = [0,1] + yLimits = [0,60] + + # Histogram properties + nBins = 50 + rangeX = (xLimits) + histTypeX = 'stepfilled' + alphaX=0.75 + densityX = True + + # Plot the histogram of the gas compositions + ax = plt.subplot(2,2,ii+1) + # Histogram for 1 material array + ax.axvline(x=molFracG1, linewidth=1, linestyle='dotted', color = '#e5383b', alpha = 0.6) + ax.hist(resultOutput[:,0], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='#e5383b', alpha = alphaX, label = '$g_1$') + + # Histogram for 2 material array + ax.axvline(x=molFracG2, linewidth=1, linestyle='dotted', color = '#343a40', alpha = 0.6) + ax.hist(resultOutput[:,1], bins = nBins, range = rangeX, density = densityX, + linewidth=1.5, histtype = histTypeX, color='#343a40', alpha = alphaX, label = '$g_2$') + + ax.set(xlim = xLimits, ylim = yLimits) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + if ii == 0 or ii == 2: + ax.set(ylabel='$f$ [-]') + ax.text(0.85, 55, "$n$ = 1", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + if ii == 0: + ax.text(0.025, 55, "(a)", fontsize=10, + backgroundcolor = 'w') + ax.text(0.56, 51, "Without Constraint", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + else: + ax.set(xlabel='$y$ [-]') + ax.text(0.025, 55, "(c)", fontsize=10, + backgroundcolor = 'w') + ax.text(0.625, 51, "With Constraint", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + ax.text(0.085, 25, "$y_1$ = 0.05", fontsize=10, + backgroundcolor = 'w', color = '#e5383b') + ax.text(0.705, 25, "$y_2$ = 0.95", fontsize=10, + backgroundcolor = 'w', color = '#343a40') + elif ii == 1 or ii == 3: + ax.text(0.85, 55, "$n$ = 2", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + if ii == 1: + ax.text(0.025, 55, "(b)", fontsize=10, + backgroundcolor = 'w') + ax.text(0.56, 51, "Without Constraint", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + else: + ax.set(xlabel='$y$ [-]') + ax.text(0.025, 55, "(d)", fontsize=10, + backgroundcolor = 'w') + ax.text(0.625, 51, "With Constraint", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + ax.text(0.085, 25, "$y_1$ = 0.05", fontsize=10, + backgroundcolor = 'w', color = '#e5383b') + ax.text(0.705, 25, "$y_2$ = 0.95", fontsize=10, + backgroundcolor = 'w', color = '#343a40') + + # Save the figure + if saveFlag: + # FileName: sensorArray___ + saveFileName = "sensorArray_" + currentDT + "_" + gitCommitID + "_" + simID_loadedFile + saveFileExtension + savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','simulationManuscript')): + os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) + plt.savefig (savePath) + + # For the figure to be saved show should appear after the save + plt.show() \ No newline at end of file From 73b990675f2657d486518cd9fb73b9437e7b9fde Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 12 Feb 2021 11:43:12 +0000 Subject: [PATCH 75/99] Small fix for plots --- plotFunctions/plotsForArticle_Simulation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 9063a16..1581f3f 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -122,7 +122,7 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, if ii == 0: ax.text(0.025, 55, "(a)", fontsize=10, backgroundcolor = 'w') - ax.text(0.56, 51, "Without Constraint", fontsize=10, + ax.text(0.625, 51, "Without Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') else: ax.set(xlabel='$y$ [-]') @@ -140,7 +140,7 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, if ii == 1: ax.text(0.025, 55, "(b)", fontsize=10, backgroundcolor = 'w') - ax.text(0.56, 51, "Without Constraint", fontsize=10, + ax.text(0.625, 51, "Without Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') else: ax.set(xlabel='$y$ [-]') From 264077f8463f8a3c1f80496d8fe733bb81fd6479 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 12 Feb 2021 11:46:13 +0000 Subject: [PATCH 76/99] Revert back fix --- plotFunctions/plotsForArticle_Simulation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 1581f3f..9063a16 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -122,7 +122,7 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, if ii == 0: ax.text(0.025, 55, "(a)", fontsize=10, backgroundcolor = 'w') - ax.text(0.625, 51, "Without Constraint", fontsize=10, + ax.text(0.56, 51, "Without Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') else: ax.set(xlabel='$y$ [-]') @@ -140,7 +140,7 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, if ii == 1: ax.text(0.025, 55, "(b)", fontsize=10, backgroundcolor = 'w') - ax.text(0.625, 51, "Without Constraint", fontsize=10, + ax.text(0.56, 51, "Without Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') else: ax.set(xlabel='$y$ [-]') From 29daa54b9c869033c1f361af783952f12cdeac14 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 12 Feb 2021 19:24:53 +0000 Subject: [PATCH 77/99] Add sensor response curve plot --- plotFunctions/plotsForArticle_Simulation.py | 182 +++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 9063a16..69c1962 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -50,6 +50,13 @@ def plotsForArticle_Simulation(**kwargs): if kwargs["sensorArray"]: plotForArticle_SensorArray(gitCommitID, currentDT, saveFlag, saveFileExtension) + + # If sensor response curve needs to be plotted + if 'responseShape' in kwargs: + if kwargs["responseShape"]: + plotForArticle_ResponseShape(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # fun: plotForArticle_SensorArray # Plots the histogram of gas compositions for a one and two material # sensor array @@ -164,4 +171,177 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, plt.savefig (savePath) # For the figure to be saved show should appear after the save - plt.show() \ No newline at end of file + plt.show() + + +# fun: plotForArticle_SensorArray +# Plots the histogram of gas compositions for a one and two material +# sensor array +def plotForArticle_ResponseShape(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import os + import pandas as pd + import seaborn as sns + import matplotlib.pyplot as plt + from simulateSensorArray import simulateSensorArray + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + + # Total pressure of the gas [Pa] + pressureTotal = np.array([1.e5]); + + # Temperature of the gas [K] + # Can be a vector of temperatures + temperature = np.array([298.15]); + + # Materials to be plotted + sensorID = np.array([17,16,6]) + sensorText = ["A", "B", "C"] + + # File to be loaded for the left of violin plot + loadFileName = ["sensitivityAnalysis_17_20210212_1259_b02f8c3.npz", # No Noise + "sensitivityAnalysis_16_20210212_1300_b02f8c3.npz", # No Noise + "sensitivityAnalysis_6_20210212_1259_b02f8c3.npz"] # No Noise + # "sensitivityAnalysis_17_20210212_1355_b02f8c3.npz", # Noise + # "sensitivityAnalysis_16_20210212_1356_b02f8c3.npz" # Noise + # "sensitivityAnalysis_6_20210212_1355_b02f8c3.npz"] # Noise + + # Colors for plot + colorsForPlot = ("#5fad56","#f78154","#b4436c") + + # Simulate the sensor response for all possible concentrations + # Number of molefractions + numMolFrac= 101 + moleFractionRange = np.array([np.linspace(0,1,numMolFrac), 1 - np.linspace(0,1,numMolFrac)]).T + + arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) + os.chdir("..") + for ii in range(moleFractionRange.shape[0]): + arraySimResponse[ii,:] = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([moleFractionRange[ii,:]])) + os.chdir("plotFunctions") + + fig = plt.figure + ax1 = plt.subplot(1,2,1) + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + ax1.plot(moleFractionRange[:,0],arraySimResponse[:,kk], + color=colorsForPlot[kk]) # Simulated Response + + ax1.set(xlabel='$y_1$ [-]', + ylabel='$m$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 300]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.text(0.03, 275, "(a)", fontsize=10, + backgroundcolor = 'w') + + # Label for the materials + ax1.text(0.9, 225, sensorText[0], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax1.text(0.05, 150, sensorText[1], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + ax1.text(0.8, 75, sensorText[2], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[2]) + + # CV - No noise + ax2 = plt.subplot(1,2,2) + # Call the concatenateConcEstimate function + cvData = concatenateConcEstimate(loadFileName[0:3],sensorText) + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1),(1,1),(1,1)], markers = ['o','s','D'], + palette = colorsForPlot[0:len(loadFileName)], linewidth = 0.5) + + ax2.set(xlabel='$y_1$ [-]', + ylabel='$\chi$ [-]', + xlim = [0.,1.], ylim = [1e-10,100]) + ax2.locator_params(axis="x", nbins=4) + ax2.set_yscale('log') + plt.legend([],[], frameon=False) + ax2.text(0.03, 10, "(b)", fontsize=10, + backgroundcolor = 'w') + + # Label for the materials + ax2.text(0.9, 6e-2, sensorText[0], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax2.text(0.8, 4e-4, sensorText[1], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + ax2.text(0.6, 2e-6, sensorText[2], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[2]) + + # Save the figure + if saveFlag: + # FileName: responseShape___ + sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-').replace(' ','-') + saveFileName = "responseShape_" + sensorText + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','simulationManuscript')): + os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) + plt.savefig (savePath) + plt.show() + +# fun: concatenateConcEstimate +# Concatenates concentration estimates into a panda dataframe and computes +# the coefficient of variation +def concatenateConcEstimate(loadFileName,sensorText): + import numpy as np + from numpy import load + import os + import pandas as pd + + # Initialize x, y, and type for the plotting + concatenatedX = [] + concatenatedY1 = [] + concatenatedY2 = [] + concatenatedType = [] + + # Loop through the different files to generate the violin plot + for kk in range(len(loadFileName)): + # Initialize x, y, and type for the local loop + xVar = [] + y1Var = [] + y2Var = [] + typeVar = [] + + simResultsFile = os.path.join('..','simulationResults',loadFileName[kk]); + resultOutput = load(simResultsFile)["arrayConcentration"] + moleFrac = load(simResultsFile)["trueMoleFrac"] + + # Loop through all the molefractions + for ii in range(resultOutput.shape[0]): + counterInd = -1 + + y1Var = np.concatenate((y1Var,resultOutput[ii,:,counterInd+2])) # y1 + y2Var = np.concatenate((y2Var,resultOutput[ii,:,counterInd+3])) # y2 + xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) + typeVar = typeVar+[sensorText[kk]] * len(resultOutput[ii,:,counterInd+2]) + + # Concatenate all the data to form a data frame with x, y, and type + concatenatedX = concatenatedX + xVar + concatenatedY1 = np.concatenate((concatenatedY1,y1Var)) + concatenatedY2 = np.concatenate((concatenatedY2,y2Var)) + concatenatedType = concatenatedType + typeVar + # Reinitialize all the loaded values to empty variable + simResultsFile = [] + resultOutput = [] + moleFrac = [] + + # Generate panda data frame + # x = molefraction (true) + # y = molefraction (estimated) + # dataType = either sensor id/comparison type + df = pd.DataFrame({'x':concatenatedX, + 'y1':concatenatedY1, + 'y2':concatenatedY2, + 'dataType':concatenatedType}) + # Compute the mean and standard deviation + meanData = df.groupby(['dataType','x'], as_index=False, sort=False).mean() + stdData = df.groupby(['dataType','x'], as_index=False, sort=False).std() + # Coefficient of variation + cvData = stdData.copy() + cvData['y1'] = stdData['y1']/meanData['y1'] + + # Return the coefficient of variation + return cvData \ No newline at end of file From 67817505e40de1629111fe93942414fe5ae38c1c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 18 Feb 2021 18:35:40 +0000 Subject: [PATCH 78/99] Function to simulate a volumetric adsorption system --- simulateVolumetricSystem.py | 214 ++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 simulateVolumetricSystem.py diff --git a/simulateVolumetricSystem.py b/simulateVolumetricSystem.py new file mode 100644 index 0000000..f6e5bbb --- /dev/null +++ b/simulateVolumetricSystem.py @@ -0,0 +1,214 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Simulates a volumetric system that can be used to estimate pure component +# isotherm and kinetics +# +# Last modified: +# - 2021-02-18, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def simulateVolumetricSystem(**kwargs): + import numpy as np + from scipy.integrate import solve_ivp + import auxiliaryFunctions + + # Plot flag + plotFlag = False + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Sensor ID + if 'sensorID' in kwargs: + sensorID = np.array([kwargs["sensorID"]]) + else: + sensorID = np.array([6]) + + # Time span for integration [tuple with t0 and tf] + if 'timeInt' in kwargs: + timeInt = kwargs["timeInt"] + else: + timeInt = (0.0,2000) + + # Kinetic rate constant [/s] - Only one constant (pure gas) + if 'rateConstant' in kwargs: + rateConstant = np.array(kwargs["rateConstant"]) + else: + rateConstant = np.array([.001]) + + # Valve rate constant [mol/s/Pa] - Only one constant (pure gas) + if 'valveConstant' in kwargs: + valveConstant = np.array(kwargs["valveConstant"]) + else: + valveConstant = np.array([0.04e5]) # From 10.1021/la026451+ (different units) + + # Temperature of the gas [K] + # Can be a vector of temperatures + if 'temperature' in kwargs: + temperature = np.array(kwargs["temperature"]); + else: + temperature = np.array([298.15]); + + # Dosing volume pressure [Pa] + if 'pressDose' in kwargs: + pressDose = np.array(kwargs["pressDose"]); + else: + pressDose = np.array([1e5]); + + # Uptake volume pressure [Pa] + if 'pressUptake' in kwargs: + pressUptake = np.array(kwargs["pressUptake"]); + else: + pressUptake = np.array([0.95e5]); + + # Dosing volume of the setup [m3] + if 'volDose' in kwargs: + volDose = np.array(kwargs["volDose"]); + else: + volDose = np.array([385e-6]); # From 10.1021/la026451+ (different units) + + # Uptake volume of the setup [m3] + if 'volUptake' in kwargs: + volUptake = np.array(kwargs["volUptake"]); + else: + volUptake = np.array([366e-6]); # From 10.1021/la026451+ (different units) + + # Uptake voidFrac of the uptake volume [-] + if 'voidFrac' in kwargs: + voidFrac = np.array(kwargs["voidFrac"]); + else: + voidFrac = np.array([0.98]); # From 10.1021/la026451+ (calculated) + + # Initial Gas Mole Fraction [-] + # Only pure gas system for volumetric system (DO NOT CHANGE) + initMoleFrac = np.array([1.,0.]) + + # Number of gases - Note this is only for loading purposes + # Only pure gas is simulated + # Used to chose the isotherm properties from the inputResources folder + numberOfGases = len(initMoleFrac) + + # Prepare tuple of input parameters for the ode solver + inputParameters = (sensorID, rateConstant, valveConstant, + numberOfGases, initMoleFrac, temperature, voidFrac, + volDose, volUptake) + + # Initial conditions for the solver + initialConditions = np.zeros([4]) + initialConditions[0] = pressDose # Dosing pressure [Pa] + initialConditions[1] = pressUptake # Uptake pressure [Pa] + # 2 and 3 are solid loading and the moles through the valve + + # Solve the system of ordinary differential equations + # Stiff solver used for the problem: BDF or Radau + outputSol = solve_ivp(solveVolumetricSystemEquation, timeInt, initialConditions, + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],1), + args = inputParameters) + + # Parse out the time and the pressure and loading from the output + timeSim = outputSol.t + resultMat = outputSol.y + + # Call the plotting function + if plotFlag: + plotFullModelResult(timeSim, resultMat, gitCommitID, currentDT) + + # Return time, the output matrix, and the parameters used in the simulation + return timeSim, resultMat, inputParameters + +# func: solveVolumetricSystemEquation +# Solves the system of ODEs to simulate the volumetric system +def solveVolumetricSystemEquation(t, f, *inputParameters): + import numpy as np + from simulateSensorArray import simulateSensorArray + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Unpack the tuple of input parameters used to solve equations + sensorID, rateConstant, valveConstant, numberOfGases, initMoleFrac, temperature, voidFrac, volDose, volUptake = inputParameters + + # Initialize the derivatives to zero + df = np.zeros([4]) + + sensorLoadingPerGasVol = simulateSensorArray(sensorID, f[1], + temperature, np.array([initMoleFrac])) + + # Linear driving force model (derivative of solid phase loadings) + df[2] = rateConstant*(sensorLoadingPerGasVol[0]-f[2]) + + # Valve flow model (derivative of gas flow through the valve) + df[3] = valveConstant*(f[0]-f[1]) + + # Rate of change of pressure in the dosing volume + df[0] = -(Rg*temperature/volDose)*df[3] + + # Rate of change of pressure in the uptake volume + df[1] = (Rg*temperature/(volUptake*voidFrac))*(df[3] - (1-voidFrac)*volUptake*df[2]) + + # Return the derivatives for the solver + return df + +# func: plotFullModelResult +# Plots the model output for the conditions simulated locally +def plotFullModelResult(timeSim, resultMat, gitCommitID, currentDT): + import numpy as np + import os + import matplotlib.pyplot as plt + + # Save settings + saveFlag = False + saveFileExtension = ".png" + + os.chdir("plotFunctions") + # Plot the solid phase compositions + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax = plt.subplot(1,2,1) + ax.plot(timeSim, resultMat[0,:]/resultMat[0,0], + linewidth=1.5,color='r', label = 'Dosing') + ax.plot(timeSim, resultMat[1,:]/resultMat[0,0], + linewidth=1.5,color='b', label = 'Uptake') + ax.set(xlabel='$t$ [s]', + ylabel='$P/P_0$ [-]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0., 1.]) + + ax = plt.subplot(1,2,2) + ax.plot(timeSim, resultMat[2,:], + linewidth=1.5,color='k') + ax.set(xlabel='$t$ [s]', + ylabel='$q$ [mol m$^{\mathregular{-3}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[2,:])]) + + # Save the figure + # if saveFlag: + # # FileName: solidLoadingFM___ + # saveFileName = "solidLoadingFM_" + str(sensorID) + "_" + currentDT + "_" + gitCommitID + saveFileExtension + # savePath = os.path.join('..','simulationFigures',saveFileName.replace('[','').replace(']','')) + # # Check if inputResources directory exists or not. If not, create the folder + # if not os.path.exists(os.path.join('..','simulationFigures')): + # os.mkdir(os.path.join('..','simulationFigures')) + # plt.savefig (savePath) + + plt.show() + + os.chdir("..") \ No newline at end of file From 7ab3ae0558cb83ebb9d48cbb7bc347aa0231a492 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 19 Feb 2021 15:48:40 +0000 Subject: [PATCH 79/99] Add relative tolerances for the full sensor model --- simulateFullModel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simulateFullModel.py b/simulateFullModel.py index ecf020f..31748bc 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-02-19, AK: Add relative tolerances for ode solver # - 2021-02-03, AK: Add total volume and void fraction # - 2021-02-02, AK: Add flow rate to output # - 2021-01-30, AK: Add constant pressure model @@ -151,7 +152,7 @@ def simulateFullModel(**kwargs): initialConditions[2*numberOfGases-1] = pressureTotal # Outlet pressure the same as inlet pressure outputSol = solve_ivp(solveSorptionEquationConstF, timeInt, initialConditions, method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), - args = inputParameters) + rtol = 1e-6, args = inputParameters) # Flow out vector in output flowOutVec = flowIn * np.ones(len(outputSol.t)) # Constant flow rate @@ -167,7 +168,7 @@ def simulateFullModel(**kwargs): initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading outputSol = solve_ivp(solveSorptionEquationConstP, timeInt, initialConditions, method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), - args = inputParameters) + rtol = 1e-6, args = inputParameters) # Presure vector in output pressureVec = pressureTotal * np.ones(len(outputSol.t)) # Constant pressure @@ -279,7 +280,6 @@ def solveSorptionEquationConstP(t, f, *inputParameters): # Return the derivatives for the solver return df - # func: plotFullModelResult # Plots the model output for the conditions simulated locally def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, From 56fa7787272d7c2d5510254631d4a1167c361809 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 19 Feb 2021 15:49:09 +0000 Subject: [PATCH 80/99] Fix for ode solver and add mass balance for volumetric system --- simulateVolumetricSystem.py | 109 +++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 28 deletions(-) diff --git a/simulateVolumetricSystem.py b/simulateVolumetricSystem.py index f6e5bbb..5161941 100644 --- a/simulateVolumetricSystem.py +++ b/simulateVolumetricSystem.py @@ -13,6 +13,7 @@ # isotherm and kinetics # # Last modified: +# - 2021-02-19, AK: Fix for ode solver and add mass balance (pressure) # - 2021-02-18, AK: Initial creation # # Input arguments: @@ -27,6 +28,7 @@ def simulateVolumetricSystem(**kwargs): import numpy as np from scipy.integrate import solve_ivp import auxiliaryFunctions + from simulateSensorArray import simulateSensorArray # Plot flag plotFlag = False @@ -47,13 +49,13 @@ def simulateVolumetricSystem(**kwargs): if 'timeInt' in kwargs: timeInt = kwargs["timeInt"] else: - timeInt = (0.0,2000) + timeInt = (0.0,500) # Kinetic rate constant [/s] - Only one constant (pure gas) if 'rateConstant' in kwargs: rateConstant = np.array(kwargs["rateConstant"]) else: - rateConstant = np.array([.001]) + rateConstant = np.array([0.1]) # Valve rate constant [mol/s/Pa] - Only one constant (pure gas) if 'valveConstant' in kwargs: @@ -78,7 +80,7 @@ def simulateVolumetricSystem(**kwargs): if 'pressUptake' in kwargs: pressUptake = np.array(kwargs["pressUptake"]); else: - pressUptake = np.array([0.95e5]); + pressUptake = np.array([0.e5]); # Dosing volume of the setup [m3] if 'volDose' in kwargs: @@ -111,29 +113,42 @@ def simulateVolumetricSystem(**kwargs): inputParameters = (sensorID, rateConstant, valveConstant, numberOfGases, initMoleFrac, temperature, voidFrac, volDose, volUptake) - + + # Obtain the initial solid loading in the material for initialization + materialLoadingPerGasVol = simulateSensorArray(sensorID, pressUptake, + temperature, np.array([initMoleFrac])) + # Initial conditions for the solver initialConditions = np.zeros([4]) initialConditions[0] = pressDose # Dosing pressure [Pa] initialConditions[1] = pressUptake # Uptake pressure [Pa] - # 2 and 3 are solid loading and the moles through the valve + initialConditions[2] = materialLoadingPerGasVol[0] # Initial solid loading [mol/m3] + # 2 is the moles through the valve # Solve the system of ordinary differential equations # Stiff solver used for the problem: BDF or Radau outputSol = solve_ivp(solveVolumetricSystemEquation, timeInt, initialConditions, - method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],1), - args = inputParameters) + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],0.01), + rtol = 1e-6, args = inputParameters) # Parse out the time and the pressure and loading from the output timeSim = outputSol.t resultMat = outputSol.y + # Compute the mass adsrobed based on mass balance (with pressures) + massAdsorbed = estimateMassAdsorbed(resultMat[0,:], resultMat[1,:], + materialLoadingPerGasVol[0]*(1-voidFrac)*volUptake, + volDose, volUptake, voidFrac, + temperature) + # Call the plotting function if plotFlag: - plotFullModelResult(timeSim, resultMat, gitCommitID, currentDT) + plotInputs = (timeSim, resultMat, massAdsorbed, voidFrac, volUptake, + sensorID, gitCommitID, currentDT) + plotFullModelResult(plotInputs) # Return time, the output matrix, and the parameters used in the simulation - return timeSim, resultMat, inputParameters + return timeSim, resultMat, massAdsorbed, inputParameters # func: solveVolumetricSystemEquation # Solves the system of ODEs to simulate the volumetric system @@ -150,11 +165,11 @@ def solveVolumetricSystemEquation(t, f, *inputParameters): # Initialize the derivatives to zero df = np.zeros([4]) - sensorLoadingPerGasVol = simulateSensorArray(sensorID, f[1], - temperature, np.array([initMoleFrac])) + materialLoadingPerGasVol = simulateSensorArray(sensorID, f[1], + temperature, np.array([initMoleFrac])) # Linear driving force model (derivative of solid phase loadings) - df[2] = rateConstant*(sensorLoadingPerGasVol[0]-f[2]) + df[2] = rateConstant*(materialLoadingPerGasVol[0]-f[2]) # Valve flow model (derivative of gas flow through the valve) df[3] = valveConstant*(f[0]-f[1]) @@ -167,14 +182,41 @@ def solveVolumetricSystemEquation(t, f, *inputParameters): # Return the derivatives for the solver return df + +# func: estimateMassAdsorbed +# Given the pressure from the two volumes, the volumes and temperature +# compute the mass adsorbed +def estimateMassAdsorbed(pressDoseALL, pressUptakeALL, initMass, + volDose, volUptake, voidFrac, temperature): + import numpy as np + + # Gas constant + Rg = 8.314; # [J/mol K] + # Compute the pressure difference over time on dosing side + delPressDosing = pressDoseALL[0] - pressDoseALL # Pd(0) - Pd(t) + + # Compute the pressure difference over time on uptake side + delPressUptake = pressUptakeALL - pressUptakeALL[0] # Pu(t) - Pu(0) + + # Calculate mass adsorbed + # massAdsorbed = Initial mass adsorbed + uptake + massAdsorbed = initMass + (np.multiply(delPressDosing,(volDose/(Rg*temperature))) + - np.multiply(delPressUptake,((voidFrac*volUptake)/(Rg*temperature)))) + + # Return the mass adsorbed + return massAdsorbed + # func: plotFullModelResult # Plots the model output for the conditions simulated locally -def plotFullModelResult(timeSim, resultMat, gitCommitID, currentDT): +def plotFullModelResult(plotInputs): import numpy as np import os import matplotlib.pyplot as plt + # Unpack the inputs + timeSim, resultMat, massAdsorbed, voidFrac, volUptake, sensorID, gitCommitID, currentDT = plotInputs + # Save settings saveFlag = False saveFileExtension = ".png" @@ -191,24 +233,35 @@ def plotFullModelResult(timeSim, resultMat, gitCommitID, currentDT): ax.set(xlabel='$t$ [s]', ylabel='$P/P_0$ [-]', xlim = [timeSim[0], timeSim[-1]], ylim = [0., 1.]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + ax.legend() ax = plt.subplot(1,2,2) - ax.plot(timeSim, resultMat[2,:], - linewidth=1.5,color='k') + volAds = (1-voidFrac)*volUptake # Volume of adsorbent [m3] + ax.plot(timeSim, resultMat[2,:]*volAds, + linewidth=1.5,color='k', + label = 'From model') # Uptake estimated from the model + ax.plot(timeSim, massAdsorbed,'--', + linewidth=1.5,color='r', + label = 'From pressure') # Uptake estimated from mass balance with pressures ax.set(xlabel='$t$ [s]', - ylabel='$q$ [mol m$^{\mathregular{-3}}$]', - xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[2,:])]) - + ylabel='$q$ [mol]', + xlim = [timeSim[0], timeSim[-1]], + ylim = [0.9*np.min(resultMat[2,:])*volAds, + 1.1*np.max(resultMat[2,:])*volAds]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) + ax.legend() + # Save the figure - # if saveFlag: - # # FileName: solidLoadingFM___ - # saveFileName = "solidLoadingFM_" + str(sensorID) + "_" + currentDT + "_" + gitCommitID + saveFileExtension - # savePath = os.path.join('..','simulationFigures',saveFileName.replace('[','').replace(']','')) - # # Check if inputResources directory exists or not. If not, create the folder - # if not os.path.exists(os.path.join('..','simulationFigures')): - # os.mkdir(os.path.join('..','simulationFigures')) - # plt.savefig (savePath) - + if saveFlag: + # FileName: solidLoadingFM___ + saveFileName = "volumetricSysFM_" + str(sensorID) + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures',saveFileName.replace('[','').replace(']','')) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures')): + os.mkdir(os.path.join('..','simulationFigures')) + plt.savefig (savePath) plt.show() - os.chdir("..") \ No newline at end of file From 48eeb91ccdf8a5d15cbe5e8cbf4d4dbf807bb747 Mon Sep 17 00:00:00 2001 From: Rajagopalan Date: Tue, 23 Feb 2021 18:29:03 +0000 Subject: [PATCH 81/99] Add mean error to sensor shape plot --- plotFunctions/plotsForArticle_Simulation.py | 78 ++++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 69c1962..894479b 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-02-23, AK: Add mean error to sensor shape plot # - 2021-02-11, AK: Initial creation # # Input arguments: @@ -54,9 +55,9 @@ def plotsForArticle_Simulation(**kwargs): # If sensor response curve needs to be plotted if 'responseShape' in kwargs: if kwargs["responseShape"]: - plotForArticle_ResponseShape(gitCommitID, currentDT, + meanErr = plotForArticle_ResponseShape(gitCommitID, currentDT, saveFlag, saveFileExtension) - + # fun: plotForArticle_SensorArray # Plots the histogram of gas compositions for a one and two material # sensor array @@ -173,7 +174,6 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, # For the figure to be saved show should appear after the save plt.show() - # fun: plotForArticle_SensorArray # Plots the histogram of gas compositions for a one and two material # sensor array @@ -222,7 +222,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, os.chdir("plotFunctions") fig = plt.figure - ax1 = plt.subplot(1,2,1) + ax1 = plt.subplot(1,3,1) # Loop through all sensors for kk in range(arraySimResponse.shape[1]): ax1.plot(moleFractionRange[:,0],arraySimResponse[:,kk], @@ -233,43 +233,76 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, xlim = [0,1], ylim = [0, 300]) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) - ax1.text(0.03, 275, "(a)", fontsize=10, + ax1.text(0.05, 270, "(a)", fontsize=10, backgroundcolor = 'w') # Label for the materials - ax1.text(0.9, 225, sensorText[0], fontsize=10, + ax1.text(0.85, 225, sensorText[0], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[0]) - ax1.text(0.05, 150, sensorText[1], fontsize=10, + ax1.text(0.1, 150, sensorText[1], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[1]) ax1.text(0.8, 75, sensorText[2], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[2]) + + # Call the concatenateConcEstimate function + meanErr, cvData = concatenateConcEstimate(loadFileName[0:3],sensorText) + + # Mean Error - No noise + ax2 = plt.subplot(1,3,2) + meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") + sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1),(1,1),(1,1)], markers = ['o','s','D'], + palette = colorsForPlot[0:len(loadFileName)], linewidth = 0.5) + + ax2.set(xlabel='$y_1$ [-]', + ylabel='$\psi$ [-]', + xlim = [0.,1.], ylim = [1e-10,100]) + ax2.locator_params(axis="x", nbins=4) + ax2.set_yscale('log') + plt.legend([],[], frameon=False) + ax2.text(0.05, 8, "(b)", fontsize=10, + backgroundcolor = 'w') + + # Label for the materials + ax2.text(0.85, 6e-3, sensorText[0], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax2.text(0.3, 4e-4, sensorText[1], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + ax2.text(0.6, 5e-7, sensorText[2], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[2]) + + # Label for the formula + ax2.text(0.38, 8, "$\psi = |\mu - \hat{\mu}|/\mu$", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') # CV - No noise - ax2 = plt.subplot(1,2,2) - # Call the concatenateConcEstimate function - cvData = concatenateConcEstimate(loadFileName[0:3],sensorText) + ax3 = plt.subplot(1,3,3) cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1),(1,1),(1,1)], markers = ['o','s','D'], palette = colorsForPlot[0:len(loadFileName)], linewidth = 0.5) - ax2.set(xlabel='$y_1$ [-]', + ax3.set(xlabel='$y_1$ [-]', ylabel='$\chi$ [-]', xlim = [0.,1.], ylim = [1e-10,100]) - ax2.locator_params(axis="x", nbins=4) - ax2.set_yscale('log') + ax3.locator_params(axis="x", nbins=4) + ax3.set_yscale('log') plt.legend([],[], frameon=False) - ax2.text(0.03, 10, "(b)", fontsize=10, + ax3.text(0.05, 8, "(c)", fontsize=10, backgroundcolor = 'w') # Label for the materials - ax2.text(0.9, 6e-2, sensorText[0], fontsize=10, + ax3.text(0.85, 6e-2, sensorText[0], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[0]) - ax2.text(0.8, 4e-4, sensorText[1], fontsize=10, + ax3.text(0.81, 4e-4, sensorText[1], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[1]) - ax2.text(0.6, 2e-6, sensorText[2], fontsize=10, + ax3.text(0.6, 1e-6, sensorText[2], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[2]) + # Label for the formula + ax3.text(0.62, 8, "$\chi = \hat{\sigma}/\hat{\mu}$", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + # Save the figure if saveFlag: # FileName: responseShape___ @@ -339,9 +372,16 @@ def concatenateConcEstimate(loadFileName,sensorText): # Compute the mean and standard deviation meanData = df.groupby(['dataType','x'], as_index=False, sort=False).mean() stdData = df.groupby(['dataType','x'], as_index=False, sort=False).std() + + # Compute the relative error of the mean (non-negative) + meanErr = stdData.copy() + meanErr['y1'] = abs(meanData['x'].astype(float) - meanData['y1'])/meanData['x'].astype(float) + meanErr['y2'] = abs((1.-meanData['x'].astype(float)) - meanData['y2'])/(1.-meanData['x'].astype(float)) + # Coefficient of variation cvData = stdData.copy() cvData['y1'] = stdData['y1']/meanData['y1'] - + cvData['y2'] = stdData['y2']/meanData['y2'] + # Return the coefficient of variation - return cvData \ No newline at end of file + return meanErr, cvData \ No newline at end of file From 2417d2c6b2e96c48eac165c4fa56e41dd9d0b1f4 Mon Sep 17 00:00:00 2001 From: Rajagopalan Date: Wed, 24 Feb 2021 12:01:06 +0000 Subject: [PATCH 82/99] Add function to generate sensitive region for each materia --- plotFunctions/plotsForArticle_Simulation.py | 84 ++++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 894479b..ff1d657 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-02-24, AK: Add function to generate sensitive region for each material # - 2021-02-23, AK: Add mean error to sensor shape plot # - 2021-02-11, AK: Initial creation # @@ -184,15 +185,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, import pandas as pd import seaborn as sns import matplotlib.pyplot as plt - from simulateSensorArray import simulateSensorArray plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file - - # Total pressure of the gas [Pa] - pressureTotal = np.array([1.e5]); - - # Temperature of the gas [K] - # Can be a vector of temperatures - temperature = np.array([298.15]); # Materials to be plotted sensorID = np.array([17,16,6]) @@ -209,18 +202,12 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, # Colors for plot colorsForPlot = ("#5fad56","#f78154","#b4436c") - # Simulate the sensor response for all possible concentrations - # Number of molefractions - numMolFrac= 101 - moleFractionRange = np.array([np.linspace(0,1,numMolFrac), 1 - np.linspace(0,1,numMolFrac)]).T - - arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) + # Get the sensor response and the sensor sensitive region os.chdir("..") - for ii in range(moleFractionRange.shape[0]): - arraySimResponse[ii,:] = simulateSensorArray(sensorID, pressureTotal, - temperature, np.array([moleFractionRange[ii,:]])) + moleFractionRange, arraySimResponse, _ = getSensorSensitiveRegion(sensorID) os.chdir("plotFunctions") + # Plot the figure fig = plt.figure ax1 = plt.subplot(1,3,1) # Loop through all sensors @@ -272,7 +259,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, backgroundcolor = 'w', color = colorsForPlot[2]) # Label for the formula - ax2.text(0.38, 8, "$\psi = |\mu - \hat{\mu}|/\mu$", fontsize=10, + ax2.text(0.38, 7, "$\psi = |\mu - \hat{\mu}|/\mu$", fontsize=10, backgroundcolor = 'w', color = '#0077b6') # CV - No noise @@ -300,7 +287,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, backgroundcolor = 'w', color = colorsForPlot[2]) # Label for the formula - ax3.text(0.62, 8, "$\chi = \hat{\sigma}/\hat{\mu}$", fontsize=10, + ax3.text(0.62, 7, "$\chi = \hat{\sigma}/\hat{\mu}$", fontsize=10, backgroundcolor = 'w', color = '#0077b6') # Save the figure @@ -314,7 +301,64 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) plt.savefig (savePath) plt.show() - + +# fun: getSensorSensitiveRegion +# Simulate the sensor array and obtain the region of sensitivity +def getSensorSensitiveRegion(sensorID): + import numpy as np + from simulateSensorArray import simulateSensorArray + from kneed import KneeLocator # To compute the knee/elbow of a curve + + # Total pressure of the gas [Pa] + pressureTotal = np.array([1.e5]); + + # Temperature of the gas [K] + # Can be a vector of temperatures + temperature = np.array([298.15]); + + # Number of molefractions + numMolFrac= 101 + moleFractionRange = np.array([np.linspace(0,1,numMolFrac), 1 - np.linspace(0,1,numMolFrac)]).T + + # Simulate the sensor response for all possible concentrations + arraySimResponse = np.zeros([moleFractionRange.shape[0],sensorID.shape[0]]) + for ii in range(moleFractionRange.shape[0]): + arraySimResponse[ii,:] = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([moleFractionRange[ii,:]])) + + # Compute the sensitive region for each sensor material in the array + sensitiveRegion = np.zeros([arraySimResponse.shape[1],2]) + # Loop through all the materials + firstDerivative = np.zeros([arraySimResponse.shape[0],arraySimResponse.shape[1]]) + firstDerivativeSimResponse_y1 = np.zeros([moleFractionRange.shape[0],arraySimResponse.shape[1]]) + firstDerivativeSimResponse_y2 = np.zeros([moleFractionRange.shape[0],arraySimResponse.shape[1]]) + for kk in range(arraySimResponse.shape[1]): + firstDerivative[:,kk] = np.gradient(arraySimResponse[:,kk],moleFractionRange[1,0]-moleFractionRange[0,0]) + firstDerivativeSimResponse_y1[:,kk] = np.gradient(moleFractionRange[:,0],arraySimResponse[:,kk]) + firstDerivativeSimResponse_y2[:,kk] = np.gradient(moleFractionRange[:,1],arraySimResponse[:,kk]) + # Get the sign of the first derivative for increasing/decreasing + if all(i >= 0. for i in firstDerivative[:,kk]): + slopeDir = "increasing" + elif all(i < 0. for i in firstDerivative[:,kk]): + slopeDir = "decreasing" + else: + print("Dangerous! I should not be here!!!") + + # Compute the knee/elbow of the curve + kneedle = KneeLocator(moleFractionRange[:,0], arraySimResponse[:,kk], + direction=slopeDir) + elbowPoint = list(kneedle.all_elbows) + + # Obtain coordinates to fill working region + if slopeDir == "increasing": + sensitiveRegion[kk,:] = [0,elbowPoint[0]] + else: + sensitiveRegion[kk,:] = [elbowPoint[0], 1.0] + + # Return the mole fraction, response and sensitive region for each + # material + return moleFractionRange, arraySimResponse, sensitiveRegion + # fun: concatenateConcEstimate # Concatenates concentration estimates into a panda dataframe and computes # the coefficient of variation From 67c7eae7be32d8876f976def85250c332cfa6828 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 26 Feb 2021 16:38:15 +0000 Subject: [PATCH 83/99] Minor change in single column plot settings --- plotFunctions/singleColumn.mplstyle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotFunctions/singleColumn.mplstyle b/plotFunctions/singleColumn.mplstyle index 3cdc0e7..ea278cf 100755 --- a/plotFunctions/singleColumn.mplstyle +++ b/plotFunctions/singleColumn.mplstyle @@ -9,7 +9,7 @@ figure.autolayout : true # for labels not being cut out ## Axes axes.titlesize : 10 axes.labelsize : 10 -axes.formatter.limits : -5, 3 +axes.formatter.limits : -5, 4 ## Grid axes.grid : true From 13fedec7dd7c8ee66906c809efef3a01be2e415e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 1 Mar 2021 10:21:06 +0000 Subject: [PATCH 84/99] Create new experimental folder --- .../simulateVolumetricSystem.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename simulateVolumetricSystem.py => experimental/simulateVolumetricSystem.py (100%) diff --git a/simulateVolumetricSystem.py b/experimental/simulateVolumetricSystem.py similarity index 100% rename from simulateVolumetricSystem.py rename to experimental/simulateVolumetricSystem.py From 92f33456865369194b52df9ec884a352f9f5f4e9 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 5 Mar 2021 19:05:17 +0000 Subject: [PATCH 85/99] ############################################################################ --- plotFunctions/doubleColumn2Row.mplstyle | 44 ++++ plotFunctions/plotsForArticle_Simulation.py | 213 +++++++++++++++++++- 2 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 plotFunctions/doubleColumn2Row.mplstyle diff --git a/plotFunctions/doubleColumn2Row.mplstyle b/plotFunctions/doubleColumn2Row.mplstyle new file mode 100644 index 0000000..d71b19b --- /dev/null +++ b/plotFunctions/doubleColumn2Row.mplstyle @@ -0,0 +1,44 @@ +# matplotlib configuration file for the ERASE project +# https://matplotlib.org/3.3.2/tutorials/introductory/customizing.html + +## Figure property +figure.figsize : 7, 6 # width, height in inches +figure.dpi : 600 # dpi +figure.autolayout : true # for labels not being cut out + +## Axes +axes.titlesize : 10 +axes.labelsize : 10 +axes.formatter.limits : -5, 4 + +## Grid +axes.grid : true +grid.color : cccccc +grid.linewidth : 0.5 + +## Lines & Scatter +lines.linewidth : 1.5 +lines.markersize : 4 +scatter.marker: o + +## Ticks +xtick.top : true +ytick.right : true +xtick.labelsize : 8 +ytick.labelsize : 8 + +## Fonts +font.family : arial +font.weight : normal +font.size : 10 + +## Legends +legend.frameon : true +legend.fontsize : 10 +legend.edgecolor : 1 +legend.framealpha : 0.6 + +## Save figure +savefig.dpi : figure +savefig.format : png +savefig.transparent : false \ No newline at end of file diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index ff1d657..59649d1 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-03-05, AK: Add plot for comparing sensor array with graphical tool # - 2021-02-24, AK: Add function to generate sensitive region for each material # - 2021-02-23, AK: Add mean error to sensor shape plot # - 2021-02-11, AK: Initial creation @@ -59,6 +60,12 @@ def plotsForArticle_Simulation(**kwargs): meanErr = plotForArticle_ResponseShape(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If graphical tool needs to be plotted + if 'graphicalTool' in kwargs: + if kwargs["graphicalTool"]: + plotForArticle_GraphicalTool(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # fun: plotForArticle_SensorArray # Plots the histogram of gas compositions for a one and two material # sensor array @@ -108,7 +115,7 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, histTypeX = 'stepfilled' alphaX=0.75 densityX = True - + # Plot the histogram of the gas compositions ax = plt.subplot(2,2,ii+1) # Histogram for 1 material array @@ -176,8 +183,7 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, plt.show() # fun: plotForArticle_SensorArray -# Plots the histogram of gas compositions for a one and two material -# sensor array +# Plots the sensor response for a given sensor array def plotForArticle_ResponseShape(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np @@ -191,7 +197,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, sensorID = np.array([17,16,6]) sensorText = ["A", "B", "C"] - # File to be loaded for the left of violin plot + # File to be loaded for the simulation results loadFileName = ["sensitivityAnalysis_17_20210212_1259_b02f8c3.npz", # No Noise "sensitivityAnalysis_16_20210212_1300_b02f8c3.npz", # No Noise "sensitivityAnalysis_6_20210212_1259_b02f8c3.npz"] # No Noise @@ -302,6 +308,193 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, plt.savefig (savePath) plt.show() +# fun: plotForArticle_GraphicalTool +# Plots the histogram of gas compositions for a one and two material +# sensor array +def plotForArticle_GraphicalTool(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import os + import pandas as pd + import seaborn as sns + import matplotlib.pyplot as plt + plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file + + # File to be loaded for the simulation results + loadFileName = ["sensitivityAnalysis_6-2_20210305_1109_b02f8c3.npz", # 6,2 + "sensitivityAnalysis_17-16_20210305_1050_b02f8c3.npz"] # 17,16 + + # Materials to be plotted + sensorID = np.array([[6,2],[17,16]]) + arrayText = ["D", "E"] + materialText = ["$\\alpha$", "$\\beta$", "$\gamma$", "$\delta$"] + + # Colors for plot + colorsForPlot = ("#5fad56","#ff9e00") + colorLeft = ("#e5383b","#6c757d") + colorRight = ("#6c757d","#e5383b") + + # Plot the figure + fig = plt.figure + ax1 = plt.subplot(2,2,1) + # Get the sensor response and the sensor sensitive region + os.chdir("..") + moleFractionRange, arraySimResponse, sensitiveRegion = getSensorSensitiveRegion(sensorID[0,:]) + os.chdir("plotFunctions") + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + ax1.plot(moleFractionRange[:,0],arraySimResponse[:,kk], + color=colorsForPlot[kk]) # Simulated Response + ax1.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax1.set(ylabel='$m$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 150]) + ax1.axes.xaxis.set_ticklabels([]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.text(0.025, 138, "(a)", fontsize=10) + ax1.text(0.78, 138, "Array D", fontsize=10, + color = '#0077b6') + + # Label for the materials + ax1.text(0.9, 120, materialText[0], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax1.text(0.9, 23, materialText[1], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + + ax2 = plt.subplot(2,2,2) + # Call the concatenateConcEstimate function + meanErr, cvData = concatenateConcEstimate([loadFileName[0]],arrayText[0]) + + # Mean Error - No noise + meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") + sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['o'], + palette = colorLeft, linewidth = 0.5) + ax2.set(ylabel='$\psi$ [-]', + xlim = [0.,1.], ylim = [1e-8,1]) + ax2.locator_params(axis="x", nbins=4) + ax2.set(xlabel=None) + ax2.axes.xaxis.set_ticklabels([]) + ax2.set_yscale('log') + ax2.yaxis.label.set_color(colorLeft[0]) + ax2.tick_params(axis='y', colors=colorLeft[0]) + plt.legend([],[], frameon=False) + # CV - No noise + ax2r = plt.twinx() + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['D'], + palette = colorRight, linewidth = 0.5, + ax = ax2r) + # Plot sensitive region + for kk in range(arraySimResponse.shape[1]): + ax2r.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax2r.set(ylabel='$\chi$ [-]',ylim = [1e-8,1]) + ax2r.locator_params(axis="x", nbins=4) + ax2r.axes.xaxis.set_ticklabels([]) + ax2r.set_yscale('log') + ax2r.yaxis.label.set_color(colorLeft[1]) + ax2r.tick_params(axis='y', colors=colorLeft[1]) + plt.legend([],[], frameon=False) + ax2r.annotate("", xy=(0.5, 5e-7), xytext=(0.6, 5e-7), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) + ax2r.annotate("", xy=(0.95, 1e-5), xytext=(0.85, 1e-5), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) + ax2r.text(0.025, 0.2, "(b)", fontsize=10) + ax2r.spines["left"].set_color(colorLeft[0]) + ax2r.spines["right"].set_color(colorLeft[1]) + + ax2r.text(0.78, 0.2, "Array D", fontsize=10, + color = '#0077b6') + + ax3 = plt.subplot(2,2,3) + # Get the sensor response and the sensor sensitive region + os.chdir("..") + moleFractionRange, arraySimResponse, sensitiveRegion = getSensorSensitiveRegion(sensorID[1,:]) + os.chdir("plotFunctions") + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + ax3.plot(moleFractionRange[:,0],arraySimResponse[:,kk], + color=colorsForPlot[kk]) # Simulated Response + ax3.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax3.set(xlabel='$y_1$ [-]', + ylabel='$m$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 300]) + ax3.locator_params(axis="x", nbins=4) + ax3.locator_params(axis="y", nbins=4) + ax3.text(0.025, 275, "(c)", fontsize=10) + ax3.text(0.78, 275, "Array E", fontsize=10, + color = '#0077b6') + + # Label for the materials + ax3.text(0.78, 225, materialText[2], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax3.text(0.1, 150, materialText[3], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + + ax4 = plt.subplot(2,2,4) + # Call the concatenateConcEstimate function + meanErr, cvData = concatenateConcEstimate([loadFileName[1]],arrayText[1]) + + # Mean Error - No noise + meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") + sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['o'], + palette = colorLeft, linewidth = 0.5) + ax4.set(xlabel='$y_1$ [-]', + ylabel='$\psi$ [-]', + xlim = [0.,1.], ylim = [1e-8,1]) + ax4.locator_params(axis="x", nbins=4) + ax4.set_yscale('log') + ax4.yaxis.label.set_color(colorLeft[0]) + ax4.tick_params(axis='y', colors=colorLeft[0]) + plt.legend([],[], frameon=False) + # CV - No noise + ax4r = plt.twinx() + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['D'], + palette = colorRight, linewidth = 0.5, + ax = ax4r) + # Plot sensitive region + for kk in range(arraySimResponse.shape[1]): + ax4r.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax4r.set(ylabel='$\chi$ [-]',ylim = [1e-8,1]) + ax4r.locator_params(axis="x", nbins=4) + ax4r.set_yscale('log') + ax4r.yaxis.label.set_color(colorLeft[1]) + ax4r.tick_params(axis='y', colors=colorLeft[1]) + plt.legend([],[], frameon=False) + ax4r.annotate("", xy=(0.2, 5e-4), xytext=(0.3, 5e-4), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) + ax4r.annotate("", xy=(0.7, 1e-3), xytext=(0.6, 1e-3), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) + ax4r.text(0.025, 0.2, "(d)", fontsize=10) + ax4r.spines["left"].set_color(colorLeft[0]) + ax4r.spines["right"].set_color(colorLeft[1]) + + ax4r.text(0.78, 0.2, "Array E", fontsize=10, + color = '#0077b6') + + # Save the figure + if saveFlag: + # FileName: responseShape___ + saveFileName = "graphicalTool_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','simulationManuscript')): + os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) + plt.savefig (savePath) + plt.show() + # fun: getSensorSensitiveRegion # Simulate the sensor array and obtain the region of sensitivity def getSensorSensitiveRegion(sensorID): @@ -373,7 +566,7 @@ def concatenateConcEstimate(loadFileName,sensorText): concatenatedY1 = [] concatenatedY2 = [] concatenatedType = [] - + # Loop through the different files to generate the violin plot for kk in range(len(loadFileName)): # Initialize x, y, and type for the local loop @@ -388,8 +581,11 @@ def concatenateConcEstimate(loadFileName,sensorText): # Loop through all the molefractions for ii in range(resultOutput.shape[0]): - counterInd = -1 - + if resultOutput.shape[2] == 3: + counterInd = -1 + elif resultOutput.shape[2] == 4: + counterInd = 0 + y1Var = np.concatenate((y1Var,resultOutput[ii,:,counterInd+2])) # y1 y2Var = np.concatenate((y2Var,resultOutput[ii,:,counterInd+3])) # y2 xVar = xVar + ([str(moleFrac[ii])] * len(resultOutput[ii,:,counterInd+2])) # x (true mole fraction) @@ -413,10 +609,11 @@ def concatenateConcEstimate(loadFileName,sensorText): 'y1':concatenatedY1, 'y2':concatenatedY2, 'dataType':concatenatedType}) + # Compute the mean and standard deviation meanData = df.groupby(['dataType','x'], as_index=False, sort=False).mean() stdData = df.groupby(['dataType','x'], as_index=False, sort=False).std() - + # Compute the relative error of the mean (non-negative) meanErr = stdData.copy() meanErr['y1'] = abs(meanData['x'].astype(float) - meanData['y1'])/meanData['x'].astype(float) From 6e69e2836d52d8dcf7709ab0ce1d09a49fd163cc Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 9 Mar 2021 10:55:39 +0000 Subject: [PATCH 86/99] Add plots for three materials --- plotFunctions/plotsForArticle_Simulation.py | 223 +++++++++++++++++++- 1 file changed, 222 insertions(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 59649d1..d2a81c2 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-03-05, AK: Add plot for three materials # - 2021-03-05, AK: Add plot for comparing sensor array with graphical tool # - 2021-02-24, AK: Add function to generate sensitive region for each material # - 2021-02-23, AK: Add mean error to sensor shape plot @@ -66,6 +67,12 @@ def plotsForArticle_Simulation(**kwargs): plotForArticle_GraphicalTool(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If graphical tool needs to be plotted + if 'threeMaterials' in kwargs: + if kwargs["threeMaterials"]: + plotForArticle_ThreeMaterials(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # fun: plotForArticle_SensorArray # Plots the histogram of gas compositions for a one and two material # sensor array @@ -486,7 +493,7 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, # Save the figure if saveFlag: - # FileName: responseShape___ + # FileName: graphicalTool__ saveFileName = "graphicalTool_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) # Check if inputResources directory exists or not. If not, create the folder @@ -495,6 +502,218 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, plt.savefig (savePath) plt.show() +# fun: plotForArticle_GraphicalTool +# Plots the histogram of gas compositions for a one and two material +# sensor array +def plotForArticle_ThreeMaterials(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import os + import pandas as pd + import seaborn as sns + import matplotlib.pyplot as plt + plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file + + # File to be loaded for the simulation results + loadFileName = ["sensitivityAnalysis_17-15-6_20210306_1515_b02f8c3.npz", # 17,15,6 + "sensitivityAnalysis_17-15-16_20210306_1515_b02f8c3.npz", # 17,15,16 + "sensitivityAnalysis_17-15_20210308_1002_b02f8c3.npz"] # 17,15 + + # Materials to be plotted + sensorID = np.array([[17,15,6],[17,15,16]]) + arrayText = ["F", "G", "Ref"] + materialText = ["$\\alpha$", "$\\beta$", "$\gamma$", "$\delta$", "$\zeta$"] + + # Colors for plot + colorsForPlot = ("#5fad56","#98c1d9","#ff9e00") + colorLeft = ("#e5383b","#6c757d") + colorRight = ("#6c757d","#e5383b") + + # Plot the figure + fig = plt.figure + ax1 = plt.subplot(2,2,1) + # Get the sensor response and the sensor sensitive region + os.chdir("..") + moleFractionRange, arraySimResponse, sensitiveRegion = getSensorSensitiveRegion(sensorID[0,:]) + os.chdir("plotFunctions") + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + ax1.plot(moleFractionRange[:,0],arraySimResponse[:,kk], + color=colorsForPlot[kk]) # Simulated Response + ax1.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax1.set(ylabel='$m$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 300]) + ax1.axes.xaxis.set_ticklabels([]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.text(0.025, 275, "(a)", fontsize=10) + ax1.text(0.78, 275, "Array F", fontsize=10, + color = '#0077b6') + + # Label for the materials + ax1.text(0.78, 225, materialText[2], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax1.text(0.78, 25, materialText[4], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + ax1.text(0.78, 80, materialText[0], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[2]) + + ax2 = plt.subplot(2,2,2) + # Call the concatenateConcEstimate function + meanErrRef, cvDataRef = concatenateConcEstimate([loadFileName[2]],arrayText[2]) + meanErr, cvData = concatenateConcEstimate([loadFileName[0]],arrayText[0]) + + # Mean Error - No noise + meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") + sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['o'], + palette = colorLeft, linewidth = 0.5) + meanErrRef["x"] = pd.to_numeric(meanErrRef["x"], downcast="float") + sns.lineplot(data=meanErrRef, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(5,5)], markers = ['o'], + palette = colorLeft, linewidth = 0.5, alpha = 0.35) + ax2.set(ylabel='$\psi$ [-]', + xlim = [0.,1.], ylim = [1e-8,1]) + ax2.locator_params(axis="x", nbins=4) + ax2.set(xlabel=None) + ax2.axes.xaxis.set_ticklabels([]) + ax2.set_yscale('log') + ax2.yaxis.label.set_color(colorLeft[0]) + ax2.tick_params(axis='y', colors=colorLeft[0]) + plt.legend([],[], frameon=False) + # CV - No noise + ax2r = plt.twinx() + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['D'], + palette = colorRight, linewidth = 0.5, + ax = ax2r) + cvDataRef["x"] = pd.to_numeric(cvDataRef["x"], downcast="float") + sns.lineplot(data=cvDataRef, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(5,5)], markers = ['D'], + palette = colorRight, linewidth = 0.5, alpha = 0.35, + ax = ax2r) + # Plot sensitive region + for kk in range(arraySimResponse.shape[1]): + ax2r.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax2r.set(ylabel='$\chi$ [-]',ylim = [1e-8,1]) + ax2r.locator_params(axis="x", nbins=4) + ax2r.axes.xaxis.set_ticklabels([]) + ax2r.set_yscale('log') + ax2r.yaxis.label.set_color(colorLeft[1]) + ax2r.tick_params(axis='y', colors=colorLeft[1]) + plt.legend([],[], frameon=False) + ax2r.annotate("", xy=(0.4, 2e-6), xytext=(0.5, 2e-6), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) + ax2r.annotate("", xy=(0.95, 5e-2), xytext=(0.85, 5e-2), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) + ax2r.text(0.025, 0.2, "(b)", fontsize=10) + ax2r.spines["left"].set_color(colorLeft[0]) + ax2r.spines["right"].set_color(colorLeft[1]) + + ax2r.text(0.78, 3e-6, "Array F", fontsize=10, + color = '#0077b6') + ax2r.text(0.4, 0.05, "Reference ($\gamma \zeta$)", fontsize=10, + color = '#0077b6', alpha = 0.35) + + ax3 = plt.subplot(2,2,3) + # Get the sensor response and the sensor sensitive region + os.chdir("..") + moleFractionRange, arraySimResponse, sensitiveRegion = getSensorSensitiveRegion(sensorID[1,:]) + os.chdir("plotFunctions") + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + ax3.plot(moleFractionRange[:,0],arraySimResponse[:,kk], + color=colorsForPlot[kk]) # Simulated Response + ax3.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax3.set(xlabel='$y_1$ [-]', + ylabel='$m$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 300]) + ax3.locator_params(axis="x", nbins=4) + ax3.locator_params(axis="y", nbins=4) + ax3.text(0.025, 275, "(c)", fontsize=10) + ax3.text(0.78, 275, "Array G", fontsize=10, + color = '#0077b6') + + # Label for the materials + ax3.text(0.78, 225, materialText[2], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax3.text(0.78, 25, materialText[4], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + ax3.text(0.1, 150, materialText[3], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[2]) + + ax4 = plt.subplot(2,2,4) + # Call the concatenateConcEstimate function + meanErr, cvData = concatenateConcEstimate([loadFileName[1]],arrayText[1]) + + # Mean Error - No noise + meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") + sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['o'], + palette = colorLeft, linewidth = 0.5) + sns.lineplot(data=meanErrRef, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(5,5)], markers = ['o'], + palette = colorLeft, linewidth = 0.5, alpha = 0.35) + ax4.set(xlabel='$y_1$ [-]', + ylabel='$\psi$ [-]', + xlim = [0.,1.], ylim = [1e-8,1]) + ax4.locator_params(axis="x", nbins=4) + ax4.set_yscale('log') + ax4.yaxis.label.set_color(colorLeft[0]) + ax4.tick_params(axis='y', colors=colorLeft[0]) + plt.legend([],[], frameon=False) + # CV - No noise + ax4r = plt.twinx() + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1)], markers = ['D'], + palette = colorRight, linewidth = 0.5, + ax = ax4r) + sns.lineplot(data=cvDataRef, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(5,5)], markers = ['o'], + palette = colorRight, linewidth = 0.5, alpha = 0.35, + ax = ax4r) + # Plot sensitive region + for kk in range(arraySimResponse.shape[1]): + ax4r.fill_between(sensitiveRegion[kk,:],1.5*np.max(arraySimResponse), + facecolor=colorsForPlot[kk], alpha=0.25) + + ax4r.set(ylabel='$\chi$ [-]',ylim = [1e-8,1]) + ax4r.locator_params(axis="x", nbins=4) + ax4r.set_yscale('log') + ax4r.yaxis.label.set_color(colorLeft[1]) + ax4r.tick_params(axis='y', colors=colorLeft[1]) + plt.legend([],[], frameon=False) + ax4r.annotate("", xy=(0.2, 5e-4), xytext=(0.3, 5e-4), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) + ax4r.annotate("", xy=(0.7, 3e-2), xytext=(0.6, 3e-2), + arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) + ax4r.text(0.025, 0.2, "(d)", fontsize=10) + ax4r.spines["left"].set_color(colorLeft[0]) + ax4r.spines["right"].set_color(colorLeft[1]) + + ax4r.text(0.6, 1e-5, "Array G", fontsize=10, + color = '#0077b6') + ax4r.text(0.3, 0.1, "Reference ($\gamma \zeta$)", fontsize=10, + color = '#0077b6', alpha = 0.35) + # Save the figure + if saveFlag: + # FileName: threeMaterials__ + saveFileName = "threeMaterials_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','simulationManuscript')): + os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) + plt.savefig (savePath) + plt.show() + # fun: getSensorSensitiveRegion # Simulate the sensor array and obtain the region of sensitivity def getSensorSensitiveRegion(sensorID): @@ -585,6 +804,8 @@ def concatenateConcEstimate(loadFileName,sensorText): counterInd = -1 elif resultOutput.shape[2] == 4: counterInd = 0 + elif resultOutput.shape[2] == 5: + counterInd = 1 y1Var = np.concatenate((y1Var,resultOutput[ii,:,counterInd+2])) # y1 y2Var = np.concatenate((y2Var,resultOutput[ii,:,counterInd+3])) # y2 From 4d6a65145339432f950197828d06c57c2f47d997 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sat, 20 Mar 2021 11:18:32 +0000 Subject: [PATCH 87/99] Minor fix --- fullModelConcentrationEstimatorWrapper.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fullModelConcentrationEstimatorWrapper.py b/fullModelConcentrationEstimatorWrapper.py index 03699d4..245fcb6 100644 --- a/fullModelConcentrationEstimatorWrapper.py +++ b/fullModelConcentrationEstimatorWrapper.py @@ -52,10 +52,9 @@ # (False) or not (True) flagIndTime = True -# Flag to determine whether constant pressure or constant flow rate model to -# be used -# (False) or not (True) -flagIndTime = True +# Flag to determine whether constant pressure (False) or constant flow rate +# model to be used (True) +modelConstF = False # Sensor ID sensorID = [6,2] # [-] @@ -78,7 +77,7 @@ # Measurement noise characteristics meanNoise = 0.0 # [g/kg] -stdNoise = 0.1 # [g/kg] +stdNoise = 0.0 # [g/kg] # Loop over all rate constants outputStruct = {} From 4b80775722ccb728bcd36ee31f90ddd52c604ed3 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sat, 20 Mar 2021 13:29:17 +0000 Subject: [PATCH 88/99] Bug fix for flow rate computation in full model --- simulateFullModel.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/simulateFullModel.py b/simulateFullModel.py index 31748bc..de7394c 100644 --- a/simulateFullModel.py +++ b/simulateFullModel.py @@ -12,6 +12,7 @@ # Simulates the sensor chamber as a CSTR incorporating kinetic effects # # Last modified: +# - 2021-03-20, AK: Bug fix for flow rate calculator # - 2021-02-19, AK: Add relative tolerances for ode solver # - 2021-02-03, AK: Add total volume and void fraction # - 2021-02-02, AK: Add flow rate to output @@ -36,7 +37,7 @@ def simulateFullModel(**kwargs): from scipy.integrate import solve_ivp from simulateSensorArray import simulateSensorArray import auxiliaryFunctions - + # Plot flag plotFlag = False @@ -89,6 +90,12 @@ def simulateFullModel(**kwargs): timeInt = kwargs["timeInt"] else: timeInt = (0.0,2000) + + # Time step for output printing + if 'timeStep' in kwargs: + timeStep = kwargs["timeStep"] + else: + timeStep = 5. # Volume of sorbent material [m3] if 'volSorbent' in kwargs: @@ -151,7 +158,7 @@ def simulateFullModel(**kwargs): initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading initialConditions[2*numberOfGases-1] = pressureTotal # Outlet pressure the same as inlet pressure outputSol = solve_ivp(solveSorptionEquationConstF, timeInt, initialConditions, - method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],timeStep), rtol = 1e-6, args = inputParameters) # Flow out vector in output @@ -167,17 +174,25 @@ def simulateFullModel(**kwargs): initialConditions[0:numberOfGases-1] = initMoleFrac[0:numberOfGases-1] # Gas mole fraction initialConditions[numberOfGases-1:2*numberOfGases-1] = sensorLoadingPerGasVol # Initial Loading outputSol = solve_ivp(solveSorptionEquationConstP, timeInt, initialConditions, - method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],timeStep), rtol = 1e-6, args = inputParameters) # Presure vector in output pressureVec = pressureTotal * np.ones(len(outputSol.t)) # Constant pressure - # Compute the outlet flow rate - dqdt = np.gradient(outputSol.y[numberOfGases-1:2*numberOfGases-1,:], - outputSol.t, axis=1) # Compute gradient of loading - sum_dqdt = np.sum(dqdt, axis=0) # Time resolved sum of gradient - flowOut = flowIn - ((volSorbent*(8.314*temperature)/pressureTotal)*(sum_dqdt)) + # This is needed to make sure constant pressure model functions well + # If the time of integration and the time step is equal then the code + # will fail because of the flow rate determination step which requires + # a gradient - very first step flow out and in are considered to be the + # same if only one element present + if len(outputSol.t) == 1 : + flowOut = flowIn + else: + # Compute the outlet flow rate + dqdt = np.gradient(outputSol.y[numberOfGases-1:2*numberOfGases-1,:], + outputSol.t, axis=1) # Compute gradient of loading + sum_dqdt = np.sum(dqdt, axis=0) # Time resolved sum of gradient + flowOut = flowIn - ((volSorbent*(8.314*temperature)/pressureTotal)*(sum_dqdt)) # Parse out the output matrix and add flow rate resultMat = np.row_stack((outputSol.y,pressureVec,flowOut)) From 345d150fc31e540cc629a59a728b4d9eff03d76b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 22 Mar 2021 10:13:53 +0000 Subject: [PATCH 89/99] Add plot for full model --- plotFunctions/plotsForArticle_Simulation.py | 128 +++++++++++++++++++- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index d2a81c2..5c6f7ac 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-03-05, AK: Add plot for full model # - 2021-03-05, AK: Add plot for three materials # - 2021-03-05, AK: Add plot for comparing sensor array with graphical tool # - 2021-02-24, AK: Add function to generate sensitive region for each material @@ -67,12 +68,18 @@ def plotsForArticle_Simulation(**kwargs): plotForArticle_GraphicalTool(gitCommitID, currentDT, saveFlag, saveFileExtension) - # If graphical tool needs to be plotted + # If three materials needs to be plotted if 'threeMaterials' in kwargs: if kwargs["threeMaterials"]: plotForArticle_ThreeMaterials(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If kinetic importance needs to be plotted + if 'kineticsImportance' in kwargs: + if kwargs["kineticsImportance"]: + plotForArticle_KineticsImportance(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # fun: plotForArticle_SensorArray # Plots the histogram of gas compositions for a one and two material # sensor array @@ -316,8 +323,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, plt.show() # fun: plotForArticle_GraphicalTool -# Plots the histogram of gas compositions for a one and two material -# sensor array +# Plots the graphical tool to screen for materials def plotForArticle_GraphicalTool(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np @@ -503,8 +509,7 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, plt.show() # fun: plotForArticle_GraphicalTool -# Plots the histogram of gas compositions for a one and two material -# sensor array +# Plots the analaysis for three material arrays def plotForArticle_ThreeMaterials(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np @@ -714,6 +719,119 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, plt.savefig (savePath) plt.show() +# fun: plotForArticle_GraphicalTool +# Plots the analaysis for three material arrays +def plotForArticle_KineticsImportance(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + from numpy import load + import os + import matplotlib.pyplot as plt + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + + # Colors for plot + colorsForPlot = ("#5fad56","#ff9e00","#e5383b","#6c757d") + + # Labels for materials + materialText = ["$\\alpha$", "$\\beta$"] + + # File name for equilibrium model and full model estimates + loadFileName_E = "fullModelConcentrationEstimate_6-2_20210320_1336_4b80775.npz" # Eqbm + loadFileName_F = "fullModelConcentrationEstimate_6-2_20210320_1338_4b80775.npz" # Full model + + # Parse out equilbirum results file + simResultsFile = os.path.join('..','simulationResults',loadFileName_E); + loadedFile_E = load(simResultsFile, allow_pickle=True) + concentrationEstimate_E = loadedFile_E["arrayConcentration"] + + # Parse out full model results file + simResultsFile = os.path.join('..','simulationResults',loadFileName_F); + loadedFile_F = load(simResultsFile, allow_pickle=True) + concentrationEstimate_F = loadedFile_F["arrayConcentration"] + + # Parse out true responses (this should be the same for both eqbm and full + # model (here loaded from full model) + trueResponseStruct = loadedFile_F["outputStruct"].item() + # Parse out time + timeSim = [] + timeSim = trueResponseStruct[0]["timeSim"] + # Parse out feed mole fraction + feedMoleFrac = trueResponseStruct[0]["inputParameters"][5] + # Parse out true sensor finger print + sensorFingerPrint = np.zeros([len(timeSim),len(trueResponseStruct)]) + for ii in range(len(trueResponseStruct)): + sensorFingerPrint[:,ii] = trueResponseStruct[ii]["sensorFingerPrint"] + + # Points that will be taken for sampling (for plots) + lenSampling = 6 + fig = plt.figure + # Plot the true sensor response (using the full model) + ax1 = plt.subplot(1,2,1) + ax1.plot(timeSim[0:len(timeSim):lenSampling], + sensorFingerPrint[0:len(timeSim):lenSampling,0], + marker = 'o', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color=colorsForPlot[0]) + ax1.plot(timeSim[0:len(timeSim):lenSampling], + sensorFingerPrint[0:len(timeSim):lenSampling,1], + marker = 'D', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color=colorsForPlot[1]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.set(xlabel='$t$ [s]', + ylabel='$m$ [g kg$^{\mathregular{-1}}$]', + xlim = [0, 1000.], ylim = [0, 40]) + + ax1.text(20, 37, "(a)", fontsize=10) + ax1.text(800, 37, "Array D", fontsize=10, + color = '#0077b6') + ax1.text(780, 33.5, "$y^{\mathregular{in}}_{\mathregular{1}}$ = 0.1", fontsize=10, + color = '#0077b6') + + # Label for the materials + ax1.text(900, 26, materialText[0], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[0]) + ax1.text(900, 4, materialText[1], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + + # Plot the evolution of the gas composition with respect to time + ax2 = plt.subplot(1,2,2) + ax2.plot(timeSim[0:len(timeSim):lenSampling], + concentrationEstimate_E[0:len(timeSim):lenSampling,2], + marker = 'v', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color=colorsForPlot[2]) + ax2.plot(timeSim[0:len(timeSim):lenSampling], + concentrationEstimate_F[0:len(timeSim):lenSampling,2], + marker = '^', markersize = 2, linestyle = 'dotted', linewidth = 0.5, + color=colorsForPlot[3]) + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + ax2.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = [0, 1000.], ylim = [0, 0.2]) + + ax2.text(20, 0.185, "(b)", fontsize=10) + ax2.text(800, 0.185, "Array D", fontsize=10, + color = '#0077b6') + ax2.text(780, 0.11, "$y^{\mathregular{in}}_{\mathregular{1}}$ = 0.1", fontsize=10, + color = '#0077b6') + + # Label for the materials + ax2.text(280, 0.06, "Equilibrium Model", fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[2]) + ax2.text(50, 0.11, "Full Model", fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[3]) + + # Save the figure + if saveFlag: + # FileName: threeMaterials__ + saveFileName = "kineticsImportance_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','simulationManuscript')): + os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) + plt.savefig (savePath) + plt.show() + # fun: getSensorSensitiveRegion # Simulate the sensor array and obtain the region of sensitivity def getSensorSensitiveRegion(sensorID): From c067fe72974f6f3cad6bccaffc927b6407c3b1ba Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 23 Mar 2021 12:12:51 +0000 Subject: [PATCH 90/99] Minor fixes for full model wrapper --- sensorFullModelWrapper.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/sensorFullModelWrapper.py b/sensorFullModelWrapper.py index b9df7e5..2b5a511 100644 --- a/sensorFullModelWrapper.py +++ b/sensorFullModelWrapper.py @@ -13,6 +13,7 @@ # variables # # Last modified: +# - 2021-03-23, AK: Minor fixes # - 2021-02-03, AK: Change output plot response to absolute values # - 2021-01-20, AK: Initial creation # @@ -29,10 +30,12 @@ from tqdm import tqdm # To track progress of the loop import numpy as np import os +from numpy import savez +import socket import matplotlib.pyplot as plt -# Save settings -saveFlag = False +# Save and plot settings +saveFlag = True saveFileExtension = ".png" saveText = "rateConstant" colorsForPlot = ["1DBDE6","52A2C4","6D95B3","A27A91","CA6678","F1515E"] @@ -46,7 +49,7 @@ # Define the variable to be looped # This has to be a be a tuple. For on condition write the values followed by a # comma to say its a tuple -volTotal = 5e-7 +volTotal = 10e-7 loopVariable = (0.001,0.25,0.5,0.75,0.9) # Define a dictionary @@ -61,6 +64,16 @@ 'sensorFingerPrint':sensorFingerPrint, 'inputParameters':inputParameters} +# Save the array concentration into a native numpy file +# The .npz file is saved in a folder called simulationResults (hardcoded) +# Save the figure +if saveFlag: + filePrefix = "fullModelSensitivity" + saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID; + savePath = os.path.join('simulationResults',saveFileName) + savez (savePath, outputStruct = outputStruct, # True response + hostName = socket.gethostname()) # Hostname of the computer + # Plot the sensor finger print os.chdir("plotFunctions") plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file @@ -79,14 +92,5 @@ xlim = [timeSim[0], timeSim[-1]], ylim = [0, None]) ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) -# Save the figure -if saveFlag: - # FileName: fullModelWrapper___ - saveFileName = "fullModelWrapper_" + saveText + "_" + currentDT + "_" + gitCommitID + saveFileExtension - savePath = os.path.join('..','simulationFigures',saveFileName.replace('[','').replace(']','')) - # Check if inputResources directory exists or not. If not, create the folder - if not os.path.exists(os.path.join('..','simulationFigures')): - os.mkdir(os.path.join('..','simulationFigures')) - plt.savefig (savePath) plt.show() os.chdir("..") \ No newline at end of file From c9e817901b4ce2ed1a418d39d5717da91e5ff468 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 24 Mar 2021 09:57:02 +0000 Subject: [PATCH 91/99] Minor fixes for wrapper --- sensorFullModelWrapper.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sensorFullModelWrapper.py b/sensorFullModelWrapper.py index 2b5a511..2d9c16b 100644 --- a/sensorFullModelWrapper.py +++ b/sensorFullModelWrapper.py @@ -37,8 +37,8 @@ # Save and plot settings saveFlag = True saveFileExtension = ".png" -saveText = "rateConstant" -colorsForPlot = ["1DBDE6","52A2C4","6D95B3","A27A91","CA6678","F1515E"] +saveFlag = "rateConstant" +colorsForPlot = ["E5383B","CD4448","B55055","9C5D63","846970","6C757D"] # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() @@ -50,7 +50,13 @@ # This has to be a be a tuple. For on condition write the values followed by a # comma to say its a tuple volTotal = 10e-7 -loopVariable = (0.001,0.25,0.5,0.75,0.9) +voidFrac = 0.5 +loopVariable = ([0.0001,0.0001,0.0001], + [0.0005,0.0005,0.0005], + [0.001,0.001,0.001], + [0.005,0.005,0.005], + [0.01,0.01,0.01], + [10000.0,10000.0,10000.0]) # Define a dictionary outputStruct = {} @@ -59,7 +65,8 @@ for ii in tqdm(range(len(loopVariable))): # Call the full model with a given rate constant timeSim, _ , sensorFingerPrint, inputParameters = simulateFullModel(volTotal = volTotal, - voidFrac = loopVariable[ii]) + voidFrac = voidFrac, + rateConstant = loopVariable[ii]) outputStruct[ii] = {'timeSim':timeSim, 'sensorFingerPrint':sensorFingerPrint, 'inputParameters':inputParameters} @@ -69,7 +76,7 @@ # Save the figure if saveFlag: filePrefix = "fullModelSensitivity" - saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID; + saveFileName = filePrefix + "_" + saveFlag + "_" + currentDT + "_" + gitCommitID; savePath = os.path.join('simulationResults',saveFileName) savez (savePath, outputStruct = outputStruct, # True response hostName = socket.gethostname()) # Hostname of the computer From cf31046e44d13bb80f9cf24739f20b842af0d9d8 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 7 Apr 2021 18:27:41 +0100 Subject: [PATCH 92/99] Add plot for design variables --- plotFunctions/plotsForArticle_Simulation.py | 133 +++++++++++++++++++- 1 file changed, 128 insertions(+), 5 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 5c6f7ac..47e733c 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-04-07, AK: Add plot for design variables # - 2021-03-05, AK: Add plot for full model # - 2021-03-05, AK: Add plot for three materials # - 2021-03-05, AK: Add plot for comparing sensor array with graphical tool @@ -80,6 +81,12 @@ def plotsForArticle_Simulation(**kwargs): plotForArticle_KineticsImportance(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If kinetic importance needs to be plotted + if 'designVariables' in kwargs: + if kwargs["designVariables"]: + plotForArticle_DesignVariables(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # fun: plotForArticle_SensorArray # Plots the histogram of gas compositions for a one and two material # sensor array @@ -90,13 +97,13 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, import os import matplotlib.pyplot as plt plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file - # For now load a given adsorbent isotherm material file + # For now load the results file loadFileName = ("arrayConcentration_20210211_1818_b02f8c3.npz", # 1 material w/o constraint "arrayConcentration_20210212_1055_b02f8c3.npz", # 2 material w/o constraint "arrayConcentration_20210212_1050_b02f8c3.npz", # 1 material with constraint "arrayConcentration_20210211_1822_b02f8c3.npz") # 2 material with constraint - # Git commit id of the loaded isotherm file + # Git commit id of the loaded file simID_loadedFile = loadFileName[0][-11:-4] # Loop through the two files to get the histogram @@ -182,6 +189,11 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, backgroundcolor = 'w', color = '#e5383b') ax.text(0.705, 25, "$y_2$ = 0.95", fontsize=10, backgroundcolor = 'w', color = '#343a40') + # Remove tick labels + if ii == 0 or ii == 1: + ax.axes.xaxis.set_ticklabels([]) + if ii == 1 or ii == 3: + ax.axes.yaxis.set_ticklabels([]) # Save the figure if saveFlag: @@ -719,8 +731,8 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, plt.savefig (savePath) plt.show() -# fun: plotForArticle_GraphicalTool -# Plots the analaysis for three material arrays +# fun: plotForArticle_KineticsImportance +# Plots to highlight the importance of incorporating kinetics def plotForArticle_KineticsImportance(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np @@ -823,7 +835,7 @@ def plotForArticle_KineticsImportance(gitCommitID, currentDT, # Save the figure if saveFlag: - # FileName: threeMaterials__ + # FileName: kineticsImportance__ saveFileName = "kineticsImportance_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) # Check if inputResources directory exists or not. If not, create the folder @@ -832,6 +844,117 @@ def plotForArticle_KineticsImportance(gitCommitID, currentDT, plt.savefig (savePath) plt.show() +# fun: plotForArticle_DesignVariables +# Plots to highlight the effect of different design variables +def plotForArticle_DesignVariables(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + from numpy import load + import os + import matplotlib.pyplot as plt + import matplotlib.patches as patches + plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file + + # Curved arrow properties + style = "Simple, tail_width=0.5, head_width=4, head_length=8" + kw = dict(arrowstyle=style, color="#0077b6") + + colorsForPlot = ["#E5383B","#CD4448","#B55055", + "#9C5D63","#846970","#6C757D"] + plotIndex = ["(a)", "(b)", "(c)", "(d)"] + + # For now load the results file + loadFileName = ("fullModelSensitivity_rateConstant_20210324_0957_c9e8179.npz", # Varying rate constant + "fullModelSensitivity_flowIn_20210407_1656_c9e8179.npz", # Varying flow in + "fullModelSensitivity_volTotal_20210324_1013_c9e8179.npz", # Varying total volume + "fullModelSensitivity_voidFrac_20210324_1006_c9e8179.npz") # Varying void fraction + + # Loop through the two files to get the histogram + for ii in range(len(loadFileName)): + ax = plt.subplot(2,2,ii+1) + # Create the file name with the path to be loaded + simResultsFile = os.path.join('..','simulationResults',loadFileName[ii]); + + # Check if the file with the simultaion results exist + if os.path.exists(simResultsFile): + # Load the file + loadedFileTemp = load(simResultsFile,allow_pickle=True)["outputStruct"] + # Unpack to get the dictionary + loadedFile = loadedFileTemp[()] + + # Prepare the data for plotting + timeSim = loadedFile[0]["timeSim"]; # Time elapsed [s] + adsorbentDensity = loadedFile[0]["inputParameters"][1]; # [kg/m3] + sensorFingerPrint = np.zeros((len(timeSim),len(loadedFile))); + adsorbentVolume = np.zeros((len(loadedFile),1)); + # Loop over to get the sensor finger print and adsorbent volume + for jj in range(len(loadedFile)): + fingerPrintTemp = (loadedFile[jj]["sensorFingerPrint"] + *loadedFile[jj]["inputParameters"][9] + *adsorbentDensity) # Compute true response [g] + ax.plot(timeSim, fingerPrintTemp, + linewidth=1.5,color=colorsForPlot[jj]) + ax.set(xlim = [timeSim[0], 2000], ylim = [0, 0.12]) + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=5) + if ii == 2 or ii == 3: + ax.set(xlabel = '$t$ [s]') + if ii == 0 or ii == 2: + ax.set(ylabel = '$m_i$ [g]') + # Remove ticks + if ii == 0 or ii == 1: + ax.axes.xaxis.set_ticklabels([]) + if ii == 1 or ii == 3: + ax.axes.yaxis.set_ticklabels([]) + # Create labels for the plots + ax.text(50, 0.11, plotIndex[ii], fontsize=10) + ax.text(1600, 0.11, "Array D", fontsize=10, + color = '#0077b6') + if ii == 0: + curvArr = patches.FancyArrowPatch((800, 0.02), (300, 0.06), + connectionstyle="arc3,rad=0.35", **kw) + ax.add_patch(curvArr) + ax.text(300, 0.065, "$k$", fontsize=10, + color = '#0077b6') + ax.text(1240, 0.101, "Varying Kinetics", fontsize=10, + color = '#0077b6') + if ii == 1: + curvArr = patches.FancyArrowPatch((800, 0.02), (300, 0.06), + connectionstyle="arc3,rad=0.35", **kw) + ax.add_patch(curvArr) + ax.text(300, 0.065, "$F^\mathregular{in}$", fontsize=10, + color = '#0077b6') + ax.text(1140, 0.101, "Varying Flow Rate", fontsize=10, + color = '#0077b6') + if ii == 2: + curvArr = patches.FancyArrowPatch((800, 0.01), (300, 0.06), + connectionstyle="arc3,rad=0.35", **kw) + ax.add_patch(curvArr) + ax.text(300, 0.065, "$V_\mathregular{T}$", fontsize=10, + color = '#0077b6') + ax.text(1020, 0.101, "Varying Total Volume", fontsize=10, + color = '#0077b6') + if ii == 3: + curvArr = patches.FancyArrowPatch((30, 0.08), (800, 0.015), + connectionstyle="arc3,rad=-0.35", **kw) + ax.add_patch(curvArr) + ax.text(800, 0.035, "$\epsilon$", fontsize=10, + color = '#0077b6') + ax.text(960, 0.101, "Varying Dead Voidage", fontsize=10, + color = '#0077b6') + + # Save the figure + if saveFlag: + # FileName: designVariables__ + saveFileName = "designVariables_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','simulationManuscript')): + os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) + plt.savefig (savePath) + plt.show() + + # fun: getSensorSensitiveRegion # Simulate the sensor array and obtain the region of sensitivity def getSensorSensitiveRegion(sensorID): From 56a82d5ae5e8398c9d931af6c5081c633aa04f8e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 7 Apr 2021 18:32:07 +0100 Subject: [PATCH 93/99] Add input resources folder to avoid issues with other branches --- ...isothermParameters_20201020_1635_5f263af.npz | Bin 0 -> 6128 bytes ...isothermParameters_20201020_1756_5f263af.npz | Bin 0 -> 6394 bytes ...isothermParameters_20201022_1056_782efa3.npz | Bin 0 -> 8802 bytes ...isothermParameters_20201028_1756_3a07f95.npz | Bin 0 -> 5642 bytes ...isothermParameters_20210120_1722_fb57143.npz | Bin 0 -> 8802 bytes ...isothermParameters_20210120_1724_fb57143.npz | Bin 0 -> 11210 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 inputResources/isothermParameters_20201020_1635_5f263af.npz create mode 100644 inputResources/isothermParameters_20201020_1756_5f263af.npz create mode 100644 inputResources/isothermParameters_20201022_1056_782efa3.npz create mode 100644 inputResources/isothermParameters_20201028_1756_3a07f95.npz create mode 100644 inputResources/isothermParameters_20210120_1722_fb57143.npz create mode 100644 inputResources/isothermParameters_20210120_1724_fb57143.npz diff --git a/inputResources/isothermParameters_20201020_1635_5f263af.npz b/inputResources/isothermParameters_20201020_1635_5f263af.npz new file mode 100644 index 0000000000000000000000000000000000000000..b1c8357d4cd4e09392b174a05dcb1d9786226d28 GIT binary patch literal 6128 zcmbtYd05Tq7yp(LNm{knO`~G6m8BV+dy%yelcli@Dn*LQ5)EI`SSqBnDWarE+byl? z-qwp-qNIq3(u{2)BqqP^J?B2p%=|gOx2NaxocHYKeZTjd=XJCnprFOzGGz|_b~K)+ z$uN@tVT=~D-o7qe%jL7ieqENnJ|9*C@apd zod2?vF$y`Jjbyzy<<*EKtCw3V0TnZ(9W;0oC4ByHp0yN>P~PSd@uI#`G^e z*}gpT`P!&wqe6D&coK0pHyM6K{bW;~)Q;O9Tq)b!4COk8lm0-SG#-cHJc*l_m`_|p z6`nMHR)h41U>q^u(5{;z#j8tx+qjtTVA3-pKgN_#7y5VW=1S7E;pRGmOWagZ9lwpuT{RHO4rbIaDV#(i=f>_Mo4bI~XskiE{5p zkY6pd$K#-$$_Spjq;MVjln?QlUbNFJ^KXiJ%qz0rPJG$9DU ztgFpKJO@23+NU^eR4FfYo*W}tOR}^(YS6 zc~?>0P3Zo~?9uu1sGc2$R3B}+AFAY^?275$=&6x?J(6R1e;s?z)o>QU^w6|tr*~CD zZOiD9E8bVYkLUd4_*(>!v2=6Az&}#JcK0^n#-sTVr#91h)ySQ&*M0rmoBXqo^kZRs zsay_J1g9q?PCNmNvU1{?h7!mRXdASp>Kb%=tUFfxu?oz`xKyVuD1c#&-+mBBRYT{J zjEy@~%RrF)^TOK~BB0Z>IzlHq9%^#U+m+O>!&@ga_hWhFu3=*Vi+13q&x={?P6} zvMb>DgY5_E8!F+&=qqOCR(rtdx!QK$(MQ29?>o!hpDH1&C(K%FdKPT9a}Hg%q8id% ztlP{UpNCjI^G=U*r$8=Y+y=Y3dEn)IIwNfMHHcSQd(+`c3?$AvYgM~96F9v}-IKf1 zVdZCs@Qb=4*tFdAN`SopOifiL{?=Fpt5uKY-XE9;+vnDV`7OB)M{4!nWc-o{3dc9u zS^QK2mQL4r`ovn5qEFa{f?CU4h6v8aQpu9n~LMV?o zH7+~-3@m&a=ec%#IrPRAtr@pa2yfOXeTojufwfktZo1xOP~h9=zuKV?q8j8rH9M6; z^cTe`qo!1YyZH|-#&55{eDN;}tY+pw`l>}gX}eqm`|^MyBT*ha)cErC@vL01{EoFf zQ&$XuXRhl{S1y3XP5F-&U#o(RGun?VSaTkZRZd!Wz9Siy4bjdjQxU@UUAe#9y($Eg z0j~mQdtCux{LT`APNUKH`OHprmNN<0L$lH&0%5EaPL5Ol) zPY{=(J$3`>WyYgiTMg>zHKSZ8$}_L{$X|H{@%4Sg>Gg=$TcyZ~DIZP<(#jM!zYgUV z6Ka(pe-+s`u0X#QXouN*2j$&!?CtqmqYqVg-n`BE+8ah}XzpGo42h#nG9Dd>xFJO@4xTe0hZX?g@wuWWTT+ z=_(a%*(_e@YOMArWYkUas#p-=PEFxpol`!vUN7*Ss|z<7PB zk2jkU&7pV)5e}rhD1HIWDdJ(YPjb#9&~GxK+n_sUz)dMs<|U3(6#?jq^yd8P>yh1l3=c z<`|lnI@G9+vV8dz=WELE0~hD)Cpf=|Wpj=u%_WAUuZZ!7aL}I(L-UXU#iL7c%H|(U zn%5pv{f6_T-`9RatowTo*&Tv3pZuv%-B_BFJ9KE?G9Y~&^cVAm=3*1N4?5`2ob&}| zR6jbWWZAhl;5^r%NB-Z@`DpMARqE88Ixj)M3D?7VkyW7D`b_jdy#o5OKJ8yWH52at zRq`NhSrrV;Z#@z`&Jz@Kw;b9zsS3&}_Vf)KB7i3rQ%uyYDKGuv{#F*6{}ECTRi~Ju~6=hqfZv8$ExxrCK?JK3k^N zwx|qdo#b?0-hLI#mhbjo7+M5Dr<#Muo-Bq)g+D5%WR*j^&57)Z$&rw< z_+x3;+cKDw&Wm~3ECSBxKg%K<%ORwAN!=;W%TOF<{C#{*CA@Nb8FcAuCfLl@dh@6C zK6qfz8vOct7E}~Pddr2C!{lSOt?aK^5Z-Y^>(jh^*mpI^_UFDVu<}S6^}PWPx_oBo z-P%?Tj~Sa*;o@S@d^vS>U(hv}aiLRD4si+%7J@D9MRZ%0q(V)9p(A@G8D$j7hV}#1{VxtU+s$#K|-JZRG1J7Q{7S$ z=bSDDAFn%i%55qkc|xOV*5N!Dzo;kRTUso57%f|MDoq`M*u-@lFFX zu2p*#WW)1mzB6WV4ubBd@VBPDB2d10DLKU~2vST`yTZduz@aeOYS^M8cr;kYtL{cV zeBO~~^kj1?%#2akRq!$uZdSjMAE|f^tUYzd^0cc!z1!ra3d0AtaYkFlc%;CT15S=U z19Cx!*}ZSPUnT4a=?YYcI|;k@jBC9ZvE<00p5;ToP)OtKsDpb=YZUE zP???mLtY8s_j$J(+#)K0zbU!Hwy7GJRL!oX zLOxszs*3XG@u6{GqDRr~GLZhBZ@h>7D5+GyNLECa6j>eWlOwVJursMo&&EE;9>9LZ z&VPpbsnj18sUNCQzaK;W(6~uzmr=hfm1h;G->t#E%^Fd^%_X@biD>sp5Mp;BVoEaF zSx|{+n}WDI9r@|F&z6$^?`RIYM16QZ_0f(BsUBPK6#Hyo6C(2n(S_y_C7cJCdYan; za?u}``u5^Blz&I_2jh%(57RubIveBqhIL^CK1dHG{g)({lY@GG7&m*D_5*3oYrHMh zW8JB4S1AvN>uB#S%`?SCNLSMw@q+5PvkLdu;T*)=p!~*%VLlnnh>Kewq~QyQ=?wnW>Izb!3j_cX;PN=1E*D%@X7^SACrq?gnDH-Y56 zX`b`BjrNz*eB?oKg;9Q+X&$vIL_2e8(a*0G_hib?F#`G4l=l#_zb#7O_>Tv;MkALW z5(Y4gH&H!ak^Sfp+NXG| zNIr-3cgyk&L^%i2OQAZ5DW4FM&nG{k<0$_f$&1PEA^!Ni5fr~I#W#rTuO$6>ff(;@vfD}Z&nLSR>D~p9eHEhTk(?Fz)1~<5(fzZedKr@c z)s%-W-D`J3C(<{J;Lv+ V;b^ZoP$D7U|L5P2>vbg5{{TwNGgJTo literal 0 HcmV?d00001 diff --git a/inputResources/isothermParameters_20201020_1756_5f263af.npz b/inputResources/isothermParameters_20201020_1756_5f263af.npz new file mode 100644 index 0000000000000000000000000000000000000000..bf47fe4485f17ee64db7cd9cf84624331742617e GIT binary patch literal 6394 zcmbtZd05Tq7yq^?ZCbV0O`~E$%F=Iex#-gBOBuFi7uIt)JMOh{N$ z99NrR#GfII4ztE1aCV@7khf>RrqOyq z=NHbud6d_CiJw0m>qO#5fh$25W9~S6@$($@s^;8;_<_e@D4`bGiTWK;ugR4p!|Kp} zEwppzaJ|LuI}OR7GPhnF#>JE?29fBkqu6bH{6~!AqK9@oQ?7WMmzNCsX))(Y=)5zh zIQl5t|3$p}93|^K4ca%DYbRFlOpuQ%2Ql1&_7h)5jd(G{%ZTzbBqTn8=CrQEl@TY( z>LPy^6ZFS7r?|by!(NH(47pfes;fwi>S0Da3=kPP+~28*apS*IU5&`Ul(#nK&C{kj z8F0UfpS^mNuRhgl81d65zXrrl4*hXCC|jE2KKlpA``s|IQ>62i!8q?M$e%9d^M@mz zpNQ(NOFWD*zPknaA4d7SB6~&3UzhmnQ2pOH;(dEh=h^7UNFEvV(`icgit1yaOMW{X zCD){LDDizv_emAk4WxB8Mc$oSlt1zFQX~IHl$Q#kB?s-;q1;tp__4)WWl$BCnUp;4 z7`%0MY}tLZ1QHA^yL`?Zhr4aRjqrVS5z?&R?meeg0^XMYYc+jy3Czt^$Nkz=1FIFz zTV4w1dl@p_?-DG0)$?^< z$_bE38nf1EW&zmw>W}8?)x4}!V7oiuvWW(>XZZvwuRKtgZdnf(1>KrVU z9Xl-Vcoo>r>ka;zkpK$$8xL$7Ujy6stM$A$_W)6ua?sGQA~>zM>blFNc+hQMTK7h! z7!DLYyzxsw6;#F?ACsGT3iOyAdu)TMVU1=(Wm;5dzq?WqMQds4!^k3Mt&R{sK5& z62&yNor9C|e^gJ*sf66wzmD(q&w%cLX$ChoSHhN=bx}bJu0orwQKnf2A7*57<6pE0 zLH_7^C#ywepnUy8YMMnTEO}tl?vb4cX=du((a~ivjjvRoR4;)3oR53gOv;8Aj?bqr zGrR_8hVR)Op41PS< zr6BZ7g&|E}|0jyAg&n&_cs{=n3x6GX=cwf_gcA=tLc8iqV19Gq!}(Wg;MwHBAE$Bl z!`Q7~O6MJ`fa=`b$8U$$!V2}n`S%7Dz_L#+(dYGr5PZz@pux!+IH6YWHsgCf9I7{X zo%M4vM0Z}(`8cZ(VjE>Xwz!pp_KQg?`a`dPY^-y`__|{F;Y^SWZzCU!bn}wdu1$e1 z^V%4_+|yv=lQQCaBQB)P|4`ojrUKNnK7R_%KLZD|KTY=Ut%a@O-68Uck&v};L)D-^ z(%{}-W%n}{*TC=zvz8QA!ghZ;`IYd60ZfvrqHI$eOhC zCBKgWHzhK2_#Vn%8&Ph%jhNMf`ut{;PhR6oe#aUgNM!cT6V%&Aq21?)s28=Pe)v6< zi=NOZTx{LdhDBtw^xZeF3?H7trPNF)#X+wWaREN$gw0nI6QGw1; zkIu2>4(dB7@6r;KM^;FX4Wc+3ThZ?k^ur#cc$Vb%GWoM9#(lO=(a-%F#C|@aVIATz z%$ME$6!UYSa}L4##7=#T`nOFOSBv~?y@PrWItSGP^tV=kST97}K=_vOn2mhcRH}Ey zb+k)xiB6@xL#bzmy9!HF53; zr+&nIz&;vI^G2s0%|V~YZ$I`!uO6Hq;+3fH=~I6kOns5&B9R>GRi%Ae8T|#)yurLB zzbahJL)wS6ssG9nGRT`R^`lO4G|=BW4#_m17-*wC)8{Ce2UV#br&>@x^5pk6=HWh! z=7d3rfx~DX8;U4WA^bpm2UB0CIhLn|vey?}=h7U>D3F~F)+yDTa2WDqjVNy&k~QcY zhY(M7%7agFNzXImN`42KW}J@#rTH79Jy%+%L3BPQG!IGn4kel9KSmLGv{+zWS~xV1 znIZpFn&&#TDbGhZ2f0h{i3ZslQr!%YA8ST=$P#wbJ?x_U6OVcErSlNYPs~eNe~0&& zC%uO}x_|AoE>CkC&E=LBD2ETjcsxVy$f-%cKX4GjeWSLpS66eOu0uV~bCUozIJrlx zURn!tpCtOOvaN&&-liW9yuA$e({*0|X}1TyMBl6DG*m&;pk$wtn-#EJZVIALXACX8@Cu;Y*1Do}5GD!i{*1=AuqT^F}p2D!5N+M!t$ z@Nv(njMvZ3gYMuJYL2f3!0A)$nb4C7j~M$l!TeI-jQq1A#Tf!60QkEZ2A zUt-D1F>?i=*<68OM^{2Tv&&9y=k(VqiZFWI`-|gF#_SIl$8aHN3 zYZ9#cvE$I3m1jY*A#8lX=_t_u82!e)PY5L&3}4K>a}~}RC%oJfF9h9|KzI^$6&8GG zJ{oBn0Hc29Cm5fN1{Fc2UP5{tgq*r+I7PV#+@5J{2^@JCrbJ{sJ+ZwOlKOW}g0T_6 zrkqW8K9CQOttOgj+Eu}(wL2lHJQt4M-?Fcvu^K*YEiie!AszaB8;;)oGXsoOZ)v)9 zU4V--w8lQ&S_3|Frx^R)u7n<+)kjJ{)Ie#J>G!Aes-bH^*1E0g6|mcT&CKh(-=M?( zSnjyglMr6I;MQ^9i?GI``rhF8)o>-WCUz&62iD)Q4ySIFf{)4Kxf)xuz-x@j#!)_L zz}tLe>&%5!5cgSO;)sd0VBu)fQ3JU!^R83N52e|VJnc7|`rX-JIm)9peNGX)Ua9yo zE+h}CN>2LAL=DWH+;geFzs~|DUAudsfCmo;>-pWfRtTzHwM%~w<3omrUAx7jvrtjB ztAB_JAFkKFmL0Be1yX(}K3y)82fO2D54F~)gh^g$$umxrgW&YGGQRaP*s?wU=R21L z5dG$IvO;AZq>gP;&pB8CQ{&~g7rjWw`6{uH&xoG{>Vqy#8!;YYLLIEN67CK&V$T4^0WOu&LfFbrx2RAR^+1Il+Je*%^O#6USqm4Fiu{c z1le+$OB$%orsU@V)uR#T95w>^uzHk_JI#AfXdXAf`m%m>ju)tIW2ycr<(S9gP>lZ_ zo)_Ce_hS&vO|CI$x0><}qc{_39`+{tS2XWB(!JFtz9w{^50py$unB0-^60)Tpt{Vz z#drPY1>BT^?mJ0x3?u%i;|n+^PrpE)pq^g|36o!}>P5N*|;ZelMy@*axh^2(h5h$;SMT{eVg~w2SCbd6_ z@;HibO@6&u)CW=AL;F$oru_%V-r*4H<3bQcClJ5TdJM&JqdesagUPOo_8lU=!UMS9 z`Y>W4)ngpx5kq`!Xum4yXHkBt6xW#SbYjqt8`~DO8U<={`CSdD6!b4_`W8A+1|eJ(S3Q zF!B73@-!xUU&^zY^6RDehp6s_q1d5IJN#>t_F(CB-urlKR z|Kslm|J?lTuif7_@6(oS{@ZWgKlgrH$-nPiqsM$(S663+L1F{Bfq!`eXsa)l{tw4Y BYuEq) literal 0 HcmV?d00001 diff --git a/inputResources/isothermParameters_20201022_1056_782efa3.npz b/inputResources/isothermParameters_20201022_1056_782efa3.npz new file mode 100644 index 0000000000000000000000000000000000000000..47cf9d43e9ae7c5c8d038f3581aab3dda7a602ec GIT binary patch literal 8802 zcmbtac|6tW_rJCX$<0otuu*rL#_YNO-SFi2gd+gjR$I_H7NyVii(s6q6ibal2a;#mf-z_(}djH~O zIgMjE!_Cgpnq%pOmmZTm@q3>NXKxR4`U)Dq$81w^5)EJnIQv4Rs_~(F3PKsKMh9aZnq)x z`lWOM(} zb8(+*xU#%GET|4z6xT4SgDE51?#f24bX${>K5%hghcKxBmMaUxoK5vKpmSi(q{!s; zC_Z`GCpktICVLpkS8-*>VnsH0uWW;dFlg@{S9Tmk9JFI)LHdSd*OcriGnls_+0&r< zsL*^6od;uz*9hw`GN=0LV!qWDw6EsmM-%I1K>I)40?(ZXhZ$?*T#-Ac5OM>>;d>b& zl;jzD^qmqwowDiHl3Rx$VfI;@#@%7?%ZQovn(xZsu~o9gDC=V|Q2v@4A$1zoLzsO4^a*c*OJ z+iaKs&h{OhWIi<;PM42#Eo?pjY4Z+G>fKQQYR$iv-PbCCCuU_~dKuSXWp7pJMME(} zD!G+GYAMXVCJOIp5W}pKoUSC#LQu|m9Jt?B0$-x0xJ+IU3l7!x_0tOY5Zdq};NiV+ zI9{-%>eRM87@GR&qi=R9glo)SxoAuY{M;uAOVP~(XT9zAo&G`yE}i6ZxibfH-OanW zH4#wRaNWZ3eGE7$T(ihOnFCt4!&5{?h46fyL(TS6xsbAHLc2wCI<$|@`~}YhfiYd3%_RgBB0`<1QyII8+MFqw} zOTh1BTM#FV2W|@mhQHnyK<&GA^TV6UV9&lWu8*%?h1XO3c#ao~U{URi#<{#KSW-W+ zWo4EG_D7WJ*=S@#6<>b$h$Gt}{LPhktzsT<`jopTcNalv!cxbkixQA4yw5(6o(8E) zw}=Kk<-z1*b}jz<1z@W1JYcp*K8!FAn)K&`Ac*V|=qb%E0L5gr?)8Pa5T*H})0&Zo z;ociJhofzokio>nKY3pa(~gg857|2j_JuqN9Cl6wvgcW$na`4rfCqfpdwfV1(%G?m z+57%UJ74zR`dY=8y(Xd`@@0CGcBH?O{vWNVm)wAQxwQTw4eje^BZXRAcWT4=gnG1V zatHn7-A6mMjVPyCk2J{<>DxkF&n5o%BIJj*BCWcI^z(hb?7cR;7UjJgkO zi_6e|Kr!;OLy@m?598}9K>iW|$`zF&e^4FL`gUBmYr}eMYDfKXsE&{C;(ARB z>Mg5BJ;ylyD(OZ(B72r~DEE@~uSS4&?o=cHhetSH_XzzoOVCciO|%=+igQIi%8zP5 zzB=(|QC&Lk@V`k1y9VX#cxbQUG}_OvLcN0&_sJxj+t#4{-{?G6@{zxQ>Ux3BlXV5= zHMJDi+o-N*>(QUR811aQkA9t-kmg-SJ)?&xxAQ7r*6+==5I6J;;1wbxc*}r;ax7KC8-p0cSpzs_ zDE0k5?rmA)@8x2=#?+Vf5RWb7Q2*4XemRVA)nAC`l8n)ghdJe?Lw;UxWw0+o7x|q7 zeRu#T4kbB5!Yi6&cR1m&ArxOH;;XeBipLz~)|wM;)uVXTDZXcf$37yiu$L$N^nq}V z5#ufcTEl35Qvr+zJ!K z??H9k%zaOl)>dqUkp^+`2YyZg^Y6Ba&M&?K&KgIvYX_Bq&bLp#V|E>cFa;0MxXg42 zd66C6Bld^SW+1SBBNJWPr&SAHknX z1z=y{?qD(}8VpU&*?2alfh6<$_K(^(AZ>?+53{@k>?0I+-*|QdI&Dv6OpK3*se5(` zemN?Ejv3xFXK_x$2(I;CVkiN<7q%|y0g>=@>36Qa>RC|LH@_x)O&YvgX|(m6VhTjt zl|-g1r9rKxLSL1`WhhRC#-`z+P;@HNIxeFOEa$82ew38~qi2rjdN3^qLaNwr3Z2U! z=(fSj)b*KA)f7K!-5W9ReA`vFip5~PId05%MvfqNe8)_KRY?%sUa!&M$Oi~l_ep+L z3eVl11z!Ekhf$h*y`8O>VC;H+HAnk$D|{JN_m>@j^8G znYn%Dp|^z)DEz_s^wScsS=FefVHpm+2hOLw?8pY&jl3wUs4@^24D(bukqtZ-a2oG? z4f=k|HE!K<1K72{PrrZW28c^PeA6AA0W)K|lq9Y>FwVz6-yxI_ZR5jJMm z=^(>R-4%S<`@gUa`-|U0zV!9a(rd&16kLbvVT5;dsjs$>d=%lPd4w-kRiXZE(mO{u z=XEvq&*_90{%XZOwuAci1nR#Xguk91M*pv<@8=L6=_DMOC`5bd6#pXX`|BT|f9Dd6 zdo1;LegN_(Q~wqc?wWKF`RfQbI!O@6_|&8S@EWA4gttZras5;l)~hrX^AO)f{s$5A zH{3!0bNEOtsh+O!I2X2}d>!FL^Bm0QCgD}i1+?>p@Rpkp{cI#ZKVQMPvxjCP)oowRFcZ{aPZ&xJ_e5*~7BK)aJ~VqWb{Xm5W8@|V}3-Nq`k8<>cEN5YA6 zVw{UB(e9y2lvA!oy=8>MyjpPHOXo+8&PgDhH?MTm*JwfNOStTI8z1i%hExB4N_|t8 z`qofJ)|Xaj(z*_VI7*9={XZm$O?_RS?iUoO|Mt**)4)BIDfL-J>Sxq1r<*Z&A7f1S z4s7bH@93VylwnDSzd5bz5e_p(xgImRhk1#3En*l`D7(Khr2cI{@fi?)vY@>5>7GW9 zK|H67d;<%@X(nj5lSB5t5q>fwd^Lc#zHsq=?Jd@=nnQhAmFlKVbz>2~mF}B#D6WCJ z=u!Pb2~TKJ9hC@Iu^8EP+e;P7*MM-BF0HdEP9>@z?PrV`-R~%3p49{QbDD&o2Jq4V zUeqPNG2yKT6t61PO&R0Lw4gd@QoWwgy{8Vvr^q0_qWi2p6f){&o`{)F(TG8N5eRXJpSYYXCoKlmCHz`N);s&)N>AIJBs~ z#+2u9I&Yu3@!Atyb{iLgDd*$_zd8{p-w&RY8+94nXEg<{b}EGdNTN|`7`6v%seQsx)*BeR|1`m zwi`V(`5+Fy9vwUJ60CjGp>**=A;=vJ&QNWNf~XPl`wr^}Ve8@>#=qS;19>N7#+=Q` zgdg^~i#!7_fQ$LO8u^}Bc_6}v>YA4q>{ZSU2eswKY zNhku9h*>qW%8MXMd0oZAgJY>r(RZSJsHw?|TTm9J z53=$$%D<=kt+BZ%4|LyjnEK>#!e876Xy+{X>!W*zxI4%%U&h6ehlH?_;SQ+ zevgo!B|`t7=$`3nJL-MCi8L}9<(Ly_KZNQQCct$b={+Rso`!M^-D5o{!~7F+kUuR3 zK}z|D%B)IPG0Myf^#o{r~Tq{VUQTm5`4A^nJ7c%Vz)Q&Hwr1X8#9KRap?j&oW3u zkyf5WDmjGI-5;s%8KiMUM~C42q#wzhK)Q$cqmSZz?s23u!jOiMpROY~PdSBDo#-Z# zv!}Sih(4fo{bMMvNO6v)IJHSH=pe59oJJZHgw*a&q^l1j?IwM9vNtyX`6ANm4aT_& z$?u_jIw*c?($6BltN%cG9@$HvxjxlTKM>cSlYJh|3n;%1qAKKfIoY?P`0I!!P&^aE z$sg7I5Y@BDLvnG0u_NSNZRu1el$p=y0=F)!YlRxzV|CC?jKpiOl zg_Ng+>efYmE)f45**h12d2FM)I+NWx^1F@dGJ^CJDL!|qpEKn-k?3)XqnqUR(D^7J z`@K|;Iyxr_6mJ*BVNY^)2bq69-|Mb8o;$~4v7|pk`k(LZJGQNI-TbS&&%om?U676e z`kx+eTkHSwXiIXL(?AVEPC6N(8v{IOH*d*{9P`{%BGYrkc_uC4m&tLmP1w3n0DqF|U%I!@Z& zSd*fpp8=E>wc15AQ{?6A=IXO?xaTH+8A^?NB!x>M(h)jm;cQ1|8EP{XVD927TI*vz zj&D9eXlriCHy3*O`1-8&Tq82R%ILLc2ndYYe(y_(pb+!fnJk`Q?EmNW2WCOl`C5NYu*02%W52#}4dJ6Ng$o2K)&C@v zj7vCvjPMG}YeHSG9CU8b&Va+3fM;et1^ar%FjI2CkAn`TZ%9tmG+sclOXtkjcDanW zIM-IQKcvH@f#4M}@T7#t9I|a>b|fH~=n-qCPQL^Gyw3sLZ(`t+r$O+2@JfZtX7_^A z&^GUBHMPTn!2XXa;M42@Yb}jetcxk>V05uvEDrCeD?$p%8}@A;T>$v81fO%C<>(Qs z<8@rzx>(QX^a$_zXp}1I@n7Zu{8R2#ENAb{1JHx8AK-=WXdln=@?4vI)RXXe*Xr@{ zkgxsCfZv`>^w3Ikc@UZ6=Ff8+#noA;aio5%@l6)uz4#dXH*aJ)KZGx7E??ow*wX#d z;Gy~$#m!rv*XQL`z|YDD`^8nD2T2vp#bG0lr&mv{8>@;o=uTEGjCi2(j6f$nnFBA$sd!NxWZ({lDn3yB9e6$R(qZbG~ftU&J*ks`a+dR3@`>i^rA;5NTV!%$yFH6es+26S8_ELSs21$&lmoAfG0CqX zekJS(yz)IBSuAi<{A!sE@ybKIOiUB-Z^p+^k6pBSmvM(OB}VI~Iv-xSxDfom+sN5x z4mXi{R0DnT)bQ-)NRJgGbJ4hSzB1wskY7eX^l-A8@c)6Pq=QkzgYEZieVLet&JJIr z6nPl>3A3~b{0<}a!VJV59-HEsl2x-TCnq8AEy=4YZh`+)axLWdXav!-Ht7$tcAFWPv?_yA=7RnFUJJ_-Uq21 zp7A97G?oKDJRQKdsbS{Q`D^z3lF^XK(Jvm`7J+?-yI_Bwg!sb){+$K*`H&j|Gs|u0 z(zvv!EE(ee7#8rYWh_@$&L%{kdU)RJt{=NEokRlKvb6vuftO#vA%|xoT3Buya?r)l*Wc3ZPH}7fRCADh z?)G%UqB~HJm1IA9R6zLDBmD4jq}pWXrGqx3%s5ovV6YqTQ*VPmvH73}k$~t&On(o3 zSi-N&L&H|F5eDZ&0e|fg%f*+L3-uc#z}$DLKQ5^sGHCIKJoMM05446e2L2Gs*+~Ncv&!JEM-{{XOLuNm#@m z@-F9uV=lydH3s6fA^g;tko=lr_t_H-Jj-*@Qk{)c+ZfVsClWnOsQ`Y=1Vlg2>8r-7 zouQ$bsNJOSgic}<#5)uCp*9zge%qT@?mWQ5W!UGoN0Lk>-+@7d zANn!iHHq#RebA4Y2_7%JdeQD=7CMwadgb}H7_h%74ETB52=!h^^!&jDA9>`xyRs@D z2}c-i80Jp))ApNSpC1hVe-MCvVoY(D`^rNZp9)Y|mCWb51zEt4?q#qa)JW=)^fzG( zy>;7Q*XLKlkPfwV=SW|2zS;@rB&MU8=s6tpK(Tay+v@4}#b?pP!#AIv+yeUxGm@OI z>M96729V!ydAvXV>jbYah3ND*g|UOjlKrS48~o`?1N}Gn9k=0FRzT%_HyxcF^m=Q2hUQ`~q)Yj%uZ z))ROeg?tII)|!|A`Zpx{pYZeiY(Ve>u+yj7Bgc$<(18-(o48dMfzJ~OtkfLS(?a}d zBKx}zc1SyIIbcpY`m}%7_`g2y8z@J)D?~D=XS!{rFPaGeS7<&d4 zC!4h@s+0cEP0lmfHJ~4{4%uIfacQO9sTux7Xl`vv!`#~ioPA~uyruQPPp~Pxzepr>O`nVI>7_X+y2pP!vN{a-q$I%O zU10x%Db!1wI@Yceq2`dgNcVH-Ta#|GpW0pp{F-c-sZTWbo}p}t7d%t*77aOws@nIY zY+H8)>{HHw^Qt6#%7Q+5`uL`cb&KH1c_iL+$Zz_*JgCP*4*)+hg!Jcuz^6?g++sBU zm~2WRnsSWae%<>P*k4}+_}yjDe}ef0PvQK;#HRNH3(@wYuKRhXfKST4j>9vl%|s8{ z#2;;JVxm0i&zb_%&`-y+>>laQ3!9-{rV(9W6OvyeoDpI)G%6_{<>YPa8PK1c&m&mC zHw6-Xw$pG}K|Rz0unn=#hS7(CfQUzm)PFuxnKf z@j7T=c<k$8Y=+8UP+*5x z&bI-izn!{A>P3U-SrOZA-ITrdUmt5Lx+s&zN(RoL67N`!98b^f&4i(LWzo zH=Z5r@%lQd=oC&r*i8D>o;R>1{Fe;CH8p7w%SEw>x~ADVFPoeP+VVheKBdqfZ2FS@T?_NO6}!fC z#iFl%cMO}>UjzMQkv`~G0qdBb^`(iQO%P3*!rzOU%*Pt5Ta$X%C;&g=qktcqk3?^Z z_^QSGT^Cf+QK!!o-h)k~zb#7yyH2%`ukg?0{Zt>vSkD$dLNg#}EI?d~|bR0ou66A4O-ud7f$^_Y^Lvq+k6Y{HWjoH9vlrgcYK_IoW>% zzasU%C>Q)kg@ET7Vs3w-YH7vFpwV}tLeQLqq$sK?6Z+NjTEO=yf_jf3=dn6dEPrI( zRP*_nXmYsxmeda5lPW2KI*BCtVofo3Z(;M84t;y;qC##Gic(y0-{D3$#Jh;ZYgfWL z{&E5Ly7cv;V-!V6KdNv6H`UcspCUQnFFDi zn>X#pzs959NKK0GeI)&;^v1K%%VV*t`#Lw@pW#R!NXO6lKZP^K^1lVc=PNaD(~6xg zV~2A;S}Z#%I{^puer@g^fu++nc}EP-r{lm5rxYUv>)vo>WVPh}-wi6nCjFn`^!AVY z)PFAiHT!>Gyu0_dwRiE~wD8ZhzcT&zwNAa>{ldGWy+R);K(6;+QtynlrQiMs4#@Wh literal 0 HcmV?d00001 diff --git a/inputResources/isothermParameters_20210120_1722_fb57143.npz b/inputResources/isothermParameters_20210120_1722_fb57143.npz new file mode 100644 index 0000000000000000000000000000000000000000..4e9335b61b773d1de3c6e3f1e147fbeb872650ba GIT binary patch literal 8802 zcmeHNX*iW@7k*8VDPx8VTTZAnYVb8I`;gcogTE6RaexCFF^vZR&*0Y|q?zNt0@3pS$mAIVZ^`v7%gUvv(GFaFJCto?@hx!H}8~U)Few{bFqo|37NZSj)RjFvyItp?(E{T z&f9zf$9$5jwYeq7+||q5*L#iUDlczm7qNc&8V?_rZoQA&n$0fVvW1l;r~BWiks}9N za(?CflSgsAtN8iDra~lsDRL{tVeE~#Vi1Ym+lbvpM100L_Bv?CGvbQ3xw=ZBpE?t+gw6*O zild9N)t|(>(?+t+Q=@%-xueAjo+0v4<{$=|(thI0s1PrPcnzTZ^a+WNj|r`7aizqG zvf9Ys-VpuqO(<>$@~~1MJAE$Jm+C4~p?Vk-4?RRi2KTq9W88@ERM!FIzlXOb=FQWj zI_Yu0i=VwZl&>z;YasE{CBJ&aPX_&QIVhW%;6AH|$os=UvXiItmBKh5Ov#@%<@3@8 z&rd{k*CrkY7~jd1{12pj-jKaK<*!ZrwW$8@Z1BE)r1Pw{VI+?f`e`wudqwrp(v}zPHbUMl8k9frb5$Y#11K*gL^BTBvHiI#zwyIzGzy?3ASpU# z%yD>cXH&QPST02BnYFtAeiH6AoEz-%<}$=vyx)64B^TVx{!?%C?h2TgD3AHArVLie zT{OEMm;>29T|1ZC=fLnK^-8Mdhrq|>&6JF6Ax!iN9KEPt9yA#(JRzM`4qgtaoC7zr z;a1i3(=&GpA$-hf;a8o z_r;z9spt`FZD(Y{Xb;`tT_<0$^)7+p(32z5lFon*vt!Rl-%?mYVsA7F4nt4hM|z0J~zpTti_doEyGYF)By^yS7ZLsT~#o)vf!pcCEhz zHdjQUH4PWwwCu~$@u|g-HtV;s9bSph<~>#K=H_D9GNU5ccYZ!Jj2w_;oXCgiN!*B6 zbwZFmw%*o!VF4)KxD*#}>JN(_S~fYSL_xf<-c_g^CQbW_k5|xZOFsCTv+W6eL6aBZGZloB5MP=fYB$o0@)>sV;z@ zFSN=DUE-i$&G-L^!pdRCuE8$PFNMLMM?ctTI0@m@qh|ls%3PRNoAqekwK8}%$>-;( zoC7dw>$kkQ$BUpeE$zws{^hV-^+?A3KAEuei+#vNT_N}#cR8eYx(rUKR60%{%7??1 zdT*0g$3RHSb*;}cvmmTm>T{iAA!xpuxV+2%8c2uPRgJC4feF9+O7S-GVSsje^xCzt z&}vd1s*`pWEZt)V4;{dT_<5fS+ujv{YVy}Fei^^Rp_DI^ygJHZYhc@c*{BncykJ8~ zpO^7)|IdO4iHpi$(72h4vr00+|72aj@Dq8^_<61a?_MD!Et|Vg+xa3$KBg&W`C?=Z zoA{FNNAFt_nK^PFm$^QnovLJ zKFZmRxL*7i<#`l;a~Rsm*Q4JY;-P~68Lukb7axb1REqYh#9Q|w%1srx9)|H5>v+^p zuSD!PkGQQ2k#`$$^mW9FYRP_9mUv$$KTwYAoIpgoI^?++^J3hhQNQ#G>W@cCWOg6< z8AkaodxHFi)}XyG0qqp2eqZk4{%FcK;Q_9Bi+L#V&km+MKjOJC&e^!0Q_Po)H+p*1U&^SjJ)?d{eJbKB^}T*vM*Ogb)aTW4 z?g*rQ#C*a&8c6d-iw?~}U&wD4_CwcpoF5_-sPE}gf9y+rk>(rujrq6YZH!8_7JVO#L{{l=6`!zjrVX zr-3vl^g;9)Nb^{KM3EBVC*s?e`Z~?AJPnjxzu`KU=14}4?6j~>aVCTVksmvN^420* zjn1(j@l>Te_!O7)JY%lpdyuKc`N*dye*?7V_SC5losS{SLp^-^lT7m;Bab}lOtCI? z9Gb_Bk$)V`b1j;b=VP3MoOX0~!L}=h_bFK_8f|{C~$Q3?GFxc+kj+bdApxW?M_&~h`rk>!mUfyyQWD4eK z`X?8`=RIc<-#)tt+I^R+*t``0r&GRtTze8cW~>?n^YVZ*xGU4unR+Nnj#0toI19<4Pw6)YUx?k*e>LZBK3p(}e7z?^2-ol0XjXZj@|hq5e$@X zt2?${g3Hr2Mm^nH2JUkv8+hI+hIaQ=NAo_FL0+)Y(6i~K&^kYP-B#5i*zLAv#tq&% zXtp|@HYV;g1m?}ZebVDHtT8RU-}hrFT=Opr+sWmD#ZRpDncI2bZn$WU+SX)n9bvd} zn0q|%HXq$OV?haof0Y|QczijS+E_N1K^n}sXInQRF9l+zp0ljnodRaVoXZntXT#eS z@}I-^r$b5ZX)meZ?zxkDA@0S;WMC3B+ZG6T@Tjkj=k4oRpxj!%SP$o=?klmjBDgo!Ks4PAsej=$4+UKME9Qz%!kNWQxnr|-S ze8QU29H3N%_Iz)|&g??}5g65H1_2_3Z#XpAql-WabmF+cLA4hq;=ApbJ39(FoJj?GP{#u9n#l?uTXl{^A z!S!i4-!NJQxX#^+s7`arRO^?w2J+>L(NTI%DX6v;kz66V9MrFrDkJ@l7P^>L?p zh;c&w;TXhvjL)i(zu?En?+ng^%sTS3{Q=G+QBG;b|WL%k85?@F3CuHn4Kv?gMl z^mGZbg*2B`QJsy*&qJz5HO@I~5b|MlC?6-9_Zn#)*Tedw*tKt_{Y&%b)V+5Kj-`}O8Oe!1EGLR3*gBK8DVg3C`L>K;XGJ%p$bfEX1* z`aneCF~k#l5p9DJ^9XB$P+lH}7*77Oj-&jn$Nn_RV<^4_`E_Ga?@MtHA3)iS_8%mB z>%*uI-;XFdh4_uuLn)3Uo0?M-a299%CqvP~vMz`;|#Qlk!uh zxCUgW6^ecw$zGTE&!o7MXuX}*ODNx3%FE3k_q!2~csdV--%!7U`07%{OT!>K*FWOGm+vtlYR%CZ#>1z zBHl}hPY3ObrFx|I?4$FLC4D&Y@SyV*(z*rJLxKGJ5zn6}PXn^|pge0SzYdCjnChP8 z&;9Z9-F$9yemKK0;$M~S&-W%Tk0mbd>)m{NKHlO5@zaC<>f>!?`ENej;y-s#Y}}=l zG(&1SxA>#Q^5fEzxDs-VT6bDf(poZFC^CQl%M4O?11lx||KI*j?O^h~WnYK}kQc9zg5~UH6t?Znv z=Qzi4NTtP=T}IkBMcQ8HT$kVHo%!>9e}8uO;c{R1wcq#iT;J}`+{bsqz z)%#~J%jq1;nQnHL)*MT>9bP_O8$4Wgcx`f(^5<^Y=Iz?g_jcc~)3tv-dXhEA)|xY6 z{P;1}oc)~tW~lz+CLL^MuDx_T;|9rwDKp8oe`K07H!Gz>i`lbVItDW`F?%g$7`uOj znlZ^q{lm(Pk&4^e>NCwv(qY1^Tp=C9X+DgJjF65*F3OuTb7Y1L7+DcmpSUQmM*cJy znY-PF%&QmD(ab$6jZ1}taoK7!vM^OF7#TlEp6nYk1^LpUPw{G#e+BYm#K_Wh(_>!B z?CKCT!gYT$(jSO_D9&eaFwZ1Kic6026>`zv4KC*Wnk%adTNC3_F{3;+s2&Q8g?ayo z;ZPlWxmX7_)|362i}9&5$7Bf_p}e^PGeW9oZp6sR3f9Hdl-8g9j*I)#L4MUR?;dli zM;jOG+s(y&uHnk^_OPHjXi;3ls1Bx#Y`ZHPxzcS-N_x-5eI3G}{u{0=40ATs(}2!_ zIg=uj*Q5C4X`kd6S(xl$Bwxjq9g7v&+`Y059>So#yIk3E5OL6sl?CY=l3i1>qs(C5 zhGb8J>Z3yQL3AFBDPAM2zsQ{GtBd(oThP9mlOIj2mjUhn3=2GW9vo(zjdMlr+(O6= z6o>C+giw-a=+Spd0CmcyTT5;pf`mC|Z5nrn!OtUR)oZ>jgGW}$7Ne|>!OVk;mid28 zfJlQ;U7P=m0qaGqeXrBR@Xou~@UBNGkN;`$?U*=NZ{X_C?Rx}YKCNci|pmdlkIK&%Tc^aaPOrGvOFNW3X zp&8|>nefIpHnFu!0GDi<)nbD)A#QqJRI!{0Z2g=UZ&FT&`IYMzSpOh`52x1LG^j3t z!|@R*PX2qrH&CfZ!9)TZ$BlG1-7bRhOL~33rX+!K@eS?cS4A+S`k-BDL@DTM6+|s} z1aP+R=w$P0*>Jjiq-$aG0Z5yFcyjNK0#IxIrR<(o2|P9{3)9QE3M+f7 zLN6MMAyUb$3{p#B&Q(!(M}rt%4w6k`GC?x7fu8gTZ6w>#wUCj>D3tpFVjf2*7>R zv__Mc`LIGRn{zd+1g2~cvvJVkfq&ek#D#}Sp?OrkvFfA@m>K;_K5uggXtugkv<=OH z?Hl((ba4szooox@gz>;_k-+emdjhC^yKX^vQyJ{pH^%kRl`HUSsvpnsVi7E^o!K~# zmjyr8Pik42C4v1BrFu3R*-*uo-#y~Ub_jocIbN%n2b@0T?kU|xP@1sRvFV}&e4NuK~H!v<(OTI|9%0ODm)9AMG4YSz6~px7* zM|W_&rUmtu)uWzc9DkK`BOj7I%Q}>MLHk!DKs&dqk^lWeoUeO`ewrm{r{D(KjcLWX zA|K^PH6UM|__L`lowxa4rGs6Aa&|nlS8*Ec=U1WLL5lli63%UF(EhJ<9xM6CUr2Sm zK~c{;7kwB69$54B#U@M)n*fwQ~^{DKRYR@V6ko3iiiP3&K4gxiY+Ez(F~dD&dVm z4C1T-oHCU9ejoRitnv49F%@@L4(NNSHi;sb=Y6n)FFNu7jgYC%3F@=Ye8|bD6e-Y*TbRnI&jW(F&--pBYU4$6YexHr}~80zo_NSLXuFcgbfq+?)l}%Ka<~M=snFOoJQRvfMOw#Yl!4^}mE8}sGGO$q5ncDE=Rimm z`*opn83f%jc#*n36RMixN3DA;2A*%b%2u%$tT)Gv8EfPSa>sYfGFX)a!R_@L4UT+( zaCM*Lm!i!w-eV4(*=}cTQ?l~7A0$=wH4i%Z#;Me1? z0~9Yr!ylR3XB~P|2!X=yolie00h?8gY8sZ|(0kx~%8QO{u-(XuvWhALaltT8l@r;( za{;Fb&R3!D*IeV)E!Tlv``e6rXRd>|^!-=e!5J_srb|iUngio~{PP_``Or2YJatr3 z39Q%ro%fe~GE{&1q5kqJ5k!C4I|C*}LBp5njhV|5VA`5Wwv}lyIJWD1xPDg(^LWu? zxJv2&qs+r5xGuL|Jj zS$_pBSep(q+|*scm%aZB+pxd*J>W}U|17;W>`%dUxE@A$N0<6)3&}?jZkkW{VpSFD z-y*$pgmYe1WB;5%c;T;B>|;BqZ%?HD+d=s2$zk;WlKOrQ;gL?lfr&!2mrn67roO-a zKKgeq!MMj!f9D4ve+u<)A>ppc7m>e?aHEq1ag0wr`VX%`no4+Ugb>$HWnsNaQ!x+m z9pt|kA%DYd^gox6)ROAy8jo{fE6UdqPBhQKd~Oh49KXU4?R4Eqr-a|43>^ z{=NkC*OZU*XU%BmG1*DGiuM-WMEXpK^bO%5hX%AeEYSddsILxaB=e=}()aaZ9(s}brM}3VJq`rj9Zng38eqlKE z|0mQpb*XO+Wn_J6g(j`*Fo>hH7}@_rlGxPO<>`Jwf%`GD?{qv1{9wG;U^2q zOP}s(^cci*%E&jcAe?4`b~`y_? zNr&PZsEZ!eFO=|vCe=}ia21P@UAMhZp?nPpcj?kPo8nZW`q6&In9==?BIa2=a6hL> z_-Ozy4d6vx;u{m*x=-<{Qr(m>u1pK6gC^DMG2MIWP<)CE;w!p8Tx&u0DDOpPxUVcW zorfOUKLv{aCE;l{-6N5_KZkIU3hi$<;X!?}Gnm19G<8Py9J2=SlQ#Ju*q0An+5N2T zV2VSF>T67S4yW_>i5ssy(Pg)B5twpLPV}o2f%3iJ$+=ONzEdoJ$3?=V=P^l&dvv)Jrg$8Ckugdgu+Am&B41s-~b870wori@&-+W_98SsC7@4IDQ z5peWUVt1~SfI`;GfE`n_;C@#CZ{IJuFn;R=>uGu-cILwP1p9aV4-}zP62;r^E z*AMXr&cW2*mpghXltP@b?u)?iJaDry-a2kG58iKS2o`orpdx9(vZs*+V9@ces?W;T z;AiEiF||P#;qjWG-Nz26!I$(NzYR0^u>0VU?ssNBAPhTYlW{E>eAfJt@#m~OD6hI3 zYU@`5osPB}Jv8|s4!#y0JLykY`?^Ev;)OzxI~bgy+7ty*BjooT))B(iCD)CAy?qAq zPR5Kmo0AFO?{gP<23`Oc^Z7OMJ+bh{{b5jcZybd7hECM}UI22@HXH37%D~l5u5ZS} zEI9r0YOIn_1S%1;Yi5@hL6q{kibctZ@EEq*|2Vu1QXE1iKJk{o$SLzy@7m*K+r9MStMfp%ulNY(xy9j>BNf`BZg%Ao4 zJW$(Pk_X9-UV`4|B5;{9xM$p>e%z(~_Ukk5bqF|LX!yNqISk>o1O?mp!m{VxUw@Cg z42d%{qHlh_0_R(rg5FmYL*e-Y`5D$C@R(B_;u9}z**`9-TFL8{A*x47Y zZ~exH`%0GXS$Ft&f7RE{?|)0PSd#15hk~fTFQ~?T=TeJ(i$!vgRVe4uiu{q$DA&`1 z^R3k1-02=<zdNB3Lfa#0@WzUeUa$>W5-xcAY{S@PFM_YQHlkzdF|{riN^4m2U( zhWe=JKK5%9q8B7MS1;pB(_{r@BE8y%ac!gfqj`km#$Q5vN9lefg5tN@kMd@OKL!zA zx^ollU!%NVl3mqE?EiQ3Q16H9XxEzXpaqXK-4`A+zAG5#&IuR95sv%ZaQ z75G+xZx#4ffo~P~R)KF7_*Q}cuL?N+?E|u~Rz!I`QonBiv`q@hSFPa>5ZLhA01)b|Y1IHIFNaDLK{ zb>GcNVT!rNKP(B?LzcuM+k>Az7qdbr7CD2@- z>Zc!w>(9tOkLCrGUk6bY^1Gbu+fn>=L=z~UN#W#=>VAmoBcb|#qPYsi;Y;?G)Baf# zJxBY~OLi*<_L<~^sBZIUzx2tU`hb7RuW_Ib6#pX1Q$ls?B0m?1e~#>(3&1?KQC*$M zZXNmEMs*oMdWsaEJJrvb@|;BUIK|OTa(n1}6p;O1sz)82lLU&li{h{+IlF_*-=A;j zTQ)bE)&KPN{|xDWzPIn#w#s$$FYZ47_jva&NJq5n_y7EP=>PI)OFt)7ZQG}P%|UK1 zBYZd7`GovT=C}T7Tb~Vs;c51#CnvA1piR;K^9xvF_Y;3P>EHkO9`>Kge}DV^r}C^( nGUfkx7yeWC@0I;eT{A1z-|OtSP-&1 Date: Thu, 8 Apr 2021 11:23:13 +0100 Subject: [PATCH 94/99] Change label for design variables plot --- plotFunctions/plotsForArticle_Simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 47e733c..5ab2510 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -908,7 +908,7 @@ def plotForArticle_DesignVariables(gitCommitID, currentDT, ax.axes.yaxis.set_ticklabels([]) # Create labels for the plots ax.text(50, 0.11, plotIndex[ii], fontsize=10) - ax.text(1600, 0.11, "Array D", fontsize=10, + ax.text(1600, 0.11, "Array C", fontsize=10, color = '#0077b6') if ii == 0: curvArr = patches.FancyArrowPatch((800, 0.02), (300, 0.06), From ee62c001b6a43dd1c56fc00b035256c24eea5b8c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 8 Apr 2021 11:44:05 +0100 Subject: [PATCH 95/99] Cosmetic change --- plotFunctions/plotsForArticle_Simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 5ab2510..586b848 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -900,7 +900,7 @@ def plotForArticle_DesignVariables(gitCommitID, currentDT, if ii == 2 or ii == 3: ax.set(xlabel = '$t$ [s]') if ii == 0 or ii == 2: - ax.set(ylabel = '$m_i$ [g]') + ax.set(ylabel = '$m$ [g]') # Remove ticks if ii == 0 or ii == 1: ax.axes.xaxis.set_ticklabels([]) From bfaacf703ba2485102f028ebb18deab9ca492e29 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 3 May 2021 17:11:38 +0100 Subject: [PATCH 96/99] Cosmetic changes to all plots for simulation article --- plotFunctions/plotsForArticle_Simulation.py | 82 +++++++++++++++++---- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 586b848..2bf0b14 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-05-03, AK: Cosmetic changes to all plots # - 2021-04-07, AK: Add plot for design variables # - 2021-03-05, AK: Add plot for full model # - 2021-03-05, AK: Add plot for three materials @@ -154,36 +155,36 @@ def plotForArticle_SensorArray(gitCommitID, currentDT, ax.locator_params(axis="y", nbins=4) if ii == 0 or ii == 2: ax.set(ylabel='$f$ [-]') - ax.text(0.85, 55, "$n$ = 1", fontsize=10, + ax.text(0.82, 55, "$n$ = 1", fontsize=10, backgroundcolor = 'w', color = '#0077b6') if ii == 0: - ax.text(0.025, 55, "(a)", fontsize=10, + ax.text(0.075, 55, "(a)", fontsize=10, backgroundcolor = 'w') - ax.text(0.56, 51, "Without Constraint", fontsize=10, + ax.text(0.53, 51, "Without Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') else: ax.set(xlabel='$y$ [-]') - ax.text(0.025, 55, "(c)", fontsize=10, + ax.text(0.075, 55, "(c)", fontsize=10, backgroundcolor = 'w') - ax.text(0.625, 51, "With Constraint", fontsize=10, + ax.text(0.595, 51, "With Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') ax.text(0.085, 25, "$y_1$ = 0.05", fontsize=10, backgroundcolor = 'w', color = '#e5383b') ax.text(0.705, 25, "$y_2$ = 0.95", fontsize=10, backgroundcolor = 'w', color = '#343a40') elif ii == 1 or ii == 3: - ax.text(0.85, 55, "$n$ = 2", fontsize=10, + ax.text(0.81, 55, "$n$ = 2", fontsize=10, backgroundcolor = 'w', color = '#0077b6') if ii == 1: - ax.text(0.025, 55, "(b)", fontsize=10, + ax.text(0.075, 55, "(b)", fontsize=10, backgroundcolor = 'w') - ax.text(0.56, 51, "Without Constraint", fontsize=10, + ax.text(0.53, 51, "Without Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') else: ax.set(xlabel='$y$ [-]') - ax.text(0.025, 55, "(d)", fontsize=10, + ax.text(0.075, 55, "(d)", fontsize=10, backgroundcolor = 'w') - ax.text(0.625, 51, "With Constraint", fontsize=10, + ax.text(0.595, 51, "With Constraint", fontsize=10, backgroundcolor = 'w', color = '#0077b6') ax.text(0.085, 25, "$y_1$ = 0.05", fontsize=10, backgroundcolor = 'w', color = '#e5383b') @@ -271,7 +272,8 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1),(1,1),(1,1)], markers = ['o','s','D'], - palette = colorsForPlot[0:len(loadFileName)], linewidth = 0.5) + palette = colorsForPlot[0:len(loadFileName)], linewidth = 0.5, + markersize = 5) ax2.set(xlabel='$y_1$ [-]', ylabel='$\psi$ [-]', @@ -299,7 +301,8 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1),(1,1),(1,1)], markers = ['o','s','D'], - palette = colorsForPlot[0:len(loadFileName)], linewidth = 0.5) + palette = colorsForPlot[0:len(loadFileName)], linewidth = 0.5, + markersize = 5) ax3.set(xlabel='$y_1$ [-]', ylabel='$\chi$ [-]', @@ -396,7 +399,7 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['o'], - palette = colorLeft, linewidth = 0.5) + palette = colorLeft, linewidth = 0.5,markersize = 6) ax2.set(ylabel='$\psi$ [-]', xlim = [0.,1.], ylim = [1e-8,1]) ax2.locator_params(axis="x", nbins=4) @@ -411,7 +414,7 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['D'], - palette = colorRight, linewidth = 0.5, + palette = colorRight, linewidth = 0.5,markersize = 6, ax = ax2r) # Plot sensitive region for kk in range(arraySimResponse.shape[1]): @@ -471,7 +474,7 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['o'], - palette = colorLeft, linewidth = 0.5) + palette = colorLeft, linewidth = 0.5,markersize = 6) ax4.set(xlabel='$y_1$ [-]', ylabel='$\psi$ [-]', xlim = [0.,1.], ylim = [1e-8,1]) @@ -485,7 +488,7 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['D'], - palette = colorRight, linewidth = 0.5, + palette = colorRight, linewidth = 0.5,markersize = 6, ax = ax4r) # Plot sensitive region for kk in range(arraySimResponse.shape[1]): @@ -853,6 +856,8 @@ def plotForArticle_DesignVariables(gitCommitID, currentDT, import os import matplotlib.pyplot as plt import matplotlib.patches as patches + from matplotlib.transforms import Bbox + plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file # Curved arrow properties @@ -918,6 +923,19 @@ def plotForArticle_DesignVariables(gitCommitID, currentDT, color = '#0077b6') ax.text(1240, 0.101, "Varying Kinetics", fontsize=10, color = '#0077b6') + ax.text(1700, 0.0025, "0.0001", fontsize=8, + color = colorsForPlot[0]) + ax.text(1700, 0.023, "0.0005", fontsize=8, + color = colorsForPlot[1], rotation=10) + ax.text(1700, 0.037, "0.001", fontsize=8, + color = colorsForPlot[2], rotation=8) + ax.text(200, 0.027, "0.005", fontsize=8, + color = colorsForPlot[3], rotation=45) + ax.text(60, 0.042, "0.01", fontsize=8, + color = colorsForPlot[4], rotation=45) + ax.text(1700, 0.056, "10000", fontsize=8, + color = colorsForPlot[5], rotation=0) + if ii == 1: curvArr = patches.FancyArrowPatch((800, 0.02), (300, 0.06), connectionstyle="arc3,rad=0.35", **kw) @@ -926,6 +944,18 @@ def plotForArticle_DesignVariables(gitCommitID, currentDT, color = '#0077b6') ax.text(1140, 0.101, "Varying Flow Rate", fontsize=10, color = '#0077b6') + ax.text(1700, 0.005, "0.001", fontsize=8, + color = colorsForPlot[0]) + ax.text(1700, 0.017, "0.005", fontsize=8, + color = colorsForPlot[1], rotation=10) + ax.text(1700, 0.033, "0.01", fontsize=8, + color = colorsForPlot[2], rotation=12) + ax.text(330, 0.023, "0.05", fontsize=8, + color = colorsForPlot[3], rotation=45) + ax.text(230, 0.032, "0.1", fontsize=8, + color = colorsForPlot[4], rotation=60) + ax.text(1800, 0.056, "1", fontsize=8, + color = colorsForPlot[5], rotation=0) if ii == 2: curvArr = patches.FancyArrowPatch((800, 0.01), (300, 0.06), connectionstyle="arc3,rad=0.35", **kw) @@ -934,6 +964,14 @@ def plotForArticle_DesignVariables(gitCommitID, currentDT, color = '#0077b6') ax.text(1020, 0.101, "Varying Total Volume", fontsize=10, color = '#0077b6') + ax.text(1820, 0.008, "0.1", fontsize=8, + color = colorsForPlot[2], rotation=0) + ax.text(1820, 0.018, "0.3", fontsize=8, + color = colorsForPlot[3], rotation=0) + ax.text(1820, 0.034, "0.6", fontsize=8, + color = colorsForPlot[4], rotation=0) + ax.text(1820, 0.056, "1.0", fontsize=8, + color = colorsForPlot[5], rotation=0) if ii == 3: curvArr = patches.FancyArrowPatch((30, 0.08), (800, 0.015), connectionstyle="arc3,rad=-0.35", **kw) @@ -942,6 +980,18 @@ def plotForArticle_DesignVariables(gitCommitID, currentDT, color = '#0077b6') ax.text(960, 0.101, "Varying Dead Voidage", fontsize=10, color = '#0077b6') + ax.text(1800, 0.090, "0.10", fontsize=8, + color = colorsForPlot[0]) + ax.text(1800, 0.074, "0.25", fontsize=8, + color = colorsForPlot[1]) + ax.text(1800, 0.056, "0.50", fontsize=8, + color = colorsForPlot[2]) + ax.text(1820, 0.029, "0.75", fontsize=8, + color = colorsForPlot[3]) + ax.text(1820, 0.013, "0.90", fontsize=8, + color = colorsForPlot[4]) + ax.text(1820, 0.003, "0.99", fontsize=8, + color = colorsForPlot[5]) # Save the figure if saveFlag: From ecbbb3ef32a7acf41fe70e6c42c11205eb651cfd Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 3 May 2021 17:18:15 +0100 Subject: [PATCH 97/99] Small fixes to plots --- plotFunctions/plotsForArticle_Simulation.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 2bf0b14..7cfb359 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -589,11 +589,11 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['o'], - palette = colorLeft, linewidth = 0.5) + palette = colorLeft, linewidth = 0.5,markersize = 6) meanErrRef["x"] = pd.to_numeric(meanErrRef["x"], downcast="float") sns.lineplot(data=meanErrRef, x='x', y='y1', hue='dataType', style='dataType', dashes = [(5,5)], markers = ['o'], - palette = colorLeft, linewidth = 0.5, alpha = 0.35) + palette = colorLeft, linewidth = 0.5, alpha = 0.35,markersize = 6) ax2.set(ylabel='$\psi$ [-]', xlim = [0.,1.], ylim = [1e-8,1]) ax2.locator_params(axis="x", nbins=4) @@ -608,12 +608,12 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['D'], - palette = colorRight, linewidth = 0.5, + palette = colorRight, linewidth = 0.5,markersize = 6, ax = ax2r) cvDataRef["x"] = pd.to_numeric(cvDataRef["x"], downcast="float") sns.lineplot(data=cvDataRef, x='x', y='y1', hue='dataType', style='dataType', dashes = [(5,5)], markers = ['D'], - palette = colorRight, linewidth = 0.5, alpha = 0.35, + palette = colorRight, linewidth = 0.5, alpha = 0.35,markersize = 6, ax = ax2r) # Plot sensitive region for kk in range(arraySimResponse.shape[1]): @@ -677,10 +677,10 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['o'], - palette = colorLeft, linewidth = 0.5) + palette = colorLeft, linewidth = 0.5,markersize = 6) sns.lineplot(data=meanErrRef, x='x', y='y1', hue='dataType', style='dataType', dashes = [(5,5)], markers = ['o'], - palette = colorLeft, linewidth = 0.5, alpha = 0.35) + palette = colorLeft, linewidth = 0.5, alpha = 0.35,markersize = 6) ax4.set(xlabel='$y_1$ [-]', ylabel='$\psi$ [-]', xlim = [0.,1.], ylim = [1e-8,1]) @@ -694,11 +694,11 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', dashes = [(1,1)], markers = ['D'], - palette = colorRight, linewidth = 0.5, + palette = colorRight, linewidth = 0.5,markersize = 6, ax = ax4r) sns.lineplot(data=cvDataRef, x='x', y='y1', hue='dataType', style='dataType', dashes = [(5,5)], markers = ['o'], - palette = colorRight, linewidth = 0.5, alpha = 0.35, + palette = colorRight, linewidth = 0.5, alpha = 0.35,markersize = 6, ax = ax4r) # Plot sensitive region for kk in range(arraySimResponse.shape[1]): From 8ec7c59db9aaf865614589c1cf061ad3d3e6e051 Mon Sep 17 00:00:00 2001 From: Rajagopalan Date: Tue, 20 Jul 2021 09:11:18 +0100 Subject: [PATCH 98/99] Cosmetic changes in plots for article revision --- plotFunctions/plotsForArticle_Simulation.py | 75 ++++++++++++--------- sensitivityAnalysis.py | 8 +-- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 7cfb359..2747231 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -61,7 +61,7 @@ def plotsForArticle_Simulation(**kwargs): # If sensor response curve needs to be plotted if 'responseShape' in kwargs: if kwargs["responseShape"]: - meanErr = plotForArticle_ResponseShape(gitCommitID, currentDT, + plotForArticle_ResponseShape(gitCommitID, currentDT, saveFlag, saveFileExtension) # If graphical tool needs to be plotted @@ -225,13 +225,15 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, sensorText = ["A", "B", "C"] # File to be loaded for the simulation results - loadFileName = ["sensitivityAnalysis_17_20210212_1259_b02f8c3.npz", # No Noise - "sensitivityAnalysis_16_20210212_1300_b02f8c3.npz", # No Noise - "sensitivityAnalysis_6_20210212_1259_b02f8c3.npz"] # No Noise - # "sensitivityAnalysis_17_20210212_1355_b02f8c3.npz", # Noise - # "sensitivityAnalysis_16_20210212_1356_b02f8c3.npz" # Noise - # "sensitivityAnalysis_6_20210212_1355_b02f8c3.npz"] # Noise - + # No Noise + # loadFileName = ["sensitivityAnalysis_17_20210212_1259_b02f8c3.npz", # No Noise + # "sensitivityAnalysis_16_20210212_1300_b02f8c3.npz", # No Noise + # "sensitivityAnalysis_6_20210212_1259_b02f8c3.npz"] # No Noise + # Noise (0.1 g/kg) + loadFileName = ["sensitivityAnalysis_17_20210706_2258_ecbbb3e.npz", # Noise + "sensitivityAnalysis_16_20210707_0842_ecbbb3e.npz", # Noise + "sensitivityAnalysis_6_20210707_1125_ecbbb3e.npz"] # Noise + # Colors for plot colorsForPlot = ("#5fad56","#f78154","#b4436c") @@ -277,7 +279,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, ax2.set(xlabel='$y_1$ [-]', ylabel='$\psi$ [-]', - xlim = [0.,1.], ylim = [1e-10,100]) + xlim = [0.,1.], ylim = [1e-8,100]) ax2.locator_params(axis="x", nbins=4) ax2.set_yscale('log') plt.legend([],[], frameon=False) @@ -285,11 +287,11 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, backgroundcolor = 'w') # Label for the materials - ax2.text(0.85, 6e-3, sensorText[0], fontsize=10, + ax2.text(0.85, 1e-2, sensorText[0], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[0]) ax2.text(0.3, 4e-4, sensorText[1], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[1]) - ax2.text(0.6, 5e-7, sensorText[2], fontsize=10, + ax2.text(0.6, 3e-6, sensorText[2], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[2]) # Label for the formula @@ -306,7 +308,7 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, ax3.set(xlabel='$y_1$ [-]', ylabel='$\chi$ [-]', - xlim = [0.,1.], ylim = [1e-10,100]) + xlim = [0.,1.], ylim = [1e-8,100]) ax3.locator_params(axis="x", nbins=4) ax3.set_yscale('log') plt.legend([],[], frameon=False) @@ -314,11 +316,11 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, backgroundcolor = 'w') # Label for the materials - ax3.text(0.85, 6e-2, sensorText[0], fontsize=10, + ax3.text(0.85, 3e-1, sensorText[0], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[0]) - ax3.text(0.81, 4e-4, sensorText[1], fontsize=10, + ax3.text(0.81, 4e-5, sensorText[1], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[1]) - ax3.text(0.6, 1e-6, sensorText[2], fontsize=10, + ax3.text(0.6, 3e-4, sensorText[2], fontsize=10, backgroundcolor = 'w', color = colorsForPlot[2]) # Label for the formula @@ -349,8 +351,12 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file # File to be loaded for the simulation results - loadFileName = ["sensitivityAnalysis_6-2_20210305_1109_b02f8c3.npz", # 6,2 - "sensitivityAnalysis_17-16_20210305_1050_b02f8c3.npz"] # 17,16 + # No noise + # loadFileName = ["sensitivityAnalysis_6-2_20210305_1109_b02f8c3.npz", # 6,2 + # "sensitivityAnalysis_17-16_20210305_1050_b02f8c3.npz"] # 17,16 + # Noise + loadFileName = ["sensitivityAnalysis_6-2_20210719_1117_ecbbb3e.npz", # 6,2 + "sensitivityAnalysis_17-16_20210706_2120_ecbbb3e.npz"] # 17,16 # Materials to be plotted sensorID = np.array([[6,2],[17,16]]) @@ -428,9 +434,9 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, ax2r.yaxis.label.set_color(colorLeft[1]) ax2r.tick_params(axis='y', colors=colorLeft[1]) plt.legend([],[], frameon=False) - ax2r.annotate("", xy=(0.5, 5e-7), xytext=(0.6, 5e-7), + ax2r.annotate("", xy=(0.55, 1e-4), xytext=(0.65, 1e-4), arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) - ax2r.annotate("", xy=(0.95, 1e-5), xytext=(0.85, 1e-5), + ax2r.annotate("", xy=(0.95, 3e-2), xytext=(0.85, 3e-2), arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) ax2r.text(0.025, 0.2, "(b)", fontsize=10) ax2r.spines["left"].set_color(colorLeft[0]) @@ -503,7 +509,7 @@ def plotForArticle_GraphicalTool(gitCommitID, currentDT, plt.legend([],[], frameon=False) ax4r.annotate("", xy=(0.2, 5e-4), xytext=(0.3, 5e-4), arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) - ax4r.annotate("", xy=(0.7, 1e-3), xytext=(0.6, 1e-3), + ax4r.annotate("", xy=(0.7, 3e-2), xytext=(0.6, 3e-2), arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) ax4r.text(0.025, 0.2, "(d)", fontsize=10) ax4r.spines["left"].set_color(colorLeft[0]) @@ -535,9 +541,14 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file # File to be loaded for the simulation results - loadFileName = ["sensitivityAnalysis_17-15-6_20210306_1515_b02f8c3.npz", # 17,15,6 - "sensitivityAnalysis_17-15-16_20210306_1515_b02f8c3.npz", # 17,15,16 - "sensitivityAnalysis_17-15_20210308_1002_b02f8c3.npz"] # 17,15 + # No Noise + # loadFileName = ["sensitivityAnalysis_17-15-6_20210306_1515_b02f8c3.npz", # 17,15,6 + # "sensitivityAnalysis_17-15-16_20210306_1515_b02f8c3.npz", # 17,15,16 + # "sensitivityAnalysis_17-15_20210308_1002_b02f8c3.npz"] # 17,15 + # Noise + loadFileName = ["sensitivityAnalysis_17-15-6_20210707_2036_ecbbb3e.npz", # 17,15,6 + "sensitivityAnalysis_17-15-16_20210708_0934_ecbbb3e.npz", # 17,15,16 + "sensitivityAnalysis_17-15_20210709_1042_ecbbb3e.npz"] # 17,15 # Materials to be plotted sensorID = np.array([[17,15,6],[17,15,16]]) @@ -627,17 +638,17 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, ax2r.yaxis.label.set_color(colorLeft[1]) ax2r.tick_params(axis='y', colors=colorLeft[1]) plt.legend([],[], frameon=False) - ax2r.annotate("", xy=(0.4, 2e-6), xytext=(0.5, 2e-6), + ax2r.annotate("", xy=(0.4, 3e-5), xytext=(0.5, 3e-5), arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) - ax2r.annotate("", xy=(0.95, 5e-2), xytext=(0.85, 5e-2), + ax2r.annotate("", xy=(0.95, 5e-3), xytext=(0.85, 5e-3), arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) ax2r.text(0.025, 0.2, "(b)", fontsize=10) ax2r.spines["left"].set_color(colorLeft[0]) ax2r.spines["right"].set_color(colorLeft[1]) - ax2r.text(0.78, 3e-6, "Array F", fontsize=10, + ax2r.text(0.78, 1e-6, "Array F", fontsize=10, color = '#0077b6') - ax2r.text(0.4, 0.05, "Reference ($\gamma \zeta$)", fontsize=10, + ax2r.text(0.4, 0.3, "Reference ($\gamma \zeta$)", fontsize=10, color = '#0077b6', alpha = 0.35) ax3 = plt.subplot(2,2,3) @@ -711,9 +722,9 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, ax4r.yaxis.label.set_color(colorLeft[1]) ax4r.tick_params(axis='y', colors=colorLeft[1]) plt.legend([],[], frameon=False) - ax4r.annotate("", xy=(0.2, 5e-4), xytext=(0.3, 5e-4), + ax4r.annotate("", xy=(0.08, 5e-4), xytext=(0.18, 5e-4), arrowprops=dict(arrowstyle="-|>", color = colorLeft[0])) - ax4r.annotate("", xy=(0.7, 3e-2), xytext=(0.6, 3e-2), + ax4r.annotate("", xy=(0.72, 1e-2), xytext=(0.62, 1e-2), arrowprops=dict(arrowstyle="-|>", color = colorLeft[1])) ax4r.text(0.025, 0.2, "(d)", fontsize=10) ax4r.spines["left"].set_color(colorLeft[0]) @@ -721,7 +732,7 @@ def plotForArticle_ThreeMaterials(gitCommitID, currentDT, ax4r.text(0.6, 1e-5, "Array G", fontsize=10, color = '#0077b6') - ax4r.text(0.3, 0.1, "Reference ($\gamma \zeta$)", fontsize=10, + ax4r.text(0.3, 0.3, "Reference ($\gamma \zeta$)", fontsize=10, color = '#0077b6', alpha = 0.35) # Save the figure if saveFlag: @@ -799,7 +810,7 @@ def plotForArticle_KineticsImportance(gitCommitID, currentDT, ax1.text(20, 37, "(a)", fontsize=10) ax1.text(800, 37, "Array D", fontsize=10, color = '#0077b6') - ax1.text(780, 33.5, "$y^{\mathregular{in}}_{\mathregular{1}}$ = 0.1", fontsize=10, + ax1.text(720, 33.5, "$y^{\mathregular{in}}_{\mathregular{1}} (t)$ = 0.1", fontsize=10, color = '#0077b6') # Label for the materials @@ -827,7 +838,7 @@ def plotForArticle_KineticsImportance(gitCommitID, currentDT, ax2.text(20, 0.185, "(b)", fontsize=10) ax2.text(800, 0.185, "Array D", fontsize=10, color = '#0077b6') - ax2.text(780, 0.11, "$y^{\mathregular{in}}_{\mathregular{1}}$ = 0.1", fontsize=10, + ax2.text(720, 0.11, "$y^{\mathregular{in}}_{\mathregular{1}} (t)$ = 0.1", fontsize=10, color = '#0077b6') # Label for the materials diff --git a/sensitivityAnalysis.py b/sensitivityAnalysis.py index b0dae8a..a906aa3 100755 --- a/sensitivityAnalysis.py +++ b/sensitivityAnalysis.py @@ -62,7 +62,7 @@ numberOfAdsorbents = 30 # Number of gases -numberOfGases = 3 +numberOfGases = 2 # Sensor combination # Check if argument provided (from terminal) @@ -73,17 +73,17 @@ # Use default values else: print("\nSensor configuration not not provided. Default used!") - sensorID = [6, 2, 1] + sensorID = [6, 2,] # Measurement noise (Guassian noise) meanError = 0. # [g/kg] stdError = 0.1 # [g/kg] # Multipler error for the sensor measurement -multiplierError = [1., 1., 1.] +multiplierError = [1., 1.,] # Custom input mole fraction for gas 1 -meanMoleFracG1 = np.array([0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90]) +meanMoleFracG1 = np.array([0.001, 0.01, 0.1, 0.25, 0.50, 0.75, 0.90, 0.99]) diffMoleFracG1 = 0.00 # This plus/minus the mean is the bound for uniform dist. numberOfMoleFrac = len(meanMoleFracG1) # For three gases generate the input concentration from a drichlet distribution From 90fffa039b4243dda5b76bbb19c545d4b8245a8b Mon Sep 17 00:00:00 2001 From: Rajagopalan Date: Fri, 13 Aug 2021 19:32:05 +0100 Subject: [PATCH 99/99] Add plot for absolute sensor response --- plotFunctions/plotsForArticle_Simulation.py | 135 ++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/plotFunctions/plotsForArticle_Simulation.py b/plotFunctions/plotsForArticle_Simulation.py index 2747231..9a262d0 100644 --- a/plotFunctions/plotsForArticle_Simulation.py +++ b/plotFunctions/plotsForArticle_Simulation.py @@ -12,6 +12,7 @@ # Plots for the simulation manuscript # # Last modified: +# - 2021-08-13, AK: Add plot for absolute sensor response # - 2021-05-03, AK: Cosmetic changes to all plots # - 2021-04-07, AK: Add plot for design variables # - 2021-03-05, AK: Add plot for full model @@ -70,6 +71,12 @@ def plotsForArticle_Simulation(**kwargs): plotForArticle_GraphicalTool(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If absolute sensor response curve needs to be plotted + if 'absoluteResponse' in kwargs: + if kwargs["absoluteResponse"]: + plotForArticle_AbsoluteResponse(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # If three materials needs to be plotted if 'threeMaterials' in kwargs: if kwargs["threeMaterials"]: @@ -338,6 +345,134 @@ def plotForArticle_ResponseShape(gitCommitID, currentDT, os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) plt.savefig (savePath) plt.show() + +# fun: plotForArticle_AbsoluteResponse +# Plots the sensor response for a given sensor array +def plotForArticle_AbsoluteResponse(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import os + import pandas as pd + import seaborn as sns + import matplotlib.pyplot as plt + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + + # Materials to be plotted + sensorID = np.array([6,2,2,2]) + multiplierError = np.array([1,1,3,10]) + sensorText = ["$\\alpha$", "$\\beta$", "$\\beta_3$", "$\\beta_{10}$"] + arrayText = ["D", "D$_3$", "D$_{10}$"] + + # File to be loaded for the simulation results + # Noise (0.1 g/kg) + loadFileName = ["sensitivityAnalysis_6-2_20210719_1117_ecbbb3e.npz", # 1 + "sensitivityAnalysis_6-2_20210719_2145_ecbbb3e.npz", # 3 + "sensitivityAnalysis_6-2_20210719_1458_ecbbb3e.npz"] # 10 + + # Colors for plot + colorsForPlot = ("#5fad56","#FF9E00","#D66612","#AD2E24") + + # Get the sensor response and the sensor sensitive region + os.chdir("..") + moleFractionRange, arraySimResponse, _ = getSensorSensitiveRegion(sensorID) + os.chdir("plotFunctions") + + # Plot the figure + fig = plt.figure + ax1 = plt.subplot(1,3,1) + # Loop through all sensors + for kk in range(arraySimResponse.shape[1]): + ax1.plot(moleFractionRange[:,0],arraySimResponse[:,kk]*multiplierError[kk], + color=colorsForPlot[kk]) # Simulated Response + + ax1.set(xlabel='$y_1$ [-]', + ylabel='$m$ [g kg$^{-1}$]', + xlim = [0,1], ylim = [0, 150]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.text(0.05, 135, "(a)", fontsize=10, + backgroundcolor = 'w') + + # Label for the materials + ax1.text(0.30, 75, sensorText[0], fontsize=10, + color = colorsForPlot[0]) + ax1.text(0.8, 17, sensorText[1], fontsize=10, + color = colorsForPlot[1]) + ax1.text(0.8, 40, sensorText[2], fontsize=10, + color = colorsForPlot[2]) + ax1.text(0.8, 130, sensorText[3], fontsize=10, + color = colorsForPlot[3]) + + # Call the concatenateConcEstimate function + meanErr, cvData = concatenateConcEstimate(loadFileName[0:3],sensorText) + + # Mean Error - No noise + ax2 = plt.subplot(1,3,2) + meanErr["x"] = pd.to_numeric(meanErr["x"], downcast="float") + sns.lineplot(data=meanErr, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1),(1,1),(1,1)], markers = ['o','s','D'], + palette = colorsForPlot[1:len(loadFileName)+1], linewidth = 0.5, + markersize = 5) + + ax2.set(xlabel='$y_1$ [-]', + ylabel='$\psi$ [-]', + xlim = [0.,1.], ylim = [1e-6,1]) + ax2.locator_params(axis="x", nbins=4) + ax2.set_yscale('log') + plt.legend([],[], frameon=False) + ax2.text(0.05, 0.25, "(b)", fontsize=10,) + plt.minorticks_off() + + # Label for the materials + ax2.text(0.85, 8e-4, arrayText[0], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[1]) + ax2.text(0.3, 1.2e-4, arrayText[1], fontsize=10, + color = colorsForPlot[2]) + ax2.text(0.63, 3e-6, arrayText[2], fontsize=10, + backgroundcolor = 'w', color = colorsForPlot[3]) + + # Label for the formula + ax2.text(0.38, 0.25, "$\psi = |\mu - \hat{\mu}|/\mu$", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + + # CV - No noise + ax3 = plt.subplot(1,3,3) + cvData["x"] = pd.to_numeric(cvData["x"], downcast="float") + sns.lineplot(data=cvData, x='x', y='y1', hue='dataType', style='dataType', + dashes = [(1,1),(1,1),(1,1)], markers = ['o','s','D'], + palette = colorsForPlot[1:len(loadFileName)+1], linewidth = 0.5, + markersize = 5) + + ax3.set(xlabel='$y_1$ [-]', + ylabel='$\chi$ [-]', + xlim = [0.,1.], ylim = [1e-6,1]) + ax3.locator_params(axis="x", nbins=4) + ax3.set_yscale('log') + plt.legend([],[], frameon=False) + ax3.text(0.05, 0.25, "(c)", fontsize=10,) + plt.minorticks_off() + + # Label for the materials + ax3.text(0.8, 1.3e-2, arrayText[0], fontsize=10, + color = colorsForPlot[1]) + ax3.text(0.8, 3e-4, arrayText[2], fontsize=10, + color = colorsForPlot[3]) + + # Label for the formula + ax3.text(0.62, 0.25, "$\chi = \hat{\sigma}/\hat{\mu}$", fontsize=10, + backgroundcolor = 'w', color = '#0077b6') + + # Save the figure + if saveFlag: + # FileName: responseShape___ + sensorText = str(sensorID).replace('[','').replace(']','').replace(' ','-').replace(' ','-') + saveFileName = "absoluteResponse_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','simulationManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','simulationManuscript')): + os.mkdir(os.path.join('..','simulationFigures','simulationManuscript')) + plt.savefig (savePath) + plt.show() # fun: plotForArticle_GraphicalTool # Plots the graphical tool to screen for materials