From a71580c907ff470c23c7a27ef3f62e9b3ecc82c7 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 1 Mar 2021 10:24:48 +0000 Subject: [PATCH 001/189] Create a new function to simulate ZLC --- experimental/simulateZLC.py | 390 ++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 experimental/simulateZLC.py diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py new file mode 100644 index 0000000..d694aa8 --- /dev/null +++ b/experimental/simulateZLC.py @@ -0,0 +1,390 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Simulates the ZLC setup +# +# Last modified: +# - 2021-03-01, 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 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() + + # Model flag (constant pressure or constant flow rate) + # Default is constant pressure + if 'modelConstF' in kwargs: + modelConstF = kwargs["modelConstF"] + else: + modelConstF = False + + # 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: + rateConstant = np.array([.01,.01,.01]) + + # Feed flow rate [m3/s] + if 'flowIn' in kwargs: + flowIn = np.array(kwargs["flowIn"]) + else: + flowIn = np.array([5e-7]) + + # Feed Mole Fraction [-] + if 'feedMoleFrac' in kwargs: + feedMoleFrac = np.array(kwargs["feedMoleFrac"]) + else: + 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.,0.,1.]) + + # Time span for integration [tuple with t0 and tf] + if 'timeInt' in 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 + + # 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)): + 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) + + # 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]); + + # Compute the initial sensor loading [mol/m3] @ initMoleFrac + sensorLoadingPerGasVol, adsorbentDensity, molecularWeight = simulateSensorArray(sensorID, pressureTotal, + temperature, np.array([initMoleFrac]), + fullModel = True) + + # Prepare tuple of input parameters for the ode solver + 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 + # The output is print out every 5 s + # 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), + rtol = 1e-6, 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 + 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, + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), + 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)) + + # Parse out the output matrix and add flow rate + resultMat = np.row_stack((outputSol.y,pressureVec,flowOut)) + + # Parse out the time + timeSim = outputSol.t + + # 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, modelConstF) + + # Return time and the output matrix + return timeSim, resultMat, sensorFingerPrint, inputParameters + +# func: solveSorptionEquationConstF - Constant flow rate model +# Solves the system of ODEs to evaluate the gas composition, loadings, and pressure +def solveSorptionEquationConstF(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, 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 + # 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])))) + 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 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-1]) + + # 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, modelConstF): + 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] == 7: + # 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, the flow rate, and the mole fraction + 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') + 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) + 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') + 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 + 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 From a8260abcc945c30636cba7c2d5aef01ac7392a1e Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 1 Mar 2021 14:15:26 +0000 Subject: [PATCH 002/189] Add readme file --- experimental/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 experimental/README.md diff --git a/experimental/README.md b/experimental/README.md new file mode 100644 index 0000000..e69de29 From d66a82e1152410cd49e955a35e3b3a13c2c338d7 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 1 Mar 2021 14:17:16 +0000 Subject: [PATCH 003/189] Update readme file --- experimental/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental/README.md b/experimental/README.md index e69de29..535c7d2 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -0,0 +1 @@ +This folder contains files that are used for the experimental work of ERASE. \ No newline at end of file From 4130e7b6e83e71d05ea61ce1b7e0add071da5fb5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 1 Mar 2021 14:24:40 +0000 Subject: [PATCH 004/189] Change to test merge --- experimental/simulateZLC.py | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index d694aa8..395c9bc 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -19,7 +19,6 @@ # # Output arguments: # -# ############################################################################ def simulateFullModel(**kwargs): From 330310beb07c4ba3cf68151971b163da6ab3d4cb Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 1 Mar 2021 14:26:59 +0000 Subject: [PATCH 005/189] Merge conflict --- experimental/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 experimental/README.md diff --git a/experimental/README.md b/experimental/README.md new file mode 100644 index 0000000..535c7d2 --- /dev/null +++ b/experimental/README.md @@ -0,0 +1 @@ +This folder contains files that are used for the experimental work of ERASE. \ No newline at end of file From 1a12826ec03a266f35413e4956a07b01ff83a6ce Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 1 Mar 2021 14:32:28 +0000 Subject: [PATCH 006/189] Change readme --- experimental/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/README.md b/experimental/README.md index 535c7d2..ac99c76 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -1 +1 @@ -This folder contains files that are used for the experimental work of ERASE. \ No newline at end of file +This folder contains files that is used for the experimental work of ERASE. \ No newline at end of file From 9d12da14642b1114a22f8cc0db3dcf411e4100a3 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 1 Mar 2021 14:33:55 +0000 Subject: [PATCH 007/189] Change read me again --- experimental/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/README.md b/experimental/README.md index 535c7d2..9f9db17 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -1 +1 @@ -This folder contains files that are used for the experimental work of ERASE. \ No newline at end of file +This folder contains files that are used for the experimental work of ERASE! \ No newline at end of file From 9d802ec988fa53a600fd98c31055947b7550a435 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 1 Mar 2021 14:37:51 +0000 Subject: [PATCH 008/189] More change to README --- experimental/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/README.md b/experimental/README.md index 9f9db17..9698d01 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -1 +1 @@ -This folder contains files that are used for the experimental work of ERASE! \ No newline at end of file +This folder contains files that is used for the experimental work of ERASE! \ No newline at end of file From 98a4cc5c464a20fb2ce52087a099eb0d75758564 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 1 Mar 2021 14:41:38 +0000 Subject: [PATCH 009/189] Final stupid change --- experimental/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/README.md b/experimental/README.md index 9698d01..29e74ef 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -1 +1 @@ -This folder contains files that is used for the experimental work of ERASE! \ No newline at end of file +This folder contains files that are used for the experimental work of ERASE!! \ No newline at end of file From 5b5857eac506150130bd38a6b04fe57efd7e0e34 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 1 Mar 2021 15:31:47 +0000 Subject: [PATCH 010/189] Add controlAuxillaryEquipments file --- .../controlAuxiliaryEquipments.m | 111 +++++++++++++++ .../generateMillingControllerCommand.m | 132 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 experimental/auxillaryEquipments/controlAuxiliaryEquipments.m create mode 100644 experimental/auxillaryEquipments/generateMillingControllerCommand.m diff --git a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m new file mode 100644 index 0000000..e245370 --- /dev/null +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -0,0 +1,111 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% ETH Zurich, Switzerland +% Separation Processes Laboratory +% +% Project: CrystOCAM 2.0 +% Year: 2018 +% MATLAB: R2017b +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Controls the pump (Ismatec BVP series) which works with the IKA Magic Lab +% (Milling setup) and the IKA Magic Lab. Additionally, two relay switches +% are available which can be used to control two overhead stirrers. +% +% Last modified: +% - 2018-09-25, AK: Removed the command that would read from the mill. +% - 2018-07-26, AK: Added new subpart to read the milling intensity from +% the mill +% - 2018-07-25, AK: Additional functionality to control the IKA Magic Lab +% - 2018-07-20, AK: Initial creation +% +% Input arguments: +% - portName : Enter the serial port ID for the connection to be +% made +% - serialCommand : Command that would be issued to the microcontoller. +% +% Output arguments: +% - controllerOutput: Values ranging from -1 to 6, serves as an output from +% the microcontroller to indicate successful execution +% of the command. For the Mill, output from the +% controller would typically be an acknowledgment +% character or the speed in rpm. +% +% Communication protocol: +% Commands to control the pump of the mill and stirrers: +% PRC : Turn on the pump and run it in clockwise direction +% PRA : Turn on the pump and run it in anticlockwise direction +% PS : Turn off the pump +% PSON : Turn on the primary stirrer +% PSOFF : Turn off the primary stirrer +% SSON : Turn on the secondary stirrer +% SSOFF : Turn off the secondary stirrer +% +% Commands to control the IKA Magic Lab +% The command is generated using generateMillingControllerCommand.m +% +% Output from the microcontroller for the mill and the stirrers: +% 6 : Succesfully turned off the secondary stirrer +% 5 : Succesfully turned on the secondary stirrer +% 4 : Succesfully turned off the primary stirrer +% 3 : Succesfully turned on the primary stirrer +% 2 : Successfully tuned on the pump and ran it in anticlockwise direction +% 1 : Successfully tuned on the pump and ran it in clockwise direction +% 0 : Successfully turned off the pump +% -1 : Unknown command +% +% Output from the IKA Magic Lab +% +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function [controllerOutput] = controlAuxiliaryEquipments(portName, serialCommand) + +%% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS +% Create a serial object with the port specified by the user +serialObj = serial(portName); + +% Check if the command is directed towards the IKA Magic Lab. If the +% command is directed towards the Magic lab, then the serial port +% settings needs to be adapted accordingly. +if strcmpi(serialCommand(2),'@') + set(serialObj,... + 'BaudRate',57600,... + 'DataBits',7,... + 'FlowControl','none',... + 'Parity','even',... + 'StopBits',1,... + 'Timeout',5); + flagIKA = true; +else + set(serialObj,... + 'BaudRate',9600,... + 'Timeout',5); + flagIKA = false; +end + +%% OPEN THE PORT, SEND THE COMMAND AND CLOSE THE CONNCETION +% Open connection with the microcontroller +fopen(serialObj); + +% Send a command to the microcontroller to regulate the valve +fprintf(serialObj, '%s', serialCommand); + +% Read response from the microcontroller and print it out +% If the mill is being commanded do not convert the ASCII text to num +if flagIKA + controllerOutput = fscanf(serialObj,'%s'); +% For all other cases convert the reply to a number (self defined) +else + controllerOutput = str2num(fscanf(serialObj,'%s')); +end + +% Terminate the connection with the microcontroller +fclose (serialObj); + +% Delete the serial object +delete (serialObj); +end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/generateMillingControllerCommand.m b/experimental/auxillaryEquipments/generateMillingControllerCommand.m new file mode 100644 index 0000000..6d15e74 --- /dev/null +++ b/experimental/auxillaryEquipments/generateMillingControllerCommand.m @@ -0,0 +1,132 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% ETH Zurich, Switzerland +% Separation Processes Laboratory +% +% Project: CrystOCAM 2.0, Population Balance Solver +% Year: 2018 +% MATLAB: R2017b, Windows 64bit +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Generate commands that would be sent to the milling controller. This +% works for IKA Magic Lab with the MK/MKO module. +% +% Last modified: +% - 2018-07-26, AK: Added command to obtain the speed from the mill +% - 2018-07-25, AK: Initial creation +% +% Input arguments: +% - millOperation: String that defines the operation of the mill. +% Includes starting, stopping, and changing the +% milling intensity +% - millingIntensity: Milling intensity (1/min) +% +% Output arguments: +% millingControllerCommand: Command that would be eventually sent to the +% controller +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function [millingControllerCommand] = generateMillingControllerCommand(millOperation,millingIntensity) +% Character that corresponds to the start of the text byte +startOfText = char(2); +% Character that corresponds to the end of the text byte +endOfText = char(3); +% Character that corresponds to the end of the transmission byte +endOfTransmission = char(4); +% Character that corresponds to the end of the transmission byte +enquiryChar = char(5); +% Character that corresponds to the address of the master/slave +% configuration: This is used for the controller of IKA Magic LAB +addressChar = '@'; +% Flag to check if command is meant to be for commanding the mill or +% enquiring the mill about a particular setting +flagEnq = false; + +% Generate codes based on the operation that needs to be performed on +% the mill +switch millOperation + % Start the mill + case 'START_MILL' + commandForTheOperation = '20551=0001'; + % Stop the mill + case 'STOP_MILL' + commandForTheOperation = '20551=0000'; + % Set intensity of the rotor shaft + case 'SET_INTENSITY' + millingIntensityBin_IEEE754 = float2bin(single(millingIntensity)); + millingIntensityDec_IEEE754 = bin2dec(millingIntensityBin_IEEE754); + millingIntensityHex_IEEE754 = dec2hex(millingIntensityDec_IEEE754); + commandForTheOperation = ['20284=',millingIntensityHex_IEEE754]; + % Read the intensity of the rotor shaft + case 'READ_INTENSITY' + flagEnq = true; + commandForTheOperation = '20284'; + % Reset error in the controller + case 'RESET_ERROR' + commandForTheOperation = '20074=0001'; + otherwise + error('generateMillingControllerCommand:invalidmillOperation',... + 'Invalid millOperation input.'); +end + +% Generate the parity bit for the command to be sent to the controller. The +% approach applied in this specific controller is called the longitudinal +% parity check. The bytes of the command being sent are arranged and an +% exclusive or operation is performed on all the bytes. In this specific +% controller the parity byte is evaluated using the command for the +% specific operation (commandForTheOperation) and the end of the text byte +% (endOfText). Refer to the manual provided by IKA. +checksumChar = computeChecksumChar([commandForTheOperation,endOfText]); + +% Generate the command that needs to be sent to the controller of the mill +% If a particular parameter needs to be read from the mill +if flagEnq + millingControllerCommand = [endOfTransmission,addressChar,startOfText,... + commandForTheOperation,enquiryChar]; +% If a particular parameter needs to be sent to the mill +else + millingControllerCommand = [endOfTransmission,addressChar,startOfText,... + commandForTheOperation,endOfText,checksumChar]; +end +end + +%% Function to genereate the checksum/parity byte +function checksumChar = computeChecksumChar(commandToBeAnalyzed) +% Convert the command to hex form and stack it as a column vector +hexCode = strsplit(sprintf('%X\t',commandToBeAnalyzed))'; +% Initialize to empty cells/vector +decCode = {}; binTemp = {}; binCode = []; +% Loop over all the characters in the commandToBeAnalyzed. Note: loops till +% size - 1, because of the \t used in the previous line. Generates an extra +% element. +for ii = 1:size(hexCode,1)-1 + % Convert the command from hexadecimal to decimal + decCode{ii,1} = hex2dec(hexCode{ii,1}); + % Convert the command from decimal to binary + binTemp{ii,1} = dec2bin(decCode{ii,1},8); + % Obtain a vector of binary representation for the command. Each + % character is represented as a row with n bits + binCode(ii,:) = binTemp{ii,1} - '0'; +end + +% Obtain the parity byte by performing the XOR operation on the command +% genereated (this has to be done on the binary form of the command) +% Initialize the XOR output to the first character binary representation +xorValue = binCode (1,:); +% Loop over all the other characters and perform the XOR oepration +for jj = 2:size(binCode,1) + xorValue = xor(xorValue,binCode(jj,:)); +end + +% Generate the binary representation from the logical values obtained from +% the XOR function +checksumBinary = sprintf('%d', xorValue); +% Convert the parity byte to decimal +checksumDecimal = bin2dec(checksumBinary); +% Convert the parity byte to hex (just to check. Not used!) +checksumHex = dec2hex(checksumDecimal); +% Convert the decimal representation to ASCII text to be used in the +% command to be sent to the controller +checksumChar = char(checksumDecimal); +end \ No newline at end of file From f622266e591180d211f2a878e3013f7b3c669f29 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 1 Mar 2021 17:57:16 +0000 Subject: [PATCH 011/189] Create function for checking gas, checking manufacturer, and get GitCommitID --- .../auxillaryEquipments/checkGasName.m | 50 +++++++++ .../auxillaryEquipments/checkManufacturer.m | 46 ++++++++ .../controlAuxiliaryEquipments.m | 101 +++++------------- .../auxillaryEquipments/getGitCommit.m | 24 +++++ 4 files changed, 144 insertions(+), 77 deletions(-) create mode 100644 experimental/auxillaryEquipments/checkGasName.m create mode 100644 experimental/auxillaryEquipments/checkManufacturer.m create mode 100644 experimental/auxillaryEquipments/getGitCommit.m diff --git a/experimental/auxillaryEquipments/checkGasName.m b/experimental/auxillaryEquipments/checkGasName.m new file mode 100644 index 0000000..3d5c04b --- /dev/null +++ b/experimental/auxillaryEquipments/checkGasName.m @@ -0,0 +1,50 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% ETH Zurich, Switzerland +% Separation Processes Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% Controls the pump (Ismatec BVP series) which works with the IKA Magic Lab +% (Milling setup) and the IKA Magic Lab. Additionally, two relay switches +% are available which can be used to control two overhead stirrers. +% +% Last modified: +% - 2021-03-01, HA: Initial creation +% +% Input arguments: +% - portProperty : Enter the serial port ID for the connection to be +% made +% - serialCommand : Command that would be issued to the microcontoller. +% +% Output arguments: +% - controllerOutput: Values ranging from -1 to 6, serves as an output from +% the microcontroller to indicate successful execution +% of the command. For the Mill, output from the +% controller would typically be an acknowledgment +% character or the speed in rpm. +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function gasID = checkGasName(gasName) +gasID = 0; +%% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS +% Create a serial object with the port and baudrate specified by the user +% Requires > MATLAB2020a +switch gasName + case 'CO2' + gasID = "ag4"; + case 'He' + gasID = "ag7"; + case 'H2' + gasID = "ag6"; + case 'CH4' + gasID = "ag2"; + case 'N2' + gasID = "ag8"; +end +end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/checkManufacturer.m b/experimental/auxillaryEquipments/checkManufacturer.m new file mode 100644 index 0000000..0573d37 --- /dev/null +++ b/experimental/auxillaryEquipments/checkManufacturer.m @@ -0,0 +1,46 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% ETH Zurich, Switzerland +% Separation Processes Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% Controls the pump (Ismatec BVP series) which works with the IKA Magic Lab +% (Milling setup) and the IKA Magic Lab. Additionally, two relay switches +% are available which can be used to control two overhead stirrers. +% +% Last modified: +% - 2021-03-01, HA: Initial creation +% +% Input arguments: +% - portProperty : Enter the serial port ID for the connection to be +% made +% - serialCommand : Command that would be issued to the microcontoller. +% +% Output arguments: +% - controllerOutput: Values ranging from -1 to 6, serves as an output from +% the microcontroller to indicate successful execution +% of the command. For the Mill, output from the +% controller would typically be an acknowledgment +% character or the speed in rpm. +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function flagAlicat = checkManufacturer(portProperty) + +%% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS +% Create a serial object with the port and baudrate specified by the user +% Requires > MATLAB2020a +flagAlicat = 0; + +switch portProperty.portName + case 'COM5' + flagAlicat = 1; + case 'COM6' + flagAlicat = 0; +end +end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m index e245370..b737cef 100644 --- a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -3,10 +3,10 @@ % ETH Zurich, Switzerland % Separation Processes Laboratory % -% Project: CrystOCAM 2.0 -% Year: 2018 -% MATLAB: R2017b -% Authors: Ashwin Kumar Rajagopalan (AK) +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) % % Purpose: % Controls the pump (Ismatec BVP series) which works with the IKA Magic Lab @@ -14,16 +14,10 @@ % are available which can be used to control two overhead stirrers. % % Last modified: -% - 2018-09-25, AK: Removed the command that would read from the mill. -% - 2018-07-26, AK: Added new subpart to read the milling intensity from -% the mill -% - 2018-07-25, AK: Additional functionality to control the IKA Magic Lab -% - 2018-07-20, AK: Initial creation +% - 2021-03-01, HA: Initial creation % % Input arguments: -% - portName : Enter the serial port ID for the connection to be +% - portProperty : Enter the serial port ID for the connection to be % made % - serialCommand : Command that would be issued to the microcontoller. % @@ -34,78 +28,31 @@ % controller would typically be an acknowledgment % character or the speed in rpm. % -% Communication protocol: -% Commands to control the pump of the mill and stirrers: -% PRC : Turn on the pump and run it in clockwise direction -% PRA : Turn on the pump and run it in anticlockwise direction -% PS : Turn off the pump -% PSON : Turn on the primary stirrer -% PSOFF : Turn off the primary stirrer -% SSON : Turn on the secondary stirrer -% SSOFF : Turn off the secondary stirrer -% -% Commands to control the IKA Magic Lab -% The command is generated using generateMillingControllerCommand.m -% -% Output from the microcontroller for the mill and the stirrers: -% 6 : Succesfully turned off the secondary stirrer -% 5 : Succesfully turned on the secondary stirrer -% 4 : Succesfully turned off the primary stirrer -% 3 : Succesfully turned on the primary stirrer -% 2 : Successfully tuned on the pump and ran it in anticlockwise direction -% 1 : Successfully tuned on the pump and ran it in clockwise direction -% 0 : Successfully turned off the pump -% -1 : Unknown command -% -% Output from the IKA Magic Lab -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function [controllerOutput] = controlAuxiliaryEquipments(portName, serialCommand) +function [controllerOutput] = controlAuxiliaryEquipments(portProperty, serialCommand, flagAlicat,gasID) %% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS -% Create a serial object with the port specified by the user -serialObj = serial(portName); - -% Check if the command is directed towards the IKA Magic Lab. If the -% command is directed towards the Magic lab, then the serial port -% settings needs to be adapted accordingly. -if strcmpi(serialCommand(2),'@') - set(serialObj,... - 'BaudRate',57600,... - 'DataBits',7,... - 'FlowControl','none',... - 'Parity','even',... - 'StopBits',1,... - 'Timeout',5); - flagIKA = true; -else - set(serialObj,... - 'BaudRate',9600,... - 'Timeout',5); - flagIKA = false; +% Create a serial object with the port and baudrate specified by the user +% Requires > MATLAB2020a +serialObj = serialport(portProperty.portName,portProperty.baudRate); +% Configure terminator as specified by the user +configureTerminator(serialObj,portProperty.terminator) + +if flagAlicat + % change name for initialisation + writeline(serialObj,"a"); + % Send a command to check the gas + writeline(serialObj, gasID); + % Add other initialisation procedure (if required) end - -%% OPEN THE PORT, SEND THE COMMAND AND CLOSE THE CONNCETION -% Open connection with the microcontroller -fopen(serialObj); - -% Send a command to the microcontroller to regulate the valve -fprintf(serialObj, '%s', serialCommand); +%% SEND THE COMMAND AND CLOSE THE CONNCETION +% Send command to controller +writeline(serialObj, serialCommand); % Read response from the microcontroller and print it out -% If the mill is being commanded do not convert the ASCII text to num -if flagIKA - controllerOutput = fscanf(serialObj,'%s'); -% For all other cases convert the reply to a number (self defined) -else - controllerOutput = str2num(fscanf(serialObj,'%s')); -end +controllerOutput = readline(serialObj); % Terminate the connection with the microcontroller -fclose (serialObj); - -% Delete the serial object -delete (serialObj); +clear serialObj end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/getGitCommit.m b/experimental/auxillaryEquipments/getGitCommit.m new file mode 100644 index 0000000..109284a --- /dev/null +++ b/experimental/auxillaryEquipments/getGitCommit.m @@ -0,0 +1,24 @@ +% Read out the git commit ID of the git repository to which the current directory +% (or the directory defined in varargin{1}) belongs. Return the ID as a string. + +function commitId = getGitCommit(varargin) +% Identify target directory +if ~isempty(varargin) + oldDir = cd; + targetDir = varargin{1}; + cd(targetDir); +end +% Get short version of git commit ID +[status,cmdout] = system('git rev-parse HEAD'); +if status == 0 + % Command was successful + commitId = cmdout(1:7); +else + commitId = []; +end +% cd back to initial directory +if exist('oldDir','var') + cd(oldDir); +end +end + From f3f69cc4a6b1be8527d9da0e993d86613c2eac11 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Tue, 2 Mar 2021 15:40:57 +0000 Subject: [PATCH 012/189] Update descriptions and add new files - DiagFile.m - generateSerialCommand.m --- experimental/auxillaryEquipments/DiagFile.m | 9 ++++ .../auxillaryEquipments/checkGasName.m | 20 +++------ .../auxillaryEquipments/checkManufacturer.m | 19 +++----- .../controlAuxiliaryEquipments.m | 25 +++++------ .../generateSerialCommand.m | 45 +++++++++++++++++++ .../auxillaryEquipments/getGitCommit.m | 26 +++++++++++ 6 files changed, 104 insertions(+), 40 deletions(-) create mode 100644 experimental/auxillaryEquipments/DiagFile.m create mode 100644 experimental/auxillaryEquipments/generateSerialCommand.m diff --git a/experimental/auxillaryEquipments/DiagFile.m b/experimental/auxillaryEquipments/DiagFile.m new file mode 100644 index 0000000..4e72401 --- /dev/null +++ b/experimental/auxillaryEquipments/DiagFile.m @@ -0,0 +1,9 @@ +clc +clear +MFM = struct('portName','COM5','baudRate',19200,'terminator','CR'); + +gasName = 'He'; +gasID = checkGasName(gasName); +flagAlicat = checkManufacturer(MFM); + +massflowout = controlAuxiliaryEquipments(MFM,generateSerialCommand('pollData',flagAlicat),flagAlicat,gasID); diff --git a/experimental/auxillaryEquipments/checkGasName.m b/experimental/auxillaryEquipments/checkGasName.m index 3d5c04b..060cfec 100644 --- a/experimental/auxillaryEquipments/checkGasName.m +++ b/experimental/auxillaryEquipments/checkGasName.m @@ -1,7 +1,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% ETH Zurich, Switzerland -% Separation Processes Laboratory +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory % % Project: ERASE % Year: 2021 @@ -9,29 +9,21 @@ % Authors: Hassan Azzan (HA) % % Purpose: -% Controls the pump (Ismatec BVP series) which works with the IKA Magic Lab -% (Milling setup) and the IKA Magic Lab. Additionally, two relay switches -% are available which can be used to control two overhead stirrers. +% % % Last modified: % - 2021-03-01, HA: Initial creation % % Input arguments: -% - portProperty : Enter the serial port ID for the connection to be -% made -% - serialCommand : Command that would be issued to the microcontoller. +% - Gas name : Name of the gas used in Alicat equipment (CO2, He, CH4, H2, N2) % % Output arguments: -% - controllerOutput: Values ranging from -1 to 6, serves as an output from -% the microcontroller to indicate successful execution -% of the command. For the Mill, output from the -% controller would typically be an acknowledgment -% character or the speed in rpm. +% - gasID : ID of the gas needed for 'controlAuxiliaryEquipments.m' % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% function to generate the gas ID required for alicat devices function gasID = checkGasName(gasName) -gasID = 0; %% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS % Create a serial object with the port and baudrate specified by the user % Requires > MATLAB2020a diff --git a/experimental/auxillaryEquipments/checkManufacturer.m b/experimental/auxillaryEquipments/checkManufacturer.m index 0573d37..927cf74 100644 --- a/experimental/auxillaryEquipments/checkManufacturer.m +++ b/experimental/auxillaryEquipments/checkManufacturer.m @@ -1,7 +1,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% ETH Zurich, Switzerland -% Separation Processes Laboratory +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory % % Project: ERASE % Year: 2021 @@ -9,24 +9,17 @@ % Authors: Hassan Azzan (HA) % % Purpose: -% Controls the pump (Ismatec BVP series) which works with the IKA Magic Lab -% (Milling setup) and the IKA Magic Lab. Additionally, two relay switches -% are available which can be used to control two overhead stirrers. +% % % Last modified: % - 2021-03-01, HA: Initial creation % % Input arguments: -% - portProperty : Enter the serial port ID for the connection to be -% made -% - serialCommand : Command that would be issued to the microcontoller. +% - portProperty : Structure containing the properties of the comms +% device % % Output arguments: -% - controllerOutput: Values ranging from -1 to 6, serves as an output from -% the microcontroller to indicate successful execution -% of the command. For the Mill, output from the -% controller would typically be an acknowledgment -% character or the speed in rpm. +% - flagAlicat : Determines whether or not the equipment is from Alicat % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m index b737cef..00f5258 100644 --- a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -1,7 +1,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% ETH Zurich, Switzerland -% Separation Processes Laboratory +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory % % Project: ERASE % Year: 2021 @@ -9,28 +9,27 @@ % Authors: Hassan Azzan (HA) % % Purpose: -% Controls the pump (Ismatec BVP series) which works with the IKA Magic Lab -% (Milling setup) and the IKA Magic Lab. Additionally, two relay switches -% are available which can be used to control two overhead stirrers. +% % % Last modified: % - 2021-03-01, HA: Initial creation % % Input arguments: -% - portProperty : Enter the serial port ID for the connection to be -% made +% - portProperty : Structure containing the properties of the comms +% device +% % - serialCommand : Command that would be issued to the microcontoller. % +% - flagAlicat : Determines whether or not the equipment is from Alicat +% +% - gasID : ID input for the gas for alicat equipment +% % Output arguments: -% - controllerOutput: Values ranging from -1 to 6, serves as an output from -% the microcontroller to indicate successful execution -% of the command. For the Mill, output from the -% controller would typically be an acknowledgment -% character or the speed in rpm. +% - controllerOutput: variable output from the controller % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function [controllerOutput] = controlAuxiliaryEquipments(portProperty, serialCommand, flagAlicat,gasID) +function [controllerOutput] = controlAuxiliaryEquipments(portProperty, serialCommand, flagAlicat, gasID) %% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS % Create a serial object with the port and baudrate specified by the user diff --git a/experimental/auxillaryEquipments/generateSerialCommand.m b/experimental/auxillaryEquipments/generateSerialCommand.m new file mode 100644 index 0000000..5e44ac4 --- /dev/null +++ b/experimental/auxillaryEquipments/generateSerialCommand.m @@ -0,0 +1,45 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% +% +% Last modified: +% - 2021-03-02, HA: Initial creation +% +% Input arguments: +% - commandToBeAnalyzed : Command that needs to be analysed to generate +% serial command +% +% - flagAlicat : Determines whether or not the equipment is from Alicat +% +% Output arguments: +% - serialCommand : Command that would be issued to the microcontoller. +% +% Communication protocol: +% Commands to be analysed to control Alicat MFC and MFM: +% setPoint : Set a new set-point for the MFC +% pollData : Poll the current data from the MFC/MFM +% ... +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function [serialCommand] = generateSerialCommand(commandToBeAnalyzed, flagAlicat, varargin) + +%% GENERATE SERIAL COMMAND FOR THE CONTROLLER +if flagAlicat + switch commandToBeAnalyzed + case 'setPoint' + serialCommand = ["as","vargin(1)"]; + case 'pollData' + serialCommand = "a??d"; + end +end +end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/getGitCommit.m b/experimental/auxillaryEquipments/getGitCommit.m index 109284a..bec7dff 100644 --- a/experimental/auxillaryEquipments/getGitCommit.m +++ b/experimental/auxillaryEquipments/getGitCommit.m @@ -1,3 +1,29 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% +% +% Last modified: +% - 2021-03-01, HA: Initial creation +% +% Input arguments: +% - portProperty : Enter the serial port ID for the connection to be +% made +% - serialCommand : Command that would be issued to the microcontoller. +% +% Output arguments: +% - controllerOutput: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % Read out the git commit ID of the git repository to which the current directory % (or the directory defined in varargin{1}) belongs. Return the ID as a string. From 8ed8e3e73f97faaabff85f302b27e8580cd7d85f Mon Sep 17 00:00:00 2001 From: ha3215 Date: Wed, 10 Mar 2021 16:38:45 +0000 Subject: [PATCH 013/189] Cosmetic changes to functions and remove files that are not needed --- experimental/auxillaryEquipments/DiagFile.m | 9 -- .../auxillaryEquipments/checkGasName.m | 6 +- .../auxillaryEquipments/checkManufacturer.m | 7 +- .../controlAuxiliaryEquipments.m | 24 ++-- .../generateMillingControllerCommand.m | 132 ------------------ .../generateSerialCommand.m | 28 ++-- 6 files changed, 27 insertions(+), 179 deletions(-) delete mode 100644 experimental/auxillaryEquipments/DiagFile.m delete mode 100644 experimental/auxillaryEquipments/generateMillingControllerCommand.m diff --git a/experimental/auxillaryEquipments/DiagFile.m b/experimental/auxillaryEquipments/DiagFile.m deleted file mode 100644 index 4e72401..0000000 --- a/experimental/auxillaryEquipments/DiagFile.m +++ /dev/null @@ -1,9 +0,0 @@ -clc -clear -MFM = struct('portName','COM5','baudRate',19200,'terminator','CR'); - -gasName = 'He'; -gasID = checkGasName(gasName); -flagAlicat = checkManufacturer(MFM); - -massflowout = controlAuxiliaryEquipments(MFM,generateSerialCommand('pollData',flagAlicat),flagAlicat,gasID); diff --git a/experimental/auxillaryEquipments/checkGasName.m b/experimental/auxillaryEquipments/checkGasName.m index 060cfec..a826fa7 100644 --- a/experimental/auxillaryEquipments/checkGasName.m +++ b/experimental/auxillaryEquipments/checkGasName.m @@ -9,7 +9,7 @@ % Authors: Hassan Azzan (HA) % % Purpose: -% +% Function to generate the gas ID required for alicat devices % % Last modified: % - 2021-03-01, HA: Initial creation @@ -21,11 +21,7 @@ % - gasID : ID of the gas needed for 'controlAuxiliaryEquipments.m' % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% function to generate the gas ID required for alicat devices function gasID = checkGasName(gasName) -%% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS -% Create a serial object with the port and baudrate specified by the user % Requires > MATLAB2020a switch gasName case 'CO2' diff --git a/experimental/auxillaryEquipments/checkManufacturer.m b/experimental/auxillaryEquipments/checkManufacturer.m index 927cf74..0bee803 100644 --- a/experimental/auxillaryEquipments/checkManufacturer.m +++ b/experimental/auxillaryEquipments/checkManufacturer.m @@ -9,7 +9,7 @@ % Authors: Hassan Azzan (HA) % % Purpose: -% +% Function to check if the device connected to a port is Alicat or not % % Last modified: % - 2021-03-01, HA: Initial creation @@ -24,12 +24,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function flagAlicat = checkManufacturer(portProperty) - -%% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS -% Create a serial object with the port and baudrate specified by the user -% Requires > MATLAB2020a flagAlicat = 0; - switch portProperty.portName case 'COM5' flagAlicat = 1; diff --git a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m index 00f5258..17ce2eb 100644 --- a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -17,34 +17,32 @@ % Input arguments: % - portProperty : Structure containing the properties of the comms % device -% % - serialCommand : Command that would be issued to the microcontoller. -% -% - flagAlicat : Determines whether or not the equipment is from Alicat -% -% - gasID : ID input for the gas for alicat equipment +% - varargin : Variable arguments for the device type and gas % % Output arguments: % - controllerOutput: variable output from the controller % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -function [controllerOutput] = controlAuxiliaryEquipments(portProperty, serialCommand, flagAlicat, gasID) - -%% CREATE CONNECTION WITH uCONTROLLER & PARSE ARGUMENTS +function [controllerOutput] = controlAuxiliaryEquipments(portProperty, serialCommand, varargin) % Create a serial object with the port and baudrate specified by the user % Requires > MATLAB2020a serialObj = serialport(portProperty.portName,portProperty.baudRate); % Configure terminator as specified by the user +% Alicat: configureTerminator(serialObj,portProperty.terminator) -if flagAlicat - % change name for initialisation +% If using Alicat (flow meter or controller) +% varargin(1): Device type - Alicat: True +% varargin(2): Gas ID - If Alicat, then ID for the process gas +if varargin(1) + % Perform a pseudo handshake for the alicat devices. Without this line + % the communcation is usually not established (AK:10.03.21) writeline(serialObj,"a"); % Send a command to check the gas - writeline(serialObj, gasID); - % Add other initialisation procedure (if required) + writeline(serialObj, varargin(2)); end + %% SEND THE COMMAND AND CLOSE THE CONNCETION % Send command to controller writeline(serialObj, serialCommand); diff --git a/experimental/auxillaryEquipments/generateMillingControllerCommand.m b/experimental/auxillaryEquipments/generateMillingControllerCommand.m deleted file mode 100644 index 6d15e74..0000000 --- a/experimental/auxillaryEquipments/generateMillingControllerCommand.m +++ /dev/null @@ -1,132 +0,0 @@ -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% ETH Zurich, Switzerland -% Separation Processes Laboratory -% -% Project: CrystOCAM 2.0, Population Balance Solver -% Year: 2018 -% MATLAB: R2017b, Windows 64bit -% Authors: Ashwin Kumar Rajagopalan (AK) -% -% Purpose: -% Generate commands that would be sent to the milling controller. This -% works for IKA Magic Lab with the MK/MKO module. -% -% Last modified: -% - 2018-07-26, AK: Added command to obtain the speed from the mill -% - 2018-07-25, AK: Initial creation -% -% Input arguments: -% - millOperation: String that defines the operation of the mill. -% Includes starting, stopping, and changing the -% milling intensity -% - millingIntensity: Milling intensity (1/min) -% -% Output arguments: -% millingControllerCommand: Command that would be eventually sent to the -% controller -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -function [millingControllerCommand] = generateMillingControllerCommand(millOperation,millingIntensity) -% Character that corresponds to the start of the text byte -startOfText = char(2); -% Character that corresponds to the end of the text byte -endOfText = char(3); -% Character that corresponds to the end of the transmission byte -endOfTransmission = char(4); -% Character that corresponds to the end of the transmission byte -enquiryChar = char(5); -% Character that corresponds to the address of the master/slave -% configuration: This is used for the controller of IKA Magic LAB -addressChar = '@'; -% Flag to check if command is meant to be for commanding the mill or -% enquiring the mill about a particular setting -flagEnq = false; - -% Generate codes based on the operation that needs to be performed on -% the mill -switch millOperation - % Start the mill - case 'START_MILL' - commandForTheOperation = '20551=0001'; - % Stop the mill - case 'STOP_MILL' - commandForTheOperation = '20551=0000'; - % Set intensity of the rotor shaft - case 'SET_INTENSITY' - millingIntensityBin_IEEE754 = float2bin(single(millingIntensity)); - millingIntensityDec_IEEE754 = bin2dec(millingIntensityBin_IEEE754); - millingIntensityHex_IEEE754 = dec2hex(millingIntensityDec_IEEE754); - commandForTheOperation = ['20284=',millingIntensityHex_IEEE754]; - % Read the intensity of the rotor shaft - case 'READ_INTENSITY' - flagEnq = true; - commandForTheOperation = '20284'; - % Reset error in the controller - case 'RESET_ERROR' - commandForTheOperation = '20074=0001'; - otherwise - error('generateMillingControllerCommand:invalidmillOperation',... - 'Invalid millOperation input.'); -end - -% Generate the parity bit for the command to be sent to the controller. The -% approach applied in this specific controller is called the longitudinal -% parity check. The bytes of the command being sent are arranged and an -% exclusive or operation is performed on all the bytes. In this specific -% controller the parity byte is evaluated using the command for the -% specific operation (commandForTheOperation) and the end of the text byte -% (endOfText). Refer to the manual provided by IKA. -checksumChar = computeChecksumChar([commandForTheOperation,endOfText]); - -% Generate the command that needs to be sent to the controller of the mill -% If a particular parameter needs to be read from the mill -if flagEnq - millingControllerCommand = [endOfTransmission,addressChar,startOfText,... - commandForTheOperation,enquiryChar]; -% If a particular parameter needs to be sent to the mill -else - millingControllerCommand = [endOfTransmission,addressChar,startOfText,... - commandForTheOperation,endOfText,checksumChar]; -end -end - -%% Function to genereate the checksum/parity byte -function checksumChar = computeChecksumChar(commandToBeAnalyzed) -% Convert the command to hex form and stack it as a column vector -hexCode = strsplit(sprintf('%X\t',commandToBeAnalyzed))'; -% Initialize to empty cells/vector -decCode = {}; binTemp = {}; binCode = []; -% Loop over all the characters in the commandToBeAnalyzed. Note: loops till -% size - 1, because of the \t used in the previous line. Generates an extra -% element. -for ii = 1:size(hexCode,1)-1 - % Convert the command from hexadecimal to decimal - decCode{ii,1} = hex2dec(hexCode{ii,1}); - % Convert the command from decimal to binary - binTemp{ii,1} = dec2bin(decCode{ii,1},8); - % Obtain a vector of binary representation for the command. Each - % character is represented as a row with n bits - binCode(ii,:) = binTemp{ii,1} - '0'; -end - -% Obtain the parity byte by performing the XOR operation on the command -% genereated (this has to be done on the binary form of the command) -% Initialize the XOR output to the first character binary representation -xorValue = binCode (1,:); -% Loop over all the other characters and perform the XOR oepration -for jj = 2:size(binCode,1) - xorValue = xor(xorValue,binCode(jj,:)); -end - -% Generate the binary representation from the logical values obtained from -% the XOR function -checksumBinary = sprintf('%d', xorValue); -% Convert the parity byte to decimal -checksumDecimal = bin2dec(checksumBinary); -% Convert the parity byte to hex (just to check. Not used!) -checksumHex = dec2hex(checksumDecimal); -% Convert the decimal representation to ASCII text to be used in the -% command to be sent to the controller -checksumChar = char(checksumDecimal); -end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/generateSerialCommand.m b/experimental/auxillaryEquipments/generateSerialCommand.m index 5e44ac4..dc5205a 100644 --- a/experimental/auxillaryEquipments/generateSerialCommand.m +++ b/experimental/auxillaryEquipments/generateSerialCommand.m @@ -8,38 +8,38 @@ % MATLAB: R2020a % Authors: Hassan Azzan (HA) % -% Purpose: +% Purpose: % % % Last modified: % - 2021-03-02, HA: Initial creation % % Input arguments: -% - commandToBeAnalyzed : Command that needs to be analysed to generate -% serial command -% -% - flagAlicat : Determines whether or not the equipment is from Alicat +% - commandToBeAnalyzed: Command that needs to be analysed to generate +% serial command +% - varargin: Arguments to determine the device and the set pt % % Output arguments: -% - serialCommand : Command that would be issued to the microcontoller. +% - serialCommand : Command that would be issued to the device % % Communication protocol: -% Commands to be analysed to control Alicat MFC and MFM: +% Commands to be analysed to control Alicat MFC and MFM: % setPoint : Set a new set-point for the MFC % pollData : Poll the current data from the MFC/MFM % ... % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -function [serialCommand] = generateSerialCommand(commandToBeAnalyzed, flagAlicat, varargin) - -%% GENERATE SERIAL COMMAND FOR THE CONTROLLER -if flagAlicat +function [serialCommand] = generateSerialCommand(commandToBeAnalyzed, varargin) +% Generate serial command for the device +% If Alicat is used: variable argument 1 is true! +if varargin(1) switch commandToBeAnalyzed case 'setPoint' - serialCommand = ["as","vargin(1)"]; + % Set the set point value for the controller + setPointValue = round(varargin(2),2); + serialCommand = ['as',num2str(setPointValue)]; case 'pollData' - serialCommand = "a??d"; + serialCommand = 'a??d'; end end end \ No newline at end of file From c95d2308358371cbac1b3b6f486c54c185b5a164 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Wed, 10 Mar 2021 16:54:15 +0000 Subject: [PATCH 014/189] Bug fix for variable arguments --- experimental/auxillaryEquipments/checkManufacturer.m | 4 ++-- experimental/auxillaryEquipments/controlAuxiliaryEquipments.m | 4 ++-- experimental/auxillaryEquipments/generateSerialCommand.m | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/experimental/auxillaryEquipments/checkManufacturer.m b/experimental/auxillaryEquipments/checkManufacturer.m index 0bee803..97a49bc 100644 --- a/experimental/auxillaryEquipments/checkManufacturer.m +++ b/experimental/auxillaryEquipments/checkManufacturer.m @@ -26,9 +26,9 @@ function flagAlicat = checkManufacturer(portProperty) flagAlicat = 0; switch portProperty.portName - case 'COM5' - flagAlicat = 1; case 'COM6' + flagAlicat = 1; + case 'COM5' flagAlicat = 0; end end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m index 17ce2eb..c9ca04f 100644 --- a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -35,12 +35,12 @@ % If using Alicat (flow meter or controller) % varargin(1): Device type - Alicat: True % varargin(2): Gas ID - If Alicat, then ID for the process gas -if varargin(1) +if varargin{1} % Perform a pseudo handshake for the alicat devices. Without this line % the communcation is usually not established (AK:10.03.21) writeline(serialObj,"a"); % Send a command to check the gas - writeline(serialObj, varargin(2)); + writeline(serialObj, varargin{2}); end %% SEND THE COMMAND AND CLOSE THE CONNCETION diff --git a/experimental/auxillaryEquipments/generateSerialCommand.m b/experimental/auxillaryEquipments/generateSerialCommand.m index dc5205a..2295734 100644 --- a/experimental/auxillaryEquipments/generateSerialCommand.m +++ b/experimental/auxillaryEquipments/generateSerialCommand.m @@ -32,11 +32,11 @@ function [serialCommand] = generateSerialCommand(commandToBeAnalyzed, varargin) % Generate serial command for the device % If Alicat is used: variable argument 1 is true! -if varargin(1) +if varargin{1} switch commandToBeAnalyzed case 'setPoint' % Set the set point value for the controller - setPointValue = round(varargin(2),2); + setPointValue = round(varargin{2},2); serialCommand = ['as',num2str(setPointValue)]; case 'pollData' serialCommand = 'a??d'; From f082c8f8fdd9cfedb9da25ee2f176f92849c5169 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Wed, 10 Mar 2021 18:42:54 +0000 Subject: [PATCH 015/189] First version of software to control the ZLC setup --- .../controlAuxiliaryEquipments.m | 9 +- experimental/auxillaryEquipments/runZLC.m | 98 +++++++++++++++++++ 2 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 experimental/auxillaryEquipments/runZLC.m diff --git a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m index c9ca04f..e6154d2 100644 --- a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -10,8 +10,8 @@ % % Purpose: % -% % Last modified: +% - 2021-03-01, HA: Remove gas selection (hard coded) % - 2021-03-01, HA: Initial creation % % Input arguments: @@ -35,12 +35,11 @@ % If using Alicat (flow meter or controller) % varargin(1): Device type - Alicat: True % varargin(2): Gas ID - If Alicat, then ID for the process gas -if varargin{1} +if nargin>2 && varargin{1} % Perform a pseudo handshake for the alicat devices. Without this line % the communcation is usually not established (AK:10.03.21) - writeline(serialObj,"a"); - % Send a command to check the gas - writeline(serialObj, varargin{2}); + writeline(serialObj,'a'); + pause(0.01); % Pause to ensure proper read end %% SEND THE COMMAND AND CLOSE THE CONNCETION diff --git a/experimental/auxillaryEquipments/runZLC.m b/experimental/auxillaryEquipments/runZLC.m new file mode 100644 index 0000000..2bd65e6 --- /dev/null +++ b/experimental/auxillaryEquipments/runZLC.m @@ -0,0 +1,98 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% Runs the ZLC setup. This function will provide set points to the +% controllers, will read flow data. +% +% Last modified: +% - 2021-03-10, HA: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function runZLC + % Maximum time of the experiment + expInfo.maxTime = 10; + % Sampling time for the device + expInfo.samplingTime = 1; + % Define gas for MFM + expInfo.gasName_MFM = 'He'; + % Define gas for MFC1 + expInfo.gasName_MFC1 = 'He'; + % Define gas for MFC2 + expInfo.gasName_MFC2 = 'CO2'; + + % Comm setup for the flow meter and controller + serialObj.MFM = struct('portName','COM6','baudRate',19200,'terminator','CR'); + serialObj.MFC1 = struct('portName','COM7','baudRate',19200,'terminator','CR'); + serialObj.MFC2 = struct('portName','COM7','baudRate',19200,'terminator','CR'); + + % Generate serial command for polling data + serialObj.cmdPollData = generateSerialCommand('pollData',1); + + %% Initialize timer + timerDevice = timer; + timerDevice.ExecutionMode = 'fixedRate'; + timerDevice.BusyMode = 'drop'; + timerDevice.Period = expInfo.samplingTime; % [s] + timerDevice.StartDelay = 0; % [s] + timerDevice.TasksToExecute = floor((expInfo.maxTime)/expInfo.samplingTime); + + % Specify timer callbacks + timerDevice.StartFcn = {@initializeTimerDevice,expInfo,serialObj}; + timerDevice.TimerFcn = {@executeTimerDevice,serialObj}; + timerDevice.StopFcn = {@stopTimerDevice}; + + % Start the experiment + % Get the date/time + currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); + disp([currentDateTime,'-> Starting the experiment!!']) + % Start the timer + start(timerDevice) +end + +%% initializeTimerDevice: Initialisation of timer device +function initializeTimerDevice(~, thisEvent, expInfo, serialObj) + % Get the event date/time + currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); + disp([currentDateTime,'-> Initializing the experiment!!']) + % Parse out gas name from expInfo + gasName_MFM = expInfo.gasName_MFM; + gasName_MFC1 = expInfo.gasName_MFC1; + gasName_MFC2 = expInfo.gasName_MFC2; + % Generate Gas ID for Alicat devices + gasID_MFM = checkGasName(gasName_MFM); + gasID_MFC1 = checkGasName(gasName_MFC1); + gasID_MFC2 = checkGasName(gasName_MFC2); + % Initialize the gas for the meter and the controller + [~] = controlAuxiliaryEquipments(serialObj.MFM, gasID_MFM,1); % Set gas for MFM + [~] = controlAuxiliaryEquipments(serialObj.MFC1, gasID_MFC1,1); % Set gas for MFC1 + [~] = controlAuxiliaryEquipments(serialObj.MFC2, gasID_MFC2,1); % Set gas for MFC2 + % Get the event date/time + currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); + disp([currentDateTime,'-> Initialization complete!!']) +end +%% executeTimerDevice: Execute function for the timer at each instant +function executeTimerDevice(timerObj, thisEvent, serialObj) + % Get the event date/time + currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); + disp([currentDateTime,'-> Performing task #', num2str(timerObj.tasksExecuted)]) + % Get the current state of the flow meter + deviceOutput = controlAuxiliaryEquipments(serialObj.MFM, serialObj.cmdPollData,1); +end +%% stopTimerDevice: Stop timer device +function stopTimerDevice(~, thisEvent) + % Get the event date/time + currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); + disp([currentDateTime,'-> And its over babyyyyyy!!']) +end \ No newline at end of file From 446ca48c9b5a625afb6382ef442ecfb7780e927d Mon Sep 17 00:00:00 2001 From: ha3215 Date: Thu, 11 Mar 2021 18:08:19 +0000 Subject: [PATCH 016/189] Add data logger, set points, and refine code --- .../controlAuxiliaryEquipments.m | 25 ++-- experimental/auxillaryEquipments/runZLC.m | 139 +++++++++++++++++- 2 files changed, 147 insertions(+), 17 deletions(-) diff --git a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m index e6154d2..1a62192 100644 --- a/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -11,6 +11,7 @@ % Purpose: % % Last modified: +% - 2021-03-11, HA: Add UMFM % - 2021-03-01, HA: Remove gas selection (hard coded) % - 2021-03-01, HA: Initial creation % @@ -28,27 +29,33 @@ % Create a serial object with the port and baudrate specified by the user % Requires > MATLAB2020a serialObj = serialport(portProperty.portName,portProperty.baudRate); -% Configure terminator as specified by the user -% Alicat: -configureTerminator(serialObj,portProperty.terminator) % If using Alicat (flow meter or controller) % varargin(1): Device type - Alicat: True -% varargin(2): Gas ID - If Alicat, then ID for the process gas if nargin>2 && varargin{1} + % Configure terminator as specified by the user + % Alicat: + configureTerminator(serialObj,portProperty.terminator) % Perform a pseudo handshake for the alicat devices. Without this line % the communcation is usually not established (AK:10.03.21) writeline(serialObj,'a'); - pause(0.01); % Pause to ensure proper read + pause(1); % Pause to ensure proper read end %% SEND THE COMMAND AND CLOSE THE CONNCETION % Send command to controller -writeline(serialObj, serialCommand); - +% Send a command if not the universal gas flow meter +if ~strcmp(serialCommand,"UMFM") + writeline(serialObj, serialCommand); +end % Read response from the microcontroller and print it out -controllerOutput = readline(serialObj); - +% Read the output from the UMFM (this is always streaming) +if strcmp(serialCommand,"UMFM") + controllerOutput = read(serialObj,10,"string"); +% For everything else +else + controllerOutput = readline(serialObj); +end % Terminate the connection with the microcontroller clear serialObj end \ No newline at end of file diff --git a/experimental/auxillaryEquipments/runZLC.m b/experimental/auxillaryEquipments/runZLC.m index 2bd65e6..555ffd2 100644 --- a/experimental/auxillaryEquipments/runZLC.m +++ b/experimental/auxillaryEquipments/runZLC.m @@ -13,6 +13,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-03-11, HA: Add data logger, set points, and refine code % - 2021-03-10, HA: Initial creation % % Input arguments: @@ -21,6 +22,8 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function runZLC + % Experiment name + expInfo.expName = 'ZLCexpTest'; % Maximum time of the experiment expInfo.maxTime = 10; % Sampling time for the device @@ -31,11 +34,16 @@ expInfo.gasName_MFC1 = 'He'; % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; - + % Define set point for MFC1 + expInfo.MFC1_SP = 0.3; + % Define gas for MFC2 + expInfo.MFC2_SP = 5.0; + % Comm setup for the flow meter and controller serialObj.MFM = struct('portName','COM6','baudRate',19200,'terminator','CR'); - serialObj.MFC1 = struct('portName','COM7','baudRate',19200,'terminator','CR'); + serialObj.MFC1 = struct('portName','COM6','baudRate',19200,'terminator','CR'); serialObj.MFC2 = struct('portName','COM7','baudRate',19200,'terminator','CR'); + serialObj.UMFM = struct('portName','COM8','baudRate',9600); % Generate serial command for polling data serialObj.cmdPollData = generateSerialCommand('pollData',1); @@ -50,7 +58,7 @@ % Specify timer callbacks timerDevice.StartFcn = {@initializeTimerDevice,expInfo,serialObj}; - timerDevice.TimerFcn = {@executeTimerDevice,serialObj}; + timerDevice.TimerFcn = {@executeTimerDevice, expInfo, serialObj}; timerDevice.StopFcn = {@stopTimerDevice}; % Start the experiment @@ -59,6 +67,17 @@ disp([currentDateTime,'-> Starting the experiment!!']) % Start the timer start(timerDevice) + % Block the command line + wait(timerDevice) + + % Load the experimental data and add a few more things + % Load the output .mat file + load(['..',filesep,'experimentalData',filesep,expInfo.expName]) + % Get the git commit ID + gitCommitID = getGitCommit; + % Load the output .mat file + save(['..',filesep,'experimentalData',filesep,expInfo.expName],... + 'gitCommitID','outputStruct') end %% initializeTimerDevice: Initialisation of timer device @@ -75,24 +94,128 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) gasID_MFC1 = checkGasName(gasName_MFC1); gasID_MFC2 = checkGasName(gasName_MFC2); % Initialize the gas for the meter and the controller - [~] = controlAuxiliaryEquipments(serialObj.MFM, gasID_MFM,1); % Set gas for MFM - [~] = controlAuxiliaryEquipments(serialObj.MFC1, gasID_MFC1,1); % Set gas for MFC1 - [~] = controlAuxiliaryEquipments(serialObj.MFC2, gasID_MFC2,1); % Set gas for MFC2 + % MFM + if ~isempty(serialObj.MFM) + [~] = controlAuxiliaryEquipments(serialObj.MFM, gasID_MFM,1); % Set gas for MFM + end + % MFC1 + if ~isempty(serialObj.MFC1) + [~] = controlAuxiliaryEquipments(serialObj.MFC1, gasID_MFC1,1); % Set gas for MFC1 + % Generate serial command for volumteric flow rate set point + cmdSetPt = generateSerialCommand('setPoint',1,expInfo.MFC1_SP); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC1, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); + outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string + if str2double(outputMFC1Temp(6)) ~= expInfo.MFC1_SP + error("You should not be here!!!") + end + end + % MFC2 + if ~isempty(serialObj.MFC2) + [~] = controlAuxiliaryEquipments(serialObj.MFC2, gasID_MFC2,1); % Set gas for MFC2 + % Generate serial command for volumteric flow rate set point + cmdSetPt = generateSerialCommand('setPoint',1,expInfo.MFC2_SP); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); + outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string + if str2double(outputMFC2Temp(6)) ~= expInfo.MFC2_SP + error("You should not be here!!!") + end + end % Get the event date/time currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> Initialization complete!!']) end %% executeTimerDevice: Execute function for the timer at each instant -function executeTimerDevice(timerObj, thisEvent, serialObj) +function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) + % Initialize outputs + MFM = []; MFC1 = []; MFC2 = []; UMFM = []; % Get the event date/time currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> Performing task #', num2str(timerObj.tasksExecuted)]) % Get the current state of the flow meter - deviceOutput = controlAuxiliaryEquipments(serialObj.MFM, serialObj.cmdPollData,1); + if ~isempty(serialObj.MFM) + outputMFM = controlAuxiliaryEquipments(serialObj.MFM, serialObj.cmdPollData,1); + outputMFMTemp = strsplit(outputMFM,' '); % Split the output string + MFM.pressure = str2double(outputMFMTemp(2)); % [bar] + MFM.temperature = str2double(outputMFMTemp(3)); % [C] + MFM.volFlow = str2double(outputMFMTemp(4)); % device units [ml/min] + MFM.massFlow = str2double(outputMFMTemp(5)); % standard units [sccm] + MFM.gas = outputMFMTemp(6); % gas in the meter + end + % Get the current state of the flow controller 1 + if ~isempty(serialObj.MFC1) + outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); + outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string + MFC1.pressure = str2double(outputMFC1Temp(2)); % [bar] + MFC1.temperature = str2double(outputMFC1Temp(3)); % [C] + MFC1.volFlow = str2double(outputMFC1Temp(4)); % device units [ml/min] + MFC1.massFlow = str2double(outputMFC1Temp(5)); % standard units [sccm] + MFC1.setpoint = str2double(outputMFC1Temp(6)); % device units [ml/min] + MFC1.gas = outputMFC1Temp(7); % gas in the controller + end + % Get the current state of the flow controller 2 + if ~isempty(serialObj.MFC2) + outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); + outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string + MFC2.pressure = str2double(outputMFC2Temp(2)); % [bar] + MFC2.temperature = str2double(outputMFC2Temp(3)); % [C] + MFC2.volFlow = str2double(outputMFC2Temp(4)); % device units [ml/min] + MFC2.massFlow = str2double(outputMFC2Temp(5)); % standard units [sccm] + MFC2.setpoint = outputMFC2Temp(6); % device units [ml/min] + MFC2.gas = outputMFC2Temp(7); % gas in the controller + end + % Get the current state of the universal flow controller + if ~isempty(serialObj.UMFM) + outputUMFM = controlAuxiliaryEquipments(serialObj.UMFM, "UMFM"); + UMFM.volFlow = str2double(outputUMFM); + end + % Call the data logger function + dataLogger(timerObj,expInfo,currentDateTime,MFM,... + MFC1,MFC2,UMFM); end %% stopTimerDevice: Stop timer device function stopTimerDevice(~, thisEvent) % Get the event date/time currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> And its over babyyyyyy!!']) +end +%% dataLogger: Function to log data into a .mat file +function dataLogger(timerObj, expInfo, currentDateTime, ... + MFM, MFC1, MFC2, UMFM) +% If running for the first time initialize the structure +if timerObj.tasksExecuted == 1 + outputStruct(1).samplingDateTime = currentDateTime; + outputStruct(1).timeElapsed = 0; + outputStruct(1).MFM = MFM; + outputStruct(1).MFC1= MFC1; + outputStruct(1).MFC2 = MFC2; + outputStruct(1).UMFM = UMFM; + % Create an experimental data folder if it doesnt exost + if ~exist(['..',filesep,'experimentalData'],'dir') + mkdir(['..',filesep,'experimentalData']); + % Save the output into a .mat file + else + save(['..',filesep,'experimentalData',filesep,expInfo.expName],'outputStruct') + end +else + % Load the output .mat file + load(['..',filesep,'experimentalData',filesep,expInfo.expName]) + % Get the size of the output structure + nCount = size(outputStruct,2); + % Save the data into the structure + outputStruct(nCount+1).samplingDateTime = currentDateTime; + outputStruct(nCount+1).timeElapsed = seconds(datetime(currentDateTime,... + 'InputFormat','yyyyMMdd_HHmmss')... + -datetime(outputStruct(1).samplingDateTime,... + 'InputFormat','yyyyMMdd_HHmmss')); % Time elapsed [s] + outputStruct(nCount+1).MFM = MFM; + outputStruct(nCount+1).MFC1= MFC1; + outputStruct(nCount+1).MFC2 = MFC2; + outputStruct(nCount+1).UMFM = UMFM; + % Save the output into a .mat file + save(['..',filesep,'experimentalData',filesep,expInfo.expName],'outputStruct') +end end \ No newline at end of file From f83d20e97414f1c8d938fd0ae259d5cc1ced9d3c Mon Sep 17 00:00:00 2001 From: QCPML Date: Thu, 11 Mar 2021 18:55:11 +0000 Subject: [PATCH 017/189] Change file structure --- .../getGitCommit.m | 0 .../auxillaryFunctions/listComPorts.vbs | 26 +++++++++++++++++++ .../{auxillaryEquipments => }/runZLC.m | 0 3 files changed, 26 insertions(+) rename experimental/{auxillaryEquipments => auxillaryFunctions}/getGitCommit.m (100%) create mode 100644 experimental/auxillaryFunctions/listComPorts.vbs rename experimental/{auxillaryEquipments => }/runZLC.m (100%) diff --git a/experimental/auxillaryEquipments/getGitCommit.m b/experimental/auxillaryFunctions/getGitCommit.m similarity index 100% rename from experimental/auxillaryEquipments/getGitCommit.m rename to experimental/auxillaryFunctions/getGitCommit.m diff --git a/experimental/auxillaryFunctions/listComPorts.vbs b/experimental/auxillaryFunctions/listComPorts.vbs new file mode 100644 index 0000000..ff03270 --- /dev/null +++ b/experimental/auxillaryFunctions/listComPorts.vbs @@ -0,0 +1,26 @@ +Set portList = GetComPorts() + +portnames = portList.Keys +for each pname in portnames + Set portinfo = portList.item(pname) + wscript.echo pname & " - " & _ + portinfo.Manufacturer & " - " & _ + portinfo.PNPDeviceID & " - " & _ + portinfo.Name +Next + +Function GetComPorts() + set portList = CreateObject("Scripting.Dictionary") + strComputer = "." + set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") + set colItems = objWMIService.ExecQuery ("Select * from Win32_PnPEntity") + for each objItem in colItems + If Not IsNull(objItem.Name) Then + set objRgx = CreateObject("vbScript.RegExp") + objRgx.Pattern = "COM[0-9]+" + Set objRegMatches = objRgx.Execute(objItem.Name) + if objRegMatches.Count = 1 Then portList.Add objRegMatches.Item(0).Value, objItem + End if + Next + set GetComPorts = portList +End Function \ No newline at end of file diff --git a/experimental/auxillaryEquipments/runZLC.m b/experimental/runZLC.m similarity index 100% rename from experimental/auxillaryEquipments/runZLC.m rename to experimental/runZLC.m From 1961abc5eb2ff00292fe2c70f5ea0782ddb842c6 Mon Sep 17 00:00:00 2001 From: QCPML Date: Fri, 12 Mar 2021 14:45:36 +0000 Subject: [PATCH 018/189] Add auto detection of ports and chage structure. New caliration routine. --- .gitignore | 3 + .../auxillaryFunctions/matchUSBport.m | 89 ++++++++++++ experimental/calibrateMeters.m | 44 ++++++ experimental/runZLC.m | 136 +++++++++++------- 4 files changed, 220 insertions(+), 52 deletions(-) create mode 100644 experimental/auxillaryFunctions/matchUSBport.m create mode 100644 experimental/calibrateMeters.m diff --git a/.gitignore b/.gitignore index d1d4543..52131f4 100755 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ __pycache__/ # C extensions *.so +# MATLAB extensions +*.mat + # Distribution / packaging .Python build/ diff --git a/experimental/auxillaryFunctions/matchUSBport.m b/experimental/auxillaryFunctions/matchUSBport.m new file mode 100644 index 0000000..7fd27ae --- /dev/null +++ b/experimental/auxillaryFunctions/matchUSBport.m @@ -0,0 +1,89 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Used to find the COM ports for the different devices (inspired from AK's +% work at ETHZ) +% +% Last modified: +% - 2021-03-12, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function portAddress = matchUSBport(varargin) +% Only one input argument is accepted (array of cells) +if nargin > 1 + error('Too many input arguments'); +end + +if exist('listComPorts.vbs','file')~=2 + warning('listComPorts.vbs not found. The script will be generated.'); + fileID = fopen('listComPorts.vbs','w'); + fprintf(fileID,['Set portList = GetComPorts()\n\nportnames = portList.Keys\nfor each pname in portnames\n\tSet portinfo = portList.item(pname)\n\t',... + 'wscript.echo pname & "~" & _\n\t\tportinfo.PNPDeviceID\nNext\n\n'... + 'Function GetComPorts()\n\tset portList = CreateObject("Scripting.Dictionary")\n\n\tstrComputer = "."\n\tset objWMIService = GetObject',... + '("winmgmts:\\\\" & strComputer & "\\root\\cimv2")\n\tset colItems = objWMIService.ExecQuery ("Select * from Win32_PnPEntity")\n\t'... + 'for each objItem in colItems\n\t\tIf Not IsNull(objItem.Name) Then\n\t\t\tset objRgx = CreateObject("vbScript.RegExp")\n\t\t\t',... + 'objRgx.Pattern = "COM[0-9]+"\n\t\t\tSet objRegMatches = objRgx.Execute(objItem.Name)\n\t\t\tif objRegMatches.Count = 1 Then portList.Add ',... + 'objRegMatches.Item(0).Value, objItem\n\t\tEnd if\n\tNext\n\tset GetComPorts = portList\nEnd Function']); + fclose(fileID); +end +bashPath = which('listComPorts.vbs'); +[~, bashOut] = system(['cscript.exe //nologo ',bashPath]); + +stringBashOut = string(bashOut(1:end-1)); +splitBashOut = split(split(stringBashOut,char(10)),'~'); %#ok + +% If no argument was provided, just output all the ports available +if nargin == 0 + portAddress = splitBashOut; + return; +else + if ~iscell(varargin{1}) + if ischar(varargin{1}) && isrow(varargin{1}) + portIdentifier = varargin(1); + else + error('The list of ports must be given as a cell array of chars'); + end + else + % Get the input device names + portIdentifier = varargin{1}; + end +end + +% If anyway no device is available throw an error and exit +if strcmp(splitBashOut,"") + error('matchUSBport:noUSBDeviceFound',... + 'No USB device was found. Aborting.'); +end + +% In case there is only one device (two entries), a column vector is output +% when performing the split, but we actually want a row vector +if isequal(size(splitBashOut),[2 1]) + splitBashOut = splitBashOut'; +end + +% Search for all the input device names (port identifiers) +for kk=1:length(portIdentifier) + cellMatch = regexp(splitBashOut,portIdentifier{kk}); + cellMatchLogic = cellfun(@(x)~isempty(x),cellMatch); + if sum(cellMatchLogic(:))>1 + warning('matchUSBport:nonUniqueIdentification',... + ['The identifier ',portIdentifier{kk},' has multiple matches among the USB ports available. No port could be assigned.']); + portAddress{kk} = ''; %#ok<*AGROW> + continue; + end + portAddress{kk} = char(splitBashOut(fliplr(cellMatchLogic))); +end +end \ No newline at end of file diff --git a/experimental/calibrateMeters.m b/experimental/calibrateMeters.m new file mode 100644 index 0000000..da6d4de --- /dev/null +++ b/experimental/calibrateMeters.m @@ -0,0 +1,44 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Calibrates the flow meter and controller for different set point values +% +% Last modified: +% - 2021-03-12, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function calibrateMeters + % Experiment name + expInfo.expName = 'ZLCCalibrateMeters'; + % Maximum time of the experiment + expInfo.maxTime = 10; + % Sampling time for the device + expInfo.samplingTime = 1; + % Define gas for MFM + expInfo.gasName_MFM = 'He'; + % Define gas for MFC1 + expInfo.gasName_MFC1 = 'He'; + % Define gas for MFC2 + expInfo.gasName_MFC2 = 'CO2'; + % Define set point for MFC1 + MFC1_SP = [0.0]; + + % Loop through all setpoints to calibrate the meters + for ii=1:length(MFC1_SP) + expInfo.MFC1_SP = MFC1_SP(ii); + % Run the setup for different calibrations + runZLC(expInfo) + end +end diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 555ffd2..0f23220 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -7,12 +7,14 @@ % Year: 2021 % MATLAB: R2020a % Authors: Hassan Azzan (HA) +% Ashwin Kumar Rajagopalan (AK) % % Purpose: % Runs the ZLC setup. This function will provide set points to the % controllers, will read flow data. % % Last modified: +% - 2021-03-12, AK: Add auto detection of ports and change structure % - 2021-03-11, HA: Add data logger, set points, and refine code % - 2021-03-10, HA: Initial creation % @@ -21,29 +23,57 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function runZLC - % Experiment name - expInfo.expName = 'ZLCexpTest'; - % Maximum time of the experiment - expInfo.maxTime = 10; - % Sampling time for the device - expInfo.samplingTime = 1; - % Define gas for MFM - expInfo.gasName_MFM = 'He'; - % Define gas for MFC1 - expInfo.gasName_MFC1 = 'He'; - % Define gas for MFC2 - expInfo.gasName_MFC2 = 'CO2'; - % Define set point for MFC1 - expInfo.MFC1_SP = 0.3; - % Define gas for MFC2 - expInfo.MFC2_SP = 5.0; - +function runZLC(varargin) + if(nargin<1) + % Display default value being used + % Get the date/time + currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); + disp([currentDateTime,'-> Default experimental settings are used!!']) + % Experiment name + expInfo.expName = 'ZLC'; + % Maximum time of the experiment + expInfo.maxTime = 10; + % Sampling time for the device + expInfo.samplingTime = 1; + % Define gas for MFM + expInfo.gasName_MFM = 'He'; + % Define gas for MFC1 + expInfo.gasName_MFC1 = 'He'; + % Define gas for MFC2 + expInfo.gasName_MFC2 = 'CO2'; + % Define set point for MFC1 + expInfo.MFC1_SP = 0.3; + % Define gas for MFC2 + expInfo.MFC2_SP = 5.0; + else + % Use the value passed to the function + currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); + disp([currentDateTime,'-> Experimental settings passed to the function are used!!']) + expInfo = varargin{1}; + end + % Find COM Ports + % Initatlize ports + portMFM = []; portMFC1 = []; portMFC2 = []; portUMFM = []; + % Find COM port for MFM + portText = matchUSBport({'FT1EQDD6A'}); + if ~isempty(portText{1}) + portMFM = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + end + % Find COM port for MFC1 + portText = matchUSBport({'FT1EU0ACA'}); + if ~isempty(portText{1}) + portMFC1 = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + end + % Find COM port for UMFM + portText = matchUSBport({'3065335A3235'}); + if ~isempty(portText{1}) + portUMFM = ['COM',portText{1}(regexp(portText{1},'COM[1234567891011]')+3)]; + end % Comm setup for the flow meter and controller - serialObj.MFM = struct('portName','COM6','baudRate',19200,'terminator','CR'); - serialObj.MFC1 = struct('portName','COM6','baudRate',19200,'terminator','CR'); - serialObj.MFC2 = struct('portName','COM7','baudRate',19200,'terminator','CR'); - serialObj.UMFM = struct('portName','COM8','baudRate',9600); + serialObj.MFM = struct('portName',portMFM,'baudRate',19200,'terminator','CR'); + serialObj.MFC1 = struct('portName',portMFC1,'baudRate',19200,'terminator','CR'); + serialObj.MFC2 = struct('portName',portMFC2,'baudRate',19200,'terminator','CR'); + serialObj.UMFM = struct('portName',portUMFM,'baudRate',9600); % Generate serial command for polling data serialObj.cmdPollData = generateSerialCommand('pollData',1); @@ -53,7 +83,7 @@ timerDevice.ExecutionMode = 'fixedRate'; timerDevice.BusyMode = 'drop'; timerDevice.Period = expInfo.samplingTime; % [s] - timerDevice.StartDelay = 0; % [s] + timerDevice.StartDelay = 5; % [s] timerDevice.TasksToExecute = floor((expInfo.maxTime)/expInfo.samplingTime); % Specify timer callbacks @@ -72,11 +102,11 @@ % Load the experimental data and add a few more things % Load the output .mat file - load(['..',filesep,'experimentalData',filesep,expInfo.expName]) + load(['experimentalData',filesep,expInfo.expName]) % Get the git commit ID gitCommitID = getGitCommit; % Load the output .mat file - save(['..',filesep,'experimentalData',filesep,expInfo.expName],... + save(['experimentalData',filesep,expInfo.expName],... 'gitCommitID','outputStruct') end @@ -95,11 +125,11 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) gasID_MFC2 = checkGasName(gasName_MFC2); % Initialize the gas for the meter and the controller % MFM - if ~isempty(serialObj.MFM) + if ~isempty(serialObj.MFM.portName) [~] = controlAuxiliaryEquipments(serialObj.MFM, gasID_MFM,1); % Set gas for MFM end % MFC1 - if ~isempty(serialObj.MFC1) + if ~isempty(serialObj.MFC1.portName) [~] = controlAuxiliaryEquipments(serialObj.MFC1, gasID_MFC1,1); % Set gas for MFC1 % Generate serial command for volumteric flow rate set point cmdSetPt = generateSerialCommand('setPoint',1,expInfo.MFC1_SP); % Same units as device @@ -112,7 +142,7 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) end end % MFC2 - if ~isempty(serialObj.MFC2) + if ~isempty(serialObj.MFC2.portName) [~] = controlAuxiliaryEquipments(serialObj.MFC2, gasID_MFC2,1); % Set gas for MFC2 % Generate serial command for volumteric flow rate set point cmdSetPt = generateSerialCommand('setPoint',1,expInfo.MFC2_SP); % Same units as device @@ -136,7 +166,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> Performing task #', num2str(timerObj.tasksExecuted)]) % Get the current state of the flow meter - if ~isempty(serialObj.MFM) + if ~isempty(serialObj.MFM.portName) outputMFM = controlAuxiliaryEquipments(serialObj.MFM, serialObj.cmdPollData,1); outputMFMTemp = strsplit(outputMFM,' '); % Split the output string MFM.pressure = str2double(outputMFMTemp(2)); % [bar] @@ -146,7 +176,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFM.gas = outputMFMTemp(6); % gas in the meter end % Get the current state of the flow controller 1 - if ~isempty(serialObj.MFC1) + if ~isempty(serialObj.MFC1.portName) outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string MFC1.pressure = str2double(outputMFC1Temp(2)); % [bar] @@ -157,7 +187,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC1.gas = outputMFC1Temp(7); % gas in the controller end % Get the current state of the flow controller 2 - if ~isempty(serialObj.MFC2) + if ~isempty(serialObj.MFC2.portName) outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string MFC2.pressure = str2double(outputMFC2Temp(2)); % [bar] @@ -168,7 +198,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC2.gas = outputMFC2Temp(7); % gas in the controller end % Get the current state of the universal flow controller - if ~isempty(serialObj.UMFM) + if ~isempty(serialObj.UMFM.portName) outputUMFM = controlAuxiliaryEquipments(serialObj.UMFM, "UMFM"); UMFM.volFlow = str2double(outputUMFM); end @@ -185,26 +215,12 @@ function stopTimerDevice(~, thisEvent) %% dataLogger: Function to log data into a .mat file function dataLogger(timerObj, expInfo, currentDateTime, ... MFM, MFC1, MFC2, UMFM) -% If running for the first time initialize the structure -if timerObj.tasksExecuted == 1 - outputStruct(1).samplingDateTime = currentDateTime; - outputStruct(1).timeElapsed = 0; - outputStruct(1).MFM = MFM; - outputStruct(1).MFC1= MFC1; - outputStruct(1).MFC2 = MFC2; - outputStruct(1).UMFM = UMFM; - % Create an experimental data folder if it doesnt exost - if ~exist(['..',filesep,'experimentalData'],'dir') - mkdir(['..',filesep,'experimentalData']); - % Save the output into a .mat file - else - save(['..',filesep,'experimentalData',filesep,expInfo.expName],'outputStruct') - end -else - % Load the output .mat file - load(['..',filesep,'experimentalData',filesep,expInfo.expName]) - % Get the size of the output structure +% Check if the file exists +if exist(['experimentalData',filesep,expInfo.expName,'.mat'])==2 + load(['experimentalData',filesep,expInfo.expName]) + % Initialize the counter to existing size plus 1 nCount = size(outputStruct,2); + % Load the output .mat file % Save the data into the structure outputStruct(nCount+1).samplingDateTime = currentDateTime; outputStruct(nCount+1).timeElapsed = seconds(datetime(currentDateTime,... @@ -216,6 +232,22 @@ function dataLogger(timerObj, expInfo, currentDateTime, ... outputStruct(nCount+1).MFC2 = MFC2; outputStruct(nCount+1).UMFM = UMFM; % Save the output into a .mat file - save(['..',filesep,'experimentalData',filesep,expInfo.expName],'outputStruct') + save(['experimentalData',filesep,expInfo.expName],'outputStruct') +% First initiaization +else + nCount = 1; + outputStruct(nCount).samplingDateTime = currentDateTime; + outputStruct(nCount).timeElapsed = 0; + outputStruct(nCount).MFM = MFM; + outputStruct(nCount).MFC1= MFC1; + outputStruct(nCount).MFC2 = MFC2; + outputStruct(nCount).UMFM = UMFM; + % Create an experimental data folder if it doesnt exost + if ~exist(['experimentalData'],'dir') + mkdir(['experimentalData']); + % Save the output into a .mat file + else + save(['experimentalData',filesep,expInfo.expName],'outputStruct') + end end end \ No newline at end of file From cad8327b7bcfc5128ff9c38e01af21216084d5b4 Mon Sep 17 00:00:00 2001 From: QCPML Date: Fri, 12 Mar 2021 16:34:51 +0000 Subject: [PATCH 019/189] Bug fixes --- experimental/calibrateMeters.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/experimental/calibrateMeters.m b/experimental/calibrateMeters.m index da6d4de..d0ef918 100644 --- a/experimental/calibrateMeters.m +++ b/experimental/calibrateMeters.m @@ -21,11 +21,12 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function calibrateMeters % Experiment name - expInfo.expName = 'ZLCCalibrateMeters'; + expInfo.expName = ['ZLCCalibrateMeters','_',... + datestr(datetime('now'),'yyyymmdd')]; % Maximum time of the experiment - expInfo.maxTime = 10; + expInfo.maxTime = 60; % Sampling time for the device - expInfo.samplingTime = 1; + expInfo.samplingTime = 2; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -33,7 +34,7 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 - MFC1_SP = [0.0]; + MFC1_SP = [0.0, 10.0, 20.0]; % Loop through all setpoints to calibrate the meters for ii=1:length(MFC1_SP) From 51adecd55c4146c56ba8db9eb09e5708dacbef1c Mon Sep 17 00:00:00 2001 From: QCPML Date: Fri, 12 Mar 2021 18:24:29 +0000 Subject: [PATCH 020/189] Add set point to zero at the end of the experiment --- experimental/runZLC.m | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 0f23220..ed50483 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-03-12, AK: Add set point to zero at the end of the experiment % - 2021-03-12, AK: Add auto detection of ports and change structure % - 2021-03-11, HA: Add data logger, set points, and refine code % - 2021-03-10, HA: Initial creation @@ -207,13 +208,31 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC1,MFC2,UMFM); end %% stopTimerDevice: Stop timer device -function stopTimerDevice(~, thisEvent) +function stopTimerDevice(~, thisEvent, serialObj) % Get the event date/time currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> And its over babyyyyyy!!']) + % Generate serial command for volumteric flow rate set point to zero + cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC1, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); + outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string + if str2double(outputMFC1Temp(6)) ~= expInfo.MFC1_SP + error("You should not be here!!!") + end + % Generate serial command for volumteric flow rate set point to zero + cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); + outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string + if str2double(outputMFC2Temp(6)) ~= expInfo.MFC2_SP + error("You should not be here!!!") + end end %% dataLogger: Function to log data into a .mat file -function dataLogger(timerObj, expInfo, currentDateTime, ... +function dataLogger(~, expInfo, currentDateTime, ... MFM, MFC1, MFC2, UMFM) % Check if the file exists if exist(['experimentalData',filesep,expInfo.expName,'.mat'])==2 From 463923b7a696a95051184e338d9f171e9fa59e41 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 15 Mar 2021 18:51:28 +0000 Subject: [PATCH 021/189] Bug fixes --- experimental/runZLC.m | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/experimental/runZLC.m b/experimental/runZLC.m index ed50483..683c12d 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-03-15, AK: Bug fixes % - 2021-03-12, AK: Add set point to zero at the end of the experiment % - 2021-03-12, AK: Add auto detection of ports and change structure % - 2021-03-11, HA: Add data logger, set points, and refine code @@ -43,7 +44,7 @@ function runZLC(varargin) % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 - expInfo.MFC1_SP = 0.3; + expInfo.MFC1_SP = 5.0; % Define gas for MFC2 expInfo.MFC2_SP = 5.0; else @@ -90,7 +91,7 @@ function runZLC(varargin) % Specify timer callbacks timerDevice.StartFcn = {@initializeTimerDevice,expInfo,serialObj}; timerDevice.TimerFcn = {@executeTimerDevice, expInfo, serialObj}; - timerDevice.StopFcn = {@stopTimerDevice}; + timerDevice.StopFcn = {@stopTimerDevice, expInfo, serialObj}; % Start the experiment % Get the date/time @@ -208,27 +209,34 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC1,MFC2,UMFM); end %% stopTimerDevice: Stop timer device -function stopTimerDevice(~, thisEvent, serialObj) +function stopTimerDevice(~, thisEvent, expInfo, serialObj) % Get the event date/time currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> And its over babyyyyyy!!']) % Generate serial command for volumteric flow rate set point to zero - cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device - [~] = controlAuxiliaryEquipments(serialObj.MFC1, cmdSetPt,1); % Set gas for MFC1 - % Check if the set point was sent to the controller - outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); - outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string - if str2double(outputMFC1Temp(6)) ~= expInfo.MFC1_SP - error("You should not be here!!!") + % MFC1 + if ~isempty(serialObj.MFC1.portName) + cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC1, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); + outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string + if str2double(outputMFC1Temp(6)) ~= 0 + error("You should not be here!!!") + 1+2 + end end % Generate serial command for volumteric flow rate set point to zero - cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device - [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC1 - % Check if the set point was sent to the controller - outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); - outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string - if str2double(outputMFC2Temp(6)) ~= expInfo.MFC2_SP - error("You should not be here!!!") + % MFC2 + if ~isempty(serialObj.MFC2.portName) + cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); + outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string + if str2double(outputMFC2Temp(6)) ~= 0 + error("You should not be here!!!") + end end end %% dataLogger: Function to log data into a .mat file From cb354aa430f55521fba5cfc6a004ab10c6eefe32 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Tue, 16 Mar 2021 15:10:54 +0000 Subject: [PATCH 022/189] Add valve switch times --- experimental/runZLC.m | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 683c12d..e31132f 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-03-16, AK: Add valve switch times % - 2021-03-15, AK: Bug fixes % - 2021-03-12, AK: Add set point to zero at the end of the experiment % - 2021-03-12, AK: Add auto detection of ports and change structure @@ -164,8 +165,24 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) % Initialize outputs MFM = []; MFC1 = []; MFC2 = []; UMFM = []; - % Get the event date/time - currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); + % Get user input to indicate switching of the valve + if timerObj.tasksExecuted == 1 + % Waiting for user to switch the valve + promptUser = 'Switch asap! When you press Y, the gas switches (you wish)! [Y/N]: '; + userInput = input(promptUser,'s'); + % This is recorded as the time of switch + % Empty readings (just for analysis purpose) + if strcmp(userInput,'Y') || strcmp(userInput,'y') + currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); + dataLogger(timerObj,expInfo,currentDateTime,[],... + [],[],[]); + end + end + % Get the sampling date/time + if timerObj.tasksExecuted > 1 + % Get the event date/time + currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); + end disp([currentDateTime,'-> Performing task #', num2str(timerObj.tasksExecuted)]) % Get the current state of the flow meter if ~isempty(serialObj.MFM.portName) @@ -223,7 +240,6 @@ function stopTimerDevice(~, thisEvent, expInfo, serialObj) outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string if str2double(outputMFC1Temp(6)) ~= 0 error("You should not be here!!!") - 1+2 end end % Generate serial command for volumteric flow rate set point to zero From d7792cca60f19e2488f3832b7b442742cb130c86 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Tue, 16 Mar 2021 15:20:06 +0000 Subject: [PATCH 023/189] Add calibrate meters flag --- experimental/calibrateMeters.m | 5 ++++- experimental/runZLC.m | 13 ++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/experimental/calibrateMeters.m b/experimental/calibrateMeters.m index d0ef918..7569290 100644 --- a/experimental/calibrateMeters.m +++ b/experimental/calibrateMeters.m @@ -12,6 +12,7 @@ % Calibrates the flow meter and controller for different set point values % % Last modified: +% - 2021-03-16, AK: Add calibrate meters flag % - 2021-03-12, AK: Initial creation % % Input arguments: @@ -34,11 +35,13 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 - MFC1_SP = [0.0, 10.0, 20.0]; + MFC1_SP = [0.0, 15.0, 30.0, 45.0, 60.0]; % Loop through all setpoints to calibrate the meters for ii=1:length(MFC1_SP) expInfo.MFC1_SP = MFC1_SP(ii); + % Flag for meter calibration + expInfo.calibrateMeters = true; % Run the setup for different calibrations runZLC(expInfo) end diff --git a/experimental/runZLC.m b/experimental/runZLC.m index e31132f..7af230d 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -35,9 +35,9 @@ function runZLC(varargin) % Experiment name expInfo.expName = 'ZLC'; % Maximum time of the experiment - expInfo.maxTime = 10; + expInfo.maxTime = 300; % Sampling time for the device - expInfo.samplingTime = 1; + expInfo.samplingTime = 2; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -48,6 +48,8 @@ function runZLC(varargin) expInfo.MFC1_SP = 5.0; % Define gas for MFC2 expInfo.MFC2_SP = 5.0; + % Calibrate meters flag + expInfo.calibrateMeters = False; else % Use the value passed to the function currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); @@ -166,7 +168,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) % Initialize outputs MFM = []; MFC1 = []; MFC2 = []; UMFM = []; % Get user input to indicate switching of the valve - if timerObj.tasksExecuted == 1 + if timerObj.tasksExecuted == 1 && ~expInfo.calibrateMeters % Waiting for user to switch the valve promptUser = 'Switch asap! When you press Y, the gas switches (you wish)! [Y/N]: '; userInput = input(promptUser,'s'); @@ -179,10 +181,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) end end % Get the sampling date/time - if timerObj.tasksExecuted > 1 - % Get the event date/time - currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); - end + currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> Performing task #', num2str(timerObj.tasksExecuted)]) % Get the current state of the flow meter if ~isempty(serialObj.MFM.portName) From fa693b0603ab17430915cccfdc47552bf7c38f44 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Tue, 16 Mar 2021 17:27:35 +0000 Subject: [PATCH 024/189] Add MS calibration script --- experimental/calibrateMS.m | 49 ++++++++++++++++++++++++++++++++++++++ experimental/runZLC.m | 40 +++++++++---------------------- 2 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 experimental/calibrateMS.m diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m new file mode 100644 index 0000000..bd17748 --- /dev/null +++ b/experimental/calibrateMS.m @@ -0,0 +1,49 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Calibrates the mass specfor different set point values +% +% Last modified: +% - 2021-03-16, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function calibrateMS + % Experiment name + expInfo.expName = ['ZLCCalibrateMS','_',... + datestr(datetime('now'),'yyyymmdd')]; + % Maximum time of the experiment + expInfo.maxTime = 240; + % Sampling time for the device + expInfo.samplingTime = 2; + % Define gas for MFM + expInfo.gasName_MFM = 'He'; + % Define gas for MFC1 + expInfo.gasName_MFC1 = 'He'; + % Define gas for MFC2 + expInfo.gasName_MFC2 = 'CO2'; + % Define set point for MFC1 + MFC1_SP = [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5 15.0]; + % Define set point for MFC2 + MFC2_SP = max(MFC1_SP)-MFC1_SP; + % Loop through all setpoints to calibrate the meters + for ii=1:length(MFC1_SP) + expInfo.MFC1_SP = MFC1_SP(ii); + expInfo.MFC2_SP = MFC2_SP(ii); + % Flag for meter calibration + expInfo.calibrateMeters = true; + % Run the setup for different calibrations + runZLC(expInfo) + end +end diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 7af230d..330c229 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-03-16, AK: Add MFC2 and fix for MS calibration % - 2021-03-16, AK: Add valve switch times % - 2021-03-15, AK: Bug fixes % - 2021-03-12, AK: Add set point to zero at the end of the experiment @@ -45,11 +46,11 @@ function runZLC(varargin) % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 - expInfo.MFC1_SP = 5.0; + expInfo.MFC1_SP = 15.0; % Define gas for MFC2 - expInfo.MFC2_SP = 5.0; + expInfo.MFC2_SP = 15.0; % Calibrate meters flag - expInfo.calibrateMeters = False; + expInfo.calibrateMeters = false; else % Use the value passed to the function currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); @@ -69,6 +70,11 @@ function runZLC(varargin) if ~isempty(portText{1}) portMFC1 = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; end + % Find COM port for MFC2 + portText = matchUSBport({'FT1EQDD6M'}); + if ~isempty(portText{1}) + portMFC2 = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + end % Find COM port for UMFM portText = matchUSBport({'3065335A3235'}); if ~isempty(portText{1}) @@ -94,7 +100,7 @@ function runZLC(varargin) % Specify timer callbacks timerDevice.StartFcn = {@initializeTimerDevice,expInfo,serialObj}; timerDevice.TimerFcn = {@executeTimerDevice, expInfo, serialObj}; - timerDevice.StopFcn = {@stopTimerDevice, expInfo, serialObj}; + timerDevice.StopFcn = {@stopTimerDevice}; % Start the experiment % Get the date/time @@ -225,34 +231,10 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC1,MFC2,UMFM); end %% stopTimerDevice: Stop timer device -function stopTimerDevice(~, thisEvent, expInfo, serialObj) +function stopTimerDevice(~, thisEvent) % Get the event date/time currentDateTime = datestr(thisEvent.Data.time,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> And its over babyyyyyy!!']) - % Generate serial command for volumteric flow rate set point to zero - % MFC1 - if ~isempty(serialObj.MFC1.portName) - cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device - [~] = controlAuxiliaryEquipments(serialObj.MFC1, cmdSetPt,1); % Set gas for MFC1 - % Check if the set point was sent to the controller - outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); - outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string - if str2double(outputMFC1Temp(6)) ~= 0 - error("You should not be here!!!") - end - end - % Generate serial command for volumteric flow rate set point to zero - % MFC2 - if ~isempty(serialObj.MFC2.portName) - cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device - [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC1 - % Check if the set point was sent to the controller - outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); - outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string - if str2double(outputMFC2Temp(6)) ~= 0 - error("You should not be here!!!") - end - end end %% dataLogger: Function to log data into a .mat file function dataLogger(~, expInfo, currentDateTime, ... From 8053cdc9e022ef8a644123b10de2e351469e53ea Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 17 Mar 2021 13:27:47 +0000 Subject: [PATCH 025/189] Add dead volume computation --- experimental/extractDeadVolume.py | 97 ++++++++++++++++++ experimental/simulateDeadVolume.py | 158 +++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 experimental/extractDeadVolume.py create mode 100644 experimental/simulateDeadVolume.py diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py new file mode 100644 index 0000000..2ee9ebc --- /dev/null +++ b/experimental/extractDeadVolume.py @@ -0,0 +1,97 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Find the dead volume and the number of tanks to describe the dead volume +# using the tanks in series (TIS) for the ZLC +# Reference: 10.1016/j.ces.2008.02.023 +# +# Last modified: +# - 2021-03-17, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def extractDeadVolume(): + import numpy as np + from geneticalgorithm2 import geneticalgorithm2 as ga # GA + import auxiliaryFunctions + import multiprocessing # For parallel processing + + # Find out the total number of cores available for parallel processing + num_cores = multiprocessing.cpu_count() + + # Number of times optimization repeated + numOptRepeat = 3 + + # 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 bounds and the type of the parameters to be optimized + optBounds = np.array(([np.finfo(float).eps,100], [1,20])) + optType=np.array(['real','int']) + # Algorithm parameters for GA + algorithm_param = {'max_num_iteration':25, + 'population_size':100, + 'mutation_probability':0.1, + 'crossover_probability': 0.55, + 'parents_portion': 0.15, + 'max_iteration_without_improv':None} + + # Minimize an objective function to compute the dead volume and the number of + # tanks for the dead volume using GA + model = ga(function = deadVolObjectiveFunction, dimension=2, + variable_type_mixed = optType, + variable_boundaries = optBounds, + algorithm_parameters=algorithm_param) + + # Call the GA optimizer using multiple cores + model.run(set_function=ga.set_function_multiprocess(deadVolObjectiveFunction, + n_jobs = num_cores), + no_plot = True) + # Repeat the optimization with the last generation repeated numOptRepeat + # times (for better accuracy) + for ii in range(numOptRepeat): + model.run(set_function=ga.set_function_multiprocess(deadVolObjectiveFunction, + n_jobs = num_cores), + start_generation=model.output_dict['last_generation'], no_plot = True) + + # Return the optimized values + return model.output_dict + +# func: deadVolObjectiveFunction +# For use with GA, the function accepts only one input (parameters from the +# optimizer) +def deadVolObjectiveFunction(x): + import numpy as np + from simulateDeadVolume import simulateDeadVolume + + # Dead volume [cc] + deadVolume = 3.25 + # Number of tanks [-] + numberOfTanks = 6 + # Generate dead volume response (pseudo experiment) + _ , _ , moleFracExp = simulateDeadVolume(deadVolume = deadVolume, + numberOfTanks = numberOfTanks) + + # Compute the dead volume response using the optimizer parameters + _ , _ , moleFracOut = simulateDeadVolume(deadVolume = x[0], + numberOfTanks = int(x[1])) + + # Compute the sum of the error for the difference between exp. and sim. + return np.sum(np.power(moleFracExp - moleFracOut,2)) \ No newline at end of file diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py new file mode 100644 index 0000000..5d266f6 --- /dev/null +++ b/experimental/simulateDeadVolume.py @@ -0,0 +1,158 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Simulates the dead volume using the tanks in series (TIS) for the ZLC +# Reference: 10.1016/j.ces.2008.02.023 +# +# Last modified: +# - 2021-03-17, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def simulateDeadVolume(**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() + + # Flow rate of the gas [cc/s] + if 'flowRate' in kwargs: + flowRate = kwargs["flowRate"] + else: + flowRate = 0.25 + # Total Dead Volume of the tanks [cc] + if 'deadVolume' in kwargs: + deadVolume = kwargs["deadVolume"] + else: + deadVolume = 1 + # Number of tanks [-] + if 'numberOfTanks' in kwargs: + numberOfTanks = kwargs["numberOfTanks"] + else: + numberOfTanks = 2 + # 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] + if 'temperature' in kwargs: + temperature = np.array(kwargs["temperature"]); + else: + temperature = np.array([298.15]); + # Feed Mole Fraction [-] + if 'feedMoleFrac' in kwargs: + feedMoleFrac = np.array(kwargs["feedMoleFrac"]) + else: + feedMoleFrac = np.array([1.]) + # Time span for integration [tuple with t0 and tf] + if 'timeInt' in kwargs: + timeInt = kwargs["timeInt"] + else: + timeInt = (0.0,20) + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Prepare tuple of input parameters for the ode solver + inputParameters = (flowRate, deadVolume, numberOfTanks, pressureTotal, temperature) + + # Prepare initial conditions vector + # The first element is the inlet composition and the rest is the dead + # volume + initialConditions = np.concatenate((feedMoleFrac, + np.ones([numberOfTanks])*(1-feedMoleFrac))) + # Solve the system of equations + outputSol = solve_ivp(solveTanksInSeries, timeInt, initialConditions, + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],0.1), + rtol = 1e-6, args = inputParameters) + + # Parse out the time + timeSim = outputSol.t + + # Inlet concentration + moleFracIn = outputSol.y[0] + + # Outlet concentration at the dead volume + moleFracOut = outputSol.y[-1] + + # Plot the dead volume response + if plotFlag: + plotOutletConcentration(timeSim,moleFracIn,moleFracOut) + + return timeSim, moleFracIn, moleFracOut + +# func: solveTanksInSeries +# Solves the system of ODE for the tanks in series model for the dead volume +def solveTanksInSeries(t, f, *inputParameters): + import numpy as np + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Unpack the tuple of input parameters used to solve equations + flowRate, deadVolume , numberOfTanks, pressureTotal, temperature = inputParameters + + # Initialize the derivatives to zero + df = np.zeros([numberOfTanks+1]) + + # Volume of each tank + volumeOfTank = deadVolume/numberOfTanks + + # Residence time of each tank + residenceTime = volumeOfTank/flowRate + + # Solve the ode + df[1:numberOfTanks+1] = ((1/residenceTime) + *(f[0:numberOfTanks] - f[1:numberOfTanks+1])) + + # Return the derivatives for the solver + return df + +# func: plotOutletConcentration +# Plot the concentration outlet after correcting for dead volume +def plotOutletConcentration(timeSim, moleFracIn, moleFracOut): + import numpy as np + import os + import matplotlib.pyplot as plt + + # Plot the solid phase compositions + os.chdir(".."+os.path.sep+"plotFunctions") + plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax = plt.subplot(1,1,1) + ax.plot(timeSim, moleFracIn, + linewidth=1.5,color='b', + label = 'In') + ax.plot(timeSim, moleFracOut, + linewidth=1.5,color='r', + label = 'Out') + ax.set(xlabel='$t$ [s]', + ylabel='$y$ [-]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(moleFracOut)]) + ax.legend() + plt.show() + os.chdir(".."+os.path.sep+"experimental") \ No newline at end of file From e4dc91cffe3248eb183085edca18949595f93c5a Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 17 Mar 2021 19:00:41 +0000 Subject: [PATCH 026/189] Add analysis script for flow meter and MS --- experimental/analyseCalibration.m | 221 ++++++++++++++++++++++++++++++ setPathERASE.m | 24 ++++ 2 files changed, 245 insertions(+) create mode 100644 experimental/analyseCalibration.m create mode 100644 setPathERASE.m diff --git a/experimental/analyseCalibration.m b/experimental/analyseCalibration.m new file mode 100644 index 0000000..65ec028 --- /dev/null +++ b/experimental/analyseCalibration.m @@ -0,0 +1,221 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% Hassan Azzan (HA) +% +% Purpose: +% +% +% Last modified: +% - 2021-03-17, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function analyseCalibration(fileToLoadMeter,fileToLoadMS) +% Find the directory of the file and move to the top folder +filePath = which('analyseCalibration'); +cd(filePath(1:end-21)); + +% Get the git commit ID +gitCommitID = getGitCommit; + +% Load the file that contains the flow meter calibration +if ~isempty(fileToLoadMeter) + flowData = load(fileToLoadMeter); + % Analyse flow data + MFM = [flowData.outputStruct.MFM]; % MFM + MFC1 = [flowData.outputStruct.MFC1]; % MFC1 + MFC2 = [flowData.outputStruct.MFC2]; % MFC2 + UMFM = [flowData.outputStruct.UMFM]; % UMFM + % Get the volumetric flow rate + volFlow_MFM = [MFM.volFlow]; + volFlow_MFC1 = [MFC1.volFlow]; + gas_MFC1 = [MFC1.gas]; + volFlow_UMFM = [UMFM.volFlow]; + % Find indices that corresponds to He/CO2 in the MFC + indexHe = find(gas_MFC1 == 'He'); + indexCO2 = find(gas_MFC1 == 'CO2'); + % Parse the flow rate from the MFC, MFM, and UMFM for each gas + % MFC + volFlow_MFC1_He = volFlow_MFC1(indexHe); + volFlow_MFC1_CO2 = volFlow_MFC1(indexCO2); + % MFM + volFlow_MFM_He = volFlow_MFM(indexHe); + volFlow_MFM_CO2 = volFlow_MFM(indexCO2); + % UMFM + volFlow_UMFM_He = volFlow_UMFM(indexHe); + volFlow_UMFM_CO2 = volFlow_UMFM(indexCO2); + + % Calibrate the meters + % MFC + calibration.MFC_He = volFlow_MFC1_He'\volFlow_UMFM_He'; + calibration.MFC_CO2 = volFlow_MFC1_CO2'\volFlow_UMFM_CO2'; + % MFM + calibration.MFM_He = volFlow_MFM_He'\volFlow_UMFM_He'; + calibration.MFM_CO2 = volFlow_MFM_CO2'\volFlow_UMFM_CO2'; + + % Save the calibration data into a .mat file + % Check if calibration data folder exists + if exist(['experimentalData',filesep,... + 'calibrationData'],'dir') == 7 + % Save the calibration data for further use + save(['experimentalData',filesep,... + 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibration',... + 'gitCommitID'); + else + % Create the calibration data folder if it does not exist + mkdir(['experimentalData',filesep,'calibrationData']) + % Save the calibration data for further use + save(['experimentalData',filesep,... + 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibration',... + 'gitCommitID'); + end + + % Plot the raw and the calibrated data + figure + MFC1Set = 0:80; + subplot(2,2,1) + hold on + scatter(volFlow_MFC1_He,volFlow_UMFM_He,'or') + plot(MFC1Set,calibration.MFC_He*MFC1Set,'b') + subplot(2,2,2) + hold on + scatter(volFlow_MFC1_CO2,volFlow_UMFM_CO2,'or') + plot(MFC1Set,calibration.MFC_CO2*MFC1Set,'b') + subplot(2,2,3) + hold on + scatter(volFlow_MFM_He,volFlow_UMFM_He,'or') + plot(MFC1Set,calibration.MFM_He*MFC1Set,'b') + subplot(2,2,4) + hold on + scatter(volFlow_MFM_CO2,volFlow_UMFM_CO2,'or') + plot(MFC1Set,calibration.MFM_CO2*MFC1Set,'b') +end +% Load the file that contains the MS calibration +if ~isempty(fileToLoadMS) + % Load flow data + flowMS = load(fileToLoadMS.flow); + if ~isfield(fileToLoadMS,'calibration') + error('You gotta calibrate your flow meters fisrst!! Or check the file name of the flow calibration!!') + end + calibrationMeters = load(fileToLoadMS.calibration); + % Analyse flow data + MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He + MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 + % Get the datetime and volumetric flow rate + dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... + 'InputFormat','yyyyMMdd_HHmmss'); + volFlow_MFC1 = [MFC1.volFlow]; % He + volFlow_MFC2 = [MFC2.volFlow]; % CO2 + % Apply the calibration for the flows + volFlow_He = volFlow_MFC1*calibrationMeters.calibration.MFC_He; + volFlow_CO2 = volFlow_MFC2*calibrationMeters.calibration.MFC_CO2; + % Load MS Ascii data + % Create file identifier + fileId = fopen(fileToLoadMS.MS); + % Load the MS Data into a cell array + rawMSData = textscan(fileId,repmat('%s',1,9),'HeaderLines',8,'Delimiter','\t'); + % Get the date time for CO2 + dateTimeHe = datetime(cell2mat(rawMSData{1,4}),... + 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); + % Get the date time for He + dateTimeCO2 = datetime(cell2mat(rawMSData{1,1}),... + 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); + % Reconcile all the data + % Initial time + initialTime = max([dateTimeFlow(1), dateTimeHe(1), dateTimeCO2(1)]); + % Final time + finalTime = min([dateTimeFlow(end), dateTimeHe(end), dateTimeCO2(end)]); + + % Find index corresponding to initial time for meters and MS + indexInitial_Flow = find(dateTimeFlow>=initialTime,1,'first'); + indexInitial_He = find(dateTimeHe>=initialTime,1,'first'); + indexInitial_CO2 = find(dateTimeCO2>=initialTime,1,'first'); + + % Find index corresponding to final time for meters and MS + indexFinal_Flow = find(dateTimeFlow<=finalTime,1,'last'); + indexFinal_He = find(dateTimeHe<=finalTime,1,'last'); + indexFinal_CO2 = find(dateTimeCO2<=finalTime,1,'last'); + + % Reconciled data (without interpolation) + % The whole reconciliation assumes that the MS is running after the + % flow meters to avoid any issues with interpolation!!! + % Meters and the controllers + reconciledData.raw.dateTimeFlow = dateTimeFlow(indexInitial_Flow:end); + reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); + reconciledData.raw.volFlow_CO2 = volFlow_CO2(indexInitial_Flow:end); + % MS + reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_He:end); + reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_CO2:end); + reconciledData.raw.signalHe = str2num(cell2mat(rawMSData{1,6}(indexInitial_He:end))); + reconciledData.raw.signalCO2 = str2num(cell2mat(rawMSData{1,3}(indexInitial_CO2:end))); + + % Reconciled data (with interpolation) + % Meters and the controllers + reconciledData.flow(:,1) = seconds(reconciledData.raw.dateTimeFlow... + -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] + reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] + reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] + + % MS + rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... + - reconciledData.raw.dateTimeMS_He(1)); % Time elapsed He [s] + rawTimeElapsedCO2 = seconds(reconciledData.raw.dateTimeMS_CO2 ... + - reconciledData.raw.dateTimeMS_CO2(1)); % Time elapsed CO2 [s] + % Interpolate the MS signal at the times of flow meter/controller + reconciledData.MS(:,1) = reconciledData.flow(:,1); % Use the time of the flow meter [s] + reconciledData.MS(:,2) = interp1(rawTimeElapsedHe,reconciledData.raw.signalHe,... + reconciledData.MS(:,1)); % Interpoloted MS signal He [-] + reconciledData.MS(:,3) = interp1(rawTimeElapsedCO2,reconciledData.raw.signalCO2,... + reconciledData.MS(:,1)); % Interpoloted MS signal CO2 [-] + + % Compute the mole fractions using the reconciled flow data + reconciledData.moleFrac(:,1) = (reconciledData.flow(:,2))./(reconciledData.flow(:,2)+reconciledData.flow(:,3)); + reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); + + % Fit a polynomial function to get the model for MS + % Fitting a 3rd order polynomial (check before accepting this) + calibration.He = polyfit(reconciledData.moleFrac(:,1),reconciledData.MS(:,2),3); % He + calibration.CO2 = polyfit(reconciledData.moleFrac(:,2),reconciledData.MS(:,3),3); % Co2 + + % Save the calibration data into a .mat file + % Check if calibration data folder exists + if exist(['experimentalData',filesep,... + 'calibrationData'],'dir') == 7 + % Save the calibration data for further use + save(['experimentalData',filesep,... + 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibration',... + 'gitCommitID'); + else + % Create the calibration data folder if it does not exist + mkdir(['experimentalData',filesep,'calibrationData']) + % Save the calibration data for further use + save(['experimentalData',filesep,... + 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibration',... + 'gitCommitID'); + end + + % Plot the raw and the calibrated data + figure + % He + subplot(1,2,1) + plot(0:0.01:1,polyval(calibration.He,0:0.01:1)) + hold on + plot(reconciledData.moleFrac(:,1),reconciledData.MS(:,2),'or') + + % CO2 + subplot(1,2,2) + plot(0:0.01:1,polyval(calibration.CO2,0:0.01:1)) + hold on + plot(reconciledData.moleFrac(:,2),reconciledData.MS(:,3),'or') +end +end \ No newline at end of file diff --git a/setPathERASE.m b/setPathERASE.m new file mode 100644 index 0000000..4b3ecc7 --- /dev/null +++ b/setPathERASE.m @@ -0,0 +1,24 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Add ERASE folder and subfolders to the path +% +% Last modified: +% - 2021-03-17, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% Add the entire ERASE folder to the path +addpath(genpath(['..',filesep,'ERASE'])) \ No newline at end of file From 14e36cda36c24d8d43429cb834c53ba14e419753 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 17 Mar 2021 19:15:58 +0000 Subject: [PATCH 027/189] Function to reconcile data from MFC and MS --- experimental/analyseCalibration.m | 82 +---------------------- experimental/reconcileData.m | 104 ++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 79 deletions(-) create mode 100644 experimental/reconcileData.m diff --git a/experimental/analyseCalibration.m b/experimental/analyseCalibration.m index 65ec028..de1478b 100644 --- a/experimental/analyseCalibration.m +++ b/experimental/analyseCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-17, AK: Change structure % - 2021-03-17, AK: Initial creation % % Input arguments: @@ -102,85 +103,8 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) end % Load the file that contains the MS calibration if ~isempty(fileToLoadMS) - % Load flow data - flowMS = load(fileToLoadMS.flow); - if ~isfield(fileToLoadMS,'calibration') - error('You gotta calibrate your flow meters fisrst!! Or check the file name of the flow calibration!!') - end - calibrationMeters = load(fileToLoadMS.calibration); - % Analyse flow data - MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He - MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 - % Get the datetime and volumetric flow rate - dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... - 'InputFormat','yyyyMMdd_HHmmss'); - volFlow_MFC1 = [MFC1.volFlow]; % He - volFlow_MFC2 = [MFC2.volFlow]; % CO2 - % Apply the calibration for the flows - volFlow_He = volFlow_MFC1*calibrationMeters.calibration.MFC_He; - volFlow_CO2 = volFlow_MFC2*calibrationMeters.calibration.MFC_CO2; - % Load MS Ascii data - % Create file identifier - fileId = fopen(fileToLoadMS.MS); - % Load the MS Data into a cell array - rawMSData = textscan(fileId,repmat('%s',1,9),'HeaderLines',8,'Delimiter','\t'); - % Get the date time for CO2 - dateTimeHe = datetime(cell2mat(rawMSData{1,4}),... - 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); - % Get the date time for He - dateTimeCO2 = datetime(cell2mat(rawMSData{1,1}),... - 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); - % Reconcile all the data - % Initial time - initialTime = max([dateTimeFlow(1), dateTimeHe(1), dateTimeCO2(1)]); - % Final time - finalTime = min([dateTimeFlow(end), dateTimeHe(end), dateTimeCO2(end)]); - - % Find index corresponding to initial time for meters and MS - indexInitial_Flow = find(dateTimeFlow>=initialTime,1,'first'); - indexInitial_He = find(dateTimeHe>=initialTime,1,'first'); - indexInitial_CO2 = find(dateTimeCO2>=initialTime,1,'first'); - - % Find index corresponding to final time for meters and MS - indexFinal_Flow = find(dateTimeFlow<=finalTime,1,'last'); - indexFinal_He = find(dateTimeHe<=finalTime,1,'last'); - indexFinal_CO2 = find(dateTimeCO2<=finalTime,1,'last'); - - % Reconciled data (without interpolation) - % The whole reconciliation assumes that the MS is running after the - % flow meters to avoid any issues with interpolation!!! - % Meters and the controllers - reconciledData.raw.dateTimeFlow = dateTimeFlow(indexInitial_Flow:end); - reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); - reconciledData.raw.volFlow_CO2 = volFlow_CO2(indexInitial_Flow:end); - % MS - reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_He:end); - reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_CO2:end); - reconciledData.raw.signalHe = str2num(cell2mat(rawMSData{1,6}(indexInitial_He:end))); - reconciledData.raw.signalCO2 = str2num(cell2mat(rawMSData{1,3}(indexInitial_CO2:end))); - - % Reconciled data (with interpolation) - % Meters and the controllers - reconciledData.flow(:,1) = seconds(reconciledData.raw.dateTimeFlow... - -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] - reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] - reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] - - % MS - rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... - - reconciledData.raw.dateTimeMS_He(1)); % Time elapsed He [s] - rawTimeElapsedCO2 = seconds(reconciledData.raw.dateTimeMS_CO2 ... - - reconciledData.raw.dateTimeMS_CO2(1)); % Time elapsed CO2 [s] - % Interpolate the MS signal at the times of flow meter/controller - reconciledData.MS(:,1) = reconciledData.flow(:,1); % Use the time of the flow meter [s] - reconciledData.MS(:,2) = interp1(rawTimeElapsedHe,reconciledData.raw.signalHe,... - reconciledData.MS(:,1)); % Interpoloted MS signal He [-] - reconciledData.MS(:,3) = interp1(rawTimeElapsedCO2,reconciledData.raw.signalCO2,... - reconciledData.MS(:,1)); % Interpoloted MS signal CO2 [-] - - % Compute the mole fractions using the reconciled flow data - reconciledData.moleFrac(:,1) = (reconciledData.flow(:,2))./(reconciledData.flow(:,2)+reconciledData.flow(:,3)); - reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); + % Call reconcileData function for calibration of the MS + reconciledData = reconcileData(fileToLoadMS); % Fit a polynomial function to get the model for MS % Fitting a 3rd order polynomial (check before accepting this) diff --git a/experimental/reconcileData.m b/experimental/reconcileData.m new file mode 100644 index 0000000..d214d0c --- /dev/null +++ b/experimental/reconcileData.m @@ -0,0 +1,104 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% Hassan Azzan (HA) +% +% Purpose: +% +% +% Last modified: +% - 2021-03-17, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function reconciledData = reconcileData(fileToLoad) + % Load flow data + flowMS = load(fileToLoad.flow); + if ~isfield(fileToLoad,'calibration') + error('You gotta calibrate your flow meters fisrst!! Or check the file name of the flow calibration!!') + end + % Flow Calibration File + calibrationMeters = load(fileToLoad.calibration); + % Analyse flow data + MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He + MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 + % Get the datetime and volumetric flow rate + dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... + 'InputFormat','yyyyMMdd_HHmmss'); + volFlow_MFC1 = [MFC1.volFlow]; % He + volFlow_MFC2 = [MFC2.volFlow]; % CO2 + % Apply the calibration for the flows + volFlow_He = volFlow_MFC1*calibrationMeters.calibration.MFC_He; + volFlow_CO2 = volFlow_MFC2*calibrationMeters.calibration.MFC_CO2; + % Load MS Ascii data + % Create file identifier + fileId = fopen(fileToLoad.MS); + % Load the MS Data into a cell array + rawMSData = textscan(fileId,repmat('%s',1,9),'HeaderLines',8,'Delimiter','\t'); + % Get the date time for CO2 + dateTimeHe = datetime(cell2mat(rawMSData{1,4}),... + 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); + % Get the date time for He + dateTimeCO2 = datetime(cell2mat(rawMSData{1,1}),... + 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); + % Reconcile all the data + % Initial time + initialTime = max([dateTimeFlow(1), dateTimeHe(1), dateTimeCO2(1)]); + % Final time + finalTime = min([dateTimeFlow(end), dateTimeHe(end), dateTimeCO2(end)]); + + % Find index corresponding to initial time for meters and MS + indexInitial_Flow = find(dateTimeFlow>=initialTime,1,'first'); + indexInitial_He = find(dateTimeHe>=initialTime,1,'first'); + indexInitial_CO2 = find(dateTimeCO2>=initialTime,1,'first'); + + % Find index corresponding to final time for meters and MS + indexFinal_Flow = find(dateTimeFlow<=finalTime,1,'last'); + indexFinal_He = find(dateTimeHe<=finalTime,1,'last'); + indexFinal_CO2 = find(dateTimeCO2<=finalTime,1,'last'); + + % Reconciled data (without interpolation) + % The whole reconciliation assumes that the MS is running after the + % flow meters to avoid any issues with interpolation!!! + % Meters and the controllers + reconciledData.raw.dateTimeFlow = dateTimeFlow(indexInitial_Flow:end); + reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); + reconciledData.raw.volFlow_CO2 = volFlow_CO2(indexInitial_Flow:end); + % MS + reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_He:end); + reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_CO2:end); + reconciledData.raw.signalHe = str2num(cell2mat(rawMSData{1,6}(indexInitial_He:end))); + reconciledData.raw.signalCO2 = str2num(cell2mat(rawMSData{1,3}(indexInitial_CO2:end))); + + % Reconciled data (with interpolation) + % Meters and the controllers + reconciledData.flow(:,1) = seconds(reconciledData.raw.dateTimeFlow... + -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] + reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] + reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] + + % MS + rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... + - reconciledData.raw.dateTimeMS_He(1)); % Time elapsed He [s] + rawTimeElapsedCO2 = seconds(reconciledData.raw.dateTimeMS_CO2 ... + - reconciledData.raw.dateTimeMS_CO2(1)); % Time elapsed CO2 [s] + % Interpolate the MS signal at the times of flow meter/controller + reconciledData.MS(:,1) = reconciledData.flow(:,1); % Use the time of the flow meter [s] + reconciledData.MS(:,2) = interp1(rawTimeElapsedHe,reconciledData.raw.signalHe,... + reconciledData.MS(:,1)); % Interpoloted MS signal He [-] + reconciledData.MS(:,3) = interp1(rawTimeElapsedCO2,reconciledData.raw.signalCO2,... + reconciledData.MS(:,1)); % Interpoloted MS signal CO2 [-] + + % Compute the mole fractions using the reconciled flow data + reconciledData.moleFrac(:,1) = (reconciledData.flow(:,2))./(reconciledData.flow(:,2)+reconciledData.flow(:,3)); + reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); +end \ No newline at end of file From 0cf1b9719ab82665488f2d309739eba5e608daca Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 18 Mar 2021 14:43:29 +0000 Subject: [PATCH 028/189] Fix for inlet concentration --- experimental/simulateDeadVolume.py | 33 ++++++++---------------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 5d266f6..3ff143e 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -13,6 +13,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-03-18, AK: Fix for inlet concentration # - 2021-03-17, AK: Initial creation # # Input arguments: @@ -52,17 +53,7 @@ def simulateDeadVolume(**kwargs): if 'numberOfTanks' in kwargs: numberOfTanks = kwargs["numberOfTanks"] else: - numberOfTanks = 2 - # 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] - if 'temperature' in kwargs: - temperature = np.array(kwargs["temperature"]); - else: - temperature = np.array([298.15]); + numberOfTanks = 1 # Feed Mole Fraction [-] if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) @@ -74,17 +65,13 @@ def simulateDeadVolume(**kwargs): else: timeInt = (0.0,20) - # Gas constant - Rg = 8.314; # [J/mol K] - # Prepare tuple of input parameters for the ode solver - inputParameters = (flowRate, deadVolume, numberOfTanks, pressureTotal, temperature) + inputParameters = (flowRate, deadVolume, numberOfTanks, feedMoleFrac) # Prepare initial conditions vector # The first element is the inlet composition and the rest is the dead # volume - initialConditions = np.concatenate((feedMoleFrac, - np.ones([numberOfTanks])*(1-feedMoleFrac))) + initialConditions = np.ones([numberOfTanks])*(1-feedMoleFrac) # Solve the system of equations outputSol = solve_ivp(solveTanksInSeries, timeInt, initialConditions, method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],0.1), @@ -110,14 +97,11 @@ def simulateDeadVolume(**kwargs): def solveTanksInSeries(t, f, *inputParameters): import numpy as np - # Gas constant - Rg = 8.314; # [J/mol K] - # Unpack the tuple of input parameters used to solve equations - flowRate, deadVolume , numberOfTanks, pressureTotal, temperature = inputParameters + flowRate, deadVolume , numberOfTanks, feedMoleFrac = inputParameters # Initialize the derivatives to zero - df = np.zeros([numberOfTanks+1]) + df = np.zeros([numberOfTanks]) # Volume of each tank volumeOfTank = deadVolume/numberOfTanks @@ -126,8 +110,9 @@ def solveTanksInSeries(t, f, *inputParameters): residenceTime = volumeOfTank/flowRate # Solve the ode - df[1:numberOfTanks+1] = ((1/residenceTime) - *(f[0:numberOfTanks] - f[1:numberOfTanks+1])) + df[0] = ((1/residenceTime)*(feedMoleFrac - f[0])) + df[1:numberOfTanks] = ((1/residenceTime) + *(f[0:numberOfTanks-1] - f[1:numberOfTanks])) # Return the derivatives for the solver return df From 7b9f7e2c6c68e73acf69282a4441ae429c8e3f44 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:31:42 +0000 Subject: [PATCH 029/189] Fix variable names and add experiment analysis mode --- experimental/analyseCalibration.m | 37 ++++++++++++++-------------- experimental/reconcileData.m | 41 +++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/experimental/analyseCalibration.m b/experimental/analyseCalibration.m index de1478b..3763ed4 100644 --- a/experimental/analyseCalibration.m +++ b/experimental/analyseCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-18, AK: Fix variable names % - 2021-03-17, AK: Change structure % - 2021-03-17, AK: Initial creation % @@ -58,11 +59,11 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) % Calibrate the meters % MFC - calibration.MFC_He = volFlow_MFC1_He'\volFlow_UMFM_He'; - calibration.MFC_CO2 = volFlow_MFC1_CO2'\volFlow_UMFM_CO2'; + calibrationFlow.MFC_He = volFlow_MFC1_He'\volFlow_UMFM_He'; + calibrationFlow.MFC_CO2 = volFlow_MFC1_CO2'\volFlow_UMFM_CO2'; % MFM - calibration.MFM_He = volFlow_MFM_He'\volFlow_UMFM_He'; - calibration.MFM_CO2 = volFlow_MFM_CO2'\volFlow_UMFM_CO2'; + calibrationFlow.MFM_He = volFlow_MFM_He'\volFlow_UMFM_He'; + calibrationFlow.MFM_CO2 = volFlow_MFM_CO2'\volFlow_UMFM_CO2'; % Save the calibration data into a .mat file % Check if calibration data folder exists @@ -70,14 +71,14 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) 'calibrationData'],'dir') == 7 % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibration',... + 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibrationFlow',... 'gitCommitID'); else % Create the calibration data folder if it does not exist mkdir(['experimentalData',filesep,'calibrationData']) % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibration',... + 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibrationFlow',... 'gitCommitID'); end @@ -87,19 +88,19 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) subplot(2,2,1) hold on scatter(volFlow_MFC1_He,volFlow_UMFM_He,'or') - plot(MFC1Set,calibration.MFC_He*MFC1Set,'b') + plot(MFC1Set,calibrationFlow.MFC_He*MFC1Set,'b') subplot(2,2,2) hold on scatter(volFlow_MFC1_CO2,volFlow_UMFM_CO2,'or') - plot(MFC1Set,calibration.MFC_CO2*MFC1Set,'b') + plot(MFC1Set,calibrationFlow.MFC_CO2*MFC1Set,'b') subplot(2,2,3) hold on scatter(volFlow_MFM_He,volFlow_UMFM_He,'or') - plot(MFC1Set,calibration.MFM_He*MFC1Set,'b') + plot(MFC1Set,calibrationFlow.MFM_He*MFC1Set,'b') subplot(2,2,4) hold on scatter(volFlow_MFM_CO2,volFlow_UMFM_CO2,'or') - plot(MFC1Set,calibration.MFM_CO2*MFC1Set,'b') + plot(MFC1Set,calibrationFlow.MFM_CO2*MFC1Set,'b') end % Load the file that contains the MS calibration if ~isempty(fileToLoadMS) @@ -108,8 +109,8 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) % Fit a polynomial function to get the model for MS % Fitting a 3rd order polynomial (check before accepting this) - calibration.He = polyfit(reconciledData.moleFrac(:,1),reconciledData.MS(:,2),3); % He - calibration.CO2 = polyfit(reconciledData.moleFrac(:,2),reconciledData.MS(:,3),3); % Co2 + calibrationMS.He = polyfit(reconciledData.MS(:,2),reconciledData.moleFrac(:,1),3); % He + calibrationMS.CO2 = polyfit(reconciledData.MS(:,3),reconciledData.moleFrac(:,2),3); % Co2 % Save the calibration data into a .mat file % Check if calibration data folder exists @@ -117,14 +118,14 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) 'calibrationData'],'dir') == 7 % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibration',... + 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibrationMS',... 'gitCommitID'); else % Create the calibration data folder if it does not exist mkdir(['experimentalData',filesep,'calibrationData']) % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibration',... + 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibrationMS',... 'gitCommitID'); end @@ -132,14 +133,14 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) figure % He subplot(1,2,1) - plot(0:0.01:1,polyval(calibration.He,0:0.01:1)) + plot(1e-13:1e-13:1e-8,polyval(calibrationMS.He,1e-13:1e-13:1e-8)) hold on - plot(reconciledData.moleFrac(:,1),reconciledData.MS(:,2),'or') + plot(reconciledData.MS(:,2),reconciledData.moleFrac(:,1),'or') % CO2 subplot(1,2,2) - plot(0:0.01:1,polyval(calibration.CO2,0:0.01:1)) + plot(1e-13:1e-13:1e-8,polyval(calibrationMS.CO2,1e-13:1e-13:1e-8)) hold on - plot(reconciledData.moleFrac(:,2),reconciledData.MS(:,3),'or') + plot(reconciledData.MS(:,3),reconciledData.moleFrac(:,2),'or') end end \ No newline at end of file diff --git a/experimental/reconcileData.m b/experimental/reconcileData.m index d214d0c..7c0f141 100644 --- a/experimental/reconcileData.m +++ b/experimental/reconcileData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-18, AK: Add experiment analysis mode % - 2021-03-17, AK: Initial creation % % Input arguments: @@ -23,11 +24,11 @@ function reconciledData = reconcileData(fileToLoad) % Load flow data flowMS = load(fileToLoad.flow); - if ~isfield(fileToLoad,'calibration') - error('You gotta calibrate your flow meters fisrst!! Or check the file name of the flow calibration!!') + if ~isfield(fileToLoad,'calibrationFlow') + error('You gotta calibrate your flow meters first!! Or check the file name of the flow calibration!!') end % Flow Calibration File - calibrationMeters = load(fileToLoad.calibration); + load(fileToLoad.calibrationFlow); % Analyse flow data MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 @@ -37,8 +38,17 @@ volFlow_MFC1 = [MFC1.volFlow]; % He volFlow_MFC2 = [MFC2.volFlow]; % CO2 % Apply the calibration for the flows - volFlow_He = volFlow_MFC1*calibrationMeters.calibration.MFC_He; - volFlow_CO2 = volFlow_MFC2*calibrationMeters.calibration.MFC_CO2; + volFlow_He = volFlow_MFC1*calibrationFlow.MFC_He; + % For calibration both MFCs are present + % Done right now to check if calibration of MS is preesnt or not + if ~isfield(fileToLoad,'calibrationMS') + volFlow_CO2 = volFlow_MFC2*calibrationFlow.MFC_CO2; + % For actual measurements, one MFC and one MFM present + % NOTE: Here MFC2 = MFM!!!!! + else + % Flow is converted assuming helium calibration for MFM + volFlow_CO2 = volFlow_MFC2*calibrationFlow.MFM_He; + end % Load MS Ascii data % Create file identifier fileId = fopen(fileToLoad.MS); @@ -67,8 +77,8 @@ indexFinal_CO2 = find(dateTimeCO2<=finalTime,1,'last'); % Reconciled data (without interpolation) - % The whole reconciliation assumes that the MS is running after the - % flow meters to avoid any issues with interpolation!!! + % NOTE: The whole reconciliation assumes that the MS is running after # + % the flow meters to avoid any issues with interpolation!!! % Meters and the controllers reconciledData.raw.dateTimeFlow = dateTimeFlow(indexInitial_Flow:end); reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); @@ -98,7 +108,18 @@ reconciledData.MS(:,3) = interp1(rawTimeElapsedCO2,reconciledData.raw.signalCO2,... reconciledData.MS(:,1)); % Interpoloted MS signal CO2 [-] - % Compute the mole fractions using the reconciled flow data - reconciledData.moleFrac(:,1) = (reconciledData.flow(:,2))./(reconciledData.flow(:,2)+reconciledData.flow(:,3)); - reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); + % Get the mole fraction used for the calibration + % This will be used in the analyzeCalibration script + if ~isfield(fileToLoad,'calibrationMS') + % Compute the mole fractions using the reconciled flow data + reconciledData.moleFrac(:,1) = (reconciledData.flow(:,2))./(reconciledData.flow(:,2)+reconciledData.flow(:,3)); + reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); + % If actual experiment is analyzed, loads the calibration MS file + else + % MS Calibration File + load(fileToLoad.calibrationMS); + % Convert the raw signal to concentration + reconciledData.moleFrac(:,1) = polyval(calibrationMS.He,reconciledData.MS(:,2)); % He [-] + reconciledData.moleFrac(:,2) = polyval(calibrationMS.CO2,reconciledData.MS(:,3)); % CO2 [-] + end end \ No newline at end of file From b3a50e9261ae0c43d443c2e31c2b032b5e2ff994 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:34:44 +0000 Subject: [PATCH 030/189] Change two file names --- experimental/{analyseCalibration.m => analyzeCalibration.m} | 0 experimental/{reconcileData.m => concatenateData.m} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename experimental/{analyseCalibration.m => analyzeCalibration.m} (100%) rename experimental/{reconcileData.m => concatenateData.m} (100%) diff --git a/experimental/analyseCalibration.m b/experimental/analyzeCalibration.m similarity index 100% rename from experimental/analyseCalibration.m rename to experimental/analyzeCalibration.m diff --git a/experimental/reconcileData.m b/experimental/concatenateData.m similarity index 100% rename from experimental/reconcileData.m rename to experimental/concatenateData.m From 09687c24b41ebaf807f4b9dbe86e402198790f26 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:35:31 +0000 Subject: [PATCH 031/189] Fixes for file name change --- experimental/analyzeCalibration.m | 2 +- experimental/concatenateData.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index 3763ed4..bff5dbf 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -105,7 +105,7 @@ function analyseCalibration(fileToLoadMeter,fileToLoadMS) % Load the file that contains the MS calibration if ~isempty(fileToLoadMS) % Call reconcileData function for calibration of the MS - reconciledData = reconcileData(fileToLoadMS); + reconciledData = concatenateData(fileToLoadMS); % Fit a polynomial function to get the model for MS % Fitting a 3rd order polynomial (check before accepting this) diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index 7c0f141..08129c3 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -21,7 +21,7 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function reconciledData = reconcileData(fileToLoad) +function reconciledData = concatenateData(fileToLoad) % Load flow data flowMS = load(fileToLoad.flow); if ~isfield(fileToLoad,'calibrationFlow') From 88fdf782c03f19d7daa2a6ec8c989053f621a55e Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 18 Mar 2021 18:20:25 +0000 Subject: [PATCH 032/189] Bug fixes --- experimental/analyzeCalibration.m | 4 +- experimental/concatenateData.m | 86 +++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index bff5dbf..264abf5 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -22,9 +22,9 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function analyseCalibration(fileToLoadMeter,fileToLoadMS) +function analyzeCalibration(fileToLoadMeter,fileToLoadMS) % Find the directory of the file and move to the top folder -filePath = which('analyseCalibration'); +filePath = which('analyzeCalibration'); cd(filePath(1:end-21)); % Get the git commit ID diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index 08129c3..01ad69e 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-18, AK: Add interpolation based on MS or flow meter % - 2021-03-18, AK: Add experiment analysis mode % - 2021-03-17, AK: Initial creation % @@ -31,7 +32,12 @@ load(fileToLoad.calibrationFlow); % Analyse flow data MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He - MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 + % Done right now to check if calibration of MS is preesnt or not + if ~isfield(fileToLoad,'calibrationMS') + MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 + else + MFC2 = [flowMS.outputStruct.MFM]; % MFC2 - CO2 + end % Get the datetime and volumetric flow rate dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... 'InputFormat','yyyyMMdd_HHmmss'); @@ -68,13 +74,15 @@ % Find index corresponding to initial time for meters and MS indexInitial_Flow = find(dateTimeFlow>=initialTime,1,'first'); - indexInitial_He = find(dateTimeHe>=initialTime,1,'first'); - indexInitial_CO2 = find(dateTimeCO2>=initialTime,1,'first'); - + + indexInitial_MS = max([find(dateTimeHe>=initialTime,1,'first'),... + find(dateTimeCO2>=initialTime,1,'first')]); + % Find index corresponding to final time for meters and MS indexFinal_Flow = find(dateTimeFlow<=finalTime,1,'last'); - indexFinal_He = find(dateTimeHe<=finalTime,1,'last'); - indexFinal_CO2 = find(dateTimeCO2<=finalTime,1,'last'); + indexFinal_MS = min([find(dateTimeHe<=finalTime,1,'last'),... + find(dateTimeCO2<=finalTime,1,'last')]); + % Reconciled data (without interpolation) % NOTE: The whole reconciliation assumes that the MS is running after # @@ -84,30 +92,54 @@ reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); reconciledData.raw.volFlow_CO2 = volFlow_CO2(indexInitial_Flow:end); % MS - reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_He:end); - reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_CO2:end); - reconciledData.raw.signalHe = str2num(cell2mat(rawMSData{1,6}(indexInitial_He:end))); - reconciledData.raw.signalCO2 = str2num(cell2mat(rawMSData{1,3}(indexInitial_CO2:end))); + reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_MS:end); + reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_MS:end); + reconciledData.raw.signalHe = str2num(cell2mat(rawMSData{1,6}(indexInitial_MS:end))); + reconciledData.raw.signalCO2 = str2num(cell2mat(rawMSData{1,3}(indexInitial_MS:end))); % Reconciled data (with interpolation) - % Meters and the controllers - reconciledData.flow(:,1) = seconds(reconciledData.raw.dateTimeFlow... - -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] - reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] - reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] + % Interpolate based on flow + if fileToLoad.interpMS + % Meters and the controllers + reconciledData.flow(:,1) = seconds(reconciledData.raw.dateTimeFlow... + -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] + reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] + reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] - % MS - rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... - - reconciledData.raw.dateTimeMS_He(1)); % Time elapsed He [s] - rawTimeElapsedCO2 = seconds(reconciledData.raw.dateTimeMS_CO2 ... - - reconciledData.raw.dateTimeMS_CO2(1)); % Time elapsed CO2 [s] - % Interpolate the MS signal at the times of flow meter/controller - reconciledData.MS(:,1) = reconciledData.flow(:,1); % Use the time of the flow meter [s] - reconciledData.MS(:,2) = interp1(rawTimeElapsedHe,reconciledData.raw.signalHe,... - reconciledData.MS(:,1)); % Interpoloted MS signal He [-] - reconciledData.MS(:,3) = interp1(rawTimeElapsedCO2,reconciledData.raw.signalCO2,... - reconciledData.MS(:,1)); % Interpoloted MS signal CO2 [-] - + % MS + rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... + - reconciledData.raw.dateTimeMS_He(1)); % Time elapsed He [s] + rawTimeElapsedCO2 = seconds(reconciledData.raw.dateTimeMS_CO2 ... + - reconciledData.raw.dateTimeMS_CO2(1)); % Time elapsed CO2 [s] + % Interpolate the MS signal at the times of flow meter/controller + reconciledData.MS(:,1) = reconciledData.flow(:,1); % Use the time of the flow meter [s] + reconciledData.MS(:,2) = interp1(rawTimeElapsedHe,reconciledData.raw.signalHe,... + reconciledData.MS(:,1)); % Interpoloted MS signal He [-] + reconciledData.MS(:,3) = interp1(rawTimeElapsedCO2,reconciledData.raw.signalCO2,... + reconciledData.MS(:,1)); % Interpoloted MS signal CO2 [-] + % Interpolate based on MS + else + % MS + rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... + - reconciledData.raw.dateTimeMS_He(1)); % Time elapsed He [s] + rawTimeElapsedCO2 = seconds(reconciledData.raw.dateTimeMS_CO2 ... + - reconciledData.raw.dateTimeMS_CO2(1)); % Time elapsed CO2 [s] + % Interpolate the MS signal at the times of flow meter/controller + reconciledData.MS(:,1) = rawTimeElapsedHe; % Use the time of He [s] + reconciledData.MS(:,2) = reconciledData.raw.signalHe; % Raw signal He [-] + reconciledData.MS(:,3) = interp1(rawTimeElapsedCO2,reconciledData.raw.signalCO2,... + reconciledData.MS(:,1)); % Interpoloted MS signal CO2 based on He time [-] + + % Meters and the controllers + rawTimeElapsedFlow = seconds(reconciledData.raw.dateTimeFlow... + -reconciledData.raw.dateTimeFlow(1)); + reconciledData.flow(:,1) = reconciledData.MS(:,1); % Time elapsed of MS [s] + reconciledData.flow(:,2) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_He,... + reconciledData.MS(:,1)); % Interpoloted He Flow [ccm] + reconciledData.flow(:,3) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_CO2,... + reconciledData.MS(:,1)); % Interpoloted CO2 flow [ccm] + end + % Get the mole fraction used for the calibration % This will be used in the analyzeCalibration script if ~isfield(fileToLoad,'calibrationMS') From f11a0c71447cb9a8a26c2dd3bbcc6b58e6b4f09b Mon Sep 17 00:00:00 2001 From: ha3215 Date: Fri, 19 Mar 2021 19:57:52 +0000 Subject: [PATCH 033/189] Added kmeans calculation to obtain mean ion current for polynomial fitting and plotting --- experimental/analyzeCalibration.m | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index 264abf5..1c51e38 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,8 @@ % % % Last modified: +% - 2021-03-19, HA: Added kmeans calculation to obtain mean ion current for +% polynomial fitting % - 2021-03-18, AK: Fix variable names % - 2021-03-17, AK: Change structure % - 2021-03-17, AK: Initial creation @@ -106,11 +108,19 @@ function analyzeCalibration(fileToLoadMeter,fileToLoadMS) if ~isempty(fileToLoadMS) % Call reconcileData function for calibration of the MS reconciledData = concatenateData(fileToLoadMS); - + % Find the mean values of Ion current at concentration steps and + % corresponding indices + [indicesHe, meansHe]=kmeans(reconciledData.MS(:,2),6); + [indicesCO2, meansCO2]=kmeans(reconciledData.MS(:,3),6); + % create new ion current array using mean values + for kk = 1:length(meansHe) + correctedMS_He(find(indicesHe==kk)) = meansHe(kk); + correctedMS_CO2(find(indicesCO2==kk)) = meansCO2(kk); + end % Fit a polynomial function to get the model for MS % Fitting a 3rd order polynomial (check before accepting this) - calibrationMS.He = polyfit(reconciledData.MS(:,2),reconciledData.moleFrac(:,1),3); % He - calibrationMS.CO2 = polyfit(reconciledData.MS(:,3),reconciledData.moleFrac(:,2),3); % Co2 + calibrationMS.He = polyfit(correctedMS_He,reconciledData.moleFrac(:,1),3); % He + calibrationMS.CO2 = polyfit(correctedMS_CO2,reconciledData.moleFrac(:,2),3); % Co2 % Save the calibration data into a .mat file % Check if calibration data folder exists @@ -135,12 +145,14 @@ function analyzeCalibration(fileToLoadMeter,fileToLoadMS) subplot(1,2,1) plot(1e-13:1e-13:1e-8,polyval(calibrationMS.He,1e-13:1e-13:1e-8)) hold on - plot(reconciledData.MS(:,2),reconciledData.moleFrac(:,1),'or') + plot(correctedMS_He,reconciledData.moleFrac(:,1),'or') + xlim([0 2e-9]); % CO2 subplot(1,2,2) plot(1e-13:1e-13:1e-8,polyval(calibrationMS.CO2,1e-13:1e-13:1e-8)) hold on - plot(reconciledData.MS(:,3),reconciledData.moleFrac(:,2),'or') + plot(correctedMS_CO2,reconciledData.moleFrac(:,2),'or') + xlim([0 3.7e-9]); end end \ No newline at end of file From 202015c6e44a79729b4af6caea0d2a3ff3ef2347 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 22 Mar 2021 18:16:27 +0000 Subject: [PATCH 034/189] Add checks for MS concatenation --- experimental/calibrateMS.m | 2 +- experimental/concatenateData.m | 33 +++++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m index bd17748..6459857 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrateMS.m @@ -24,7 +24,7 @@ expInfo.expName = ['ZLCCalibrateMS','_',... datestr(datetime('now'),'yyyymmdd')]; % Maximum time of the experiment - expInfo.maxTime = 240; + expInfo.maxTime = 300; % Sampling time for the device expInfo.samplingTime = 2; % Define gas for MFM diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index 01ad69e..f27e1f4 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-22, AK: Add checks for MS concatenation % - 2021-03-18, AK: Add interpolation based on MS or flow meter % - 2021-03-18, AK: Add experiment analysis mode % - 2021-03-17, AK: Initial creation @@ -36,7 +37,7 @@ if ~isfield(fileToLoad,'calibrationMS') MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 else - MFC2 = [flowMS.outputStruct.MFM]; % MFC2 - CO2 + MFC2 = [flowMS.outputStruct.MFM]; % MFM - CO2 end % Get the datetime and volumetric flow rate dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... @@ -92,11 +93,31 @@ reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); reconciledData.raw.volFlow_CO2 = volFlow_CO2(indexInitial_Flow:end); % MS - reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_MS:end); - reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_MS:end); - reconciledData.raw.signalHe = str2num(cell2mat(rawMSData{1,6}(indexInitial_MS:end))); - reconciledData.raw.signalCO2 = str2num(cell2mat(rawMSData{1,3}(indexInitial_MS:end))); - + % Find the index of the last entry (from one of the two gases) + concantenateLastInd = min([size(dateTimeHe(indexInitial_MS:end),1), ... + size(dateTimeHe(indexInitial_MS:end),1)]); + reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_MS:concantenateLastInd); + reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_MS:concantenateLastInd); + % Check if any element is negative for concatenation + for ii=1:length(indexInitial_MS:concantenateLastInd) + % He + % If negative element, initialize to eps + if str2num(cell2mat(rawMSData{1,6}(ii))) < 0 + reconciledData.raw.signalHe(ii) = eps; + % If not, use the actual value + else + reconciledData.raw.signalHe(ii) = str2num(cell2mat(rawMSData{1,6}(ii))); + end + % CO2 + % If negative element, initialize to eps + if str2num(cell2mat(rawMSData{1,3}(ii))) < 0 + reconciledData.raw.signalCO2(ii) = eps; + % If not, use the actual value + else + reconciledData.raw.signalCO2(ii) = str2num(cell2mat(rawMSData{1,3}(ii))); + end + end + % Reconciled data (with interpolation) % Interpolate based on flow if fileToLoad.interpMS From 6c5a52f4048ff0b507b661a62734447a82a4586f Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Tue, 23 Mar 2021 15:44:15 +0000 Subject: [PATCH 035/189] Bug fix --- experimental/concatenateData.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index f27e1f4..9c649dc 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -99,22 +99,22 @@ reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_MS:concantenateLastInd); reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_MS:concantenateLastInd); % Check if any element is negative for concatenation - for ii=1:length(indexInitial_MS:concantenateLastInd) + for ii=indexInitial_MS:concantenateLastInd % He % If negative element, initialize to eps if str2num(cell2mat(rawMSData{1,6}(ii))) < 0 - reconciledData.raw.signalHe(ii) = eps; + reconciledData.raw.signalHe(ii-indexInitial_MS+1) = eps; % If not, use the actual value else - reconciledData.raw.signalHe(ii) = str2num(cell2mat(rawMSData{1,6}(ii))); + reconciledData.raw.signalHe(ii-indexInitial_MS+1) = str2num(cell2mat(rawMSData{1,6}(ii))); end % CO2 % If negative element, initialize to eps if str2num(cell2mat(rawMSData{1,3}(ii))) < 0 - reconciledData.raw.signalCO2(ii) = eps; + reconciledData.raw.signalCO2(ii-indexInitial_MS+1) = eps; % If not, use the actual value else - reconciledData.raw.signalCO2(ii) = str2num(cell2mat(rawMSData{1,3}(ii))); + reconciledData.raw.signalCO2(ii-indexInitial_MS+1) = str2num(cell2mat(rawMSData{1,3}(ii))); end end From 7abc27a9269f3b095079ae94f300a620f9b87d38 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 24 Mar 2021 15:32:20 +0000 Subject: [PATCH 036/189] First functional calibration and analysis script (major changes) --- experimental/analyzeCalibration.m | 68 +++++++++++++++++++------------ experimental/analyzeExperiment.m | 47 +++++++++++++++++++++ experimental/concatenateData.m | 11 ++++- experimental/runZLC.m | 8 +--- 4 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 experimental/analyzeExperiment.m diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index 1c51e38..ebd38e0 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-24, AK: Remove k-means and replace with averaging of n points % - 2021-03-19, HA: Added kmeans calculation to obtain mean ion current for % polynomial fitting % - 2021-03-18, AK: Fix variable names @@ -24,7 +25,7 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function analyzeCalibration(fileToLoadMeter,fileToLoadMS) +function analyzeCalibration(parametersFlow,parametersMS) % Find the directory of the file and move to the top folder filePath = which('analyzeCalibration'); cd(filePath(1:end-21)); @@ -33,8 +34,8 @@ function analyzeCalibration(fileToLoadMeter,fileToLoadMS) gitCommitID = getGitCommit; % Load the file that contains the flow meter calibration -if ~isempty(fileToLoadMeter) - flowData = load(fileToLoadMeter); +if ~isempty(parametersFlow) + flowData = load(parametersFlow); % Analyse flow data MFM = [flowData.outputStruct.MFM]; % MFM MFC1 = [flowData.outputStruct.MFC1]; % MFC1 @@ -73,14 +74,14 @@ function analyzeCalibration(fileToLoadMeter,fileToLoadMS) 'calibrationData'],'dir') == 7 % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibrationFlow',... + 'calibrationData',filesep,parametersFlow,'_Model'],'calibrationFlow',... 'gitCommitID'); else % Create the calibration data folder if it does not exist mkdir(['experimentalData',filesep,'calibrationData']) % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMeter,'_Model'],'calibrationFlow',... + 'calibrationData',filesep,parametersFlow,'_Model'],'calibrationFlow',... 'gitCommitID'); end @@ -105,22 +106,35 @@ function analyzeCalibration(fileToLoadMeter,fileToLoadMS) plot(MFC1Set,calibrationFlow.MFM_CO2*MFC1Set,'b') end % Load the file that contains the MS calibration -if ~isempty(fileToLoadMS) +if ~isempty(parametersMS) % Call reconcileData function for calibration of the MS - reconciledData = concatenateData(fileToLoadMS); - % Find the mean values of Ion current at concentration steps and - % corresponding indices - [indicesHe, meansHe]=kmeans(reconciledData.MS(:,2),6); - [indicesCO2, meansCO2]=kmeans(reconciledData.MS(:,3),6); - % create new ion current array using mean values - for kk = 1:length(meansHe) - correctedMS_He(find(indicesHe==kk)) = meansHe(kk); - correctedMS_CO2(find(indicesCO2==kk)) = meansCO2(kk); + reconciledData = concatenateData(parametersMS); + % Find the index that corresponds to the last time for a given set + % point + setPtMFC = unique(reconciledData.flow(:,4)); + % Find indices that corresponds to a given set point + indList = ones(length(setPtMFC),2); + % Loop over all the set points + for ii=1:length(setPtMFC) + % Indices for a given set point + indList(ii,1) = find(reconciledData.flow(:,4)==setPtMFC(ii),1,'first'); + indList(ii,2) = find(reconciledData.flow(:,4)==setPtMFC(ii),1,'last'); + % Find the mean value of the signal for numMean number of points + % for each set point + indMean = find(reconciledData.flow(:,4)==setPtMFC(ii),... + parametersMS.numMean,'last'); + % MS Signal mean + meanHeSignal(ii) = mean(reconciledData.MS(indMean(1):indMean(end),2)); % He + meanCO2Signal(ii) = mean(reconciledData.MS(indMean(1):indMean(end),3)); % CO2 + % Mole fraction mean + meanMoleFrac(ii,1) = mean(reconciledData.moleFrac(indMean(1):indMean(end),1)); % He + meanMoleFrac(ii,2) = mean(reconciledData.moleFrac(indMean(1):indMean(end),2)); % CO2 end + % Fit a polynomial function to get the model for MS % Fitting a 3rd order polynomial (check before accepting this) - calibrationMS.He = polyfit(correctedMS_He,reconciledData.moleFrac(:,1),3); % He - calibrationMS.CO2 = polyfit(correctedMS_CO2,reconciledData.moleFrac(:,2),3); % Co2 + calibrationMS.He = polyfit(meanHeSignal,meanMoleFrac(:,1),parametersMS.polyDeg); % He + calibrationMS.CO2 = polyfit(meanCO2Signal,meanMoleFrac(:,2),parametersMS.polyDeg); % COo2 % Save the calibration data into a .mat file % Check if calibration data folder exists @@ -128,31 +142,33 @@ function analyzeCalibration(fileToLoadMeter,fileToLoadMS) 'calibrationData'],'dir') == 7 % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibrationMS',... + 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... 'gitCommitID'); else % Create the calibration data folder if it does not exist mkdir(['experimentalData',filesep,'calibrationData']) % Save the calibration data for further use save(['experimentalData',filesep,... - 'calibrationData',filesep,fileToLoadMS.flow,'_Model'],'calibrationMS',... + 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... 'gitCommitID'); end % Plot the raw and the calibrated data - figure + figure(1) % He subplot(1,2,1) - plot(1e-13:1e-13:1e-8,polyval(calibrationMS.He,1e-13:1e-13:1e-8)) hold on - plot(correctedMS_He,reconciledData.moleFrac(:,1),'or') - xlim([0 2e-9]); + plot(1e-13:1e-13:1e-8,polyval(calibrationMS.He,1e-13:1e-13:1e-8)) + scatter(meanHeSignal,meanMoleFrac(:,1)) + xlim([0 1.1*max(meanHeSignal)]); + ylim([0 1]); % CO2 subplot(1,2,2) - plot(1e-13:1e-13:1e-8,polyval(calibrationMS.CO2,1e-13:1e-13:1e-8)) hold on - plot(correctedMS_CO2,reconciledData.moleFrac(:,2),'or') - xlim([0 3.7e-9]); + plot(1e-13:1e-13:1e-8,polyval(calibrationMS.CO2,1e-13:1e-13:1e-8)) + scatter(meanCO2Signal,meanMoleFrac(:,2)) + xlim([0 1.1*max(meanCO2Signal)]); + ylim([0 1]); end end \ No newline at end of file diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m new file mode 100644 index 0000000..2fb8607 --- /dev/null +++ b/experimental/analyzeExperiment.m @@ -0,0 +1,47 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% Hassan Azzan (HA) +% +% Purpose: +% Script to define inputs to calibrate flowmeter and MS or to analyze a +% real experiment using calibrated flow meters and MS +% +% Last modified: +% - 2021-03-18, AK: Updates to structure +% - 2021-03-18, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +flagCalibration = false; % Flag to decide calibration or analysis + +% Mode to switch between calibration and analyzing real experiment +if flagCalibration + experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) + experimentStruct.flow = 'ZLCCalibrateMS_20210324'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210324.asc'; % Experimental MS file (.asc) + experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) + experimentStruct.numMean = 10; % Number of points for averaging + experimentStruct.polyDeg = 3; % Degree of polynomial fit + % Call reconcileData function for calibration of the MS + analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file +else + experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) + experimentStruct.flow = 'ZLCCalibrateMS_Short_20210324'; % Experimental flow file (.mat) + experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210324_Model'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_Short_20210324.asc'; % Experimental MS file (.asc) + experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) + % Call reconcileData function to get the output mole fraction for a + % real experiment + outputStruct = concatenateData(experimentStruct); +end \ No newline at end of file diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index 9c649dc..c970ce8 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-22, AK: Bug fixes for finding indices % - 2021-03-22, AK: Add checks for MS concatenation % - 2021-03-18, AK: Add interpolation based on MS or flow meter % - 2021-03-18, AK: Add experiment analysis mode @@ -43,6 +44,7 @@ dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... 'InputFormat','yyyyMMdd_HHmmss'); volFlow_MFC1 = [MFC1.volFlow]; % He + setPt_MFC1 = [MFC1.setpoint]; % He setpoint for calibration volFlow_MFC2 = [MFC2.volFlow]; % CO2 % Apply the calibration for the flows volFlow_He = volFlow_MFC1*calibrationFlow.MFC_He; @@ -92,10 +94,12 @@ reconciledData.raw.dateTimeFlow = dateTimeFlow(indexInitial_Flow:end); reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); reconciledData.raw.volFlow_CO2 = volFlow_CO2(indexInitial_Flow:end); + reconciledData.raw.setPt_He = setPt_MFC1(indexInitial_Flow:end); + % MS % Find the index of the last entry (from one of the two gases) - concantenateLastInd = min([size(dateTimeHe(indexInitial_MS:end),1), ... - size(dateTimeHe(indexInitial_MS:end),1)]); + concantenateLastInd = indexInitial_MS + min([size(dateTimeHe(indexInitial_MS:end),1), ... + size(dateTimeCO2(indexInitial_MS:end),1)]) - 1; reconciledData.raw.dateTimeMS_He = dateTimeHe(indexInitial_MS:concantenateLastInd); reconciledData.raw.dateTimeMS_CO2 = dateTimeCO2(indexInitial_MS:concantenateLastInd); % Check if any element is negative for concatenation @@ -126,6 +130,7 @@ -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] + reconciledData.flow(:,4) = reconciledData.raw.setPt_He; % He set point [ccm] % MS rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... @@ -159,6 +164,8 @@ reconciledData.MS(:,1)); % Interpoloted He Flow [ccm] reconciledData.flow(:,3) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_CO2,... reconciledData.MS(:,1)); % Interpoloted CO2 flow [ccm] + reconciledData.flow(:,4) = interp1(rawTimeElapsedFlow,reconciledData.raw.setPt_He,... + reconciledData.MS(:,1)); % Interpoloted He setpoint[ccm] end % Get the mole fraction used for the calibration diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 330c229..2836745 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-03-24, AK: Cosmetic changes % - 2021-03-16, AK: Add MFC2 and fix for MS calibration % - 2021-03-16, AK: Add valve switch times % - 2021-03-15, AK: Bug fixes @@ -178,13 +179,6 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) % Waiting for user to switch the valve promptUser = 'Switch asap! When you press Y, the gas switches (you wish)! [Y/N]: '; userInput = input(promptUser,'s'); - % This is recorded as the time of switch - % Empty readings (just for analysis purpose) - if strcmp(userInput,'Y') || strcmp(userInput,'y') - currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); - dataLogger(timerObj,expInfo,currentDateTime,[],... - [],[],[]); - end end % Get the sampling date/time currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); From 22a9536e7aafec7c5175b8437240102c3e780084 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:09:49 +0000 Subject: [PATCH 037/189] Add flow rate computation --- experimental/analyzeExperiment.m | 42 +++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 2fb8607..073ea83 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -14,6 +14,8 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-03-24, AK: Add flow rate computation and prepare structure for +% Python script % - 2021-03-18, AK: Updates to structure % - 2021-03-18, AK: Initial creation % @@ -36,12 +38,46 @@ % Call reconcileData function for calibration of the MS analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file else + setTotalFlowRate = 15; experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_Short_20210324'; % Experimental flow file (.mat) - experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210324_Model'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_Short_20210324.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLC_DeadVolume_Exp05A'; % Experimental flow file (.mat) + experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210323_Model'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_DeadVolume_Exp05A.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) % Call reconcileData function to get the output mole fraction for a % real experiment outputStruct = concatenateData(experimentStruct); + + % Clean mole fraction to remove negative values (due to calibration) + % Replace all negative molefraction with eps + outputStruct.moleFrac(outputStruct.moleFrac(:,1)<0,1)=eps; % He + outputStruct.moleFrac(:,2)=1-outputStruct.moleFrac(:,1); % Compute CO2 with mass balance + + % Convert the MFM flow to real flow + % Flow rate when there is pure He (He equivalent) + flowRateAtPureHe = setTotalFlowRate*calibrationFlow.MFC_He/calibrationFlow.MFM_He; + % Flow rate when there is pure CO2 (He equivalent) + flowRateAtPureCO2 = setTotalFlowRate*calibrationFlow.MFC_CO2/calibrationFlow.MFM_CO2; + % Compute the slope of the line (to relate mole fraction of CO2 to the + % change in the flow rate of He equivalent in MFM) + delFlowRate = flowRateAtPureHe - flowRateAtPureCO2; + % Compute the flow of CO2 (He equivalent) at the different mole + % fractions as the experiment progress (by doing a mass balance) + flowRateCO2 = outputStruct.moleFrac(:,2).*(flowRateAtPureHe-delFlowRate.*outputStruct.moleFrac(:,2)); + % Compute the real flow rate of CO2 (by performing a transformation + % from He equivalent CO2 to UMFM) [ccm] + realFlowRateCO2 = flowRateCO2*calibrationFlow.MFM_CO2; + % Compute the flow of He (He equivalent) at the different mole + % fractions as the experiment progress (by doing a mass balance) + flowRateHe = (1-outputStruct.moleFrac(:,2)).*(flowRateAtPureHe-delFlowRate.*outputStruct.moleFrac(:,2)); + % Compute the real flow rate of He (by performing a transformation + % from He equivalent He to UMFM) [ccm] + realFlowRateHe = flowRateHe*calibrationFlow.MFM_He; + % Compute the total flow rate of the gas [ccm] + totalFlowRate = realFlowRateHe+realFlowRateCO2; + + % Input for the ZLC script (Python) + experimentOutput.timeExp = outputStruct.flow(:,1); % Time elapsed [s] + experimentOutput.moleFrac = outputStruct.moleFrac(:,2); % Mole fraction CO2 [-] + experimentOutput.totalFlowRate = totalFlowRate./60; % Total flow rate of the gas [ccs] end \ No newline at end of file From 753bb553f058fb196b04a58d521968c9b7109489 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:14:33 +0000 Subject: [PATCH 038/189] Save data to .mat file --- experimental/analyzeExperiment.m | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 073ea83..f090a26 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -25,6 +25,9 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Get the git commit ID +gitCommitID = getGitCommit; + flagCalibration = false; % Flag to decide calibration or analysis % Mode to switch between calibration and analyzing real experiment @@ -54,6 +57,8 @@ outputStruct.moleFrac(:,2)=1-outputStruct.moleFrac(:,1); % Compute CO2 with mass balance % Convert the MFM flow to real flow + % Load the meter calibrations + load(experimentStruct.calibrationFlow); % Flow rate when there is pure He (He equivalent) flowRateAtPureHe = setTotalFlowRate*calibrationFlow.MFC_He/calibrationFlow.MFM_He; % Flow rate when there is pure CO2 (He equivalent) @@ -80,4 +85,20 @@ experimentOutput.timeExp = outputStruct.flow(:,1); % Time elapsed [s] experimentOutput.moleFrac = outputStruct.moleFrac(:,2); % Mole fraction CO2 [-] experimentOutput.totalFlowRate = totalFlowRate./60; % Total flow rate of the gas [ccs] + % Save the experimental output into a .mat file + % Check if runData data folder exists + if exist(['experimentalData',filesep,... + 'runData'],'dir') == 7 + % Save the calibration data for further use + save(['experimentalData',filesep,... + 'runData',filesep,experimentStruct.flow,'_Output'],'experimentOutput',... + 'gitCommitID'); + else + % Create the calibration data folder if it does not exist + mkdir(['experimentalData',filesep,'runData']) + % Save the calibration data for further use + save(['experimentalData',filesep,... + 'runData',filesep,experimentStruct.flow,'_Output'],'experimentOutput',... + 'gitCommitID'); + end end \ No newline at end of file From 9894aba6f90a90bbb9158cb103b489d5c9f7501f Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:16:47 +0000 Subject: [PATCH 039/189] Cosmetic change --- experimental/analyzeExperiment.m | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index f090a26..c4ec8be 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -85,6 +85,7 @@ experimentOutput.timeExp = outputStruct.flow(:,1); % Time elapsed [s] experimentOutput.moleFrac = outputStruct.moleFrac(:,2); % Mole fraction CO2 [-] experimentOutput.totalFlowRate = totalFlowRate./60; % Total flow rate of the gas [ccs] + experimentOutput.setTotalFlowRate = setTotalFlowRate/60; % Set point for total flow rate [ccs] % Save the experimental output into a .mat file % Check if runData data folder exists if exist(['experimentalData',filesep,... From 54d63ecdae65c325b84dd1e98bc883f25d7c1771 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 25 Mar 2021 10:59:19 +0000 Subject: [PATCH 040/189] Estimate dead volume parameters using experimental data --- experimental/extractDeadVolume.py | 34 ++++++++++++++++++++---------- experimental/simulateDeadVolume.py | 3 ++- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 2ee9ebc..5f49fa5 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -14,6 +14,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-03-25, AK: Estimate parameters using experimental data # - 2021-03-17, AK: Initial creation # # Input arguments: @@ -43,7 +44,7 @@ def extractDeadVolume(): currentDT = auxiliaryFunctions.getCurrentDateTime() # Define the bounds and the type of the parameters to be optimized - optBounds = np.array(([np.finfo(float).eps,100], [1,20])) + optBounds = np.array(([np.finfo(float).eps,100], [1,30])) optType=np.array(['real','int']) # Algorithm parameters for GA algorithm_param = {'max_num_iteration':25, @@ -80,18 +81,29 @@ def extractDeadVolume(): def deadVolObjectiveFunction(x): import numpy as np from simulateDeadVolume import simulateDeadVolume + from numpy import load + from scipy.interpolate import interp1d - # Dead volume [cc] - deadVolume = 3.25 - # Number of tanks [-] - numberOfTanks = 6 - # Generate dead volume response (pseudo experiment) - _ , _ , moleFracExp = simulateDeadVolume(deadVolume = deadVolume, - numberOfTanks = numberOfTanks) + # Directory of raw data + mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' + # File name of the experinent + fileName = 'ZLC_DeadVolume_Exp05A_Output_0cf1b97.npz' + # Path of the file name + fileToLoad = mainDir + fileName + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExp = load(fileToLoad)["moleFrac"].flatten() # Compute the dead volume response using the optimizer parameters - _ , _ , moleFracOut = simulateDeadVolume(deadVolume = x[0], - numberOfTanks = int(x[1])) + timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume = x[0], + numberOfTanks = int(x[1])) + + # Interpolate simulation data (generate function) + interpSim = interp1d(timeSimOut, moleFracOut) + + # Find the interpolated simulation mole fraction at times corresponding to + # the experimental ones + moleFracSim = interpSim(timeElapsedExp) # Compute the sum of the error for the difference between exp. and sim. - return np.sum(np.power(moleFracExp - moleFracOut,2)) \ No newline at end of file + return np.sum(np.power(moleFracExp - moleFracSim,2)) \ No newline at end of file diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 3ff143e..10cc6b7 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -13,6 +13,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-03-25, AK: Fix for plot # - 2021-03-18, AK: Fix for inlet concentration # - 2021-03-17, AK: Initial creation # @@ -81,7 +82,7 @@ def simulateDeadVolume(**kwargs): timeSim = outputSol.t # Inlet concentration - moleFracIn = outputSol.y[0] + moleFracIn = np.ones((len(outputSol.t),1))*feedMoleFrac # Outlet concentration at the dead volume moleFracOut = outputSol.y[-1] From d18822620db941f6d9add0c37621f97d4dc84020 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 25 Mar 2021 11:01:13 +0000 Subject: [PATCH 041/189] Add file to process experimental .mat file --- experimental/processExpMatFile.py | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 experimental/processExpMatFile.py diff --git a/experimental/processExpMatFile.py b/experimental/processExpMatFile.py new file mode 100644 index 0000000..89394c5 --- /dev/null +++ b/experimental/processExpMatFile.py @@ -0,0 +1,60 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Clean the experimental .mat file generated by each ZLC run +# +# Last modified: +# - 2021-03-24, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import auxiliaryFunctions +import scipy.io as sio +import numpy as np +import os +from numpy import savez +import socket + +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() + +# Directory of raw data +mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' +# File name of the experinent +fileName = 'ZLC_DeadVolume_Exp05A_Output.mat' +# Path of the file name +fileToLoad = mainDir + fileName + +# Load .mat file +rawData = sio.loadmat(fileToLoad)["experimentOutput"] +# Convert the nDarray to list +nDArrayToList = np.ndarray.tolist(rawData) +# Unpack another time (due to the structure of loadmat) +tempListData = nDArrayToList[0][0] +# Get the necessary variables +timeElapsed = tempListData[0] +moleFrac = tempListData[1] +flowRate = tempListData[2] + +# Save the array concentration into a native numpy file +# The .npz file is saved in a folder called simulationResults (hardcoded) +saveFileName = fileName[0:-4] + "_" + gitCommitID; +savePath = os.path.join('runData',saveFileName) +savez (savePath, timeElapsed = timeElapsed, # Time elapsed [s] + moleFrac = moleFrac, # Mole fraction of CO2 [-] + flowRate = flowRate, # Total flow rate [ccs] + hostName = socket.gethostname()) # Hostname of the computer \ No newline at end of file From f3c79624576098200883d58d90dc25c56281bb33 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 25 Mar 2021 14:33:08 +0000 Subject: [PATCH 042/189] Fix rounding error and add labels for the plot --- experimental/analyzeCalibration.m | 23 ++++++++++++++--------- experimental/calibrateMS.m | 4 ++-- experimental/runZLC.m | 9 +++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index ebd38e0..88dc203 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -9,12 +9,13 @@ % Authors: Ashwin Kumar Rajagopalan (AK) % Hassan Azzan (HA) % -% Purpose: -% +% Purpose: +% % % Last modified: +% - 2021-03-19, HA: Add legends to the plots % - 2021-03-24, AK: Remove k-means and replace with averaging of n points -% - 2021-03-19, HA: Added kmeans calculation to obtain mean ion current for +% - 2021-03-19, HA: Add kmeans calculation to obtain mean ion current for % polynomial fitting % - 2021-03-18, AK: Fix variable names % - 2021-03-17, AK: Change structure @@ -37,7 +38,7 @@ function analyzeCalibration(parametersFlow,parametersMS) if ~isempty(parametersFlow) flowData = load(parametersFlow); % Analyse flow data - MFM = [flowData.outputStruct.MFM]; % MFM + MFM = [flowData.outputStruct.MFM]; % MFM MFC1 = [flowData.outputStruct.MFC1]; % MFC1 MFC2 = [flowData.outputStruct.MFC2]; % MFC2 UMFM = [flowData.outputStruct.UMFM]; % UMFM @@ -84,7 +85,7 @@ function analyzeCalibration(parametersFlow,parametersMS) 'calibrationData',filesep,parametersFlow,'_Model'],'calibrationFlow',... 'gitCommitID'); end - + % Plot the raw and the calibrated data figure MFC1Set = 0:80; @@ -95,11 +96,11 @@ function analyzeCalibration(parametersFlow,parametersMS) subplot(2,2,2) hold on scatter(volFlow_MFC1_CO2,volFlow_UMFM_CO2,'or') - plot(MFC1Set,calibrationFlow.MFC_CO2*MFC1Set,'b') + plot(MFC1Set,calibrationFlow.MFC_CO2*MFC1Set,'b') subplot(2,2,3) hold on scatter(volFlow_MFM_He,volFlow_UMFM_He,'or') - plot(MFC1Set,calibrationFlow.MFM_He*MFC1Set,'b') + plot(MFC1Set,calibrationFlow.MFM_He*MFC1Set,'b') subplot(2,2,4) hold on scatter(volFlow_MFM_CO2,volFlow_UMFM_CO2,'or') @@ -112,7 +113,7 @@ function analyzeCalibration(parametersFlow,parametersMS) % Find the index that corresponds to the last time for a given set % point setPtMFC = unique(reconciledData.flow(:,4)); - % Find indices that corresponds to a given set point + % Find indices that corresponds to a given set point indList = ones(length(setPtMFC),2); % Loop over all the set points for ii=1:length(setPtMFC) @@ -130,7 +131,7 @@ function analyzeCalibration(parametersFlow,parametersMS) meanMoleFrac(ii,1) = mean(reconciledData.moleFrac(indMean(1):indMean(end),1)); % He meanMoleFrac(ii,2) = mean(reconciledData.moleFrac(indMean(1):indMean(end),2)); % CO2 end - + % Fit a polynomial function to get the model for MS % Fitting a 3rd order polynomial (check before accepting this) calibrationMS.He = polyfit(meanHeSignal,meanMoleFrac(:,1),parametersMS.polyDeg); % He @@ -162,6 +163,8 @@ function analyzeCalibration(parametersFlow,parametersMS) scatter(meanHeSignal,meanMoleFrac(:,1)) xlim([0 1.1*max(meanHeSignal)]); ylim([0 1]); + xlabel('Helium Signal [A]') + ylabel('Helium mole frac [-]') % CO2 subplot(1,2,2) @@ -170,5 +173,7 @@ function analyzeCalibration(parametersFlow,parametersMS) scatter(meanCO2Signal,meanMoleFrac(:,2)) xlim([0 1.1*max(meanCO2Signal)]); ylim([0 1]); + xlabel('CO2 Signal [A]') + ylabel('CO2 mole frac [-]') end end \ No newline at end of file diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m index 6459857..3e8f6f9 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrateMS.m @@ -34,9 +34,9 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 - MFC1_SP = [0.0, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5 15.0]; + MFC1_SP = [0.2, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5 14.8]; % Define set point for MFC2 - MFC2_SP = max(MFC1_SP)-MFC1_SP; + MFC2_SP = 15.0-MFC1_SP; % Loop through all setpoints to calibrate the meters for ii=1:length(MFC1_SP) expInfo.MFC1_SP = MFC1_SP(ii); diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 2836745..61af389 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-03-25, AK: Fix rounding errors % - 2021-03-24, AK: Cosmetic changes % - 2021-03-16, AK: Add MFC2 and fix for MS calibration % - 2021-03-16, AK: Add valve switch times @@ -149,7 +150,9 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) % Check if the set point was sent to the controller outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string - if str2double(outputMFC1Temp(6)) ~= expInfo.MFC1_SP + % Rounding required due to rounding errors. Differences of around + % eps can be observed + if round(str2double(outputMFC1Temp(6)),4) ~= round(expInfo.MFC1_SP,4) error("You should not be here!!!") end end @@ -162,7 +165,9 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) % Check if the set point was sent to the controller outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string - if str2double(outputMFC2Temp(6)) ~= expInfo.MFC2_SP + % Rounding required due to rounding errors. Differences of around + % eps can be observed + if round(str2double(outputMFC2Temp(6)),4) ~= round(expInfo.MFC2_SP,4) error("You should not be here!!!") end end From b585fed0423d55834e9d3c900fbc8dd013b934d7 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 25 Mar 2021 14:38:13 +0000 Subject: [PATCH 043/189] Add expInfo to .mat results file --- experimental/runZLC.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 61af389..628eb4e 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -120,7 +120,7 @@ function runZLC(varargin) gitCommitID = getGitCommit; % Load the output .mat file save(['experimentalData',filesep,expInfo.expName],... - 'gitCommitID','outputStruct') + 'gitCommitID','outputStruct','expInfo') end %% initializeTimerDevice: Initialisation of timer device From e5eb1d8e674c65ad8ec0d9c4b4b5fb5cba024b7a Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 25 Mar 2021 14:50:20 +0000 Subject: [PATCH 044/189] Add input resources files for simulations --- ...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, 25 Mar 2021 16:45:15 +0000 Subject: [PATCH 045/189] Clean up code and remove constant flow model --- experimental/simulateZLC.py | 118 +++++++++--------------------------- 1 file changed, 29 insertions(+), 89 deletions(-) diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 395c9bc..c0f0537 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -9,9 +9,12 @@ # Authors: Ashwin Kumar Rajagopalan (AK) # # Purpose: -# Simulates the ZLC setup +# Simulates the ZLC setup. This is inspired from Ruthven's work and from the +# sensor model. Note that there is no analytical solution and it uses a full +# model with mass transfer defined using linear driving force. # # Last modified: +# - 2021-03-25, AK: Remove the constant F model # - 2021-03-01, AK: Initial creation # # Input arguments: @@ -21,11 +24,15 @@ # ############################################################################ -def simulateFullModel(**kwargs): +def simulateZLC(**kwargs): import numpy as np from scipy.integrate import solve_ivp from simulateSensorArray import simulateSensorArray import auxiliaryFunctions + import os + + # Move to top level folder (to avoid path issues) + os.chdir("..") # Plot flag plotFlag = False @@ -35,13 +42,6 @@ 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 'modelConstF' in kwargs: - modelConstF = kwargs["modelConstF"] - else: - modelConstF = False # Sensor ID if 'sensorID' in kwargs: @@ -133,44 +133,26 @@ 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 5 s - # 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), - rtol = 1e-6, 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 - 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, - method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), - rtol = 1e-6, args = inputParameters) - - # Presure vector in output - pressureVec = pressureTotal * np.ones(len(outputSol.t)) # Constant pressure + # Prepare initial conditions vector + 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, + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), + 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)) - - # Parse out the output matrix and add flow rate - resultMat = np.row_stack((outputSol.y,pressureVec,flowOut)) + # 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 @@ -184,53 +166,11 @@ def simulateFullModel(**kwargs): # Call the plotting function if plotFlag: plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, - gitCommitID, currentDT, modelConstF) + gitCommitID, currentDT) # Return time and the output matrix return timeSim, resultMat, sensorFingerPrint, inputParameters -# func: solveSorptionEquationConstF - Constant flow rate model -# Solves the system of ODEs to evaluate the gas composition, loadings, and pressure -def solveSorptionEquationConstF(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, 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 - # 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])))) - 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 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): @@ -273,7 +213,7 @@ def solveSorptionEquationConstP(t, f, *inputParameters): # func: plotFullModelResult # Plots the model output for the conditions simulated locally def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, - gitCommitID, currentDT, modelConstF): + gitCommitID, currentDT): import numpy as np import os import matplotlib.pyplot as plt From a713a518c2b011a22319de0a2813b6681cca7ff7 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 25 Mar 2021 18:22:54 +0000 Subject: [PATCH 046/189] Small fix to perform multiple calibration --- experimental/calibrateMS.m | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m index 3e8f6f9..57afd15 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrateMS.m @@ -23,8 +23,6 @@ % Experiment name expInfo.expName = ['ZLCCalibrateMS','_',... datestr(datetime('now'),'yyyymmdd')]; - % Maximum time of the experiment - expInfo.maxTime = 300; % Sampling time for the device expInfo.samplingTime = 2; % Define gas for MFM @@ -34,7 +32,7 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 - MFC1_SP = [0.2, 1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5 14.8]; + MFC1_SP = repmat([0.0 0.2 1.5 3.0 4.5, 6.0 7.5, 9.0 10.5 12.0 13.5 14.8 15.0],[1,2]); % Define set point for MFC2 MFC2_SP = 15.0-MFC1_SP; % Loop through all setpoints to calibrate the meters @@ -43,6 +41,16 @@ expInfo.MFC2_SP = MFC2_SP(ii); % Flag for meter calibration expInfo.calibrateMeters = true; + % When the set point goes back to zero wait for 5 more min before + % starting the measurement + if ii == find(MFC1_SP == 0,1,'last') + % Maximum time of the experiment + % Change the max time to 10 min + expInfo.maxTime = 600; + else + % Else use 5 min + expInfo.maxTime = 300; + end % Run the setup for different calibrations runZLC(expInfo) end From d5c25d9ad27d32ea7a4b359a963b1cfab3d7cef3 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 26 Mar 2021 15:29:27 +0000 Subject: [PATCH 047/189] Change for MS calibration repetition and pass expInfo as an output --- experimental/analyzeCalibration.m | 55 ++++++++++++++++++++++--------- experimental/analyzeExperiment.m | 2 +- experimental/concatenateData.m | 5 ++- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index 88dc203..aec4142 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-26, AK: Fix for number of repetitions % - 2021-03-19, HA: Add legends to the plots % - 2021-03-24, AK: Remove k-means and replace with averaging of n points % - 2021-03-19, HA: Add kmeans calculation to obtain mean ion current for @@ -109,27 +110,49 @@ function analyzeCalibration(parametersFlow,parametersMS) % Load the file that contains the MS calibration if ~isempty(parametersMS) % Call reconcileData function for calibration of the MS - reconciledData = concatenateData(parametersMS); + [reconciledData, expInfo] = concatenateData(parametersMS); % Find the index that corresponds to the last time for a given set % point setPtMFC = unique(reconciledData.flow(:,4)); + % Find total number of data points + numDataPoints = length(reconciledData.flow(:,1)); + % Total number of points per set point + numPointsSetPt = expInfo.maxTime/expInfo.samplingTime; + % Number of repetitions per set point (assuming repmat in calibrateMS) + numRepetitions = floor((numDataPoints/numPointsSetPt)/length(setPtMFC)); + % Remove the 5 min idle time between repetitions + % For two repetitions + if numRepetitions == 2 + indRepFirst = numPointsSetPt*length(setPtMFC)+1; + indRepLast = indRepFirst+numPointsSetPt-1; + reconciledData.flow(indRepFirst:indRepLast,:) = []; + reconciledData.MS(indRepFirst:indRepLast,:) = []; + reconciledData.moleFrac(indRepFirst:indRepLast,:) = []; + % For one repetition + elseif numRepetitions == 1 + % Do nothing % + else + error('Currently more than two repetitions are not supported by analyzeCalibration.m'); + end % Find indices that corresponds to a given set point - indList = ones(length(setPtMFC),2); + indList = ones(numRepetitions*length(setPtMFC),2); % Loop over all the set points - for ii=1:length(setPtMFC) - % Indices for a given set point - indList(ii,1) = find(reconciledData.flow(:,4)==setPtMFC(ii),1,'first'); - indList(ii,2) = find(reconciledData.flow(:,4)==setPtMFC(ii),1,'last'); - % Find the mean value of the signal for numMean number of points - % for each set point - indMean = find(reconciledData.flow(:,4)==setPtMFC(ii),... - parametersMS.numMean,'last'); - % MS Signal mean - meanHeSignal(ii) = mean(reconciledData.MS(indMean(1):indMean(end),2)); % He - meanCO2Signal(ii) = mean(reconciledData.MS(indMean(1):indMean(end),3)); % CO2 - % Mole fraction mean - meanMoleFrac(ii,1) = mean(reconciledData.moleFrac(indMean(1):indMean(end),1)); % He - meanMoleFrac(ii,2) = mean(reconciledData.moleFrac(indMean(1):indMean(end),2)); % CO2 + for kk = 1:numRepetitions + for ii=1:length(setPtMFC) + % Indices for a given set point accounting for set point and + % number of repetitions + initInd = length(setPtMFC)*numPointsSetPt*(kk-1) + (ii-1)*numPointsSetPt + 1; + finalInd = initInd + numPointsSetPt - 1; + % Find the mean value of the signal for numMean number of points + % for each set point + indMean = (finalInd-parametersMS.numMean+1):finalInd; + % MS Signal mean + meanHeSignal((kk-1)*length(setPtMFC)+ii) = mean(reconciledData.MS(indMean,2)); % He + meanCO2Signal((kk-1)*length(setPtMFC)+ii) = mean(reconciledData.MS(indMean,3)); % CO2 + % Mole fraction mean + meanMoleFrac(((kk-1)*length(setPtMFC)+ii),1) = mean(reconciledData.moleFrac(indMean,1)); % He + meanMoleFrac(((kk-1)*length(setPtMFC)+ii),2) = mean(reconciledData.moleFrac(indMean,2)); % CO2 + end end % Fit a polynomial function to get the model for MS diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index c4ec8be..1c833e4 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -49,7 +49,7 @@ experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) % Call reconcileData function to get the output mole fraction for a % real experiment - outputStruct = concatenateData(experimentStruct); + [outputStruct,~] = concatenateData(experimentStruct); % Clean mole fraction to remove negative values (due to calibration) % Replace all negative molefraction with eps diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index c970ce8..c941a70 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-03-26, AK: Add expInfo to output % - 2021-03-22, AK: Bug fixes for finding indices % - 2021-03-22, AK: Add checks for MS concatenation % - 2021-03-18, AK: Add interpolation based on MS or flow meter @@ -24,7 +25,7 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function reconciledData = concatenateData(fileToLoad) +function [reconciledData, expInfo] = concatenateData(fileToLoad) % Load flow data flowMS = load(fileToLoad.flow); if ~isfield(fileToLoad,'calibrationFlow') @@ -32,6 +33,8 @@ end % Flow Calibration File load(fileToLoad.calibrationFlow); + % Return expInfo + expInfo = flowMS.expInfo; % Analyse flow data MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He % Done right now to check if calibration of MS is preesnt or not From 5c7b0b8740244918d2eaf998763f72e53fc0a3e6 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 7 Apr 2021 16:28:32 +0100 Subject: [PATCH 048/189] Add MFM with MFC1 and MFC2, add interval for MFC collection --- experimental/analyzeCalibration.m | 3 ++- experimental/concatenateData.m | 32 ++++++++++++++----------------- experimental/runZLC.m | 28 +++++++++++++++++++-------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index aec4142..4dcc452 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-07, AK: Modify for addition of MFM % - 2021-03-26, AK: Fix for number of repetitions % - 2021-03-19, HA: Add legends to the plots % - 2021-03-24, AK: Remove k-means and replace with averaging of n points @@ -113,7 +114,7 @@ function analyzeCalibration(parametersFlow,parametersMS) [reconciledData, expInfo] = concatenateData(parametersMS); % Find the index that corresponds to the last time for a given set % point - setPtMFC = unique(reconciledData.flow(:,4)); + setPtMFC = unique(reconciledData.flow(:,5)); % Find total number of data points numDataPoints = length(reconciledData.flow(:,1)); % Total number of points per set point diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index c941a70..7b021f0 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-07, AK: Modify for addition of MFM % - 2021-03-26, AK: Add expInfo to output % - 2021-03-22, AK: Bug fixes for finding indices % - 2021-03-22, AK: Add checks for MS concatenation @@ -37,30 +38,21 @@ expInfo = flowMS.expInfo; % Analyse flow data MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He - % Done right now to check if calibration of MS is preesnt or not - if ~isfield(fileToLoad,'calibrationMS') - MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 - else - MFC2 = [flowMS.outputStruct.MFM]; % MFM - CO2 - end + MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 + MFM = [flowMS.outputStruct.MFM]; % MFM - He % Get the datetime and volumetric flow rate dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... 'InputFormat','yyyyMMdd_HHmmss'); volFlow_MFC1 = [MFC1.volFlow]; % He setPt_MFC1 = [MFC1.setpoint]; % He setpoint for calibration volFlow_MFC2 = [MFC2.volFlow]; % CO2 + volFlow_MFM = [MFM.volFlow]; % CO2 % Apply the calibration for the flows volFlow_He = volFlow_MFC1*calibrationFlow.MFC_He; - % For calibration both MFCs are present - % Done right now to check if calibration of MS is preesnt or not - if ~isfield(fileToLoad,'calibrationMS') - volFlow_CO2 = volFlow_MFC2*calibrationFlow.MFC_CO2; - % For actual measurements, one MFC and one MFM present - % NOTE: Here MFC2 = MFM!!!!! - else - % Flow is converted assuming helium calibration for MFM - volFlow_CO2 = volFlow_MFC2*calibrationFlow.MFM_He; - end + volFlow_CO2 = volFlow_MFC2*calibrationFlow.MFC_CO2; + % Flow is converted assuming helium calibration for MFM + volFlow_MFM = volFlow_MFM*calibrationFlow.MFM_He; + % Load MS Ascii data % Create file identifier fileId = fopen(fileToLoad.MS); @@ -97,6 +89,7 @@ reconciledData.raw.dateTimeFlow = dateTimeFlow(indexInitial_Flow:end); reconciledData.raw.volFlow_He = volFlow_He(indexInitial_Flow:end); reconciledData.raw.volFlow_CO2 = volFlow_CO2(indexInitial_Flow:end); + reconciledData.raw.volFlow_MFM = volFlow_MFM(indexInitial_Flow:end); reconciledData.raw.setPt_He = setPt_MFC1(indexInitial_Flow:end); % MS @@ -133,7 +126,8 @@ -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] - reconciledData.flow(:,4) = reconciledData.raw.setPt_He; % He set point [ccm] + reconciledData.flow(:,4) = reconciledData.raw.volFlow_MFM; % MFM flow [ccm] + reconciledData.flow(:,5) = reconciledData.raw.setPt_He; % He set point [ccm] % MS rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... @@ -167,7 +161,9 @@ reconciledData.MS(:,1)); % Interpoloted He Flow [ccm] reconciledData.flow(:,3) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_CO2,... reconciledData.MS(:,1)); % Interpoloted CO2 flow [ccm] - reconciledData.flow(:,4) = interp1(rawTimeElapsedFlow,reconciledData.raw.setPt_He,... + reconciledData.flow(:,4) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_MFM,... + reconciledData.MS(:,1)); % Interpoloted MFM flow [ccm] + reconciledData.flow(:,5) = interp1(rawTimeElapsedFlow,reconciledData.raw.setPt_He,... reconciledData.MS(:,1)); % Interpoloted He setpoint[ccm] end diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 628eb4e..5b7dc07 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,8 @@ % controllers, will read flow data. % % Last modified: +% - 2021-04-07, AK: Add MFM with MFC1 and MFC2, add interval for MFC +% collection % - 2021-03-25, AK: Fix rounding errors % - 2021-03-24, AK: Cosmetic changes % - 2021-03-16, AK: Add MFC2 and fix for MS calibration @@ -41,6 +43,8 @@ function runZLC(varargin) expInfo.maxTime = 300; % Sampling time for the device expInfo.samplingTime = 2; + % Intervals for collecting MFC data + expInfo.MFCInterval = 10; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -63,24 +67,28 @@ function runZLC(varargin) % Initatlize ports portMFM = []; portMFC1 = []; portMFC2 = []; portUMFM = []; % Find COM port for MFM - portText = matchUSBport({'FT1EQDD6A'}); + portText = matchUSBport({'FT4U1GABA'}); if ~isempty(portText{1}) - portMFM = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + [startInd, stopInd] = regexp(portText{1},'COM(\d+)'); + portMFM = portText{1}(startInd(1):stopInd(1)); end % Find COM port for MFC1 portText = matchUSBport({'FT1EU0ACA'}); if ~isempty(portText{1}) - portMFC1 = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + [startInd, stopInd] = regexp(portText{1},'COM(\d+)'); + portMFC1 = portText{1}(startInd(1):stopInd(1)); end % Find COM port for MFC2 - portText = matchUSBport({'FT1EQDD6M'}); + portText = matchUSBport({'FT1EQDD6A'}); if ~isempty(portText{1}) - portMFC2 = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + [startInd, stopInd] = regexp(portText{1},'COM(\d+)'); + portMFC2 = portText{1}(startInd(1):stopInd(1)); end % Find COM port for UMFM portText = matchUSBport({'3065335A3235'}); if ~isempty(portText{1}) - portUMFM = ['COM',portText{1}(regexp(portText{1},'COM[1234567891011]')+3)]; + [startInd, stopInd] = regexp(portText{1},'COM(\d+)'); + portUMFM = portText{1}(startInd(1):stopInd(1)); end % Comm setup for the flow meter and controller serialObj.MFM = struct('portName',portMFM,'baudRate',19200,'terminator','CR'); @@ -198,8 +206,12 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFM.massFlow = str2double(outputMFMTemp(5)); % standard units [sccm] MFM.gas = outputMFMTemp(6); % gas in the meter end + % Generate a flag to collect MFC data + flagCollect = expInfo.calibrateMeters ... + || (mod(timerObj.tasksExecuted,expInfo.MFCInterval)==0 ... + || timerObj.tasksExecuted == 1 || timerObj.tasksExecuted == timerObj.TasksToExecute); % Get the current state of the flow controller 1 - if ~isempty(serialObj.MFC1.portName) + if ~isempty(serialObj.MFC1.portName) && flagCollect outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string MFC1.pressure = str2double(outputMFC1Temp(2)); % [bar] @@ -210,7 +222,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC1.gas = outputMFC1Temp(7); % gas in the controller end % Get the current state of the flow controller 2 - if ~isempty(serialObj.MFC2.portName) + if ~isempty(serialObj.MFC2.portName) && flagCollect outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string MFC2.pressure = str2double(outputMFC2Temp(2)); % [bar] From ed476429e3ef026ed44346d1c5efd7078f344ff0 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 8 Apr 2021 18:26:26 +0100 Subject: [PATCH 049/189] Add ratio of gas for calibration --- experimental/analyzeCalibration.m | 73 ++++++++++++++++++++++--------- experimental/analyzeExperiment.m | 21 +++++---- experimental/concatenateData.m | 17 ++++++- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index 4dcc452..b4c8931 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-04-07, AK: Modify for addition of MFM % - 2021-03-26, AK: Fix for number of repetitions % - 2021-03-19, HA: Add legends to the plots @@ -158,8 +159,17 @@ function analyzeCalibration(parametersFlow,parametersMS) % Fit a polynomial function to get the model for MS % Fitting a 3rd order polynomial (check before accepting this) - calibrationMS.He = polyfit(meanHeSignal,meanMoleFrac(:,1),parametersMS.polyDeg); % He - calibrationMS.CO2 = polyfit(meanCO2Signal,meanMoleFrac(:,2),parametersMS.polyDeg); % COo2 + calibrationMS.flagUseIndGas = parametersMS.flagUseIndGas; + if parametersMS.flagUseIndGas + calibrationMS.He = polyfit(meanHeSignal,meanMoleFrac(:,1),parametersMS.polyDeg); % He + calibrationMS.CO2 = polyfit(meanCO2Signal,meanMoleFrac(:,2),parametersMS.polyDeg); % COo2 + else + % Perform an optimization to obtain parameter estimates to fit the + % ratio of the helium signal to CO2 signal w.r.t He concentration + y0 = [1, 0.5, 0.5]; % Initial conditions + [paramFit,resErr] = fminunc(@(p) objectiveFunction(meanMoleFrac(:,1),log(meanHeSignal./meanCO2Signal),p),y0); + calibrationMS.ratioHeCO2 = paramFit; + end % Save the calibration data into a .mat file % Check if calibration data folder exists @@ -180,24 +190,45 @@ function analyzeCalibration(parametersFlow,parametersMS) % Plot the raw and the calibrated data figure(1) - % He - subplot(1,2,1) - hold on - plot(1e-13:1e-13:1e-8,polyval(calibrationMS.He,1e-13:1e-13:1e-8)) - scatter(meanHeSignal,meanMoleFrac(:,1)) - xlim([0 1.1*max(meanHeSignal)]); - ylim([0 1]); - xlabel('Helium Signal [A]') - ylabel('Helium mole frac [-]') - - % CO2 - subplot(1,2,2) - hold on - plot(1e-13:1e-13:1e-8,polyval(calibrationMS.CO2,1e-13:1e-13:1e-8)) - scatter(meanCO2Signal,meanMoleFrac(:,2)) - xlim([0 1.1*max(meanCO2Signal)]); - ylim([0 1]); - xlabel('CO2 Signal [A]') - ylabel('CO2 mole frac [-]') + % Plot for independent gas calibrations + if parametersMS.flagUseIndGas + % He + subplot(1,2,1) + hold on + plot(1e-13:1e-13:1e-8,polyval(calibrationMS.He,1e-13:1e-13:1e-8)) + scatter(meanHeSignal,meanMoleFrac(:,1)) + xlim([0 1.1*max(meanHeSignal)]); + ylim([0 1]); + box on; grid on; + xlabel('Helium Signal [A]') + ylabel('Helium mole frac [-]') + + % CO2 + subplot(1,2,2) + hold on + plot(1e-13:1e-13:1e-8,polyval(calibrationMS.CO2,1e-13:1e-13:1e-8),'b') + plot(meanCO2Signal,meanMoleFrac(:,2),'or') + xlim([0 1.1*max(meanCO2Signal)]); + ylim([0 1]); + box on; grid on; + xlabel('CO2 Signal [A]') + ylabel('CO2 mole frac [-]') + % Ratio of He to CO2 + else + plot(log(meanHeSignal./meanCO2Signal),meanMoleFrac(:,1),'or') % Experimental + hold on + plot(-10:1:10,1./(1+paramFit(1).*exp(-paramFit(2).*(-10:1:10))).^(1/paramFit(3)),'b') + xlim([-10 10]); + ylim([0 1]); + box on; grid on; + xlabel('log(Helium/CO2 Signal) [-]') + ylabel('Helium mole frac [-]') + end +end end +% Objective function to evaluate model parameters for the logistic +% function (generalized) +function errSignal = objectiveFunction(meanMoleFrac,expSignal,p) + % Calculate the sum of errors + errSignal = sum((meanMoleFrac' - 1./(1+p(1).*exp(-p(2).*expSignal)).^(1/p(3))).^2); end \ No newline at end of file diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 1c833e4..004773a 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -14,6 +14,7 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-03-24, AK: Add flow rate computation and prepare structure for % Python script % - 2021-03-18, AK: Updates to structure @@ -28,24 +29,26 @@ % Get the git commit ID gitCommitID = getGitCommit; -flagCalibration = false; % Flag to decide calibration or analysis +% Flag to decide calibration or analysis +flagCalibration = true; % Mode to switch between calibration and analyzing real experiment if flagCalibration experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_20210324'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210324.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLCCalibrateMS_20210408'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210408.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) experimentStruct.numMean = 10; % Number of points for averaging - experimentStruct.polyDeg = 3; % Degree of polynomial fit + experimentStruct.flagUseIndGas = false; % Flag to determine whether independent (true) or ratio of signals used for calibration + experimentStruct.polyDeg = 3; % Degree of polynomial fit for independent gas calibraiton % Call reconcileData function for calibration of the MS analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file else setTotalFlowRate = 15; experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLC_DeadVolume_Exp05A'; % Experimental flow file (.mat) - experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210323_Model'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_DeadVolume_Exp05A.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLCCalibrateMS_20210408'; % Experimental flow file (.mat) + experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210408_Model'; % Experimental calibration file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210408.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) % Call reconcileData function to get the output mole fraction for a % real experiment @@ -53,8 +56,8 @@ % Clean mole fraction to remove negative values (due to calibration) % Replace all negative molefraction with eps - outputStruct.moleFrac(outputStruct.moleFrac(:,1)<0,1)=eps; % He - outputStruct.moleFrac(:,2)=1-outputStruct.moleFrac(:,1); % Compute CO2 with mass balance + outputStruct.moleFrac(outputStruct.moleFrac(:,2)<0,1)=eps; % CO2 + outputStruct.moleFrac(:,1)=1-outputStruct.moleFrac(:,2); % Compute He with mass balance % Convert the MFM flow to real flow % Load the meter calibrations diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index 7b021f0..fc475ca 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-04-07, AK: Modify for addition of MFM % - 2021-03-26, AK: Add expInfo to output % - 2021-03-22, AK: Bug fixes for finding indices @@ -178,7 +179,19 @@ % MS Calibration File load(fileToLoad.calibrationMS); % Convert the raw signal to concentration - reconciledData.moleFrac(:,1) = polyval(calibrationMS.He,reconciledData.MS(:,2)); % He [-] - reconciledData.moleFrac(:,2) = polyval(calibrationMS.CO2,reconciledData.MS(:,3)); % CO2 [-] + % When independent gas signals are used + if calibrationMS.flagUseIndGas + reconciledData.moleFrac(:,1) = polyval(calibrationMS.He,reconciledData.MS(:,2)); % He [-] + reconciledData.moleFrac(:,2) = polyval(calibrationMS.CO2,reconciledData.MS(:,3)); % CO2 [-] + % When ratio of gas signals are used + else + % Parse out the fitting parameters + paramFit = calibrationMS.ratioHeCO2; + % Use a generalized logistic function + reconciledData.moleFrac(:,1) = 1./(1+paramFit(1)... + .*exp(-paramFit(2).*log(reconciledData.MS(:,2)./reconciledData.MS(:,3))))... + .^(1/paramFit(3)); % He [-] + reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); % CO2 [-] + end end end \ No newline at end of file From 7034bf4b582f2019beb6f835ccfbaffbea178aaa Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 9 Apr 2021 18:07:12 +0100 Subject: [PATCH 050/189] Change output for calibration or non calibrate mode --- experimental/concatenateData.m | 42 ++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index fc475ca..e694b51 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-09, AK: Change output for calibration or non calibrate mode % - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-04-07, AK: Modify for addition of MFM % - 2021-03-26, AK: Add expInfo to output @@ -39,7 +40,7 @@ expInfo = flowMS.expInfo; % Analyse flow data MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He - MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 + MFC2 = [flowMS.outputStruct.MFC2]; % MFC2 - CO2 MFM = [flowMS.outputStruct.MFM]; % MFM - He % Get the datetime and volumetric flow rate dateTimeFlow = datetime({flowMS.outputStruct.samplingDateTime},... @@ -125,11 +126,17 @@ % Meters and the controllers reconciledData.flow(:,1) = seconds(reconciledData.raw.dateTimeFlow... -reconciledData.raw.dateTimeFlow(1)); % Time elapsed [s] - reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] - reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] - reconciledData.flow(:,4) = reconciledData.raw.volFlow_MFM; % MFM flow [ccm] - reconciledData.flow(:,5) = reconciledData.raw.setPt_He; % He set point [ccm] - + % Save the MFC and MFM values for the calibrate meters experiments + if expInfo.calibrateMeters + reconciledData.flow(:,2) = reconciledData.raw.volFlow_He; % He Flow [ccm] + reconciledData.flow(:,3) = reconciledData.raw.volFlow_CO2; % CO2 flow [ccm] + reconciledData.flow(:,4) = reconciledData.raw.volFlow_MFM; % MFM flow [ccm] + reconciledData.flow(:,5) = reconciledData.raw.setPt_He; % He set point [ccm] + % Save only the MFM values for the true experiments + else + reconciledData.flow(:,2) = reconciledData.raw.volFlow_MFM; % He set point [ccm] + end + % MS rawTimeElapsedHe = seconds(reconciledData.raw.dateTimeMS_He ... - reconciledData.raw.dateTimeMS_He(1)); % Time elapsed He [s] @@ -158,14 +165,21 @@ rawTimeElapsedFlow = seconds(reconciledData.raw.dateTimeFlow... -reconciledData.raw.dateTimeFlow(1)); reconciledData.flow(:,1) = reconciledData.MS(:,1); % Time elapsed of MS [s] - reconciledData.flow(:,2) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_He,... - reconciledData.MS(:,1)); % Interpoloted He Flow [ccm] - reconciledData.flow(:,3) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_CO2,... - reconciledData.MS(:,1)); % Interpoloted CO2 flow [ccm] - reconciledData.flow(:,4) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_MFM,... - reconciledData.MS(:,1)); % Interpoloted MFM flow [ccm] - reconciledData.flow(:,5) = interp1(rawTimeElapsedFlow,reconciledData.raw.setPt_He,... - reconciledData.MS(:,1)); % Interpoloted He setpoint[ccm] + % Save the MFC and MFM values for the calibrate meters experiments + if expInfo.calibrateMeters + reconciledData.flow(:,2) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_He,... + reconciledData.MS(:,1)); % Interpoloted He Flow [ccm] + reconciledData.flow(:,3) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_CO2,... + reconciledData.MS(:,1)); % Interpoloted CO2 flow [ccm] + reconciledData.flow(:,4) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_MFM,... + reconciledData.MS(:,1)); % Interpoloted MFM flow [ccm] + reconciledData.flow(:,5) = interp1(rawTimeElapsedFlow,reconciledData.raw.setPt_He,... + reconciledData.MS(:,1)); % Interpoloted He setpoint[ccm] + % Save only the MFM values for the true experiments + else + reconciledData.flow(:,2) = interp1(rawTimeElapsedFlow,reconciledData.raw.volFlow_MFM,... + reconciledData.MS(:,1)); % Interpoloted MFM flow [ccm] + end end % Get the mole fraction used for the calibration From 10f6fad073d765bb7440cfc522372952d7f900a5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 12 Apr 2021 16:12:04 +0100 Subject: [PATCH 051/189] Add functionality for extracting dead volume parameters from multiple experiments --- experimental/extractDeadVolume.py | 57 ++++++++++++++++++++---------- experimental/simulateDeadVolume.py | 5 +-- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 5f49fa5..5757623 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -14,6 +14,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-04-12, AK: Add functionality for multiple experiments # - 2021-03-25, AK: Estimate parameters using experimental data # - 2021-03-17, AK: Initial creation # @@ -86,24 +87,44 @@ def deadVolObjectiveFunction(x): # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' - # File name of the experinent - fileName = 'ZLC_DeadVolume_Exp05A_Output_0cf1b97.npz' - # Path of the file name - fileToLoad = mainDir + fileName - # Load experimental molefraction - timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() - moleFracExp = load(fileToLoad)["moleFrac"].flatten() + # File name of the experiments + fileName = ['ZLC_DeadVolume_Exp10A_Output_5fac6fa.npz', + 'ZLC_DeadVolume_Exp10B_Output_5fac6fa.npz', + 'ZLC_DeadVolume_Exp10C_Output_5fac6fa.npz', + 'ZLC_DeadVolume_Exp10D_Output_5fac6fa.npz'] - # Compute the dead volume response using the optimizer parameters - timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume = x[0], - numberOfTanks = int(x[1])) - - # Interpolate simulation data (generate function) - interpSim = interp1d(timeSimOut, moleFracOut) - - # Find the interpolated simulation mole fraction at times corresponding to - # the experimental ones - moleFracSim = interpSim(timeElapsedExp) + # Initialize error for objective function + computedError = 0 + # Loop over all available files + for ii in range(len(fileName)): + # Initialize outputs + timeSimOut = [] + moleFracOut = [] + moleFracSim = [] + # Path of the file name + fileToLoad = mainDir + fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExp = load(fileToLoad)["moleFrac"].flatten() + # Parse out flow rate of the experiment + # Obtain the mean and round it to the 2 decimal to be used in the + # simulation + flowRate = round(np.mean(load(fileToLoad)["flowRate"]),2) + + # Compute the dead volume response using the optimizer parameters + timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume = x[0], + numberOfTanks = int(x[1]), + flowRate = flowRate) + + # Interpolate simulation data (generate function) + interpSim = interp1d(timeSimOut, moleFracOut) + + # Find the interpolated simulation mole fraction at times corresponding to + # the experimental ones + moleFracSim = interpSim(timeElapsedExp) + + # Compute the sum of the error for the difference between exp. and sim. + computedError += np.sum(np.power(moleFracExp - moleFracSim,2)) # Compute the sum of the error for the difference between exp. and sim. - return np.sum(np.power(moleFracExp - moleFracSim,2)) \ No newline at end of file + return computedError \ No newline at end of file diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 10cc6b7..4fdd086 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -13,6 +13,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-04-12, AK: Small fixed # - 2021-03-25, AK: Fix for plot # - 2021-03-18, AK: Fix for inlet concentration # - 2021-03-17, AK: Initial creation @@ -59,12 +60,12 @@ def simulateDeadVolume(**kwargs): if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) else: - feedMoleFrac = np.array([1.]) + feedMoleFrac = np.array([0.]) # Time span for integration [tuple with t0 and tf] if 'timeInt' in kwargs: timeInt = kwargs["timeInt"] else: - timeInt = (0.0,20) + timeInt = (0.0,1000) # Prepare tuple of input parameters for the ode solver inputParameters = (flowRate, deadVolume, numberOfTanks, feedMoleFrac) From d9979ada5f29d3b74ce95209849a236878fe9863 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Tue, 13 Apr 2021 18:32:02 +0100 Subject: [PATCH 052/189] Add threshold to cut data below a given mole fraction --- experimental/analyzeExperiment.m | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 004773a..32d2f1d 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -14,6 +14,7 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-04-13, AK: Add threshold to cut data below a given mole fraction % - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-03-24, AK: Add flow rate computation and prepare structure for % Python script @@ -30,13 +31,13 @@ gitCommitID = getGitCommit; % Flag to decide calibration or analysis -flagCalibration = true; +flagCalibration = false; % Mode to switch between calibration and analyzing real experiment if flagCalibration experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_20210408'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210408.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLCCalibrateMS_20210413'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210413.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) experimentStruct.numMean = 10; % Number of points for averaging experimentStruct.flagUseIndGas = false; % Flag to determine whether independent (true) or ratio of signals used for calibration @@ -44,11 +45,12 @@ % Call reconcileData function for calibration of the MS analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file else - setTotalFlowRate = 15; + setTotalFlowRate = 15; % Total flow rate of the experiment [ccm] + moleFracThreshold = 1e-4; % Threshold to cut data below a given mole fraction [-] experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_20210408'; % Experimental flow file (.mat) - experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210408_Model'; % Experimental calibration file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210408.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLC_DeadVolume_Exp12D'; % Experimental flow file (.mat) + experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210413_Model'; % Experimental calibration file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_DeadVolume_Exp12D.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) % Call reconcileData function to get the output mole fraction for a % real experiment @@ -85,9 +87,12 @@ totalFlowRate = realFlowRateHe+realFlowRateCO2; % Input for the ZLC script (Python) - experimentOutput.timeExp = outputStruct.flow(:,1); % Time elapsed [s] - experimentOutput.moleFrac = outputStruct.moleFrac(:,2); % Mole fraction CO2 [-] - experimentOutput.totalFlowRate = totalFlowRate./60; % Total flow rate of the gas [ccs] + % Find the index for the mole fraction that corresponds to the + % threshold mole fraction + moleFracThresholdInd = find(outputStruct.moleFrac(:,2) Date: Wed, 14 Apr 2021 16:37:07 +0100 Subject: [PATCH 053/189] Change TIS model to series of parallel CSTRs --- experimental/extractDeadVolume.py | 118 +++++++++++++++++----- experimental/processExpMatFile.py | 71 +++++++------- experimental/simulateDeadVolume.py | 151 +++++++++++++++++++++++------ 3 files changed, 249 insertions(+), 91 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 5757623..58bcc41 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -14,6 +14,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-04-14, AK: Change strucure and perform series of parallel CSTRs # - 2021-04-12, AK: Add functionality for multiple experiments # - 2021-03-25, AK: Estimate parameters using experimental data # - 2021-03-17, AK: Initial creation @@ -30,26 +31,42 @@ def extractDeadVolume(): import numpy as np from geneticalgorithm2 import geneticalgorithm2 as ga # GA import auxiliaryFunctions + import os + from numpy import savez import multiprocessing # For parallel processing + import socket # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() # Number of times optimization repeated - numOptRepeat = 3 + numOptRepeat = 10 # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() # Get the current date and time for saving purposes currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Directory of raw data + mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' + # File name of the experiments + fileName = ['ZLC_DeadVolume_Exp12D_Output.mat', + 'ZLC_DeadVolume_Exp12E_Output.mat'] + # Generate .npz file for python processing of the .mat file + filesToProcess(True,mainDir,fileName) - # Define the bounds and the type of the parameters to be optimized - optBounds = np.array(([np.finfo(float).eps,100], [1,30])) - optType=np.array(['real','int']) + # Define the bounds and the type of the parameters to be optimized + optBounds = np.array(([np.finfo(float).eps,100], [np.finfo(float).eps,100], + [np.finfo(float).eps,100], [np.finfo(float).eps,100], + [1,30], [1,30], [1,30], [1,30], + [np.finfo(float).eps,1-np.finfo(float).eps], + [np.finfo(float).eps,1-np.finfo(float).eps])) + + optType=np.array(['real','real','real','real','int','int','int','int','real','real']) # Algorithm parameters for GA - algorithm_param = {'max_num_iteration':25, - 'population_size':100, + algorithm_param = {'max_num_iteration':20, + 'population_size':400, 'mutation_probability':0.1, 'crossover_probability': 0.55, 'parents_portion': 0.15, @@ -57,7 +74,7 @@ def extractDeadVolume(): # Minimize an objective function to compute the dead volume and the number of # tanks for the dead volume using GA - model = ga(function = deadVolObjectiveFunction, dimension=2, + model = ga(function = deadVolObjectiveFunction, dimension=10, variable_type_mixed = optType, variable_boundaries = optBounds, algorithm_parameters=algorithm_param) @@ -73,6 +90,24 @@ def extractDeadVolume(): n_jobs = num_cores), start_generation=model.output_dict['last_generation'], no_plot = True) + # Save the array concentration into a native numpy file + # The .npz file is saved in a folder called simulationResults (hardcoded) + filePrefix = "deadVolumeCharacteristics" + saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID; + savePath = os.path.join('..','simulationResults',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationResults')): + os.mkdir('simulationResults') + + + # Save the output into a .npz file + savez (savePath, modelOutput = model.output_dict, # Model output + optBounds = optBounds, # Optimizer bounds + algoParameters = algorithm_param, # Algorithm parameters + fileName = fileName, # Names of file used for fitting + hostName = socket.gethostname()) # Hostname of the computer) + # Return the optimized values return model.output_dict @@ -85,35 +120,48 @@ def deadVolObjectiveFunction(x): from numpy import load from scipy.interpolate import interp1d - # Directory of raw data - mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' - # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp10A_Output_5fac6fa.npz', - 'ZLC_DeadVolume_Exp10B_Output_5fac6fa.npz', - 'ZLC_DeadVolume_Exp10C_Output_5fac6fa.npz', - 'ZLC_DeadVolume_Exp10D_Output_5fac6fa.npz'] + # Load the names of the file to be used for estimating dead volume characteristics + filePath = filesToProcess(False,[],[]) + numPointsExp = np.zeros(len(filePath)) + for ii in range(len(filePath)): + # Load experimental molefraction + timeElapsedExp = load(filePath[ii])["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + # Initialize error for objective function computedError = 0 + numPoints = 0 # Loop over all available files - for ii in range(len(fileName)): + for ii in range(len(filePath)): # Initialize outputs timeSimOut = [] moleFracOut = [] - moleFracSim = [] - # Path of the file name - fileToLoad = mainDir + fileName[ii] + moleFracSim = [] # Load experimental molefraction - timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() - moleFracExp = load(fileToLoad)["moleFrac"].flatten() + timeElapsedExpTemp = load(filePath[ii])["timeElapsed"].flatten() + moleFracExpTemp = load(filePath[ii])["moleFrac"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] # Parse out flow rate of the experiment # Obtain the mean and round it to the 2 decimal to be used in the # simulation - flowRate = round(np.mean(load(fileToLoad)["flowRate"]),2) + flowRate = round(np.mean(load(filePath[ii])["flowRate"]),2) # Compute the dead volume response using the optimizer parameters - timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume = x[0], - numberOfTanks = int(x[1]), + timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume_1M = x[0], + deadVolume_1D = x[1], + deadVolume_2M = x[2], + deadVolume_2D = x[3], + numTanks_1M = int(x[4]), + numTanks_1D = int(x[5]), + numTanks_2M = int(x[6]), + numTanks_2D = int(x[7]), + splitRatio_1 = x[8], + splitRatio_2 = x[9], flowRate = flowRate) # Interpolate simulation data (generate function) @@ -124,7 +172,27 @@ def deadVolObjectiveFunction(x): moleFracSim = interpSim(timeElapsedExp) # Compute the sum of the error for the difference between exp. and sim. - computedError += np.sum(np.power(moleFracExp - moleFracSim,2)) + numPoints += len(moleFracExp) + computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) # Compute the sum of the error for the difference between exp. and sim. - return computedError \ No newline at end of file + return (numPoints/2)*computedError + +# func: filesToProcess +# Loads .mat experimental file and processes it for python +def filesToProcess(initFlag,mainDir,fileName): + from processExpMatFile import processExpMatFile + from numpy import savez + from numpy import load + # Process the data for python (if needed) + if initFlag: + savePath=list() + for ii in range(len(fileName)): + savePath.append(processExpMatFile(mainDir, fileName[ii])) + # Save the .npz file names in a dummy file + savez ('tempCreation.npz', savePath = savePath) + # Returns the path of the .npz file to be used + else: + # Load the dummy file with file names for processing + savePath = load ('tempCreation.npz')["savePath"] + return savePath \ No newline at end of file diff --git a/experimental/processExpMatFile.py b/experimental/processExpMatFile.py index 89394c5..529c8ad 100644 --- a/experimental/processExpMatFile.py +++ b/experimental/processExpMatFile.py @@ -12,6 +12,7 @@ # Clean the experimental .mat file generated by each ZLC run # # Last modified: +# - 2021-03-24, AK: Make it a function # - 2021-03-24, AK: Initial creation # # Input arguments: @@ -22,39 +23,37 @@ # ############################################################################ -import auxiliaryFunctions -import scipy.io as sio -import numpy as np -import os -from numpy import savez -import socket - -# Get the commit ID of the current repository -gitCommitID = auxiliaryFunctions.getCommitID() - -# Directory of raw data -mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' -# File name of the experinent -fileName = 'ZLC_DeadVolume_Exp05A_Output.mat' -# Path of the file name -fileToLoad = mainDir + fileName - -# Load .mat file -rawData = sio.loadmat(fileToLoad)["experimentOutput"] -# Convert the nDarray to list -nDArrayToList = np.ndarray.tolist(rawData) -# Unpack another time (due to the structure of loadmat) -tempListData = nDArrayToList[0][0] -# Get the necessary variables -timeElapsed = tempListData[0] -moleFrac = tempListData[1] -flowRate = tempListData[2] - -# Save the array concentration into a native numpy file -# The .npz file is saved in a folder called simulationResults (hardcoded) -saveFileName = fileName[0:-4] + "_" + gitCommitID; -savePath = os.path.join('runData',saveFileName) -savez (savePath, timeElapsed = timeElapsed, # Time elapsed [s] - moleFrac = moleFrac, # Mole fraction of CO2 [-] - flowRate = flowRate, # Total flow rate [ccs] - hostName = socket.gethostname()) # Hostname of the computer \ No newline at end of file +def processExpMatFile(mainDir, fileName): + import auxiliaryFunctions + import scipy.io as sio + import numpy as np + import os + from numpy import savez + import socket + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Path of the file name + fileToLoad = mainDir + fileName + + # Load .mat file + rawData = sio.loadmat(fileToLoad)["experimentOutput"] + # Convert the nDarray to list + nDArrayToList = np.ndarray.tolist(rawData) + # Unpack another time (due to the structure of loadmat) + tempListData = nDArrayToList[0][0] + # Get the necessary variables + timeElapsed = tempListData[0] + moleFrac = tempListData[1] + flowRate = tempListData[2] + + # Save the array concentration into a native numpy file + # The .npz file is saved in a folder called simulationResults (hardcoded) + saveFileName = fileName[0:-4] + "_" + gitCommitID + ".npz"; + savePath = os.path.join(mainDir,saveFileName) + savez (savePath, timeElapsed = timeElapsed, # Time elapsed [s] + moleFrac = moleFrac, # Mole fraction of CO2 [-] + flowRate = flowRate, # Total flow rate [ccs] + hostName = socket.gethostname()) # Hostname of the computer + return savePath \ No newline at end of file diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 4fdd086..bd6dcc1 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -13,6 +13,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-04-14, AK: Change from simple TIS to series of parallel CSTRs # - 2021-04-12, AK: Small fixed # - 2021-03-25, AK: Fix for plot # - 2021-03-18, AK: Fix for inlet concentration @@ -29,9 +30,8 @@ def simulateDeadVolume(**kwargs): import numpy as np from scipy.integrate import solve_ivp - from simulateSensorArray import simulateSensorArray import auxiliaryFunctions - + # Plot flag plotFlag = False @@ -46,16 +46,58 @@ def simulateDeadVolume(**kwargs): flowRate = kwargs["flowRate"] else: flowRate = 0.25 - # Total Dead Volume of the tanks [cc] - if 'deadVolume' in kwargs: - deadVolume = kwargs["deadVolume"] + # Dead Volume of the first volume (mixing) [cc] + if 'deadVolume_1M' in kwargs: + deadVolume_1M = kwargs["deadVolume_1M"] + else: + deadVolume_1M = 5 + # Dead Volume of the first volume (diffusive) [cc] + if 'deadVolume_1D' in kwargs: + deadVolume_1D = kwargs["deadVolume_1D"] + else: + deadVolume_1D = 1.5 + # Dead Volume of the second volume (mixing) [cc] + if 'deadVolume_2M' in kwargs: + deadVolume_2M = kwargs["deadVolume_2M"] + else: + deadVolume_2M = 2 + # Dead Volume of the second volume (diffusive) [cc] + if 'deadVolume_2D' in kwargs: + deadVolume_2D = kwargs["deadVolume_2D"] + else: + deadVolume_2D = 0.5 + + # Number of tanks of the first volume (mixing) [-] + if 'numTanks_1M' in kwargs: + numTanks_1M = kwargs["numTanks_1M"] else: - deadVolume = 1 - # Number of tanks [-] - if 'numberOfTanks' in kwargs: - numberOfTanks = kwargs["numberOfTanks"] + numTanks_1M = 10 + # Number of tanks of the first volume (mixing) [-] + if 'numTanks_1D' in kwargs: + numTanks_1D = kwargs["numTanks_1D"] else: - numberOfTanks = 1 + numTanks_1D = 1 + # Number of tanks of the second volume (mixing) [-] + if 'numTanks_2M' in kwargs: + numTanks_2M = kwargs["numTanks_2M"] + else: + numTanks_2M = 1 + # Number of tanks of the second volume (mixing) [-] + if 'numTanks_2D' in kwargs: + numTanks_2D = kwargs["numTanks_2D"] + else: + numTanks_2D = 1 + + # Split ratio for flow rate of the first volume [-] + if 'splitRatio_1' in kwargs: + splitRatio_1 = kwargs["splitRatio_1"] + else: + splitRatio_1 = 0.99 + # Split ratio for flow rate of the second volume [-] + if 'splitRatio_2' in kwargs: + splitRatio_2 = kwargs["splitRatio_2"] + else: + splitRatio_2 = 0.9 # Feed Mole Fraction [-] if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) @@ -65,15 +107,22 @@ def simulateDeadVolume(**kwargs): if 'timeInt' in kwargs: timeInt = kwargs["timeInt"] else: - timeInt = (0.0,1000) + timeInt = (0.0,2000) # Prepare tuple of input parameters for the ode solver - inputParameters = (flowRate, deadVolume, numberOfTanks, feedMoleFrac) + inputParameters = (flowRate, deadVolume_1M,deadVolume_1D, + deadVolume_2M,deadVolume_2D, + numTanks_1M, numTanks_1D, numTanks_2M, + numTanks_2D, splitRatio_1, splitRatio_2, + feedMoleFrac) + + # Total number of tanks[-] + numTanksTotal = numTanks_1M + numTanks_2M + numTanks_1D + numTanks_2D # Prepare initial conditions vector # The first element is the inlet composition and the rest is the dead # volume - initialConditions = np.ones([numberOfTanks])*(1-feedMoleFrac) + initialConditions = np.ones([numTanksTotal])*(1-feedMoleFrac) # Solve the system of equations outputSol = solve_ivp(solveTanksInSeries, timeInt, initialConditions, method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],0.1), @@ -84,9 +133,15 @@ def simulateDeadVolume(**kwargs): # Inlet concentration moleFracIn = np.ones((len(outputSol.t),1))*feedMoleFrac - - # Outlet concentration at the dead volume - moleFracOut = outputSol.y[-1] + + # Mole fraction at the outlet + # Mixing volume + moleFracMix = outputSol.y[numTanksTotal-numTanks_2D-1] + # Diffusive volume + moleFracDiff = outputSol.y[-1] + # Composition after mixing + moleFracOut = (splitRatio_2*flowRate*moleFracMix + + (1-splitRatio_2)*flowRate*moleFracDiff)/(flowRate) # Plot the dead volume response if plotFlag: @@ -98,24 +153,60 @@ def simulateDeadVolume(**kwargs): # Solves the system of ODE for the tanks in series model for the dead volume def solveTanksInSeries(t, f, *inputParameters): import numpy as np - + # Unpack the tuple of input parameters used to solve equations - flowRate, deadVolume , numberOfTanks, feedMoleFrac = inputParameters + flowRate, deadVolume_1M, deadVolume_1D, deadVolume_2M, deadVolume_2D, numTanks_1M, numTanks_1D, numTanks_2M, numTanks_2D, splitRatio_1, splitRatio_2, feedMoleFrac = inputParameters + + # Total number of tanks [-] + numTanksTotal = numTanks_1M + numTanks_2M + numTanks_1D + numTanks_2D + + # Total number of tanks of individual volumes [-] + numTanksTotal_1 = numTanks_1M + numTanks_1D # Initialize the derivatives to zero - df = np.zeros([numberOfTanks]) + df = np.zeros([numTanksTotal]) - # Volume of each tank - volumeOfTank = deadVolume/numberOfTanks + # Volume of each tank in each section + volTank_1M = deadVolume_1M/numTanks_1M + volTank_1D = deadVolume_1D/numTanks_1D + volTank_2M = deadVolume_2M/numTanks_2M + volTank_2D = deadVolume_2D/numTanks_2D - # Residence time of each tank - residenceTime = volumeOfTank/flowRate - - # Solve the ode - df[0] = ((1/residenceTime)*(feedMoleFrac - f[0])) - df[1:numberOfTanks] = ((1/residenceTime) - *(f[0:numberOfTanks-1] - f[1:numberOfTanks])) + # Residence time of each tank in the mixing and diffusive volume + residenceTime_1M = volTank_1M/(splitRatio_1*flowRate) + residenceTime_1D = volTank_1D/((1-splitRatio_1)*flowRate) + residenceTime_2M = volTank_2M/(splitRatio_2*flowRate) + residenceTime_2D = volTank_2D/((1-splitRatio_2)*flowRate) + + # Solve the odes + # Volume 1: Mixing volume + df[0] = ((1/residenceTime_1M)*(feedMoleFrac - f[0])) + df[1:numTanks_1M] = ((1/residenceTime_1M) + *(f[0:numTanks_1M-1] - f[1:numTanks_1M])) + + # Volume 1: Diffusive volume + df[numTanks_1M] = ((1/residenceTime_1D)*(feedMoleFrac - f[numTanks_1M])) + df[numTanks_1M+1:numTanksTotal_1] = ((1/residenceTime_1D) + *(f[numTanks_1M:numTanksTotal_1-1] + - f[numTanks_1M+1:numTanksTotal_1])) + # Compute the outlet composition for volume 1 + yOut_1 = (splitRatio_1*flowRate*f[numTanks_1M-1] + + (1-splitRatio_1)*flowRate*f[numTanksTotal_1-1])/flowRate + + # Volume 2: Mixing volume + df[numTanksTotal_1] = ((1/residenceTime_2M)*(yOut_1 - f[numTanksTotal_1])) + df[numTanksTotal_1+1:numTanks_2M] = ((1/residenceTime_2M) + *(f[numTanksTotal_1:numTanks_2M-1] + - f[numTanksTotal_1+1:numTanks_2M])) + + # Volume 2: Diffusive volume + df[numTanksTotal_1+numTanks_2D] = ((1/residenceTime_2D) + *(yOut_1 - f[numTanksTotal_1+numTanks_2D])) + df[numTanksTotal_1+numTanks_2D+1:numTanksTotal] = ((1/residenceTime_2D) + *(f[numTanksTotal_1+numTanks_2D:numTanksTotal-1] + - f[numTanksTotal_1+numTanks_2D+1:numTanksTotal])) + # Return the derivatives for the solver return df @@ -134,12 +225,12 @@ def plotOutletConcentration(timeSim, moleFracIn, moleFracOut): ax.plot(timeSim, moleFracIn, linewidth=1.5,color='b', label = 'In') - ax.plot(timeSim, moleFracOut, + ax.semilogy(timeSim, moleFracOut, linewidth=1.5,color='r', label = 'Out') ax.set(xlabel='$t$ [s]', ylabel='$y$ [-]', - xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(moleFracOut)]) + xlim = [timeSim[0], 1000], ylim = [1e-4, 1.1*np.max(moleFracOut)]) ax.legend() plt.show() os.chdir(".."+os.path.sep+"experimental") \ No newline at end of file From fc4f296355c10ef0d6e917178d50d600d9ff9169 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 14 Apr 2021 19:05:33 +0100 Subject: [PATCH 054/189] Bug fix --- experimental/extractDeadVolume.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 58bcc41..95bc5cb 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -14,6 +14,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-04-14, AK: Bug fix # - 2021-04-14, AK: Change strucure and perform series of parallel CSTRs # - 2021-04-12, AK: Add functionality for multiple experiments # - 2021-03-25, AK: Estimate parameters using experimental data @@ -49,10 +50,11 @@ def extractDeadVolume(): currentDT = auxiliaryFunctions.getCurrentDateTime() # Directory of raw data - mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' + mainDir = 'runData/' # File name of the experiments fileName = ['ZLC_DeadVolume_Exp12D_Output.mat', - 'ZLC_DeadVolume_Exp12E_Output.mat'] + 'ZLC_DeadVolume_Exp12E_Output.mat', + 'ZLC_DeadVolume_Exp12F_Output.mat'] # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName) @@ -65,7 +67,7 @@ def extractDeadVolume(): optType=np.array(['real','real','real','real','int','int','int','int','real','real']) # Algorithm parameters for GA - algorithm_param = {'max_num_iteration':20, + algorithm_param = {'max_num_iteration':25, 'population_size':400, 'mutation_probability':0.1, 'crossover_probability': 0.55, @@ -98,7 +100,7 @@ def extractDeadVolume(): # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationResults')): - os.mkdir('simulationResults') + os.mkdir(os.path.join('..','simulationResults')) # Save the output into a .npz file From b74e94ad3aff9c450d0baed65f8f2ac97948350a Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 14 Apr 2021 19:44:03 +0100 Subject: [PATCH 055/189] More bug fix --- experimental/extractDeadVolume.py | 2 +- experimental/processExpMatFile.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 95bc5cb..76e2558 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -50,7 +50,7 @@ def extractDeadVolume(): currentDT = auxiliaryFunctions.getCurrentDateTime() # Directory of raw data - mainDir = 'runData/' + mainDir = 'experimental/runData' # File name of the experiments fileName = ['ZLC_DeadVolume_Exp12D_Output.mat', 'ZLC_DeadVolume_Exp12E_Output.mat', diff --git a/experimental/processExpMatFile.py b/experimental/processExpMatFile.py index 529c8ad..4d5b121 100644 --- a/experimental/processExpMatFile.py +++ b/experimental/processExpMatFile.py @@ -30,12 +30,12 @@ def processExpMatFile(mainDir, fileName): import os from numpy import savez import socket - + # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() - + # Path of the file name - fileToLoad = mainDir + fileName + fileToLoad = os.path.join(mainDir,fileName) # Load .mat file rawData = sio.loadmat(fileToLoad)["experimentOutput"] From a72563b96839db6aef3396e48ae89f967be6699e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 15 Apr 2021 11:47:03 +0100 Subject: [PATCH 056/189] Modify GA parameters and add penalty function --- experimental/extractDeadVolume.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 76e2558..470419b 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -14,6 +14,7 @@ # Reference: 10.1016/j.ces.2008.02.023 # # Last modified: +# - 2021-04-15, AK: Modify GA parameters and add penalty function # - 2021-04-14, AK: Bug fix # - 2021-04-14, AK: Change strucure and perform series of parallel CSTRs # - 2021-04-12, AK: Add functionality for multiple experiments @@ -67,11 +68,12 @@ def extractDeadVolume(): optType=np.array(['real','real','real','real','int','int','int','int','real','real']) # Algorithm parameters for GA - algorithm_param = {'max_num_iteration':25, - 'population_size':400, + algorithm_param = {'max_num_iteration':10, + 'population_size':800, 'mutation_probability':0.1, 'crossover_probability': 0.55, 'parents_portion': 0.15, + 'elit_ratio': 0.01, 'max_iteration_without_improv':None} # Minimize an objective function to compute the dead volume and the number of @@ -137,6 +139,7 @@ def deadVolObjectiveFunction(x): # Initialize error for objective function computedError = 0 numPoints = 0 + expVolume = 0 # Loop over all available files for ii in range(len(filePath)): # Initialize outputs @@ -152,6 +155,9 @@ def deadVolObjectiveFunction(x): # Obtain the mean and round it to the 2 decimal to be used in the # simulation flowRate = round(np.mean(load(filePath[ii])["flowRate"]),2) + + # Compute the experimental volume (using trapz) + expVolume = max([expVolume, np.trapz(moleFracExp,flowRate*timeElapsedExp)]) # Compute the dead volume response using the optimizer parameters timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume_1M = x[0], @@ -177,8 +183,14 @@ def deadVolObjectiveFunction(x): numPoints += len(moleFracExp) computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) - # Compute the sum of the error for the difference between exp. and sim. - return (numPoints/2)*computedError + # Penalize if the total volume of the system is greater than experiemntal + # volume + penaltyObj = 0 + if sum(x[0:4])>1.5*expVolume: + penaltyObj = 10000 + # Compute the sum of the error for the difference between exp. and sim. and + # add a penalty if needed + return (numPoints/2)*computedError + penaltyObj # func: filesToProcess # Loads .mat experimental file and processes it for python From e36ac1b6f0d982c19cdabf7bd08ea1371baa8190 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 15 Apr 2021 19:49:55 +0100 Subject: [PATCH 057/189] Add scripts for mixture experiments using a T junction --- experimental/calibrateMS.m | 9 +++++++-- experimental/runZLC.m | 28 ++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m index 57afd15..272e6d7 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrateMS.m @@ -12,6 +12,7 @@ % Calibrates the mass specfor different set point values % % Last modified: +% - 2021-04-15, AK: Modify function for mixture experiments % - 2021-03-16, AK: Initial creation % % Input arguments: @@ -35,12 +36,16 @@ MFC1_SP = repmat([0.0 0.2 1.5 3.0 4.5, 6.0 7.5, 9.0 10.5 12.0 13.5 14.8 15.0],[1,2]); % Define set point for MFC2 MFC2_SP = 15.0-MFC1_SP; + % Start delay (used for adsorbent equilibration) + expInfo.equilibrationTime = 1800; % [s] + % Flag for meter calibration + expInfo.calibrateMeters = false; + % Mixtures Flag - When a T junction instead of 6 way valve used + expInfo.runMixtures = true; % Loop through all setpoints to calibrate the meters for ii=1:length(MFC1_SP) expInfo.MFC1_SP = MFC1_SP(ii); expInfo.MFC2_SP = MFC2_SP(ii); - % Flag for meter calibration - expInfo.calibrateMeters = true; % When the set point goes back to zero wait for 5 more min before % starting the measurement if ii == find(MFC1_SP == 0,1,'last') diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 5b7dc07..b80fc47 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-04-15, AK: Modify function for mixture experiments % - 2021-04-07, AK: Add MFM with MFC1 and MFC2, add interval for MFC % collection % - 2021-03-25, AK: Fix rounding errors @@ -55,8 +56,12 @@ function runZLC(varargin) expInfo.MFC1_SP = 15.0; % Define gas for MFC2 expInfo.MFC2_SP = 15.0; + % Adsorbemt equilibration time (start delay for the timer) + expInfo.equilibrationTime = 5; % [s] % Calibrate meters flag expInfo.calibrateMeters = false; + % Mixtures Flag - When a T junction instead of 6 way valve used + expInfo.runMixtures = false; % Cannot be true for calibration meters else % Use the value passed to the function currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); @@ -104,7 +109,7 @@ function runZLC(varargin) timerDevice.ExecutionMode = 'fixedRate'; timerDevice.BusyMode = 'drop'; timerDevice.Period = expInfo.samplingTime; % [s] - timerDevice.StartDelay = 5; % [s] + timerDevice.StartDelay = expInfo.equilibrationTime; % [s] timerDevice.TasksToExecute = floor((expInfo.maxTime)/expInfo.samplingTime); % Specify timer callbacks @@ -188,11 +193,30 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) % Initialize outputs MFM = []; MFC1 = []; MFC2 = []; UMFM = []; % Get user input to indicate switching of the valve - if timerObj.tasksExecuted == 1 && ~expInfo.calibrateMeters + if timerObj.tasksExecuted == 1 && ~expInfo.calibrateMeters && ~expInfo.runMixtures % Waiting for user to switch the valve promptUser = 'Switch asap! When you press Y, the gas switches (you wish)! [Y/N]: '; userInput = input(promptUser,'s'); end + % If mixtures is run, at the first instant turn off CO2 (MFC2) + if timerObj.tasksExecuted == 1 && expInfo.runMixtures && ~isempty(serialObj.MFC2.portName) + % Parse out gas name from expInfo + gasName_MFC2 = expInfo.gasName_MFC2; + % Generate Gas ID for Alicat devices + gasID_MFC2 = checkGasName(gasName_MFC2); + [~] = controlAuxiliaryEquipments(serialObj.MFC2, gasID_MFC2,1); % Set gas for MFC2 + % Generate serial command for volumteric flow rate set point + cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); + outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string + % Rounding required due to rounding errors. Differences of around + % eps can be observed + if round(str2double(outputMFC2Temp(6)),4) ~= round(0,4) + error("You should not be here!!!") + end + end % Get the sampling date/time currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> Performing task #', num2str(timerObj.tasksExecuted)]) From e7458ae3794d27a7f31a344880b03c67ae5bb2f8 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 15 Apr 2021 19:50:17 +0100 Subject: [PATCH 058/189] Add multiple ZLC experiments functionality --- experimental/runMultipleZLC.m | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 experimental/runMultipleZLC.m diff --git a/experimental/runMultipleZLC.m b/experimental/runMultipleZLC.m new file mode 100644 index 0000000..4a69184 --- /dev/null +++ b/experimental/runMultipleZLC.m @@ -0,0 +1,62 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Runs multiple ZLC experiments in series +% +% Last modified: +% - 2021-04-15, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function runMultipleZLC + % Series name for the experiments + expSeries = 'ZLC_ActivatedCarbon_Exp10'; + % Maximum time of the experiment + expInfo.maxTime = 3600; + % Sampling time for the device + expInfo.samplingTime = 1; + % Intervals for collecting MFC data + expInfo.MFCInterval = 50; + % Define gas for MFM + expInfo.gasName_MFM = 'He'; + % Define gas for MFC1 + expInfo.gasName_MFC1 = 'He'; + % Define gas for MFC2 + expInfo.gasName_MFC2 = 'CO2'; + % Total flow rate + expTotalFlowRate = [2 4 10 20 30 40 60]; + % Fraction CO2 + fracCO2 = 0.05; + % Define set point for MFC1 + MFC1_SP = expTotalFlowRate; + % Define set point for MFC2 + MFC2_SP = fracCO2*expTotalFlowRate; + % Start delay (used for adsorbent equilibration) + expInfo.equilibrationTime = 1800; % [s] + % Flag for meter calibration + expInfo.calibrateMeters = false; + % Mixtures Flag - When a T junction instead of 6 way valve used + expInfo.runMixtures = true; + % Loop through all setpoints to calibrate the meters + for ii=1:length(MFC1_SP) + % Experiment name + expInfo.expName = [expSeries,char(64+ii)]; + expInfo.MFC1_SP = MFC1_SP(ii); + expInfo.MFC2_SP = MFC2_SP(ii); + % Run the setup for different calibrations + runZLC(expInfo) + % Wait for 1 min before starting the next experiment + pause(30) + end +end From e6ee48ddc389925ebe9cb9fc865332eb1ba63862 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 16 Apr 2021 10:20:34 +0100 Subject: [PATCH 059/189] Add plots for experimental outcome --- plotFunctions/plotExperimentOutcome.py | 189 +++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 plotFunctions/plotExperimentOutcome.py diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py new file mode 100644 index 0000000..0c5e389 --- /dev/null +++ b/plotFunctions/plotExperimentOutcome.py @@ -0,0 +1,189 @@ +############################################################################ +# +# 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 experimental outcome (along with model) +# +# Last modified: +# - 2021-04-16, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from simulateDeadVolume import simulateDeadVolume +from numpy import load +import os +from scipy.interpolate import interp1d +import matplotlib.pyplot as plt +import auxiliaryFunctions +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 flag for plot +saveFlag = False + +# Save file extension +saveFileExtension = ".png" + +# Flag to plot dead volume results +flagDeadVolume = True + +# Flag to plot simulations +simulateModel = False + +# Flag to plot dead volume results +plotFt = False + +# Directory of raw data +mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' + +colorsForPlot = ["#E5383B","#B55055","#6C757D"] + +if flagDeadVolume: + # File name of the experiments + fileName = ['ZLC_DeadVolume_Exp12D_Output_10f6fad.npz', + 'ZLC_DeadVolume_Exp12E_Output_10f6fad.npz', + 'ZLC_DeadVolume_Exp12F_Output_10f6fad.npz'] + + # File with parameter estimates + simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' + fileParameter = 'deadVolumeCharacteristics_20210415_1657_a72563b.npz' + modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] + x = modelOutputTemp[()]["variable"] + # Print the objective function and volume from model parameters + print("Objective Function",round(modelOutputTemp[()]["function"],0)) + print("Model Volume",round(sum(x[0:4]),2)) + # Initialize error for objective function + # Loop over all available files + for ii in range(len(fileName)): + # Initialize outputs + timeSimOut = [] + moleFracOut = [] + moleFracSim = [] + # Path of the file name + fileToLoad = mainDir + fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExp = load(fileToLoad)["moleFrac"].flatten() + # Parse out flow rate of the experiment + # Obtain the mean and round it to the 2 decimal to be used in the + # simulation + flowRate = round(np.mean(load(fileToLoad)["flowRate"]),2) + + # Print experimental volume + print("Experiment",str(ii+1),round(np.trapz(moleFracExp, + np.multiply(load(fileToLoad)["flowRate"].flatten(), + timeElapsedExp)),2)) + if simulateModel: + # Compute the dead volume response using the optimizer parameters + timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume_1M = x[0], + deadVolume_1D = x[1], + deadVolume_2M = x[2], + deadVolume_2D = x[3], + numTanks_1M = int(x[4]), + numTanks_1D = int(x[5]), + numTanks_2M = int(x[6]), + numTanks_2D = int(x[7]), + splitRatio_1 = x[8], + splitRatio_2 = x[9], + flowRate = flowRate) + + # Interpolate simulation data (generate function) + interpSim = interp1d(timeSimOut, moleFracOut) + + # Find the interpolated simulation mole fraction at times corresponding to + # the experimental ones + moleFracSim = interpSim(timeElapsedExp) + + # Print simulation volume + print("Simulation",str(ii+1),round(np.trapz(moleFracSim, + np.multiply(load(fileToLoad)["flowRate"].flatten(), + timeElapsedExp)),2)) + + # Plot the expreimental and model output + if not plotFt: + # Linear scale + fig = plt.figure + ax1 = plt.subplot(1,2,1) + ax1.plot(timeElapsedExp,moleFracExp, + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + if simulateModel: + ax1.plot(timeElapsedExp,moleFracSim, + color=colorsForPlot[ii]) # Simulation response + ax1.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = [0,50], ylim = [0, 1]) + ax1.legend() + + # Log scale + ax2 = plt.subplot(1,2,2) + ax2.semilogy(timeElapsedExp,moleFracExp, + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + if simulateModel: + ax2.semilogy(timeElapsedExp,moleFracSim, + color=colorsForPlot[ii]) # Simulation response + ax2.set(xlabel='$t$ [s]', + xlim = [0,250], ylim = [1e-3, 1]) + ax2.legend() + + # Save the figure + if saveFlag: + # FileName: deadVolumeCharacteristics___ + saveFileName = "deadVolumeCharacteristics_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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) + else: + # Linear scale + fig = plt.figure + ax1 = plt.subplot(1,2,1) + ax1.plot(np.multiply(load(fileToLoad)["flowRate"].flatten(),timeElapsedExp),moleFracExp, + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + if simulateModel: + ax1.plot(flowRate*timeElapsedExp,moleFracSim, + color=colorsForPlot[ii]) # Simulation response + ax1.set(xlabel='$Ft$ [cc]', + ylabel='$y_1$ [-]', + xlim = [0,50], ylim = [0, 1]) + ax1.legend() + + # Log scale + ax2 = plt.subplot(1,2,2) + ax2.semilogy(np.multiply(load(fileToLoad)["flowRate"].flatten(),timeElapsedExp),moleFracExp, + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + if simulateModel: + ax2.semilogy(flowRate*timeElapsedExp,moleFracSim, + color=colorsForPlot[ii]) # Simulation response + ax2.set(xlabel='$Ft$ [cc]', + xlim = [0,30], ylim = [1e-2, 1]) + ax2.legend() + + # Save the figure + if saveFlag: + # FileName: deadVolumeCharacteristicsFt___ + saveFileName = "deadVolumeCharacteristicsFt_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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) \ No newline at end of file From 767872310d9d8308701fe051167ea23e1b6d421a Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 16 Apr 2021 17:32:20 +0100 Subject: [PATCH 060/189] Small fix --- experimental/analyzeExperiment.m | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 32d2f1d..2d87f21 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -36,8 +36,8 @@ % Mode to switch between calibration and analyzing real experiment if flagCalibration experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_20210413'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210413.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLCCalibrateMS_20210414'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210414.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) experimentStruct.numMean = 10; % Number of points for averaging experimentStruct.flagUseIndGas = false; % Flag to determine whether independent (true) or ratio of signals used for calibration @@ -45,12 +45,12 @@ % Call reconcileData function for calibration of the MS analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file else - setTotalFlowRate = 15; % Total flow rate of the experiment [ccm] - moleFracThreshold = 1e-4; % Threshold to cut data below a given mole fraction [-] + setTotalFlowRate = 60; % Total flow rate of the experiment [ccm] + moleFracThreshold = 1e-3; % Threshold to cut data below a given mole fraction [-] experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLC_DeadVolume_Exp12D'; % Experimental flow file (.mat) - experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210413_Model'; % Experimental calibration file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_DeadVolume_Exp12D.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLC_ActivatedCarbon_Exp10G'; % Experimental flow file (.mat) + experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210414_Model'; % Experimental calibration file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_ActivatedCarbon_Exp10.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) % Call reconcileData function to get the output mole fraction for a % real experiment @@ -90,6 +90,11 @@ % Find the index for the mole fraction that corresponds to the % threshold mole fraction moleFracThresholdInd = find(outputStruct.moleFrac(:,2) Date: Mon, 19 Apr 2021 14:10:41 +0100 Subject: [PATCH 061/189] Add mutlitple meters for calibrating flow meters and fix for calibrate MS --- experimental/calibrateMS.m | 8 ++++---- experimental/calibrateMeters.m | 26 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m index 272e6d7..03b6e3d 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrateMS.m @@ -36,12 +36,12 @@ MFC1_SP = repmat([0.0 0.2 1.5 3.0 4.5, 6.0 7.5, 9.0 10.5 12.0 13.5 14.8 15.0],[1,2]); % Define set point for MFC2 MFC2_SP = 15.0-MFC1_SP; - % Start delay (used for adsorbent equilibration) - expInfo.equilibrationTime = 1800; % [s] + % Start delay + expInfo.equilibrationTime = 5; % [s] % Flag for meter calibration - expInfo.calibrateMeters = false; + expInfo.calibrateMeters = true; % Mixtures Flag - When a T junction instead of 6 way valve used - expInfo.runMixtures = true; + expInfo.runMixtures = false; % Loop through all setpoints to calibrate the meters for ii=1:length(MFC1_SP) expInfo.MFC1_SP = MFC1_SP(ii); diff --git a/experimental/calibrateMeters.m b/experimental/calibrateMeters.m index 7569290..3563440 100644 --- a/experimental/calibrateMeters.m +++ b/experimental/calibrateMeters.m @@ -12,6 +12,7 @@ % Calibrates the flow meter and controller for different set point values % % Last modified: +% - 2021-04-19, AK: Change functionality for mixtures % - 2021-03-16, AK: Add calibrate meters flag % - 2021-03-12, AK: Initial creation % @@ -27,7 +28,7 @@ % Maximum time of the experiment expInfo.maxTime = 60; % Sampling time for the device - expInfo.samplingTime = 2; + expInfo.samplingTime = 5; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -36,13 +37,24 @@ expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 MFC1_SP = [0.0, 15.0, 30.0, 45.0, 60.0]; - + % Start delay + expInfo.equilibrationTime = 0; % [s] + % Flag for meter calibration + expInfo.calibrateMeters = true; + % Mixtures Flag - When a T junction instead of 6 way valve used + expInfo.runMixtures = false; + % Loop through all setpoints to calibrate the meters for ii=1:length(MFC1_SP) - expInfo.MFC1_SP = MFC1_SP(ii); - % Flag for meter calibration - expInfo.calibrateMeters = true; - % Run the setup for different calibrations - runZLC(expInfo) + for jj=1:length(MFC1_SP) + % Set point for MFC1 + expInfo.MFC1_SP = MFC1_SP(ii); + % Set point for MFC2 (same as MFC1) + expInfo.MFC2_SP = MFC1_SP(jj); + % Flag for meter calibration + expInfo.calibrateMeters = true; + % Run the setup for different calibrations + runZLC(expInfo) + end end end From e23da38cbae180b3b47d4ddc87da956293cbfcbc Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 19 Apr 2021 16:13:48 +0100 Subject: [PATCH 062/189] Major revamp for flow meter and controller calibration (for mixtures) --- experimental/analyzeCalibration.m | 98 ++++++++++++++++++++----------- experimental/analyzeExperiment.m | 67 +++++++++++---------- experimental/concatenateData.m | 5 +- experimental/runZLC.m | 2 +- 4 files changed, 100 insertions(+), 72 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index b4c8931..cd0c942 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-19, AK: Change MFC and MFM calibration (for mixtures) % - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-04-07, AK: Modify for addition of MFM % - 2021-03-26, AK: Fix for number of repetitions @@ -47,30 +48,38 @@ function analyzeCalibration(parametersFlow,parametersMS) UMFM = [flowData.outputStruct.UMFM]; % UMFM % Get the volumetric flow rate volFlow_MFM = [MFM.volFlow]; - volFlow_MFC1 = [MFC1.volFlow]; - gas_MFC1 = [MFC1.gas]; - volFlow_UMFM = [UMFM.volFlow]; - % Find indices that corresponds to He/CO2 in the MFC - indexHe = find(gas_MFC1 == 'He'); - indexCO2 = find(gas_MFC1 == 'CO2'); - % Parse the flow rate from the MFC, MFM, and UMFM for each gas + volFlow_MFC1 = [MFC1.volFlow]; % Flow rate for MFC1 [ccm] + setPt_MFC1 = [MFC1.setpoint]; % Set point for MFC1 + volFlow_MFC2 = [MFC2.volFlow]; % Flow rate for MFC2 [ccm] + setPt_MFC2 = [MFC2.setpoint]; % Set point for MFC2 + volFlow_UMFM = [UMFM.volFlow]; % Flow rate for UMFM [ccm] + % Find indices corresponding to pure gases + indexPureHe = find(setPt_MFC2 == 0); % Find pure He index + indexPureCO2 = find(setPt_MFC1 == 0); % Find pure CO2 index + % Parse the flow rate from the MFC, MFM, and UMFM for pure gas % MFC - volFlow_MFC1_He = volFlow_MFC1(indexHe); - volFlow_MFC1_CO2 = volFlow_MFC1(indexCO2); - % MFM - volFlow_MFM_He = volFlow_MFM(indexHe); - volFlow_MFM_CO2 = volFlow_MFM(indexCO2); - % UMFM - volFlow_UMFM_He = volFlow_UMFM(indexHe); - volFlow_UMFM_CO2 = volFlow_UMFM(indexCO2); + volFlow_MFC1_PureHe = volFlow_MFC1(indexPureHe); + volFlow_MFC2_PureCO2 = volFlow_MFC2(indexPureCO2); + % UMFM for pure gases + volFlow_UMFM_PureHe = volFlow_UMFM(indexPureHe); + volFlow_UMFM_PureCO2 = volFlow_UMFM(indexPureCO2); + % Calibrate the MFC + calibrationFlow.MFC_He = volFlow_MFC1_PureHe'\volFlow_UMFM_PureHe'; % MFC 1 + calibrationFlow.MFC_CO2 = volFlow_MFC2_PureCO2'\volFlow_UMFM_PureCO2'; % MFC 2 - % Calibrate the meters - % MFC - calibrationFlow.MFC_He = volFlow_MFC1_He'\volFlow_UMFM_He'; - calibrationFlow.MFC_CO2 = volFlow_MFC1_CO2'\volFlow_UMFM_CO2'; - % MFM - calibrationFlow.MFM_He = volFlow_MFM_He'\volFlow_UMFM_He'; - calibrationFlow.MFM_CO2 = volFlow_MFM_CO2'\volFlow_UMFM_CO2'; + % Compute the mole fraction of CO2 using flow data + moleFracCO2 = (calibrationFlow.MFC_CO2*volFlow_MFC2)./... + (calibrationFlow.MFC_CO2*volFlow_MFC2 + calibrationFlow.MFC_He*volFlow_MFC1); + indNoNan = ~isnan(moleFracCO2); % Find indices correponsing to no Nan + % Calibrate the MFM + % Fit a 23 (2nd order in mole frac and 3rd order in MFM flow) to UMFM + % Note that the MFM flow rate corresponds to He gas configuration in + % the MFM + modelFlow = fit([moleFracCO2(indNoNan)',volFlow_MFM(indNoNan)'],volFlow_UMFM(indNoNan)','poly23'); + calibrationFlow.MFM = modelFlow; + + % Also save the raw data into the calibration file + calibrationFlow.rawData = flowData; % Save the calibration data into a .mat file % Check if calibration data folder exists @@ -89,26 +98,47 @@ function analyzeCalibration(parametersFlow,parametersMS) 'gitCommitID'); end - % Plot the raw and the calibrated data + % Plot the raw and the calibrated data (for pure gases at MFC) figure MFC1Set = 0:80; - subplot(2,2,1) + subplot(1,2,1) hold on - scatter(volFlow_MFC1_He,volFlow_UMFM_He,'or') + scatter(volFlow_MFC1_PureHe,volFlow_UMFM_PureHe,'or') plot(MFC1Set,calibrationFlow.MFC_He*MFC1Set,'b') - subplot(2,2,2) + xlim([0 1.1*max(volFlow_MFC1_PureHe)]); + ylim([0 1.1*max(volFlow_UMFM_PureHe)]); + box on; grid on; + xlabel('He MFC Flow Rate [ccm]') + ylabel('He Actual Flow Rate [ccm]') + subplot(1,2,2) hold on - scatter(volFlow_MFC1_CO2,volFlow_UMFM_CO2,'or') + scatter(volFlow_MFC2_PureCO2,volFlow_UMFM_PureCO2,'or') plot(MFC1Set,calibrationFlow.MFC_CO2*MFC1Set,'b') - subplot(2,2,3) - hold on - scatter(volFlow_MFM_He,volFlow_UMFM_He,'or') - plot(MFC1Set,calibrationFlow.MFM_He*MFC1Set,'b') - subplot(2,2,4) + xlim([0 1.1*max(volFlow_MFC2_PureCO2)]); + ylim([0 1.1*max(volFlow_UMFM_PureCO2)]); + box on; grid on; + xlabel('CO2 MFC Flow Rate [ccm]') + ylabel('CO2 Actual Flow Rate [ccm]') + + % Plot the raw and the calibrated data (for mixtures at MFM) + figure + x = 0:0.1:1; % Mole fraction + y = 0:1:150; % Total flow rate + [X,Y] = meshgrid(x,y); % Create a grid for the flow model + Z = modelFlow(X,Y); % Actual flow rate from the model % [ccm] hold on - scatter(volFlow_MFM_CO2,volFlow_UMFM_CO2,'or') - plot(MFC1Set,calibrationFlow.MFM_CO2*MFC1Set,'b') + surf(X,Y,Z,'FaceAlpha',0.25,'EdgeColor','none'); + scatter3(moleFracCO2,volFlow_MFM,volFlow_UMFM,'r'); + xlim([0 1.1*max(X(:))]); + ylim([0 1.1*max(Y(:))]); + zlim([0 1.1*max(Z(:))]); + box on; grid on; + xlabel('CO2 Mole Fraction [-]') + ylabel('MFM Flow Rate [ccm]') + zlabel('Actual Flow Rate [ccm]') + view([30 30]) end + % Load the file that contains the MS calibration if ~isempty(parametersMS) % Call reconcileData function for calibration of the MS diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 2d87f21..8af8e62 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -14,6 +14,7 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-04-19, AK: Major revamp for flow rate computation % - 2021-04-13, AK: Add threshold to cut data below a given mole fraction % - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-03-24, AK: Add flow rate computation and prepare structure for @@ -33,22 +34,35 @@ % Flag to decide calibration or analysis flagCalibration = false; +% Flag to decide calibration of flow meters or MS +flagFlowMeter = true; + % Mode to switch between calibration and analyzing real experiment +% Analyze calibration data if flagCalibration - experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_20210414'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210414.asc'; % Experimental MS file (.asc) - experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) - experimentStruct.numMean = 10; % Number of points for averaging - experimentStruct.flagUseIndGas = false; % Flag to determine whether independent (true) or ratio of signals used for calibration - experimentStruct.polyDeg = 3; % Degree of polynomial fit for independent gas calibraiton - % Call reconcileData function for calibration of the MS - analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file + % Calibrate flow meter + if flagFlowMeter + % File with the calibration data to build a model for MFC/MFM + experimentStruct = 'ZLCCalibrateMeters_20210419_E'; % Experimental flow file (.mat) + % Call analyzeCalibration function for calibration of the MS + analyzeCalibration(experimentStruct,[]) % Call the function to generate the calibration file + % Calibrate MS + else + experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) + experimentStruct.flow = 'ZLCCalibrateMS_20210414'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210414.asc'; % Experimental MS file (.asc) + experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) + experimentStruct.numMean = 10; % Number of points for averaging + experimentStruct.flagUseIndGas = false; % Flag to determine whether independent (true) or ratio of signals used for calibration + experimentStruct.polyDeg = 3; % Degree of polynomial fit for independent gas calibraiton + % Call analyzeCalibration function for calibration of the MS + analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file + end +% Analyze real experiment else - setTotalFlowRate = 60; % Total flow rate of the experiment [ccm] moleFracThreshold = 1e-3; % Threshold to cut data below a given mole fraction [-] - experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLC_ActivatedCarbon_Exp10G'; % Experimental flow file (.mat) + experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210419_E_Model'; % Calibration file for meters (.mat) + experimentStruct.flow = 'ZLC_ActivatedCarbon_Exp10A'; % Experimental flow file (.mat) experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210414_Model'; % Experimental calibration file (.mat) experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_ActivatedCarbon_Exp10.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) @@ -60,31 +74,16 @@ % Replace all negative molefraction with eps outputStruct.moleFrac(outputStruct.moleFrac(:,2)<0,1)=eps; % CO2 outputStruct.moleFrac(:,1)=1-outputStruct.moleFrac(:,2); % Compute He with mass balance - + % Convert the MFM flow to real flow % Load the meter calibrations load(experimentStruct.calibrationFlow); - % Flow rate when there is pure He (He equivalent) - flowRateAtPureHe = setTotalFlowRate*calibrationFlow.MFC_He/calibrationFlow.MFM_He; - % Flow rate when there is pure CO2 (He equivalent) - flowRateAtPureCO2 = setTotalFlowRate*calibrationFlow.MFC_CO2/calibrationFlow.MFM_CO2; - % Compute the slope of the line (to relate mole fraction of CO2 to the - % change in the flow rate of He equivalent in MFM) - delFlowRate = flowRateAtPureHe - flowRateAtPureCO2; - % Compute the flow of CO2 (He equivalent) at the different mole - % fractions as the experiment progress (by doing a mass balance) - flowRateCO2 = outputStruct.moleFrac(:,2).*(flowRateAtPureHe-delFlowRate.*outputStruct.moleFrac(:,2)); - % Compute the real flow rate of CO2 (by performing a transformation - % from He equivalent CO2 to UMFM) [ccm] - realFlowRateCO2 = flowRateCO2*calibrationFlow.MFM_CO2; - % Compute the flow of He (He equivalent) at the different mole - % fractions as the experiment progress (by doing a mass balance) - flowRateHe = (1-outputStruct.moleFrac(:,2)).*(flowRateAtPureHe-delFlowRate.*outputStruct.moleFrac(:,2)); - % Compute the real flow rate of He (by performing a transformation - % from He equivalent He to UMFM) [ccm] - realFlowRateHe = flowRateHe*calibrationFlow.MFM_He; + % Get the MFM flow rate + volFlow_MFM = outputStruct.flow(:,2); + % Get the CO2 mole fraction for obtaining real flow rate + moleFracCO2 = outputStruct.moleFrac(:,2); % Compute the total flow rate of the gas [ccm] - totalFlowRate = realFlowRateHe+realFlowRateCO2; + totalFlowRate = calibrationFlow.MFM(moleFracCO2,volFlow_MFM); % Input for the ZLC script (Python) % Find the index for the mole fraction that corresponds to the @@ -98,7 +97,7 @@ experimentOutput.timeExp = outputStruct.flow(1:moleFracThresholdInd,1); % Time elapsed [s] experimentOutput.moleFrac = outputStruct.moleFrac(1:moleFracThresholdInd,2); % Mole fraction CO2 [-] experimentOutput.totalFlowRate = totalFlowRate(1:moleFracThresholdInd)./60; % Total flow rate of the gas [ccs] - experimentOutput.setTotalFlowRate = setTotalFlowRate/60; % Set point for total flow rate [ccs] + % Save the experimental output into a .mat file % Check if runData data folder exists if exist(['experimentalData',filesep,... diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index e694b51..59e4d72 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-19, AK: Remove MFM calibration (check analyzeExperiment) % - 2021-04-09, AK: Change output for calibration or non calibrate mode % - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-04-07, AK: Modify for addition of MFM @@ -52,8 +53,6 @@ % Apply the calibration for the flows volFlow_He = volFlow_MFC1*calibrationFlow.MFC_He; volFlow_CO2 = volFlow_MFC2*calibrationFlow.MFC_CO2; - % Flow is converted assuming helium calibration for MFM - volFlow_MFM = volFlow_MFM*calibrationFlow.MFM_He; % Load MS Ascii data % Create file identifier @@ -184,7 +183,7 @@ % Get the mole fraction used for the calibration % This will be used in the analyzeCalibration script - if ~isfield(fileToLoad,'calibrationMS') + if expInfo.calibrateMeters % Compute the mole fractions using the reconciled flow data reconciledData.moleFrac(:,1) = (reconciledData.flow(:,2))./(reconciledData.flow(:,2)+reconciledData.flow(:,3)); reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); diff --git a/experimental/runZLC.m b/experimental/runZLC.m index b80fc47..f2945c3 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -253,7 +253,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC2.temperature = str2double(outputMFC2Temp(3)); % [C] MFC2.volFlow = str2double(outputMFC2Temp(4)); % device units [ml/min] MFC2.massFlow = str2double(outputMFC2Temp(5)); % standard units [sccm] - MFC2.setpoint = outputMFC2Temp(6); % device units [ml/min] + MFC2.setpoint = str2double(outputMFC2Temp(6)); % device units [ml/min] MFC2.gas = outputMFC2Temp(7); % gas in the controller end % Get the current state of the universal flow controller From 98f748b43b31b63f073cad24c83f60b8133d276d Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 19 Apr 2021 16:28:23 +0100 Subject: [PATCH 063/189] Change from individual flow to total flow rate --- experimental/calibrateMeters.m | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/experimental/calibrateMeters.m b/experimental/calibrateMeters.m index 3563440..d9bab91 100644 --- a/experimental/calibrateMeters.m +++ b/experimental/calibrateMeters.m @@ -12,6 +12,7 @@ % Calibrates the flow meter and controller for different set point values % % Last modified: +% - 2021-04-19, AK: Change from individual flow to total flow rate % - 2021-04-19, AK: Change functionality for mixtures % - 2021-03-16, AK: Add calibrate meters flag % - 2021-03-12, AK: Initial creation @@ -26,7 +27,7 @@ expInfo.expName = ['ZLCCalibrateMeters','_',... datestr(datetime('now'),'yyyymmdd')]; % Maximum time of the experiment - expInfo.maxTime = 60; + expInfo.maxTime = 30; % Sampling time for the device expInfo.samplingTime = 5; % Define gas for MFM @@ -35,22 +36,28 @@ expInfo.gasName_MFC1 = 'He'; % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; + % Set the total flow rate for the calibration + totalFlowRate = [0.0, 2.0, 4.0, 15.0, 30.0, 45.0, 60.0, 80.0, 100.0]; + % Mole fraction of CO2 desired + moleFracCO2 = 0:0.1:1; % Define set point for MFC1 - MFC1_SP = [0.0, 15.0, 30.0, 45.0, 60.0]; + MFC1_SP = totalFlowRate'*(1-moleFracCO2); + % Define set point for MFC2 + MFC2_SP = totalFlowRate'*moleFracCO2; % Start delay - expInfo.equilibrationTime = 0; % [s] + expInfo.equilibrationTime = 10; % [s] % Flag for meter calibration expInfo.calibrateMeters = true; % Mixtures Flag - When a T junction instead of 6 way valve used expInfo.runMixtures = false; % Loop through all setpoints to calibrate the meters - for ii=1:length(MFC1_SP) - for jj=1:length(MFC1_SP) + for ii=1:length(totalFlowRate) + for jj=1:length(moleFracCO2) % Set point for MFC1 - expInfo.MFC1_SP = MFC1_SP(ii); - % Set point for MFC2 (same as MFC1) - expInfo.MFC2_SP = MFC1_SP(jj); + expInfo.MFC1_SP = MFC1_SP(ii,jj); + % Set point for MFC2 + expInfo.MFC2_SP = MFC2_SP(ii,jj); % Flag for meter calibration expInfo.calibrateMeters = true; % Run the setup for different calibrations From 6489bf773209f0741bd044964540f85ba5e5b5d7 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 19 Apr 2021 18:12:45 +0100 Subject: [PATCH 064/189] Round the flow rates to the nearest first decimal --- experimental/analyzeExperiment.m | 10 ++++++---- experimental/concatenateData.m | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 8af8e62..5747187 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -43,7 +43,7 @@ % Calibrate flow meter if flagFlowMeter % File with the calibration data to build a model for MFC/MFM - experimentStruct = 'ZLCCalibrateMeters_20210419_E'; % Experimental flow file (.mat) + experimentStruct = 'ZLCCalibrateMeters_20210419'; % Experimental flow file (.mat) % Call analyzeCalibration function for calibration of the MS analyzeCalibration(experimentStruct,[]) % Call the function to generate the calibration file % Calibrate MS @@ -61,8 +61,8 @@ % Analyze real experiment else moleFracThreshold = 1e-3; % Threshold to cut data below a given mole fraction [-] - experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210419_E_Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLC_ActivatedCarbon_Exp10A'; % Experimental flow file (.mat) + experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210419_Model'; % Calibration file for meters (.mat) + experimentStruct.flow = 'ZLC_ActivatedCarbon_Exp10F'; % Experimental flow file (.mat) experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210414_Model'; % Experimental calibration file (.mat) experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_ActivatedCarbon_Exp10.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) @@ -83,7 +83,9 @@ % Get the CO2 mole fraction for obtaining real flow rate moleFracCO2 = outputStruct.moleFrac(:,2); % Compute the total flow rate of the gas [ccm] - totalFlowRate = calibrationFlow.MFM(moleFracCO2,volFlow_MFM); + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + totalFlowRate = round(calibrationFlow.MFM(moleFracCO2,volFlow_MFM),1); % Input for the ZLC script (Python) % Find the index for the mole fraction that corresponds to the diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index 59e4d72..0a5680b 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -51,8 +51,10 @@ volFlow_MFC2 = [MFC2.volFlow]; % CO2 volFlow_MFM = [MFM.volFlow]; % CO2 % Apply the calibration for the flows - volFlow_He = volFlow_MFC1*calibrationFlow.MFC_He; - volFlow_CO2 = volFlow_MFC2*calibrationFlow.MFC_CO2; + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + volFlow_He = round(volFlow_MFC1*calibrationFlow.MFC_He,1); + volFlow_CO2 = round(volFlow_MFC2*calibrationFlow.MFC_CO2,1); % Load MS Ascii data % Create file identifier From 2cdce7e9bd88c69ab54a7830a89a737c81a95f35 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 19 Apr 2021 18:18:00 +0100 Subject: [PATCH 065/189] Round all the flow meter values to the first decimal --- experimental/calibrateMS.m | 8 ++++++-- experimental/calibrateMeters.m | 8 ++++++-- experimental/runMultipleZLC.m | 8 ++++++-- experimental/runZLC.m | 12 ++++++++---- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m index 03b6e3d..1ec36c7 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrateMS.m @@ -33,9 +33,13 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Define set point for MFC1 - MFC1_SP = repmat([0.0 0.2 1.5 3.0 4.5, 6.0 7.5, 9.0 10.5 12.0 13.5 14.8 15.0],[1,2]); + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + MFC1_SP = round(repmat([0.0 0.2 1.5 3.0 4.5, 6.0 7.5, 9.0 10.5 12.0 13.5 14.8 15.0],[1,2]),1); % Define set point for MFC2 - MFC2_SP = 15.0-MFC1_SP; + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + MFC2_SP = round(15.0-MFC1_SP,1); % Start delay expInfo.equilibrationTime = 5; % [s] % Flag for meter calibration diff --git a/experimental/calibrateMeters.m b/experimental/calibrateMeters.m index d9bab91..d22386d 100644 --- a/experimental/calibrateMeters.m +++ b/experimental/calibrateMeters.m @@ -41,9 +41,13 @@ % Mole fraction of CO2 desired moleFracCO2 = 0:0.1:1; % Define set point for MFC1 - MFC1_SP = totalFlowRate'*(1-moleFracCO2); + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + MFC1_SP = round(totalFlowRate'*(1-moleFracCO2),1); % Define set point for MFC2 - MFC2_SP = totalFlowRate'*moleFracCO2; + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + MFC2_SP = round(totalFlowRate'*moleFracCO2,1); % Start delay expInfo.equilibrationTime = 10; % [s] % Flag for meter calibration diff --git a/experimental/runMultipleZLC.m b/experimental/runMultipleZLC.m index 4a69184..31e82f7 100644 --- a/experimental/runMultipleZLC.m +++ b/experimental/runMultipleZLC.m @@ -39,9 +39,13 @@ % Fraction CO2 fracCO2 = 0.05; % Define set point for MFC1 - MFC1_SP = expTotalFlowRate; + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + MFC1_SP = round(expTotalFlowRate,1); % Define set point for MFC2 - MFC2_SP = fracCO2*expTotalFlowRate; + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + MFC2_SP = round(fracCO2*expTotalFlowRate,1); % Start delay (used for adsorbent equilibration) expInfo.equilibrationTime = 1800; % [s] % Flag for meter calibration diff --git a/experimental/runZLC.m b/experimental/runZLC.m index f2945c3..4efe129 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -165,7 +165,9 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string % Rounding required due to rounding errors. Differences of around % eps can be observed - if round(str2double(outputMFC1Temp(6)),4) ~= round(expInfo.MFC1_SP,4) + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + if round(str2double(outputMFC1Temp(6)),1) ~= round(expInfo.MFC1_SP,1) error("You should not be here!!!") end end @@ -179,8 +181,10 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string % Rounding required due to rounding errors. Differences of around - % eps can be observed - if round(str2double(outputMFC2Temp(6)),4) ~= round(expInfo.MFC2_SP,4) + % eps can be observed + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + if round(str2double(outputMFC2Temp(6)),1) ~= round(expInfo.MFC2_SP,1) error("You should not be here!!!") end end @@ -213,7 +217,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string % Rounding required due to rounding errors. Differences of around % eps can be observed - if round(str2double(outputMFC2Temp(6)),4) ~= round(0,4) + if round(str2double(outputMFC2Temp(6)),1) ~= round(0,1) error("You should not be here!!!") end end From 0228e35eb7be05009db67fff1ead2a69c3684bc1 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 20 Apr 2021 12:00:00 +0100 Subject: [PATCH 066/189] Implement time-resolved experimental flow rate for dead volume calculations --- experimental/extractDeadVolume.py | 33 ++++++++--------- experimental/simulateDeadVolume.py | 45 +++++++++++++++++++---- plotFunctions/plotExperimentOutcome.py | 49 +++++++++++--------------- 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 470419b..0ec7e86 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -12,8 +12,12 @@ # Find the dead volume and the number of tanks to describe the dead volume # using the tanks in series (TIS) for the ZLC # Reference: 10.1016/j.ces.2008.02.023 +# The methodolgy is slighlty modified to incorporate diffusive pockets using +# compartment models (see Levenspiel, chapter 12) or Lisa Joss's article +# Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-15, AK: Modify GA parameters and add penalty function # - 2021-04-14, AK: Bug fix # - 2021-04-14, AK: Change strucure and perform series of parallel CSTRs @@ -104,7 +108,6 @@ def extractDeadVolume(): if not os.path.exists(os.path.join('..','simulationResults')): os.mkdir(os.path.join('..','simulationResults')) - # Save the output into a .npz file savez (savePath, modelOutput = model.output_dict, # Model output optBounds = optBounds, # Optimizer bounds @@ -122,7 +125,6 @@ def deadVolObjectiveFunction(x): import numpy as np from simulateDeadVolume import simulateDeadVolume from numpy import load - from scipy.interpolate import interp1d # Load the names of the file to be used for estimating dead volume characteristics filePath = filesToProcess(False,[],[]) @@ -143,24 +145,23 @@ def deadVolObjectiveFunction(x): # Loop over all available files for ii in range(len(filePath)): # Initialize outputs - timeSimOut = [] - moleFracOut = [] moleFracSim = [] - # Load experimental molefraction + # Load experimental time, molefraction and flowrate (accounting for downsampling) timeElapsedExpTemp = load(filePath[ii])["timeElapsed"].flatten() moleFracExpTemp = load(filePath[ii])["moleFrac"].flatten() + flowRateTemp = load(filePath[ii])["flowRate"].flatten() timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] - # Parse out flow rate of the experiment - # Obtain the mean and round it to the 2 decimal to be used in the - # simulation - flowRate = round(np.mean(load(filePath[ii])["flowRate"]),2) + flowRate = flowRateTemp[::int(np.round(downsampleInt[ii]))] + + # Integration and ode evaluation time (check simulateDeadVolume) + timeInt = timeElapsedExp # Compute the experimental volume (using trapz) - expVolume = max([expVolume, np.trapz(moleFracExp,flowRate*timeElapsedExp)]) + expVolume = max([expVolume, np.trapz(moleFracExp,np.multiply(flowRate, timeElapsedExp))]) # Compute the dead volume response using the optimizer parameters - timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume_1M = x[0], + _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1M = x[0], deadVolume_1D = x[1], deadVolume_2M = x[2], deadVolume_2D = x[3], @@ -170,15 +171,9 @@ def deadVolObjectiveFunction(x): numTanks_2D = int(x[7]), splitRatio_1 = x[8], splitRatio_2 = x[9], + timeInt = timeInt, flowRate = flowRate) - - # Interpolate simulation data (generate function) - interpSim = interp1d(timeSimOut, moleFracOut) - - # Find the interpolated simulation mole fraction at times corresponding to - # the experimental ones - moleFracSim = interpSim(timeElapsedExp) - + # Compute the sum of the error for the difference between exp. and sim. numPoints += len(moleFracExp) computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index bd6dcc1..18314a1 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -11,8 +11,12 @@ # Purpose: # Simulates the dead volume using the tanks in series (TIS) for the ZLC # Reference: 10.1016/j.ces.2008.02.023 +# The methodolgy is slighlty modified to incorporate diffusive pockets using +# compartment models (see Levenspiel, chapter 12) or Lisa Joss's article +# Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-14, AK: Change from simple TIS to series of parallel CSTRs # - 2021-04-12, AK: Small fixed # - 2021-03-25, AK: Fix for plot @@ -45,7 +49,7 @@ def simulateDeadVolume(**kwargs): if 'flowRate' in kwargs: flowRate = kwargs["flowRate"] else: - flowRate = 0.25 + flowRate = np.array([0.25]) # Dead Volume of the first volume (mixing) [cc] if 'deadVolume_1M' in kwargs: deadVolume_1M = kwargs["deadVolume_1M"] @@ -98,6 +102,11 @@ def simulateDeadVolume(**kwargs): splitRatio_2 = kwargs["splitRatio_2"] else: splitRatio_2 = 0.9 + # Initial Mole Fraction [-] + if 'initMoleFrac' in kwargs: + initMoleFrac = np.array(kwargs["initMoleFrac"]) + else: + initMoleFrac = np.array([1.]) # Feed Mole Fraction [-] if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) @@ -109,8 +118,19 @@ def simulateDeadVolume(**kwargs): else: timeInt = (0.0,2000) + # If experimental data used, then initialize ode evaluation time to + # experimental time, else use default + if flowRate.size == 1: + t_eval = np.arange(timeInt[0],timeInt[-1],0.1) + else: + # Use experimental time (from timeInt) for ode evaluations to avoid + # interpolating any data. t_eval is also used for interpolating + # flow rate in the ode equations + t_eval = timeInt + timeInt = (0.0,max(timeInt)) + # Prepare tuple of input parameters for the ode solver - inputParameters = (flowRate, deadVolume_1M,deadVolume_1D, + inputParameters = (t_eval,flowRate, deadVolume_1M,deadVolume_1D, deadVolume_2M,deadVolume_2D, numTanks_1M, numTanks_1D, numTanks_2M, numTanks_2D, splitRatio_1, splitRatio_2, @@ -122,10 +142,10 @@ def simulateDeadVolume(**kwargs): # Prepare initial conditions vector # The first element is the inlet composition and the rest is the dead # volume - initialConditions = np.ones([numTanksTotal])*(1-feedMoleFrac) + initialConditions = np.ones([numTanksTotal])*initMoleFrac # Solve the system of equations outputSol = solve_ivp(solveTanksInSeries, timeInt, initialConditions, - method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],0.1), + method='Radau', t_eval = t_eval, rtol = 1e-6, args = inputParameters) # Parse out the time @@ -139,9 +159,10 @@ def simulateDeadVolume(**kwargs): moleFracMix = outputSol.y[numTanksTotal-numTanks_2D-1] # Diffusive volume moleFracDiff = outputSol.y[-1] + # Composition after mixing - moleFracOut = (splitRatio_2*flowRate*moleFracMix - + (1-splitRatio_2)*flowRate*moleFracDiff)/(flowRate) + moleFracOut = np.divide(splitRatio_2*np.multiply(flowRate,moleFracMix) + + (1-splitRatio_2)*np.multiply(flowRate,moleFracDiff),flowRate) # Plot the dead volume response if plotFlag: @@ -153,10 +174,20 @@ def simulateDeadVolume(**kwargs): # Solves the system of ODE for the tanks in series model for the dead volume def solveTanksInSeries(t, f, *inputParameters): import numpy as np + from scipy.interpolate import interp1d # Unpack the tuple of input parameters used to solve equations - flowRate, deadVolume_1M, deadVolume_1D, deadVolume_2M, deadVolume_2D, numTanks_1M, numTanks_1D, numTanks_2M, numTanks_2D, splitRatio_1, splitRatio_2, feedMoleFrac = inputParameters + timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, deadVolume_2M, deadVolume_2D, numTanks_1M, numTanks_1D, numTanks_2M, numTanks_2D, splitRatio_1, splitRatio_2, feedMoleFrac = inputParameters + # Check if experimental data available + # If size of florate is one, then no need for interpolation + # If one, then interpolate flow rate values to get at ode time + if flowRateALL.size != 1: + interpFlow = interp1d(timeElapsed, flowRateALL) + flowRate = interpFlow(t) + else: + flowRate = flowRateALL + # Total number of tanks [-] numTanksTotal = numTanks_1M + numTanks_2M + numTanks_1D + numTanks_2D diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 0c5e389..17e0679 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-16, AK: Initial creation # # Input arguments: @@ -26,7 +27,6 @@ from simulateDeadVolume import simulateDeadVolume from numpy import load import os -from scipy.interpolate import interp1d import matplotlib.pyplot as plt import auxiliaryFunctions plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file @@ -47,7 +47,7 @@ flagDeadVolume = True # Flag to plot simulations -simulateModel = False +simulateModel = True # Flag to plot dead volume results plotFt = False @@ -75,26 +75,23 @@ # Loop over all available files for ii in range(len(fileName)): # Initialize outputs - timeSimOut = [] - moleFracOut = [] moleFracSim = [] # Path of the file name fileToLoad = mainDir + fileName[ii] - # Load experimental molefraction + # Load experimental timeelapsed, molefraction and flowRate timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() moleFracExp = load(fileToLoad)["moleFrac"].flatten() - # Parse out flow rate of the experiment - # Obtain the mean and round it to the 2 decimal to be used in the - # simulation - flowRate = round(np.mean(load(fileToLoad)["flowRate"]),2) + flowRate = load(fileToLoad)["flowRate"].flatten() + + # Integration and ode evaluation time + timeInt = timeElapsedExp # Print experimental volume print("Experiment",str(ii+1),round(np.trapz(moleFracExp, - np.multiply(load(fileToLoad)["flowRate"].flatten(), - timeElapsedExp)),2)) + np.multiply(flowRate,timeElapsedExp)),2)) if simulateModel: # Compute the dead volume response using the optimizer parameters - timeSimOut , _ , moleFracOut = simulateDeadVolume(deadVolume_1M = x[0], + _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1M = x[0], deadVolume_1D = x[1], deadVolume_2M = x[2], deadVolume_2D = x[3], @@ -104,18 +101,12 @@ numTanks_2D = int(x[7]), splitRatio_1 = x[8], splitRatio_2 = x[9], + timeInt = timeInt, flowRate = flowRate) - - # Interpolate simulation data (generate function) - interpSim = interp1d(timeSimOut, moleFracOut) - - # Find the interpolated simulation mole fraction at times corresponding to - # the experimental ones - moleFracSim = interpSim(timeElapsedExp) - + # Print simulation volume print("Simulation",str(ii+1),round(np.trapz(moleFracSim, - np.multiply(load(fileToLoad)["flowRate"].flatten(), + np.multiply(flowRate, timeElapsedExp)),2)) # Plot the expreimental and model output @@ -124,7 +115,7 @@ fig = plt.figure ax1 = plt.subplot(1,2,1) ax1.plot(timeElapsedExp,moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: ax1.plot(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -136,7 +127,7 @@ # Log scale ax2 = plt.subplot(1,2,2) ax2.semilogy(timeElapsedExp,moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -157,10 +148,10 @@ # Linear scale fig = plt.figure ax1 = plt.subplot(1,2,1) - ax1.plot(np.multiply(load(fileToLoad)["flowRate"].flatten(),timeElapsedExp),moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + ax1.plot(np.multiply(flowRate,timeElapsedExp),moleFracExp, + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: - ax1.plot(flowRate*timeElapsedExp,moleFracSim, + ax1.plot(np.multiply(flowRate,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$Ft$ [cc]', ylabel='$y_1$ [-]', @@ -169,10 +160,10 @@ # Log scale ax2 = plt.subplot(1,2,2) - ax2.semilogy(np.multiply(load(fileToLoad)["flowRate"].flatten(),timeElapsedExp),moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(flowRate)+" ccs") # Experimental response + ax2.semilogy(np.multiply(flowRate,timeElapsedExp),moleFracExp, + 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: - ax2.semilogy(flowRate*timeElapsedExp,moleFracSim, + ax2.semilogy(np.multiply(flowRate,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$Ft$ [cc]', xlim = [0,30], ylim = [1e-2, 1]) From 92e47e7ab9ee1b306503e99ce2716771aacbde66 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 20 Apr 2021 15:37:06 +0100 Subject: [PATCH 067/189] Add variable mole fraction at the boundary for DV --- experimental/simulateDeadVolume.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 18314a1..c14c189 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -177,10 +177,10 @@ def solveTanksInSeries(t, f, *inputParameters): from scipy.interpolate import interp1d # Unpack the tuple of input parameters used to solve equations - timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, deadVolume_2M, deadVolume_2D, numTanks_1M, numTanks_1D, numTanks_2M, numTanks_2D, splitRatio_1, splitRatio_2, feedMoleFrac = inputParameters + timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, deadVolume_2M, deadVolume_2D, numTanks_1M, numTanks_1D, numTanks_2M, numTanks_2D, splitRatio_1, splitRatio_2, feedMoleFracALL = inputParameters # Check if experimental data available - # If size of florate is one, then no need for interpolation + # If size of flowrate is one, then no need for interpolation # If one, then interpolate flow rate values to get at ode time if flowRateALL.size != 1: interpFlow = interp1d(timeElapsed, flowRateALL) @@ -188,6 +188,14 @@ def solveTanksInSeries(t, f, *inputParameters): else: flowRate = flowRateALL + # If size of mole fraction is one, then no need for interpolation + # If one, then interpolate mole fraction values to get at ode time + if feedMoleFracALL.size != 1: + interpMoleFrac = interp1d(timeElapsed, feedMoleFracALL) + feedMoleFrac = interpMoleFrac(t) + else: + feedMoleFrac = feedMoleFracALL + # Total number of tanks [-] numTanksTotal = numTanks_1M + numTanks_2M + numTanks_1D + numTanks_2D From 70fe2f3e0cc6f4b494ddbaca77dbec905072e718 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 20 Apr 2021 18:20:45 +0100 Subject: [PATCH 068/189] Change model to flow dependent split ratio --- experimental/extractDeadVolume.py | 36 +++++------ experimental/simulateDeadVolume.py | 88 ++++++-------------------- plotFunctions/plotExperimentOutcome.py | 46 +++++++------- 3 files changed, 60 insertions(+), 110 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 0ec7e86..4c0a8be 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-20, AK: Change model to flow dependent split # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-15, AK: Modify GA parameters and add penalty function # - 2021-04-14, AK: Bug fix @@ -57,23 +58,23 @@ def extractDeadVolume(): # Directory of raw data mainDir = 'experimental/runData' # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp12D_Output.mat', - 'ZLC_DeadVolume_Exp12E_Output.mat', - 'ZLC_DeadVolume_Exp12F_Output.mat'] + fileName = ['ZLC_DeadVolume_Exp13A_Output.mat', + 'ZLC_DeadVolume_Exp13B_Output.mat', + 'ZLC_DeadVolume_Exp13C_Output.mat', + 'ZLC_DeadVolume_Exp13D_Output.mat', + 'ZLC_DeadVolume_Exp13E_Output.mat', + 'ZLC_DeadVolume_Exp13F_Output.mat'] # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName) # Define the bounds and the type of the parameters to be optimized optBounds = np.array(([np.finfo(float).eps,100], [np.finfo(float).eps,100], - [np.finfo(float).eps,100], [np.finfo(float).eps,100], - [1,30], [1,30], [1,30], [1,30], - [np.finfo(float).eps,1-np.finfo(float).eps], - [np.finfo(float).eps,1-np.finfo(float).eps])) + [1,30], [1,30], [np.finfo(float).eps,10])) - optType=np.array(['real','real','real','real','int','int','int','int','real','real']) + optType=np.array(['real','real','int','int','real']) # Algorithm parameters for GA - algorithm_param = {'max_num_iteration':10, - 'population_size':800, + algorithm_param = {'max_num_iteration':5, + 'population_size':1600, 'mutation_probability':0.1, 'crossover_probability': 0.55, 'parents_portion': 0.15, @@ -82,7 +83,7 @@ def extractDeadVolume(): # Minimize an objective function to compute the dead volume and the number of # tanks for the dead volume using GA - model = ga(function = deadVolObjectiveFunction, dimension=10, + model = ga(function = deadVolObjectiveFunction, dimension=5, variable_type_mixed = optType, variable_boundaries = optBounds, algorithm_parameters=algorithm_param) @@ -163,14 +164,9 @@ def deadVolObjectiveFunction(x): # Compute the dead volume response using the optimizer parameters _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1M = x[0], deadVolume_1D = x[1], - deadVolume_2M = x[2], - deadVolume_2D = x[3], - numTanks_1M = int(x[4]), - numTanks_1D = int(x[5]), - numTanks_2M = int(x[6]), - numTanks_2D = int(x[7]), - splitRatio_1 = x[8], - splitRatio_2 = x[9], + numTanks_1M = int(x[2]), + numTanks_1D = int(x[3]), + splitRatioFactor = x[4], timeInt = timeInt, flowRate = flowRate) @@ -181,7 +177,7 @@ def deadVolObjectiveFunction(x): # Penalize if the total volume of the system is greater than experiemntal # volume penaltyObj = 0 - if sum(x[0:4])>1.5*expVolume: + if sum(x[0:2])>1.5*expVolume: penaltyObj = 10000 # Compute the sum of the error for the difference between exp. and sim. and # add a penalty if needed diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index c14c189..e29f60f 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -16,6 +16,8 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-20, AK: Change model to flow dependent split +# - 2021-04-20, AK: Change model to flow dependent split # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-14, AK: Change from simple TIS to series of parallel CSTRs # - 2021-04-12, AK: Small fixed @@ -59,18 +61,7 @@ def simulateDeadVolume(**kwargs): if 'deadVolume_1D' in kwargs: deadVolume_1D = kwargs["deadVolume_1D"] else: - deadVolume_1D = 1.5 - # Dead Volume of the second volume (mixing) [cc] - if 'deadVolume_2M' in kwargs: - deadVolume_2M = kwargs["deadVolume_2M"] - else: - deadVolume_2M = 2 - # Dead Volume of the second volume (diffusive) [cc] - if 'deadVolume_2D' in kwargs: - deadVolume_2D = kwargs["deadVolume_2D"] - else: - deadVolume_2D = 0.5 - + deadVolume_1D = 1 # Number of tanks of the first volume (mixing) [-] if 'numTanks_1M' in kwargs: numTanks_1M = kwargs["numTanks_1M"] @@ -81,27 +72,11 @@ def simulateDeadVolume(**kwargs): numTanks_1D = kwargs["numTanks_1D"] else: numTanks_1D = 1 - # Number of tanks of the second volume (mixing) [-] - if 'numTanks_2M' in kwargs: - numTanks_2M = kwargs["numTanks_2M"] - else: - numTanks_2M = 1 - # Number of tanks of the second volume (mixing) [-] - if 'numTanks_2D' in kwargs: - numTanks_2D = kwargs["numTanks_2D"] - else: - numTanks_2D = 1 - # Split ratio for flow rate of the first volume [-] - if 'splitRatio_1' in kwargs: - splitRatio_1 = kwargs["splitRatio_1"] - else: - splitRatio_1 = 0.99 - # Split ratio for flow rate of the second volume [-] - if 'splitRatio_2' in kwargs: - splitRatio_2 = kwargs["splitRatio_2"] + if 'splitRatioFactor' in kwargs: + splitRatioFactor = kwargs["splitRatioFactor"] else: - splitRatio_2 = 0.9 + splitRatioFactor = 1.1 # Initial Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) @@ -131,13 +106,11 @@ def simulateDeadVolume(**kwargs): # Prepare tuple of input parameters for the ode solver inputParameters = (t_eval,flowRate, deadVolume_1M,deadVolume_1D, - deadVolume_2M,deadVolume_2D, - numTanks_1M, numTanks_1D, numTanks_2M, - numTanks_2D, splitRatio_1, splitRatio_2, + numTanks_1M, numTanks_1D, splitRatioFactor, feedMoleFrac) # Total number of tanks[-] - numTanksTotal = numTanks_1M + numTanks_2M + numTanks_1D + numTanks_2D + numTanksTotal = numTanks_1M + numTanks_1D # Prepare initial conditions vector # The first element is the inlet composition and the rest is the dead @@ -156,14 +129,16 @@ def simulateDeadVolume(**kwargs): # Mole fraction at the outlet # Mixing volume - moleFracMix = outputSol.y[numTanksTotal-numTanks_2D-1] + moleFracMix = outputSol.y[numTanks_1M-1] # Diffusive volume moleFracDiff = outputSol.y[-1] # Composition after mixing - moleFracOut = np.divide(splitRatio_2*np.multiply(flowRate,moleFracMix) - + (1-splitRatio_2)*np.multiply(flowRate,moleFracDiff),flowRate) - + splitRatio_1 = np.divide(np.multiply(splitRatioFactor,flowRate), + (1+np.multiply(splitRatioFactor,flowRate))) + moleFracOut = np.divide(np.multiply(splitRatio_1,np.multiply(flowRate,moleFracMix)) + + np.multiply((1-splitRatio_1),np.multiply(flowRate,moleFracDiff)),flowRate) + # Plot the dead volume response if plotFlag: plotOutletConcentration(timeSim,moleFracIn,moleFracOut) @@ -177,7 +152,7 @@ def solveTanksInSeries(t, f, *inputParameters): from scipy.interpolate import interp1d # Unpack the tuple of input parameters used to solve equations - timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, deadVolume_2M, deadVolume_2D, numTanks_1M, numTanks_1D, numTanks_2M, numTanks_2D, splitRatio_1, splitRatio_2, feedMoleFracALL = inputParameters + timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, numTanks_1M, numTanks_1D, splitRatioFactor, feedMoleFracALL = inputParameters # Check if experimental data available # If size of flowrate is one, then no need for interpolation @@ -197,10 +172,7 @@ def solveTanksInSeries(t, f, *inputParameters): feedMoleFrac = feedMoleFracALL # Total number of tanks [-] - numTanksTotal = numTanks_1M + numTanks_2M + numTanks_1D + numTanks_2D - - # Total number of tanks of individual volumes [-] - numTanksTotal_1 = numTanks_1M + numTanks_1D + numTanksTotal = numTanks_1M + numTanks_1D # Initialize the derivatives to zero df = np.zeros([numTanksTotal]) @@ -208,14 +180,11 @@ def solveTanksInSeries(t, f, *inputParameters): # Volume of each tank in each section volTank_1M = deadVolume_1M/numTanks_1M volTank_1D = deadVolume_1D/numTanks_1D - volTank_2M = deadVolume_2M/numTanks_2M - volTank_2D = deadVolume_2D/numTanks_2D # Residence time of each tank in the mixing and diffusive volume + splitRatio_1 = splitRatioFactor*flowRate/(1+splitRatioFactor*flowRate) residenceTime_1M = volTank_1M/(splitRatio_1*flowRate) residenceTime_1D = volTank_1D/((1-splitRatio_1)*flowRate) - residenceTime_2M = volTank_2M/(splitRatio_2*flowRate) - residenceTime_2D = volTank_2D/((1-splitRatio_2)*flowRate) # Solve the odes # Volume 1: Mixing volume @@ -225,27 +194,10 @@ def solveTanksInSeries(t, f, *inputParameters): # Volume 1: Diffusive volume df[numTanks_1M] = ((1/residenceTime_1D)*(feedMoleFrac - f[numTanks_1M])) - df[numTanks_1M+1:numTanksTotal_1] = ((1/residenceTime_1D) - *(f[numTanks_1M:numTanksTotal_1-1] - - f[numTanks_1M+1:numTanksTotal_1])) + df[numTanks_1M+1:numTanksTotal] = ((1/residenceTime_1D) + *(f[numTanks_1M:numTanksTotal-1] + - f[numTanks_1M+1:numTanksTotal])) - # Compute the outlet composition for volume 1 - yOut_1 = (splitRatio_1*flowRate*f[numTanks_1M-1] - + (1-splitRatio_1)*flowRate*f[numTanksTotal_1-1])/flowRate - - # Volume 2: Mixing volume - df[numTanksTotal_1] = ((1/residenceTime_2M)*(yOut_1 - f[numTanksTotal_1])) - df[numTanksTotal_1+1:numTanks_2M] = ((1/residenceTime_2M) - *(f[numTanksTotal_1:numTanks_2M-1] - - f[numTanksTotal_1+1:numTanks_2M])) - - # Volume 2: Diffusive volume - df[numTanksTotal_1+numTanks_2D] = ((1/residenceTime_2D) - *(yOut_1 - f[numTanksTotal_1+numTanks_2D])) - df[numTanksTotal_1+numTanks_2D+1:numTanksTotal] = ((1/residenceTime_2D) - *(f[numTanksTotal_1+numTanks_2D:numTanksTotal-1] - - f[numTanksTotal_1+numTanks_2D+1:numTanksTotal])) - # Return the derivatives for the solver return df diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 17e0679..7f2a882 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -50,27 +50,30 @@ simulateModel = True # Flag to plot dead volume results -plotFt = False +plotFt = True # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' -colorsForPlot = ["#E5383B","#B55055","#6C757D"] +colorsForPlot = ["#E5383B","#E5383B","#B55055","#B55055","#6C757D","#6C757D"] +markerForPlot = ["o","v","o","v","o","v"] if flagDeadVolume: # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp12D_Output_10f6fad.npz', - 'ZLC_DeadVolume_Exp12E_Output_10f6fad.npz', - 'ZLC_DeadVolume_Exp12F_Output_10f6fad.npz'] - + fileName = ['ZLC_DeadVolume_Exp13A_Output_92e47e7.npz', + 'ZLC_DeadVolume_Exp13B_Output_92e47e7.npz', + 'ZLC_DeadVolume_Exp13C_Output_92e47e7.npz', + 'ZLC_DeadVolume_Exp13D_Output_92e47e7.npz', + 'ZLC_DeadVolume_Exp13E_Output_92e47e7.npz', + 'ZLC_DeadVolume_Exp13F_Output_92e47e7.npz'] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210415_1657_a72563b.npz' + fileParameter = 'deadVolumeCharacteristics_20210420_1806_92e47e7.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] # Print the objective function and volume from model parameters print("Objective Function",round(modelOutputTemp[()]["function"],0)) - print("Model Volume",round(sum(x[0:4]),2)) + print("Model Volume",round(sum(x[0:2]),2)) # Initialize error for objective function # Loop over all available files for ii in range(len(fileName)): @@ -93,14 +96,9 @@ # Compute the dead volume response using the optimizer parameters _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1M = x[0], deadVolume_1D = x[1], - deadVolume_2M = x[2], - deadVolume_2D = x[3], - numTanks_1M = int(x[4]), - numTanks_1D = int(x[5]), - numTanks_2M = int(x[6]), - numTanks_2D = int(x[7]), - splitRatio_1 = x[8], - splitRatio_2 = x[9], + numTanks_1M = int(x[2]), + numTanks_1D = int(x[3]), + splitRatioFactor = x[4], timeInt = timeInt, flowRate = flowRate) @@ -115,7 +113,8 @@ fig = plt.figure ax1 = plt.subplot(1,2,1) ax1.plot(timeElapsedExp,moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: ax1.plot(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -127,12 +126,13 @@ # Log scale ax2 = plt.subplot(1,2,2) ax2.semilogy(timeElapsedExp,moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', - xlim = [0,250], ylim = [1e-3, 1]) + xlim = [0,400], ylim = [1e-3, 1]) ax2.legend() # Save the figure @@ -149,7 +149,8 @@ fig = plt.figure ax1 = plt.subplot(1,2,1) ax1.plot(np.multiply(flowRate,timeElapsedExp),moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: ax1.plot(np.multiply(flowRate,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -161,12 +162,13 @@ # Log scale ax2 = plt.subplot(1,2,2) ax2.semilogy(np.multiply(flowRate,timeElapsedExp),moleFracExp, - 'o',color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response if simulateModel: ax2.semilogy(np.multiply(flowRate,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$Ft$ [cc]', - xlim = [0,30], ylim = [1e-2, 1]) + xlim = [0,100], ylim = [1e-3, 1]) ax2.legend() # Save the figure From 21b8c55126113c91d559342a58615a5b8d4e164d Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Tue, 20 Apr 2021 18:53:16 +0100 Subject: [PATCH 069/189] Add multiple equilibration time --- experimental/analyzeExperiment.m | 17 +++++++++-------- experimental/runMultipleZLC.m | 10 ++++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 5747187..6f7df7a 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -14,6 +14,7 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-04-20, AK: Add experiment struct to output .mat file % - 2021-04-19, AK: Major revamp for flow rate computation % - 2021-04-13, AK: Add threshold to cut data below a given mole fraction % - 2021-04-08, AK: Add ratio of gas for calibration @@ -35,7 +36,7 @@ flagCalibration = false; % Flag to decide calibration of flow meters or MS -flagFlowMeter = true; +flagFlowMeter = false; % Mode to switch between calibration and analyzing real experiment % Analyze calibration data @@ -49,8 +50,8 @@ % Calibrate MS else experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_20210414'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210414.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLCCalibrateMS_20210420'; % Experimental flow file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210420.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) experimentStruct.numMean = 10; % Number of points for averaging experimentStruct.flagUseIndGas = false; % Flag to determine whether independent (true) or ratio of signals used for calibration @@ -62,9 +63,9 @@ else moleFracThreshold = 1e-3; % Threshold to cut data below a given mole fraction [-] experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210419_Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLC_ActivatedCarbon_Exp10F'; % Experimental flow file (.mat) - experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210414_Model'; % Experimental calibration file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_ActivatedCarbon_Exp10.asc'; % Experimental MS file (.asc) + experimentStruct.flow = 'ZLC_DeadVolume_Exp14A'; % Experimental flow file (.mat) + experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210420_Model'; % Experimental calibration file (.mat) + experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_DeadVolume_Exp14.asc'; % Experimental MS file (.asc) experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) % Call reconcileData function to get the output mole fraction for a % real experiment @@ -107,13 +108,13 @@ % Save the calibration data for further use save(['experimentalData',filesep,... 'runData',filesep,experimentStruct.flow,'_Output'],'experimentOutput',... - 'gitCommitID'); + 'experimentStruct','gitCommitID'); else % Create the calibration data folder if it does not exist mkdir(['experimentalData',filesep,'runData']) % Save the calibration data for further use save(['experimentalData',filesep,... 'runData',filesep,experimentStruct.flow,'_Output'],'experimentOutput',... - 'gitCommitID'); + 'experimentStruct','gitCommitID'); end end \ No newline at end of file diff --git a/experimental/runMultipleZLC.m b/experimental/runMultipleZLC.m index 31e82f7..66ffd43 100644 --- a/experimental/runMultipleZLC.m +++ b/experimental/runMultipleZLC.m @@ -12,6 +12,7 @@ % Runs multiple ZLC experiments in series % % Last modified: +% - 2021-04-20, AK: Add multiple equilibration time % - 2021-04-15, AK: Initial creation % % Input arguments: @@ -21,13 +22,13 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function runMultipleZLC % Series name for the experiments - expSeries = 'ZLC_ActivatedCarbon_Exp10'; + expSeries = 'ZLC_ActivatedCarbon_Exp11'; % Maximum time of the experiment expInfo.maxTime = 3600; % Sampling time for the device expInfo.samplingTime = 1; % Intervals for collecting MFC data - expInfo.MFCInterval = 50; + expInfo.MFCInterval = 100; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -35,7 +36,7 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Total flow rate - expTotalFlowRate = [2 4 10 20 30 40 60]; + expTotalFlowRate = [4 6 10 15 30]; % Fraction CO2 fracCO2 = 0.05; % Define set point for MFC1 @@ -47,7 +48,7 @@ % resolution of the meter) MFC2_SP = round(fracCO2*expTotalFlowRate,1); % Start delay (used for adsorbent equilibration) - expInfo.equilibrationTime = 1800; % [s] + equilibrationTime = [14400 10800 7200 7200 3600]; % [s] % Flag for meter calibration expInfo.calibrateMeters = false; % Mixtures Flag - When a T junction instead of 6 way valve used @@ -56,6 +57,7 @@ for ii=1:length(MFC1_SP) % Experiment name expInfo.expName = [expSeries,char(64+ii)]; + expInfo.equilibrationTime = equilibrationTime(ii); expInfo.MFC1_SP = MFC1_SP(ii); expInfo.MFC2_SP = MFC2_SP(ii); % Run the setup for different calibrations From bd246d68251ceeeab6d6fb0848dbbc9d09b84db5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 20 Apr 2021 20:25:27 +0100 Subject: [PATCH 070/189] Revert back split ratio to flow independent value --- experimental/extractDeadVolume.py | 2 +- experimental/simulateDeadVolume.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 4c0a8be..7eb257e 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -69,7 +69,7 @@ def extractDeadVolume(): # Define the bounds and the type of the parameters to be optimized optBounds = np.array(([np.finfo(float).eps,100], [np.finfo(float).eps,100], - [1,30], [1,30], [np.finfo(float).eps,10])) + [1,30], [1,30], [np.finfo(float).eps,1])) optType=np.array(['real','real','int','int','real']) # Algorithm parameters for GA diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index e29f60f..897923b 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -134,8 +134,7 @@ def simulateDeadVolume(**kwargs): moleFracDiff = outputSol.y[-1] # Composition after mixing - splitRatio_1 = np.divide(np.multiply(splitRatioFactor,flowRate), - (1+np.multiply(splitRatioFactor,flowRate))) + splitRatio_1 = splitRatioFactor moleFracOut = np.divide(np.multiply(splitRatio_1,np.multiply(flowRate,moleFracMix)) + np.multiply((1-splitRatio_1),np.multiply(flowRate,moleFracDiff)),flowRate) @@ -182,7 +181,7 @@ def solveTanksInSeries(t, f, *inputParameters): volTank_1D = deadVolume_1D/numTanks_1D # Residence time of each tank in the mixing and diffusive volume - splitRatio_1 = splitRatioFactor*flowRate/(1+splitRatioFactor*flowRate) + splitRatio_1 = splitRatioFactor residenceTime_1M = volTank_1M/(splitRatio_1*flowRate) residenceTime_1D = volTank_1D/((1-splitRatio_1)*flowRate) From 1b02ca760d17ff50d0e3de205021c8b6debcb18d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 21 Apr 2021 11:03:50 +0100 Subject: [PATCH 071/189] Change model to fix split velocity --- experimental/extractDeadVolume.py | 5 +++-- experimental/simulateDeadVolume.py | 31 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 7eb257e..5dab854 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-21, AK: Change model to fix split velocity # - 2021-04-20, AK: Change model to flow dependent split # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-15, AK: Modify GA parameters and add penalty function @@ -69,7 +70,7 @@ def extractDeadVolume(): # Define the bounds and the type of the parameters to be optimized optBounds = np.array(([np.finfo(float).eps,100], [np.finfo(float).eps,100], - [1,30], [1,30], [np.finfo(float).eps,1])) + [1,30], [1,30], [np.finfo(float).eps,0.05])) optType=np.array(['real','real','int','int','real']) # Algorithm parameters for GA @@ -166,7 +167,7 @@ def deadVolObjectiveFunction(x): deadVolume_1D = x[1], numTanks_1M = int(x[2]), numTanks_1D = int(x[3]), - splitRatioFactor = x[4], + flowRate_D = x[4], timeInt = timeInt, flowRate = flowRate) diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 897923b..1f82ad6 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -16,6 +16,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-21, AK: Change model to fix split velocity # - 2021-04-20, AK: Change model to flow dependent split # - 2021-04-20, AK: Change model to flow dependent split # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV @@ -56,27 +57,27 @@ def simulateDeadVolume(**kwargs): if 'deadVolume_1M' in kwargs: deadVolume_1M = kwargs["deadVolume_1M"] else: - deadVolume_1M = 5 + deadVolume_1M = 2.62 # Dead Volume of the first volume (diffusive) [cc] if 'deadVolume_1D' in kwargs: deadVolume_1D = kwargs["deadVolume_1D"] else: - deadVolume_1D = 1 + deadVolume_1D = 0.67 # Number of tanks of the first volume (mixing) [-] if 'numTanks_1M' in kwargs: numTanks_1M = kwargs["numTanks_1M"] else: - numTanks_1M = 10 + numTanks_1M = 1 # Number of tanks of the first volume (mixing) [-] if 'numTanks_1D' in kwargs: numTanks_1D = kwargs["numTanks_1D"] else: numTanks_1D = 1 - # Split ratio for flow rate of the first volume [-] - if 'splitRatioFactor' in kwargs: - splitRatioFactor = kwargs["splitRatioFactor"] + # Flow rate in the diffusive volume [-] + if 'flowRate_D' in kwargs: + flowRate_D = kwargs["flowRate_D"] else: - splitRatioFactor = 1.1 + flowRate_D = 0.1 # Initial Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) @@ -106,7 +107,7 @@ def simulateDeadVolume(**kwargs): # Prepare tuple of input parameters for the ode solver inputParameters = (t_eval,flowRate, deadVolume_1M,deadVolume_1D, - numTanks_1M, numTanks_1D, splitRatioFactor, + numTanks_1M, numTanks_1D, flowRate_D, feedMoleFrac) # Total number of tanks[-] @@ -134,9 +135,9 @@ def simulateDeadVolume(**kwargs): moleFracDiff = outputSol.y[-1] # Composition after mixing - splitRatio_1 = splitRatioFactor - moleFracOut = np.divide(np.multiply(splitRatio_1,np.multiply(flowRate,moleFracMix)) - + np.multiply((1-splitRatio_1),np.multiply(flowRate,moleFracDiff)),flowRate) + flowRate_M = flowRate - flowRate_D + moleFracOut = np.divide(np.multiply(flowRate_M,moleFracMix) + + np.multiply(flowRate_D,moleFracDiff),flowRate) # Plot the dead volume response if plotFlag: @@ -151,7 +152,7 @@ def solveTanksInSeries(t, f, *inputParameters): from scipy.interpolate import interp1d # Unpack the tuple of input parameters used to solve equations - timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, numTanks_1M, numTanks_1D, splitRatioFactor, feedMoleFracALL = inputParameters + timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, numTanks_1M, numTanks_1D, flowRate_D, feedMoleFracALL = inputParameters # Check if experimental data available # If size of flowrate is one, then no need for interpolation @@ -181,9 +182,9 @@ def solveTanksInSeries(t, f, *inputParameters): volTank_1D = deadVolume_1D/numTanks_1D # Residence time of each tank in the mixing and diffusive volume - splitRatio_1 = splitRatioFactor - residenceTime_1M = volTank_1M/(splitRatio_1*flowRate) - residenceTime_1D = volTank_1D/((1-splitRatio_1)*flowRate) + flowRate_M = flowRate - flowRate_D + residenceTime_1M = volTank_1M/(flowRate_M) + residenceTime_1D = volTank_1D/(flowRate_D) # Solve the odes # Volume 1: Mixing volume From 49eb234388d031098652076dc09e49c7da440a70 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 21 Apr 2021 13:44:19 +0100 Subject: [PATCH 072/189] Add a PFR before the two CSTRs --- experimental/extractDeadVolume.py | 22 +++---- experimental/simulateDeadVolume.py | 84 ++++++++++++++------------ plotFunctions/plotExperimentOutcome.py | 21 ++++--- 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 5dab854..a687f5a 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -69,10 +69,10 @@ def extractDeadVolume(): filesToProcess(True,mainDir,fileName) # Define the bounds and the type of the parameters to be optimized - optBounds = np.array(([np.finfo(float).eps,100], [np.finfo(float).eps,100], - [1,30], [1,30], [np.finfo(float).eps,0.05])) + optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,10], + [np.finfo(float).eps,10], [1,30], [np.finfo(float).eps,0.05])) - optType=np.array(['real','real','int','int','real']) + optType=np.array(['real','real','real','int','real']) # Algorithm parameters for GA algorithm_param = {'max_num_iteration':5, 'population_size':1600, @@ -163,13 +163,13 @@ def deadVolObjectiveFunction(x): expVolume = max([expVolume, np.trapz(moleFracExp,np.multiply(flowRate, timeElapsedExp))]) # Compute the dead volume response using the optimizer parameters - _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1M = x[0], - deadVolume_1D = x[1], - numTanks_1M = int(x[2]), - numTanks_1D = int(x[3]), - flowRate_D = x[4], - timeInt = timeInt, - flowRate = flowRate) + _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = x[0], + deadVolume_2M = x[1], + deadVolume_2D = x[2], + numTanks_1 = int(x[3]), + flowRate_D = x[4], + timeInt = timeInt, + flowRate = flowRate) # Compute the sum of the error for the difference between exp. and sim. numPoints += len(moleFracExp) @@ -178,7 +178,7 @@ def deadVolObjectiveFunction(x): # Penalize if the total volume of the system is greater than experiemntal # volume penaltyObj = 0 - if sum(x[0:2])>1.5*expVolume: + if sum(x[0:3])>1.5*expVolume: penaltyObj = 10000 # Compute the sum of the error for the difference between exp. and sim. and # add a penalty if needed diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 1f82ad6..a1aa259 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -53,31 +53,31 @@ def simulateDeadVolume(**kwargs): flowRate = kwargs["flowRate"] else: flowRate = np.array([0.25]) - # Dead Volume of the first volume (mixing) [cc] - if 'deadVolume_1M' in kwargs: - deadVolume_1M = kwargs["deadVolume_1M"] + # Dead Volume of the first volume [cc] + if 'deadVolume_1' in kwargs: + deadVolume_1 = kwargs["deadVolume_1"] else: - deadVolume_1M = 2.62 - # Dead Volume of the first volume (diffusive) [cc] - if 'deadVolume_1D' in kwargs: - deadVolume_1D = kwargs["deadVolume_1D"] + deadVolume_1 = 3 + # Number of tanks of the first volume [-] + if 'numTanks_1' in kwargs: + numTanks_1 = kwargs["numTanks_1"] else: - deadVolume_1D = 0.67 - # Number of tanks of the first volume (mixing) [-] - if 'numTanks_1M' in kwargs: - numTanks_1M = kwargs["numTanks_1M"] + numTanks_1 = 20 + # Dead Volume of the second volume (mixing) [cc] + if 'deadVolume_2M' in kwargs: + deadVolume_2M = kwargs["deadVolume_2M"] else: - numTanks_1M = 1 - # Number of tanks of the first volume (mixing) [-] - if 'numTanks_1D' in kwargs: - numTanks_1D = kwargs["numTanks_1D"] + deadVolume_2M = 0.2 + # Dead Volume of the second volume (diffusive) [cc] + if 'deadVolume_2D' in kwargs: + deadVolume_2D = kwargs["deadVolume_2D"] else: - numTanks_1D = 1 + deadVolume_2D = 0.1 # Flow rate in the diffusive volume [-] if 'flowRate_D' in kwargs: flowRate_D = kwargs["flowRate_D"] else: - flowRate_D = 0.1 + flowRate_D = 0.05 # Initial Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) @@ -106,12 +106,12 @@ def simulateDeadVolume(**kwargs): timeInt = (0.0,max(timeInt)) # Prepare tuple of input parameters for the ode solver - inputParameters = (t_eval,flowRate, deadVolume_1M,deadVolume_1D, - numTanks_1M, numTanks_1D, flowRate_D, + inputParameters = (t_eval,flowRate, deadVolume_1,deadVolume_2M, + deadVolume_2D, numTanks_1, flowRate_D, feedMoleFrac) # Total number of tanks[-] - numTanksTotal = numTanks_1M + numTanks_1D + numTanksTotal = numTanks_1 + 2 # Prepare initial conditions vector # The first element is the inlet composition and the rest is the dead @@ -130,7 +130,7 @@ def simulateDeadVolume(**kwargs): # Mole fraction at the outlet # Mixing volume - moleFracMix = outputSol.y[numTanks_1M-1] + moleFracMix = outputSol.y[numTanks_1] # Diffusive volume moleFracDiff = outputSol.y[-1] @@ -152,7 +152,7 @@ def solveTanksInSeries(t, f, *inputParameters): from scipy.interpolate import interp1d # Unpack the tuple of input parameters used to solve equations - timeElapsed, flowRateALL, deadVolume_1M, deadVolume_1D, numTanks_1M, numTanks_1D, flowRate_D, feedMoleFracALL = inputParameters + timeElapsed, flowRateALL, deadVolume_1, deadVolume_2M, deadVolume_2D, numTanks_1, flowRate_D, feedMoleFracALL = inputParameters # Check if experimental data available # If size of flowrate is one, then no need for interpolation @@ -172,32 +172,38 @@ def solveTanksInSeries(t, f, *inputParameters): feedMoleFrac = feedMoleFracALL # Total number of tanks [-] - numTanksTotal = numTanks_1M + numTanks_1D + numTanksTotal = numTanks_1 + 2 - # Initialize the derivatives to zero + # Initialize the derivatives to zero df = np.zeros([numTanksTotal]) - # Volume of each tank in each section - volTank_1M = deadVolume_1M/numTanks_1M - volTank_1D = deadVolume_1D/numTanks_1D + # Volume 1: Mixing volume + # Volume of each tank in the mixing volume + volTank_1 = deadVolume_1/numTanks_1 + residenceTime_1 = volTank_1/(flowRate) + + # Solve the odes + df[0] = ((1/residenceTime_1)*(feedMoleFrac - f[0])) + df[1:numTanks_1] = ((1/residenceTime_1) + *(f[0:numTanks_1-1] - f[1:numTanks_1])) + + # Volume 2: Diffusive volume + # Volume of each tank in the mixing volume + volTank_2M = deadVolume_2M + volTank_2D = deadVolume_2D # Residence time of each tank in the mixing and diffusive volume flowRate_M = flowRate - flowRate_D - residenceTime_1M = volTank_1M/(flowRate_M) - residenceTime_1D = volTank_1D/(flowRate_D) + residenceTime_2M = volTank_2M/(flowRate_M) + residenceTime_2D = volTank_2D/(flowRate_D) # Solve the odes - # Volume 1: Mixing volume - df[0] = ((1/residenceTime_1M)*(feedMoleFrac - f[0])) - df[1:numTanks_1M] = ((1/residenceTime_1M) - *(f[0:numTanks_1M-1] - f[1:numTanks_1M])) - - # Volume 1: Diffusive volume - df[numTanks_1M] = ((1/residenceTime_1D)*(feedMoleFrac - f[numTanks_1M])) - df[numTanks_1M+1:numTanksTotal] = ((1/residenceTime_1D) - *(f[numTanks_1M:numTanksTotal-1] - - f[numTanks_1M+1:numTanksTotal])) + # Volume 2: Mixing volume + df[numTanks_1] = ((1/residenceTime_2M)*(f[numTanks_1-1] - f[numTanks_1])) + # Volume 2: Diffusive volume + df[numTanks_1+1] = ((1/residenceTime_2D)*(f[numTanks_1-1] - f[numTanks_1+1])) + # Return the derivatives for the solver return df diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 7f2a882..6dd583d 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -50,7 +50,7 @@ simulateModel = True # Flag to plot dead volume results -plotFt = True +plotFt = False # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' @@ -68,9 +68,10 @@ 'ZLC_DeadVolume_Exp13F_Output_92e47e7.npz'] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210420_1806_92e47e7.npz' + fileParameter = 'deadVolumeCharacteristics_20210421_1125_1b02ca7.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] + # Print the objective function and volume from model parameters print("Objective Function",round(modelOutputTemp[()]["function"],0)) print("Model Volume",round(sum(x[0:2]),2)) @@ -94,13 +95,13 @@ np.multiply(flowRate,timeElapsedExp)),2)) if simulateModel: # Compute the dead volume response using the optimizer parameters - _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1M = x[0], - deadVolume_1D = x[1], - numTanks_1M = int(x[2]), - numTanks_1D = int(x[3]), - splitRatioFactor = x[4], - timeInt = timeInt, - flowRate = flowRate) + _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = x[0], + deadVolume_2M = x[1], + deadVolume_2D = x[2], + numTanks_1 = int(x[3]), + flowRate_D = x[4], + timeInt = timeInt, + flowRate = flowRate) # Print simulation volume print("Simulation",str(ii+1),round(np.trapz(moleFracSim, @@ -120,7 +121,7 @@ color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$t$ [s]', ylabel='$y_1$ [-]', - xlim = [0,50], ylim = [0, 1]) + xlim = [0,200], ylim = [0, 1]) ax1.legend() # Log scale From 4e5fba2adc38f93c5b28b364f24bab672b2f1cd9 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 21 Apr 2021 16:33:54 +0100 Subject: [PATCH 073/189] Change the calibration equation to mole fraction like --- experimental/analyzeCalibration.m | 17 +++++++++-------- experimental/concatenateData.m | 8 ++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index cd0c942..d62a287 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-21, AK: Change the calibration equation to mole fraction like % - 2021-04-19, AK: Change MFC and MFM calibration (for mixtures) % - 2021-04-08, AK: Add ratio of gas for calibration % - 2021-04-07, AK: Modify for addition of MFM @@ -195,9 +196,9 @@ function analyzeCalibration(parametersFlow,parametersMS) calibrationMS.CO2 = polyfit(meanCO2Signal,meanMoleFrac(:,2),parametersMS.polyDeg); % COo2 else % Perform an optimization to obtain parameter estimates to fit the - % ratio of the helium signal to CO2 signal w.r.t He concentration - y0 = [1, 0.5, 0.5]; % Initial conditions - [paramFit,resErr] = fminunc(@(p) objectiveFunction(meanMoleFrac(:,1),log(meanHeSignal./meanCO2Signal),p),y0); + % signal fraction of the helium signal to He+CO2 signal + y0 = [1]; % Initial conditions + [paramFit,resErr] = fminunc(@(p) objectiveFunction(meanMoleFrac(:,1),meanHeSignal./(meanCO2Signal+meanHeSignal),p),y0); calibrationMS.ratioHeCO2 = paramFit; end @@ -245,13 +246,13 @@ function analyzeCalibration(parametersFlow,parametersMS) ylabel('CO2 mole frac [-]') % Ratio of He to CO2 else - plot(log(meanHeSignal./meanCO2Signal),meanMoleFrac(:,1),'or') % Experimental + plot(meanHeSignal./(meanHeSignal+meanCO2Signal),meanMoleFrac(:,1),'or') % Experimental hold on - plot(-10:1:10,1./(1+paramFit(1).*exp(-paramFit(2).*(-10:1:10))).^(1/paramFit(3)),'b') - xlim([-10 10]); + plot(0:0.1:1,(0:0.1:1).^(paramFit(1)),'b') + xlim([0 1]); ylim([0 1]); box on; grid on; - xlabel('log(Helium/CO2 Signal) [-]') + xlabel('Helium Signal/(CO2 Signal+Helium Signal) [-]') ylabel('Helium mole frac [-]') end end @@ -260,5 +261,5 @@ function analyzeCalibration(parametersFlow,parametersMS) % function (generalized) function errSignal = objectiveFunction(meanMoleFrac,expSignal,p) % Calculate the sum of errors - errSignal = sum((meanMoleFrac' - 1./(1+p(1).*exp(-p(2).*expSignal)).^(1/p(3))).^2); + errSignal = sum((meanMoleFrac' - expSignal.^p(1)).^2); end \ No newline at end of file diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index 0a5680b..a6cdb4d 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-21, AK: Change the calibration equation to mole fraction like % - 2021-04-19, AK: Remove MFM calibration (check analyzeExperiment) % - 2021-04-09, AK: Change output for calibration or non calibrate mode % - 2021-04-08, AK: Add ratio of gas for calibration @@ -202,10 +203,9 @@ else % Parse out the fitting parameters paramFit = calibrationMS.ratioHeCO2; - % Use a generalized logistic function - reconciledData.moleFrac(:,1) = 1./(1+paramFit(1)... - .*exp(-paramFit(2).*log(reconciledData.MS(:,2)./reconciledData.MS(:,3))))... - .^(1/paramFit(3)); % He [-] + % Use a power function + reconciledData.moleFrac(:,1) = (reconciledData.MS(:,2)./... + (reconciledData.MS(:,2)+reconciledData.MS(:,3))).^(paramFit(1)); % He [-] reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); % CO2 [-] end end From 1e1127b90a574521254c8a2652965f07932adffb Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 23 Apr 2021 18:18:59 +0100 Subject: [PATCH 074/189] Change the calibration model to Fourier series based --- experimental/analyzeCalibration.m | 14 ++++---------- experimental/concatenateData.m | 7 ++++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index d62a287..91cd0ff 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-23, AK: Change the calibration model to Fourier series based % - 2021-04-21, AK: Change the calibration equation to mole fraction like % - 2021-04-19, AK: Change MFC and MFM calibration (for mixtures) % - 2021-04-08, AK: Add ratio of gas for calibration @@ -197,9 +198,8 @@ function analyzeCalibration(parametersFlow,parametersMS) else % Perform an optimization to obtain parameter estimates to fit the % signal fraction of the helium signal to He+CO2 signal - y0 = [1]; % Initial conditions - [paramFit,resErr] = fminunc(@(p) objectiveFunction(meanMoleFrac(:,1),meanHeSignal./(meanCO2Signal+meanHeSignal),p),y0); - calibrationMS.ratioHeCO2 = paramFit; + % Use a fourier series to fit the calibration data + calibrationMS.ratioHeCO2 = fit((meanHeSignal./(meanCO2Signal+meanHeSignal))',meanMoleFrac(:,1),'fourier2'); end % Save the calibration data into a .mat file @@ -248,7 +248,7 @@ function analyzeCalibration(parametersFlow,parametersMS) else plot(meanHeSignal./(meanHeSignal+meanCO2Signal),meanMoleFrac(:,1),'or') % Experimental hold on - plot(0:0.1:1,(0:0.1:1).^(paramFit(1)),'b') + plot(0:0.01:1,calibrationMS.ratioHeCO2(0:0.01:1),'b') xlim([0 1]); ylim([0 1]); box on; grid on; @@ -256,10 +256,4 @@ function analyzeCalibration(parametersFlow,parametersMS) ylabel('Helium mole frac [-]') end end -end -% Objective function to evaluate model parameters for the logistic -% function (generalized) -function errSignal = objectiveFunction(meanMoleFrac,expSignal,p) - % Calculate the sum of errors - errSignal = sum((meanMoleFrac' - expSignal.^p(1)).^2); end \ No newline at end of file diff --git a/experimental/concatenateData.m b/experimental/concatenateData.m index a6cdb4d..246511d 100644 --- a/experimental/concatenateData.m +++ b/experimental/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-23, AK: Change the calibration model to Fourier series based % - 2021-04-21, AK: Change the calibration equation to mole fraction like % - 2021-04-19, AK: Remove MFM calibration (check analyzeExperiment) % - 2021-04-09, AK: Change output for calibration or non calibrate mode @@ -203,9 +204,9 @@ else % Parse out the fitting parameters paramFit = calibrationMS.ratioHeCO2; - % Use a power function - reconciledData.moleFrac(:,1) = (reconciledData.MS(:,2)./... - (reconciledData.MS(:,2)+reconciledData.MS(:,3))).^(paramFit(1)); % He [-] + % Use a fourier series model to obtain the mole fraction + reconciledData.moleFrac(:,1) = paramFit(reconciledData.MS(:,2)./... + (reconciledData.MS(:,2)+reconciledData.MS(:,3))); % He [-] reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); % CO2 [-] end end From 1ce97cfe3f6e56a5c4261a983d60e98915e06d08 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 26 Apr 2021 12:34:36 +0100 Subject: [PATCH 075/189] Add functionality to simulate real adsorbents with ZLC --- experimental/computeEquilibriumLoading.py | 136 +++++++++++ experimental/simulateCombinedModel.py | 80 +++++++ experimental/simulateDeadVolume.py | 15 +- experimental/simulateZLC.py | 262 ++++++++-------------- 4 files changed, 315 insertions(+), 178 deletions(-) create mode 100644 experimental/computeEquilibriumLoading.py create mode 100644 experimental/simulateCombinedModel.py diff --git a/experimental/computeEquilibriumLoading.py b/experimental/computeEquilibriumLoading.py new file mode 100644 index 0000000..e144f53 --- /dev/null +++ b/experimental/computeEquilibriumLoading.py @@ -0,0 +1,136 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Computes the equilibrium loading at a given pressure, temperature, or mole +# fraction using either single site Langmuir (SSL) or dual site Langmuir +# model (DSL) for pure gases. +# +# Last modified: +# - 2021-04-26, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def computeEquilibriumLoading(**kwargs): + import numpy as np + + # Total pressure of the gas [Pa] + if 'pressureTotal' in kwargs: + pressureTotal = np.array(kwargs["pressureTotal"]) + else: + pressureTotal = np.array([1e5]) + + # Temperature of the gas [K] + if 'temperature' in kwargs: + temperature = np.array(kwargs["temperature"]) + else: + temperature = np.array([298.15]) + + # Mole fraction of the gas [-] + if 'moleFrac' in kwargs: + moleFrac = np.array(kwargs["moleFrac"]) + else: + moleFrac = np.array([1.]) + + # Isotherm Model (SSL or DSL) + if 'isothermModel' in kwargs: + isothermModel = np.array(kwargs["isothermModel"]) + # If model has three parameters - SSL + if len(isothermModel) == 3: + modelType = 'SSL' + # If model has six parameters - DSL + elif len(isothermModel) == 6: + modelType = 'DSL' + else: + # Default isotherm model is DSL and uses CO2 isotherm on AC8 + # Reference: 10.1007/s10450-020-00268-7 + isothermModel = np.array([0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3]) + modelType = 'DSL' + + # Prepare the input parameters for the model + inputParameters = (pressureTotal,temperature,moleFrac,isothermModel) + + # Call the function of the relevant model type + if modelType == 'SSL': + equilibriumLoading = simulateSSL(*inputParameters) + elif modelType == 'DSL': + equilibriumLoading = simulateDSL(*inputParameters) + + # Return the equilibrium loading + return equilibriumLoading + +# fun: simulateSSL +# Simulates the single site Langmuir model (pure) at a given pressure, +# temperature and mole fraction +def simulateSSL(*inputParameters): + import numpy as np + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Unpack the tuple with the inputs for the isotherm model + pressureTotal,temperature,moleFrac,isothermModel = inputParameters + + # Compute the concentration at input pressure, temperature and mole fraction + localConc = pressureTotal*moleFrac/(Rg*temperature) + + # Compute the adsorption affinity + isoAffinity = isothermModel[1]*np.exp(-isothermModel[2]/(Rg*temperature)) + + # Compute the numerator and denominator of a pure single site Langmuir + isoNumerator = isothermModel[0]*isoAffinity*localConc + isoDenominator = 1 + isoAffinity*localConc + + # Compute the equilibrium loading + equilibriumLoading = isoNumerator/isoDenominator + + # Return the loading + return equilibriumLoading + +# fun: simulateDSL +# Simulates the dual site Langmuir model (pure) at a given pressure, +# temperature and mole fraction +def simulateDSL(*inputParameters): + import numpy as np + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Unpack the tuple with the inputs for the isotherm model + pressureTotal,temperature,moleFrac,isothermModel = inputParameters + + # Compute the concentration at input pressure, temperature and mole fraction + localConc = pressureTotal*moleFrac/(Rg*temperature) + + # Site 1 + # Compute the adsorption affinity + isoAffinity_1 = isothermModel[1]*np.exp(isothermModel[2]/(Rg*temperature)) + # Compute the numerator and denominator of a pure single site Langmuir + isoNumerator_1 = isothermModel[0]*isoAffinity_1*localConc + isoDenominator_1 = 1 + isoAffinity_1*localConc + + # Site 2 + # Compute the adsorption affinity + isoAffinity_2 = isothermModel[4]*np.exp(isothermModel[5]/(Rg*temperature)) + # Compute the numerator and denominator of a pure single site Langmuir + isoNumerator_2 = isothermModel[3]*isoAffinity_2*localConc + isoDenominator_2 = 1 + isoAffinity_2*localConc + + # Compute the equilibrium loading + equilibriumLoading = isoNumerator_1/isoDenominator_1 + isoNumerator_2/isoDenominator_2 + + # Return the loading + return equilibriumLoading \ No newline at end of file diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py new file mode 100644 index 0000000..a569329 --- /dev/null +++ b/experimental/simulateCombinedModel.py @@ -0,0 +1,80 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Simulates the full ZLC setup. The model calls the simulate ZLC function +# to simulate the sorption process and the response is fed to the dead +# volume simulator +# +# Last modified: +# - 2021-04-22, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +############################################################################ + +from simulateZLC import simulateZLC +from simulateDeadVolume import simulateDeadVolume +import os +from numpy import load + +# Call the simulateZLC function to simulate the sorption in a given sorbent +timeZLC, resultMat, _ = simulateZLC() + +# Parse out the mole fraction out from ZLC +moleFracZLC = resultMat[0,:] + +# Parse out the flow rate out from ZLC [m3/s] +flowRateZLC = resultMat[3,:]*1e6 # Convert to ccs + +# File with parameter estimates for the dead volume +simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' +fileParameter = 'deadVolumeCharacteristics_20210421_1558_49eb234.npz' +modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] +# Parse out dead volume parameters +x = modelOutputTemp[()]["variable"] +# Call the simulateDeadVolume function to simulate the dead volume of the setup +_ , _ , moleFracOut = simulateDeadVolume(timeInt = timeZLC, + initMoleFrac = moleFracZLC[0], + feedMoleFrac = moleFracZLC, + flowRate = flowRateZLC, + deadVolume_1 = x[0], + deadVolume_2M = x[1], + deadVolume_2D = x[2], + numTanks_1 = int(x[3]), + flowRate_D = x[4]) + +# Plot the model response +# Linear scale +# os.chdir("plotFunctions") +# import matplotlib.pyplot as plt +# plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file +# fig = plt.figure +# ax1 = plt.subplot(1,2,1) +# ax1.plot(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response +# ax1.plot(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response +# ax1.set(xlabel='$t$ [s]', +# ylabel='$y_1$ [-]', +# xlim = [0,300], ylim = [0, 1]) +# ax1.locator_params(axis="x", nbins=4) +# ax1.locator_params(axis="y", nbins=4) +# ax1.legend() + +# # Log scale +# ax2 = plt.subplot(1,2,2) +# ax2.semilogy(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response +# ax2.semilogy(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response +# ax2.set(xlabel='$t$ [s]', +# xlim = [0,300], ylim = [1e-3, 1]) +# ax2.locator_params(axis="x", nbins=4) +# ax2.legend() diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index a1aa259..2d40e6d 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -12,10 +12,11 @@ # Simulates the dead volume using the tanks in series (TIS) for the ZLC # Reference: 10.1016/j.ces.2008.02.023 # The methodolgy is slighlty modified to incorporate diffusive pockets using -# compartment models (see Levenspiel, chapter 12) or Lisa Joss's article +# compartment models (see Levenspiel, chapter 12 or Lisa Joss's article) # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-26, AK: Change default model parameter values # - 2021-04-21, AK: Change model to fix split velocity # - 2021-04-20, AK: Change model to flow dependent split # - 2021-04-20, AK: Change model to flow dependent split @@ -57,27 +58,27 @@ def simulateDeadVolume(**kwargs): if 'deadVolume_1' in kwargs: deadVolume_1 = kwargs["deadVolume_1"] else: - deadVolume_1 = 3 + deadVolume_1 = 3.67 # Number of tanks of the first volume [-] if 'numTanks_1' in kwargs: numTanks_1 = kwargs["numTanks_1"] else: - numTanks_1 = 20 + numTanks_1 = 16 # Dead Volume of the second volume (mixing) [cc] if 'deadVolume_2M' in kwargs: deadVolume_2M = kwargs["deadVolume_2M"] else: - deadVolume_2M = 0.2 + deadVolume_2M = 1.10 # Dead Volume of the second volume (diffusive) [cc] if 'deadVolume_2D' in kwargs: deadVolume_2D = kwargs["deadVolume_2D"] else: - deadVolume_2D = 0.1 + deadVolume_2D = 0.51 # Flow rate in the diffusive volume [-] if 'flowRate_D' in kwargs: flowRate_D = kwargs["flowRate_D"] else: - flowRate_D = 0.05 + flowRate_D = 0.009 # Initial Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) @@ -92,7 +93,7 @@ def simulateDeadVolume(**kwargs): if 'timeInt' in kwargs: timeInt = kwargs["timeInt"] else: - timeInt = (0.0,2000) + timeInt = (0.0,3600) # If experimental data used, then initialize ode evaluation time to # experimental time, else use default diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index c0f0537..2afdd3b 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -14,6 +14,7 @@ # model with mass transfer defined using linear driving force. # # Last modified: +# - 2021-04-26, AK: Revamp the code for real sorbent simulation # - 2021-03-25, AK: Remove the constant F model # - 2021-03-01, AK: Initial creation # @@ -27,7 +28,7 @@ def simulateZLC(**kwargs): import numpy as np from scipy.integrate import solve_ivp - from simulateSensorArray import simulateSensorArray + from computeEquilibriumLoading import computeEquilibriumLoading import auxiliaryFunctions import os @@ -42,18 +43,12 @@ def simulateZLC(**kwargs): # 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]) # Kinetic rate constants [/s] if 'rateConstant' in kwargs: rateConstant = np.array(kwargs["rateConstant"]) else: - rateConstant = np.array([.01,.01,.01]) + rateConstant = np.array([0.1]) # Feed flow rate [m3/s] if 'flowIn' in kwargs: @@ -65,48 +60,38 @@ def simulateZLC(**kwargs): if 'feedMoleFrac' in kwargs: feedMoleFrac = np.array(kwargs["feedMoleFrac"]) else: - feedMoleFrac = np.array([1.,0.,0.]) + feedMoleFrac = np.array([0.]) # Initial Gas Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) else: # Equilibrium process - initMoleFrac = np.array([0.,0.,1.]) + initMoleFrac = np.array([1.]) # Time span for integration [tuple with t0 and tf] if 'timeInt' in kwargs: timeInt = kwargs["timeInt"] else: - timeInt = (0.0,2000) + timeInt = (0.0,300) # Volume of sorbent material [m3] if 'volSorbent' in kwargs: volSorbent = kwargs["volSorbent"] else: - volSorbent = 5e-7 + volSorbent = 1.5e-8 # Volume of gas chamber (dead volume) [m3] if 'volGas' in kwargs: volGas = kwargs["volGas"] else: - volGas = 5e-7 + volGas = 2.5e-8 - # 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)): - raise Exception("The dimensions of the mole fraction or rate constant and the number of gases in the adsorbent is not consistent!") + # Adsorbent density [kg/m3] + if 'adsorbentDensity' in kwargs: + adsorbentDensity = kwargs["adsorbentDensity"] else: - numberOfGases = len(feedMoleFrac) + adsorbentDensity = 760 # Activated carbon density [kg/m3] # Total pressure of the gas [Pa] if 'pressureTotal' in kwargs: @@ -122,33 +107,33 @@ def simulateZLC(**kwargs): temperature = np.array([298.15]); # Compute the initial sensor loading [mol/m3] @ initMoleFrac - sensorLoadingPerGasVol, adsorbentDensity, molecularWeight = simulateSensorArray(sensorID, pressureTotal, - temperature, np.array([initMoleFrac]), - fullModel = True) + equilibriumLoading = computeEquilibriumLoading(pressureTotal=pressureTotal, + temperature=temperature, + moleFrac=initMoleFrac)*adsorbentDensity # [mol/m3] # Prepare tuple of input parameters for the ode solver - inputParameters = (sensorID, adsorbentDensity, rateConstant, numberOfGases, flowIn, - feedMoleFrac, initMoleFrac, pressureTotal, temperature, volSorbent, volGas) + inputParameters = (adsorbentDensity, rateConstant, flowIn, feedMoleFrac, + initMoleFrac, pressureTotal, temperature, volSorbent, volGas) # Solve the system of ordinary differential equations # Stiff solver used for the problem: BDF or Radau - # The output is print out every 5 s + # The output is print out every 0.1 s # Solves the model assuming constant/negligible pressure across the sensor # Prepare initial conditions vector - 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, - method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],5), - rtol = 1e-6, args = inputParameters) + initialConditions = np.zeros([2]) + initialConditions[0] = initMoleFrac[0] # Gas mole fraction + initialConditions[1] = equilibriumLoading # Initial Loading + + outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, + method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],0.1), + rtol = 1e-8, 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 + sum_dqdt = np.gradient(outputSol.y[1,:], + outputSol.t) # Compute gradient of loading flowOut = flowIn - ((volSorbent*(8.314*temperature)/pressureTotal)*(sum_dqdt)) # Parse out the output matrix and add flow rate @@ -157,62 +142,53 @@ def simulateZLC(**kwargs): # Parse out the time timeSim = outputSol.t - # 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, + plotFullModelResult(timeSim, resultMat, inputParameters, gitCommitID, currentDT) # Return time and the output matrix - return timeSim, resultMat, sensorFingerPrint, inputParameters + return timeSim, resultMat, inputParameters -# func: solveSorptionEquationConstP - Constant pressure model +# func: solveSorptionEquation - Constant pressure model # Solves the system of ODEs to evaluate the gas composition and loadings -def solveSorptionEquationConstP(t, f, *inputParameters): +def solveSorptionEquation(t, f, *inputParameters): import numpy as np - from simulateSensorArray import simulateSensorArray - + from computeEquilibriumLoading import computeEquilibriumLoading + # 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 + adsorbentDensity, rateConstant, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero - df = np.zeros([2*numberOfGases-1]) - - # 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) + df = np.zeros([2]) + # Compute the initial sensor loading [mol/m3] @ initMoleFrac + equilibriumLoading = computeEquilibriumLoading(pressureTotal=pressureTotal, + temperature=temperature, + moleFrac=f[0])*adsorbentDensity # [mol/m3] + # 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])) + df[1] = rateConstant*(equilibriumLoading-f[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]))) + flowOut = flowIn - (volSorbent*(Rg*temperature)/pressureTotal)*df[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 + term2 = ((flowIn*feedMoleFrac - flowOut*f[0]) + - (volSorbent*(Rg*temperature)/pressureTotal)*df[1]) + df[0] = 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, +def plotFullModelResult(timeSim, resultMat, inputParameters, gitCommitID, currentDT): import numpy as np import os @@ -223,107 +199,51 @@ def plotFullModelResult(timeSim, resultMat, sensorFingerPrint, inputParameters, saveFileExtension = ".png" # Unpack the tuple of input parameters used to solve equations - sensorID, _ , _ , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters + adsorbentDensity , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters os.chdir("plotFunctions") - if resultMat.shape[0] == 7: - # 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, the flow rate, and the mole fraction - 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') - 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) - 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') - 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 - 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() + # 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[1,:]/adsorbentDensity, + linewidth=1.5,color='r') + ax.set(xlabel='$t$ [s]', + ylabel='$q_1$ [mol kg$^{\mathregular{-1}}$]', + xlim = [timeSim[0], timeSim[-1]], ylim = [0, 1.1*np.max(resultMat[1,:]/adsorbentDensity)]) + + ax = plt.subplot(1,3,2) + ax.semilogy(timeSim, resultMat[0,:],linewidth=1.5,color='r') + ax.set(xlabel='$t$ [s]', + ylabel='$y$ [-]', + xlim = [timeSim[0], timeSim[-1]], ylim = [1e-4, 1.]) + + ax = plt.subplot(1,3,3) + ax.plot(timeSim, resultMat[2,:], + 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[2,:])]) + + ax2 = plt.twinx() + ax2.plot(timeSim, resultMat[3,:], + 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[3,:])]) + + # Save the figure + if saveFlag: + # FileName: ZLCResponse__ + saveFileName = "ZLCResponse_" + "_" + 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 4d61591716fc5ccff8a8b81ce40247c25b17e7a5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 26 Apr 2021 12:36:00 +0100 Subject: [PATCH 076/189] Cosmetic change --- experimental/simulateCombinedModel.py | 42 +++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index a569329..df5230c 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -56,25 +56,25 @@ # Plot the model response # Linear scale -# os.chdir("plotFunctions") -# import matplotlib.pyplot as plt -# plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file -# fig = plt.figure -# ax1 = plt.subplot(1,2,1) -# ax1.plot(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response -# ax1.plot(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response -# ax1.set(xlabel='$t$ [s]', -# ylabel='$y_1$ [-]', -# xlim = [0,300], ylim = [0, 1]) -# ax1.locator_params(axis="x", nbins=4) -# ax1.locator_params(axis="y", nbins=4) -# ax1.legend() +os.chdir("plotFunctions") +import matplotlib.pyplot as plt +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file +fig = plt.figure +ax1 = plt.subplot(1,2,1) +ax1.plot(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response +ax1.plot(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response +ax1.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = [0,300], ylim = [0, 1]) +ax1.locator_params(axis="x", nbins=4) +ax1.locator_params(axis="y", nbins=4) +ax1.legend() -# # Log scale -# ax2 = plt.subplot(1,2,2) -# ax2.semilogy(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response -# ax2.semilogy(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response -# ax2.set(xlabel='$t$ [s]', -# xlim = [0,300], ylim = [1e-3, 1]) -# ax2.locator_params(axis="x", nbins=4) -# ax2.legend() +# Log scale +ax2 = plt.subplot(1,2,2) +ax2.semilogy(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response +ax2.semilogy(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response +ax2.set(xlabel='$t$ [s]', + xlim = [0,300], ylim = [1e-3, 1]) +ax2.locator_params(axis="x", nbins=4) +ax2.legend() From d2963b1e0e25d2042a09bd6db7aa33a0765b5bac Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 27 Apr 2021 11:36:34 +0100 Subject: [PATCH 077/189] First version of ZLC parameter estimation --- experimental/extractDeadVolume.py | 11 +- experimental/simulateCombinedModel.py | 185 +++++++++++++++++++------- experimental/simulateDeadVolume.py | 19 ++- experimental/simulateZLC.py | 43 +++++- 4 files changed, 196 insertions(+), 62 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index a687f5a..977fc0c 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-04-27, AK: Cosmetic changes to structure # - 2021-04-21, AK: Change model to fix split velocity # - 2021-04-20, AK: Change model to flow dependent split # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV @@ -57,7 +58,7 @@ def extractDeadVolume(): currentDT = auxiliaryFunctions.getCurrentDateTime() # Directory of raw data - mainDir = 'experimental/runData' + mainDir = 'runData' # File name of the experiments fileName = ['ZLC_DeadVolume_Exp13A_Output.mat', 'ZLC_DeadVolume_Exp13B_Output.mat', @@ -65,6 +66,7 @@ def extractDeadVolume(): 'ZLC_DeadVolume_Exp13D_Output.mat', 'ZLC_DeadVolume_Exp13E_Output.mat', 'ZLC_DeadVolume_Exp13F_Output.mat'] + # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName) @@ -154,13 +156,13 @@ def deadVolObjectiveFunction(x): flowRateTemp = load(filePath[ii])["flowRate"].flatten() timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] - flowRate = flowRateTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] # Integration and ode evaluation time (check simulateDeadVolume) timeInt = timeElapsedExp # Compute the experimental volume (using trapz) - expVolume = max([expVolume, np.trapz(moleFracExp,np.multiply(flowRate, timeElapsedExp))]) + expVolume = max([expVolume, np.trapz(moleFracExp,np.multiply(flowRateExp, timeElapsedExp))]) # Compute the dead volume response using the optimizer parameters _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = x[0], @@ -169,7 +171,8 @@ def deadVolObjectiveFunction(x): numTanks_1 = int(x[3]), flowRate_D = x[4], timeInt = timeInt, - flowRate = flowRate) + flowRate = flowRateExp, + expFlag = True) # Compute the sum of the error for the difference between exp. and sim. numPoints += len(moleFracExp) diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index df5230c..7130b14 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -14,6 +14,7 @@ # volume simulator # # Last modified: +# - 2021-04-27, AK: Convert to a function for parameter estimation # - 2021-04-22, AK: Initial creation # # Input arguments: @@ -23,58 +24,144 @@ # ############################################################################ -from simulateZLC import simulateZLC -from simulateDeadVolume import simulateDeadVolume -import os -from numpy import load +def simulateCombinedModel(**kwargs): + import numpy as np + import auxiliaryFunctions + from simulateZLC import simulateZLC + from simulateDeadVolume import simulateDeadVolume + from numpy import load + import os -# Call the simulateZLC function to simulate the sorption in a given sorbent -timeZLC, resultMat, _ = simulateZLC() + # 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() -# Parse out the mole fraction out from ZLC -moleFracZLC = resultMat[0,:] + # Isotherm model parameters (SSL or DSL) + if 'isothermModel' in kwargs: + isothermModel = ["isothermModel"] + else: + # Default isotherm model is DSL and uses CO2 isotherm on AC8 + # Reference: 10.1007/s10450-020-00268-7 + isothermModel = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3] -# Parse out the flow rate out from ZLC [m3/s] -flowRateZLC = resultMat[3,:]*1e6 # Convert to ccs + # Kinetic rate constants [/s] + if 'rateConstant' in kwargs: + rateConstant = kwargs["rateConstant"] + else: + rateConstant = [0.5] -# File with parameter estimates for the dead volume -simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' -fileParameter = 'deadVolumeCharacteristics_20210421_1558_49eb234.npz' -modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] -# Parse out dead volume parameters -x = modelOutputTemp[()]["variable"] -# Call the simulateDeadVolume function to simulate the dead volume of the setup -_ , _ , moleFracOut = simulateDeadVolume(timeInt = timeZLC, - initMoleFrac = moleFracZLC[0], - feedMoleFrac = moleFracZLC, - flowRate = flowRateZLC, - deadVolume_1 = x[0], - deadVolume_2M = x[1], - deadVolume_2D = x[2], - numTanks_1 = int(x[3]), - flowRate_D = x[4]) + # Feed flow rate [m3/s] + if 'flowIn' in kwargs: + flowIn = kwargs["flowIn"] + else: + flowIn = [5e-7] -# Plot the model response -# Linear scale -os.chdir("plotFunctions") -import matplotlib.pyplot as plt -plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file -fig = plt.figure -ax1 = plt.subplot(1,2,1) -ax1.plot(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response -ax1.plot(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response -ax1.set(xlabel='$t$ [s]', - ylabel='$y_1$ [-]', - xlim = [0,300], ylim = [0, 1]) -ax1.locator_params(axis="x", nbins=4) -ax1.locator_params(axis="y", nbins=4) -ax1.legend() + # Initial Gas Mole Fraction [-] + if 'initMoleFrac' in kwargs: + initMoleFrac = kwargs["initMoleFrac"] + else: + # Equilibrium process + initMoleFrac = [0.8] -# Log scale -ax2 = plt.subplot(1,2,2) -ax2.semilogy(timeZLC,moleFracZLC,linewidth = 2,color='b',label = 'ZLC') # ZLC response -ax2.semilogy(timeZLC,moleFracOut,linewidth = 2,color='r',label = 'ZLC+DV') # Combined model response -ax2.set(xlabel='$t$ [s]', - xlim = [0,300], ylim = [1e-3, 1]) -ax2.locator_params(axis="x", nbins=4) -ax2.legend() + # Time span for integration [tuple with t0 and tf] + if 'timeInt' in kwargs: + timeInt = kwargs["timeInt"] + else: + timeInt = (0.0,300) + + # File name with dead volume characteristics parameters + if 'deadVolumeFile' in kwargs: + deadVolumeFile = kwargs["deadVolumeFile"] + else: + deadVolumeFile = 'deadVolumeCharacteristics_20210421_1558_49eb234.npz' + + # Flag to check if experimental data used + if 'expFlag' in kwargs: + expFlag = kwargs["expFlag"] + else: + expFlag = False + + # Call the simulateZLC function to simulate the sorption in a given sorbent + timeZLC, resultMat, _ = simulateZLC(isothermModel=isothermModel, + rateConstant=rateConstant, + flowIn = flowIn, + initMoleFrac = initMoleFrac, + timeInt = timeInt, + expFlag=expFlag) + + # Parse out the mole fraction out from ZLC + moleFracZLC = resultMat[0,:] + + # Parse out the flow rate out from ZLC [m3/s] + flowRateZLC = resultMat[3,:]*1e6 # Convert to ccs + + # File with parameter estimates for the dead volume + deadVolumeDir = '..' + os.path.sep + 'simulationResults/' + modelOutputTemp = load(deadVolumeDir+deadVolumeFile, allow_pickle=True)["modelOutput"] + # Parse out dead volume parameters + x = modelOutputTemp[()]["variable"] + # Call the simulateDeadVolume function to simulate the dead volume of the setup + _ , _ , moleFracOut = simulateDeadVolume(timeInt = timeZLC, + initMoleFrac = moleFracZLC[0], + feedMoleFrac = moleFracZLC, + flowRate = flowRateZLC, + expFlag = True, # Note this is true as flow rate from ZLC used + deadVolume_1 = x[0], + deadVolume_2M = x[1], + deadVolume_2D = x[2], + numTanks_1 = int(x[3]), + flowRate_D = x[4]) + + # Plot results if needed + if plotFlag: + plotCombinedModel(timeZLC,moleFracOut,moleFracZLC,flowRateZLC) + + # Return the time, mole fraction (all), resultMat (ZLC) + return timeZLC, moleFracOut, resultMat + +# fun: plotCombinedModel +# Plots the response of the combined ZLC+DV model +def plotCombinedModel(timeZLC,moleFracOut,moleFracZLC,flowRateZLC): + import os + import numpy as np + os.chdir(".."+os.path.sep+"plotFunctions") + import matplotlib.pyplot as plt + + # Plot the model response + # Linear scale + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax1 = plt.subplot(1,3,1) + ax1.plot(timeZLC,moleFracZLC,linewidth = 2,color='b',label='ZLC') # ZLC response + ax1.plot(timeZLC,moleFracOut,linewidth = 2,color='r',label='ZLC+DV') # Combined model response + ax1.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = [0,300], ylim = [0, 1]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.legend() + + # Log scale + ax2 = plt.subplot(1,3,2) + ax2.plot(timeZLC,moleFracZLC,linewidth = 2,color='b') # ZLC response + ax2.plot(timeZLC,moleFracOut,linewidth = 2,color='r') # Combined model response + ax2.set(xlabel='$t$ [s]', + xlim = [0,300], ylim = [1e-4, 1.]) + ax2.locator_params(axis="x", nbins=4) + ax2.legend() + + # Ft - Log scale + ax2 = plt.subplot(1,3,3) + ax2.semilogy(np.multiply(flowRateZLC,timeZLC),moleFracZLC,linewidth = 2,color='b') # ZLC response + ax2.semilogy(np.multiply(flowRateZLC,timeZLC),moleFracOut,linewidth = 2,color='r') # Combined model response + ax2.set(xlabel='$t$ [s]', + xlim = [0,300], ylim = [1e-4, 1.]) + ax2.locator_params(axis="x", nbins=4) + ax2.legend() + plt.show() + os.chdir("..") diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 2d40e6d..73c3dd8 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -39,6 +39,10 @@ def simulateDeadVolume(**kwargs): import numpy as np from scipy.integrate import solve_ivp import auxiliaryFunctions + import os + + # Move to top level folder (to avoid path issues) + os.chdir("..") # Plot flag plotFlag = False @@ -95,9 +99,15 @@ def simulateDeadVolume(**kwargs): else: timeInt = (0.0,3600) + # Flag to check if experimental data used + if 'expFlag' in kwargs: + expFlag = kwargs["expFlag"] + else: + expFlag = False + # If experimental data used, then initialize ode evaluation time to # experimental time, else use default - if flowRate.size == 1: + if expFlag is False: t_eval = np.arange(timeInt[0],timeInt[-1],0.1) else: # Use experimental time (from timeInt) for ode evaluations to avoid @@ -143,6 +153,9 @@ def simulateDeadVolume(**kwargs): # Plot the dead volume response if plotFlag: plotOutletConcentration(timeSim,moleFracIn,moleFracOut) + + # Move to local folder (to avoid path issues) + os.chdir("experimental") return timeSim, moleFracIn, moleFracOut @@ -216,7 +229,7 @@ def plotOutletConcentration(timeSim, moleFracIn, moleFracOut): import matplotlib.pyplot as plt # Plot the solid phase compositions - os.chdir(".."+os.path.sep+"plotFunctions") + os.chdir("plotFunctions") plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file fig = plt.figure ax = plt.subplot(1,1,1) @@ -231,4 +244,4 @@ def plotOutletConcentration(timeSim, moleFracIn, moleFracOut): xlim = [timeSim[0], 1000], ylim = [1e-4, 1.1*np.max(moleFracOut)]) ax.legend() plt.show() - os.chdir(".."+os.path.sep+"experimental") \ No newline at end of file + os.chdir("..") \ No newline at end of file diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 2afdd3b..8516600 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -14,6 +14,7 @@ # model with mass transfer defined using linear driving force. # # Last modified: +# - 2021-04-27, AK: Fix inputs and add isotherm model as input # - 2021-04-26, AK: Revamp the code for real sorbent simulation # - 2021-03-25, AK: Remove the constant F model # - 2021-03-01, AK: Initial creation @@ -74,6 +75,23 @@ def simulateZLC(**kwargs): timeInt = kwargs["timeInt"] else: timeInt = (0.0,300) + + # Flag to check if experimental data used + if 'expFlag' in kwargs: + expFlag = kwargs["expFlag"] + else: + expFlag = False + + # If experimental data used, then initialize ode evaluation time to + # experimental time, else use default + if expFlag is False: + t_eval = np.arange(timeInt[0],timeInt[-1],0.1) + else: + # Use experimental time (from timeInt) for ode evaluations to avoid + # interpolating any data. t_eval is also used for interpolating + # flow rate in the ode equations + t_eval = timeInt + timeInt = (0.0,max(timeInt)) # Volume of sorbent material [m3] if 'volSorbent' in kwargs: @@ -87,6 +105,14 @@ def simulateZLC(**kwargs): else: volGas = 2.5e-8 + # Isotherm model parameters (SSL or DSL) + if 'isothermModel' in kwargs: + isothermModel = kwargs["isothermModel"] + else: + # Default isotherm model is DSL and uses CO2 isotherm on AC8 + # Reference: 10.1007/s10450-020-00268-7 + isothermModel = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3] + # Adsorbent density [kg/m3] if 'adsorbentDensity' in kwargs: adsorbentDensity = kwargs["adsorbentDensity"] @@ -109,10 +135,11 @@ def simulateZLC(**kwargs): # Compute the initial sensor loading [mol/m3] @ initMoleFrac equilibriumLoading = computeEquilibriumLoading(pressureTotal=pressureTotal, temperature=temperature, - moleFrac=initMoleFrac)*adsorbentDensity # [mol/m3] + moleFrac=initMoleFrac, + isothermModel=isothermModel)*adsorbentDensity # [mol/m3] # Prepare tuple of input parameters for the ode solver - inputParameters = (adsorbentDensity, rateConstant, flowIn, feedMoleFrac, + inputParameters = (adsorbentDensity, isothermModel, rateConstant, flowIn, feedMoleFrac, initMoleFrac, pressureTotal, temperature, volSorbent, volGas) # Solve the system of ordinary differential equations @@ -125,7 +152,7 @@ def simulateZLC(**kwargs): initialConditions[1] = equilibriumLoading # Initial Loading outputSol = solve_ivp(solveSorptionEquation, timeInt, initialConditions, - method='Radau', t_eval = np.arange(timeInt[0],timeInt[1],0.1), + method='Radau', t_eval = t_eval, rtol = 1e-8, args = inputParameters) # Presure vector in output @@ -147,6 +174,9 @@ def simulateZLC(**kwargs): plotFullModelResult(timeSim, resultMat, inputParameters, gitCommitID, currentDT) + # Move to local folder (to avoid path issues) + os.chdir("experimental") + # Return time and the output matrix return timeSim, resultMat, inputParameters @@ -160,7 +190,7 @@ def solveSorptionEquation(t, f, *inputParameters): Rg = 8.314; # [J/mol K] # Unpack the tuple of input parameters used to solve equations - adsorbentDensity, rateConstant, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters + adsorbentDensity, isothermModel, rateConstant, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero df = np.zeros([2]) @@ -168,7 +198,8 @@ def solveSorptionEquation(t, f, *inputParameters): # Compute the initial sensor loading [mol/m3] @ initMoleFrac equilibriumLoading = computeEquilibriumLoading(pressureTotal=pressureTotal, temperature=temperature, - moleFrac=f[0])*adsorbentDensity # [mol/m3] + moleFrac=f[0], + isothermModel=isothermModel)*adsorbentDensity # [mol/m3] # Linear driving force model (derivative of solid phase loadings) df[1] = rateConstant*(equilibriumLoading-f[1]) @@ -199,7 +230,7 @@ def plotFullModelResult(timeSim, resultMat, inputParameters, saveFileExtension = ".png" # Unpack the tuple of input parameters used to solve equations - adsorbentDensity , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters + adsorbentDensity , _ , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters os.chdir("plotFunctions") # Plot the solid phase compositions From aaa98e43c1df53d048287ed5c5f6e8eb4ab40681 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 27 Apr 2021 11:39:19 +0100 Subject: [PATCH 078/189] First working version of ZLC parameter estimation --- experimental/extractZLCParameters.py | 191 ++++++++++++++++++++++++++ experimental/simulateCombinedModel.py | 17 ++- 2 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 experimental/extractZLCParameters.py diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py new file mode 100644 index 0000000..42344ce --- /dev/null +++ b/experimental/extractZLCParameters.py @@ -0,0 +1,191 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Find the isotherm parameters and the kinetic rate constant by fitting +# the complete response curve from the ZLC experiment. Note that currently +# the isotherm can be SSL or DSL model. The rate constant is assumed to be a +# constant in the LDF model +# Reference: 10.1016/j.ces.2014.12.062 +# +# Last modified: +# - 2021-04-27, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +############################################################################ + +def extractZLCParameters(): + import numpy as np + from geneticalgorithm2 import geneticalgorithm2 as ga # GA + from extractDeadVolume import filesToProcess # File processing script + import auxiliaryFunctions + import os + from numpy import savez + import multiprocessing # For parallel processing + import socket + + # Find out the total number of cores available for parallel processing + num_cores = multiprocessing.cpu_count() + + # Isotherm model type + modelType = 'DSL' + + # Number of times optimization repeated + numOptRepeat = 10 + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Directory of raw data + mainDir = 'runData' + # File name of the experiments + fileName = ['ZLC_ActivatedCarbon_Exp15A_Output.mat', + 'ZLC_ActivatedCarbon_Exp15B_Output.mat', + 'ZLC_ActivatedCarbon_Exp15C_Output.mat', + 'ZLC_ActivatedCarbon_Exp15D_Output.mat', + 'ZLC_ActivatedCarbon_Exp15E_Output.mat', + 'ZLC_ActivatedCarbon_Exp15F_Output.mat'] + + # NOTE: Dead volume characteristics filename is hardcoded in + # simulateCombinedModel. This is because of the python GA function unable + # to pass arguments + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,mainDir,fileName) + + # Define the bounds and the type of the parameters to be optimized + # Single-site Langmuir + if modelType == 'SSL': + optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,1], + [np.finfo(float).eps,50e3], [np.finfo(float).eps,100])) + optType=np.array(['real','real','real','real']) + problemDimension = len(optType) + # Dual-site Langmuir + elif modelType == 'DSL': + optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,1], + [np.finfo(float).eps,50e3], [np.finfo(float).eps,10], + [np.finfo(float).eps,1], [np.finfo(float).eps,50e3], + [np.finfo(float).eps,100])) + optType=np.array(['real','real','real','real','real','real','real']) + problemDimension = len(optType) + + # Algorithm parameters for GA + algorithm_param = {'max_num_iteration':5, + 'population_size':1600, + 'mutation_probability':0.1, + 'crossover_probability': 0.55, + 'parents_portion': 0.15, + 'elit_ratio': 0.01, + 'max_iteration_without_improv':None} + + # Minimize an objective function to compute the dead volume and the number of + # tanks for the dead volume using GA + model = ga(function = ZLCObjectiveFunction, dimension=problemDimension, + variable_type_mixed = optType, + variable_boundaries = optBounds, + algorithm_parameters=algorithm_param) + + # Call the GA optimizer using multiple cores + model.run(set_function=ga.set_function_multiprocess(ZLCObjectiveFunction, + n_jobs = num_cores), + no_plot = True) + # Repeat the optimization with the last generation repeated numOptRepeat + # times (for better accuracy) + for ii in range(numOptRepeat): + model.run(set_function=ga.set_function_multiprocess(ZLCObjectiveFunction, + n_jobs = num_cores), + start_generation=model.output_dict['last_generation'], no_plot = True) + + # Save the array concentration into a native numpy file + # The .npz file is saved in a folder called simulationResults (hardcoded) + filePrefix = "zlcParameters" + saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID; + savePath = os.path.join('..','simulationResults',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationResults')): + os.mkdir(os.path.join('..','simulationResults')) + + # Save the output into a .npz file + savez (savePath, modelOutput = model.output_dict, # Model output + optBounds = optBounds, # Optimizer bounds + algoParameters = algorithm_param, # Algorithm parameters + fileName = fileName, # Names of file used for fitting + hostName = socket.gethostname()) # Hostname of the computer) + + # Return the optimized values + return model.output_dict + +# func: deadVolObjectiveFunction +# For use with GA, the function accepts only one input (parameters from the +# optimizer) +def ZLCObjectiveFunction(x): + import numpy as np + from numpy import load + from extractDeadVolume import filesToProcess # File processing script + from simulateCombinedModel import simulateCombinedModel + + # Prepare isotherm model (the first n-1 parameters are for the isotherm model) + isothermModel = x[0:-1] + + # Load the names of the file to be used for estimating dead volume characteristics + filePath = filesToProcess(False,[],[]) + + # Parse out number of data points for each experiment (for downsampling) + numPointsExp = np.zeros(len(filePath)) + for ii in range(len(filePath)): + # Load experimental molefraction + timeElapsedExp = load(filePath[ii])["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Initialize error for objective function + computedError = 0 + numPoints = 0 + + # Loop over all available files + for ii in range(len(filePath)): + # Initialize outputs + moleFracSim = [] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(filePath[ii])["timeElapsed"].flatten() + moleFracExpTemp = load(filePath[ii])["moleFrac"].flatten() + flowRateTemp = load(filePath[ii])["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Compute the dead volume response using the optimizer parameters + _ , moleFracSim , _ = simulateCombinedModel(isothermModel = isothermModel, + rateConstant = x[-1], # Last element is rate constant + timeInt = timeInt, + initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point + flowIn = np.mean(flowRateExp[-1:-10:-1]), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = True) + + # Compute the sum of the error for the difference between exp. and sim. + numPoints += len(moleFracExp) + computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) + + # Compute the sum of the error for the difference between exp. and sim. and + # add a penalty if needed + return (numPoints/2)*computedError \ No newline at end of file diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index 7130b14..d0db7e3 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -43,7 +43,7 @@ def simulateCombinedModel(**kwargs): # Isotherm model parameters (SSL or DSL) if 'isothermModel' in kwargs: - isothermModel = ["isothermModel"] + isothermModel = kwargs["isothermModel"] else: # Default isotherm model is DSL and uses CO2 isotherm on AC8 # Reference: 10.1007/s10450-020-00268-7 @@ -65,8 +65,7 @@ def simulateCombinedModel(**kwargs): if 'initMoleFrac' in kwargs: initMoleFrac = kwargs["initMoleFrac"] else: - # Equilibrium process - initMoleFrac = [0.8] + initMoleFrac = [1.] # Time span for integration [tuple with t0 and tf] if 'timeInt' in kwargs: @@ -156,12 +155,12 @@ def plotCombinedModel(timeZLC,moleFracOut,moleFracZLC,flowRateZLC): ax2.legend() # Ft - Log scale - ax2 = plt.subplot(1,3,3) - ax2.semilogy(np.multiply(flowRateZLC,timeZLC),moleFracZLC,linewidth = 2,color='b') # ZLC response - ax2.semilogy(np.multiply(flowRateZLC,timeZLC),moleFracOut,linewidth = 2,color='r') # Combined model response - ax2.set(xlabel='$t$ [s]', + ax3 = plt.subplot(1,3,3) + ax3.semilogy(np.multiply(flowRateZLC,timeZLC),moleFracZLC,linewidth = 2,color='b') # ZLC response + ax3.semilogy(np.multiply(flowRateZLC,timeZLC),moleFracOut,linewidth = 2,color='r') # Combined model response + ax3.set(xlabel='$t$ [s]', xlim = [0,300], ylim = [1e-4, 1.]) - ax2.locator_params(axis="x", nbins=4) - ax2.legend() + ax3.locator_params(axis="x", nbins=4) + ax3.legend() plt.show() os.chdir("..") From 3e12991a6ce621546d16af7efe6ab4d7569403ed Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 27 Apr 2021 14:08:00 +0100 Subject: [PATCH 079/189] Fix flow rate units --- experimental/extractZLCParameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 42344ce..3e73b6b 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -169,7 +169,7 @@ def ZLCObjectiveFunction(x): flowRateTemp = load(filePath[ii])["flowRate"].flatten() timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] - flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] # [cc/s] # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) timeInt = timeElapsedExp @@ -179,7 +179,7 @@ def ZLCObjectiveFunction(x): rateConstant = x[-1], # Last element is rate constant timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point - flowIn = np.mean(flowRateExp[-1:-10:-1]), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) expFlag = True) # Compute the sum of the error for the difference between exp. and sim. From a7668fba7d3bc589b913c3f52195a7acb5f46d08 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 27 Apr 2021 14:28:18 +0100 Subject: [PATCH 080/189] Bug fix for directory path --- experimental/extractZLCParameters.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 3e73b6b..37b8f74 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -35,6 +35,12 @@ def extractZLCParameters(): import multiprocessing # For parallel processing import socket + # Change path directory + # Assumes either running from ERASE or from experimental. Either ways + # this has to be run from experimental + if not os.getcwd().split(os.path.sep)[-1] == 'experimental': + os.chdir("experimental") + # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() From 8e60357e6171ce182b08f1063d7e9e576ecb16c9 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 27 Apr 2021 16:31:25 +0100 Subject: [PATCH 081/189] Fix another path issue --- experimental/simulateCombinedModel.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index d0db7e3..9864157 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -26,20 +26,23 @@ def simulateCombinedModel(**kwargs): import numpy as np - import auxiliaryFunctions from simulateZLC import simulateZLC from simulateDeadVolume import simulateDeadVolume from numpy import load import os - # Plot flag - plotFlag = False - + # Move to top level folder (to avoid path issues) + os.chdir("..") + import auxiliaryFunctions # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() + os.chdir("experimental") # Get the current date and time for saving purposes currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Plot flag + plotFlag = False # Isotherm model parameters (SSL or DSL) if 'isothermModel' in kwargs: @@ -53,7 +56,7 @@ def simulateCombinedModel(**kwargs): if 'rateConstant' in kwargs: rateConstant = kwargs["rateConstant"] else: - rateConstant = [0.5] + rateConstant = [0.3] # Feed flow rate [m3/s] if 'flowIn' in kwargs: From 4275f6d6b65a6e7889d064417a3d6b5dc8ed7ea9 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Tue, 27 Apr 2021 18:56:58 +0100 Subject: [PATCH 082/189] Change the calibration model to linear interpolation --- experimental/analyzeCalibration.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index 91cd0ff..24af9eb 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-04-27, AK: Change the calibration model to linear interpolation % - 2021-04-23, AK: Change the calibration model to Fourier series based % - 2021-04-21, AK: Change the calibration equation to mole fraction like % - 2021-04-19, AK: Change MFC and MFM calibration (for mixtures) @@ -199,7 +200,7 @@ function analyzeCalibration(parametersFlow,parametersMS) % Perform an optimization to obtain parameter estimates to fit the % signal fraction of the helium signal to He+CO2 signal % Use a fourier series to fit the calibration data - calibrationMS.ratioHeCO2 = fit((meanHeSignal./(meanCO2Signal+meanHeSignal))',meanMoleFrac(:,1),'fourier2'); + calibrationMS.ratioHeCO2 = fit((meanHeSignal./(meanCO2Signal+meanHeSignal))',meanMoleFrac(:,1),'linearinterp'); end % Save the calibration data into a .mat file @@ -209,14 +210,14 @@ function analyzeCalibration(parametersFlow,parametersMS) % Save the calibration data for further use save(['experimentalData',filesep,... 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... - 'gitCommitID'); + 'gitCommitID','parametersMS'); else % Create the calibration data folder if it does not exist mkdir(['experimentalData',filesep,'calibrationData']) % Save the calibration data for further use save(['experimentalData',filesep,... 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... - 'gitCommitID'); + 'gitCommitID','parametersMS'); end % Plot the raw and the calibrated data @@ -248,7 +249,7 @@ function analyzeCalibration(parametersFlow,parametersMS) else plot(meanHeSignal./(meanHeSignal+meanCO2Signal),meanMoleFrac(:,1),'or') % Experimental hold on - plot(0:0.01:1,calibrationMS.ratioHeCO2(0:0.01:1),'b') + plot(0:0.001:1,calibrationMS.ratioHeCO2(0:0.001:1),'b') xlim([0 1]); ylim([0 1]); box on; grid on; From 10a7d64d0b329ee8d23381c9264b3231b983f573 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 28 Apr 2021 10:08:14 +0100 Subject: [PATCH 083/189] Add reference for isotherm parameters to help the optimizer --- experimental/extractZLCParameters.py | 33 ++++++++++++++++++---------- experimental/simulateDeadVolume.py | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 37b8f74..fcd638b 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-04-28, AK: Add reference values for isotherm parameters # - 2021-04-27, AK: Initial creation # # Input arguments: @@ -59,12 +60,12 @@ def extractZLCParameters(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp15A_Output.mat', - 'ZLC_ActivatedCarbon_Exp15B_Output.mat', - 'ZLC_ActivatedCarbon_Exp15C_Output.mat', - 'ZLC_ActivatedCarbon_Exp15D_Output.mat', - 'ZLC_ActivatedCarbon_Exp15E_Output.mat', - 'ZLC_ActivatedCarbon_Exp15F_Output.mat'] + fileName = ['ZLC_ActivatedCarbon_Exp17A_Output.mat', + 'ZLC_ActivatedCarbon_Exp17B_Output.mat', + 'ZLC_ActivatedCarbon_Exp17C_Output.mat', + 'ZLC_ActivatedCarbon_Exp17D_Output.mat', + 'ZLC_ActivatedCarbon_Exp17E_Output.mat', + 'ZLC_ActivatedCarbon_Exp17F_Output.mat'] # NOTE: Dead volume characteristics filename is hardcoded in # simulateCombinedModel. This is because of the python GA function unable @@ -82,10 +83,10 @@ def extractZLCParameters(): problemDimension = len(optType) # Dual-site Langmuir elif modelType == 'DSL': - optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,1], - [np.finfo(float).eps,50e3], [np.finfo(float).eps,10], - [np.finfo(float).eps,1], [np.finfo(float).eps,50e3], - [np.finfo(float).eps,100])) + optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1], + [np.finfo(float).eps,1], [np.finfo(float).eps,1], + [np.finfo(float).eps,1], [np.finfo(float).eps,1], + [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real','real','real']) problemDimension = len(optType) @@ -145,8 +146,16 @@ def ZLCObjectiveFunction(x): from extractDeadVolume import filesToProcess # File processing script from simulateCombinedModel import simulateCombinedModel + # Reference for the isotherm parameters + # For SSL isotherm + if len(x) == 5: + isoRef = [10, 1e-5, 50e3, 100] + # For DSL isotherm + elif len(x) == 7: + isoRef = [10, 1e-5, 50e3, 10, 1e-5, 50e3, 100] + # Prepare isotherm model (the first n-1 parameters are for the isotherm model) - isothermModel = x[0:-1] + isothermModel = np.multiply(x[0:-1],isoRef[0:-1]) # Load the names of the file to be used for estimating dead volume characteristics filePath = filesToProcess(False,[],[]) @@ -182,7 +191,7 @@ def ZLCObjectiveFunction(x): # Compute the dead volume response using the optimizer parameters _ , moleFracSim , _ = simulateCombinedModel(isothermModel = isothermModel, - rateConstant = x[-1], # Last element is rate constant + rateConstant = x[-1]*isoRef[-1], # Last element is rate constant timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 73c3dd8..5fbb504 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -131,7 +131,7 @@ def simulateDeadVolume(**kwargs): # Solve the system of equations outputSol = solve_ivp(solveTanksInSeries, timeInt, initialConditions, method='Radau', t_eval = t_eval, - rtol = 1e-6, args = inputParameters) + rtol = 1e-8, args = inputParameters) # Parse out the time timeSim = outputSol.t From 40c62c83511cd5005b182c2cc4224b43cf488900 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 28 Apr 2021 15:45:01 +0100 Subject: [PATCH 084/189] Cosmetic change on parsing flow data --- experimental/runZLC.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 4efe129..8051d88 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -203,7 +203,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) userInput = input(promptUser,'s'); end % If mixtures is run, at the first instant turn off CO2 (MFC2) - if timerObj.tasksExecuted == 1 && expInfo.runMixtures && ~isempty(serialObj.MFC2.portName) + if expInfo.runMixtures && ~isempty(serialObj.MFC2.portName) % Parse out gas name from expInfo gasName_MFC2 = expInfo.gasName_MFC2; % Generate Gas ID for Alicat devices From 750e9a8368fc525cf5232d7401cdf0626c1f3061 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:55:41 +0100 Subject: [PATCH 085/189] Add flow rate sweep --- experimental/calibrateMS.m | 25 ++++++++++-------- experimental/runMultipleMSCalibration.m | 34 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 experimental/runMultipleMSCalibration.m diff --git a/experimental/calibrateMS.m b/experimental/calibrateMS.m index 1ec36c7..86d23aa 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrateMS.m @@ -12,6 +12,7 @@ % Calibrates the mass specfor different set point values % % Last modified: +% - 2021-04-30, AK: Add flow rate sweep % - 2021-04-15, AK: Modify function for mixture experiments % - 2021-03-16, AK: Initial creation % @@ -20,10 +21,7 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function calibrateMS - % Experiment name - expInfo.expName = ['ZLCCalibrateMS','_',... - datestr(datetime('now'),'yyyymmdd')]; +function calibrateMS(varargin) % Sampling time for the device expInfo.samplingTime = 2; % Define gas for MFM @@ -32,14 +30,21 @@ expInfo.gasName_MFC1 = 'He'; % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; - % Define set point for MFC1 - % Round the flow rate to the nearest first decimal (as this is the - % resolution of the meter) - MFC1_SP = round(repmat([0.0 0.2 1.5 3.0 4.5, 6.0 7.5, 9.0 10.5 12.0 13.5 14.8 15.0],[1,2]),1); % Define set point for MFC2 % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + if ~varargin{1} + MFC2_SP = round(repmat([0.0 0.2 0.4 3.0 6.0 9.0 10.5 12.0 15 29.6 29.8 30.0],[1,1]),1); + else + MFC2_SP = varargin{1}; + end + % Define set point for MFC1 + % Round the flow rate to the nearest first decimal (as this is the % resolution of the meter) - MFC2_SP = round(15.0-MFC1_SP,1); + MFC1_SP = round(max(MFC2_SP)-MFC2_SP,1); + % Experiment name + expInfo.expName = ['ZLCCalibrateMS','_',... + datestr(datetime('now'),'yyyymmdd'),'_',num2str(max(MFC2_SP)),'ccm']; % Start delay expInfo.equilibrationTime = 5; % [s] % Flag for meter calibration @@ -58,7 +63,7 @@ expInfo.maxTime = 600; else % Else use 5 min - expInfo.maxTime = 300; + expInfo.maxTime = 600; end % Run the setup for different calibrations runZLC(expInfo) diff --git a/experimental/runMultipleMSCalibration.m b/experimental/runMultipleMSCalibration.m new file mode 100644 index 0000000..128473b --- /dev/null +++ b/experimental/runMultipleMSCalibration.m @@ -0,0 +1,34 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Runs multiple MS calibration with different total flow rates +% +% Last modified: +% - 2021-04-30, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% Define the total flow rate of interest +totalFlowRate = [5, 7, 9 , 15, 30, 45, 60, 90]; + +% Loop through all total flow rates +for ii=1:length(totalFlowRate) + % Define the set point for MFC2 (CO2) + % This is done on a log scale to have a high reoslution at low CO2 + % concentrations + MFC2 = round(logspace(log10(0.2),log10(totalFlowRate(ii)),20),1); + % Call calibrateMS function + calibrateMS(MFC2) +end \ No newline at end of file From cb5686f72935660a145a32b62fbe1e83a92fc7ef Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 3 May 2021 09:54:29 +0100 Subject: [PATCH 086/189] Fix path issues for dead volume characterization --- experimental/extractDeadVolume.py | 15 +++++++++------ experimental/simulateCombinedModel.py | 2 +- experimental/simulateDeadVolume.py | 12 +++--------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 977fc0c..161086f 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -57,15 +57,18 @@ def extractDeadVolume(): # Get the current date and time for saving purposes currentDT = auxiliaryFunctions.getCurrentDateTime() + # Change path directory + # Assumes either running from ERASE or from experimental. Either ways + # this has to be run from experimental + if not os.getcwd().split(os.path.sep)[-1] == 'experimental': + os.chdir("experimental") + # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp13A_Output.mat', - 'ZLC_DeadVolume_Exp13B_Output.mat', - 'ZLC_DeadVolume_Exp13C_Output.mat', - 'ZLC_DeadVolume_Exp13D_Output.mat', - 'ZLC_DeadVolume_Exp13E_Output.mat', - 'ZLC_DeadVolume_Exp13F_Output.mat'] + fileName = ['ZLC_DeadVolume_Exp15A_Output.mat', + 'ZLC_DeadVolume_Exp15B_Output.mat', + 'ZLC_DeadVolume_Exp15C_Output.mat'] # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName) diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index 9864157..937af40 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -80,7 +80,7 @@ def simulateCombinedModel(**kwargs): if 'deadVolumeFile' in kwargs: deadVolumeFile = kwargs["deadVolumeFile"] else: - deadVolumeFile = 'deadVolumeCharacteristics_20210421_1558_49eb234.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210502_2023_10a7d64.npz' # Flag to check if experimental data used if 'expFlag' in kwargs: diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 5fbb504..a551000 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -16,6 +16,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-05-03, AK: Fix path issues # - 2021-04-26, AK: Change default model parameter values # - 2021-04-21, AK: Change model to fix split velocity # - 2021-04-20, AK: Change model to flow dependent split @@ -38,21 +39,14 @@ def simulateDeadVolume(**kwargs): import numpy as np from scipy.integrate import solve_ivp - import auxiliaryFunctions import os # Move to top level folder (to avoid path issues) os.chdir("..") - + # 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() - + # Flow rate of the gas [cc/s] if 'flowRate' in kwargs: flowRate = kwargs["flowRate"] From 59ccd96be7a5525c6663e3cf78727d9bae8a919b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 4 May 2021 11:37:30 +0100 Subject: [PATCH 087/189] Change dead volume error computation for parameter estimation --- experimental/extractDeadVolume.py | 49 +++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 161086f..2e4f148 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-05-04, AK: Modify error computation for dead volume # - 2021-04-27, AK: Cosmetic changes to structure # - 2021-04-21, AK: Change model to fix split velocity # - 2021-04-20, AK: Change model to flow dependent split @@ -146,13 +147,19 @@ def deadVolObjectiveFunction(x): downsampleInt = numPointsExp/np.min(numPointsExp) # Initialize error for objective function - computedError = 0 + computedError = 0 # Total error + computedErrorHigh = 0 # High composition error + computedErrorLow = 0 # Low composition error numPoints = 0 expVolume = 0 # Loop over all available files for ii in range(len(filePath)): # Initialize outputs moleFracSim = [] + moleFracHighExp = [] + moleFracHighSim = [] + moleFracLowExp = [] + moleFracLowExp = [] # Load experimental time, molefraction and flowrate (accounting for downsampling) timeElapsedExpTemp = load(filePath[ii])["timeElapsed"].flatten() moleFracExpTemp = load(filePath[ii])["moleFrac"].flatten() @@ -177,10 +184,40 @@ def deadVolObjectiveFunction(x): flowRate = flowRateExp, expFlag = True) - # Compute the sum of the error for the difference between exp. and sim. - numPoints += len(moleFracExp) - computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) - + # Objective function error + # Find error for mole fraction below a given threshold + thresholdFactor = 1e-2 + lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) + # Do downsampling if the number of points in higher and lower + # compositions does not match + numPointsConc = np.zeros([2]) + numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition + numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition + downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals + + # Compute error for higher concentrations (accounting for downsampling) + moleFracHighExp = moleFracExp[0:lastIndThreshold] + moleFracHighSim = moleFracSim[0:lastIndThreshold] + computedErrorHigh += np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] + - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) + + # Find scaling factor for lower concentrations + scalingFactor = int(1/thresholdFactor) # Assumes max composition is one + # Compute error for lower concentrations + moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor + moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor + + # Compute error for low concentrations (accounting for downsampling) + computedErrorLow += np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] + - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) + + # Find the overall computed error + computedError += computedErrorHigh + computedErrorLow + + # Compute the number of points per experiment (accouting for down- + # sampling in both experiments and high and low compositions + numPoints += len(moleFracHighExp) + len(moleFracLowExp) + # Penalize if the total volume of the system is greater than experiemntal # volume penaltyObj = 0 @@ -188,7 +225,7 @@ def deadVolObjectiveFunction(x): penaltyObj = 10000 # Compute the sum of the error for the difference between exp. and sim. and # add a penalty if needed - return (numPoints/2)*computedError + penaltyObj + return (numPoints/2)*(computedError) + penaltyObj # func: filesToProcess # Loads .mat experimental file and processes it for python From 76a69ffa972ae4f9eba804ca5385c5f9dfa10d78 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 4 May 2021 11:37:57 +0100 Subject: [PATCH 088/189] Plots for ZLC experiments --- plotFunctions/plotExperimentOutcome.py | 228 +++++++++++++++++++++---- 1 file changed, 195 insertions(+), 33 deletions(-) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 6dd583d..d45ce31 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-05-04, AK: Implement plots for ZLC and change DV error computaiton # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-16, AK: Initial creation # @@ -50,31 +51,48 @@ simulateModel = True # Flag to plot dead volume results -plotFt = False +plotFt = True # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' colorsForPlot = ["#E5383B","#E5383B","#B55055","#B55055","#6C757D","#6C757D"] +colorsForPlot = ["#E5383B","#CD4448","#B55055", + "#9C5D63","#846970","#6C757D"] +# colorsForPlot = ["#E5383B","#6C757D"] markerForPlot = ["o","v","o","v","o","v"] if flagDeadVolume: # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp13A_Output_92e47e7.npz', - 'ZLC_DeadVolume_Exp13B_Output_92e47e7.npz', - 'ZLC_DeadVolume_Exp13C_Output_92e47e7.npz', - 'ZLC_DeadVolume_Exp13D_Output_92e47e7.npz', - 'ZLC_DeadVolume_Exp13E_Output_92e47e7.npz', - 'ZLC_DeadVolume_Exp13F_Output_92e47e7.npz'] + fileName = ['ZLC_DeadVolume_Exp15A_Output_10a7d64.npz', + 'ZLC_DeadVolume_Exp15B_Output_10a7d64.npz', + 'ZLC_DeadVolume_Exp15C_Output_10a7d64.npz'] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210421_1125_1b02ca7.npz' + fileParameter = 'deadVolumeCharacteristics_20210503_0956_cb5686f.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = mainDir + fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) # Print the objective function and volume from model parameters print("Objective Function",round(modelOutputTemp[()]["function"],0)) print("Model Volume",round(sum(x[0:2]),2)) + computedErrorHigh = 0 + computedErrorLow = 0 + numPoints = 0 + # Create the instance for the plots + fig = plt.figure + ax1 = plt.subplot(1,3,1) + ax2 = plt.subplot(1,3,2) # Initialize error for objective function # Loop over all available files for ii in range(len(fileName)): @@ -82,17 +100,20 @@ moleFracSim = [] # Path of the file name fileToLoad = mainDir + fileName[ii] - # Load experimental timeelapsed, molefraction and flowRate - timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() - moleFracExp = load(fileToLoad)["moleFrac"].flatten() - flowRate = load(fileToLoad)["flowRate"].flatten() + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] # Integration and ode evaluation time timeInt = timeElapsedExp # Print experimental volume print("Experiment",str(ii+1),round(np.trapz(moleFracExp, - np.multiply(flowRate,timeElapsedExp)),2)) + np.multiply(flowRateExp,timeElapsedExp)),2)) if simulateModel: # Compute the dead volume response using the optimizer parameters _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = x[0], @@ -101,21 +122,51 @@ numTanks_1 = int(x[3]), flowRate_D = x[4], timeInt = timeInt, - flowRate = flowRate) + flowRate = flowRateExp, + expFlag = True) - # Print simulation volume + # Print simulation volume print("Simulation",str(ii+1),round(np.trapz(moleFracSim, - np.multiply(flowRate, + np.multiply(flowRateExp, timeElapsedExp)),2)) - + + # Objective function error + # Find error for mole fraction below a given threshold + thresholdFactor = 1e-2 + lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) + # Do downsampling if the number of points in higher and lower + # compositions does not match + numPointsConc = np.zeros([2]) + numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) + numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) + downsampleConc = numPointsConc/np.min(numPointsConc) + + # Compute error for higher concentrations + moleFracHighExp = moleFracExp[0:lastIndThreshold] + moleFracHighSim = moleFracSim[0:lastIndThreshold] + computedErrorHigh += np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] + - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) + + # Find scaling factor for lower concentrations + scalingFactor = int(1/thresholdFactor) # Assumes max composition is one + # Compute error for lower concentrations + moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor + moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor + + # Compute error for low concentrations (also scaling the compositions) + computedErrorLow += np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] + - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) + + # Compute the number of points per experiment (accouting for down- + # sampling in both experiments and high and low compositions + numPoints += len(moleFracHighExp) + len(moleFracLowExp) + # Plot the expreimental and model output if not plotFt: # Linear scale - fig = plt.figure - ax1 = plt.subplot(1,2,1) ax1.plot(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax1.plot(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -125,16 +176,14 @@ ax1.legend() # Log scale - ax2 = plt.subplot(1,2,2) ax2.semilogy(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', xlim = [0,400], ylim = [1e-3, 1]) - ax2.legend() # Save the figure if saveFlag: @@ -147,13 +196,11 @@ plt.savefig (savePath) else: # Linear scale - fig = plt.figure - ax1 = plt.subplot(1,2,1) - ax1.plot(np.multiply(flowRate,timeElapsedExp),moleFracExp, + ax1.plot(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: - ax1.plot(np.multiply(flowRate,timeElapsedExp),moleFracSim, + ax1.plot(np.multiply(flowRateExp,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$Ft$ [cc]', ylabel='$y_1$ [-]', @@ -161,12 +208,11 @@ ax1.legend() # Log scale - ax2 = plt.subplot(1,2,2) - ax2.semilogy(np.multiply(flowRate,timeElapsedExp),moleFracExp, + ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRate),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: - ax2.semilogy(np.multiply(flowRate,timeElapsedExp),moleFracSim, + ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$Ft$ [cc]', xlim = [0,100], ylim = [1e-3, 1]) @@ -180,4 +226,120 @@ # 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) \ No newline at end of file + plt.savefig (savePath) + + print(numPoints*(computedErrorHigh+computedErrorLow)/2) + +else: + from simulateCombinedModel import simulateCombinedModel + + # Directory of raw data + mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' + # File name of the experiments + fileName = ['ZLC_ActivatedCarbon_Exp17A_Output_8e60357.npz', + 'ZLC_ActivatedCarbon_Exp17B_Output_8e60357.npz'] + + # File with parameter estimates + simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' + fileParameter = 'zlcParameters_20210428_1011_10a7d64.npz' # 17A-F + fileParameter = 'zlcParameters_20210503_1156_cb5686f.npz' # 17A-B + modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] + print("Objective Function",round(modelOutputTemp[()]["function"],0)) + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x = np.multiply(modelNonDim,[10, 1e-5, 50e3, 10, 1e-5, 50e3, 100]) + + # Ronny AC Data + # x = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3,100] + + computedError = 0 + numPoints = 0 + # Create the instance for the plots + fig = plt.figure + ax1 = plt.subplot(1,3,1) + ax2 = plt.subplot(1,3,2) + ax3 = plt.subplot(1,3,3) + + # Loop over all available files + for ii in range(len(fileName)): + fileToLoad = mainDir + fileName[ii] + + # Initialize outputs + moleFracSim = [] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExp = load(fileToLoad)["moleFrac"].flatten() + flowRateExp = load(fileToLoad)["flowRate"].flatten() + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Print experimental volume + print("Experiment",str(ii+1),round(np.trapz(np.multiply(flowRateExp,moleFracExp),timeElapsedExp),2)) + + if simulateModel: + # Compute the dead volume response using the optimizer parameters + _ , moleFracSim , resultMat = simulateCombinedModel(timeInt = timeInt, + initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point + flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = True, + isothermModel=x[0:-1], + rateConstant=x[-1]) + # Print simulation volume + print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, + resultMat[0,:]), + timeElapsedExp),2)) + + # Objective function error + computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) + numPoints += len(moleFracExp) + + # y - Linear scale + ax1.semilogy(timeElapsedExp,moleFracExp, + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.05) # Experimental response + if simulateModel: + ax1.plot(timeElapsedExp,moleFracSim, + color=colorsForPlot[ii],label=str(round(np.mean(flowRateExp),2))+" ccs") # Simulation response + ax1.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = [0,300], ylim = [1e-3, 1]) + ax1.locator_params(axis="x", nbins=4) + ax1.legend() + + # Ft - Log scale + ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.05) # Experimental response + if simulateModel: + ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracSim, + color=colorsForPlot[ii],label=str(round(np.mean(flowRateExp),2))+" ccs") # Simulation response + ax2.set(xlabel='$Ft$ [cc]', + xlim = [0,100], ylim = [1e-3, 1]) + ax2.locator_params(axis="x", nbins=4) + + # Flow rates + ax3.plot(timeElapsedExp,flowRateExp, + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.05,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + if simulateModel: + ax3.plot(timeElapsedExp,resultMat[3,:]*1e6, + color=colorsForPlot[ii]) # Simulation response + ax3.set(xlabel='$t$ [s]', + ylabel='$F$ [ccs]', + xlim = [0,300], ylim = [0, 0.5]) + ax3.locator_params(axis="x", nbins=4) + ax3.locator_params(axis="y", nbins=4) + + # Save the figure + if saveFlag: + # FileName: zlcCharacteristics___ + saveFileName = "zlcCharacteristics_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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() + print(numPoints*computedError/2) \ No newline at end of file From 68b8ac70492c0d533f066791406cc14f092dca53 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 5 May 2021 09:19:03 +0100 Subject: [PATCH 089/189] Bug fix for error computation --- experimental/extractDeadVolume.py | 11 +++++------ experimental/simulateCombinedModel.py | 2 +- plotFunctions/plotExperimentOutcome.py | 22 +++++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 2e4f148..be7ede1 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-05-04, AK: Bug fix for error computation # - 2021-05-04, AK: Modify error computation for dead volume # - 2021-04-27, AK: Cosmetic changes to structure # - 2021-04-21, AK: Change model to fix split velocity @@ -148,8 +149,6 @@ def deadVolObjectiveFunction(x): # Initialize error for objective function computedError = 0 # Total error - computedErrorHigh = 0 # High composition error - computedErrorLow = 0 # Low composition error numPoints = 0 expVolume = 0 # Loop over all available files @@ -198,7 +197,7 @@ def deadVolObjectiveFunction(x): # Compute error for higher concentrations (accounting for downsampling) moleFracHighExp = moleFracExp[0:lastIndThreshold] moleFracHighSim = moleFracSim[0:lastIndThreshold] - computedErrorHigh += np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] + computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) # Find scaling factor for lower concentrations @@ -208,10 +207,10 @@ def deadVolObjectiveFunction(x): moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor # Compute error for low concentrations (accounting for downsampling) - computedErrorLow += np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] + computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) - # Find the overall computed error + # Find the sum of computed error computedError += computedErrorHigh + computedErrorLow # Compute the number of points per experiment (accouting for down- @@ -224,7 +223,7 @@ def deadVolObjectiveFunction(x): if sum(x[0:3])>1.5*expVolume: penaltyObj = 10000 # Compute the sum of the error for the difference between exp. and sim. and - # add a penalty if needed + # add a penalty if needed (using MLE) return (numPoints/2)*(computedError) + penaltyObj # func: filesToProcess diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index 937af40..c5b1c83 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -80,7 +80,7 @@ def simulateCombinedModel(**kwargs): if 'deadVolumeFile' in kwargs: deadVolumeFile = kwargs["deadVolumeFile"] else: - deadVolumeFile = 'deadVolumeCharacteristics_20210502_2023_10a7d64.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210504_1818_76a69ff.npz' # Flag to check if experimental data used if 'expFlag' in kwargs: diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index d45ce31..d6cec30 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-05-04, AK: Bug fix for error computation # - 2021-05-04, AK: Implement plots for ZLC and change DV error computaiton # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV # - 2021-04-16, AK: Initial creation @@ -51,7 +52,7 @@ simulateModel = True # Flag to plot dead volume results -plotFt = True +plotFt = False # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' @@ -69,7 +70,7 @@ 'ZLC_DeadVolume_Exp15C_Output_10a7d64.npz'] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210503_0956_cb5686f.npz' + fileParameter = 'deadVolumeCharacteristics_20210504_1818_76a69ff.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] @@ -86,8 +87,7 @@ # Print the objective function and volume from model parameters print("Objective Function",round(modelOutputTemp[()]["function"],0)) print("Model Volume",round(sum(x[0:2]),2)) - computedErrorHigh = 0 - computedErrorLow = 0 + computedError = 0 numPoints = 0 # Create the instance for the plots fig = plt.figure @@ -144,7 +144,7 @@ # Compute error for higher concentrations moleFracHighExp = moleFracExp[0:lastIndThreshold] moleFracHighSim = moleFracSim[0:lastIndThreshold] - computedErrorHigh += np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] + computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) # Find scaling factor for lower concentrations @@ -154,9 +154,12 @@ moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor # Compute error for low concentrations (also scaling the compositions) - computedErrorLow += np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] + computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) + # Find the sum of computed error + computedError += computedErrorHigh + computedErrorLow + # Compute the number of points per experiment (accouting for down- # sampling in both experiments and high and low compositions numPoints += len(moleFracHighExp) + len(moleFracLowExp) @@ -227,8 +230,9 @@ if not os.path.exists(os.path.join('..','simulationFigures')): os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) - - print(numPoints*(computedErrorHigh+computedErrorLow)/2) + + # Print the MLE error + print(np.round(numPoints*(computedError)/2,0)) else: from simulateCombinedModel import simulateCombinedModel @@ -250,7 +254,7 @@ x = np.multiply(modelNonDim,[10, 1e-5, 50e3, 10, 1e-5, 50e3, 100]) # Ronny AC Data - # x = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3,100] + x = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3,100] computedError = 0 numPoints = 0 From 31987ca90c7ae1d486b5087cd5bea83fd47d7d91 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 5 May 2021 15:54:33 +0100 Subject: [PATCH 090/189] Bug fix for MLE error computation --- experimental/computeMLEError.py | 65 ++++++++++++++++++++++++++ experimental/extractDeadVolume.py | 55 ++++++---------------- experimental/extractZLCParameters.py | 25 +++++----- plotFunctions/plotExperimentOutcome.py | 62 +++++++++--------------- 4 files changed, 113 insertions(+), 94 deletions(-) create mode 100644 experimental/computeMLEError.py diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py new file mode 100644 index 0000000..b9882c3 --- /dev/null +++ b/experimental/computeMLEError.py @@ -0,0 +1,65 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Computes the MLE error for ZLC experiments. The function splits the +# response curve to two distinct regions (high and low composition) to given +# equal weights. The cut off threshold for high and low composition is hard +# coded in this function. Note that the error is +# +# Last modified: +# - 2021-05-05, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def computeMLEError(moleFracExp,moleFracSim): + import numpy as np + + # Objective function error + # Find error for mole fraction below a given threshold + thresholdFactor = 1e-2 + lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) + # Do downsampling if the number of points in higher and lower + # compositions does not match + numPointsConc = np.zeros([2]) + numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition + numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition + downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals + + # Compute error for higher concentrations (accounting for downsampling) + moleFracHighExp = moleFracExp[0:lastIndThreshold] + moleFracHighSim = moleFracSim[0:lastIndThreshold] + computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] + - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) + + # Find scaling factor for lower concentrations + scalingFactor = int(1/thresholdFactor) # Assumes max composition is one + # Compute error for lower concentrations + moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor + moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor + + # Compute error for low concentrations (accounting for downsampling) + computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] + - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) + + # Find the sum of computed error + computedError = computedErrorHigh + computedErrorLow + + # Compute the number of points per experiment (accouting for down- + # sampling in both experiments and high and low compositions + numPoints = len(moleFracHighExp) + len(moleFracLowExp) + + return (numPoints/2)*(computedError) \ No newline at end of file diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index be7ede1..fe542d0 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,7 +17,8 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: -# - 2021-05-04, AK: Bug fix for error computation +# - 2021-05-05, AK: Bug fix for MLE error computation +# - 2021-05-05, AK: Bug fix for error computation # - 2021-05-04, AK: Modify error computation for dead volume # - 2021-04-27, AK: Cosmetic changes to structure # - 2021-04-21, AK: Change model to fix split velocity @@ -133,6 +134,7 @@ def extractDeadVolume(): def deadVolObjectiveFunction(x): import numpy as np from simulateDeadVolume import simulateDeadVolume + from computeMLEError import computeMLEError from numpy import load # Load the names of the file to be used for estimating dead volume characteristics @@ -149,16 +151,13 @@ def deadVolObjectiveFunction(x): # Initialize error for objective function computedError = 0 # Total error - numPoints = 0 + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) expVolume = 0 # Loop over all available files for ii in range(len(filePath)): # Initialize outputs - moleFracSim = [] - moleFracHighExp = [] - moleFracHighSim = [] - moleFracLowExp = [] - moleFracLowExp = [] + moleFracSim = [] # Load experimental time, molefraction and flowrate (accounting for downsampling) timeElapsedExpTemp = load(filePath[ii])["timeElapsed"].flatten() moleFracExpTemp = load(filePath[ii])["moleFrac"].flatten() @@ -182,40 +181,11 @@ def deadVolObjectiveFunction(x): timeInt = timeInt, flowRate = flowRateExp, expFlag = True) - - # Objective function error - # Find error for mole fraction below a given threshold - thresholdFactor = 1e-2 - lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) - # Do downsampling if the number of points in higher and lower - # compositions does not match - numPointsConc = np.zeros([2]) - numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition - numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition - downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals - - # Compute error for higher concentrations (accounting for downsampling) - moleFracHighExp = moleFracExp[0:lastIndThreshold] - moleFracHighSim = moleFracSim[0:lastIndThreshold] - computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] - - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) - - # Find scaling factor for lower concentrations - scalingFactor = int(1/thresholdFactor) # Assumes max composition is one - # Compute error for lower concentrations - moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor - moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor - - # Compute error for low concentrations (accounting for downsampling) - computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] - - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) - - # Find the sum of computed error - computedError += computedErrorHigh + computedErrorLow - - # Compute the number of points per experiment (accouting for down- - # sampling in both experiments and high and low compositions - numPoints += len(moleFracHighExp) + len(moleFracLowExp) + + # Stack mole fraction from experiments and simulation for error + # computation + moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) + moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) # Penalize if the total volume of the system is greater than experiemntal # volume @@ -224,7 +194,8 @@ def deadVolObjectiveFunction(x): penaltyObj = 10000 # Compute the sum of the error for the difference between exp. and sim. and # add a penalty if needed (using MLE) - return (numPoints/2)*(computedError) + penaltyObj + computedError = computeMLEError(moleFracExpALL,moleFracSimALL) + return computedError + penaltyObj # func: filesToProcess # Loads .mat experimental file and processes it for python diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index fcd638b..610943b 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,8 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-05-05, AK: Bug fix for MLE error computation +# - 2021-05-05, AK: Modify error computation for dead volume # - 2021-04-28, AK: Add reference values for isotherm parameters # - 2021-04-27, AK: Initial creation # @@ -61,11 +63,7 @@ def extractZLCParameters(): mainDir = 'runData' # File name of the experiments fileName = ['ZLC_ActivatedCarbon_Exp17A_Output.mat', - 'ZLC_ActivatedCarbon_Exp17B_Output.mat', - 'ZLC_ActivatedCarbon_Exp17C_Output.mat', - 'ZLC_ActivatedCarbon_Exp17D_Output.mat', - 'ZLC_ActivatedCarbon_Exp17E_Output.mat', - 'ZLC_ActivatedCarbon_Exp17F_Output.mat'] + 'ZLC_ActivatedCarbon_Exp17B_Output.mat'] # NOTE: Dead volume characteristics filename is hardcoded in # simulateCombinedModel. This is because of the python GA function unable @@ -145,6 +143,7 @@ def ZLCObjectiveFunction(x): from numpy import load from extractDeadVolume import filesToProcess # File processing script from simulateCombinedModel import simulateCombinedModel + from computeMLEError import computeMLEError # Reference for the isotherm parameters # For SSL isotherm @@ -172,7 +171,8 @@ def ZLCObjectiveFunction(x): # Initialize error for objective function computedError = 0 - numPoints = 0 + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) # Loop over all available files for ii in range(len(filePath)): @@ -197,10 +197,11 @@ def ZLCObjectiveFunction(x): flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) expFlag = True) - # Compute the sum of the error for the difference between exp. and sim. - numPoints += len(moleFracExp) - computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) + # Stack mole fraction from experiments and simulation for error + # computation + moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) + moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) - # Compute the sum of the error for the difference between exp. and sim. and - # add a penalty if needed - return (numPoints/2)*computedError \ No newline at end of file + # Compute the sum of the error for the difference between exp. and sim. + computedError = computeMLEError(moleFracExpALL,moleFracSimALL) + return computedError \ No newline at end of file diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index d6cec30..ddaacb5 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-05-05, AK: Bug fix for MLE error computation # - 2021-05-04, AK: Bug fix for error computation # - 2021-05-04, AK: Implement plots for ZLC and change DV error computaiton # - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV @@ -27,6 +28,7 @@ import numpy as np from simulateDeadVolume import simulateDeadVolume +from computeMLEError import computeMLEError from numpy import load import os import matplotlib.pyplot as plt @@ -70,7 +72,7 @@ 'ZLC_DeadVolume_Exp15C_Output_10a7d64.npz'] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210504_1818_76a69ff.npz' + fileParameter = 'deadVolumeCharacteristics_20210505_0936_68b8ac7.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] @@ -89,6 +91,9 @@ print("Model Volume",round(sum(x[0:2]),2)) computedError = 0 numPoints = 0 + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + # Create the instance for the plots fig = plt.figure ax1 = plt.subplot(1,3,1) @@ -129,40 +134,10 @@ print("Simulation",str(ii+1),round(np.trapz(moleFracSim, np.multiply(flowRateExp, timeElapsedExp)),2)) - - # Objective function error - # Find error for mole fraction below a given threshold - thresholdFactor = 1e-2 - lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) - # Do downsampling if the number of points in higher and lower - # compositions does not match - numPointsConc = np.zeros([2]) - numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) - numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) - downsampleConc = numPointsConc/np.min(numPointsConc) - - # Compute error for higher concentrations - moleFracHighExp = moleFracExp[0:lastIndThreshold] - moleFracHighSim = moleFracSim[0:lastIndThreshold] - computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] - - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) - - # Find scaling factor for lower concentrations - scalingFactor = int(1/thresholdFactor) # Assumes max composition is one - # Compute error for lower concentrations - moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor - moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor - - # Compute error for low concentrations (also scaling the compositions) - computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] - - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) - - # Find the sum of computed error - computedError += computedErrorHigh + computedErrorLow - # Compute the number of points per experiment (accouting for down- - # sampling in both experiments and high and low compositions - numPoints += len(moleFracHighExp) + len(moleFracLowExp) + # Stack mole fraction from experiments and simulation + moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) + moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) # Plot the expreimental and model output if not plotFt: @@ -232,7 +207,8 @@ plt.savefig (savePath) # Print the MLE error - print(np.round(numPoints*(computedError)/2,0)) + computedError = computeMLEError(moleFracExpALL,moleFracSimALL) + print(round(computedError,1)) else: from simulateCombinedModel import simulateCombinedModel @@ -258,6 +234,9 @@ computedError = 0 numPoints = 0 + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + # Create the instance for the plots fig = plt.figure ax1 = plt.subplot(1,3,1) @@ -293,11 +272,11 @@ print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, resultMat[0,:]), timeElapsedExp),2)) - - # Objective function error - computedError += np.log(np.sum(np.power(moleFracExp - moleFracSim,2))) - numPoints += len(moleFracExp) + # Stack mole fraction from experiments and simulation + moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) + moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) + # y - Linear scale ax1.semilogy(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, @@ -346,4 +325,7 @@ plt.savefig (savePath) plt.show() - print(numPoints*computedError/2) \ No newline at end of file + + # Print the MLE error + computedError = computeMLEError(moleFracExpALL,moleFracSimALL) + print(round(computedError,1)) \ No newline at end of file From b09dc47c130eedd8c7039db99eb20bb01d874b59 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 6 May 2021 13:04:51 +0100 Subject: [PATCH 091/189] Fix for dead volume --- experimental/simulateCombinedModel.py | 2 +- experimental/simulateDeadVolume.py | 10 +++---- plotFunctions/plotExperimentOutcome.py | 36 ++++++++++++++++++-------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index c5b1c83..0a1b1db 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -80,7 +80,7 @@ def simulateCombinedModel(**kwargs): if 'deadVolumeFile' in kwargs: deadVolumeFile = kwargs["deadVolumeFile"] else: - deadVolumeFile = 'deadVolumeCharacteristics_20210504_1818_76a69ff.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210505_1849_31987ca.npz' # Flag to check if experimental data used if 'expFlag' in kwargs: diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index a551000..61ee66e 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -56,27 +56,27 @@ def simulateDeadVolume(**kwargs): if 'deadVolume_1' in kwargs: deadVolume_1 = kwargs["deadVolume_1"] else: - deadVolume_1 = 3.67 + deadVolume_1 = 4.25 # Number of tanks of the first volume [-] if 'numTanks_1' in kwargs: numTanks_1 = kwargs["numTanks_1"] else: - numTanks_1 = 16 + numTanks_1 = 30 # Dead Volume of the second volume (mixing) [cc] if 'deadVolume_2M' in kwargs: deadVolume_2M = kwargs["deadVolume_2M"] else: - deadVolume_2M = 1.10 + deadVolume_2M = 1.59 # Dead Volume of the second volume (diffusive) [cc] if 'deadVolume_2D' in kwargs: deadVolume_2D = kwargs["deadVolume_2D"] else: - deadVolume_2D = 0.51 + deadVolume_2D = 5.93e-1 # Flow rate in the diffusive volume [-] if 'flowRate_D' in kwargs: flowRate_D = kwargs["flowRate_D"] else: - flowRate_D = 0.009 + flowRate_D = 1.35e-2 # Initial Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index ddaacb5..64cca53 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -54,7 +54,7 @@ simulateModel = True # Flag to plot dead volume results -plotFt = False +plotFt = True # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' @@ -72,7 +72,7 @@ 'ZLC_DeadVolume_Exp15C_Output_10a7d64.npz'] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210505_0936_68b8ac7.npz' + fileParameter = 'deadVolumeCharacteristics_20210505_1849_31987ca.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] @@ -93,7 +93,7 @@ numPoints = 0 moleFracExpALL = np.array([]) moleFracSimALL = np.array([]) - + # Create the instance for the plots fig = plt.figure ax1 = plt.subplot(1,3,1) @@ -208,7 +208,7 @@ # Print the MLE error computedError = computeMLEError(moleFracExpALL,moleFracSimALL) - print(round(computedError,1)) + print(round(computedError,0)) else: from simulateCombinedModel import simulateCombinedModel @@ -222,13 +222,24 @@ # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' fileParameter = 'zlcParameters_20210428_1011_10a7d64.npz' # 17A-F - fileParameter = 'zlcParameters_20210503_1156_cb5686f.npz' # 17A-B + fileParameter = 'zlcParameters_20210505_1735_31987ca.npz' # 17A-B modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] print("Objective Function",round(modelOutputTemp[()]["function"],0)) modelNonDim = modelOutputTemp[()]["variable"] + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = mainDir + fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + # Multiply the paremeters by the reference values x = np.multiply(modelNonDim,[10, 1e-5, 50e3, 10, 1e-5, 50e3, 100]) - + # Ronny AC Data x = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3,100] @@ -250,9 +261,12 @@ # Initialize outputs moleFracSim = [] # Load experimental time, molefraction and flowrate (accounting for downsampling) - timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() - moleFracExp = load(fileToLoad)["moleFrac"].flatten() - flowRateExp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) timeInt = timeElapsedExp @@ -270,7 +284,7 @@ rateConstant=x[-1]) # Print simulation volume print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, - resultMat[0,:]), + moleFracSim), timeElapsedExp),2)) # Stack mole fraction from experiments and simulation @@ -328,4 +342,4 @@ # Print the MLE error computedError = computeMLEError(moleFracExpALL,moleFracSimALL) - print(round(computedError,1)) \ No newline at end of file + print(round(computedError,0)) \ No newline at end of file From 0ade35a50525229dc1d168ec83d2295bedc8a014 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 6 May 2021 13:08:17 +0100 Subject: [PATCH 092/189] Fix extractZLCParameters --- experimental/extractZLCParameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 610943b..29299f8 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -75,8 +75,8 @@ def extractZLCParameters(): # Define the bounds and the type of the parameters to be optimized # Single-site Langmuir if modelType == 'SSL': - optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,1], - [np.finfo(float).eps,50e3], [np.finfo(float).eps,100])) + optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1], + [np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real']) problemDimension = len(optType) # Dual-site Langmuir From 985c175e967a2bf6137fdf9a6eb799cd8350d8b6 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 6 May 2021 14:31:02 +0100 Subject: [PATCH 093/189] Bug fix --- experimental/extractZLCParameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 29299f8..e625c71 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -147,7 +147,7 @@ def ZLCObjectiveFunction(x): # Reference for the isotherm parameters # For SSL isotherm - if len(x) == 5: + if len(x) == 4: isoRef = [10, 1e-5, 50e3, 100] # For DSL isotherm elif len(x) == 7: From ebb447ee03f6328e7244810e223fabb5b1016c51 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 7 May 2021 09:57:37 +0100 Subject: [PATCH 094/189] Bug fix in SSL model --- experimental/computeEquilibriumLoading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/computeEquilibriumLoading.py b/experimental/computeEquilibriumLoading.py index e144f53..209c0b3 100644 --- a/experimental/computeEquilibriumLoading.py +++ b/experimental/computeEquilibriumLoading.py @@ -88,7 +88,7 @@ def simulateSSL(*inputParameters): localConc = pressureTotal*moleFrac/(Rg*temperature) # Compute the adsorption affinity - isoAffinity = isothermModel[1]*np.exp(-isothermModel[2]/(Rg*temperature)) + isoAffinity = isothermModel[1]*np.exp(isothermModel[2]/(Rg*temperature)) # Compute the numerator and denominator of a pure single site Langmuir isoNumerator = isothermModel[0]*isoAffinity*localConc From 31ee61d88799ae4deffc595e6ea6e7dc44bac367 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 10 May 2021 14:18:44 +0100 Subject: [PATCH 095/189] Update data analysis to take into account averaging of multiple calibrations of MS --- experimental/analyzeCalibration.m | 59 ++-------- experimental/analyzeExperiment.m | 149 +++++++++++------------- experimental/analyzeExperimentWrapper.m | 92 +++++++++++++++ experimental/concatenateData.m | 29 +++-- 4 files changed, 187 insertions(+), 142 deletions(-) create mode 100644 experimental/analyzeExperimentWrapper.m diff --git a/experimental/analyzeCalibration.m b/experimental/analyzeCalibration.m index 24af9eb..4b33f3f 100644 --- a/experimental/analyzeCalibration.m +++ b/experimental/analyzeCalibration.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-05-10, AK: Remove single gas calibration % - 2021-04-27, AK: Change the calibration model to linear interpolation % - 2021-04-23, AK: Change the calibration model to Fourier series based % - 2021-04-21, AK: Change the calibration equation to mole fraction like @@ -189,19 +190,9 @@ function analyzeCalibration(parametersFlow,parametersMS) meanMoleFrac(((kk-1)*length(setPtMFC)+ii),2) = mean(reconciledData.moleFrac(indMean,2)); % CO2 end end - - % Fit a polynomial function to get the model for MS - % Fitting a 3rd order polynomial (check before accepting this) - calibrationMS.flagUseIndGas = parametersMS.flagUseIndGas; - if parametersMS.flagUseIndGas - calibrationMS.He = polyfit(meanHeSignal,meanMoleFrac(:,1),parametersMS.polyDeg); % He - calibrationMS.CO2 = polyfit(meanCO2Signal,meanMoleFrac(:,2),parametersMS.polyDeg); % COo2 - else - % Perform an optimization to obtain parameter estimates to fit the - % signal fraction of the helium signal to He+CO2 signal - % Use a fourier series to fit the calibration data - calibrationMS.ratioHeCO2 = fit((meanHeSignal./(meanCO2Signal+meanHeSignal))',meanMoleFrac(:,1),'linearinterp'); - end + % Use a linear interpolation to fit the calibration data of the signal + % ratio w.r.t He composition + calibrationMS.ratioHeCO2 = fit((meanHeSignal./(meanCO2Signal+meanHeSignal))',meanMoleFrac(:,1),'linearinterp'); % Save the calibration data into a .mat file % Check if calibration data folder exists @@ -222,39 +213,13 @@ function analyzeCalibration(parametersFlow,parametersMS) % Plot the raw and the calibrated data figure(1) - % Plot for independent gas calibrations - if parametersMS.flagUseIndGas - % He - subplot(1,2,1) - hold on - plot(1e-13:1e-13:1e-8,polyval(calibrationMS.He,1e-13:1e-13:1e-8)) - scatter(meanHeSignal,meanMoleFrac(:,1)) - xlim([0 1.1*max(meanHeSignal)]); - ylim([0 1]); - box on; grid on; - xlabel('Helium Signal [A]') - ylabel('Helium mole frac [-]') - - % CO2 - subplot(1,2,2) - hold on - plot(1e-13:1e-13:1e-8,polyval(calibrationMS.CO2,1e-13:1e-13:1e-8),'b') - plot(meanCO2Signal,meanMoleFrac(:,2),'or') - xlim([0 1.1*max(meanCO2Signal)]); - ylim([0 1]); - box on; grid on; - xlabel('CO2 Signal [A]') - ylabel('CO2 mole frac [-]') - % Ratio of He to CO2 - else - plot(meanHeSignal./(meanHeSignal+meanCO2Signal),meanMoleFrac(:,1),'or') % Experimental - hold on - plot(0:0.001:1,calibrationMS.ratioHeCO2(0:0.001:1),'b') - xlim([0 1]); - ylim([0 1]); - box on; grid on; - xlabel('Helium Signal/(CO2 Signal+Helium Signal) [-]') - ylabel('Helium mole frac [-]') - end + plot(meanHeSignal./(meanHeSignal+meanCO2Signal),meanMoleFrac(:,1),'or') % Experimental + hold on + plot(0:0.001:1,calibrationMS.ratioHeCO2(0:0.001:1),'b') + xlim([0 1]); + ylim([0 1]); + box on; grid on; + xlabel('Helium Signal/(CO2 Signal+Helium Signal) [-]') + ylabel('Helium mole frac [-]') end end \ No newline at end of file diff --git a/experimental/analyzeExperiment.m b/experimental/analyzeExperiment.m index 6f7df7a..79d41f5 100644 --- a/experimental/analyzeExperiment.m +++ b/experimental/analyzeExperiment.m @@ -14,6 +14,7 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-05-10, AK: Convert into a function % - 2021-04-20, AK: Add experiment struct to output .mat file % - 2021-04-19, AK: Major revamp for flow rate computation % - 2021-04-13, AK: Add threshold to cut data below a given mole fraction @@ -28,93 +29,75 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function analyzeExperiment(experimentStruct,flagCalibration,flagFlowMeter) + % Get the git commit ID + gitCommitID = getGitCommit; -% Get the git commit ID -gitCommitID = getGitCommit; + % Mode to switch between calibration and analyzing real experiment + % Analyze calibration data + if flagCalibration + % Calibrate flow meter + if flagFlowMeter + % File with the calibration data to build a model for MFC/MFM + experimentStruct = 'ZLCCalibrateMeters_20210419'; % Experimental flow file (.mat) + % Call analyzeCalibration function for calibration of the MS + analyzeCalibration(experimentStruct,[]) % Call the function to generate the calibration file + % Calibrate MS + else + % Call analyzeCalibration function for calibration of the MS + analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file + end + % Analyze real experiment + else + % Call reconcileData function to get the output mole fraction for a + % real experiment + [outputStruct,~] = concatenateData(experimentStruct); -% Flag to decide calibration or analysis -flagCalibration = false; + % Clean mole fraction to remove negative values (due to calibration) + % Replace all negative molefraction with eps + outputStruct.moleFrac(outputStruct.moleFrac(:,2)<0,1)=eps; % CO2 + outputStruct.moleFrac(:,1)=1-outputStruct.moleFrac(:,2); % Compute He with mass balance -% Flag to decide calibration of flow meters or MS -flagFlowMeter = false; + % Convert the MFM flow to real flow + % Load the meter calibrations + load(experimentStruct.calibrationFlow); + % Get the MFM flow rate + volFlow_MFM = outputStruct.flow(:,2); + % Get the CO2 mole fraction for obtaining real flow rate + moleFracCO2 = outputStruct.moleFrac(:,2); + % Compute the total flow rate of the gas [ccm] + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + totalFlowRate = round(calibrationFlow.MFM(moleFracCO2,volFlow_MFM),1); -% Mode to switch between calibration and analyzing real experiment -% Analyze calibration data -if flagCalibration - % Calibrate flow meter - if flagFlowMeter - % File with the calibration data to build a model for MFC/MFM - experimentStruct = 'ZLCCalibrateMeters_20210419'; % Experimental flow file (.mat) - % Call analyzeCalibration function for calibration of the MS - analyzeCalibration(experimentStruct,[]) % Call the function to generate the calibration file - % Calibrate MS - else - experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210316__Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLCCalibrateMS_20210420'; % Experimental flow file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLCCalibrateMS_20210420.asc'; % Experimental MS file (.asc) - experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) - experimentStruct.numMean = 10; % Number of points for averaging - experimentStruct.flagUseIndGas = false; % Flag to determine whether independent (true) or ratio of signals used for calibration - experimentStruct.polyDeg = 3; % Degree of polynomial fit for independent gas calibraiton - % Call analyzeCalibration function for calibration of the MS - analyzeCalibration([],experimentStruct) % Call the function to generate the calibration file - end -% Analyze real experiment -else - moleFracThreshold = 1e-3; % Threshold to cut data below a given mole fraction [-] - experimentStruct.calibrationFlow = 'ZLCCalibrateMeters_20210419_Model'; % Calibration file for meters (.mat) - experimentStruct.flow = 'ZLC_DeadVolume_Exp14A'; % Experimental flow file (.mat) - experimentStruct.calibrationMS = 'ZLCCalibrateMS_20210420_Model'; % Experimental calibration file (.mat) - experimentStruct.MS = 'C:\Users\QCPML\Desktop\Ashwin\MS\ZLC_DeadVolume_Exp14.asc'; % Experimental MS file (.asc) - experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) - % Call reconcileData function to get the output mole fraction for a - % real experiment - [outputStruct,~] = concatenateData(experimentStruct); - - % Clean mole fraction to remove negative values (due to calibration) - % Replace all negative molefraction with eps - outputStruct.moleFrac(outputStruct.moleFrac(:,2)<0,1)=eps; % CO2 - outputStruct.moleFrac(:,1)=1-outputStruct.moleFrac(:,2); % Compute He with mass balance - - % Convert the MFM flow to real flow - % Load the meter calibrations - load(experimentStruct.calibrationFlow); - % Get the MFM flow rate - volFlow_MFM = outputStruct.flow(:,2); - % Get the CO2 mole fraction for obtaining real flow rate - moleFracCO2 = outputStruct.moleFrac(:,2); - % Compute the total flow rate of the gas [ccm] - % Round the flow rate to the nearest first decimal (as this is the - % resolution of the meter) - totalFlowRate = round(calibrationFlow.MFM(moleFracCO2,volFlow_MFM),1); - - % Input for the ZLC script (Python) - % Find the index for the mole fraction that corresponds to the - % threshold mole fraction - moleFracThresholdInd = find(outputStruct.moleFrac(:,2) Date: Mon, 10 May 2021 14:22:28 +0100 Subject: [PATCH 096/189] Small fix for MS calibration experiment script --- experimental/defineSetPtManual.m | 70 +++++++++++++++++++++++++ experimental/runMultipleMSCalibration.m | 18 +++++-- 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 experimental/defineSetPtManual.m diff --git a/experimental/defineSetPtManual.m b/experimental/defineSetPtManual.m new file mode 100644 index 0000000..53020b1 --- /dev/null +++ b/experimental/defineSetPtManual.m @@ -0,0 +1,70 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% +% Purpose: +% Function to define manual set points for the two flow controllers in the +% ZLC setup +% +% Last modified: +% - 2021-05-10, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function defineSetPtManual(MFC1_SP,MFC2_SP) + % Define gas and set point for MFC1 + expInfo.gasName_MFC1 = 'He'; + gasName_MFC1 = expInfo.gasName_MFC1; + expInfo.MFC1_SP = MFC1_SP; + % Find the port corresponding MFC1 + portText = matchUSBport({'FT1EU0ACA'}); + if ~isempty(portText{1}) + portMFC1 = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + end + % Generate Serial port object + serialObj.MFC1 = struct('portName',portMFC1,'baudRate',19200,'terminator','CR'); + % Generate serial command for polling data + serialObj.cmdPollData = generateSerialCommand('pollData',1); + % Generate Gas ID for Alicat devices + gasID_MFC1 = checkGasName(gasName_MFC1); + % Set the gas for MFC1 + [~] = controlAuxiliaryEquipments(serialObj.MFC1, gasID_MFC1,1); % Set gas for MFC1 + % Generate serial command for volumteric flow rate set poin + cmdSetPt = generateSerialCommand('setPoint',1,expInfo.MFC1_SP); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC1, cmdSetPt,1); % Set gas for MFC1 + % Check if the set point was sent to the controller + outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); + + % Define gas and set point for MFC1 + expInfo.gasName_MFC2 = 'CO2'; + gasName_MFC2 = expInfo.gasName_MFC2; + expInfo.MFC2_SP = MFC2_SP; + % Find the port corresponding MFC2 + portText = matchUSBport({'FT1EQDD6A'}); + if ~isempty(portText{1}) + portMFC2 = ['COM',portText{1}(regexp(portText{1},'COM[123456789] - FTDI')+3)]; + end + % Generate Serial port object + serialObj.MFC2 = struct('portName',portMFC2,'baudRate',19200,'terminator','CR'); + % Generate serial command for polling data + serialObj.cmdPollData = generateSerialCommand('pollData',1); + % Generate Gas ID for Alicat devices + gasID_MFC2 = checkGasName(gasName_MFC2); + % Set the gas for MFC2 + [~] = controlAuxiliaryEquipments(serialObj.MFC2, gasID_MFC2,1); % Set gas for MFC2 + % Generate serial command for volumteric flow rate set poin + cmdSetPt = generateSerialCommand('setPoint',1,expInfo.MFC2_SP); % Same units as device + [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC2 + % Check if the set point was sent to the controller + outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); +end diff --git a/experimental/runMultipleMSCalibration.m b/experimental/runMultipleMSCalibration.m index 128473b..a8d06f6 100644 --- a/experimental/runMultipleMSCalibration.m +++ b/experimental/runMultipleMSCalibration.m @@ -21,14 +21,24 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Define the total flow rate of interest -totalFlowRate = [5, 7, 9 , 15, 30, 45, 60, 90]; +totalFlowRate = [5 10 15 30 45 60]; % Loop through all total flow rates for ii=1:length(totalFlowRate) % Define the set point for MFC2 (CO2) % This is done on a log scale to have a high reoslution at low CO2 - % concentrations - MFC2 = round(logspace(log10(0.2),log10(totalFlowRate(ii)),20),1); + % concentrations. Linear spaced flow rates for high compositions + MFC2 = unique([0 round(logspace(log10(0.2),log10(1),10),1) ... + round(linspace(1,max(totalFlowRate(ii)),10),1)]); + pause(3600); % Call calibrateMS function calibrateMS(MFC2) -end \ No newline at end of file + % Change the flow rate of CO2 to 0 and of He to the next set point to + % equilibrate + if ii Date: Mon, 10 May 2021 14:26:31 +0100 Subject: [PATCH 097/189] Change folder structure for experimental analysis script --- experimental/{ => analysis}/analyzeCalibration.m | 0 experimental/{ => analysis}/analyzeExperiment.m | 0 experimental/{ => analysis}/concatenateData.m | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename experimental/{ => analysis}/analyzeCalibration.m (100%) rename experimental/{ => analysis}/analyzeExperiment.m (100%) rename experimental/{ => analysis}/concatenateData.m (100%) diff --git a/experimental/analyzeCalibration.m b/experimental/analysis/analyzeCalibration.m similarity index 100% rename from experimental/analyzeCalibration.m rename to experimental/analysis/analyzeCalibration.m diff --git a/experimental/analyzeExperiment.m b/experimental/analysis/analyzeExperiment.m similarity index 100% rename from experimental/analyzeExperiment.m rename to experimental/analysis/analyzeExperiment.m diff --git a/experimental/concatenateData.m b/experimental/analysis/concatenateData.m similarity index 100% rename from experimental/concatenateData.m rename to experimental/analysis/concatenateData.m From 429927cb9bac2e71fbf8bfb5cc946fe6d8df31c0 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 10 May 2021 14:46:59 +0100 Subject: [PATCH 098/189] Change directory structure and associated file nomenclatures --- experimental/analysis/analyzeCalibration.m | 16 ++++++------ experimental/analysis/analyzeExperiment.m | 15 +++++------ experimental/analysis/concatenateData.m | 4 +-- experimental/analyzeExperimentWrapper.m | 25 ++++++++++--------- .../defineSetPtManual.m | 0 .../{ => calibrationFunctions}/calibrateMS.m | 6 ++--- .../calibrateMeters.m | 0 .../runMultipleMSCalibration.m | 0 8 files changed, 34 insertions(+), 32 deletions(-) rename experimental/{ => auxillaryEquipments}/defineSetPtManual.m (100%) rename experimental/{ => calibrationFunctions}/calibrateMS.m (95%) rename experimental/{ => calibrationFunctions}/calibrateMeters.m (100%) rename experimental/{ => calibrationFunctions}/runMultipleMSCalibration.m (100%) diff --git a/experimental/analysis/analyzeCalibration.m b/experimental/analysis/analyzeCalibration.m index 4b33f3f..e9b0960 100644 --- a/experimental/analysis/analyzeCalibration.m +++ b/experimental/analysis/analyzeCalibration.m @@ -87,17 +87,17 @@ function analyzeCalibration(parametersFlow,parametersMS) % Save the calibration data into a .mat file % Check if calibration data folder exists - if exist(['experimentalData',filesep,... + if exist(['..',filesep,'experimentalData',filesep,... 'calibrationData'],'dir') == 7 % Save the calibration data for further use - save(['experimentalData',filesep,... + save(['..',filesep,'experimentalData',filesep,... 'calibrationData',filesep,parametersFlow,'_Model'],'calibrationFlow',... 'gitCommitID'); else % Create the calibration data folder if it does not exist - mkdir(['experimentalData',filesep,'calibrationData']) + mkdir(['..',filesep,'experimentalData',filesep,'calibrationData']) % Save the calibration data for further use - save(['experimentalData',filesep,... + save(['..',filesep,'experimentalData',filesep,... 'calibrationData',filesep,parametersFlow,'_Model'],'calibrationFlow',... 'gitCommitID'); end @@ -196,17 +196,17 @@ function analyzeCalibration(parametersFlow,parametersMS) % Save the calibration data into a .mat file % Check if calibration data folder exists - if exist(['experimentalData',filesep,... + if exist(['..',filesep,'experimentalData',filesep,... 'calibrationData'],'dir') == 7 % Save the calibration data for further use - save(['experimentalData',filesep,... + save(['..',filesep,'experimentalData',filesep,... 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... 'gitCommitID','parametersMS'); else % Create the calibration data folder if it does not exist - mkdir(['experimentalData',filesep,'calibrationData']) + mkdir(['..',filesep,'experimentalData',filesep,'calibrationData']) % Save the calibration data for further use - save(['experimentalData',filesep,... + save(['..',filesep,'experimentalData',filesep,... 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... 'gitCommitID','parametersMS'); end diff --git a/experimental/analysis/analyzeExperiment.m b/experimental/analysis/analyzeExperiment.m index 79d41f5..0f64d86 100644 --- a/experimental/analysis/analyzeExperiment.m +++ b/experimental/analysis/analyzeExperiment.m @@ -82,22 +82,23 @@ function analyzeExperiment(experimentStruct,flagCalibration,flagFlowMeter) experimentOutput.timeExp = outputStruct.flow(1:moleFracThresholdInd,1); % Time elapsed [s] experimentOutput.moleFrac = outputStruct.moleFrac(1:moleFracThresholdInd,2); % Mole fraction CO2 [-] experimentOutput.totalFlowRate = totalFlowRate(1:moleFracThresholdInd)./60; % Total flow rate of the gas [ccs] - + % Save outputStruct to semiProcessedStruct + semiProcessedStruct = outputStruct; % Check concatenateData for more (this is reconciledData there) % Save the experimental output into a .mat file % Check if runData data folder exists - if exist(['experimentalData',filesep,... + if exist(['..',filesep,'experimentalData',filesep,... 'runData'],'dir') == 7 % Save the calibration data for further use - save(['experimentalData',filesep,... + save(['..',filesep,'experimentalData',filesep,... 'runData',filesep,experimentStruct.flow,'_Output'],'experimentOutput',... - 'experimentStruct','gitCommitID'); + 'experimentStruct','semiProcessedStruct','gitCommitID'); else % Create the calibration data folder if it does not exist - mkdir(['experimentalData',filesep,'runData']) + mkdir(['..',filesep,'experimentalData',filesep,'runData']) % Save the calibration data for further use - save(['experimentalData',filesep,... + save(['..',filesep,'experimentalData',filesep,... 'runData',filesep,experimentStruct.flow,'_Output'],'experimentOutput',... - 'experimentStruct','gitCommitID'); + 'experimentStruct','semiProcessedStruct','gitCommitID'); end end end \ No newline at end of file diff --git a/experimental/analysis/concatenateData.m b/experimental/analysis/concatenateData.m index 2f4e8d7..a9e8c52 100644 --- a/experimental/analysis/concatenateData.m +++ b/experimental/analysis/concatenateData.m @@ -207,12 +207,12 @@ % Parse out the fitting parameters paramFit = calibrationMS.ratioHeCO2; % Use a fourier series model to obtain the mole fraction - moleFracTemp(:,ii) = paramFit(reconciledData.MS(:,2)./... + reconciledData.moleFracIndCalib(:,ii) = paramFit(reconciledData.MS(:,2)./... (reconciledData.MS(:,2)+reconciledData.MS(:,3))); % He [-] end % Take the mean of all the compositions obtained from the different % calibrations - reconciledData.moleFrac(:,1) = mean(moleFracTemp,2); % He [-] + reconciledData.moleFrac(:,1) = mean(reconciledData.moleFracIndCalib,2); % He [-] reconciledData.moleFrac(:,2) = 1 - reconciledData.moleFrac(:,1); % CO2 [-] end end \ No newline at end of file diff --git a/experimental/analyzeExperimentWrapper.m b/experimental/analyzeExperimentWrapper.m index c7615b3..3dde182 100644 --- a/experimental/analyzeExperimentWrapper.m +++ b/experimental/analyzeExperimentWrapper.m @@ -62,18 +62,18 @@ end % % Loop through all the MS calibration files -% for ii = 1:length(msCalibrationFiles) -% experimentStruct.calibrationFlow = flowMeterCalibration; % Calibration file for meters (.mat) -% experimentStruct.flow = msCalibrationFiles{ii}; % Experimental flow file (.mat) -% experimentStruct.MS = [msFileDir,filesep,msRawFileALL{ii},'.asc']; % Experimental MS file (.asc) -% experimentStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) -% experimentStruct.numMean = 10; % Number of points for averaging -% % Call the analyzeExperiment function to calibrate the MS at the conditions -% % experiment was performed for calibration -% % The output calibration model is usually in calibration folder -% % Syntax: analyzeExperiment(experimentStruct,calibrationMode,calibrationFlowMeter) -% analyzeExperiment(experimentStruct,true,false); % Calibrate MS -% end +for ii = 1:length(msCalibrationFiles) + calibrationStruct.calibrationFlow = flowMeterCalibration; % Calibration file for meters (.mat) + calibrationStruct.flow = msCalibrationFiles{ii}; % Experimental flow file (.mat) + calibrationStruct.MS = [msFileDir,filesep,msRawFileALL{ii},'.asc']; % Experimental MS file (.asc) + calibrationStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) + calibrationStruct.numMean = 10; % Number of points for averaging + % Call the analyzeExperiment function to calibrate the MS at the conditions + % experiment was performed for calibration + % The output calibration model is usually in calibration folder + % Syntax: analyzeExperiment(experimentStruct,calibrationMode,calibrationFlowMeter) + analyzeExperiment(calibrationStruct,true,false); % Calibrate MS +end % Loop through all the experimental files if ~isempty(experimentFiles) @@ -82,6 +82,7 @@ experimentStruct.flow = experimentFiles{ii}; % Experimental flow file (.mat) experimentStruct.MS = [msFileDir,filesep,msExpFile,'.asc']; % Experimental MS file (.asc). Assumes name of file to be the date of the first flow rate experimentStruct.calibrationMS = msCalibrationFiles; % Experimental calibration file list + experimentStruct.interpMS = calibrationStruct.interpMS; % Flag for interpolating MS data, same as calibration experimentStruct.moleFracThreshold = 1e-3; % Threshold for cutting off data below a given mole fraction % Call the analyzeExperiment function to analyze the experimental data % using the calibration files given by msCalibrationFiles diff --git a/experimental/defineSetPtManual.m b/experimental/auxillaryEquipments/defineSetPtManual.m similarity index 100% rename from experimental/defineSetPtManual.m rename to experimental/auxillaryEquipments/defineSetPtManual.m diff --git a/experimental/calibrateMS.m b/experimental/calibrationFunctions/calibrateMS.m similarity index 95% rename from experimental/calibrateMS.m rename to experimental/calibrationFunctions/calibrateMS.m index 86d23aa..d9c042e 100644 --- a/experimental/calibrateMS.m +++ b/experimental/calibrationFunctions/calibrateMS.m @@ -23,7 +23,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function calibrateMS(varargin) % Sampling time for the device - expInfo.samplingTime = 2; + expInfo.samplingTime = 1; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -60,10 +60,10 @@ function calibrateMS(varargin) if ii == find(MFC1_SP == 0,1,'last') % Maximum time of the experiment % Change the max time to 10 min - expInfo.maxTime = 600; + expInfo.maxTime = 300; else % Else use 5 min - expInfo.maxTime = 600; + expInfo.maxTime = 300; end % Run the setup for different calibrations runZLC(expInfo) diff --git a/experimental/calibrateMeters.m b/experimental/calibrationFunctions/calibrateMeters.m similarity index 100% rename from experimental/calibrateMeters.m rename to experimental/calibrationFunctions/calibrateMeters.m diff --git a/experimental/runMultipleMSCalibration.m b/experimental/calibrationFunctions/runMultipleMSCalibration.m similarity index 100% rename from experimental/runMultipleMSCalibration.m rename to experimental/calibrationFunctions/runMultipleMSCalibration.m From 128960933cbfd94af1ca644d26c18ebc80bacc28 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 10 May 2021 17:24:03 +0100 Subject: [PATCH 099/189] Add plots in analyze experiment --- experimental/analyzeExperimentWrapper.m | 28 +++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/experimental/analyzeExperimentWrapper.m b/experimental/analyzeExperimentWrapper.m index 3dde182..7a41667 100644 --- a/experimental/analyzeExperimentWrapper.m +++ b/experimental/analyzeExperimentWrapper.m @@ -44,9 +44,13 @@ %%%% Experimet to be analyzed %%%% % List the experiments that have to be analyzed -msExpFile = 'ZLC_DeadVolume_Exp15'; % Raw MS data file name +msExpFile = 'ZLC_DeadVolume_Exp16'; % Raw MS data file name % Flow rate files for experiments -experimentFiles = {'ZLC_DeadVolume_Exp15A'}; +experimentFiles = {'ZLC_DeadVolume_Exp16A',... + 'ZLC_DeadVolume_Exp16B',... + 'ZLC_DeadVolume_Exp16C',... + 'ZLC_DeadVolume_Exp16D',... + 'ZLC_DeadVolume_Exp16E'}; % Initialize the name of the msRawFile to be used for all calibrations startInd = 1; @@ -90,4 +94,24 @@ % Syntax: analyzeExperiment(experimentStruct,calibrationMode,calibrationFlowMeter) analyzeExperiment(experimentStruct,false,false); % Analyze experiment end +end + +% Loop through all the experimental files and plot the output mole fraction +if ~isempty(experimentFiles) + colorForPlot = {'5C73B9','7262C3','8852CD','9D41D7','B330E1'}; + for ii = 1:length(experimentFiles) + load([experimentFiles{ii},'_Output'],'experimentOutput'); + subplot(1,2,1) + semilogy(experimentOutput.timeExp,experimentOutput.moleFrac,'color',['#',colorForPlot{ii}]); + hold on + box on;grid on; + xlim([0,500]); ylim([0,1]); + xlabel('t [s]'); ylabel('y [-]'); + subplot(1,2,2) + semilogy(experimentOutput.timeExp.*experimentOutput.totalFlowRate,experimentOutput.moleFrac,'color',['#',colorForPlot{ii}]); + hold on + xlim([0,100]); ylim([0,1]); + xlabel('Ft [cc]'); ylabel('y [-]'); + box on;grid on; + end end \ No newline at end of file From 56a071742112b15fd16d5dc32acf0b759c9f12b1 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 11 May 2021 16:36:18 +0100 Subject: [PATCH 100/189] Small fixes --- experimental/extractDeadVolume.py | 5 ++--- experimental/extractZLCParameters.py | 7 ++++--- experimental/simulateZLC.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index fe542d0..3d5ae73 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -69,9 +69,8 @@ def extractDeadVolume(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp15A_Output.mat', - 'ZLC_DeadVolume_Exp15B_Output.mat', - 'ZLC_DeadVolume_Exp15C_Output.mat'] + fileName = ['ZLC_DeadVolume_Exp16A_Output.mat', + 'ZLC_DeadVolume_Exp16B_Output.mat'] # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index e625c71..9949397 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -48,7 +48,7 @@ def extractZLCParameters(): num_cores = multiprocessing.cpu_count() # Isotherm model type - modelType = 'DSL' + modelType = 'SSL' # Number of times optimization repeated numOptRepeat = 10 @@ -62,8 +62,9 @@ def extractZLCParameters(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp17A_Output.mat', - 'ZLC_ActivatedCarbon_Exp17B_Output.mat'] + fileName = ['ZLC_ActivatedCarbon_Exp20A_Output.mat', + 'ZLC_ActivatedCarbon_Exp20B_Output.mat', + 'ZLC_ActivatedCarbon_Exp20C_Output.mat'] # NOTE: Dead volume characteristics filename is hardcoded in # simulateCombinedModel. This is because of the python GA function unable diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 8516600..6473485 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -97,13 +97,13 @@ def simulateZLC(**kwargs): if 'volSorbent' in kwargs: volSorbent = kwargs["volSorbent"] else: - volSorbent = 1.5e-8 + volSorbent = 1.70e-8 # Volume of gas chamber (dead volume) [m3] if 'volGas' in kwargs: volGas = kwargs["volGas"] else: - volGas = 2.5e-8 + volGas = 2.65e-8 # Isotherm model parameters (SSL or DSL) if 'isothermModel' in kwargs: From ba091f50ecb4337e7cb145917b4f48dd6f545911 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 11 May 2021 18:38:41 +0100 Subject: [PATCH 101/189] Small bug fixes --- experimental/computeMLEError.py | 2 +- experimental/extractDeadVolume.py | 2 +- experimental/extractZLCParameters.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py index b9882c3..8117a54 100644 --- a/experimental/computeMLEError.py +++ b/experimental/computeMLEError.py @@ -30,7 +30,7 @@ def computeMLEError(moleFracExp,moleFracSim): # Objective function error # Find error for mole fraction below a given threshold - thresholdFactor = 1e-2 + thresholdFactor = 5e-2 lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) # Do downsampling if the number of points in higher and lower # compositions does not match diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 3d5ae73..29a05dd 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -52,7 +52,7 @@ def extractDeadVolume(): num_cores = multiprocessing.cpu_count() # Number of times optimization repeated - numOptRepeat = 10 + numOptRepeat = 5 # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 9949397..14fd949 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -51,7 +51,7 @@ def extractZLCParameters(): modelType = 'SSL' # Number of times optimization repeated - numOptRepeat = 10 + numOptRepeat = 5 # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() From 329e8b780c8d2fc2dd37393b4c6add66e57e7dc3 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 14 May 2021 13:38:20 +0100 Subject: [PATCH 102/189] Major structure changes and change density from pellet to skeletal --- experimental/computeMLEError.py | 86 +++++++++++++++------------ experimental/extractZLCParameters.py | 31 ++++++++-- experimental/simulateCombinedModel.py | 25 +++++++- experimental/simulateZLC.py | 8 ++- 4 files changed, 102 insertions(+), 48 deletions(-) diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py index 8117a54..354351f 100644 --- a/experimental/computeMLEError.py +++ b/experimental/computeMLEError.py @@ -9,12 +9,10 @@ # Authors: Ashwin Kumar Rajagopalan (AK) # # Purpose: -# Computes the MLE error for ZLC experiments. The function splits the -# response curve to two distinct regions (high and low composition) to given -# equal weights. The cut off threshold for high and low composition is hard -# coded in this function. Note that the error is +# Computes the MLE error for ZLC experiments. # # Last modified: +# - 2021-05-13, AK: Add different modes for MLE error computations # - 2021-05-05, AK: Initial creation # # Input arguments: @@ -25,41 +23,55 @@ # ############################################################################ -def computeMLEError(moleFracExp,moleFracSim): +def computeMLEError(moleFracExp,moleFracSim,**kwargs): import numpy as np - - # Objective function error - # Find error for mole fraction below a given threshold - thresholdFactor = 5e-2 - lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) - # Do downsampling if the number of points in higher and lower - # compositions does not match - numPointsConc = np.zeros([2]) - numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition - numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition - downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals - - # Compute error for higher concentrations (accounting for downsampling) - moleFracHighExp = moleFracExp[0:lastIndThreshold] - moleFracHighSim = moleFracSim[0:lastIndThreshold] - computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] - - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) - - # Find scaling factor for lower concentrations - scalingFactor = int(1/thresholdFactor) # Assumes max composition is one - # Compute error for lower concentrations - moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor - moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor - # Compute error for low concentrations (accounting for downsampling) - computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] - - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) - - # Find the sum of computed error - computedError = computedErrorHigh + computedErrorLow + # Check if threshold is provided to split data to high and low compositions + if 'thresholdFactor' in kwargs: + thresholdFlag = True + thresholdFactor = np.array(kwargs["thresholdFactor"]) + # Default is flag, uses all data with equal weights + else: + thresholdFlag = False + + # If no threshold provided just do normal MLE + if not thresholdFlag: + computedError = np.log(np.sum(np.power(moleFracExp - moleFracSim, 2))) + numPoints = len(moleFracExp) + # If threshold provided, split the data to two and compute the error + else: + # Objective function error + # Find error for mole fraction below a given threshold + thresholdFactor = 5e-2 + lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) + # Do downsampling if the number of points in higher and lower + # compositions does not match + numPointsConc = np.zeros([2]) + numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition + numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition + downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals + + # Compute error for higher concentrations (accounting for downsampling) + moleFracHighExp = moleFracExp[0:lastIndThreshold] + moleFracHighSim = moleFracSim[0:lastIndThreshold] + computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] + - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) + + # Find scaling factor for lower concentrations + scalingFactor = int(1/thresholdFactor) # Assumes max composition is one + # Compute error for lower concentrations + moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor + moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor - # Compute the number of points per experiment (accouting for down- - # sampling in both experiments and high and low compositions - numPoints = len(moleFracHighExp) + len(moleFracLowExp) + # Compute error for low concentrations (accounting for downsampling) + computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] + - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) + + # Find the sum of computed error + computedError = computedErrorHigh + computedErrorLow + + # Compute the number of points per experiment (accouting for down- + # sampling in both experiments and high and low compositions + numPoints = len(moleFracHighExp) + len(moleFracLowExp) return (numPoints/2)*(computedError) \ No newline at end of file diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 14fd949..0ad7815 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-05-13, AK: Change structure to input mass of adsorbent # - 2021-05-05, AK: Bug fix for MLE error computation # - 2021-05-05, AK: Modify error computation for dead volume # - 2021-04-28, AK: Add reference values for isotherm parameters @@ -62,9 +63,11 @@ def extractZLCParameters(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp20A_Output.mat', - 'ZLC_ActivatedCarbon_Exp20B_Output.mat', - 'ZLC_ActivatedCarbon_Exp20C_Output.mat'] + fileName = ['ZLC_ActivatedCarbon_Exp24A_Output.mat', + 'ZLC_ActivatedCarbon_Exp24B_Output.mat', + 'ZLC_ActivatedCarbon_Exp24C_Output.mat', + 'ZLC_ActivatedCarbon_Exp24D_Output.mat', + 'ZLC_ActivatedCarbon_Exp24E_Output.mat'] # NOTE: Dead volume characteristics filename is hardcoded in # simulateCombinedModel. This is because of the python GA function unable @@ -145,7 +148,22 @@ def ZLCObjectiveFunction(x): from extractDeadVolume import filesToProcess # File processing script from simulateCombinedModel import simulateCombinedModel from computeMLEError import computeMLEError - + + # Dead volume model + deadVolumeFile = 'deadVolumeCharacteristics_20210511_1015_ebb447e.npz' + + # Adsorbent density [kg/m3] + # This has to be the skeletal density + adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] + # Particle porosity + particleEpsilon = 0.61 + # Particle mass [g] + massSorbent = 0.0846 + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + # Reference for the isotherm parameters # For SSL isotherm if len(x) == 4: @@ -196,7 +214,10 @@ def ZLCObjectiveFunction(x): timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) - expFlag = True) + expFlag = True, + deadVolumeFile = deadVolumeFile, + volSorbent = volSorbent, + volGas = volGas) # Stack mole fraction from experiments and simulation for error # computation diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index 0a1b1db..db1d8cb 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -14,6 +14,7 @@ # volume simulator # # Last modified: +# - 2021-05-13, AK: Add volumes and density as inputs # - 2021-04-27, AK: Convert to a function for parameter estimation # - 2021-04-22, AK: Initial creation # @@ -75,12 +76,27 @@ def simulateCombinedModel(**kwargs): timeInt = kwargs["timeInt"] else: timeInt = (0.0,300) - + # Volume of sorbent material [m3] + if 'volSorbent' in kwargs: + volSorbent = kwargs["volSorbent"] + else: + volSorbent = 4.35e-8 + # Volume of gas chamber (dead volume) [m3] + if 'volGas' in kwargs: + volGas = kwargs["volGas"] + else: + volGas = 6.81e-8 + # Adsorbent density [kg/m3] + # This has to be the skeletal density + if 'adsorbentDensity' in kwargs: + adsorbentDensity = kwargs["adsorbentDensity"] + else: + adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] # File name with dead volume characteristics parameters if 'deadVolumeFile' in kwargs: deadVolumeFile = kwargs["deadVolumeFile"] else: - deadVolumeFile = 'deadVolumeCharacteristics_20210505_1849_31987ca.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210511_1015_ebb447e.npz' # Flag to check if experimental data used if 'expFlag' in kwargs: @@ -94,7 +110,10 @@ def simulateCombinedModel(**kwargs): flowIn = flowIn, initMoleFrac = initMoleFrac, timeInt = timeInt, - expFlag=expFlag) + expFlag=expFlag, + volSorbent = volSorbent, + volGas = volGas, + adsorbentDensity = adsorbentDensity) # Parse out the mole fraction out from ZLC moleFracZLC = resultMat[0,:] diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 6473485..d1b25b7 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -14,6 +14,7 @@ # model with mass transfer defined using linear driving force. # # Last modified: +# - 2021-05-13, AK: IMPORTANT: Change density from particle to skeletal # - 2021-04-27, AK: Fix inputs and add isotherm model as input # - 2021-04-26, AK: Revamp the code for real sorbent simulation # - 2021-03-25, AK: Remove the constant F model @@ -97,13 +98,13 @@ def simulateZLC(**kwargs): if 'volSorbent' in kwargs: volSorbent = kwargs["volSorbent"] else: - volSorbent = 1.70e-8 + volSorbent = 4.35e-8 # Volume of gas chamber (dead volume) [m3] if 'volGas' in kwargs: volGas = kwargs["volGas"] else: - volGas = 2.65e-8 + volGas = 6.81e-8 # Isotherm model parameters (SSL or DSL) if 'isothermModel' in kwargs: @@ -114,10 +115,11 @@ def simulateZLC(**kwargs): isothermModel = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3] # Adsorbent density [kg/m3] + # This has to be the skeletal density if 'adsorbentDensity' in kwargs: adsorbentDensity = kwargs["adsorbentDensity"] else: - adsorbentDensity = 760 # Activated carbon density [kg/m3] + adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] # Total pressure of the gas [Pa] if 'pressureTotal' in kwargs: From 7d48492dca967558a1f39f9147f766489547ca07 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 14 May 2021 15:37:02 +0100 Subject: [PATCH 103/189] Improve plotting capabilities --- plotFunctions/plotExperimentOutcome.py | 134 ++++++++++++++++++++----- 1 file changed, 108 insertions(+), 26 deletions(-) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 64cca53..917b64e 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-05-14, AK: Improve plotting capabilities # - 2021-05-05, AK: Bug fix for MLE error computation # - 2021-05-04, AK: Bug fix for error computation # - 2021-05-04, AK: Implement plots for ZLC and change DV error computaiton @@ -29,6 +30,7 @@ import numpy as np from simulateDeadVolume import simulateDeadVolume from computeMLEError import computeMLEError +from computeEquilibriumLoading import computeEquilibriumLoading from numpy import load import os import matplotlib.pyplot as plt @@ -48,13 +50,36 @@ saveFileExtension = ".png" # Flag to plot dead volume results -flagDeadVolume = True +flagDeadVolume = False # Flag to plot simulations simulateModel = True # Flag to plot dead volume results -plotFt = True +plotFt = False + +# Adsorbent density [kg/m3] +# This has to be the skeletal density +adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] + +# Particle porosity +particleEpsilon = 0.61 + +# Particle mass [g] +massSorbent = 0.0846 + +# Volume of sorbent material [m3] +volSorbent = (massSorbent/1000)/adsorbentDensity + +# Volume of gas chamber (dead volume) [m3] +volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + +# Total pressure of the gas [Pa] +pressureTotal = np.array([1.e5]); + +# Temperature of the gas [K] +temperature = np.array([298.15]); + # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' @@ -67,15 +92,15 @@ if flagDeadVolume: # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp15A_Output_10a7d64.npz', - 'ZLC_DeadVolume_Exp15B_Output_10a7d64.npz', - 'ZLC_DeadVolume_Exp15C_Output_10a7d64.npz'] + fileName = ['ZLC_DeadVolume_Exp16B_Output_ba091f5.npz', + 'ZLC_DeadVolume_Exp16C_Output_ba091f5.npz', + 'ZLC_DeadVolume_Exp16D_Output_ba091f5.npz'] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210505_1849_31987ca.npz' + fileParameter = 'deadVolumeCharacteristics_20210511_1203_ebb447e.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] - + numPointsExp = np.zeros(len(fileName)) for ii in range(len(fileName)): fileToLoad = mainDir + fileName[ii] @@ -150,7 +175,7 @@ color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$t$ [s]', ylabel='$y_1$ [-]', - xlim = [0,200], ylim = [0, 1]) + xlim = [0,100], ylim = [0, 1]) ax1.legend() # Log scale @@ -161,7 +186,7 @@ ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', - xlim = [0,400], ylim = [1e-3, 1]) + xlim = [0,150], ylim = [5e-3, 1]) # Save the figure if saveFlag: @@ -216,17 +241,22 @@ # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp17A_Output_8e60357.npz', - 'ZLC_ActivatedCarbon_Exp17B_Output_8e60357.npz'] + fileName = [ + 'ZLC_ActivatedCarbon_Exp25A_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp25B_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp25C_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp25D_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp25E_Output_ba091f5.npz',] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'zlcParameters_20210428_1011_10a7d64.npz' # 17A-F - fileParameter = 'zlcParameters_20210505_1735_31987ca.npz' # 17A-B + # Dead volume model + deadVolumeFile = 'deadVolumeCharacteristics_20210511_1015_ebb447e.npz' + # ZLC parameter model + fileParameter = 'zlcParameters_20210513_2239_ba091f5.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] print("Objective Function",round(modelOutputTemp[()]["function"],0)) modelNonDim = modelOutputTemp[()]["variable"] - numPointsExp = np.zeros(len(fileName)) for ii in range(len(fileName)): fileToLoad = mainDir + fileName[ii] @@ -236,18 +266,20 @@ # Downsample intervals downsampleInt = numPointsExp/np.min(numPointsExp) - - # Multiply the paremeters by the reference values - x = np.multiply(modelNonDim,[10, 1e-5, 50e3, 10, 1e-5, 50e3, 100]) + + # Multiply the paremeters by the reference values (for SSL) + x = np.multiply(modelNonDim,[10, 1e-5, 50e3,100]) # Ronny AC Data - x = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3,100] + x_RP = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3,100] + # Initialize loadings computedError = 0 numPoints = 0 moleFracExpALL = np.array([]) moleFracSimALL = np.array([]) - + massBalanceALL = np.zeros((len(fileName),2)) + # Create the instance for the plots fig = plt.figure ax1 = plt.subplot(1,3,1) @@ -281,7 +313,10 @@ flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) expFlag = True, isothermModel=x[0:-1], - rateConstant=x[-1]) + rateConstant=x[-1], + deadVolumeFile = deadVolumeFile, + volSorbent = volSorbent, + volGas = volGas) # Print simulation volume print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, moleFracSim), @@ -291,34 +326,42 @@ moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) + # Compute the mass balance at the end end of the ZLC + massBalanceALL[ii,0] = moleFracExp[0] + massBalanceALL[ii,1] = ((round(np.trapz(np.multiply(resultMat[3,:]*1e6, + resultMat[0,:]), + timeElapsedExp),2)*1e-6 + - volGas*moleFracExp[0])* + (pressureTotal/(8.314*temperature))/(massSorbent/1000)) + # y - Linear scale ax1.semilogy(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.05) # Experimental response + color=colorsForPlot[ii],alpha=0.25) # Experimental response if simulateModel: ax1.plot(timeElapsedExp,moleFracSim, color=colorsForPlot[ii],label=str(round(np.mean(flowRateExp),2))+" ccs") # Simulation response ax1.set(xlabel='$t$ [s]', ylabel='$y_1$ [-]', - xlim = [0,300], ylim = [1e-3, 1]) + xlim = [0,300], ylim = [5e-3, 1]) ax1.locator_params(axis="x", nbins=4) ax1.legend() # Ft - Log scale ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.05) # Experimental response + color=colorsForPlot[ii],alpha=0.25) # Experimental response if simulateModel: ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracSim, color=colorsForPlot[ii],label=str(round(np.mean(flowRateExp),2))+" ccs") # Simulation response ax2.set(xlabel='$Ft$ [cc]', - xlim = [0,100], ylim = [1e-3, 1]) + xlim = [0,100], ylim = [5e-3, 1]) ax2.locator_params(axis="x", nbins=4) # Flow rates ax3.plot(timeElapsedExp,flowRateExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.05,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.25,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax3.plot(timeElapsedExp,resultMat[3,:]*1e6, color=colorsForPlot[ii]) # Simulation response @@ -342,4 +385,43 @@ # Print the MLE error computedError = computeMLEError(moleFracExpALL,moleFracSimALL) - print(round(computedError,0)) \ No newline at end of file + print(round(computedError,0)) + + # Create the grid for mole fractions + y = np.linspace(0,1.,100) + # Initialize isotherms + isoLoading_RP = np.zeros([len(y)]) + isoLoading_ZLC = np.zeros([len(y)]) + + # Loop over all the mole fractions + for ii in range(len(y)): + isoLoading_RP[ii] = computeEquilibriumLoading(isothermModel=x_RP[0:-1], + moleFrac = y[ii]) + isoLoading_ZLC[ii] = computeEquilibriumLoading(isothermModel=x[0:-1], + moleFrac = y[ii]) + + # Plot the isotherms + os.chdir(os.path.join('..','plotFunctions')) + plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file + fig = plt.figure + ax1 = plt.subplot(1,1,1) + ax1.plot(y,isoLoading_RP,color='#2a9d8f',label="Autosorb") # Ronny's isotherm + ax1.plot(y,isoLoading_ZLC,color='#e76f51',label="ZLC") # ALL + ax1.scatter(massBalanceALL[:,0],massBalanceALL[:,1],c='dimgrey') + ax1.set(xlabel='$P$ [bar]', + ylabel='$q^*$ [mol kg$^\mathregular{-1}$]', + xlim = [0,1], ylim = [0, 3]) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.legend() + # Save the figure + if saveFlag: + # FileName: isothermComparison___ + saveFileName = "isothermComparison_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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 From 0ef2ff6786b40940c359ed283fb355d8bb058f62 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 14 May 2021 16:01:51 +0100 Subject: [PATCH 104/189] Cosmetic changes --- experimental/analysis/analyzeCalibration.m | 1 + experimental/analyzeExperimentWrapper.m | 48 ++++++++++++++-------- experimental/runMultipleZLC.m | 15 +++---- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/experimental/analysis/analyzeCalibration.m b/experimental/analysis/analyzeCalibration.m index e9b0960..97f4527 100644 --- a/experimental/analysis/analyzeCalibration.m +++ b/experimental/analysis/analyzeCalibration.m @@ -221,5 +221,6 @@ function analyzeCalibration(parametersFlow,parametersMS) box on; grid on; xlabel('Helium Signal/(CO2 Signal+Helium Signal) [-]') ylabel('Helium mole frac [-]') + set(gca,'FontSize',8) end end \ No newline at end of file diff --git a/experimental/analyzeExperimentWrapper.m b/experimental/analyzeExperimentWrapper.m index 7a41667..2c9c448 100644 --- a/experimental/analyzeExperimentWrapper.m +++ b/experimental/analyzeExperimentWrapper.m @@ -16,6 +16,7 @@ % experiment % % Last modified: +% - 2021-05-10, AK: Cosmetic changes to plots % - 2021-05-10, AK: Initial creation % % Input arguments: @@ -23,8 +24,6 @@ % Output arguments: % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%%% NOTE: Move analysis scripts to an analyze folder (change path for file saving) - analyzeCalibration, analyzeExperiment, - % List the flow meter calibration file (this is usually in calibraiton folder) flowMeterCalibration = 'ZLCCalibrateMeters_20210419_Model'; @@ -32,25 +31,24 @@ % List the MS calibration files (this is usually in experimental data folder) msFileDir = 'C:\Users\QCPML\Desktop\Ashwin\MS'; % Directory with MS data msRawFiles = {'ZLCCalibrateMS_20210505'}; % Raw MS data file names for all calibration -numExpForEachRawFile = [6]; % Number of experiments that use the same raw MS file (vector corresponding to number of MS files) +numExpForEachRawFile = [4]; % Number of experiments that use the same raw MS file (vector corresponding to number of MS files) % Flow rate files for calibration -msCalibrationFiles = {'ZLCCalibrateMS_20210505_5ccm',... - 'ZLCCalibrateMS_20210506_10ccm',... - 'ZLCCalibrateMS_20210506_15ccm',... +msCalibrationFiles = {'ZLCCalibrateMS_20210506_15ccm',... 'ZLCCalibrateMS_20210506_30ccm',... 'ZLCCalibrateMS_20210506_45ccm',... 'ZLCCalibrateMS_20210507_60ccm'}; %%%% Experimet to be analyzed %%%% % List the experiments that have to be analyzed -msExpFile = 'ZLC_DeadVolume_Exp16'; % Raw MS data file name +msExpFile = 'ZLC_ActivatedCarbon_Exp25'; % Raw MS data file name % Flow rate files for experiments -experimentFiles = {'ZLC_DeadVolume_Exp16A',... - 'ZLC_DeadVolume_Exp16B',... - 'ZLC_DeadVolume_Exp16C',... - 'ZLC_DeadVolume_Exp16D',... - 'ZLC_DeadVolume_Exp16E'}; +experimentFiles = {'ZLC_ActivatedCarbon_Exp25A',... + 'ZLC_ActivatedCarbon_Exp25B',... + 'ZLC_ActivatedCarbon_Exp25C',... + 'ZLC_ActivatedCarbon_Exp25D',... + 'ZLC_ActivatedCarbon_Exp25E',... + 'ZLC_ActivatedCarbon_Exp25F'}; % Initialize the name of the msRawFile to be used for all calibrations startInd = 1; @@ -71,7 +69,7 @@ calibrationStruct.flow = msCalibrationFiles{ii}; % Experimental flow file (.mat) calibrationStruct.MS = [msFileDir,filesep,msRawFileALL{ii},'.asc']; % Experimental MS file (.asc) calibrationStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) - calibrationStruct.numMean = 10; % Number of points for averaging + calibrationStruct.numMean = 50; % Number of points for averaging % Call the analyzeExperiment function to calibrate the MS at the conditions % experiment was performed for calibration % The output calibration model is usually in calibration folder @@ -87,7 +85,7 @@ experimentStruct.MS = [msFileDir,filesep,msExpFile,'.asc']; % Experimental MS file (.asc). Assumes name of file to be the date of the first flow rate experimentStruct.calibrationMS = msCalibrationFiles; % Experimental calibration file list experimentStruct.interpMS = calibrationStruct.interpMS; % Flag for interpolating MS data, same as calibration - experimentStruct.moleFracThreshold = 1e-3; % Threshold for cutting off data below a given mole fraction + experimentStruct.moleFracThreshold = 5e-3; % Threshold for cutting off data below a given mole fraction % Call the analyzeExperiment function to analyze the experimental data % using the calibration files given by msCalibrationFiles % The output is usually in runData folder @@ -99,19 +97,35 @@ % Loop through all the experimental files and plot the output mole fraction if ~isempty(experimentFiles) colorForPlot = {'5C73B9','7262C3','8852CD','9D41D7','B330E1'}; + f1 = figure('Units','inch','Position',[2 2 7 3.3]); for ii = 1:length(experimentFiles) - load([experimentFiles{ii},'_Output'],'experimentOutput'); + load([experimentFiles{ii},'_Output']); + % Plot the output from different experiments (in y and Ft plots) + figure(f1); subplot(1,2,1) semilogy(experimentOutput.timeExp,experimentOutput.moleFrac,'color',['#',colorForPlot{ii}]); hold on box on;grid on; xlim([0,500]); ylim([0,1]); - xlabel('t [s]'); ylabel('y [-]'); + xlabel('{\it{t}} [s]'); ylabel('{\it{y}} [-]'); + set(gca,'FontSize',8) + subplot(1,2,2) semilogy(experimentOutput.timeExp.*experimentOutput.totalFlowRate,experimentOutput.moleFrac,'color',['#',colorForPlot{ii}]); hold on xlim([0,100]); ylim([0,1]); - xlabel('Ft [cc]'); ylabel('y [-]'); + xlabel('{\it{Ft}} [cc]'); ylabel('{\it{y}} [-]'); + set(gca,'FontSize',8) + box on;grid on; + + % Plot data from different calibrations + figure('Units','inch','Position',[2 2 3.3 3.3]) + semilogy(semiProcessedStruct.flow(:,1),1-semiProcessedStruct.moleFracIndCalib); + hold on + semilogy(experimentOutput.timeExp,experimentOutput.moleFrac,'--k'); + xlim([0,500]); ylim([0,1]); + xlabel('{\it{t}} [s]'); ylabel('{\it{y}} [-]'); + set(gca,'FontSize',8) box on;grid on; end end \ No newline at end of file diff --git a/experimental/runMultipleZLC.m b/experimental/runMultipleZLC.m index 66ffd43..262e7ff 100644 --- a/experimental/runMultipleZLC.m +++ b/experimental/runMultipleZLC.m @@ -22,13 +22,13 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function runMultipleZLC % Series name for the experiments - expSeries = 'ZLC_ActivatedCarbon_Exp11'; + expSeries = 'ZLC_ActivatedCarbon_Exp25'; % Maximum time of the experiment - expInfo.maxTime = 3600; + expInfo.maxTime = 400; % Sampling time for the device expInfo.samplingTime = 1; % Intervals for collecting MFC data - expInfo.MFCInterval = 100; + expInfo.MFCInterval = 300; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -36,9 +36,9 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Total flow rate - expTotalFlowRate = [4 6 10 15 30]; + expTotalFlowRate = [10, 10, 10, 10, 10, 10]; % Fraction CO2 - fracCO2 = 0.05; + fracCO2 = [1/8 1/3 1 2 4 10]; % Define set point for MFC1 % Round the flow rate to the nearest first decimal (as this is the % resolution of the meter) @@ -46,9 +46,9 @@ % Define set point for MFC2 % Round the flow rate to the nearest first decimal (as this is the % resolution of the meter) - MFC2_SP = round(fracCO2*expTotalFlowRate,1); + MFC2_SP = round(fracCO2.*expTotalFlowRate,1); % Start delay (used for adsorbent equilibration) - equilibrationTime = [14400 10800 7200 7200 3600]; % [s] + equilibrationTime = [14400 3600 3600 3600 3600 3600]; % [s] % Flag for meter calibration expInfo.calibrateMeters = false; % Mixtures Flag - When a T junction instead of 6 way valve used @@ -65,4 +65,5 @@ % Wait for 1 min before starting the next experiment pause(30) end + defineSetPtManual(10,0) end From e474f62b9066ecacf9494facae1e06d8586e2f14 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 14 May 2021 17:20:02 +0100 Subject: [PATCH 105/189] Small fix --- plotFunctions/plotExperimentOutcome.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 917b64e..9d48f48 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -242,11 +242,11 @@ mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' # File name of the experiments fileName = [ - 'ZLC_ActivatedCarbon_Exp25A_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp25B_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp25C_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp25D_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp25E_Output_ba091f5.npz',] + 'ZLC_ActivatedCarbon_Exp24A_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp24B_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp24C_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp24D_Output_ba091f5.npz', + 'ZLC_ActivatedCarbon_Exp24E_Output_ba091f5.npz',] # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' @@ -328,11 +328,8 @@ # Compute the mass balance at the end end of the ZLC massBalanceALL[ii,0] = moleFracExp[0] - massBalanceALL[ii,1] = ((round(np.trapz(np.multiply(resultMat[3,:]*1e6, - resultMat[0,:]), - timeElapsedExp),2)*1e-6 - - volGas*moleFracExp[0])* - (pressureTotal/(8.314*temperature))/(massSorbent/1000)) + massBalanceALL[ii,1] = ((np.trapz(np.multiply(resultMat[3,:],resultMat[0,:]),timeElapsedExp) + - volGas*moleFracExp[0])*(pressureTotal/(8.314*temperature))/(massSorbent/1000)) # y - Linear scale ax1.semilogy(timeElapsedExp,moleFracExp, From 434a71d650e53bf648676857e234ed84b8a50261 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 14 May 2021 18:10:53 +0100 Subject: [PATCH 106/189] Add flow rate sweep functionality --- experimental/runMultipleZLC.m | 41 +++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/experimental/runMultipleZLC.m b/experimental/runMultipleZLC.m index 262e7ff..2fadef3 100644 --- a/experimental/runMultipleZLC.m +++ b/experimental/runMultipleZLC.m @@ -12,6 +12,7 @@ % Runs multiple ZLC experiments in series % % Last modified: +% - 2021-05-14, AK: Add flow rate sweep functionality % - 2021-04-20, AK: Add multiple equilibration time % - 2021-04-15, AK: Initial creation % @@ -22,7 +23,12 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function runMultipleZLC % Series name for the experiments - expSeries = 'ZLC_ActivatedCarbon_Exp25'; + expSeries = {'ZLC_ActivatedCarbon_Exp26',... + 'ZLC_ActivatedCarbon_Exp27',... + 'ZLC_ActivatedCarbon_Exp28',... + 'ZLC_ActivatedCarbon_Exp29',... + 'ZLC_ActivatedCarbon_Exp30',... + 'ZLC_ActivatedCarbon_Exp31'}; % Maximum time of the experiment expInfo.maxTime = 400; % Sampling time for the device @@ -36,9 +42,14 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Total flow rate - expTotalFlowRate = [10, 10, 10, 10, 10, 10]; + expTotalFlowRate = [10, 10, 10, 10, 10, 10;... + 15, 15, 15, 15, 15, 15;... + 30, 30, 30, 30, 30, 30;... + 45, 45, 45, 45, 45, 45;... + 60, 60, 60, 60, 60, 60;... + 80, 80, 80, 80, 80, 80]; % Fraction CO2 - fracCO2 = [1/8 1/3 1 2 4 10]; + fracCO2 = repmat([1/8 1/3 1 2 4 10],[length(expSeries),1]); % Define set point for MFC1 % Round the flow rate to the nearest first decimal (as this is the % resolution of the meter) @@ -48,22 +59,24 @@ % resolution of the meter) MFC2_SP = round(fracCO2.*expTotalFlowRate,1); % Start delay (used for adsorbent equilibration) - equilibrationTime = [14400 3600 3600 3600 3600 3600]; % [s] + equilibrationTime = repmat([7200 3600 3600 3600 3600 3600],[length(expSeries),1]); % [s] % Flag for meter calibration expInfo.calibrateMeters = false; % Mixtures Flag - When a T junction instead of 6 way valve used expInfo.runMixtures = true; % Loop through all setpoints to calibrate the meters - for ii=1:length(MFC1_SP) - % Experiment name - expInfo.expName = [expSeries,char(64+ii)]; - expInfo.equilibrationTime = equilibrationTime(ii); - expInfo.MFC1_SP = MFC1_SP(ii); - expInfo.MFC2_SP = MFC2_SP(ii); - % Run the setup for different calibrations - runZLC(expInfo) - % Wait for 1 min before starting the next experiment - pause(30) + for jj=1:size(MFC1_SP,1) + for ii=1:size(MFC1_SP,2) + % Experiment name + expInfo.expName = [expSeries{jj},char(64+ii)]; + expInfo.equilibrationTime = equilibrationTime(jj,ii); + expInfo.MFC1_SP = MFC1_SP(jj,ii); + expInfo.MFC2_SP = MFC2_SP(jj,ii); + % Run the setup for different calibrations + runZLC(expInfo) + % Wait for 1 min before starting the next experiment + pause(30) + end end defineSetPtManual(10,0) end From 13e5ff7e88d42451b3da743e9e912f19e5276e03 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Mon, 17 May 2021 14:16:22 +0100 Subject: [PATCH 107/189] Minor cosmetic changes for set point check and analysis --- experimental/analyzeExperimentWrapper.m | 17 +++++++++-------- experimental/runMultipleZLC.m | 8 ++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/experimental/analyzeExperimentWrapper.m b/experimental/analyzeExperimentWrapper.m index 2c9c448..db1f028 100644 --- a/experimental/analyzeExperimentWrapper.m +++ b/experimental/analyzeExperimentWrapper.m @@ -16,6 +16,7 @@ % experiment % % Last modified: +% - 2021-05-17, AK: Change MS interpolation flag % - 2021-05-10, AK: Cosmetic changes to plots % - 2021-05-10, AK: Initial creation % @@ -41,14 +42,14 @@ %%%% Experimet to be analyzed %%%% % List the experiments that have to be analyzed -msExpFile = 'ZLC_ActivatedCarbon_Exp25'; % Raw MS data file name +msExpFile = 'ZLC_ActivatedCarbon_Exp26_28'; % Raw MS data file name % Flow rate files for experiments -experimentFiles = {'ZLC_ActivatedCarbon_Exp25A',... - 'ZLC_ActivatedCarbon_Exp25B',... - 'ZLC_ActivatedCarbon_Exp25C',... - 'ZLC_ActivatedCarbon_Exp25D',... - 'ZLC_ActivatedCarbon_Exp25E',... - 'ZLC_ActivatedCarbon_Exp25F'}; +experimentFiles = {'ZLC_ActivatedCarbon_Exp26A',... + 'ZLC_ActivatedCarbon_Exp26B',... + 'ZLC_ActivatedCarbon_Exp26C',... + 'ZLC_ActivatedCarbon_Exp26D',... + 'ZLC_ActivatedCarbon_Exp26E',... + 'ZLC_ActivatedCarbon_Exp26F'}; % Initialize the name of the msRawFile to be used for all calibrations startInd = 1; @@ -84,7 +85,7 @@ experimentStruct.flow = experimentFiles{ii}; % Experimental flow file (.mat) experimentStruct.MS = [msFileDir,filesep,msExpFile,'.asc']; % Experimental MS file (.asc). Assumes name of file to be the date of the first flow rate experimentStruct.calibrationMS = msCalibrationFiles; % Experimental calibration file list - experimentStruct.interpMS = calibrationStruct.interpMS; % Flag for interpolating MS data, same as calibration + experimentStruct.interpMS = false; % Flag for interpolating flow data, to have a higher resolution for actual experiments experimentStruct.moleFracThreshold = 5e-3; % Threshold for cutting off data below a given mole fraction % Call the analyzeExperiment function to analyze the experimental data % using the calibration files given by msCalibrationFiles diff --git a/experimental/runMultipleZLC.m b/experimental/runMultipleZLC.m index 2fadef3..9427d79 100644 --- a/experimental/runMultipleZLC.m +++ b/experimental/runMultipleZLC.m @@ -12,6 +12,7 @@ % Runs multiple ZLC experiments in series % % Last modified: +% - 2021-05-17, AK: Add check for CO2 set point % - 2021-05-14, AK: Add flow rate sweep functionality % - 2021-04-20, AK: Add multiple equilibration time % - 2021-04-15, AK: Initial creation @@ -67,6 +68,13 @@ % Loop through all setpoints to calibrate the meters for jj=1:size(MFC1_SP,1) for ii=1:size(MFC1_SP,2) + % The MFC can support only 200 sccm (180 ccm is borderline) + % Keep an eye out + % If MFC2SP > 180 ccm break and move to the next operating + % condition + if MFC2_SP(jj,ii) >= 180 + break; + end % Experiment name expInfo.expName = [expSeries{jj},char(64+ii)]; expInfo.equilibrationTime = equilibrationTime(jj,ii); From a079f4ab336e91a5c821cf8998c5b03c43834432 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 24 May 2021 20:47:00 +0100 Subject: [PATCH 108/189] Improve information passing for parameter estimators --- experimental/computeMLEError.py | 6 +- experimental/extractDeadVolume.py | 71 ++++++++++---- experimental/extractZLCParameters.py | 142 ++++++++++++++++++--------- 3 files changed, 153 insertions(+), 66 deletions(-) diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py index 354351f..9020a60 100644 --- a/experimental/computeMLEError.py +++ b/experimental/computeMLEError.py @@ -12,6 +12,7 @@ # Computes the MLE error for ZLC experiments. # # Last modified: +# - 2021-05-24, AK: Add -inf input to avoid splitting compositions # - 2021-05-13, AK: Add different modes for MLE error computations # - 2021-05-05, AK: Initial creation # @@ -30,6 +31,10 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): if 'thresholdFactor' in kwargs: thresholdFlag = True thresholdFactor = np.array(kwargs["thresholdFactor"]) + # If negative infinity provided as a threshold, do not split and uses + # all data with equal weights + if np.isneginf(thresholdFactor): + thresholdFlag = False # Default is flag, uses all data with equal weights else: thresholdFlag = False @@ -42,7 +47,6 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): else: # Objective function error # Find error for mole fraction below a given threshold - thresholdFactor = 5e-2 lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) # Do downsampling if the number of points in higher and lower # compositions does not match diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 29a05dd..27472e7 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-05-24, AK: Improve information passing (for output) # - 2021-05-05, AK: Bug fix for MLE error computation # - 2021-05-05, AK: Bug fix for error computation # - 2021-05-04, AK: Modify error computation for dead volume @@ -48,32 +49,46 @@ def extractDeadVolume(): import multiprocessing # For parallel processing import socket - # Find out the total number of cores available for parallel processing - num_cores = multiprocessing.cpu_count() - - # Number of times optimization repeated - numOptRepeat = 5 - - # Get the commit ID of the current repository - gitCommitID = auxiliaryFunctions.getCommitID() - - # Get the current date and time for saving purposes - currentDT = auxiliaryFunctions.getCurrentDateTime() - # Change path directory # Assumes either running from ERASE or from experimental. Either ways # this has to be run from experimental if not os.getcwd().split(os.path.sep)[-1] == 'experimental': os.chdir("experimental") + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() - # Directory of raw data + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Find out the total number of cores available for parallel processing + num_cores = multiprocessing.cpu_count() + + ##################################### + ###### USER DEFINED PROPERTIES ###### + + # Number of times optimization repeated + numOptRepeat = 10 + + # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp16A_Output.mat', - 'ZLC_DeadVolume_Exp16B_Output.mat'] + fileName = ['ZLC_DeadVolume_Exp16B_Output.mat', + 'ZLC_DeadVolume_Exp16C_Output.mat', + 'ZLC_DeadVolume_Exp16D_Output.mat'] + + # Threshold factor (If -negative infinity not used, if not need a float) + # This is used to split the compositions into two distint regions + thresholdFactor = -np.inf + + ##################################### + ##################################### + + # Save the threshold factor to a dummy file (to pass through GA - IDIOTIC) + savez ('tempFittingParametersDV.npz',thresholdFactor=thresholdFactor) # Generate .npz file for python processing of the .mat file - filesToProcess(True,mainDir,fileName) + filesToProcess(True,mainDir,fileName,'DV') # Define the bounds and the type of the parameters to be optimized optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,10], @@ -121,8 +136,17 @@ def extractDeadVolume(): savez (savePath, modelOutput = model.output_dict, # Model output optBounds = optBounds, # Optimizer bounds algoParameters = algorithm_param, # Algorithm parameters + numOptRepeat = numOptRepeat, # Number of times optimization repeated fileName = fileName, # Names of file used for fitting + mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] hostName = socket.gethostname()) # Hostname of the computer) + + # Remove all the .npy files genereated from the .mat + # Load the names of the file to be used for estimating dead volume characteristics + filePath = filesToProcess(False,[],[],'DV') + # Loop over all available files + for ii in range(len(filePath)): + os.remove(filePath[ii]) # Return the optimized values return model.output_dict @@ -136,8 +160,11 @@ def deadVolObjectiveFunction(x): from computeMLEError import computeMLEError from numpy import load + # Load the threshold factor from the dummy file + thresholdFactor = load ('tempFittingParametersDV.npz')["thresholdFactor"] + # Load the names of the file to be used for estimating dead volume characteristics - filePath = filesToProcess(False,[],[]) + filePath = filesToProcess(False,[],[],'DV') numPointsExp = np.zeros(len(filePath)) for ii in range(len(filePath)): @@ -193,12 +220,12 @@ def deadVolObjectiveFunction(x): penaltyObj = 10000 # Compute the sum of the error for the difference between exp. and sim. and # add a penalty if needed (using MLE) - computedError = computeMLEError(moleFracExpALL,moleFracSimALL) + computedError = computeMLEError(moleFracExpALL,moleFracSimALL,thresholdFactor=thresholdFactor) return computedError + penaltyObj # func: filesToProcess # Loads .mat experimental file and processes it for python -def filesToProcess(initFlag,mainDir,fileName): +def filesToProcess(initFlag,mainDir,fileName,expType): from processExpMatFile import processExpMatFile from numpy import savez from numpy import load @@ -208,9 +235,11 @@ def filesToProcess(initFlag,mainDir,fileName): for ii in range(len(fileName)): savePath.append(processExpMatFile(mainDir, fileName[ii])) # Save the .npz file names in a dummy file - savez ('tempCreation.npz', savePath = savePath) + dummyFileName = 'tempCreation' + '_' + expType + '.npz' + savez (dummyFileName, savePath = savePath) # Returns the path of the .npz file to be used else: # Load the dummy file with file names for processing - savePath = load ('tempCreation.npz')["savePath"] + dummyFileName = 'tempCreation' + '_' + expType + '.npz' + savePath = load (dummyFileName)["savePath"] return savePath \ No newline at end of file diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 0ad7815..fa5ddcf 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-05-24, AK: Improve information passing (for output) # - 2021-05-13, AK: Change structure to input mass of adsorbent # - 2021-05-05, AK: Bug fix for MLE error computation # - 2021-05-05, AK: Modify error computation for dead volume @@ -44,37 +45,56 @@ def extractZLCParameters(): # this has to be run from experimental if not os.getcwd().split(os.path.sep)[-1] == 'experimental': os.chdir("experimental") + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() # Find out the total number of cores available for parallel processing num_cores = multiprocessing.cpu_count() + + ##################################### + ###### USER DEFINED PROPERTIES ###### # Isotherm model type modelType = 'SSL' # Number of times optimization repeated - numOptRepeat = 5 - - # Get the commit ID of the current repository - gitCommitID = auxiliaryFunctions.getCommitID() - - # Get the current date and time for saving purposes - currentDT = auxiliaryFunctions.getCurrentDateTime() + numOptRepeat = 10 # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp24A_Output.mat', - 'ZLC_ActivatedCarbon_Exp24B_Output.mat', - 'ZLC_ActivatedCarbon_Exp24C_Output.mat', - 'ZLC_ActivatedCarbon_Exp24D_Output.mat', - 'ZLC_ActivatedCarbon_Exp24E_Output.mat'] + fileName = ['ZLC_ActivatedCarbon_Exp34A_Output.mat', + 'ZLC_ActivatedCarbon_Exp34B_Output.mat', + 'ZLC_ActivatedCarbon_Exp34C_Output.mat', + 'ZLC_ActivatedCarbon_Exp34D_Output.mat', + 'ZLC_ActivatedCarbon_Exp34E_Output.mat', + 'ZLC_ActivatedCarbon_Exp34F_Output.mat'] + + # Dead volume model + deadVolumeFile = 'deadVolumeCharacteristics_20210521_1609_434a71d.npz' + + # Adsorbent properties + # Adsorbent density [kg/m3] + # This has to be the skeletal density + adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] + # Particle porosity + particleEpsilon = 0.61 + # Particle mass [g] + massSorbent = 0.0625 + + # Threshold factor (If -ngative infinity not used, if not need a float) + # This is used to split the compositions into two distint regions + thresholdFactor = -np.inf - # NOTE: Dead volume characteristics filename is hardcoded in - # simulateCombinedModel. This is because of the python GA function unable - # to pass arguments + ##################################### + ##################################### # Generate .npz file for python processing of the .mat file - filesToProcess(True,mainDir,fileName) + filesToProcess(True,mainDir,fileName,'ZLC') # Define the bounds and the type of the parameters to be optimized # Single-site Langmuir @@ -83,6 +103,7 @@ def extractZLCParameters(): [np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real']) problemDimension = len(optType) + isoRef = [10, 1e-5, 50e3, 100] # Reference for the isotherm parameters # Dual-site Langmuir elif modelType == 'DSL': optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1], @@ -91,6 +112,10 @@ def extractZLCParameters(): [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real','real','real']) problemDimension = len(optType) + isoRef = [10, 1e-5, 50e3, 10, 1e-5, 50e3, 100] # Reference for the isotherm parameters + + # Initialize the parameters used for ZLC fitting process + fittingParameters(True,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef,thresholdFactor) # Algorithm parameters for GA algorithm_param = {'max_num_iteration':5, @@ -101,8 +126,8 @@ def extractZLCParameters(): 'elit_ratio': 0.01, 'max_iteration_without_improv':None} - # Minimize an objective function to compute the dead volume and the number of - # tanks for the dead volume using GA + # Minimize an objective function to compute the equilibrium and kinetic + # parameters from ZLC experiments model = ga(function = ZLCObjectiveFunction, dimension=problemDimension, variable_type_mixed = optType, variable_boundaries = optBounds, @@ -133,9 +158,23 @@ def extractZLCParameters(): savez (savePath, modelOutput = model.output_dict, # Model output optBounds = optBounds, # Optimizer bounds algoParameters = algorithm_param, # Algorithm parameters + numOptRepeat = numOptRepeat, # Number of times optimization repeated fileName = fileName, # Names of file used for fitting - hostName = socket.gethostname()) # Hostname of the computer) - + deadVolumeFile = deadVolumeFile, # Dead volume file used for parameter estimation + adsorbentDensity = adsorbentDensity, # Adsorbent density [kg/m3] + particleEpsilon = particleEpsilon, # Particle voidage [-] + massSorbent = massSorbent, # Mass of sorbent [g] + parameterReference = isoRef, # Parameter references [-] + mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] + hostName = socket.gethostname()) # Hostname of the computer + + # Remove all the .npy files genereated from the .mat + # Load the names of the file to be used for estimating ZLC parameters + filePath = filesToProcess(False,[],[],'ZLC') + # Loop over all available files + for ii in range(len(filePath)): + os.remove(filePath[ii]) + # Return the optimized values return model.output_dict @@ -149,34 +188,19 @@ def ZLCObjectiveFunction(x): from simulateCombinedModel import simulateCombinedModel from computeMLEError import computeMLEError - # Dead volume model - deadVolumeFile = 'deadVolumeCharacteristics_20210511_1015_ebb447e.npz' + # Get the zlc parameters needed for the solver + deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor = fittingParameters(False,[],[],[],[],[],[]) - # Adsorbent density [kg/m3] - # This has to be the skeletal density - adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] - # Particle porosity - particleEpsilon = 0.61 - # Particle mass [g] - massSorbent = 0.0846 # Volume of sorbent material [m3] volSorbent = (massSorbent/1000)/adsorbentDensity - # Volume of gas chamber (dead volume) [m3] + # Volume of gas in pores [m3] volGas = volSorbent/(1-particleEpsilon)*particleEpsilon - # Reference for the isotherm parameters - # For SSL isotherm - if len(x) == 4: - isoRef = [10, 1e-5, 50e3, 100] - # For DSL isotherm - elif len(x) == 7: - isoRef = [10, 1e-5, 50e3, 10, 1e-5, 50e3, 100] - # Prepare isotherm model (the first n-1 parameters are for the isotherm model) isothermModel = np.multiply(x[0:-1],isoRef[0:-1]) - # Load the names of the file to be used for estimating dead volume characteristics - filePath = filesToProcess(False,[],[]) + # Load the names of the file to be used for estimating zlc parameters + filePath = filesToProcess(False,[],[],'ZLC') # Parse out number of data points for each experiment (for downsampling) numPointsExp = np.zeros(len(filePath)) @@ -208,14 +232,14 @@ def ZLCObjectiveFunction(x): # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) timeInt = timeElapsedExp - # Compute the dead volume response using the optimizer parameters + # Compute the composite response using the optimizer parameters _ , moleFracSim , _ = simulateCombinedModel(isothermModel = isothermModel, rateConstant = x[-1]*isoRef[-1], # Last element is rate constant timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) expFlag = True, - deadVolumeFile = deadVolumeFile, + deadVolumeFile = str(deadVolumeFile), volSorbent = volSorbent, volGas = volGas) @@ -225,5 +249,35 @@ def ZLCObjectiveFunction(x): moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) # Compute the sum of the error for the difference between exp. and sim. - computedError = computeMLEError(moleFracExpALL,moleFracSimALL) - return computedError \ No newline at end of file + computedError = computeMLEError(moleFracExpALL,moleFracSimALL,thresholdFactor=thresholdFactor) + return computedError + +# func: fittingParameters +# Parses dead volume calibration file, adsorbent density, voidage, mass to +# be used for parameter estimation, parameter references and threshold for MLE +# This is done because the ga cannot handle additional user inputs +def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef, thresholdFactor): + from numpy import savez + from numpy import load + # Process the data for python (if needed) + if initFlag: + # Save the necessary inputs to a temp file + dummyFileName = 'tempFittingParametersZLC.npz' + savez (dummyFileName, deadVolumeFile = deadVolumeFile, + adsorbentDensity=adsorbentDensity, + particleEpsilon=particleEpsilon, + massSorbent=massSorbent, + isoRef=isoRef, + thresholdFactor=thresholdFactor) + # Returns the path of the .npz file to be used + else: + # Load the dummy file with deadVolumeFile, adsorbent density, particle voidage, + # and mass of sorbent + dummyFileName = 'tempFittingParametersZLC.npz' + deadVolumeFile = load (dummyFileName)["deadVolumeFile"] + adsorbentDensity = load (dummyFileName)["adsorbentDensity"] + particleEpsilon = load (dummyFileName)["particleEpsilon"] + massSorbent = load (dummyFileName)["massSorbent"] + isoRef = load (dummyFileName)["isoRef"] + thresholdFactor = load (dummyFileName)["thresholdFactor"] + return deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor \ No newline at end of file From 7c0209e384f0fb7c0f8b4a57ebdc84755854db4a Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 25 May 2021 16:43:16 +0100 Subject: [PATCH 109/189] Add kinetic mode for estimation --- experimental/extractZLCParameters.py | 43 ++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index fa5ddcf..e9d1502 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-05-25, AK: Add kinetic mode for estimation # - 2021-05-24, AK: Improve information passing (for output) # - 2021-05-13, AK: Change structure to input mass of adsorbent # - 2021-05-05, AK: Bug fix for MLE error computation @@ -37,6 +38,7 @@ def extractZLCParameters(): import auxiliaryFunctions import os from numpy import savez + from numpy import load import multiprocessing # For parallel processing import socket @@ -77,6 +79,9 @@ def extractZLCParameters(): # Dead volume model deadVolumeFile = 'deadVolumeCharacteristics_20210521_1609_434a71d.npz' + # Isotherm model (if fitting only kinetic constant) + isothermFile = 'zlcParameters_20210525_1610_a079f4a.npz' + # Adsorbent properties # Adsorbent density [kg/m3] # This has to be the skeletal density @@ -104,6 +109,9 @@ def extractZLCParameters(): optType=np.array(['real','real','real','real']) problemDimension = len(optType) isoRef = [10, 1e-5, 50e3, 100] # Reference for the isotherm parameters + isothermFile = [] # Isotherm file is empty as it is fit + paramIso = [] # Isotherm parameters is empty as it is fit + # Dual-site Langmuir elif modelType == 'DSL': optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1], @@ -113,9 +121,26 @@ def extractZLCParameters(): optType=np.array(['real','real','real','real','real','real','real']) problemDimension = len(optType) isoRef = [10, 1e-5, 50e3, 10, 1e-5, 50e3, 100] # Reference for the isotherm parameters + isothermFile = [] # Isotherm file is empty as it is fit + paramIso = [] # Isotherm parameters is empty as it is fit + + # Kinetic constant only + elif modelType == 'Kinetic': + optBounds = np.array([[np.finfo(float).eps,1]]) + optType=np.array(['real']) + problemDimension = len(optType) + isoRef = [100] # Reference for the parameter (has to be a list) + # File with parameter estimates for isotherm (ZLC) + isothermDir = '..' + os.path.sep + 'simulationResults/' + modelOutputTemp = load(isothermDir+isothermFile, allow_pickle=True)["modelOutput"] + modelNonDim = modelOutputTemp[()]["variable"] + parameterRefTemp = load(isothermDir+isothermFile, allow_pickle=True)["parameterReference"] + # Get the isotherm parameters + paramIso = np.multiply(modelNonDim,parameterRefTemp) # Initialize the parameters used for ZLC fitting process - fittingParameters(True,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef,thresholdFactor) + fittingParameters(True,deadVolumeFile,adsorbentDensity,particleEpsilon, + massSorbent,isoRef,thresholdFactor,paramIso) # Algorithm parameters for GA algorithm_param = {'max_num_iteration':5, @@ -161,6 +186,7 @@ def extractZLCParameters(): numOptRepeat = numOptRepeat, # Number of times optimization repeated fileName = fileName, # Names of file used for fitting deadVolumeFile = deadVolumeFile, # Dead volume file used for parameter estimation + isothermFile = isothermFile, # Isotherm parameters file, if only kinetics estimated adsorbentDensity = adsorbentDensity, # Adsorbent density [kg/m3] particleEpsilon = particleEpsilon, # Particle voidage [-] massSorbent = massSorbent, # Mass of sorbent [g] @@ -189,7 +215,7 @@ def ZLCObjectiveFunction(x): from computeMLEError import computeMLEError # Get the zlc parameters needed for the solver - deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor = fittingParameters(False,[],[],[],[],[],[]) + deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso = fittingParameters(False,[],[],[],[],[],[],[]) # Volume of sorbent material [m3] volSorbent = (massSorbent/1000)/adsorbentDensity @@ -197,7 +223,10 @@ def ZLCObjectiveFunction(x): volGas = volSorbent/(1-particleEpsilon)*particleEpsilon # Prepare isotherm model (the first n-1 parameters are for the isotherm model) - isothermModel = np.multiply(x[0:-1],isoRef[0:-1]) + if len(paramIso) != 0: + isothermModel = paramIso[0:-1] # Use this if isotherm parameter provided (for kinetics only) + else: + isothermModel = np.multiply(x[0:-1],isoRef[0:-1]) # Use this if both equilibrium and kinetics is fit # Load the names of the file to be used for estimating zlc parameters filePath = filesToProcess(False,[],[],'ZLC') @@ -256,7 +285,7 @@ def ZLCObjectiveFunction(x): # Parses dead volume calibration file, adsorbent density, voidage, mass to # be used for parameter estimation, parameter references and threshold for MLE # This is done because the ga cannot handle additional user inputs -def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef, thresholdFactor): +def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef, thresholdFactor,paramIso): from numpy import savez from numpy import load # Process the data for python (if needed) @@ -268,7 +297,8 @@ def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,m particleEpsilon=particleEpsilon, massSorbent=massSorbent, isoRef=isoRef, - thresholdFactor=thresholdFactor) + thresholdFactor=thresholdFactor, + paramIso = paramIso) # Returns the path of the .npz file to be used else: # Load the dummy file with deadVolumeFile, adsorbent density, particle voidage, @@ -280,4 +310,5 @@ def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,m massSorbent = load (dummyFileName)["massSorbent"] isoRef = load (dummyFileName)["isoRef"] thresholdFactor = load (dummyFileName)["thresholdFactor"] - return deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor \ No newline at end of file + paramIso = load (dummyFileName)["paramIso"] + return deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso \ No newline at end of file From 61649be59415a05cb2f49bc5d92aa5afc25e7546 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 28 May 2021 11:30:24 +0100 Subject: [PATCH 110/189] Add DV model for MS --- experimental/extractDeadVolume.py | 58 ++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 27472e7..af5c10e 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-05-28, AK: Add model for MS # - 2021-05-24, AK: Improve information passing (for output) # - 2021-05-05, AK: Bug fix for MLE error computation # - 2021-05-05, AK: Bug fix for error computation @@ -73,9 +74,18 @@ def extractDeadVolume(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp16B_Output.mat', - 'ZLC_DeadVolume_Exp16C_Output.mat', - 'ZLC_DeadVolume_Exp16D_Output.mat'] + fileName = ['ZLC_DeadVolume_Exp19A_Output.mat', + 'ZLC_DeadVolume_Exp19B_Output.mat', + 'ZLC_DeadVolume_Exp19C_Output.mat', + 'ZLC_DeadVolume_Exp19D_Output.mat', + 'ZLC_DeadVolume_Exp19E_Output.mat'] + + # Fit MS data alone (implemented on 28.05.21) + # Flag to fit MS data + flagMSFit = True + # Flow rate through the MS capillary (determined by performing experiments) + # Pfeiffer Vaccum (in Ronny Pini's lab has 0.4 ccm) + msFlowRate = 0.4/60 # [ccs] # Threshold factor (If -negative infinity not used, if not need a float) # This is used to split the compositions into two distint regions @@ -85,15 +95,24 @@ def extractDeadVolume(): ##################################### # Save the threshold factor to a dummy file (to pass through GA - IDIOTIC) - savez ('tempFittingParametersDV.npz',thresholdFactor=thresholdFactor) + savez ('tempFittingParametersDV.npz',thresholdFactor=thresholdFactor, + flagMSFit = flagMSFit, msFlowRate = msFlowRate) # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName,'DV') - # Define the bounds and the type of the parameters to be optimized - optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,10], - [np.finfo(float).eps,10], [1,30], [np.finfo(float).eps,0.05])) - + # Define the bounds and the type of the parameters to be optimized + # MS does not have diffusive pockets, so setting the bounds for the diffusive + # volumes to be very very small + if flagMSFit: + optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,2*np.finfo(float).eps], + [np.finfo(float).eps,2*np.finfo(float).eps], [1,30], + [np.finfo(float).eps,2*np.finfo(float).eps])) + # When the actual dead volume is used, diffusive volume allowed to change + else: + optBounds = np.array(([np.finfo(float).eps,10], [np.finfo(float).eps,10], + [np.finfo(float).eps,10], [1,30], [np.finfo(float).eps,0.05])) + optType=np.array(['real','real','real','int','real']) # Algorithm parameters for GA algorithm_param = {'max_num_iteration':5, @@ -138,8 +157,10 @@ def extractDeadVolume(): algoParameters = algorithm_param, # Algorithm parameters numOptRepeat = numOptRepeat, # Number of times optimization repeated fileName = fileName, # Names of file used for fitting + flagMSFit = flagMSFit, # Flag to check if MS data is fit + msFlowRate = msFlowRate, # Flow rate through MS capillary [ccs] mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] - hostName = socket.gethostname()) # Hostname of the computer) + hostName = socket.gethostname()) # Hostname of the computer # Remove all the .npy files genereated from the .mat # Load the names of the file to be used for estimating dead volume characteristics @@ -160,8 +181,11 @@ def deadVolObjectiveFunction(x): from computeMLEError import computeMLEError from numpy import load - # Load the threshold factor from the dummy file + # Load the threshold factor, MS fit flag and MS flow rate from the dummy + # file thresholdFactor = load ('tempFittingParametersDV.npz')["thresholdFactor"] + flagMSFit = load ('tempFittingParametersDV.npz')["flagMSFit"] # Used only if MS data is fit + msFlowRate = load ('tempFittingParametersDV.npz')["msFlowRate"] # Used only if MS data is fit # Load the names of the file to be used for estimating dead volume characteristics filePath = filesToProcess(False,[],[],'DV') @@ -191,12 +215,20 @@ def deadVolObjectiveFunction(x): timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] - + + # Change the flow rate if fit only MS data + if flagMSFit: + flowRateDV = msFlowRate + else: + # Flow rate for dead volume considered the mean of last 10 points + # (to avoid delay issues) + flowRateDV = np.mean(flowRateExp[-1:-10:-1]) + # Integration and ode evaluation time (check simulateDeadVolume) timeInt = timeElapsedExp # Compute the experimental volume (using trapz) - expVolume = max([expVolume, np.trapz(moleFracExp,np.multiply(flowRateExp, timeElapsedExp))]) + expVolume = max([expVolume, np.trapz(moleFracExp,np.multiply(flowRateDV, timeElapsedExp))]) # Compute the dead volume response using the optimizer parameters _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = x[0], @@ -205,7 +237,7 @@ def deadVolObjectiveFunction(x): numTanks_1 = int(x[3]), flowRate_D = x[4], timeInt = timeInt, - flowRate = flowRateExp, + flowRate = flowRateDV, expFlag = True) # Stack mole fraction from experiments and simulation for error From 318b280c44e47198ef766003a0e7b23d9e6e0bdd Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 28 May 2021 12:45:47 +0100 Subject: [PATCH 111/189] Combine dead volume and MS model --- experimental/deadVolumeWrapper.py | 69 +++++++++++++++++++++++++++++++ experimental/extractDeadVolume.py | 36 +++++++++------- 2 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 experimental/deadVolumeWrapper.py diff --git a/experimental/deadVolumeWrapper.py b/experimental/deadVolumeWrapper.py new file mode 100644 index 0000000..6229bef --- /dev/null +++ b/experimental/deadVolumeWrapper.py @@ -0,0 +1,69 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Simulates the dead volume of the ZLC setup. The function can either simualte +# one lumped dead volume or can simulate a cascade of dead volume and MS +# +# Last modified: +# - 2021-05-28, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def deadVolumeWrapper(timeInt, flowRateDV, DV_p, flagMSDeadVolume, msDeadVolumeFile): + import os + from numpy import load + from simulateDeadVolume import simulateDeadVolume + + # Simulates the tubings and fittings + # Compute the dead volume response using the dead volume parameters input + timeDV , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = DV_p[0], + deadVolume_2M = DV_p[1], + deadVolume_2D = DV_p[2], + numTanks_1 = int(DV_p[3]), + flowRate_D = DV_p[4], + timeInt = timeInt, + flowRate = flowRateDV, + expFlag = True) + + # Simulates the MS response + # Pass the mole fractions to the MS model (this uses a completely + # different flow rate) and simulate the model + if flagMSDeadVolume: + # File with parameter estimates for the MS dead volume + msDeadVolumeDir = '..' + os.path.sep + 'simulationResults/' + modelOutputTemp = load(msDeadVolumeDir+str(msDeadVolumeFile), allow_pickle=True)["modelOutput"] + # Parse out dead volume parameters + msDV_p = modelOutputTemp[()]["variable"] + # Get the MS flow rate + msFlowRate = load(msDeadVolumeDir+str(msDeadVolumeFile))["msFlowRate"] + + _ , _ , moleFracMS = simulateDeadVolume(deadVolume_1 = msDV_p[0], + deadVolume_2M = msDV_p[1], + deadVolume_2D = msDV_p[2], + numTanks_1 = int(msDV_p[3]), + flowRate_D = msDV_p[4], + initMoleFrac = moleFracSim[0], + feedMoleFrac = moleFracSim, + timeInt = timeDV, + flowRate = msFlowRate, + expFlag = True) + + moleFracSim = [] # Initialize moleFracSim to empty + moleFracSim = moleFracMS # Set moleFracSim to moleFrac MS for error computation + + # Return moleFracSim to the top function + return moleFracSim \ No newline at end of file diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index af5c10e..12c8a5f 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-05-28, AK: Add the existing DV model with MS DV model and structure change # - 2021-05-28, AK: Add model for MS # - 2021-05-24, AK: Improve information passing (for output) # - 2021-05-05, AK: Bug fix for MLE error computation @@ -86,6 +87,14 @@ def extractDeadVolume(): # Flow rate through the MS capillary (determined by performing experiments) # Pfeiffer Vaccum (in Ronny Pini's lab has 0.4 ccm) msFlowRate = 0.4/60 # [ccs] + + # MS dead volume model + msDeadVolumeFile = [] # DO NOT CHANGE (initialization) + flagMSDeadVolume = False # It should be the opposite of flagMSfit (if used) + # If MS dead volume used separately, use the file defined here with ms + # parameters + if flagMSDeadVolume: + msDeadVolumeFile = 'deadVolumeCharacteristics_20210528_1110_7c0209e.npz' # Threshold factor (If -negative infinity not used, if not need a float) # This is used to split the compositions into two distint regions @@ -94,9 +103,11 @@ def extractDeadVolume(): ##################################### ##################################### - # Save the threshold factor to a dummy file (to pass through GA - IDIOTIC) + # Save the parameters to be used for fitting to a dummy file (to pass + # through GA - IDIOTIC) savez ('tempFittingParametersDV.npz',thresholdFactor=thresholdFactor, - flagMSFit = flagMSFit, msFlowRate = msFlowRate) + flagMSFit = flagMSFit, msFlowRate = msFlowRate, + flagMSDeadVolume = flagMSDeadVolume, msDeadVolumeFile = msDeadVolumeFile) # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName,'DV') @@ -159,6 +170,8 @@ def extractDeadVolume(): fileName = fileName, # Names of file used for fitting flagMSFit = flagMSFit, # Flag to check if MS data is fit msFlowRate = msFlowRate, # Flow rate through MS capillary [ccs] + flagMSDeadVolume = flagMSDeadVolume, # Flag for checking if ms dead volume used + msDeadVolumeFile = msDeadVolumeFile, # MS dead volume parameter file mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] hostName = socket.gethostname()) # Hostname of the computer @@ -177,8 +190,8 @@ def extractDeadVolume(): # optimizer) def deadVolObjectiveFunction(x): import numpy as np - from simulateDeadVolume import simulateDeadVolume from computeMLEError import computeMLEError + from deadVolumeWrapper import deadVolumeWrapper from numpy import load # Load the threshold factor, MS fit flag and MS flow rate from the dummy @@ -186,6 +199,8 @@ def deadVolObjectiveFunction(x): thresholdFactor = load ('tempFittingParametersDV.npz')["thresholdFactor"] flagMSFit = load ('tempFittingParametersDV.npz')["flagMSFit"] # Used only if MS data is fit msFlowRate = load ('tempFittingParametersDV.npz')["msFlowRate"] # Used only if MS data is fit + flagMSDeadVolume = load ('tempFittingParametersDV.npz')["flagMSDeadVolume"] # Used only if MS dead volume model is separate + msDeadVolumeFile = load ('tempFittingParametersDV.npz')["msDeadVolumeFile"] # Load the ms dead volume parameter file # Load the names of the file to be used for estimating dead volume characteristics filePath = filesToProcess(False,[],[],'DV') @@ -229,22 +244,15 @@ def deadVolObjectiveFunction(x): # Compute the experimental volume (using trapz) expVolume = max([expVolume, np.trapz(moleFracExp,np.multiply(flowRateDV, timeElapsedExp))]) + + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + moleFracSim = deadVolumeWrapper(timeInt, flowRateDV, x, flagMSDeadVolume, msDeadVolumeFile) - # Compute the dead volume response using the optimizer parameters - _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = x[0], - deadVolume_2M = x[1], - deadVolume_2D = x[2], - numTanks_1 = int(x[3]), - flowRate_D = x[4], - timeInt = timeInt, - flowRate = flowRateDV, - expFlag = True) - # Stack mole fraction from experiments and simulation for error # computation moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) - + # Penalize if the total volume of the system is greater than experiemntal # volume penaltyObj = 0 From 664b02cb5c08caf86bb1a1e9d53b2f969ae50cbd Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sat, 29 May 2021 10:51:32 +0100 Subject: [PATCH 112/189] Add MS dead volume model to combined model --- experimental/deadVolumeWrapper.py | 18 ++++++++++++++- experimental/simulateCombinedModel.py | 32 ++++++++++++++++----------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/experimental/deadVolumeWrapper.py b/experimental/deadVolumeWrapper.py index 6229bef..7690432 100644 --- a/experimental/deadVolumeWrapper.py +++ b/experimental/deadVolumeWrapper.py @@ -13,6 +13,7 @@ # one lumped dead volume or can simulate a cascade of dead volume and MS # # Last modified: +# - 2021-05-29, AK: Add optional arguments for combind model # - 2021-05-28, AK: Initial creation # # Input arguments: @@ -23,11 +24,24 @@ # ############################################################################ -def deadVolumeWrapper(timeInt, flowRateDV, DV_p, flagMSDeadVolume, msDeadVolumeFile): +def deadVolumeWrapper(timeInt, flowRateDV, DV_p, flagMSDeadVolume, + msDeadVolumeFile, **kwargs): import os from numpy import load from simulateDeadVolume import simulateDeadVolume + # Initial Gas Mole Fraction [-] + if 'initMoleFrac' in kwargs: + initMoleFrac = kwargs["initMoleFrac"] + else: + initMoleFrac = [1.] + + # Feed Gas Mole Fraction [-] + if 'feedMoleFrac' in kwargs: + feedMoleFrac = kwargs["feedMoleFrac"] + else: + feedMoleFrac = [0.] + # Simulates the tubings and fittings # Compute the dead volume response using the dead volume parameters input timeDV , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = DV_p[0], @@ -35,6 +49,8 @@ def deadVolumeWrapper(timeInt, flowRateDV, DV_p, flagMSDeadVolume, msDeadVolumeF deadVolume_2D = DV_p[2], numTanks_1 = int(DV_p[3]), flowRate_D = DV_p[4], + initMoleFrac = initMoleFrac, + feedMoleFrac = feedMoleFrac, timeInt = timeInt, flowRate = flowRateDV, expFlag = True) diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index db1d8cb..8538718 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -14,6 +14,7 @@ # volume simulator # # Last modified: +# - 2021-05-29, AK: Add a separate MS dead volume # - 2021-05-13, AK: Add volumes and density as inputs # - 2021-04-27, AK: Convert to a function for parameter estimation # - 2021-04-22, AK: Initial creation @@ -26,9 +27,8 @@ ############################################################################ def simulateCombinedModel(**kwargs): - import numpy as np from simulateZLC import simulateZLC - from simulateDeadVolume import simulateDeadVolume + from deadVolumeWrapper import deadVolumeWrapper from numpy import load import os @@ -126,17 +126,23 @@ def simulateCombinedModel(**kwargs): modelOutputTemp = load(deadVolumeDir+deadVolumeFile, allow_pickle=True)["modelOutput"] # Parse out dead volume parameters x = modelOutputTemp[()]["variable"] - # Call the simulateDeadVolume function to simulate the dead volume of the setup - _ , _ , moleFracOut = simulateDeadVolume(timeInt = timeZLC, - initMoleFrac = moleFracZLC[0], - feedMoleFrac = moleFracZLC, - flowRate = flowRateZLC, - expFlag = True, # Note this is true as flow rate from ZLC used - deadVolume_1 = x[0], - deadVolume_2M = x[1], - deadVolume_2D = x[2], - numTanks_1 = int(x[3]), - flowRate_D = x[4]) + + # Get the MS dead volume file, if available + # Additionally, load the flag that will be used in deadVolumeWrapper + # This is back compatible with older versions without MS model + dvFileLoadTemp = load(deadVolumeDir+deadVolumeFile) + # With MS Model + if 'flagMSDeadVolume' in dvFileLoadTemp.files: + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + # Without MS Model + else: + flagMSDeadVolume = False + msDeadVolumeFile = [] + + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + moleFracOut = deadVolumeWrapper(timeZLC, flowRateZLC, x, flagMSDeadVolume, msDeadVolumeFile, + initMoleFrac = moleFracZLC[0], feedMoleFrac = moleFracZLC) # Plot results if needed if plotFlag: From 3a2c7f2ba3f11e190317e21c6304a9a79d8732d4 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sat, 29 May 2021 11:16:23 +0100 Subject: [PATCH 113/189] Experimental plotting fixes and structure changes --- plotFunctions/plotExperimentOutcome.py | 140 +++++++++++++++---------- 1 file changed, 87 insertions(+), 53 deletions(-) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 9d48f48..301c115 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-05-14, AK: Fixes and structure changes # - 2021-05-14, AK: Improve plotting capabilities # - 2021-05-05, AK: Bug fix for MLE error computation # - 2021-05-04, AK: Bug fix for error computation @@ -28,9 +29,10 @@ ############################################################################ import numpy as np -from simulateDeadVolume import simulateDeadVolume from computeMLEError import computeMLEError from computeEquilibriumLoading import computeEquilibriumLoading +from deadVolumeWrapper import deadVolumeWrapper +from extractDeadVolume import filesToProcess # File processing script from numpy import load import os import matplotlib.pyplot as plt @@ -66,7 +68,7 @@ particleEpsilon = 0.61 # Particle mass [g] -massSorbent = 0.0846 +massSorbent = 0.0645 # Volume of sorbent material [m3] volSorbent = (massSorbent/1000)/adsorbentDensity @@ -92,18 +94,38 @@ if flagDeadVolume: # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp16B_Output_ba091f5.npz', - 'ZLC_DeadVolume_Exp16C_Output_ba091f5.npz', - 'ZLC_DeadVolume_Exp16D_Output_ba091f5.npz'] + rawFileName = ['ZLC_DeadVolume_Exp16B_Output.mat', + 'ZLC_DeadVolume_Exp16C_Output.mat', + 'ZLC_DeadVolume_Exp16D_Output.mat'] + # Generate .npz file for python processing of the .mat file + filesToProcess(True,mainDir,rawFileName,'DV') + # Get the processed file names + fileName = filesToProcess(False,[],[],'DV') + # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210511_1203_ebb447e.npz' + fileParameter = 'deadVolumeCharacteristics_20210528_1319_318b280.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] - + + # Get the MS fit flag, flow rates and msDeadVolumeFile (if needed) + # Check needs to be done to see if MS file available or not + # Checked using flagMSDeadVolume in the saved file + dvFileLoadTemp = load(simulationDir+fileParameter) + if 'flagMSDeadVolume' in dvFileLoadTemp.files: + flagMSFit = dvFileLoadTemp["flagMSFit"] + msFlowRate = dvFileLoadTemp["msFlowRate"] + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + else: + flagMSFit = False + msFlowRate = -np.inf + flagMSDeadVolume = False + msDeadVolumeFile = [] + numPointsExp = np.zeros(len(fileName)) for ii in range(len(fileName)): - fileToLoad = mainDir + fileName[ii] + fileToLoad = fileName[ii] # Load experimental molefraction timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() numPointsExp[ii] = len(timeElapsedExp) @@ -121,15 +143,15 @@ # Create the instance for the plots fig = plt.figure - ax1 = plt.subplot(1,3,1) - ax2 = plt.subplot(1,3,2) + ax1 = plt.subplot(1,2,1) + ax2 = plt.subplot(1,2,2) # Initialize error for objective function # Loop over all available files for ii in range(len(fileName)): # Initialize outputs moleFracSim = [] # Path of the file name - fileToLoad = mainDir + fileName[ii] + fileToLoad = fileName[ii] # Load experimental time, molefraction and flowrate (accounting for downsampling) timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() @@ -137,6 +159,12 @@ timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + # Get the flow rates from the fit file + # When MS used + if flagMSFit: + flowRateDV = msFlowRate + else: + flowRateDV = np.mean(flowRateExp[-1:-10:-1]) # Integration and ode evaluation time timeInt = timeElapsedExp @@ -144,17 +172,10 @@ # Print experimental volume print("Experiment",str(ii+1),round(np.trapz(moleFracExp, np.multiply(flowRateExp,timeElapsedExp)),2)) - if simulateModel: - # Compute the dead volume response using the optimizer parameters - _ , _ , moleFracSim = simulateDeadVolume(deadVolume_1 = x[0], - deadVolume_2M = x[1], - deadVolume_2D = x[2], - numTanks_1 = int(x[3]), - flowRate_D = x[4], - timeInt = timeInt, - flowRate = flowRateExp, - expFlag = True) - + if simulateModel: + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + moleFracSim = deadVolumeWrapper(timeInt, flowRateDV, x, flagMSDeadVolume, msDeadVolumeFile) + # Print simulation volume print("Simulation",str(ii+1),round(np.trapz(moleFracSim, np.multiply(flowRateExp, @@ -175,7 +196,9 @@ color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$t$ [s]', ylabel='$y_1$ [-]', - xlim = [0,100], ylim = [0, 1]) + xlim = [0,150], ylim = [0, 1]) + ax1.locator_params(axis="x", nbins=5) + ax1.locator_params(axis="y", nbins=5) ax1.legend() # Log scale @@ -186,7 +209,9 @@ ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', - xlim = [0,150], ylim = [5e-3, 1]) + xlim = [0,150], ylim = [5e-3, 1]) + ax2.locator_params(axis="x", nbins=5) + # Save the figure if saveFlag: @@ -199,26 +224,26 @@ plt.savefig (savePath) else: # Linear scale - ax1.plot(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, + ax1.plot(np.multiply(flowRateDV,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateDV),2))+" ccs") # Experimental response if simulateModel: - ax1.plot(np.multiply(flowRateExp,timeElapsedExp),moleFracSim, + ax1.plot(np.multiply(flowRateDV,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$Ft$ [cc]', ylabel='$y_1$ [-]', - xlim = [0,50], ylim = [0, 1]) + xlim = [0,0.1], ylim = [0, 1]) ax1.legend() # Log scale - ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, + ax2.semilogy(np.multiply(flowRateDV,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateDV),2))+" ccs") # Experimental response if simulateModel: - ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracSim, + ax2.semilogy(np.multiply(flowRateDV,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$Ft$ [cc]', - xlim = [0,100], ylim = [1e-3, 1]) + xlim = [0,0.1], ylim = [1e-3, 1]) ax2.legend() # Save the figure @@ -235,38 +260,42 @@ computedError = computeMLEError(moleFracExpALL,moleFracSimALL) print(round(computedError,0)) + # Remove all the .npy files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) + + else: from simulateCombinedModel import simulateCombinedModel # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' # File name of the experiments - fileName = [ - 'ZLC_ActivatedCarbon_Exp24A_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp24B_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp24C_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp24D_Output_ba091f5.npz', - 'ZLC_ActivatedCarbon_Exp24E_Output_ba091f5.npz',] - + rawFileName = ['ZLC_ActivatedCarbon_Exp36A_Output.mat', + 'ZLC_ActivatedCarbon_Exp36D_Output.mat'] + # Generate .npz file for python processing of the .mat file + filesToProcess(True,mainDir,rawFileName,'ZLC') + # Get the processed file names + fileName = filesToProcess(False,[],[],'ZLC') # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' # Dead volume model - deadVolumeFile = 'deadVolumeCharacteristics_20210511_1015_ebb447e.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210528_1319_318b280.npz' # ZLC parameter model - fileParameter = 'zlcParameters_20210513_2239_ba091f5.npz' + fileParameter = 'zlcParameters_20210525_2337_7c0209e.npz' modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] print("Objective Function",round(modelOutputTemp[()]["function"],0)) modelNonDim = modelOutputTemp[()]["variable"] numPointsExp = np.zeros(len(fileName)) for ii in range(len(fileName)): - fileToLoad = mainDir + fileName[ii] + fileToLoad = fileName[ii] # Load experimental molefraction timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() numPointsExp[ii] = len(timeElapsedExp) # Downsample intervals downsampleInt = numPointsExp/np.min(numPointsExp) - # Multiply the paremeters by the reference values (for SSL) x = np.multiply(modelNonDim,[10, 1e-5, 50e3,100]) @@ -288,7 +317,7 @@ # Loop over all available files for ii in range(len(fileName)): - fileToLoad = mainDir + fileName[ii] + fileToLoad = fileName[ii] # Initialize outputs moleFracSim = [] @@ -334,31 +363,31 @@ # y - Linear scale ax1.semilogy(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.25) # Experimental response + color=colorsForPlot[ii],alpha=0.15) # Experimental response if simulateModel: ax1.plot(timeElapsedExp,moleFracSim, color=colorsForPlot[ii],label=str(round(np.mean(flowRateExp),2))+" ccs") # Simulation response ax1.set(xlabel='$t$ [s]', ylabel='$y_1$ [-]', - xlim = [0,300], ylim = [5e-3, 1]) + xlim = [0,150], ylim = [5e-3, 1]) ax1.locator_params(axis="x", nbins=4) ax1.legend() # Ft - Log scale ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.25) # Experimental response + color=colorsForPlot[ii],alpha=0.15) # Experimental response if simulateModel: - ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracSim, - color=colorsForPlot[ii],label=str(round(np.mean(flowRateExp),2))+" ccs") # Simulation response + ax2.semilogy(np.multiply(resultMat[3,:]*1e6,timeElapsedExp),moleFracSim, + color=colorsForPlot[ii],label=str(round(np.mean(resultMat[3,:]*1e6),2))+" ccs") # Simulation response ax2.set(xlabel='$Ft$ [cc]', - xlim = [0,100], ylim = [5e-3, 1]) + xlim = [0,20], ylim = [5e-3, 1]) ax2.locator_params(axis="x", nbins=4) # Flow rates ax3.plot(timeElapsedExp,flowRateExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.25,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.05,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax3.plot(timeElapsedExp,resultMat[3,:]*1e6, color=colorsForPlot[ii]) # Simulation response @@ -407,7 +436,7 @@ ax1.scatter(massBalanceALL[:,0],massBalanceALL[:,1],c='dimgrey') ax1.set(xlabel='$P$ [bar]', ylabel='$q^*$ [mol kg$^\mathregular{-1}$]', - xlim = [0,1], ylim = [0, 3]) + xlim = [0,0.8], ylim = [0, 3]) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.legend() @@ -421,4 +450,9 @@ os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) - plt.show() \ No newline at end of file + plt.show() + + # Remove all the .npy files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) \ No newline at end of file From 25dbed09e53c1e35f50c4f0d538cf0cf300463bf Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 1 Jun 2021 20:19:19 +0100 Subject: [PATCH 114/189] Add temperature as an input for parameter estimation --- experimental/extractZLCParameters.py | 34 +++++++++++++++++---------- experimental/simulateCombinedModel.py | 21 +++++++++++++---- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index e9d1502..93a553b 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-06-01, AK: Add temperature as an input # - 2021-05-25, AK: Add kinetic mode for estimation # - 2021-05-24, AK: Improve information passing (for output) # - 2021-05-13, AK: Change structure to input mass of adsorbent @@ -69,15 +70,18 @@ def extractZLCParameters(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp34A_Output.mat', - 'ZLC_ActivatedCarbon_Exp34B_Output.mat', - 'ZLC_ActivatedCarbon_Exp34C_Output.mat', - 'ZLC_ActivatedCarbon_Exp34D_Output.mat', - 'ZLC_ActivatedCarbon_Exp34E_Output.mat', - 'ZLC_ActivatedCarbon_Exp34F_Output.mat'] + fileName = ['ZLC_ActivatedCarbon_Exp39A_Output.mat', + 'ZLC_ActivatedCarbon_Exp39B_Output.mat', + 'ZLC_ActivatedCarbon_Exp39C_Output.mat', + 'ZLC_ActivatedCarbon_Exp39D_Output.mat', + 'ZLC_ActivatedCarbon_Exp39E_Output.mat', + 'ZLC_ActivatedCarbon_Exp39F_Output.mat'] + + # Temperature (for each experiment) + temperature = [313.15, 313.15, 313.15, 313.15, 313.15, 313.15] # Dead volume model - deadVolumeFile = 'deadVolumeCharacteristics_20210521_1609_434a71d.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210528_1319_318b280.npz' # Isotherm model (if fitting only kinetic constant) isothermFile = 'zlcParameters_20210525_1610_a079f4a.npz' @@ -139,7 +143,7 @@ def extractZLCParameters(): paramIso = np.multiply(modelNonDim,parameterRefTemp) # Initialize the parameters used for ZLC fitting process - fittingParameters(True,deadVolumeFile,adsorbentDensity,particleEpsilon, + fittingParameters(True,temperature,deadVolumeFile,adsorbentDensity,particleEpsilon, massSorbent,isoRef,thresholdFactor,paramIso) # Algorithm parameters for GA @@ -185,6 +189,7 @@ def extractZLCParameters(): algoParameters = algorithm_param, # Algorithm parameters numOptRepeat = numOptRepeat, # Number of times optimization repeated fileName = fileName, # Names of file used for fitting + temperature = temperature, # Temperature [K] deadVolumeFile = deadVolumeFile, # Dead volume file used for parameter estimation isothermFile = isothermFile, # Isotherm parameters file, if only kinetics estimated adsorbentDensity = adsorbentDensity, # Adsorbent density [kg/m3] @@ -215,7 +220,7 @@ def ZLCObjectiveFunction(x): from computeMLEError import computeMLEError # Get the zlc parameters needed for the solver - deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso = fittingParameters(False,[],[],[],[],[],[],[]) + temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso = fittingParameters(False,[],[],[],[],[],[],[],[]) # Volume of sorbent material [m3] volSorbent = (massSorbent/1000)/adsorbentDensity @@ -264,6 +269,7 @@ def ZLCObjectiveFunction(x): # Compute the composite response using the optimizer parameters _ , moleFracSim , _ = simulateCombinedModel(isothermModel = isothermModel, rateConstant = x[-1]*isoRef[-1], # Last element is rate constant + temperature = temperature[ii], # Temperature [K] timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) @@ -285,14 +291,15 @@ def ZLCObjectiveFunction(x): # Parses dead volume calibration file, adsorbent density, voidage, mass to # be used for parameter estimation, parameter references and threshold for MLE # This is done because the ga cannot handle additional user inputs -def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef, thresholdFactor,paramIso): +def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef, thresholdFactor,paramIso): from numpy import savez from numpy import load # Process the data for python (if needed) if initFlag: # Save the necessary inputs to a temp file dummyFileName = 'tempFittingParametersZLC.npz' - savez (dummyFileName, deadVolumeFile = deadVolumeFile, + savez (dummyFileName, temperature = temperature, + deadVolumeFile = deadVolumeFile, adsorbentDensity=adsorbentDensity, particleEpsilon=particleEpsilon, massSorbent=massSorbent, @@ -301,9 +308,10 @@ def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,m paramIso = paramIso) # Returns the path of the .npz file to be used else: - # Load the dummy file with deadVolumeFile, adsorbent density, particle voidage, + # Load the dummy file with temperature, deadVolumeFile, adsorbent density, particle voidage, # and mass of sorbent dummyFileName = 'tempFittingParametersZLC.npz' + temperature = load (dummyFileName)["temperature"] deadVolumeFile = load (dummyFileName)["deadVolumeFile"] adsorbentDensity = load (dummyFileName)["adsorbentDensity"] particleEpsilon = load (dummyFileName)["particleEpsilon"] @@ -311,4 +319,4 @@ def fittingParameters(initFlag,deadVolumeFile,adsorbentDensity,particleEpsilon,m isoRef = load (dummyFileName)["isoRef"] thresholdFactor = load (dummyFileName)["thresholdFactor"] paramIso = load (dummyFileName)["paramIso"] - return deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso \ No newline at end of file + return temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso \ No newline at end of file diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index 8538718..ac3a9b5 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -14,6 +14,7 @@ # volume simulator # # Last modified: +# - 2021-06-01, AK: Add temperature as an input # - 2021-05-29, AK: Add a separate MS dead volume # - 2021-05-13, AK: Add volumes and density as inputs # - 2021-04-27, AK: Convert to a function for parameter estimation @@ -31,6 +32,7 @@ def simulateCombinedModel(**kwargs): from deadVolumeWrapper import deadVolumeWrapper from numpy import load import os + import numpy as np # Move to top level folder (to avoid path issues) os.chdir("..") @@ -44,7 +46,7 @@ def simulateCombinedModel(**kwargs): # Plot flag plotFlag = False - + # Isotherm model parameters (SSL or DSL) if 'isothermModel' in kwargs: isothermModel = kwargs["isothermModel"] @@ -58,6 +60,12 @@ def simulateCombinedModel(**kwargs): rateConstant = kwargs["rateConstant"] else: rateConstant = [0.3] + + # Temperature of the gas [K] + if 'temperature' in kwargs: + temperature = np.array(kwargs["temperature"]); + else: + temperature = np.array([298.15]); # Feed flow rate [m3/s] if 'flowIn' in kwargs: @@ -70,34 +78,38 @@ def simulateCombinedModel(**kwargs): initMoleFrac = kwargs["initMoleFrac"] else: initMoleFrac = [1.] - + # Time span for integration [tuple with t0 and tf] if 'timeInt' in kwargs: timeInt = kwargs["timeInt"] else: - timeInt = (0.0,300) + timeInt = (0.0,300) + # Volume of sorbent material [m3] if 'volSorbent' in kwargs: volSorbent = kwargs["volSorbent"] else: volSorbent = 4.35e-8 + # Volume of gas chamber (dead volume) [m3] if 'volGas' in kwargs: volGas = kwargs["volGas"] else: volGas = 6.81e-8 + # Adsorbent density [kg/m3] # This has to be the skeletal density if 'adsorbentDensity' in kwargs: adsorbentDensity = kwargs["adsorbentDensity"] else: adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] + # File name with dead volume characteristics parameters if 'deadVolumeFile' in kwargs: deadVolumeFile = kwargs["deadVolumeFile"] else: deadVolumeFile = 'deadVolumeCharacteristics_20210511_1015_ebb447e.npz' - + # Flag to check if experimental data used if 'expFlag' in kwargs: expFlag = kwargs["expFlag"] @@ -107,6 +119,7 @@ def simulateCombinedModel(**kwargs): # Call the simulateZLC function to simulate the sorption in a given sorbent timeZLC, resultMat, _ = simulateZLC(isothermModel=isothermModel, rateConstant=rateConstant, + temperature = temperature, flowIn = flowIn, initMoleFrac = initMoleFrac, timeInt = timeInt, From 7f84aab23cd235f3d4c60c8c01976c1781c612b4 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 2 Jun 2021 16:18:35 +0100 Subject: [PATCH 115/189] Minor functional changes to GA --- experimental/extractDeadVolume.py | 9 +++++---- experimental/extractZLCParameters.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 12c8a5f..340554d 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -70,7 +70,7 @@ def extractDeadVolume(): ###### USER DEFINED PROPERTIES ###### # Number of times optimization repeated - numOptRepeat = 10 + numOptRepeat = 5 # Directory of raw data mainDir = 'runData' @@ -126,8 +126,8 @@ def extractDeadVolume(): optType=np.array(['real','real','real','int','real']) # Algorithm parameters for GA - algorithm_param = {'max_num_iteration':5, - 'population_size':1600, + algorithm_param = {'max_num_iteration':30, + 'population_size':200, 'mutation_probability':0.1, 'crossover_probability': 0.55, 'parents_portion': 0.15, @@ -139,7 +139,8 @@ def extractDeadVolume(): model = ga(function = deadVolObjectiveFunction, dimension=5, variable_type_mixed = optType, variable_boundaries = optBounds, - algorithm_parameters=algorithm_param) + algorithm_parameters=algorithm_param, + function_timeout = 300) # Call the GA optimizer using multiple cores model.run(set_function=ga.set_function_multiprocess(deadVolObjectiveFunction, diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 93a553b..515f41c 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -70,7 +70,13 @@ def extractZLCParameters(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp39A_Output.mat', + fileName = ['ZLC_ActivatedCarbon_Exp34A_Output.mat', + 'ZLC_ActivatedCarbon_Exp34B_Output.mat', + 'ZLC_ActivatedCarbon_Exp34C_Output.mat', + 'ZLC_ActivatedCarbon_Exp34D_Output.mat', + 'ZLC_ActivatedCarbon_Exp34E_Output.mat', + 'ZLC_ActivatedCarbon_Exp34F_Output.mat', + 'ZLC_ActivatedCarbon_Exp39A_Output.mat', 'ZLC_ActivatedCarbon_Exp39B_Output.mat', 'ZLC_ActivatedCarbon_Exp39C_Output.mat', 'ZLC_ActivatedCarbon_Exp39D_Output.mat', @@ -78,7 +84,8 @@ def extractZLCParameters(): 'ZLC_ActivatedCarbon_Exp39F_Output.mat'] # Temperature (for each experiment) - temperature = [313.15, 313.15, 313.15, 313.15, 313.15, 313.15] + temperature = [298.15, 298.15, 298.15, 298.15, 298.15, 298.15, + 313.15, 313.15, 313.15, 313.15, 313.15, 313.15] # Dead volume model deadVolumeFile = 'deadVolumeCharacteristics_20210528_1319_318b280.npz' From 4df70b4329d3102585cde440032891fa2eaa81ed Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 2 Jun 2021 21:22:18 +0100 Subject: [PATCH 116/189] Add normalization for error --- experimental/extractZLCParameters.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 515f41c..8152d90 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-06-02, AK: Add normalization for error # - 2021-06-01, AK: Add temperature as an input # - 2021-05-25, AK: Add kinetic mode for estimation # - 2021-05-24, AK: Improve information passing (for output) @@ -65,7 +66,7 @@ def extractZLCParameters(): modelType = 'SSL' # Number of times optimization repeated - numOptRepeat = 10 + numOptRepeat = 5 # Directory of raw data mainDir = 'runData' @@ -154,8 +155,8 @@ def extractZLCParameters(): massSorbent,isoRef,thresholdFactor,paramIso) # Algorithm parameters for GA - algorithm_param = {'max_num_iteration':5, - 'population_size':1600, + algorithm_param = {'max_num_iteration':30, + 'population_size':200, 'mutation_probability':0.1, 'crossover_probability': 0.55, 'parents_portion': 0.15, @@ -167,7 +168,8 @@ def extractZLCParameters(): model = ga(function = ZLCObjectiveFunction, dimension=problemDimension, variable_type_mixed = optType, variable_boundaries = optBounds, - algorithm_parameters=algorithm_param) + algorithm_parameters=algorithm_param, + function_timeout = 300) # Timeout set to 300 s (change if code crashes) # Call the GA optimizer using multiple cores model.run(set_function=ga.set_function_multiprocess(ZLCObjectiveFunction, @@ -287,8 +289,12 @@ def ZLCObjectiveFunction(x): # Stack mole fraction from experiments and simulation for error # computation - moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) - moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) + # Normalize the error by dividing it by maximum value to avoid + # irregular weightings for different experiment (at diff. scales) + # This might not be needed in DV model as all the experiments are + # start at 1 and end at 5e-3 + moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp/np.max(moleFracExp))) + moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim/np.max(moleFracSim))) # Compute the sum of the error for the difference between exp. and sim. computedError = computeMLEError(moleFracExpALL,moleFracSimALL,thresholdFactor=thresholdFactor) From 0d919e5e970923b16a8cad484c1bf0dc942825ac Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Thu, 10 Jun 2021 12:09:26 +0100 Subject: [PATCH 117/189] Minor bug fix --- .gitignore | 1 + experimental/analysis/concatenateData.m | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 52131f4..d1db017 100755 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ __pycache__/ #*.bib *.xlsx *.txt +*.zip # Python related outputs *.npy diff --git a/experimental/analysis/concatenateData.m b/experimental/analysis/concatenateData.m index a9e8c52..abb9ac8 100644 --- a/experimental/analysis/concatenateData.m +++ b/experimental/analysis/concatenateData.m @@ -13,6 +13,7 @@ % % % Last modified: +% - 2021-06-10, AK: Bug fix for MS datetime % - 2021-05-10, AK: Change the calibration analysis to take average of % multiple calibrations % - 2021-04-23, AK: Change the calibration model to Fourier series based @@ -66,10 +67,10 @@ % Load the MS Data into a cell array rawMSData = textscan(fileId,repmat('%s',1,9),'HeaderLines',8,'Delimiter','\t'); % Get the date time for CO2 - dateTimeHe = datetime(cell2mat(rawMSData{1,4}),... + dateTimeHe = datetime(rawMSData{1,4},... 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); % Get the date time for He - dateTimeCO2 = datetime(cell2mat(rawMSData{1,1}),... + dateTimeCO2 = datetime(rawMSData{1,1},... 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); % Reconcile all the data % Initial time From 31ca6e54b7bb6f283a92258f4fe6e91b2b89d80e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 11 Jun 2021 15:45:41 +0100 Subject: [PATCH 118/189] Change normalization error for ZLC parameter estimation --- experimental/extractZLCParameters.py | 53 ++++++++++++++++++---------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 8152d90..9422faf 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-06-11, AK: Change normalization for error # - 2021-06-02, AK: Add normalization for error # - 2021-06-01, AK: Add temperature as an input # - 2021-05-25, AK: Add kinetic mode for estimation @@ -71,25 +72,39 @@ def extractZLCParameters(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp34A_Output.mat', - 'ZLC_ActivatedCarbon_Exp34B_Output.mat', - 'ZLC_ActivatedCarbon_Exp34C_Output.mat', - 'ZLC_ActivatedCarbon_Exp34D_Output.mat', - 'ZLC_ActivatedCarbon_Exp34E_Output.mat', - 'ZLC_ActivatedCarbon_Exp34F_Output.mat', - 'ZLC_ActivatedCarbon_Exp39A_Output.mat', - 'ZLC_ActivatedCarbon_Exp39B_Output.mat', - 'ZLC_ActivatedCarbon_Exp39C_Output.mat', - 'ZLC_ActivatedCarbon_Exp39D_Output.mat', - 'ZLC_ActivatedCarbon_Exp39E_Output.mat', - 'ZLC_ActivatedCarbon_Exp39F_Output.mat'] + fileName = ['ZLC_ActivatedCarbon_Exp43A_Output.mat', + 'ZLC_ActivatedCarbon_Exp43B_Output.mat', + 'ZLC_ActivatedCarbon_Exp43C_Output.mat', + 'ZLC_ActivatedCarbon_Exp43D_Output.mat', + 'ZLC_ActivatedCarbon_Exp43E_Output.mat', + 'ZLC_ActivatedCarbon_Exp43F_Output.mat', + 'ZLC_ActivatedCarbon_Exp44A_Output.mat', + 'ZLC_ActivatedCarbon_Exp44B_Output.mat', + 'ZLC_ActivatedCarbon_Exp44C_Output.mat', + 'ZLC_ActivatedCarbon_Exp44D_Output.mat', + 'ZLC_ActivatedCarbon_Exp44E_Output.mat', + 'ZLC_ActivatedCarbon_Exp44F_Output.mat', + 'ZLC_ActivatedCarbon_Exp48A_Output.mat', + 'ZLC_ActivatedCarbon_Exp48B_Output.mat', + 'ZLC_ActivatedCarbon_Exp48C_Output.mat', + 'ZLC_ActivatedCarbon_Exp48D_Output.mat', + 'ZLC_ActivatedCarbon_Exp48E_Output.mat', + 'ZLC_ActivatedCarbon_Exp48F_Output.mat', + 'ZLC_ActivatedCarbon_Exp49A_Output.mat', + 'ZLC_ActivatedCarbon_Exp49B_Output.mat', + 'ZLC_ActivatedCarbon_Exp49C_Output.mat', + 'ZLC_ActivatedCarbon_Exp49D_Output.mat', + 'ZLC_ActivatedCarbon_Exp49E_Output.mat', + 'ZLC_ActivatedCarbon_Exp49F_Output.mat',] # Temperature (for each experiment) - temperature = [298.15, 298.15, 298.15, 298.15, 298.15, 298.15, - 313.15, 313.15, 313.15, 313.15, 313.15, 313.15] + temperature = [306.47, 306.47, 306.47, 306.47, 306.47, 306.47, + 306.47, 306.47, 306.47, 306.47, 306.47, 306.47, + 317.18, 317.18, 317.18, 317.18, 317.18, 317.18, + 317.18, 317.18, 317.18, 317.18, 317.18, 317.18,] # Dead volume model - deadVolumeFile = 'deadVolumeCharacteristics_20210528_1319_318b280.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210603_1207_4df70b4.npz' # Isotherm model (if fitting only kinetic constant) isothermFile = 'zlcParameters_20210525_1610_a079f4a.npz' @@ -289,12 +304,12 @@ def ZLCObjectiveFunction(x): # Stack mole fraction from experiments and simulation for error # computation - # Normalize the error by dividing it by maximum value to avoid + # Normalize the mole fraction by dividing it by maximum value to avoid # irregular weightings for different experiment (at diff. scales) - # This might not be needed in DV model as all the experiments are + # This might not be needed in DV model as all the experiments # start at 1 and end at 5e-3 - moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp/np.max(moleFracExp))) - moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim/np.max(moleFracSim))) + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/np.max(moleFracExp))) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/np.max(moleFracSim))) # Compute the sum of the error for the difference between exp. and sim. computedError = computeMLEError(moleFracExpALL,moleFracSimALL,thresholdFactor=thresholdFactor) From 8313a043a3cb15cfbe1ffa35fddfc360953b0539 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sat, 12 Jun 2021 20:52:31 +0100 Subject: [PATCH 119/189] Fix error computation for parameter estimation (major) --- experimental/computeMLEError.py | 122 +++++++++++++++++++-------- experimental/extractDeadVolume.py | 22 +++-- experimental/extractZLCParameters.py | 31 ++++--- 3 files changed, 124 insertions(+), 51 deletions(-) diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py index 9020a60..1504f1a 100644 --- a/experimental/computeMLEError.py +++ b/experimental/computeMLEError.py @@ -12,6 +12,7 @@ # Computes the MLE error for ZLC experiments. # # Last modified: +# - 2021-06-12, AK: Add pure data downsampling # - 2021-05-24, AK: Add -inf input to avoid splitting compositions # - 2021-05-13, AK: Add different modes for MLE error computations # - 2021-05-05, AK: Initial creation @@ -27,52 +28,101 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): import numpy as np - # Check if threshold is provided to split data to high and low compositions - if 'thresholdFactor' in kwargs: - thresholdFlag = True - thresholdFactor = np.array(kwargs["thresholdFactor"]) - # If negative infinity provided as a threshold, do not split and uses - # all data with equal weights - if np.isneginf(thresholdFactor): - thresholdFlag = False - # Default is flag, uses all data with equal weights + # Check if flag for downsampling data in low and high composition provided + if 'downsampleData' in kwargs: + downsampleData = kwargs["downsampleData"] + # Threshold Factor is needed for pure downsampling + if 'thresholdFactor' in kwargs: + thresholdFactor = np.array(kwargs["thresholdFactor"]) + else: + thresholdFactor = 0.5 # Default set to 0.5 (as data scaled between 0 and 1) + # Default is false, uses either pure MLE or split MLE with scaling based + # on threshold else: - thresholdFlag = False + downsampleData = False + # Check if threshold is provided to split data to high and low + # compositions and scale for MLE computation + if 'thresholdFactor' in kwargs: + thresholdFlag = True + thresholdFactor = np.array(kwargs["thresholdFactor"]) + # If negative infinity provided as a threshold, do not split and uses + # all data with equal weights + if np.isneginf(thresholdFactor): + thresholdFlag = False + # Default is false, uses all data with equal weights + else: + thresholdFlag = False - # If no threshold provided just do normal MLE - if not thresholdFlag: - computedError = np.log(np.sum(np.power(moleFracExp - moleFracSim, 2))) - numPoints = len(moleFracExp) - # If threshold provided, split the data to two and compute the error + # If not pure downsampling of the data at different composition ranges + if not downsampleData: + # If no threshold provided just do normal MLE + if not thresholdFlag: + computedError = np.log(np.sum(np.power(moleFracExp - moleFracSim, 2))) + numPoints = len(moleFracExp) + # If threshold provided, split the data to two and compute the error + else: + #### Quite a lot of bugs because of changing how error computed ### + #### DO NOT USE THIS!!! ### + # Objective function error + # Find error for mole fraction below a given threshold + lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) + # Do downsampling if the number of points in higher and lower + # compositions does not match + numPointsConc = np.zeros([2]) + numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition + numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition + downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals + + # Compute error for higher concentrations (accounting for downsampling) + moleFracHighExp = moleFracExp[0:lastIndThreshold] + moleFracHighSim = moleFracSim[0:lastIndThreshold] + computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] + - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) + + # Find scaling factor for lower concentrations + scalingFactor = int(1/thresholdFactor) # Assumes max composition is one + # Compute error for lower concentrations + moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor + moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor + + # Compute error for low concentrations (accounting for downsampling) + computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] + - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) + + # Find the sum of computed error + computedError = computedErrorHigh + computedErrorLow + + # Compute the number of points per experiment (accouting for down- + # sampling in both experiments and high and low compositions + numPoints = len(moleFracHighExp) + len(moleFracLowExp) + # Pure downsampling of the data at different composition ranges else: # Objective function error + # Concatenate the experiment and simulated data into an array + concatenatedData = np.vstack((moleFracExp,moleFracSim)).T + # Sort the data first (as everything is concatenated) + sortedData = concatenatedData[np.argsort(concatenatedData[:,0]),:] # Find error for mole fraction below a given threshold - lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) + lastIndThreshold = int(np.argwhere(sortedData[:,0]>thresholdFactor)[0]) + # Do downsampling if the number of points in higher and lower # compositions does not match numPointsConc = np.zeros([2]) - numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition - numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition + numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # Low composition + numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # High composition downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals - # Compute error for higher concentrations (accounting for downsampling) - moleFracHighExp = moleFracExp[0:lastIndThreshold] - moleFracHighSim = moleFracSim[0:lastIndThreshold] - computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] - - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) - - # Find scaling factor for lower concentrations - scalingFactor = int(1/thresholdFactor) # Assumes max composition is one - # Compute error for lower concentrations - moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor - moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor - - # Compute error for low concentrations (accounting for downsampling) - computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] - - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) - - # Find the sum of computed error - computedError = computedErrorHigh + computedErrorLow + # Compute error (accounting for downsampling) + # Lower concentrations + moleFracLowExp = moleFracExp[0:lastIndThreshold:int(np.round(downsampleConc[0]))] + moleFracLowSim = moleFracSim[0:lastIndThreshold:int(np.round(downsampleConc[0]))] + # Higher concentrations + moleFracHighExp = moleFracExp[lastIndThreshold:-1:int(np.round(downsampleConc[1]))] + moleFracHighSim = moleFracSim[lastIndThreshold:-1:int(np.round(downsampleConc[1]))] + # Compute the error + computedErrorLow = np.sum(np.power(moleFracLowExp - moleFracLowSim,2)) + computedErrorHigh = np.sum(np.power(moleFracHighExp - moleFracHighSim,2)) + computedError = np.log(computedErrorLow+computedErrorHigh) # Compute the number of points per experiment (accouting for down- # sampling in both experiments and high and low compositions diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 340554d..7375d91 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-06-12, AK: Fix for error computation (major) # - 2021-05-28, AK: Add the existing DV model with MS DV model and structure change # - 2021-05-28, AK: Add model for MS # - 2021-05-24, AK: Improve information passing (for output) @@ -96,16 +97,21 @@ def extractDeadVolume(): if flagMSDeadVolume: msDeadVolumeFile = 'deadVolumeCharacteristics_20210528_1110_7c0209e.npz' + # Downsample the data at different compositions (this is done on + # normalized data) + downsampleData = True + # Threshold factor (If -negative infinity not used, if not need a float) # This is used to split the compositions into two distint regions - thresholdFactor = -np.inf + thresholdFactor = 0.5 ##################################### ##################################### # Save the parameters to be used for fitting to a dummy file (to pass # through GA - IDIOTIC) - savez ('tempFittingParametersDV.npz',thresholdFactor=thresholdFactor, + savez ('tempFittingParametersDV.npz', + downsampleData=downsampleData,thresholdFactor=thresholdFactor, flagMSFit = flagMSFit, msFlowRate = msFlowRate, flagMSDeadVolume = flagMSDeadVolume, msDeadVolumeFile = msDeadVolumeFile) @@ -173,6 +179,7 @@ def extractDeadVolume(): msFlowRate = msFlowRate, # Flow rate through MS capillary [ccs] flagMSDeadVolume = flagMSDeadVolume, # Flag for checking if ms dead volume used msDeadVolumeFile = msDeadVolumeFile, # MS dead volume parameter file + downsampleFlag = downsampleData, # Flag for downsampling data [-] mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] hostName = socket.gethostname()) # Hostname of the computer @@ -197,6 +204,7 @@ def deadVolObjectiveFunction(x): # Load the threshold factor, MS fit flag and MS flow rate from the dummy # file + downsampleData = load ('tempFittingParametersDV.npz')["downsampleData"] thresholdFactor = load ('tempFittingParametersDV.npz')["thresholdFactor"] flagMSFit = load ('tempFittingParametersDV.npz')["flagMSFit"] # Used only if MS data is fit msFlowRate = load ('tempFittingParametersDV.npz')["msFlowRate"] # Used only if MS data is fit @@ -251,8 +259,10 @@ def deadVolObjectiveFunction(x): # Stack mole fraction from experiments and simulation for error # computation - moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) - moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) + # Normalize the mole fraction by dividing it by maximum value to avoid + # irregular weightings for different experiment (at diff. scales) + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/(np.max(moleFracExp-np.min(moleFracExp))))) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/(np.max(moleFracSim-np.min(moleFracSim))))) # Penalize if the total volume of the system is greater than experiemntal # volume @@ -261,7 +271,9 @@ def deadVolObjectiveFunction(x): penaltyObj = 10000 # Compute the sum of the error for the difference between exp. and sim. and # add a penalty if needed (using MLE) - computedError = computeMLEError(moleFracExpALL,moleFracSimALL,thresholdFactor=thresholdFactor) + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData, + thresholdFactor=thresholdFactor) return computedError + penaltyObj # func: filesToProcess diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 9422faf..ed47f99 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-06-12, AK: Fix for error computation (major) # - 2021-06-11, AK: Change normalization for error # - 2021-06-02, AK: Add normalization for error # - 2021-06-01, AK: Add temperature as an input @@ -117,10 +118,15 @@ def extractZLCParameters(): particleEpsilon = 0.61 # Particle mass [g] massSorbent = 0.0625 - + + # Downsample the data at different compositions (this is done on + # normalized data) + downsampleData = True + # Threshold factor (If -ngative infinity not used, if not need a float) # This is used to split the compositions into two distint regions - thresholdFactor = -np.inf + # Note that this is also needed for (pure) downsampling data + thresholdFactor = 0.5 ##################################### ##################################### @@ -167,7 +173,7 @@ def extractZLCParameters(): # Initialize the parameters used for ZLC fitting process fittingParameters(True,temperature,deadVolumeFile,adsorbentDensity,particleEpsilon, - massSorbent,isoRef,thresholdFactor,paramIso) + massSorbent,isoRef,downsampleData,thresholdFactor,paramIso) # Algorithm parameters for GA algorithm_param = {'max_num_iteration':30, @@ -220,6 +226,7 @@ def extractZLCParameters(): particleEpsilon = particleEpsilon, # Particle voidage [-] massSorbent = massSorbent, # Mass of sorbent [g] parameterReference = isoRef, # Parameter references [-] + downsampleFlag = downsampleData, # Flag for downsampling data [-] mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] hostName = socket.gethostname()) # Hostname of the computer @@ -244,7 +251,7 @@ def ZLCObjectiveFunction(x): from computeMLEError import computeMLEError # Get the zlc parameters needed for the solver - temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso = fittingParameters(False,[],[],[],[],[],[],[],[]) + temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, thresholdFactor, paramIso = fittingParameters(False,[],[],[],[],[],[],[],[],[]) # Volume of sorbent material [m3] volSorbent = (massSorbent/1000)/adsorbentDensity @@ -306,20 +313,22 @@ def ZLCObjectiveFunction(x): # computation # Normalize the mole fraction by dividing it by maximum value to avoid # irregular weightings for different experiment (at diff. scales) - # This might not be needed in DV model as all the experiments - # start at 1 and end at 5e-3 - moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/np.max(moleFracExp))) - moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/np.max(moleFracSim))) + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/(np.max(moleFracExp-np.min(moleFracExp))))) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/(np.max(moleFracSim-np.min(moleFracSim))))) # Compute the sum of the error for the difference between exp. and sim. - computedError = computeMLEError(moleFracExpALL,moleFracSimALL,thresholdFactor=thresholdFactor) + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData, + thresholdFactor=thresholdFactor) return computedError # func: fittingParameters # Parses dead volume calibration file, adsorbent density, voidage, mass to # be used for parameter estimation, parameter references and threshold for MLE # This is done because the ga cannot handle additional user inputs -def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity,particleEpsilon,massSorbent,isoRef, thresholdFactor,paramIso): +def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity, + particleEpsilon,massSorbent,isoRef,downsampleData, + thresholdFactor,paramIso): from numpy import savez from numpy import load # Process the data for python (if needed) @@ -332,6 +341,7 @@ def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity,parti particleEpsilon=particleEpsilon, massSorbent=massSorbent, isoRef=isoRef, + downsampleData=downsampleData, thresholdFactor=thresholdFactor, paramIso = paramIso) # Returns the path of the .npz file to be used @@ -345,6 +355,7 @@ def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity,parti particleEpsilon = load (dummyFileName)["particleEpsilon"] massSorbent = load (dummyFileName)["massSorbent"] isoRef = load (dummyFileName)["isoRef"] + downsampleData = load (dummyFileName)["downsampleData"] thresholdFactor = load (dummyFileName)["thresholdFactor"] paramIso = load (dummyFileName)["paramIso"] return temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso \ No newline at end of file From 31cb8637dafdc4e71074490abc9a97ebe4fa043e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sun, 13 Jun 2021 10:19:07 +0100 Subject: [PATCH 120/189] Bug fix --- experimental/extractZLCParameters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index ed47f99..60cffcf 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -105,7 +105,7 @@ def extractZLCParameters(): 317.18, 317.18, 317.18, 317.18, 317.18, 317.18,] # Dead volume model - deadVolumeFile = 'deadVolumeCharacteristics_20210603_1207_4df70b4.npz' + deadVolumeFile = 'deadVolumeCharacteristics_20210613_0847_8313a04.npz' # Isotherm model (if fitting only kinetic constant) isothermFile = 'zlcParameters_20210525_1610_a079f4a.npz' @@ -178,7 +178,7 @@ def extractZLCParameters(): # Algorithm parameters for GA algorithm_param = {'max_num_iteration':30, 'population_size':200, - 'mutation_probability':0.1, + 'mutation_probability':0.25, 'crossover_probability': 0.55, 'parents_portion': 0.15, 'elit_ratio': 0.01, @@ -358,4 +358,4 @@ def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity, downsampleData = load (dummyFileName)["downsampleData"] thresholdFactor = load (dummyFileName)["thresholdFactor"] paramIso = load (dummyFileName)["paramIso"] - return temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, thresholdFactor, paramIso \ No newline at end of file + return temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, thresholdFactor, paramIso \ No newline at end of file From 0dfe1b10d6edcaa215a862f861e1ba47c4f83ab5 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 14 Jun 2021 10:44:54 +0100 Subject: [PATCH 121/189] Bug fix for error computation --- experimental/extractZLCParameters.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 60cffcf..ae3c6ee 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-06-14, AK: More fixes for error computation # - 2021-06-12, AK: Fix for error computation (major) # - 2021-06-11, AK: Change normalization for error # - 2021-06-02, AK: Add normalization for error @@ -311,11 +312,16 @@ def ZLCObjectiveFunction(x): # Stack mole fraction from experiments and simulation for error # computation - # Normalize the mole fraction by dividing it by maximum value to avoid - # irregular weightings for different experiment (at diff. scales) - moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/(np.max(moleFracExp-np.min(moleFracExp))))) - moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/(np.max(moleFracSim-np.min(moleFracSim))))) + moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) + moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) + # Normalize the mole fraction by dividing it by maximum value to avoid + # irregular weightings for different experiment (at diff. scales) + minExpALL = np.min(moleFracExpALL) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExpALL - np.min(moleFracExpALL)) # Compute the max from normalized data + moleFracExpALL = (moleFracExpALL - minExpALL)/normalizeFactor # Perform the rescaling for exp. (0-1) + moleFracSimALL = (moleFracSimALL - minExpALL)/normalizeFactor # Perform the rescaling for sim. (0-1) + # Compute the sum of the error for the difference between exp. and sim. computedError = computeMLEError(moleFracExpALL,moleFracSimALL, downsampleData=downsampleData, From 10d71083e205a271d35aa06b9b51ca4a967221d2 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 14 Jun 2021 12:11:17 +0100 Subject: [PATCH 122/189] Bug fix for error computation --- experimental/extractZLCParameters.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index ae3c6ee..520f334 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -312,15 +312,12 @@ def ZLCObjectiveFunction(x): # Stack mole fraction from experiments and simulation for error # computation - moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) - moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) - - # Normalize the mole fraction by dividing it by maximum value to avoid - # irregular weightings for different experiment (at diff. scales) - minExpALL = np.min(moleFracExpALL) # Compute the minimum from experiment - normalizeFactor = np.max(moleFracExpALL - np.min(moleFracExpALL)) # Compute the max from normalized data - moleFracExpALL = (moleFracExpALL - minExpALL)/normalizeFactor # Perform the rescaling for exp. (0-1) - moleFracSimALL = (moleFracSimALL - minExpALL)/normalizeFactor # Perform the rescaling for sim. (0-1) + # Normalize the mole fraction by dividing it by maximum value to avoid + # irregular weightings for different experiment (at diff. scales) + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) # Compute the sum of the error for the difference between exp. and sim. computedError = computeMLEError(moleFracExpALL,moleFracSimALL, From 4d8a46e4fd09a2be369c38452d80ed6f1ae752c7 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 15 Jun 2021 17:08:27 +0100 Subject: [PATCH 123/189] Add correction factor for kinetic constant --- experimental/extractZLCParameters.py | 8 ++++---- experimental/simulateZLC.py | 23 ++++++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 520f334..4357557 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -142,7 +142,7 @@ def extractZLCParameters(): [np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 50e3, 100] # Reference for the isotherm parameters + isoRef = [10, 1e-5, 40e3, 100] # Reference for the isotherm parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit @@ -154,7 +154,7 @@ def extractZLCParameters(): [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 50e3, 10, 1e-5, 50e3, 100] # Reference for the isotherm parameters + isoRef = [10, 1e-5, 40e3, 10, 1e-5, 40e3, 100] # Reference for the isotherm parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit @@ -177,8 +177,8 @@ def extractZLCParameters(): massSorbent,isoRef,downsampleData,thresholdFactor,paramIso) # Algorithm parameters for GA - algorithm_param = {'max_num_iteration':30, - 'population_size':200, + algorithm_param = {'max_num_iteration':15, + 'population_size':400, 'mutation_probability':0.25, 'crossover_probability': 0.55, 'parents_portion': 0.15, diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index d1b25b7..18ee465 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -14,6 +14,7 @@ # model with mass transfer defined using linear driving force. # # Last modified: +# - 2021-06-15, AK: Add correction factor to LDF # - 2021-05-13, AK: IMPORTANT: Change density from particle to skeletal # - 2021-04-27, AK: Fix inputs and add isotherm model as input # - 2021-04-26, AK: Revamp the code for real sorbent simulation @@ -197,14 +198,30 @@ def solveSorptionEquation(t, f, *inputParameters): # Initialize the derivatives to zero df = np.zeros([2]) - # Compute the initial sensor loading [mol/m3] @ initMoleFrac + # Compute the loading [mol/m3] @ f[0] equilibriumLoading = computeEquilibriumLoading(pressureTotal=pressureTotal, temperature=temperature, moleFrac=f[0], isothermModel=isothermModel)*adsorbentDensity # [mol/m3] - + + # Partial pressure of the gas + partialPressure = f[0]*pressureTotal + # delta pressure to compute gradient + delP = 1e-3 + # Mole fraction (up) + moleFractionUp = (partialPressure + delP)/pressureTotal + # Compute the loading [mol/m3] @ moleFractionUp + equilibriumLoadingUp = computeEquilibriumLoading(pressureTotal=pressureTotal, + temperature=temperature, + moleFrac=moleFractionUp, + isothermModel=isothermModel)*adsorbentDensity # [mol/m3] + # Compute the gradient (delp/delq*) + dPbydq = delP/(equilibriumLoadingUp-equilibriumLoading) + # Compute the correction factor for kinetic factor + correctionFactor = (equilibriumLoading/partialPressure)*dPbydq + # Linear driving force model (derivative of solid phase loadings) - df[1] = rateConstant*(equilibriumLoading-f[1]) + df[1] = correctionFactor*rateConstant*(equilibriumLoading-f[1]) # Total mass balance # Assumes constant pressure, so flow rate evalauted From fb01717d9a393ae0954286acbfacc0a721a2d99e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 16 Jun 2021 09:33:42 +0100 Subject: [PATCH 124/189] Add temperature dependence to LDF --- experimental/extractZLCParameters.py | 85 ++++++++++++--------------- experimental/simulateCombinedModel.py | 10 +++- experimental/simulateZLC.py | 26 ++++++-- 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 4357557..6541ec7 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-06-16, AK: Add temperature dependence to kinetics # - 2021-06-14, AK: More fixes for error computation # - 2021-06-12, AK: Fix for error computation (major) # - 2021-06-11, AK: Change normalization for error @@ -34,9 +35,10 @@ # # Output arguments: # +# ############################################################################ -def extractZLCParameters(): +def extractZLCParameters(**kwargs): import numpy as np from geneticalgorithm2 import geneticalgorithm2 as ga # GA from extractDeadVolume import filesToProcess # File processing script @@ -63,10 +65,13 @@ def extractZLCParameters(): num_cores = multiprocessing.cpu_count() ##################################### - ###### USER DEFINED PROPERTIES ###### - + ###### USER DEFINED PROPERTIES ###### + # If not passed to the function, default values used # Isotherm model type - modelType = 'SSL' + if 'modelType' in kwargs: + modelType = kwargs["modelType"] + else: + modelType = 'SSL' # Number of times optimization repeated numOptRepeat = 5 @@ -74,36 +79,18 @@ def extractZLCParameters(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_ActivatedCarbon_Exp43A_Output.mat', - 'ZLC_ActivatedCarbon_Exp43B_Output.mat', - 'ZLC_ActivatedCarbon_Exp43C_Output.mat', - 'ZLC_ActivatedCarbon_Exp43D_Output.mat', - 'ZLC_ActivatedCarbon_Exp43E_Output.mat', - 'ZLC_ActivatedCarbon_Exp43F_Output.mat', - 'ZLC_ActivatedCarbon_Exp44A_Output.mat', - 'ZLC_ActivatedCarbon_Exp44B_Output.mat', - 'ZLC_ActivatedCarbon_Exp44C_Output.mat', - 'ZLC_ActivatedCarbon_Exp44D_Output.mat', - 'ZLC_ActivatedCarbon_Exp44E_Output.mat', - 'ZLC_ActivatedCarbon_Exp44F_Output.mat', - 'ZLC_ActivatedCarbon_Exp48A_Output.mat', - 'ZLC_ActivatedCarbon_Exp48B_Output.mat', - 'ZLC_ActivatedCarbon_Exp48C_Output.mat', - 'ZLC_ActivatedCarbon_Exp48D_Output.mat', - 'ZLC_ActivatedCarbon_Exp48E_Output.mat', - 'ZLC_ActivatedCarbon_Exp48F_Output.mat', - 'ZLC_ActivatedCarbon_Exp49A_Output.mat', - 'ZLC_ActivatedCarbon_Exp49B_Output.mat', - 'ZLC_ActivatedCarbon_Exp49C_Output.mat', - 'ZLC_ActivatedCarbon_Exp49D_Output.mat', - 'ZLC_ActivatedCarbon_Exp49E_Output.mat', - 'ZLC_ActivatedCarbon_Exp49F_Output.mat',] + if 'fileName' in kwargs: + fileName = kwargs["fileName"] + else: + fileName = ['ZLC_ActivatedCarbon_Exp43F_Output.mat', + 'ZLC_ActivatedCarbon_Exp48F_Output.mat', + 'ZLC_ActivatedCarbon_Exp55F_Output.mat',] # Temperature (for each experiment) - temperature = [306.47, 306.47, 306.47, 306.47, 306.47, 306.47, - 306.47, 306.47, 306.47, 306.47, 306.47, 306.47, - 317.18, 317.18, 317.18, 317.18, 317.18, 317.18, - 317.18, 317.18, 317.18, 317.18, 317.18, 317.18,] + if 'temperature' in kwargs: + temperature = kwargs["temperature"] + else: + temperature = [306.47,317.18,339.14] # Dead volume model deadVolumeFile = 'deadVolumeCharacteristics_20210613_0847_8313a04.npz' @@ -135,14 +122,17 @@ def extractZLCParameters(): # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,fileName,'ZLC') - # Define the bounds and the type of the parameters to be optimized + # Define the bounds and the type of the parameters to be optimized + # Parameters optimized: qs,b0,delU (for DSL: both sites), k0 and delE + # (16.06.21: Arrhenius constant and activation energy) # Single-site Langmuir if modelType == 'SSL': optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1], - [np.finfo(float).eps,1], [np.finfo(float).eps,1])) - optType=np.array(['real','real','real','real']) + [np.finfo(float).eps,1], [np.finfo(float).eps,1], + [np.finfo(float).eps,1])) + optType=np.array(['real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 40e3, 100] # Reference for the isotherm parameters + isoRef = [10, 1e-5, 40e3, 10000, 40e3] # Reference for the isotherm parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit @@ -151,19 +141,21 @@ def extractZLCParameters(): optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1], [np.finfo(float).eps,1], [np.finfo(float).eps,1], [np.finfo(float).eps,1], [np.finfo(float).eps,1], - [np.finfo(float).eps,1])) - optType=np.array(['real','real','real','real','real','real','real']) + [np.finfo(float).eps,1], [np.finfo(float).eps,1])) + optType=np.array(['real','real','real','real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 40e3, 10, 1e-5, 40e3, 100] # Reference for the isotherm parameters + isoRef = [10, 1e-5, 40e3, 10, 1e-5, 40e3, 10000, 40e3] # Reference for the isotherm parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit - # Kinetic constant only + # Kinetic constants only + # Note: This might be buggy for simulations performed before 16.06.21 + # This is because of the addition of activation energy for kinetics elif modelType == 'Kinetic': - optBounds = np.array([[np.finfo(float).eps,1]]) - optType=np.array(['real']) + optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1])) + optType=np.array(['real','real']) problemDimension = len(optType) - isoRef = [100] # Reference for the parameter (has to be a list) + isoRef = [10000, 40e3] # Reference for the parameter (has to be a list) # File with parameter estimates for isotherm (ZLC) isothermDir = '..' + os.path.sep + 'simulationResults/' modelOutputTemp = load(isothermDir+isothermFile, allow_pickle=True)["modelOutput"] @@ -261,9 +253,9 @@ def ZLCObjectiveFunction(x): # Prepare isotherm model (the first n-1 parameters are for the isotherm model) if len(paramIso) != 0: - isothermModel = paramIso[0:-1] # Use this if isotherm parameter provided (for kinetics only) + isothermModel = paramIso[0:-2] # Use this if isotherm parameter provided (for kinetics only) else: - isothermModel = np.multiply(x[0:-1],isoRef[0:-1]) # Use this if both equilibrium and kinetics is fit + isothermModel = np.multiply(x[0:-2],isoRef[0:-2]) # Use this if both equilibrium and kinetics is fit # Load the names of the file to be used for estimating zlc parameters filePath = filesToProcess(False,[],[],'ZLC') @@ -300,7 +292,8 @@ def ZLCObjectiveFunction(x): # Compute the composite response using the optimizer parameters _ , moleFracSim , _ = simulateCombinedModel(isothermModel = isothermModel, - rateConstant = x[-1]*isoRef[-1], # Last element is rate constant + rateConstant = x[-2]*isoRef[-2], # Last but one element is rate constant (Arrhenius constant) + kineticActEnergy = x[-1]*isoRef[-1], # Last element is activation energy temperature = temperature[ii], # Temperature [K] timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index ac3a9b5..624fab8 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -14,6 +14,7 @@ # volume simulator # # Last modified: +# - 2021-06-16, AK: Add temperature dependence to kinetics # - 2021-06-01, AK: Add temperature as an input # - 2021-05-29, AK: Add a separate MS dead volume # - 2021-05-13, AK: Add volumes and density as inputs @@ -60,7 +61,13 @@ def simulateCombinedModel(**kwargs): rateConstant = kwargs["rateConstant"] else: rateConstant = [0.3] - + + # Kinetic activation energy constants [J/mol] + if 'kineticActEnergy' in kwargs: + kineticActEnergy = np.array(kwargs["kineticActEnergy"]) + else: + kineticActEnergy = np.array([0]) # To simulate the case with temp. dep. + # Temperature of the gas [K] if 'temperature' in kwargs: temperature = np.array(kwargs["temperature"]); @@ -119,6 +126,7 @@ def simulateCombinedModel(**kwargs): # Call the simulateZLC function to simulate the sorption in a given sorbent timeZLC, resultMat, _ = simulateZLC(isothermModel=isothermModel, rateConstant=rateConstant, + kineticActEnergy = kineticActEnergy, temperature = temperature, flowIn = flowIn, initMoleFrac = initMoleFrac, diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 18ee465..1f06eb8 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -14,6 +14,7 @@ # model with mass transfer defined using linear driving force. # # Last modified: +# - 2021-06-16, AK: Add temperature correction factor to LDF # - 2021-06-15, AK: Add correction factor to LDF # - 2021-05-13, AK: IMPORTANT: Change density from particle to skeletal # - 2021-04-27, AK: Fix inputs and add isotherm model as input @@ -26,6 +27,7 @@ # # Output arguments: # +# ############################################################################ def simulateZLC(**kwargs): @@ -52,6 +54,12 @@ def simulateZLC(**kwargs): rateConstant = np.array(kwargs["rateConstant"]) else: rateConstant = np.array([0.1]) + + # Kinetic activation energy constants [J/mol] + if 'kineticActEnergy' in kwargs: + kineticActEnergy = np.array(kwargs["kineticActEnergy"]) + else: + kineticActEnergy = np.array([0]) # To simulate the case with temp. dep. # Feed flow rate [m3/s] if 'flowIn' in kwargs: @@ -142,8 +150,9 @@ def simulateZLC(**kwargs): isothermModel=isothermModel)*adsorbentDensity # [mol/m3] # Prepare tuple of input parameters for the ode solver - inputParameters = (adsorbentDensity, isothermModel, rateConstant, flowIn, feedMoleFrac, - initMoleFrac, pressureTotal, temperature, volSorbent, volGas) + inputParameters = (adsorbentDensity, isothermModel, rateConstant, kineticActEnergy, + flowIn, feedMoleFrac, initMoleFrac, pressureTotal, + temperature, volSorbent, volGas) # Solve the system of ordinary differential equations # Stiff solver used for the problem: BDF or Radau @@ -193,7 +202,7 @@ def solveSorptionEquation(t, f, *inputParameters): Rg = 8.314; # [J/mol K] # Unpack the tuple of input parameters used to solve equations - adsorbentDensity, isothermModel, rateConstant, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters + adsorbentDensity, isothermModel, rateConstant, kineticActEnergy, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero df = np.zeros([2]) @@ -217,8 +226,13 @@ def solveSorptionEquation(t, f, *inputParameters): isothermModel=isothermModel)*adsorbentDensity # [mol/m3] # Compute the gradient (delp/delq*) dPbydq = delP/(equilibriumLoadingUp-equilibriumLoading) - # Compute the correction factor for kinetic factor - correctionFactor = (equilibriumLoading/partialPressure)*dPbydq + # Compute the correction factor for kinetic constant + # Darken factor for concentration dependence + correctionFactor_Conc = (equilibriumLoading/partialPressure)*dPbydq + # Arrhenius factor for temperature dependence + correctionFactor_Temp = np.exp(kineticActEnergy/(Rg*temperature)) + # Correction factor + correctionFactor = correctionFactor_Conc*correctionFactor_Temp # Linear driving force model (derivative of solid phase loadings) df[1] = correctionFactor*rateConstant*(equilibriumLoading-f[1]) @@ -249,7 +263,7 @@ def plotFullModelResult(timeSim, resultMat, inputParameters, saveFileExtension = ".png" # Unpack the tuple of input parameters used to solve equations - adsorbentDensity , _ , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters + adsorbentDensity , _ , _ , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters os.chdir("plotFunctions") # Plot the solid phase compositions From c1980daf4d88d04ff7e52cea817b03284c13472d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 16 Jun 2021 11:55:49 +0100 Subject: [PATCH 125/189] Bug fix --- experimental/simulateZLC.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 1f06eb8..b2734d8 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -230,7 +230,7 @@ def solveSorptionEquation(t, f, *inputParameters): # Darken factor for concentration dependence correctionFactor_Conc = (equilibriumLoading/partialPressure)*dPbydq # Arrhenius factor for temperature dependence - correctionFactor_Temp = np.exp(kineticActEnergy/(Rg*temperature)) + correctionFactor_Temp = np.exp(-kineticActEnergy/(Rg*temperature)) # Correction factor correctionFactor = correctionFactor_Conc*correctionFactor_Temp From 36d3aa3887220f00efa2a151ecc21c27103c8bfa Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 18 Jun 2021 16:54:09 +0100 Subject: [PATCH 126/189] Chnage density of AC from 1950 to 2000 --- experimental/extractZLCParameters.py | 2 +- experimental/simulateCombinedModel.py | 2 +- experimental/simulateZLC.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 6541ec7..90ae4ed 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -101,7 +101,7 @@ def extractZLCParameters(**kwargs): # Adsorbent properties # Adsorbent density [kg/m3] # This has to be the skeletal density - adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] + adsorbentDensity = 2000 # Activated carbon skeletal density [kg/m3] # Particle porosity particleEpsilon = 0.61 # Particle mass [g] diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index 624fab8..c4dbaf3 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -109,7 +109,7 @@ def simulateCombinedModel(**kwargs): if 'adsorbentDensity' in kwargs: adsorbentDensity = kwargs["adsorbentDensity"] else: - adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] + adsorbentDensity = 2000 # Activated carbon skeletal density [kg/m3] # File name with dead volume characteristics parameters if 'deadVolumeFile' in kwargs: diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index b2734d8..6c80f05 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -128,7 +128,7 @@ def simulateZLC(**kwargs): if 'adsorbentDensity' in kwargs: adsorbentDensity = kwargs["adsorbentDensity"] else: - adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] + adsorbentDensity = 2000 # Activated carbon skeletal density [kg/m3] # Total pressure of the gas [Pa] if 'pressureTotal' in kwargs: From 4caa83f854bdc4278387be21d902a682a9d25848 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 23 Jun 2021 09:04:53 +0100 Subject: [PATCH 127/189] Change variable name in dead volume simuator --- experimental/deadVolumeWrapper.py | 4 ++-- experimental/simulateDeadVolume.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/experimental/deadVolumeWrapper.py b/experimental/deadVolumeWrapper.py index 7690432..8b96932 100644 --- a/experimental/deadVolumeWrapper.py +++ b/experimental/deadVolumeWrapper.py @@ -48,7 +48,7 @@ def deadVolumeWrapper(timeInt, flowRateDV, DV_p, flagMSDeadVolume, deadVolume_2M = DV_p[1], deadVolume_2D = DV_p[2], numTanks_1 = int(DV_p[3]), - flowRate_D = DV_p[4], + flowRate_2D = DV_p[4], initMoleFrac = initMoleFrac, feedMoleFrac = feedMoleFrac, timeInt = timeInt, @@ -71,7 +71,7 @@ def deadVolumeWrapper(timeInt, flowRateDV, DV_p, flagMSDeadVolume, deadVolume_2M = msDV_p[1], deadVolume_2D = msDV_p[2], numTanks_1 = int(msDV_p[3]), - flowRate_D = msDV_p[4], + flowRate_2D = msDV_p[4], initMoleFrac = moleFracSim[0], feedMoleFrac = moleFracSim, timeInt = timeDV, diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py index 61ee66e..bf8e86d 100644 --- a/experimental/simulateDeadVolume.py +++ b/experimental/simulateDeadVolume.py @@ -73,10 +73,10 @@ def simulateDeadVolume(**kwargs): else: deadVolume_2D = 5.93e-1 # Flow rate in the diffusive volume [-] - if 'flowRate_D' in kwargs: - flowRate_D = kwargs["flowRate_D"] + if 'flowRate_2D' in kwargs: + flowRate_2D = kwargs["flowRate_2D"] else: - flowRate_D = 1.35e-2 + flowRate_2D = 1.35e-2 # Initial Mole Fraction [-] if 'initMoleFrac' in kwargs: initMoleFrac = np.array(kwargs["initMoleFrac"]) @@ -112,7 +112,7 @@ def simulateDeadVolume(**kwargs): # Prepare tuple of input parameters for the ode solver inputParameters = (t_eval,flowRate, deadVolume_1,deadVolume_2M, - deadVolume_2D, numTanks_1, flowRate_D, + deadVolume_2D, numTanks_1, flowRate_2D, feedMoleFrac) # Total number of tanks[-] @@ -140,9 +140,9 @@ def simulateDeadVolume(**kwargs): moleFracDiff = outputSol.y[-1] # Composition after mixing - flowRate_M = flowRate - flowRate_D + flowRate_M = flowRate - flowRate_2D moleFracOut = np.divide(np.multiply(flowRate_M,moleFracMix) - + np.multiply(flowRate_D,moleFracDiff),flowRate) + + np.multiply(flowRate_2D,moleFracDiff),flowRate) # Plot the dead volume response if plotFlag: @@ -160,7 +160,7 @@ def solveTanksInSeries(t, f, *inputParameters): from scipy.interpolate import interp1d # Unpack the tuple of input parameters used to solve equations - timeElapsed, flowRateALL, deadVolume_1, deadVolume_2M, deadVolume_2D, numTanks_1, flowRate_D, feedMoleFracALL = inputParameters + timeElapsed, flowRateALL, deadVolume_1, deadVolume_2M, deadVolume_2D, numTanks_1, flowRate_2D, feedMoleFracALL = inputParameters # Check if experimental data available # If size of flowrate is one, then no need for interpolation @@ -201,9 +201,9 @@ def solveTanksInSeries(t, f, *inputParameters): volTank_2D = deadVolume_2D # Residence time of each tank in the mixing and diffusive volume - flowRate_M = flowRate - flowRate_D + flowRate_M = flowRate - flowRate_2D residenceTime_2M = volTank_2M/(flowRate_M) - residenceTime_2D = volTank_2D/(flowRate_D) + residenceTime_2D = volTank_2D/(flowRate_2D) # Solve the odes # Volume 2: Mixing volume From b0924656d6bd3651e9fba0a8aaebb8c6b78b96f6 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 23 Jun 2021 18:52:14 +0100 Subject: [PATCH 128/189] Small changes to extractDeadVolume --- experimental/extractDeadVolume.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 7375d91..5d72bab 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -43,7 +43,7 @@ # ############################################################################ -def extractDeadVolume(): +def extractDeadVolume(**kwargs): import numpy as np from geneticalgorithm2 import geneticalgorithm2 as ga # GA import auxiliaryFunctions @@ -76,26 +76,29 @@ def extractDeadVolume(): # Directory of raw data mainDir = 'runData' # File name of the experiments - fileName = ['ZLC_DeadVolume_Exp19A_Output.mat', - 'ZLC_DeadVolume_Exp19B_Output.mat', - 'ZLC_DeadVolume_Exp19C_Output.mat', - 'ZLC_DeadVolume_Exp19D_Output.mat', - 'ZLC_DeadVolume_Exp19E_Output.mat'] + if 'fileName' in kwargs: + fileName = kwargs["fileName"] + else: + fileName = ['ZLC_DeadVolume_Exp20A_Output.mat', + 'ZLC_DeadVolume_Exp20B_Output.mat', + 'ZLC_DeadVolume_Exp20C_Output.mat', + 'ZLC_DeadVolume_Exp20D_Output.mat', + 'ZLC_DeadVolume_Exp20E_Output.mat'] # Fit MS data alone (implemented on 28.05.21) # Flag to fit MS data - flagMSFit = True + flagMSFit = False # Flow rate through the MS capillary (determined by performing experiments) # Pfeiffer Vaccum (in Ronny Pini's lab has 0.4 ccm) msFlowRate = 0.4/60 # [ccs] # MS dead volume model msDeadVolumeFile = [] # DO NOT CHANGE (initialization) - flagMSDeadVolume = False # It should be the opposite of flagMSfit (if used) + flagMSDeadVolume = True # It should be the opposite of flagMSfit (if used) # If MS dead volume used separately, use the file defined here with ms # parameters if flagMSDeadVolume: - msDeadVolumeFile = 'deadVolumeCharacteristics_20210528_1110_7c0209e.npz' + msDeadVolumeFile = 'deadVolumeCharacteristics_20210612_2100_8313a04.npz' # Downsample the data at different compositions (this is done on # normalized data) @@ -134,7 +137,7 @@ def extractDeadVolume(): # Algorithm parameters for GA algorithm_param = {'max_num_iteration':30, 'population_size':200, - 'mutation_probability':0.1, + 'mutation_probability':0.25, 'crossover_probability': 0.55, 'parents_portion': 0.15, 'elit_ratio': 0.01, @@ -142,7 +145,7 @@ def extractDeadVolume(): # Minimize an objective function to compute the dead volume and the number of # tanks for the dead volume using GA - model = ga(function = deadVolObjectiveFunction, dimension=5, + model = ga(function = deadVolObjectiveFunction, dimension=len(optType), variable_type_mixed = optType, variable_boundaries = optBounds, algorithm_parameters=algorithm_param, From 8a81d168ce688e73b3d86932dda1e403b936477b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 29 Jun 2021 11:21:09 +0100 Subject: [PATCH 129/189] Add sensitivity analysis for zlc model --- experimental/extractDeadVolume.py | 4 +- experimental/extractZLCParameters.py | 4 +- .../sensitivityAnalysis.py | 266 ++++++++++++++++++ 3 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 experimental/sensitivityAnalysis/sensitivityAnalysis.py diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 5d72bab..3f89283 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -162,13 +162,13 @@ def extractDeadVolume(**kwargs): n_jobs = num_cores), start_generation=model.output_dict['last_generation'], no_plot = True) - # Save the array concentration into a native numpy file + # Save the dead volume parameters into a native numpy file # The .npz file is saved in a folder called simulationResults (hardcoded) filePrefix = "deadVolumeCharacteristics" saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID; savePath = os.path.join('..','simulationResults',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder + # Check if simulationResults directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationResults')): os.mkdir(os.path.join('..','simulationResults')) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 90ae4ed..1aa2b0e 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -196,13 +196,13 @@ def extractZLCParameters(**kwargs): n_jobs = num_cores), start_generation=model.output_dict['last_generation'], no_plot = True) - # Save the array concentration into a native numpy file + # Save the zlc parameters into a native numpy file # The .npz file is saved in a folder called simulationResults (hardcoded) filePrefix = "zlcParameters" saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID; savePath = os.path.join('..','simulationResults',saveFileName) - # Check if inputResources directory exists or not. If not, create the folder + # Check if simulationResults directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationResults')): os.mkdir(os.path.join('..','simulationResults')) diff --git a/experimental/sensitivityAnalysis/sensitivityAnalysis.py b/experimental/sensitivityAnalysis/sensitivityAnalysis.py new file mode 100644 index 0000000..ae37008 --- /dev/null +++ b/experimental/sensitivityAnalysis/sensitivityAnalysis.py @@ -0,0 +1,266 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2020 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Performs the sensitivity analysis on the model parameters obtained by the +# ZLC parameter estimation routine. The theory behind this can be found +# in Nonlinear Parameter Estimation by Bard. Additional references are +# provided in the code +# +# Last modified: +# - 2021-06-28, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def sensitivityAnalysis(): + import auxiliaryFunctions + import numpy as np + from numpy import load + from numpy import savez + from numpy.linalg import multi_dot # Performs (multiple) matrix multiplication + from numpy.linalg import inv + import os + from scipy.stats import chi2 + import socket + + # Change directory to avoid path issues + os.chdir("..") + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Confidence level + alpha = 0.95 + + # Directory of raw data + mainDir = 'runData' + # File with parameter estimates + simulationDir = os.path.join('..','simulationResults') + # ZLC parameter model + fileParameter = 'zlcParameters_20210619_0128_36d3aa3.npz' + # ZLC parameter path + zlcParameterPath = os.path.join(simulationDir,fileParameter) + # Parse out the optimized model parameters + # Note that this is nondimensional (reference value in the function) + pOptTemp = load(zlcParameterPath, allow_pickle=True)["modelOutput"] + pOpt = pOptTemp[()]["variable"] + # Isotherm parameter reference + pRef = load(zlcParameterPath)["parameterReference"] + + # Call the computeObjectiveFunction + _ , moleFracExpALL, moleFracSimALL = computeObjectiveFunction(mainDir, zlcParameterPath, pOpt, pRef) + + # Number of parameters + Np = len(pOpt) + # Number of time points + Nt = len(moleFracExpALL) + # Compute the approximate diagonal covariance matrix of the errors + # See eq. 12: 10.1021/ie4031852 + # Here there is only one output, therefore V is a scalar and not a vector + V = (1/Nt)*np.sum((moleFracExpALL-moleFracSimALL)**2) + # Construct fancyV (eq. 15) + fancyV = np.zeros((Nt,Nt)) # Initialize with a zero matrix + np.fill_diagonal(fancyV, V) # Create diagnoal matrix with V (this changes directly in memory) + + # Compute the fancyW (eq. 15) + # Define delp (delta of parameter to compute the Jacobian) + delp = np.zeros((Np,Np)) + np.fill_diagonal(delp,np.multiply((np.finfo(float).eps**(1/3)),pOpt)) + # Parameter to the left + pOpt_Left = pOpt - delp + # Parameter to the right + pOpt_Right = pOpt + delp + # Initialize fancyW + fancyW = np.zeros((Nt,Np)) + # Loop over all parameters to compute fancyW + for ii in range(len(pOpt)): + # Initialize model outputs + modelOutput_Left = np.zeros((Nt,1)) + modelOutput_Right = np.zeros((Nt,1)) + # Compute the model output for the left side (derivative) + _ , _ , modelOutput_Left = computeObjectiveFunction(mainDir, zlcParameterPath, pOpt_Left[ii,:], pRef) + # Compute the model output for the left side (derivative) + _ , _ , modelOutput_Right = computeObjectiveFunction(mainDir, zlcParameterPath, pOpt_Right[ii,:], pRef) + # Compute the model Jacobian for the current parameter + fancyW[:,ii] = (modelOutput_Right - modelOutput_Left)/(2*delp[ii,ii]) + + # Compute the covariance matrix for the multiplers (non dimensional) + Vpinv = multi_dot([fancyW.T,inv(fancyV),fancyW]) # This is the inverse + Vp = inv(Vpinv) + + # Transform the multiplier covariance matrix into parameter covariance + # matrix. Check eq. 7-20-2 in Nonlinear Parameter Estimation by Bard + # Create a diagnol matrix for multipler references (pRef) + T = np.zeros((Np,Np)) + np.fill_diagonal(T, pRef) + Vx = multi_dot([T,Vp,T.T]) + + # Obtain chi2 statistics for alpha confidence level and Np degrees + # of freedom (inverse) + chi2Statistics = chi2.ppf(alpha, Np) + + # Confidence intervals for actual model WITHOUT the linearization + # assumption for the model equations. This method corresponds to the + # intersection of one of the delp axes with the confidence + # hyperellipsoid, i.e., to setting all other deltap to zero (see Bard 1974, + # pp. 172-173) + delpNonLinearized = np.sqrt(chi2Statistics/np.diag(inv(Vx))) + + # Compute the bounding box of the confidence hyperellipsoid. Note that + # the matrix that defines the hyperellipsoid is given by + # (chi2Statistics*Vx)^-1, and that the semiaxes of the bounding box are + # given by the square roots of the diagonal elements of the inverse of + # this matrix. + delpBoundingBox = np.sqrt(np.diag(chi2Statistics*Vx)) + + # Print the parameters and the confidence intervals + xOpt = np.multiply(pRef,pOpt) + print("Confidence intervals (Nonlinearized):") + for ii in range(Np): + print('p' + str(ii+1) + ' : ' + str("{:.2e}".format(xOpt[ii])) + + ' +/- ' + str("{:.2e}".format(delpNonLinearized[ii]))) + + print("Confidence intervals (Bounding Box):") + for ii in range(Np): + print('p' + str(ii+1) + ' : ' + str("{:.2e}".format(xOpt[ii])) + + ' +/- ' + str("{:.2e}".format(delpBoundingBox[ii]))) + + + # Save the sensitivity analysis output in .npz file + # The .npz file is saved in a folder called simulationResults (hardcoded) + filePrefix = "sensitivityAnalysis" + saveFileName = filePrefix + "_" + fileParameter[0:-12] + "_" + gitCommitID; + savePath = os.path.join('..','simulationResults',saveFileName) + + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationResults')): + os.mkdir(os.path.join('..','simulationResults')) + + # Save the output into a .npz file + savez (savePath, parameterFile = fileParameter, # File name of parameter estimates + pOpt = pOpt, # Optimized parameters (multipliers) + pRef = pRef, # References for the multipliers + xOpt = xOpt, # Optimized parameters (in actual units) + confidenceLevel = alpha, # Confidence level for the parameters + Np = Np, # Number of parameters + Nt = Nt, # Number of data points + Vp = Vp, # Covariance matrix of the multipliers + Vx = Vx, # Covariance matrix of actual parameters + chi2Statistics = chi2Statistics, # Inverse chi squared statistics + delpNonLinearized = delpNonLinearized, # Confidence intervals (intersection with axis) + delpBoundingBox = delpBoundingBox, # Confidence intervals (bounding box) + hostName = socket.gethostname()) # Hostname of the computer + +# func: computeObjectiveFunction +# Computes the objective function and the model output for a given set of +# parameters +def computeObjectiveFunction(mainDir, zlcParameterPath, pOpt, pRef): + import numpy as np + from numpy import load + from simulateCombinedModel import simulateCombinedModel + from computeMLEError import computeMLEError + from extractDeadVolume import filesToProcess # File processing script + # Parse out the experimental file names and temperatures + rawFileName = load(zlcParameterPath)["fileName"] + temperatureExp = load(zlcParameterPath)["temperature"] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,mainDir,rawFileName,'ZLC') + # Get the processed file names + fileName = filesToProcess(False,[],[],'ZLC') + + # Obtain the downsampling conditions + downsampleData = load(zlcParameterPath)["downsampleFlag"] + thresholdFactor = load(zlcParameterPath)["mleThreshold"] + + # Adsorbent density, mass of sorbent and particle epsilon + adsorbentDensity = load(zlcParameterPath)["adsorbentDensity"] + particleEpsilon = load(zlcParameterPath)["particleEpsilon"] + massSorbent = load(zlcParameterPath)["massSorbent"] + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + # Dead volume model + deadVolumeFile = str(load(zlcParameterPath)["deadVolumeFile"]) + # Get the parameter values (in actual units) + xOpt = np.multiply(pOpt,pRef) + + # Compute the downsample intervals for the experiments + # This is only to make sure that all experiments get equal weights + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Initialize variables + computedError = 0 + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # Loop over all available experiments + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + + # Initialize simulation mole fraction + moleFracSim = [] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Parse out the xOpt to the isotherm model and kinetic parameters + isothermModel = xOpt[0:-2] + rateConstant = xOpt[-2] + kineticActEnergy = xOpt[-1] + + # Compute the model response using the optimized parameters + _ , moleFracSim , resultMat = simulateCombinedModel(timeInt = timeInt, + initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point + flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = True, + isothermModel=isothermModel, + rateConstant=rateConstant, + kineticActEnergy = kineticActEnergy, + deadVolumeFile = deadVolumeFile, + volSorbent = volSorbent, + volGas = volGas, + temperature = temperatureExp[ii]) + + # Stack mole fraction from experiments and simulation for error + # computation + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) + + # Compute the MLE error of the model for the given parameters + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData, + thresholdFactor=thresholdFactor) + + # Return the objective function value, experimental and simulated output + return computedError, moleFracExpALL, moleFracSimALL \ No newline at end of file From 4fd9c1969e8e0129d81d2e9482e8e3fa52da0803 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 30 Jun 2021 11:46:46 +0100 Subject: [PATCH 130/189] Cosmetic changes --- experimental/analyzeExperimentWrapper.m | 35 ++++++++++++++----------- experimental/runMultipleZLC.m | 26 +++++++----------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/experimental/analyzeExperimentWrapper.m b/experimental/analyzeExperimentWrapper.m index db1f028..d7216ab 100644 --- a/experimental/analyzeExperimentWrapper.m +++ b/experimental/analyzeExperimentWrapper.m @@ -36,20 +36,20 @@ % Flow rate files for calibration msCalibrationFiles = {'ZLCCalibrateMS_20210506_15ccm',... - 'ZLCCalibrateMS_20210506_30ccm',... - 'ZLCCalibrateMS_20210506_45ccm',... - 'ZLCCalibrateMS_20210507_60ccm'}; + 'ZLCCalibrateMS_20210506_30ccm',... + 'ZLCCalibrateMS_20210506_45ccm',... + 'ZLCCalibrateMS_20210507_60ccm'}; %%%% Experimet to be analyzed %%%% % List the experiments that have to be analyzed -msExpFile = 'ZLC_ActivatedCarbon_Exp26_28'; % Raw MS data file name +% MS Raw data should contain only two gases and the pressure. For now +% cannot handle more gases. +msExpFile = 'ZLC_Zeolite13X_Exp27_28'; % Raw MS data file name % Flow rate files for experiments -experimentFiles = {'ZLC_ActivatedCarbon_Exp26A',... - 'ZLC_ActivatedCarbon_Exp26B',... - 'ZLC_ActivatedCarbon_Exp26C',... - 'ZLC_ActivatedCarbon_Exp26D',... - 'ZLC_ActivatedCarbon_Exp26E',... - 'ZLC_ActivatedCarbon_Exp26F'}; +experimentFiles = {'ZLC_Zeolite13X_Exp27A',... + 'ZLC_Zeolite13X_Exp27B',... + 'ZLC_Zeolite13X_Exp28A',... + 'ZLC_Zeolite13X_Exp28B'}; % Initialize the name of the msRawFile to be used for all calibrations startInd = 1; @@ -86,7 +86,7 @@ experimentStruct.MS = [msFileDir,filesep,msExpFile,'.asc']; % Experimental MS file (.asc). Assumes name of file to be the date of the first flow rate experimentStruct.calibrationMS = msCalibrationFiles; % Experimental calibration file list experimentStruct.interpMS = false; % Flag for interpolating flow data, to have a higher resolution for actual experiments - experimentStruct.moleFracThreshold = 5e-3; % Threshold for cutting off data below a given mole fraction + experimentStruct.moleFracThreshold = 1e-2; % Threshold for cutting off data below a given mole fraction % Call the analyzeExperiment function to analyze the experimental data % using the calibration files given by msCalibrationFiles % The output is usually in runData folder @@ -97,24 +97,29 @@ % Loop through all the experimental files and plot the output mole fraction if ~isempty(experimentFiles) - colorForPlot = {'5C73B9','7262C3','8852CD','9D41D7','B330E1'}; + colorForPlot = {'5C73B9','7262C3','8852CD','9D41D7','B330E1',... + '5C73B9','7262C3','8852CD','9D41D7','B330E1',... + '5C73B9','7262C3','8852CD','9D41D7','B330E1',... + '5C73B9','7262C3','8852CD','9D41D7','B330E1',... + '5C73B9','7262C3','8852CD','9D41D7','B330E1',... + '5C73B9','7262C3','8852CD','9D41D7','B330E1'}; f1 = figure('Units','inch','Position',[2 2 7 3.3]); for ii = 1:length(experimentFiles) load([experimentFiles{ii},'_Output']); % Plot the output from different experiments (in y and Ft plots) figure(f1); subplot(1,2,1) - semilogy(experimentOutput.timeExp,experimentOutput.moleFrac,'color',['#',colorForPlot{ii}]); + semilogy(experimentOutput.timeExp,experimentOutput.moleFrac,'color','b'); hold on box on;grid on; - xlim([0,500]); ylim([0,1]); + xlim([0,300]); ylim([0,1]); xlabel('{\it{t}} [s]'); ylabel('{\it{y}} [-]'); set(gca,'FontSize',8) subplot(1,2,2) semilogy(experimentOutput.timeExp.*experimentOutput.totalFlowRate,experimentOutput.moleFrac,'color',['#',colorForPlot{ii}]); hold on - xlim([0,100]); ylim([0,1]); + xlim([0,10]); ylim([0,1]); xlabel('{\it{Ft}} [cc]'); ylabel('{\it{y}} [-]'); set(gca,'FontSize',8) box on;grid on; diff --git a/experimental/runMultipleZLC.m b/experimental/runMultipleZLC.m index 9427d79..f5607ee 100644 --- a/experimental/runMultipleZLC.m +++ b/experimental/runMultipleZLC.m @@ -23,19 +23,16 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function runMultipleZLC + run('C:\Users\QCPML\Desktop\Ashwin\ERASE\setPathERASE.m') % Series name for the experiments - expSeries = {'ZLC_ActivatedCarbon_Exp26',... - 'ZLC_ActivatedCarbon_Exp27',... - 'ZLC_ActivatedCarbon_Exp28',... - 'ZLC_ActivatedCarbon_Exp29',... - 'ZLC_ActivatedCarbon_Exp30',... - 'ZLC_ActivatedCarbon_Exp31'}; + expSeries = {'ZLC_Zeolite13X_Exp27',... + 'ZLC_Zeolite13X_Exp28'}; % Maximum time of the experiment - expInfo.maxTime = 400; + expInfo.maxTime = 300; % Sampling time for the device expInfo.samplingTime = 1; % Intervals for collecting MFC data - expInfo.MFCInterval = 300; + expInfo.MFCInterval = 100; % Define gas for MFM expInfo.gasName_MFM = 'He'; % Define gas for MFC1 @@ -43,14 +40,11 @@ % Define gas for MFC2 expInfo.gasName_MFC2 = 'CO2'; % Total flow rate - expTotalFlowRate = [10, 10, 10, 10, 10, 10;... - 15, 15, 15, 15, 15, 15;... - 30, 30, 30, 30, 30, 30;... - 45, 45, 45, 45, 45, 45;... - 60, 60, 60, 60, 60, 60;... - 80, 80, 80, 80, 80, 80]; + expTotalFlowRate = [10, 10; + 60, 60]; % Fraction CO2 - fracCO2 = repmat([1/8 1/3 1 2 4 10],[length(expSeries),1]); + fracCO2 = [1/8, 16;... + 1/8, 2.66]; % Define set point for MFC1 % Round the flow rate to the nearest first decimal (as this is the % resolution of the meter) @@ -60,7 +54,7 @@ % resolution of the meter) MFC2_SP = round(fracCO2.*expTotalFlowRate,1); % Start delay (used for adsorbent equilibration) - equilibrationTime = repmat([7200 3600 3600 3600 3600 3600],[length(expSeries),1]); % [s] + equilibrationTime = repmat([900, 900],[length(expSeries),1]); % [s] % Flag for meter calibration expInfo.calibrateMeters = false; % Mixtures Flag - When a T junction instead of 6 way valve used From 0a303ec49b729d937e1dfa0e6d5741f84a5bdad9 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 1 Jul 2021 16:45:19 +0100 Subject: [PATCH 131/189] Changes to plot functions --- plotFunctions/plotExperimentOutcome.py | 249 ++++++++++-------- .../plotIsothermComparisonMultiParam.py | 156 +++++++++++ 2 files changed, 288 insertions(+), 117 deletions(-) create mode 100644 plotFunctions/plotIsothermComparisonMultiParam.py diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 301c115..ba38d4d 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-07-01, AK: Cosmetic changes # - 2021-05-14, AK: Fixes and structure changes # - 2021-05-14, AK: Improve plotting capabilities # - 2021-05-05, AK: Bug fix for MLE error computation @@ -30,13 +31,13 @@ import numpy as np from computeMLEError import computeMLEError -from computeEquilibriumLoading import computeEquilibriumLoading from deadVolumeWrapper import deadVolumeWrapper from extractDeadVolume import filesToProcess # File processing script from numpy import load import os import matplotlib.pyplot as plt import auxiliaryFunctions +from datetime import datetime plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -59,44 +60,26 @@ # Flag to plot dead volume results plotFt = False - -# Adsorbent density [kg/m3] -# This has to be the skeletal density -adsorbentDensity = 1950 # Activated carbon skeletal density [kg/m3] - -# Particle porosity -particleEpsilon = 0.61 - -# Particle mass [g] -massSorbent = 0.0645 - -# Volume of sorbent material [m3] -volSorbent = (massSorbent/1000)/adsorbentDensity - -# Volume of gas chamber (dead volume) [m3] -volGas = volSorbent/(1-particleEpsilon)*particleEpsilon # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); - -# Temperature of the gas [K] -temperature = np.array([298.15]); - # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' -colorsForPlot = ["#E5383B","#E5383B","#B55055","#B55055","#6C757D","#6C757D"] -colorsForPlot = ["#E5383B","#CD4448","#B55055", - "#9C5D63","#846970","#6C757D"] -# colorsForPlot = ["#E5383B","#6C757D"] -markerForPlot = ["o","v","o","v","o","v"] +# colorsForPlot = ["#E5383B","#CD4448","#B55055", +# "#9C5D63","#846970","#6C757D"] +colorsForPlot = ["#FF1B6B","#A273B5","#45CAFF"]*2 +# colorsForPlot = ["#E5383B","#6C757D",] +markerForPlot = ["o","v","o","v","o","v","o","v","o","v","o","v"] if flagDeadVolume: # File name of the experiments - rawFileName = ['ZLC_DeadVolume_Exp16B_Output.mat', - 'ZLC_DeadVolume_Exp16C_Output.mat', - 'ZLC_DeadVolume_Exp16D_Output.mat'] + rawFileName = ['ZLC_DeadVolume_Exp20A_Output.mat', + 'ZLC_DeadVolume_Exp20B_Output.mat', + 'ZLC_DeadVolume_Exp20C_Output.mat', + 'ZLC_DeadVolume_Exp20D_Output.mat', + 'ZLC_DeadVolume_Exp20E_Output.mat',] # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,rawFileName,'DV') # Get the processed file names @@ -104,10 +87,15 @@ # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210528_1319_318b280.npz' + fileParameter = 'deadVolumeCharacteristics_20210613_0847_8313a04.npz' + fileNameList = load(simulationDir+fileParameter, allow_pickle=True)["fileName"] modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] - + + # This was added on 12.06 (not back compatible for error computation) + downsampleData = load(simulationDir+fileParameter)["downsampleFlag"] + thresholdFactor = load(simulationDir+fileParameter)["mleThreshold"] + # Get the MS fit flag, flow rates and msDeadVolumeFile (if needed) # Check needs to be done to see if MS file available or not # Checked using flagMSDeadVolume in the saved file @@ -181,16 +169,17 @@ np.multiply(flowRateExp, timeElapsedExp)),2)) - # Stack mole fraction from experiments and simulation - moleFracExpALL = np.hstack((moleFracExpALL, moleFracExp)) - moleFracSimALL = np.hstack((moleFracSimALL, moleFracSim)) - + # Stack mole fraction from experiments and simulation for error + # computation + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/(np.max(moleFracExp-np.min(moleFracExp))))) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/(np.max(moleFracSim-np.min(moleFracSim))))) + # Plot the expreimental and model output if not plotFt: # Linear scale ax1.plot(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.1,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax1.plot(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -204,12 +193,12 @@ # Log scale ax2.semilogy(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.05,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', - xlim = [0,150], ylim = [5e-3, 1]) + xlim = [0,150], ylim = [1e-3, 1]) ax2.locator_params(axis="x", nbins=5) @@ -218,7 +207,7 @@ # FileName: deadVolumeCharacteristics___ saveFileName = "deadVolumeCharacteristics_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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) @@ -226,7 +215,7 @@ # Linear scale ax1.plot(np.multiply(flowRateDV,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateDV),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.05,label=str(round(np.mean(flowRateDV),2))+" ccs") # Experimental response if simulateModel: ax1.plot(np.multiply(flowRateDV,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -238,7 +227,7 @@ # Log scale ax2.semilogy(np.multiply(flowRateDV,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateDV),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.05,label=str(round(np.mean(flowRateDV),2))+" ccs") # Experimental response if simulateModel: ax2.semilogy(np.multiply(flowRateDV,timeElapsedExp),moleFracSim, color=colorsForPlot[ii]) # Simulation response @@ -251,42 +240,69 @@ # FileName: deadVolumeCharacteristicsFt___ saveFileName = "deadVolumeCharacteristicsFt_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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) # Print the MLE error - computedError = computeMLEError(moleFracExpALL,moleFracSimALL) + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData, + thresholdFactor=thresholdFactor) print(round(computedError,0)) # Remove all the .npy files genereated from the .mat # Loop over all available files for ii in range(len(fileName)): os.remove(fileName[ii]) - - + else: from simulateCombinedModel import simulateCombinedModel # Directory of raw data mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' # File name of the experiments - rawFileName = ['ZLC_ActivatedCarbon_Exp36A_Output.mat', - 'ZLC_ActivatedCarbon_Exp36D_Output.mat'] + rawFileName = ['ZLC_ActivatedCarbon_Exp60A_Output.mat', + 'ZLC_ActivatedCarbon_Exp62A_Output.mat', + 'ZLC_ActivatedCarbon_Exp64A_Output.mat', + 'ZLC_ActivatedCarbon_Exp60B_Output.mat', + 'ZLC_ActivatedCarbon_Exp62B_Output.mat', + 'ZLC_ActivatedCarbon_Exp64B_Output.mat'] + # Legend flag + useFlow = False + # Temperature (for each experiment) + temperatureExp = [306.44, 325.98, 345.17]*2 # Generate .npz file for python processing of the .mat file filesToProcess(True,mainDir,rawFileName,'ZLC') # Get the processed file names fileName = filesToProcess(False,[],[],'ZLC') # File with parameter estimates simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - # Dead volume model - deadVolumeFile = 'deadVolumeCharacteristics_20210528_1319_318b280.npz' # ZLC parameter model - fileParameter = 'zlcParameters_20210525_2337_7c0209e.npz' + fileParameter = 'zlcParameters_20210701_0843_4fd9c19.npz' + # Mass of sorbent and particle epsilon + adsorbentDensity = load(simulationDir+fileParameter)["adsorbentDensity"] + particleEpsilon = load(simulationDir+fileParameter)["particleEpsilon"] + massSorbent = load(simulationDir+fileParameter)["massSorbent"] + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + + # Dead volume model + deadVolumeFile = str(load(simulationDir+fileParameter)["deadVolumeFile"]) + # Isotherm parameter reference + parameterReference = load(simulationDir+fileParameter)["parameterReference"] + # Load the model modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] - print("Objective Function",round(modelOutputTemp[()]["function"],0)) modelNonDim = modelOutputTemp[()]["variable"] + + # This was added on 12.06 (not back compatible for error computation) + downsampleData = load(simulationDir+fileParameter)["downsampleFlag"] + thresholdFactor = load(simulationDir+fileParameter)["mleThreshold"] + print("Objective Function",round(modelOutputTemp[()]["function"],0)) + numPointsExp = np.zeros(len(fileName)) for ii in range(len(fileName)): fileToLoad = fileName[ii] @@ -296,18 +312,15 @@ # Downsample intervals downsampleInt = numPointsExp/np.min(numPointsExp) - # Multiply the paremeters by the reference values (for SSL) - x = np.multiply(modelNonDim,[10, 1e-5, 50e3,100]) - - # Ronny AC Data - x_RP = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3,100] - + # Multiply the paremeters by the reference values + x = np.multiply(modelNonDim,parameterReference) + # Initialize loadings computedError = 0 numPoints = 0 moleFracExpALL = np.array([]) moleFracSimALL = np.array([]) - massBalanceALL = np.zeros((len(fileName),2)) + massBalanceALL = np.zeros((len(fileName),3)) # Create the instance for the plots fig = plt.figure @@ -336,64 +349,102 @@ print("Experiment",str(ii+1),round(np.trapz(np.multiply(flowRateExp,moleFracExp),timeElapsedExp),2)) if simulateModel: + # Get the date time of the parameter estimates + parameterDateTime = datetime.strptime(fileParameter[14:27], '%Y%m%d_%H%M') + # Date time when the kinetic model shifted from 1 parameter to 2 parameter + parameterSwitchTime = datetime.strptime('20210616_0800', '%Y%m%d_%H%M') + # When 2 parameter model absent + if parameterDateTime__ saveFileName = "zlcCharacteristics_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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) @@ -410,47 +461,11 @@ plt.show() # Print the MLE error - computedError = computeMLEError(moleFracExpALL,moleFracSimALL) - print(round(computedError,0)) - - # Create the grid for mole fractions - y = np.linspace(0,1.,100) - # Initialize isotherms - isoLoading_RP = np.zeros([len(y)]) - isoLoading_ZLC = np.zeros([len(y)]) - - # Loop over all the mole fractions - for ii in range(len(y)): - isoLoading_RP[ii] = computeEquilibriumLoading(isothermModel=x_RP[0:-1], - moleFrac = y[ii]) - isoLoading_ZLC[ii] = computeEquilibriumLoading(isothermModel=x[0:-1], - moleFrac = y[ii]) - - # Plot the isotherms - os.chdir(os.path.join('..','plotFunctions')) - plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file - fig = plt.figure - ax1 = plt.subplot(1,1,1) - ax1.plot(y,isoLoading_RP,color='#2a9d8f',label="Autosorb") # Ronny's isotherm - ax1.plot(y,isoLoading_ZLC,color='#e76f51',label="ZLC") # ALL - ax1.scatter(massBalanceALL[:,0],massBalanceALL[:,1],c='dimgrey') - ax1.set(xlabel='$P$ [bar]', - ylabel='$q^*$ [mol kg$^\mathregular{-1}$]', - xlim = [0,0.8], ylim = [0, 3]) - ax1.locator_params(axis="x", nbins=4) - ax1.locator_params(axis="y", nbins=4) - ax1.legend() - # Save the figure - if saveFlag: - # FileName: isothermComparison___ - saveFileName = "isothermComparison_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-25:-12] + 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() + if simulateModel: + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData = downsampleData, + thresholdFactor = 0.5) + print(round(computedError,0)) # Remove all the .npy files genereated from the .mat # Loop over all available files diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py new file mode 100644 index 0000000..f505ff3 --- /dev/null +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -0,0 +1,156 @@ +############################################################################ +# +# 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 comparison of isotherms obtained from different devices and +# different fits from ZLC +# +# Last modified: +# - 2021-07-01, AK: Cosmetic changes +# - 2021-06-15, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from computeEquilibriumLoading import computeEquilibriumLoading +import matplotlib.pyplot as plt +from matplotlib.ticker import MaxNLocator +from datetime import datetime +import os +from numpy import load +import auxiliaryFunctions +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 flag and file name extension +saveFlag = False +saveFileExtension = ".png" + +# Colors +colorForPlot = ["ff1b6b","a273b5","45caff"] +# colorForPlot = ["E5383B","6C757D"] + +# Plot text +plotText = 'DSL' + +# Define temperature +temperature = [306.44, 325.98] + +# AC Isotherm parameters +x_VOL = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3, 100] # (Pini 2020) +# x_VOL = [2.81e-5, 1.25e-7, 2.07e2, 4.12, 7.29e-7, 2.65e4, 100] # (Hassan, QC) +# 13X Isotherm parameters (Haghpanah 2013) +# x_VOL = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4, 100] # (Hassan, QC) + +# File with parameter estimates from ZLC +simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' + +# Experiment 43 and 48 +# zlcFileName = ['zlcParameters_20210618_1837_36d3aa3.npz', +# 'zlcParameters_20210618_2209_36d3aa3.npz', +# 'zlcParameters_20210619_0128_36d3aa3.npz', +# 'zlcParameters_20210619_0447_36d3aa3.npz', +# 'zlcParameters_20210619_0759_36d3aa3.npz',] + +# Experiment 60 - +zlcFileName = ['zlcParameters_20210701_0843_4fd9c19.npz',] + +# Create the grid for mole fractions +y = np.linspace(0,1.,100) +# Initialize isotherms +isoLoading_VOL = np.zeros([len(y),len(temperature)]) +isoLoading_VOL_HA = np.zeros([len(y),len(temperature)]) +isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) +objectiveFunction = np.zeros([len(zlcFileName)]) + +# Loop over all the mole fractions +# Volumetric data +for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_VOL[ii,jj] = computeEquilibriumLoading(isothermModel=x_VOL[0:-1], + moleFrac = y[ii], + temperature = temperature[jj]) +# Loop over all available ZLC files +for kk in range(len(zlcFileName)): + # ZLC Data + parameterReference = load(simulationDir+zlcFileName[kk])["parameterReference"] + modelOutputTemp = load(simulationDir+zlcFileName[kk], allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + modelNonDim = modelOutputTemp[()]["variable"] + # Get the date time of the parameter estimates + parameterDateTime = datetime.strptime(zlcFileName[kk][14:27], '%Y%m%d_%H%M') + # Date time when the kinetic model shifted from 1 parameter to 2 parameter + parameterSwitchTime = datetime.strptime('20210616_0800', '%Y%m%d_%H%M') + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + # When 2 parameter model absent + if parameterDateTime__ + saveFileName = "isothermComparison_" + currentDT + "_" + gitCommitID + "_" + zlcFileName[-25:-12] + 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 1c447a021036798708cedbedca15bddddd387f8b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 1 Jul 2021 18:29:41 +0100 Subject: [PATCH 132/189] Cosmetic changes for the plots --- plotFunctions/plotExperimentOutcome.py | 88 ++++++++++--------- .../plotIsothermComparisonMultiParam.py | 19 ++-- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index ba38d4d..1eaf469 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -52,8 +52,15 @@ # Save file extension saveFileExtension = ".png" +# File with parameter estimates +fileParameter = 'deadVolumeCharacteristics_20210613_0847_8313a04.npz' + # Flag to plot dead volume results -flagDeadVolume = False +# Dead volume files have a certain name, use that to find what to plot +if fileParameter[0:10] == 'deadVolume': + flagDeadVolume = True +else: + flagDeadVolume = False # Flag to plot simulations simulateModel = True @@ -64,14 +71,12 @@ # Total pressure of the gas [Pa] pressureTotal = np.array([1.e5]); -# Directory of raw data -mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' - +# Plot colors # colorsForPlot = ["#E5383B","#CD4448","#B55055", # "#9C5D63","#846970","#6C757D"] colorsForPlot = ["#FF1B6B","#A273B5","#45CAFF"]*2 # colorsForPlot = ["#E5383B","#6C757D",] -markerForPlot = ["o","v","o","v","o","v","o","v","o","v","o","v"] +markerForPlot = ["o"]*len(colorsForPlot) if flagDeadVolume: # File name of the experiments @@ -80,26 +85,27 @@ 'ZLC_DeadVolume_Exp20C_Output.mat', 'ZLC_DeadVolume_Exp20D_Output.mat', 'ZLC_DeadVolume_Exp20E_Output.mat',] + + # Dead volume parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + # Generate .npz file for python processing of the .mat file - filesToProcess(True,mainDir,rawFileName,'DV') + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'DV') # Get the processed file names fileName = filesToProcess(False,[],[],'DV') - - # File with parameter estimates - simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - fileParameter = 'deadVolumeCharacteristics_20210613_0847_8313a04.npz' - fileNameList = load(simulationDir+fileParameter, allow_pickle=True)["fileName"] - modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] + # Load file names and the model + fileNameList = load(parameterPath, allow_pickle=True)["fileName"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] # This was added on 12.06 (not back compatible for error computation) - downsampleData = load(simulationDir+fileParameter)["downsampleFlag"] - thresholdFactor = load(simulationDir+fileParameter)["mleThreshold"] + downsampleData = load(parameterPath)["downsampleFlag"] + thresholdFactor = load(parameterPath)["mleThreshold"] # Get the MS fit flag, flow rates and msDeadVolumeFile (if needed) # Check needs to be done to see if MS file available or not # Checked using flagMSDeadVolume in the saved file - dvFileLoadTemp = load(simulationDir+fileParameter) + dvFileLoadTemp = load(parameterPath) if 'flagMSDeadVolume' in dvFileLoadTemp.files: flagMSFit = dvFileLoadTemp["flagMSFit"] msFlowRate = dvFileLoadTemp["msFlowRate"] @@ -246,10 +252,11 @@ plt.savefig (savePath) # Print the MLE error - computedError = computeMLEError(moleFracExpALL,moleFracSimALL, - downsampleData=downsampleData, - thresholdFactor=thresholdFactor) - print(round(computedError,0)) + if simulateModel: + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData, + thresholdFactor=thresholdFactor) + print("Sanity check objective function: ",round(computedError,0)) # Remove all the .npy files genereated from the .mat # Loop over all available files @@ -258,9 +265,7 @@ else: from simulateCombinedModel import simulateCombinedModel - - # Directory of raw data - mainDir = '/Users/ash23win/Google Drive/ERASE/experimental/runData/' + # File name of the experiments rawFileName = ['ZLC_ActivatedCarbon_Exp60A_Output.mat', 'ZLC_ActivatedCarbon_Exp62A_Output.mat', @@ -268,22 +273,24 @@ 'ZLC_ActivatedCarbon_Exp60B_Output.mat', 'ZLC_ActivatedCarbon_Exp62B_Output.mat', 'ZLC_ActivatedCarbon_Exp64B_Output.mat'] - # Legend flag - useFlow = False + + # ZLC parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + # Temperature (for each experiment) temperatureExp = [306.44, 325.98, 345.17]*2 + + # Legend flag + useFlow = False + # Generate .npz file for python processing of the .mat file - filesToProcess(True,mainDir,rawFileName,'ZLC') + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'ZLC') # Get the processed file names fileName = filesToProcess(False,[],[],'ZLC') - # File with parameter estimates - simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' - # ZLC parameter model - fileParameter = 'zlcParameters_20210701_0843_4fd9c19.npz' # Mass of sorbent and particle epsilon - adsorbentDensity = load(simulationDir+fileParameter)["adsorbentDensity"] - particleEpsilon = load(simulationDir+fileParameter)["particleEpsilon"] - massSorbent = load(simulationDir+fileParameter)["massSorbent"] + adsorbentDensity = load(parameterPath)["adsorbentDensity"] + particleEpsilon = load(parameterPath)["particleEpsilon"] + massSorbent = load(parameterPath)["massSorbent"] # Volume of sorbent material [m3] volSorbent = (massSorbent/1000)/adsorbentDensity @@ -291,16 +298,16 @@ volGas = volSorbent/(1-particleEpsilon)*particleEpsilon # Dead volume model - deadVolumeFile = str(load(simulationDir+fileParameter)["deadVolumeFile"]) + deadVolumeFile = str(load(parameterPath)["deadVolumeFile"]) # Isotherm parameter reference - parameterReference = load(simulationDir+fileParameter)["parameterReference"] + parameterReference = load(parameterPath)["parameterReference"] # Load the model - modelOutputTemp = load(simulationDir+fileParameter, allow_pickle=True)["modelOutput"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] modelNonDim = modelOutputTemp[()]["variable"] # This was added on 12.06 (not back compatible for error computation) - downsampleData = load(simulationDir+fileParameter)["downsampleFlag"] - thresholdFactor = load(simulationDir+fileParameter)["mleThreshold"] + downsampleData = load(parameterPath)["downsampleFlag"] + thresholdFactor = load(parameterPath)["mleThreshold"] print("Objective Function",round(modelOutputTemp[()]["function"],0)) numPointsExp = np.zeros(len(fileName)) @@ -395,9 +402,10 @@ massBalanceALL[ii,2] = volGas*moleFracExp[0]*(pressureTotal/(8.314*temperatureExp[ii]))/(massSorbent/1000) # Call the deadVolume Wrapper function to obtain the outlet mole fraction - modelOutputTemp = load(simulationDir+deadVolumeFile, allow_pickle=True)["modelOutput"] + deadVolumePath = os.path.join('..','simulationResults',deadVolumeFile) + modelOutputTemp = load(deadVolumePath, allow_pickle=True)["modelOutput"] pDV = modelOutputTemp[()]["variable"] - dvFileLoadTemp = load(simulationDir+deadVolumeFile) + dvFileLoadTemp = load(deadVolumePath) flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] moleFracDV = deadVolumeWrapper(timeInt, resultMat[3,:]*1e6, pDV, flagMSDeadVolume, msDeadVolumeFile, initMoleFrac = [moleFracExp[0]]) @@ -465,7 +473,7 @@ computedError = computeMLEError(moleFracExpALL,moleFracSimALL, downsampleData = downsampleData, thresholdFactor = 0.5) - print(round(computedError,0)) + print("Sanity check objective function: ",round(computedError,0)) # Remove all the .npy files genereated from the .mat # Loop over all available files diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py index f505ff3..f2333de 100644 --- a/plotFunctions/plotIsothermComparisonMultiParam.py +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -45,24 +45,23 @@ saveFileExtension = ".png" # Colors -colorForPlot = ["ff1b6b","a273b5","45caff"] +colorForPlot = ["FF1B6B","A273B5","45CAFF"] # colorForPlot = ["E5383B","6C757D"] # Plot text plotText = 'DSL' # Define temperature -temperature = [306.44, 325.98] +temperature = [306.44, 325.98, 345.17] # AC Isotherm parameters x_VOL = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3, 100] # (Pini 2020) # x_VOL = [2.81e-5, 1.25e-7, 2.07e2, 4.12, 7.29e-7, 2.65e4, 100] # (Hassan, QC) -# 13X Isotherm parameters (Haghpanah 2013) -# x_VOL = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4, 100] # (Hassan, QC) -# File with parameter estimates from ZLC -simulationDir = '/Users/ash23win/Google Drive/ERASE/simulationResults/' +# 13X Isotherm parameters +# x_VOL = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4, 100] # (Hassan, QC) +# ZLC parameter estimate files # Experiment 43 and 48 # zlcFileName = ['zlcParameters_20210618_1837_36d3aa3.npz', # 'zlcParameters_20210618_2209_36d3aa3.npz', @@ -90,11 +89,13 @@ temperature = temperature[jj]) # Loop over all available ZLC files for kk in range(len(zlcFileName)): - # ZLC Data - parameterReference = load(simulationDir+zlcFileName[kk])["parameterReference"] - modelOutputTemp = load(simulationDir+zlcFileName[kk], allow_pickle=True)["modelOutput"] + # ZLC Data + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + parameterReference = load(parameterPath)["parameterReference"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) modelNonDim = modelOutputTemp[()]["variable"] + # Get the date time of the parameter estimates parameterDateTime = datetime.strptime(zlcFileName[kk][14:27], '%Y%m%d_%H%M') # Date time when the kinetic model shifted from 1 parameter to 2 parameter From c2e9d9368869cba307ad2e08a649357d5e3abd9a Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 1 Jul 2021 18:43:54 +0100 Subject: [PATCH 133/189] Move sensitivity analysis --- experimental/{sensitivityAnalysis => }/sensitivityAnalysis.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename experimental/{sensitivityAnalysis => }/sensitivityAnalysis.py (100%) diff --git a/experimental/sensitivityAnalysis/sensitivityAnalysis.py b/experimental/sensitivityAnalysis.py similarity index 100% rename from experimental/sensitivityAnalysis/sensitivityAnalysis.py rename to experimental/sensitivityAnalysis.py From 07082e3fa91f44b5917f18750bd1798c381bb89b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 1 Jul 2021 19:49:32 +0100 Subject: [PATCH 134/189] Integrate sensitivity analysis to extractZLCParameters --- experimental/extractZLCParameters.py | 12 ++++++++++-- experimental/sensitivityAnalysis.py | 23 ++++++++++++----------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 1aa2b0e..c0898f5 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-07-01, AK: Add sensitivity analysis # - 2021-06-16, AK: Add temperature dependence to kinetics # - 2021-06-14, AK: More fixes for error computation # - 2021-06-12, AK: Fix for error computation (major) @@ -42,6 +43,7 @@ def extractZLCParameters(**kwargs): import numpy as np from geneticalgorithm2 import geneticalgorithm2 as ga # GA from extractDeadVolume import filesToProcess # File processing script + from sensitivityAnalysis import sensitivityAnalysis import auxiliaryFunctions import os from numpy import savez @@ -116,6 +118,9 @@ def extractZLCParameters(**kwargs): # Note that this is also needed for (pure) downsampling data thresholdFactor = 0.5 + # Confidence interval for the sensitivity analysis + alpha = 0.95 + ##################################### ##################################### @@ -195,11 +200,11 @@ def extractZLCParameters(**kwargs): model.run(set_function=ga.set_function_multiprocess(ZLCObjectiveFunction, n_jobs = num_cores), start_generation=model.output_dict['last_generation'], no_plot = True) - + # Save the zlc parameters into a native numpy file # The .npz file is saved in a folder called simulationResults (hardcoded) filePrefix = "zlcParameters" - saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID; + saveFileName = filePrefix + "_" + currentDT + "_" + gitCommitID savePath = os.path.join('..','simulationResults',saveFileName) # Check if simulationResults directory exists or not. If not, create the folder @@ -229,6 +234,9 @@ def extractZLCParameters(**kwargs): # Loop over all available files for ii in range(len(filePath)): os.remove(filePath[ii]) + + # Perform the sensitivity analysis with the optimized parameters + sensitivityAnalysis(saveFileName, alpha) # Return the optimized values return model.output_dict diff --git a/experimental/sensitivityAnalysis.py b/experimental/sensitivityAnalysis.py index ae37008..f213a08 100644 --- a/experimental/sensitivityAnalysis.py +++ b/experimental/sensitivityAnalysis.py @@ -15,6 +15,7 @@ # provided in the code # # Last modified: +# - 2021-07-01, AK: Change structure (to call from extractZLCParameters) # - 2021-06-28, AK: Initial creation # # Input arguments: @@ -25,8 +26,9 @@ # ############################################################################ -def sensitivityAnalysis(): +def sensitivityAnalysis(fileParameter,alpha): import auxiliaryFunctions + from extractDeadVolume import filesToProcess # File processing script import numpy as np from numpy import load from numpy import savez @@ -36,23 +38,15 @@ def sensitivityAnalysis(): from scipy.stats import chi2 import socket - # Change directory to avoid path issues - os.chdir("..") - # Get the commit ID of the current repository gitCommitID = auxiliaryFunctions.getCommitID() - - # Confidence level - alpha = 0.95 # Directory of raw data mainDir = 'runData' # File with parameter estimates simulationDir = os.path.join('..','simulationResults') - # ZLC parameter model - fileParameter = 'zlcParameters_20210619_0128_36d3aa3.npz' # ZLC parameter path - zlcParameterPath = os.path.join(simulationDir,fileParameter) + zlcParameterPath = os.path.join(simulationDir,fileParameter+'.npz') # Parse out the optimized model parameters # Note that this is nondimensional (reference value in the function) pOptTemp = load(zlcParameterPath, allow_pickle=True)["modelOutput"] @@ -142,7 +136,7 @@ def sensitivityAnalysis(): # Save the sensitivity analysis output in .npz file # The .npz file is saved in a folder called simulationResults (hardcoded) filePrefix = "sensitivityAnalysis" - saveFileName = filePrefix + "_" + fileParameter[0:-12] + "_" + gitCommitID; + saveFileName = filePrefix + "_" + fileParameter[0:-8] + "_" + gitCommitID; savePath = os.path.join('..','simulationResults',saveFileName) # Check if inputResources directory exists or not. If not, create the folder @@ -164,6 +158,13 @@ def sensitivityAnalysis(): delpBoundingBox = delpBoundingBox, # Confidence intervals (bounding box) hostName = socket.gethostname()) # Hostname of the computer + # Remove all the .npy files genereated from the .mat + # Load the names of the file to be used for estimating ZLC parameters + filePath = filesToProcess(False,[],[],'ZLC') + # Loop over all available files + for ii in range(len(filePath)): + os.remove(filePath[ii]) + # func: computeObjectiveFunction # Computes the objective function and the model output for a given set of # parameters From 72e44f27d99018923f332b6dfb9ecdd1496e9179 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 2 Jul 2021 09:27:29 +0100 Subject: [PATCH 135/189] Bug fix in computeMLEError --- experimental/computeMLEError.py | 63 ++++---------------------- plotFunctions/plotExperimentOutcome.py | 6 +-- 2 files changed, 12 insertions(+), 57 deletions(-) diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py index 1504f1a..5f5155e 100644 --- a/experimental/computeMLEError.py +++ b/experimental/computeMLEError.py @@ -12,6 +12,7 @@ # Computes the MLE error for ZLC experiments. # # Last modified: +# - 2021-07-02, AK: Bug fix for data sorting # - 2021-06-12, AK: Add pure data downsampling # - 2021-05-24, AK: Add -inf input to avoid splitting compositions # - 2021-05-13, AK: Add different modes for MLE error computations @@ -36,65 +37,15 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): thresholdFactor = np.array(kwargs["thresholdFactor"]) else: thresholdFactor = 0.5 # Default set to 0.5 (as data scaled between 0 and 1) - # Default is false, uses either pure MLE or split MLE with scaling based - # on threshold + # Default is false, uses pure MLE with no downsampling else: downsampleData = False - # Check if threshold is provided to split data to high and low - # compositions and scale for MLE computation - if 'thresholdFactor' in kwargs: - thresholdFlag = True - thresholdFactor = np.array(kwargs["thresholdFactor"]) - # If negative infinity provided as a threshold, do not split and uses - # all data with equal weights - if np.isneginf(thresholdFactor): - thresholdFlag = False - # Default is false, uses all data with equal weights - else: - thresholdFlag = False # If not pure downsampling of the data at different composition ranges if not downsampleData: # If no threshold provided just do normal MLE - if not thresholdFlag: - computedError = np.log(np.sum(np.power(moleFracExp - moleFracSim, 2))) - numPoints = len(moleFracExp) - # If threshold provided, split the data to two and compute the error - else: - #### Quite a lot of bugs because of changing how error computed ### - #### DO NOT USE THIS!!! ### - # Objective function error - # Find error for mole fraction below a given threshold - lastIndThreshold = int(np.argwhere(np.array(moleFracExp)>thresholdFactor)[-1]) - # Do downsampling if the number of points in higher and lower - # compositions does not match - numPointsConc = np.zeros([2]) - numPointsConc[0] = len(moleFracExp[0:lastIndThreshold]) # High composition - numPointsConc[1] = len(moleFracExp[lastIndThreshold:-1]) # Low composition - downsampleConc = numPointsConc/np.min(numPointsConc) # Downsampled intervals - - # Compute error for higher concentrations (accounting for downsampling) - moleFracHighExp = moleFracExp[0:lastIndThreshold] - moleFracHighSim = moleFracSim[0:lastIndThreshold] - computedErrorHigh = np.log(np.sum(np.power(moleFracHighExp[::int(np.round(downsampleConc[0]))] - - moleFracHighSim[::int(np.round(downsampleConc[0]))],2))) - - # Find scaling factor for lower concentrations - scalingFactor = int(1/thresholdFactor) # Assumes max composition is one - # Compute error for lower concentrations - moleFracLowExp = moleFracExp[lastIndThreshold:-1]*scalingFactor - moleFracLowSim = moleFracSim[lastIndThreshold:-1]*scalingFactor - - # Compute error for low concentrations (accounting for downsampling) - computedErrorLow = np.log(np.sum(np.power(moleFracLowExp[::int(np.round(downsampleConc[1]))] - - moleFracLowSim[::int(np.round(downsampleConc[1]))],2))) - - # Find the sum of computed error - computedError = computedErrorHigh + computedErrorLow - - # Compute the number of points per experiment (accouting for down- - # sampling in both experiments and high and low compositions - numPoints = len(moleFracHighExp) + len(moleFracLowExp) + computedError = np.log(np.sum(np.power(moleFracExp - moleFracSim, 2))) + numPoints = len(moleFracExp) # Pure downsampling of the data at different composition ranges else: # Objective function error @@ -104,7 +55,11 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): sortedData = concatenatedData[np.argsort(concatenatedData[:,0]),:] # Find error for mole fraction below a given threshold lastIndThreshold = int(np.argwhere(sortedData[:,0]>thresholdFactor)[0]) - + + # Reinitialize mole fraction experimental and simulation based on sorted data + moleFracExp = sortedData[:,0] # Experimental + moleFracSim = sortedData[:,1] # Simulation + # Do downsampling if the number of points in higher and lower # compositions does not match numPointsConc = np.zeros([2]) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 1eaf469..96b2bf7 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -53,7 +53,7 @@ saveFileExtension = ".png" # File with parameter estimates -fileParameter = 'deadVolumeCharacteristics_20210613_0847_8313a04.npz' +fileParameter = 'zlcParameters_20210702_0207_07082e3.npz' # Flag to plot dead volume results # Dead volume files have a certain name, use that to find what to plot @@ -394,7 +394,7 @@ normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) - + # Compute the mass balance at the end end of the ZLC massBalanceALL[ii,0] = moleFracExp[0] massBalanceALL[ii,1] = ((np.trapz(np.multiply(resultMat[3,:],resultMat[0,:]),timeElapsedExp) @@ -472,7 +472,7 @@ if simulateModel: computedError = computeMLEError(moleFracExpALL,moleFracSimALL, downsampleData = downsampleData, - thresholdFactor = 0.5) + thresholdFactor = thresholdFactor) print("Sanity check objective function: ",round(computedError,0)) # Remove all the .npy files genereated from the .mat From a021de6133e36755644b8244f8825c03f3e48479 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 2 Jul 2021 11:51:31 +0100 Subject: [PATCH 136/189] Optimization bug fix and add kinetic plots --- experimental/extractDeadVolume.py | 6 +- experimental/extractZLCParameters.py | 2 +- plotFunctions/plotExperimentOutcome.py | 18 +++--- .../plotIsothermComparisonMultiParam.py | 61 +++++++++++++++++-- 4 files changed, 72 insertions(+), 15 deletions(-) diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index 3f89283..f648f8a 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -264,8 +264,10 @@ def deadVolObjectiveFunction(x): # computation # Normalize the mole fraction by dividing it by maximum value to avoid # irregular weightings for different experiment (at diff. scales) - moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/(np.max(moleFracExp-np.min(moleFracExp))))) - moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/(np.max(moleFracSim-np.min(moleFracSim))))) + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - minExp) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) # Penalize if the total volume of the system is greater than experiemntal # volume diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index c0898f5..4042d55 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -316,7 +316,7 @@ def ZLCObjectiveFunction(x): # Normalize the mole fraction by dividing it by maximum value to avoid # irregular weightings for different experiment (at diff. scales) minExp = np.min(moleFracExp) # Compute the minimum from experiment - normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data + normalizeFactor = np.max(moleFracExp - minExp) # Compute the max from normalized data moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 96b2bf7..cfe43ee 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -53,7 +53,7 @@ saveFileExtension = ".png" # File with parameter estimates -fileParameter = 'zlcParameters_20210702_0207_07082e3.npz' +fileParameter = 'deadVolumeCharacteristics_20210624_1149_b092465.npz' # Flag to plot dead volume results # Dead volume files have a certain name, use that to find what to plot @@ -63,7 +63,7 @@ flagDeadVolume = False # Flag to plot simulations -simulateModel = True +simulateModel = False # Flag to plot dead volume results plotFt = False @@ -72,9 +72,9 @@ pressureTotal = np.array([1.e5]); # Plot colors -# colorsForPlot = ["#E5383B","#CD4448","#B55055", -# "#9C5D63","#846970","#6C757D"] -colorsForPlot = ["#FF1B6B","#A273B5","#45CAFF"]*2 +colorsForPlot = ["#E5383B","#CD4448","#B55055", + "#9C5D63","#846970","#6C757D"] +# colorsForPlot = ["#FF1B6B","#A273B5","#45CAFF"]*2 # colorsForPlot = ["#E5383B","#6C757D",] markerForPlot = ["o"]*len(colorsForPlot) @@ -177,8 +177,10 @@ # Stack mole fraction from experiments and simulation for error # computation - moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-np.min(moleFracExp))/(np.max(moleFracExp-np.min(moleFracExp))))) - moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-np.min(moleFracSim))/(np.max(moleFracSim-np.min(moleFracSim))))) + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - minExp) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) # Plot the expreimental and model output if not plotFt: @@ -204,7 +206,7 @@ ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', - xlim = [0,150], ylim = [1e-3, 1]) + xlim = [0,150], ylim = [5e-3, 1]) ax2.locator_params(axis="x", nbins=5) diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py index f2333de..799fbb0 100644 --- a/plotFunctions/plotIsothermComparisonMultiParam.py +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -51,6 +51,12 @@ # Plot text plotText = 'DSL' +# Universal gas constant +Rg = 8.314 + +# Total pressure +pressureTotal = np.array([1.e5]); + # Define temperature temperature = [306.44, 325.98, 345.17] @@ -70,14 +76,16 @@ # 'zlcParameters_20210619_0759_36d3aa3.npz',] # Experiment 60 - -zlcFileName = ['zlcParameters_20210701_0843_4fd9c19.npz',] +zlcFileName = ['zlcParameters_20210701_1407_4fd9c19.npz', + 'zlcParameters_20210701_2133_07082e3.npz', + 'zlcParameters_20210702_0207_07082e3.npz'] # Create the grid for mole fractions y = np.linspace(0,1.,100) # Initialize isotherms isoLoading_VOL = np.zeros([len(y),len(temperature)]) -isoLoading_VOL_HA = np.zeros([len(y),len(temperature)]) isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) +kineticConstant_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) objectiveFunction = np.zeros([len(zlcFileName)]) # Loop over all the mole fractions @@ -108,12 +116,37 @@ # When 2 parameter model present else: isothermModel = x_ZLC[0:-2] + rateConstant = x_ZLC[-2] + kineticActEnergy = x_ZLC[-1] for jj in range(len(temperature)): for ii in range(len(y)): isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, moleFrac = y[ii], - temperature = temperature[jj]) + temperature = temperature[jj]) # [mol/kg] + # Partial pressure of the gas + partialPressure = y[ii]*pressureTotal + # delta pressure to compute gradient + delP = 1e-3 + # Mole fraction (up) + moleFractionUp = (partialPressure + delP)/pressureTotal + # Compute the loading [mol/m3] @ moleFractionUp + equilibriumLoadingUp = computeEquilibriumLoading(pressureTotal=pressureTotal, + temperature=temperature[jj], + moleFrac=moleFractionUp, + isothermModel=isothermModel) # [mol/kg] + # Compute the gradient (delp/delq*) + dPbydq = delP/(equilibriumLoadingUp-isoLoading_ZLC[kk,ii,jj]) + # Compute the correction factor for kinetic constant + # Darken factor for concentration dependence + correctionFactor_Conc = (isoLoading_ZLC[kk,ii,jj]/partialPressure)*dPbydq + # Arrhenius factor for temperature dependence + correctionFactor_Temp = np.exp(-kineticActEnergy/(Rg*temperature[jj])) + # Correction factor + correctionFactor = correctionFactor_Conc*correctionFactor_Temp + + # Linear driving force model (derivative of solid phase loadings) + kineticConstant_ZLC[kk,ii,jj] = correctionFactor*rateConstant # Plot the isotherms fig = plt.figure @@ -154,4 +187,24 @@ 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 kinetic constant as a function of mole fraction +plt.style.use('singleColumn.mplstyle') # Custom matplotlib style file +fig = plt.figure +ax1 = plt.subplot(1,1,1) +for jj in range(len(temperature)): + for kk in range(len(zlcFileName)): + if kk == 0: + labelText = str(temperature[jj])+' K' + else: + labelText = '' + ax1.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorForPlot[jj],alpha=0.5, + label=labelText) # ALL + +ax1.set(xlabel='$P$ [bar]', +ylabel='$k$ [s$^\mathregular{-1}$]', +xlim = [0,1], ylim = [0, 0.5]) +ax1.locator_params(axis="x", nbins=4) +ax1.locator_params(axis="y", nbins=4) +ax1.legend() \ No newline at end of file From 879446018a987edd4daf87ffdeecfbcb9f82c3a1 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 2 Jul 2021 15:39:30 +0100 Subject: [PATCH 137/189] Add check for gas flow --- experimental/runZLC.m | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 8051d88..ee60565 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-07-02, AK: Add check for gas flow % - 2021-04-15, AK: Modify function for mixture experiments % - 2021-04-07, AK: Add MFM with MFC1 and MFC2, add interval for MFC % collection @@ -188,6 +189,32 @@ function initializeTimerDevice(~, thisEvent, expInfo, serialObj) error("You should not be here!!!") end end + % Pause for 20 s and check if there is enough gas flow + pause(20) + % MFC1 + outputMFC1 = controlAuxiliaryEquipments(serialObj.MFC1, serialObj.cmdPollData,1); + outputMFC1Temp = strsplit(outputMFC1,' '); % Split the output string + % Rounding required due to rounding errors. Differences of around + % eps can be observed + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) and check if the flow in the gas is within 1 + % mL/min from the setpoint + if ~(round(expInfo.MFC1_SP,1)-1 < round(str2double(outputMFC1Temp(4)),1)) ... + || ~(round(str2double(outputMFC1Temp(4)),1) < round(expInfo.MFC1_SP,1)+1) + error("Dude. There is no gas in MFC1!!!") + end + % MFC2 + outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); + outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string + % Rounding required due to rounding errors. Differences of around + % eps can be observed + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) and check if the flow in the gas is within 1 + % mL/min from the setpoint + if ~(round(expInfo.MFC2_SP,1)-1 < round(str2double(outputMFC2Temp(4)),1)) ... + || ~(round(str2double(outputMFC2Temp(4)),1) < round(expInfo.MFC2_SP,1)+1) + error("Dude. There is no gas in MFC2!!!") + end % Get the event date/time currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); disp([currentDateTime,'-> Initialization complete!!']) From d107c5e3d6663299e92114cbf8794a9172451c11 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 2 Jul 2021 18:25:36 +0100 Subject: [PATCH 138/189] Bug fix for threshold --- experimental/analysis/analyzeExperiment.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/experimental/analysis/analyzeExperiment.m b/experimental/analysis/analyzeExperiment.m index 0f64d86..717a5ac 100644 --- a/experimental/analysis/analyzeExperiment.m +++ b/experimental/analysis/analyzeExperiment.m @@ -14,6 +14,7 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-07-02, AK: Bug fix for threshold % - 2021-05-10, AK: Convert into a function % - 2021-04-20, AK: Add experiment struct to output .mat file % - 2021-04-19, AK: Major revamp for flow rate computation @@ -73,7 +74,8 @@ function analyzeExperiment(experimentStruct,flagCalibration,flagFlowMeter) % Input for the ZLC script (Python) % Find the index for the mole fraction that corresponds to the % threshold mole fraction - moleFracThresholdInd = find(outputStruct.moleFrac(:,2) Date: Sat, 3 Jul 2021 17:04:32 +0100 Subject: [PATCH 139/189] Remove thresholdFactor from the parameter estimation --- experimental/computeMLEError.py | 12 +++++------- experimental/extractDeadVolume.py | 15 ++++----------- experimental/extractZLCParameters.py | 20 ++++++-------------- plotFunctions/plotExperimentOutcome.py | 23 +++++++++-------------- 4 files changed, 24 insertions(+), 46 deletions(-) diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py index 5f5155e..d54f508 100644 --- a/experimental/computeMLEError.py +++ b/experimental/computeMLEError.py @@ -12,6 +12,7 @@ # Computes the MLE error for ZLC experiments. # # Last modified: +# - 2021-07-02, AK: Remove threshold factor # - 2021-07-02, AK: Bug fix for data sorting # - 2021-06-12, AK: Add pure data downsampling # - 2021-05-24, AK: Add -inf input to avoid splitting compositions @@ -32,11 +33,6 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): # Check if flag for downsampling data in low and high composition provided if 'downsampleData' in kwargs: downsampleData = kwargs["downsampleData"] - # Threshold Factor is needed for pure downsampling - if 'thresholdFactor' in kwargs: - thresholdFactor = np.array(kwargs["thresholdFactor"]) - else: - thresholdFactor = 0.5 # Default set to 0.5 (as data scaled between 0 and 1) # Default is false, uses pure MLE with no downsampling else: downsampleData = False @@ -53,8 +49,10 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): concatenatedData = np.vstack((moleFracExp,moleFracSim)).T # Sort the data first (as everything is concatenated) sortedData = concatenatedData[np.argsort(concatenatedData[:,0]),:] - # Find error for mole fraction below a given threshold - lastIndThreshold = int(np.argwhere(sortedData[:,0]>thresholdFactor)[0]) + # Find error for mole fraction below a given threshold. The threshold + # corresponds to the median of the overall data (experimental) + # This would enable equal weights to all the compositions + lastIndThreshold = int(np.argwhere(sortedData[:,0]>np.median(sortedData[:,0]))[0]) # Reinitialize mole fraction experimental and simulation based on sorted data moleFracExp = sortedData[:,0] # Experimental diff --git a/experimental/extractDeadVolume.py b/experimental/extractDeadVolume.py index f648f8a..b5cc4fb 100644 --- a/experimental/extractDeadVolume.py +++ b/experimental/extractDeadVolume.py @@ -17,6 +17,7 @@ # Reference: 10.1007/s10450-012-9417-z # # Last modified: +# - 2021-07-02, AK: Remove threshold factor # - 2021-06-12, AK: Fix for error computation (major) # - 2021-05-28, AK: Add the existing DV model with MS DV model and structure change # - 2021-05-28, AK: Add model for MS @@ -103,19 +104,14 @@ def extractDeadVolume(**kwargs): # Downsample the data at different compositions (this is done on # normalized data) downsampleData = True - - # Threshold factor (If -negative infinity not used, if not need a float) - # This is used to split the compositions into two distint regions - thresholdFactor = 0.5 - + ##################################### ##################################### # Save the parameters to be used for fitting to a dummy file (to pass # through GA - IDIOTIC) savez ('tempFittingParametersDV.npz', - downsampleData=downsampleData,thresholdFactor=thresholdFactor, - flagMSFit = flagMSFit, msFlowRate = msFlowRate, + downsampleData=downsampleData, flagMSFit = flagMSFit, msFlowRate = msFlowRate, flagMSDeadVolume = flagMSDeadVolume, msDeadVolumeFile = msDeadVolumeFile) # Generate .npz file for python processing of the .mat file @@ -183,7 +179,6 @@ def extractDeadVolume(**kwargs): flagMSDeadVolume = flagMSDeadVolume, # Flag for checking if ms dead volume used msDeadVolumeFile = msDeadVolumeFile, # MS dead volume parameter file downsampleFlag = downsampleData, # Flag for downsampling data [-] - mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] hostName = socket.gethostname()) # Hostname of the computer # Remove all the .npy files genereated from the .mat @@ -208,7 +203,6 @@ def deadVolObjectiveFunction(x): # Load the threshold factor, MS fit flag and MS flow rate from the dummy # file downsampleData = load ('tempFittingParametersDV.npz')["downsampleData"] - thresholdFactor = load ('tempFittingParametersDV.npz')["thresholdFactor"] flagMSFit = load ('tempFittingParametersDV.npz')["flagMSFit"] # Used only if MS data is fit msFlowRate = load ('tempFittingParametersDV.npz')["msFlowRate"] # Used only if MS data is fit flagMSDeadVolume = load ('tempFittingParametersDV.npz')["flagMSDeadVolume"] # Used only if MS dead volume model is separate @@ -277,8 +271,7 @@ def deadVolObjectiveFunction(x): # Compute the sum of the error for the difference between exp. and sim. and # add a penalty if needed (using MLE) computedError = computeMLEError(moleFracExpALL,moleFracSimALL, - downsampleData=downsampleData, - thresholdFactor=thresholdFactor) + downsampleData=downsampleData) return computedError + penaltyObj # func: filesToProcess diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 4042d55..a4c34af 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-07-02, AK: Remove threshold factor # - 2021-07-01, AK: Add sensitivity analysis # - 2021-06-16, AK: Add temperature dependence to kinetics # - 2021-06-14, AK: More fixes for error computation @@ -112,11 +113,6 @@ def extractZLCParameters(**kwargs): # Downsample the data at different compositions (this is done on # normalized data) downsampleData = True - - # Threshold factor (If -ngative infinity not used, if not need a float) - # This is used to split the compositions into two distint regions - # Note that this is also needed for (pure) downsampling data - thresholdFactor = 0.5 # Confidence interval for the sensitivity analysis alpha = 0.95 @@ -171,7 +167,7 @@ def extractZLCParameters(**kwargs): # Initialize the parameters used for ZLC fitting process fittingParameters(True,temperature,deadVolumeFile,adsorbentDensity,particleEpsilon, - massSorbent,isoRef,downsampleData,thresholdFactor,paramIso) + massSorbent,isoRef,downsampleData,paramIso) # Algorithm parameters for GA algorithm_param = {'max_num_iteration':15, @@ -225,7 +221,6 @@ def extractZLCParameters(**kwargs): massSorbent = massSorbent, # Mass of sorbent [g] parameterReference = isoRef, # Parameter references [-] downsampleFlag = downsampleData, # Flag for downsampling data [-] - mleThreshold = thresholdFactor, # Threshold for MLE composition split [-] hostName = socket.gethostname()) # Hostname of the computer # Remove all the .npy files genereated from the .mat @@ -252,7 +247,7 @@ def ZLCObjectiveFunction(x): from computeMLEError import computeMLEError # Get the zlc parameters needed for the solver - temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, thresholdFactor, paramIso = fittingParameters(False,[],[],[],[],[],[],[],[],[]) + temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, paramIso = fittingParameters(False,[],[],[],[],[],[],[],[]) # Volume of sorbent material [m3] volSorbent = (massSorbent/1000)/adsorbentDensity @@ -322,8 +317,7 @@ def ZLCObjectiveFunction(x): # Compute the sum of the error for the difference between exp. and sim. computedError = computeMLEError(moleFracExpALL,moleFracSimALL, - downsampleData=downsampleData, - thresholdFactor=thresholdFactor) + downsampleData=downsampleData) return computedError # func: fittingParameters @@ -332,7 +326,7 @@ def ZLCObjectiveFunction(x): # This is done because the ga cannot handle additional user inputs def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity, particleEpsilon,massSorbent,isoRef,downsampleData, - thresholdFactor,paramIso): + paramIso): from numpy import savez from numpy import load # Process the data for python (if needed) @@ -346,7 +340,6 @@ def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity, massSorbent=massSorbent, isoRef=isoRef, downsampleData=downsampleData, - thresholdFactor=thresholdFactor, paramIso = paramIso) # Returns the path of the .npz file to be used else: @@ -360,6 +353,5 @@ def fittingParameters(initFlag,temperature,deadVolumeFile,adsorbentDensity, massSorbent = load (dummyFileName)["massSorbent"] isoRef = load (dummyFileName)["isoRef"] downsampleData = load (dummyFileName)["downsampleData"] - thresholdFactor = load (dummyFileName)["thresholdFactor"] paramIso = load (dummyFileName)["paramIso"] - return temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, thresholdFactor, paramIso \ No newline at end of file + return temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, paramIso \ No newline at end of file diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index cfe43ee..9a6d048 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-07-03, AK: Remove threshold factor # - 2021-07-01, AK: Cosmetic changes # - 2021-05-14, AK: Fixes and structure changes # - 2021-05-14, AK: Improve plotting capabilities @@ -53,7 +54,7 @@ saveFileExtension = ".png" # File with parameter estimates -fileParameter = 'deadVolumeCharacteristics_20210624_1149_b092465.npz' +fileParameter = 'deadVolumeCharacteristics_20210703_1234_a021de6.npz' # Flag to plot dead volume results # Dead volume files have a certain name, use that to find what to plot @@ -63,7 +64,7 @@ flagDeadVolume = False # Flag to plot simulations -simulateModel = False +simulateModel = True # Flag to plot dead volume results plotFt = False @@ -72,8 +73,7 @@ pressureTotal = np.array([1.e5]); # Plot colors -colorsForPlot = ["#E5383B","#CD4448","#B55055", - "#9C5D63","#846970","#6C757D"] +colorsForPlot = ["#E5383B","#B55055","#9C5D63","#6C757D"] # colorsForPlot = ["#FF1B6B","#A273B5","#45CAFF"]*2 # colorsForPlot = ["#E5383B","#6C757D",] markerForPlot = ["o"]*len(colorsForPlot) @@ -83,7 +83,6 @@ rawFileName = ['ZLC_DeadVolume_Exp20A_Output.mat', 'ZLC_DeadVolume_Exp20B_Output.mat', 'ZLC_DeadVolume_Exp20C_Output.mat', - 'ZLC_DeadVolume_Exp20D_Output.mat', 'ZLC_DeadVolume_Exp20E_Output.mat',] # Dead volume parameter model path @@ -100,7 +99,6 @@ # This was added on 12.06 (not back compatible for error computation) downsampleData = load(parameterPath)["downsampleFlag"] - thresholdFactor = load(parameterPath)["mleThreshold"] # Get the MS fit flag, flow rates and msDeadVolumeFile (if needed) # Check needs to be done to see if MS file available or not @@ -193,7 +191,7 @@ color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$t$ [s]', ylabel='$y_1$ [-]', - xlim = [0,150], ylim = [0, 1]) + xlim = [0,300], ylim = [0, 1]) ax1.locator_params(axis="x", nbins=5) ax1.locator_params(axis="y", nbins=5) ax1.legend() @@ -206,7 +204,7 @@ ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', - xlim = [0,150], ylim = [5e-3, 1]) + xlim = [0,300], ylim = [1e-3, 1]) ax2.locator_params(axis="x", nbins=5) @@ -252,12 +250,11 @@ if not os.path.exists(os.path.join('..','simulationFigures')): os.mkdir(os.path.join('..','simulationFigures')) plt.savefig (savePath) - + plt.show() # Print the MLE error if simulateModel: computedError = computeMLEError(moleFracExpALL,moleFracSimALL, - downsampleData=downsampleData, - thresholdFactor=thresholdFactor) + downsampleData=downsampleData,) print("Sanity check objective function: ",round(computedError,0)) # Remove all the .npy files genereated from the .mat @@ -309,7 +306,6 @@ # This was added on 12.06 (not back compatible for error computation) downsampleData = load(parameterPath)["downsampleFlag"] - thresholdFactor = load(parameterPath)["mleThreshold"] print("Objective Function",round(modelOutputTemp[()]["function"],0)) numPointsExp = np.zeros(len(fileName)) @@ -473,8 +469,7 @@ # Print the MLE error if simulateModel: computedError = computeMLEError(moleFracExpALL,moleFracSimALL, - downsampleData = downsampleData, - thresholdFactor = thresholdFactor) + downsampleData = downsampleData,) print("Sanity check objective function: ",round(computedError,0)) # Remove all the .npy files genereated from the .mat From 6f5896ceacea3ea46ad8fa6dd92c63523dee848d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 5 Jul 2021 15:08:43 +0100 Subject: [PATCH 140/189] Bug fix --- experimental/sensitivityAnalysis.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/experimental/sensitivityAnalysis.py b/experimental/sensitivityAnalysis.py index f213a08..de98fee 100644 --- a/experimental/sensitivityAnalysis.py +++ b/experimental/sensitivityAnalysis.py @@ -15,6 +15,7 @@ # provided in the code # # Last modified: +# - 2021-07-05, AK: Bug fix # - 2021-07-01, AK: Change structure (to call from extractZLCParameters) # - 2021-06-28, AK: Initial creation # @@ -185,7 +186,6 @@ def computeObjectiveFunction(mainDir, zlcParameterPath, pOpt, pRef): # Obtain the downsampling conditions downsampleData = load(zlcParameterPath)["downsampleFlag"] - thresholdFactor = load(zlcParameterPath)["mleThreshold"] # Adsorbent density, mass of sorbent and particle epsilon adsorbentDensity = load(zlcParameterPath)["adsorbentDensity"] @@ -260,8 +260,7 @@ def computeObjectiveFunction(mainDir, zlcParameterPath, pOpt, pRef): # Compute the MLE error of the model for the given parameters computedError = computeMLEError(moleFracExpALL,moleFracSimALL, - downsampleData=downsampleData, - thresholdFactor=thresholdFactor) + downsampleData=downsampleData) # Return the objective function value, experimental and simulated output return computedError, moleFracExpALL, moleFracSimALL \ No newline at end of file From f99d6a8091a763d669903c714a7ce5f94aea07e9 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Tue, 13 Jul 2021 16:26:39 +0100 Subject: [PATCH 141/189] Add pore volume analysis script --- experimental/analysis/analyzePoreVolume.m | 133 ++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 experimental/analysis/analyzePoreVolume.m diff --git a/experimental/analysis/analyzePoreVolume.m b/experimental/analysis/analyzePoreVolume.m new file mode 100644 index 0000000..7707ca2 --- /dev/null +++ b/experimental/analysis/analyzePoreVolume.m @@ -0,0 +1,133 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% Process pore volume data from Quantachrome (Ar/N2) and Autopore IV (Hg) +% +% Last modified: +% - 2021-07-13, HA: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%% USER INPUT %%% +% File name to be used for analysis +rawDataFileName = 'dummy.mat'; + +% Name of files from QC and MIP (only for reference purposes) +poreVolume.rawFiles.quantachromeFileName = 'AC_S1_N2_77K_2021_07_09 (DFT method Pore Size Distribution)'; +poreVolume.rawFiles.mercuryIntrusionFileName = 'AC_P'; +%%%%%% +%%%%%% + +% Create poreVolumeData folder to have experiemntal data from Quantachrome +% and MIPporeVolume +if ~exist('poreVolumeData','dir') + mkdir('poreVolumeData') +end + +% Create a dummy file to process +if ~exist(['poreVolumeData',filesep,rawDataFileName],'file') + % First column - pore diameter [nm] + % Second column - cummulative volume, V [ml/g] (Ar/N2) + poreVolume.QC = []; + % First column - pore radius [nm] + % Second column - incremental intrusion [ml/g] (Hg) + poreVolume.MIP = []; + % Save the file + save(['poreVolumeData',filesep,rawDataFileName], 'poreVolume') + % Print error message to let the user know about the create of the + % dummy file that should be populated with the raw data + load(['poreVolumeData',filesep,rawDataFileName]); + clear quantachromeFileName mercuryIntrusionFileName rawDataFileName; + error('Raw file in .mat format does not exist. Populate the data using raw files from QC or MIP and resave the file. A file with the file name is created in poreVolumeData!!') +end +% Load the saved raw data from the .mat file +load(['poreVolumeData',filesep,rawDataFileName]) + +% Quantachrome +% Compute incremental pore volume +poreVolume.QC(:,3) = [poreVolume.QC(1,2); diff(poreVolume.QC(:,2))]; + +% MIP +% Sort MIP data based on ascending pore radius +poreVolume.MIP = sortrows(poreVolume.MIP,1); +% Convert pore radius to pore diameter [nm] +poreVolume.MIP(:,1) = 2.*poreVolume.MIP(:,1); + +% Plot incremental pore volume data +figure('Units','inch','Position',[2 2 6.6 6.6]) +semilogx(poreVolume.QC(:,1),poreVolume.QC(:,3),'or:'); +hold on +semilogx(poreVolume.MIP(:,1),poreVolume.MIP(:,2),'xk:'); +xlim([0,max(poreVolume.MIP(:,1))]); ylim([0,max([max(poreVolume.MIP(:,2)) max(poreVolume.QC(:,3))])]); +legend('QC', 'MIP','Location','northwest'); +xlabel('{\it{D}} [nm]'); ylabel('{\it{dV}} [mL/g]'); +set(gca,'FontSize',8) +box on;grid on; + +% Prompt user to enter threshold for combining QC and MIP data based on +% plot +prompt = {'Enter pore width threshold [nm]:'}; +dlgtitle = 'PoreVolume'; +dims = [1 35]; +definput = {'20','hsv'}; +poreWidthThreshold = str2num(cell2mat(inputdlg(prompt,dlgtitle,dims,definput))); + +% Close the plot window +close all + +% Find the indices for QC and MIP to fit threshold +QCindexLast = find(poreVolume.QC(:,1)=poreWidthThreshold,1,'first'); + +% Combine QC and MIP +% Note that low pore diameter values come from the QC and high pore +% diameter values come from MIP (due to the theory behind their working) +% First colume: Pore diamater [nm] +% Second column: Incremental volume [mL/g] +% Third column: Cummulative volume [mL/g] +poreVolume.combined = [poreVolume.QC(1:QCindexLast,[1 3]); poreVolume.MIP(MIPindexFirst:end,1:2)]; +poreVolume.combined(:,3) = cumsum(poreVolume.combined(:,2)); + +% Prompt user to enter bulk density of sample from MIP +prompt = {'Enter bulk density [g/mL]:'}; +dlgtitle = 'PoreVolume'; +dims = [1 35]; +definput = {'20','hsv'}; +bulkDensity = str2num(cell2mat(inputdlg(prompt,dlgtitle,dims,definput))); + +% Calculate material properties from data +poreVolume.properties.bulkDensity = bulkDensity; +poreVolume.properties.bulkVolume = 1./bulkDensity; +poreVolume.properties.totalPoreVolume = poreVolume.combined(end,3); +poreVolume.properties.skeletalDensity = 1/(poreVolume.properties.bulkVolume - poreVolume.properties.totalPoreVolume); +poreVolume.properties.totalVoidage = poreVolume.properties.totalPoreVolume./poreVolume.properties.bulkVolume; + +% Get the git commit ID +poreVolume.gitCommitID = getGitCommit; + +% Plot the combined cumulative pore volumne distribution +% Plot incremental pore volume data +figure('Units','inch','Position',[2 2 6.6 6.6]) +semilogx(poreVolume.combined(1:QCindexLast,1),poreVolume.combined(1:QCindexLast,3),'or:'); +hold on +semilogx(poreVolume.combined(QCindexLast+1:end,1),poreVolume.combined(QCindexLast+1:end,3),'ok:'); +xlim([0,max(poreVolume.combined(:,1))]); ylim([0,1.1.*max(poreVolume.combined(:,3))]); +legend('QC', 'MIP','Location','northwest'); +xlabel('{\it{D}} [nm]'); ylabel('{\it{V}} [mL/g]'); +set(gca,'FontSize',8) +box on;grid on; + +% Save the file with processed data in the poreVolumeData folder +save(['poreVolumeData',filesep,rawDataFileName], 'poreVolume') \ No newline at end of file From 67f0157f52e233ee077a093616a332c81b249a58 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 21 Jul 2021 18:11:39 +0100 Subject: [PATCH 142/189] Add adsorbent density as an input to extractZLCParameters --- experimental/extractZLCParameters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index a4c34af..d9401c9 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -16,6 +16,7 @@ # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-07-21, AK: Add adsorbent density as an input # - 2021-07-02, AK: Remove threshold factor # - 2021-07-01, AK: Add sensitivity analysis # - 2021-06-16, AK: Add temperature dependence to kinetics @@ -304,7 +305,8 @@ def ZLCObjectiveFunction(x): expFlag = True, deadVolumeFile = str(deadVolumeFile), volSorbent = volSorbent, - volGas = volGas) + volGas = volGas, + adsorbentDensity = adsorbentDensity) # Stack mole fraction from experiments and simulation for error # computation From 1aaff97b53039e776218012c301b92eb21912318 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Fri, 23 Jul 2021 13:45:18 +0100 Subject: [PATCH 143/189] Add interpolation for cumulative pore size distribution --- experimental/analysis/analyzePoreVolume.m | 166 +++++++++++++--------- 1 file changed, 97 insertions(+), 69 deletions(-) diff --git a/experimental/analysis/analyzePoreVolume.m b/experimental/analysis/analyzePoreVolume.m index 7707ca2..1c9ee12 100644 --- a/experimental/analysis/analyzePoreVolume.m +++ b/experimental/analysis/analyzePoreVolume.m @@ -12,6 +12,8 @@ % Process pore volume data from Quantachrome (Ar/N2) and Autopore IV (Hg) % % Last modified: +% - 2021-07-19, HA: Add interpolation for cumulative pore size distribution +% - 2021-07-15, HA: Minor bug fixes % - 2021-07-13, HA: Initial creation % % Input arguments: @@ -21,12 +23,12 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% USER INPUT %%% -% File name to be used for analysis -rawDataFileName = 'dummy.mat'; +% File name to be used for analysis/plotting +rawDataFileName = '13X_H_50nm_poreVolume'; % Name of files from QC and MIP (only for reference purposes) -poreVolume.rawFiles.quantachromeFileName = 'AC_S1_N2_77K_2021_07_09 (DFT method Pore Size Distribution)'; -poreVolume.rawFiles.mercuryIntrusionFileName = 'AC_P'; +poreVolume.rawFiles.quantachromeFileName = '13X_H_N2_77K_2021_07_16 (DFT method Pore Size Distribution)'; +poreVolume.rawFiles.mercuryIntrusionFileName = '13X_H'; %%%%%% %%%%%% @@ -37,7 +39,7 @@ end % Create a dummy file to process -if ~exist(['poreVolumeData',filesep,rawDataFileName],'file') +if ~exist(['poreVolumeData',filesep,rawDataFileName,'.mat'],'file') % First column - pore diameter [nm] % Second column - cummulative volume, V [ml/g] (Ar/N2) poreVolume.QC = []; @@ -49,85 +51,111 @@ % Print error message to let the user know about the create of the % dummy file that should be populated with the raw data load(['poreVolumeData',filesep,rawDataFileName]); - clear quantachromeFileName mercuryIntrusionFileName rawDataFileName; + clear quantachromeFileName mercuryIntrusionFileName rawDataFileName processFlag; error('Raw file in .mat format does not exist. Populate the data using raw files from QC or MIP and resave the file. A file with the file name is created in poreVolumeData!!') end % Load the saved raw data from the .mat file load(['poreVolumeData',filesep,rawDataFileName]) -% Quantachrome -% Compute incremental pore volume -poreVolume.QC(:,3) = [poreVolume.QC(1,2); diff(poreVolume.QC(:,2))]; - -% MIP -% Sort MIP data based on ascending pore radius -poreVolume.MIP = sortrows(poreVolume.MIP,1); -% Convert pore radius to pore diameter [nm] -poreVolume.MIP(:,1) = 2.*poreVolume.MIP(:,1); +if ~isfield(poreVolume,'properties') + % Save the raw data + poreVolume.rawData.QC = poreVolume.QC; + poreVolume.rawData.MIP = poreVolume.MIP; + % Quantachrome + % Compute interpolation query points + interpQC = exp(1).^(linspace(log(min(poreVolume.QC(:,1))),log(max(poreVolume.QC(:,1))),200)); + poreVolume.interp.QC(:,1) = interpQC'; + % interpolate cumulative pore volume + poreVolume.interp.QC(:,2) = interp1(poreVolume.QC(:,1),poreVolume.QC(:,2),interpQC'); + % Compute incremental pore volume + poreVolume.interp.QC(:,3) = [poreVolume.interp.QC(1,2); diff(poreVolume.interp.QC(:,2))]; + % MIP + % Sort MIP data based on ascending pore radius + poreVolume.MIP = sortrows(poreVolume.MIP,1); + % Convert pore radius to pore diameter [nm] + poreVolume.MIP(:,1) = 2.*poreVolume.MIP(:,1); + % Compute cumulative pore size distribution + poreVolume.MIP(:,3) = cumsum(poreVolume.MIP(:,2)); + % Compute interpolation query points + interpMIP = exp(1).^(linspace(log(min(poreVolume.MIP(:,1))),log(max(poreVolume.MIP(:,1))),200)); + poreVolume.interp.MIP(:,1) = interpMIP'; + % interpolate cumulative pore volume + poreVolume.interp.MIP(:,2) = interp1(poreVolume.MIP(:,1),poreVolume.MIP(:,3),interpMIP'); + % Compute incremental pore volume + poreVolume.interp.MIP(:,3) = [poreVolume.interp.MIP(1,2); diff(poreVolume.interp.MIP(:,2))]; +end % Plot incremental pore volume data -figure('Units','inch','Position',[2 2 6.6 6.6]) -semilogx(poreVolume.QC(:,1),poreVolume.QC(:,3),'or:'); +figure('Units','inch','Position',[2 2 5 5]) +semilogx(poreVolume.interp.QC(:,1),poreVolume.interp.QC(:,3),'or:'); hold on -semilogx(poreVolume.MIP(:,1),poreVolume.MIP(:,2),'xk:'); -xlim([0,max(poreVolume.MIP(:,1))]); ylim([0,max([max(poreVolume.MIP(:,2)) max(poreVolume.QC(:,3))])]); -legend('QC', 'MIP','Location','northwest'); +semilogx(poreVolume.interp.MIP(:,1),poreVolume.interp.MIP(:,3),'xk:'); +xlim([0,max(poreVolume.interp.MIP(:,1))]); ylim([0,max([max(poreVolume.interp.MIP(:,3)) max(poreVolume.interp.QC(:,3))])]); +legend('QC', 'MIP','Location','northeast'); xlabel('{\it{D}} [nm]'); ylabel('{\it{dV}} [mL/g]'); -set(gca,'FontSize',8) +set(gca,'FontSize',14) box on;grid on; -% Prompt user to enter threshold for combining QC and MIP data based on -% plot -prompt = {'Enter pore width threshold [nm]:'}; -dlgtitle = 'PoreVolume'; -dims = [1 35]; -definput = {'20','hsv'}; -poreWidthThreshold = str2num(cell2mat(inputdlg(prompt,dlgtitle,dims,definput))); - -% Close the plot window -close all - -% Find the indices for QC and MIP to fit threshold -QCindexLast = find(poreVolume.QC(:,1)=poreWidthThreshold,1,'first'); - -% Combine QC and MIP -% Note that low pore diameter values come from the QC and high pore -% diameter values come from MIP (due to the theory behind their working) -% First colume: Pore diamater [nm] -% Second column: Incremental volume [mL/g] -% Third column: Cummulative volume [mL/g] -poreVolume.combined = [poreVolume.QC(1:QCindexLast,[1 3]); poreVolume.MIP(MIPindexFirst:end,1:2)]; -poreVolume.combined(:,3) = cumsum(poreVolume.combined(:,2)); - -% Prompt user to enter bulk density of sample from MIP -prompt = {'Enter bulk density [g/mL]:'}; -dlgtitle = 'PoreVolume'; -dims = [1 35]; -definput = {'20','hsv'}; -bulkDensity = str2num(cell2mat(inputdlg(prompt,dlgtitle,dims,definput))); - -% Calculate material properties from data -poreVolume.properties.bulkDensity = bulkDensity; -poreVolume.properties.bulkVolume = 1./bulkDensity; -poreVolume.properties.totalPoreVolume = poreVolume.combined(end,3); -poreVolume.properties.skeletalDensity = 1/(poreVolume.properties.bulkVolume - poreVolume.properties.totalPoreVolume); -poreVolume.properties.totalVoidage = poreVolume.properties.totalPoreVolume./poreVolume.properties.bulkVolume; - -% Get the git commit ID -poreVolume.gitCommitID = getGitCommit; +if ~isfield(poreVolume,'properties') + % Prompt user to enter threshold for combining QC and MIP data based on + % plot + prompt = {'Enter pore width threshold [nm]:'}; + dlgtitle = 'PoreVolume'; + dims = [1 35]; + definput = {'20','hsv'}; + poreVolume.options.poreWidthThreshold = str2num(cell2mat(inputdlg(prompt,dlgtitle,dims,definput))); + + % Close the plot window + close all + + % Find the indices for QC and MIP to fit threshold + poreVolume.options.QCindexLast = find(poreVolume.interp.QC(:,1)=poreVolume.options.poreWidthThreshold,1,'first'); + + % Combine QC and MIP + % Note that low pore diameter values come from the QC and high pore + % diameter values come from MIP (due to the theory behind their working) + % First colume: Pore diamater [nm] + % Second column: Incremental volume [mL/g] + % Third column: Cummulative volume [mL/g] + poreVolume.combined = [poreVolume.interp.QC(1:poreVolume.options.QCindexLast,[1 3]); poreVolume.interp.MIP(poreVolume.options.MIPindexFirst:end,[1 3])]; + poreVolume.combined(:,3) = cumsum(poreVolume.combined(:,2)); + + % Prompt user to enter bulk density of sample from MIP + prompt = {'Enter bulk density [g/mL]:'}; + dlgtitle = 'PoreVolume'; + dims = [1 35]; + definput = {'20','hsv'}; + bulkDensity = str2num(cell2mat(inputdlg(prompt,dlgtitle,dims,definput))); + + + % Calculate material properties from data + poreVolume.properties.bulkDensity = bulkDensity; + poreVolume.properties.bulkVolume = 1./bulkDensity; + poreVolume.properties.totalPoreVolume = poreVolume.combined(end-1,3); + poreVolume.properties.skeletalDensity = 1/(poreVolume.properties.bulkVolume - poreVolume.properties.totalPoreVolume); + poreVolume.properties.totalVoidage = poreVolume.properties.totalPoreVolume./poreVolume.properties.bulkVolume; + + % Get the git commit ID + poreVolume.gitCommitID = getGitCommit; +end % Plot the combined cumulative pore volumne distribution -% Plot incremental pore volume data -figure('Units','inch','Position',[2 2 6.6 6.6]) -semilogx(poreVolume.combined(1:QCindexLast,1),poreVolume.combined(1:QCindexLast,3),'or:'); +figure('Units','inch','Position',[2 2 5 5]) +semilogx(poreVolume.combined(1:poreVolume.options.QCindexLast,1),poreVolume.combined(1:poreVolume.options.QCindexLast,3),'or:'); hold on -semilogx(poreVolume.combined(QCindexLast+1:end,1),poreVolume.combined(QCindexLast+1:end,3),'ok:'); +semilogx(poreVolume.combined(poreVolume.options.QCindexLast+1:end,1),poreVolume.combined(poreVolume.options.QCindexLast+1:end,3),'ok:'); xlim([0,max(poreVolume.combined(:,1))]); ylim([0,1.1.*max(poreVolume.combined(:,3))]); -legend('QC', 'MIP','Location','northwest'); +legend('QC', 'MIP','Location','southeast'); xlabel('{\it{D}} [nm]'); ylabel('{\it{V}} [mL/g]'); -set(gca,'FontSize',8) +set(gca,'FontSize',14) box on;grid on; -% Save the file with processed data in the poreVolumeData folder -save(['poreVolumeData',filesep,rawDataFileName], 'poreVolume') \ No newline at end of file +if isfield(poreVolume,'properties') + % Save the file with processed data in the poreVolumeData folder + save(['poreVolumeData',filesep,rawDataFileName], 'poreVolume') +end + +fprintf('Skeletal Density = %5.4e g/mL \n',poreVolume.properties.skeletalDensity); +fprintf('Total pore volume = %5.4e mL/g \n',poreVolume.properties.totalPoreVolume); +fprintf('Total voidage = %5.4e \n',poreVolume.properties.totalVoidage); \ No newline at end of file From 1a58967b81ca164db94e4f72f503ab41eb3d02c6 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 23 Jul 2021 13:48:08 +0100 Subject: [PATCH 144/189] Minor tweaks to analysis functions --- experimental/analysis/analyzeExperiment.m | 9 ++++++++ experimental/analyzeExperimentWrapper.m | 27 +++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/experimental/analysis/analyzeExperiment.m b/experimental/analysis/analyzeExperiment.m index 717a5ac..c863a49 100644 --- a/experimental/analysis/analyzeExperiment.m +++ b/experimental/analysis/analyzeExperiment.m @@ -14,6 +14,7 @@ % real experiment using calibrated flow meters and MS % % Last modified: +% - 2021-07-23, AK: Add calibration model to the output % - 2021-07-02, AK: Bug fix for threshold % - 2021-05-10, AK: Convert into a function % - 2021-04-20, AK: Add experiment struct to output .mat file @@ -84,6 +85,14 @@ function analyzeExperiment(experimentStruct,flagCalibration,flagFlowMeter) experimentOutput.timeExp = outputStruct.flow(1:moleFracThresholdInd,1); % Time elapsed [s] experimentOutput.moleFrac = outputStruct.moleFrac(1:moleFracThresholdInd,2); % Mole fraction CO2 [-] experimentOutput.totalFlowRate = totalFlowRate(1:moleFracThresholdInd)./60; % Total flow rate of the gas [ccs] + experimentOutput.volFlow_MFM = volFlow_MFM; % Actual MFM flow rate of the gas [ccm] + % Flow calibration model + % p00 + p10*x + p01*y + p20*x^2 + p11*x*y + p02*y^2 + p21*x^2*y + p12*x*y^2 + p03*y^3 + % x - mole fraction of CO2; y = volumetric flow rate from MFM [ccm] + % This wilL be used in the simulateCombinedModel.py to compute the + % correct flow accounting for the time lag in the concentration + % due to the MS + experimentOutput.flowCalibration = coeffvalues(calibrationFlow.MFM)'; % Coefficient of flow calibration % Save outputStruct to semiProcessedStruct semiProcessedStruct = outputStruct; % Check concatenateData for more (this is reconciledData there) % Save the experimental output into a .mat file diff --git a/experimental/analyzeExperimentWrapper.m b/experimental/analyzeExperimentWrapper.m index d7216ab..249cfe8 100644 --- a/experimental/analyzeExperimentWrapper.m +++ b/experimental/analyzeExperimentWrapper.m @@ -16,6 +16,7 @@ % experiment % % Last modified: +% - 2021-07-23, AK: Change calibration files % - 2021-05-17, AK: Change MS interpolation flag % - 2021-05-10, AK: Cosmetic changes to plots % - 2021-05-10, AK: Initial creation @@ -31,25 +32,27 @@ %%%% Calibration files to be used %%%% % List the MS calibration files (this is usually in experimental data folder) msFileDir = 'C:\Users\QCPML\Desktop\Ashwin\MS'; % Directory with MS data -msRawFiles = {'ZLCCalibrateMS_20210505'}; % Raw MS data file names for all calibration -numExpForEachRawFile = [4]; % Number of experiments that use the same raw MS file (vector corresponding to number of MS files) +msRawFiles = {'ZLCCalibrateMS_20210721',... + 'ZLCCalibrateMS_20210722'}; % Raw MS data file names for all calibration +numExpForEachRawFile = [2, 2]; % Number of experiments that use the same raw MS file (vector corresponding to number of MS files) % Flow rate files for calibration -msCalibrationFiles = {'ZLCCalibrateMS_20210506_15ccm',... - 'ZLCCalibrateMS_20210506_30ccm',... - 'ZLCCalibrateMS_20210506_45ccm',... - 'ZLCCalibrateMS_20210507_60ccm'}; +msCalibrationFiles = {'ZLCCalibrateMS_20210721_15ccm',... + 'ZLCCalibrateMS_20210722_30ccm',... + 'ZLCCalibrateMS_20210722_45ccm',... + 'ZLCCalibrateMS_20210723_60ccm'}; %%%% Experimet to be analyzed %%%% % List the experiments that have to be analyzed % MS Raw data should contain only two gases and the pressure. For now % cannot handle more gases. -msExpFile = 'ZLC_Zeolite13X_Exp27_28'; % Raw MS data file name +msExpFile = 'ZLC_DeadVolume_Exp20'; % Raw MS data file name % Flow rate files for experiments -experimentFiles = {'ZLC_Zeolite13X_Exp27A',... - 'ZLC_Zeolite13X_Exp27B',... - 'ZLC_Zeolite13X_Exp28A',... - 'ZLC_Zeolite13X_Exp28B'}; +experimentFiles = {'ZLC_DeadVolume_Exp20A',... + 'ZLC_DeadVolume_Exp20B',... + 'ZLC_DeadVolume_Exp20C',... + 'ZLC_DeadVolume_Exp20D',... + 'ZLC_DeadVolume_Exp20E'}; % Initialize the name of the msRawFile to be used for all calibrations startInd = 1; @@ -70,7 +73,7 @@ calibrationStruct.flow = msCalibrationFiles{ii}; % Experimental flow file (.mat) calibrationStruct.MS = [msFileDir,filesep,msRawFileALL{ii},'.asc']; % Experimental MS file (.asc) calibrationStruct.interpMS = true; % Flag for interpolating MS data (true) or flow data (false) - calibrationStruct.numMean = 50; % Number of points for averaging + calibrationStruct.numMean = 25; % Number of points for averaging % Call the analyzeExperiment function to calibrate the MS at the conditions % experiment was performed for calibration % The output calibration model is usually in calibration folder From c739801bd7afcc1ccb886e4d80fa06c12e88d8a7 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Fri, 23 Jul 2021 14:24:28 +0100 Subject: [PATCH 145/189] Modify check for CO2 set point --- experimental/runZLC.m | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/experimental/runZLC.m b/experimental/runZLC.m index ee60565..3a6ec34 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-07-23, AK: Modify check for CO2 set point % - 2021-07-02, AK: Add check for gas flow % - 2021-04-15, AK: Modify function for mixture experiments % - 2021-04-07, AK: Add MFM with MFC1 and MFC2, add interval for MFC @@ -239,14 +240,6 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) % Generate serial command for volumteric flow rate set point cmdSetPt = generateSerialCommand('setPoint',1,0); % Same units as device [~] = controlAuxiliaryEquipments(serialObj.MFC2, cmdSetPt,1); % Set gas for MFC1 - % Check if the set point was sent to the controller - outputMFC2 = controlAuxiliaryEquipments(serialObj.MFC2, serialObj.cmdPollData,1); - outputMFC2Temp = strsplit(outputMFC2,' '); % Split the output string - % Rounding required due to rounding errors. Differences of around - % eps can be observed - if round(str2double(outputMFC2Temp(6)),1) ~= round(0,1) - error("You should not be here!!!") - end end % Get the sampling date/time currentDateTime = datestr(now,'yyyymmdd_HHMMSS'); @@ -286,6 +279,14 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) MFC2.massFlow = str2double(outputMFC2Temp(5)); % standard units [sccm] MFC2.setpoint = str2double(outputMFC2Temp(6)); % device units [ml/min] MFC2.gas = outputMFC2Temp(7); % gas in the controller + % If mixture is being run, check that MFC2 (CO2) set point is zero + if expInfo.runMixtures + % Round the flow rate to the nearest first decimal (as this is the + % resolution of the meter) + if round(MFC2.setpoint,1) ~= round(0,1) + error("You should not be here!!!") + end + end end % Get the current state of the universal flow controller if ~isempty(serialObj.UMFM.portName) From df1ad948bcfab68dc6e3191e2c6495e3669e5cb6 Mon Sep 17 00:00:00 2001 From: Ashwin <15141279+ash23win@users.noreply.github.com> Date: Wed, 28 Jul 2021 13:01:14 +0100 Subject: [PATCH 146/189] Minor bug fix in runZLC --- experimental/analyzeExperimentWrapper.m | 23 ++++++++++------------- experimental/runZLC.m | 3 ++- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/experimental/analyzeExperimentWrapper.m b/experimental/analyzeExperimentWrapper.m index 249cfe8..9d787c2 100644 --- a/experimental/analyzeExperimentWrapper.m +++ b/experimental/analyzeExperimentWrapper.m @@ -32,27 +32,24 @@ %%%% Calibration files to be used %%%% % List the MS calibration files (this is usually in experimental data folder) msFileDir = 'C:\Users\QCPML\Desktop\Ashwin\MS'; % Directory with MS data -msRawFiles = {'ZLCCalibrateMS_20210721',... - 'ZLCCalibrateMS_20210722'}; % Raw MS data file names for all calibration -numExpForEachRawFile = [2, 2]; % Number of experiments that use the same raw MS file (vector corresponding to number of MS files) +msRawFiles = {'ZLCCalibrateMS_20210726'}; % Raw MS data file names for all calibration +numExpForEachRawFile = [3]; % Number of experiments that use the same raw MS file (vector corresponding to number of MS files) % Flow rate files for calibration -msCalibrationFiles = {'ZLCCalibrateMS_20210721_15ccm',... - 'ZLCCalibrateMS_20210722_30ccm',... - 'ZLCCalibrateMS_20210722_45ccm',... - 'ZLCCalibrateMS_20210723_60ccm'}; +msCalibrationFiles = {'ZLCCalibrateMS_20210726_10ccm',... + 'ZLCCalibrateMS_20210726_30ccm',... + 'ZLCCalibrateMS_20210727_60ccm'}; %%%% Experimet to be analyzed %%%% % List the experiments that have to be analyzed % MS Raw data should contain only two gases and the pressure. For now % cannot handle more gases. -msExpFile = 'ZLC_DeadVolume_Exp20'; % Raw MS data file name +msExpFile = 'ZLC_DeadVolume_Exp23'; % Raw MS data file name % Flow rate files for experiments -experimentFiles = {'ZLC_DeadVolume_Exp20A',... - 'ZLC_DeadVolume_Exp20B',... - 'ZLC_DeadVolume_Exp20C',... - 'ZLC_DeadVolume_Exp20D',... - 'ZLC_DeadVolume_Exp20E'}; +experimentFiles = {'ZLC_DeadVolume_Exp23A',... + 'ZLC_DeadVolume_Exp23B',... + 'ZLC_DeadVolume_Exp23C',... + 'ZLC_DeadVolume_Exp23D'}; % Initialize the name of the msRawFile to be used for all calibrations startInd = 1; diff --git a/experimental/runZLC.m b/experimental/runZLC.m index 3a6ec34..d06ea8a 100644 --- a/experimental/runZLC.m +++ b/experimental/runZLC.m @@ -14,6 +14,7 @@ % controllers, will read flow data. % % Last modified: +% - 2021-07-28, AK: Bug fix for mixtures % - 2021-07-23, AK: Modify check for CO2 set point % - 2021-07-02, AK: Add check for gas flow % - 2021-04-15, AK: Modify function for mixture experiments @@ -231,7 +232,7 @@ function executeTimerDevice(timerObj, thisEvent, expInfo, serialObj) userInput = input(promptUser,'s'); end % If mixtures is run, at the first instant turn off CO2 (MFC2) - if expInfo.runMixtures && ~isempty(serialObj.MFC2.portName) + if expInfo.runMixtures && ~isempty(serialObj.MFC2.portName) && timerObj.tasksExecuted == 1 % Parse out gas name from expInfo gasName_MFC2 = expInfo.gasName_MFC2; % Generate Gas ID for Alicat devices From eddec535fff64ae1fecb2bd29643b5823b21046c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 Aug 2021 11:22:27 +0100 Subject: [PATCH 147/189] Fix for error computation --- experimental/computeMLEError.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/experimental/computeMLEError.py b/experimental/computeMLEError.py index d54f508..c2d33aa 100644 --- a/experimental/computeMLEError.py +++ b/experimental/computeMLEError.py @@ -12,6 +12,7 @@ # Computes the MLE error for ZLC experiments. # # Last modified: +# - 2021-08-10, AK: Fix for error computation # - 2021-07-02, AK: Remove threshold factor # - 2021-07-02, AK: Bug fix for data sorting # - 2021-06-12, AK: Add pure data downsampling @@ -72,13 +73,33 @@ def computeMLEError(moleFracExp,moleFracSim,**kwargs): # Higher concentrations moleFracHighExp = moleFracExp[lastIndThreshold:-1:int(np.round(downsampleConc[1]))] moleFracHighSim = moleFracSim[lastIndThreshold:-1:int(np.round(downsampleConc[1]))] + + # Normalize the mole fraction by dividing it by maximum value to avoid + # irregular weightings and scale it to the highest composition range + # This is part of the downsampling/reweighting procedure + minExp = np.min(moleFracHighExp) # Compute the minimum from experiment + normalizeFactorHigh = np.max(moleFracHighExp - minExp) # Compute the max from normalized data + moleFracHighExp = (moleFracHighExp - minExp) + moleFracHighSim = (moleFracHighSim - minExp) + + # Low concentrations + minExp = np.min(moleFracLowExp) # Compute the minimum from experiment + normalizeFactorLow = np.max(moleFracLowExp - minExp) # Compute the max from normalized data + moleFracLowExp = (moleFracLowExp - minExp)/normalizeFactorLow*normalizeFactorHigh + moleFracLowSim = (moleFracLowSim - minExp)/normalizeFactorLow*normalizeFactorHigh + # Compute the error computedErrorLow = np.sum(np.power(moleFracLowExp - moleFracLowSim,2)) computedErrorHigh = np.sum(np.power(moleFracHighExp - moleFracHighSim,2)) - computedError = np.log(computedErrorLow+computedErrorHigh) + # The higher and lower composition is treated as two independent + # measurements + computedError = np.log(computedErrorLow) + np.log(computedErrorHigh) # Compute the number of points per experiment (accouting for down- # sampling in both experiments and high and low compositions - numPoints = len(moleFracHighExp) + len(moleFracLowExp) - + # The number of points in both the low and high compositions area + # similar, so the minimum number of points from the two is chosen + # to be representative of the number of points in the experiments + numPoints = min(len(moleFracHighExp),len(moleFracLowExp)) + return (numPoints/2)*(computedError) \ No newline at end of file From ea32ed7857a87479a244894cd927de4c1d6fa462 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 11 Aug 2021 16:07:11 +0100 Subject: [PATCH 148/189] Add experimental response generator and fix minor things --- experimental/generateExperimentalResponse.py | 176 ++++++++++++++++++ experimental/simulateCombinedModel.py | 2 +- experimental/simulateZLC.py | 2 +- plotFunctions/plotExperimentOutcome.py | 62 +++--- .../plotIsothermComparisonMultiParam.py | 28 ++- 5 files changed, 223 insertions(+), 47 deletions(-) create mode 100644 experimental/generateExperimentalResponse.py diff --git a/experimental/generateExperimentalResponse.py b/experimental/generateExperimentalResponse.py new file mode 100644 index 0000000..774a495 --- /dev/null +++ b/experimental/generateExperimentalResponse.py @@ -0,0 +1,176 @@ +############################################################################ +# +# Imperial College London, United Kingdom +# Multifunctional Nanomaterials Laboratory +# +# Project: ERASE +# Year: 2021 +# Python: Python 3.7 +# Authors: Ashwin Kumar Rajagopalan (AK) +# +# Purpose: +# Generates "true" experimetnal response for the ZLC+Dead volume setup +# and saves the time, mole fraction and the flow rate in the same fashion +# as is done by the the experimental setup. The output from this file is a +# .mat file that can then be fed to the parameter estimator. +# +# Last modified: +# - 2021-08-11, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +from simulateCombinedModel import simulateCombinedModel +import numpy as np +import os +import matplotlib.pyplot as plt +import scipy.io as sio + +os.chdir(".."+os.path.sep+"plotFunctions") +plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file +os.chdir(".."+os.path.sep+"experimental") + +# Move to top level folder (to avoid path issues) +os.chdir("..") +import auxiliaryFunctions +# Get the commit ID of the current repository +gitCommitID = auxiliaryFunctions.getCommitID() +os.chdir("experimental") + +# Get the current date and time for saving purposes +currentDT = auxiliaryFunctions.getCurrentDateTime() + +##### USER INPUT ##### +# Experimental file name +# Note that one file name corresponds to one flow rate, one temperature +# Alphabets in the final file denotes the mole fraction +fileName = ['ZLC_ActivatedCarbon_Sim01', + 'ZLC_ActivatedCarbon_Sim02'] + +# Material isotherm parameters and kinetic rate constants +# Note that the material isotherm parameters is obtained from the Quantachrome +# measurements +# Activated Carbon (non dimensional) +x = [0.0567, 2.61, 0.5525, 0.685, 0.0105, 0.71, 0.033, 0.533]; + +# Skeletal density, mass and porosity of the material used +adsorbentDensity = 1680 # [kg/m3] +massSorbent = 0.0625 # [g] +particleEpsilon = 0.61 # [-] + +# Temperature of the simulate experiment [K] +temperature = 303.15 + +# Inlet flow rate [ccm] +flowRate = [10, 60] + +# Saturation mole fraction (works for a binary system) +initMoleFrac = np.array(([0.11, 0.94], [0.11, 0.73])) + +# Dead volume file for the setup +deadVolumeFile = 'deadVolumeCharacteristics_20210810_1653_eddec53.npz' + +############ + +# Reference isotherm parameters (DSL) +isoRef = [10, 1e-5, 40e3, 10, 1e-5, 40e3, 10000, 40e3] + +# Integration time (set to 1000 s, default) +timeInt = (0.0,1000.0) + +# Volume of sorbent material [m3] +volSorbent = (massSorbent/1000)/adsorbentDensity +# Volume of gas in pores [m3] +volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + +# Create the instance for the plots +fig = plt.figure +ax1 = plt.subplot(1,3,1) +ax2 = plt.subplot(1,3,2) +ax3 = plt.subplot(1,3,3) +# Plot colors +colorsForPlot = ["#FE7F2D","#233D4D"]*2 +markerForPlot = ["o"]*4 + +# Loop over all the conditions +for ii in range(len(flowRate)): + for jj in range(np.size(initMoleFrac,1)): + # Initialize the output dictionary + experimentOutput = {} + # Compute the composite response using the optimizer parameters + timeElapsedSim , moleFracSim , resultMat = simulateCombinedModel(isothermModel = np.multiply(x[0:-2],isoRef[0:-2]), + rateConstant = x[-2]*isoRef[-2], # Last but one element is rate constant (Arrhenius constant) + kineticActEnergy = x[-1]*isoRef[-1], # Last element is activation energy + temperature = temperature, # Temperature [K] + timeInt = timeInt, + initMoleFrac = [initMoleFrac[ii,jj]], # Initial mole fraction assumed to be the first experimental point + flowIn = flowRate[ii]*1e-6/60, # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = False, + deadVolumeFile = str(deadVolumeFile), + volSorbent = volSorbent, + volGas = volGas, + adsorbentDensity = adsorbentDensity) + + # Find the index that corresponds to 1e-2 (to be consistent with the + # experiments) + lastIndThreshold = int(np.argwhere(moleFracSim<=1e-2)[0]) + + # Cut the time, mole fraction and the flow rate to the last index + # threshold + timeExp = timeElapsedSim[0:lastIndThreshold] # Time elapsed [s] + moleFrac = moleFracSim[0:lastIndThreshold] # Mole fraction [-] + totalFlowRate = resultMat[3,0:lastIndThreshold]*1e6 # Total flow rate[ccs] + + # Save the output and git commit ID to .mat file (similar to experiments) + experimentOutput = {'timeExp': timeExp.reshape(len(timeExp),1), + 'moleFrac': moleFrac.reshape(len(moleFrac),1), + 'totalFlowRate': totalFlowRate.reshape(len(totalFlowRate),1)} + saveFileName = fileName[ii] + chr(65+jj) + '_Output.mat' + sio.savemat('runData' + os.path.sep + saveFileName, + {'experimentOutput': experimentOutput, # This is the only thing used for the parameter estimator (same as experiemnt) + # The fields below are saved only for checking purposes + 'gitCommitID': gitCommitID, + 'modelParameters': x, + 'adsorbentDensity': adsorbentDensity, + 'massSorbent': massSorbent, + 'particleEpsilon': particleEpsilon, + 'temperature': temperature, + 'flowRate': flowRate, + 'initMoleFrac': initMoleFrac, + 'deadVolumeFile': deadVolumeFile, + 'isoRef': isoRef}) + + # Plot the responses for sanity check + # y - Linear scale + ax1.semilogy(timeExp,moleFrac, + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.1) # Experimental response + + ax1.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = [0,250], ylim = [1e-2, 1]) + ax1.locator_params(axis="x", nbins=4) + ax1.legend() + + # Ft - Log scale + ax2.semilogy(np.multiply(totalFlowRate,timeExp),moleFrac, + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.1) # Experimental response + ax2.set(xlabel='$Ft$ [cc]', + xlim = [0,60], ylim = [1e-2, 1]) + ax2.locator_params(axis="x", nbins=4) + + # Flow rates + ax3.plot(timeExp,totalFlowRate, + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.1,label=str(round(np.mean(totalFlowRate),2))+" ccs") # Experimental response + ax3.set(xlabel='$t$ [s]', + ylabel='$F$ [ccs]', + xlim = [0,250], ylim = [0, 1.2]) + ax3.locator_params(axis="x", nbins=4) + ax3.locator_params(axis="y", nbins=4) \ No newline at end of file diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index c4dbaf3..11b1e84 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -212,4 +212,4 @@ def plotCombinedModel(timeZLC,moleFracOut,moleFracZLC,flowRateZLC): ax3.locator_params(axis="x", nbins=4) ax3.legend() plt.show() - os.chdir("..") + os.chdir(".."+os.path.sep+"experimental") diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 6c80f05..3340d09 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -95,7 +95,7 @@ def simulateZLC(**kwargs): # If experimental data used, then initialize ode evaluation time to # experimental time, else use default if expFlag is False: - t_eval = np.arange(timeInt[0],timeInt[-1],0.1) + t_eval = np.arange(timeInt[0],timeInt[-1],0.2) else: # Use experimental time (from timeInt) for ode evaluations to avoid # interpolating any data. t_eval is also used for interpolating diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 9a6d048..6058d5a 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -54,7 +54,7 @@ saveFileExtension = ".png" # File with parameter estimates -fileParameter = 'deadVolumeCharacteristics_20210703_1234_a021de6.npz' +fileParameter = 'zlcParameters_20210806_2230_c739801.npz' # Flag to plot dead volume results # Dead volume files have a certain name, use that to find what to plot @@ -73,18 +73,18 @@ pressureTotal = np.array([1.e5]); # Plot colors -colorsForPlot = ["#E5383B","#B55055","#9C5D63","#6C757D"] -# colorsForPlot = ["#FF1B6B","#A273B5","#45CAFF"]*2 -# colorsForPlot = ["#E5383B","#6C757D",] -markerForPlot = ["o"]*len(colorsForPlot) +colorsForPlot = ["#faa307","#d00000","#03071e"]*4 +markerForPlot = ["o"]*20 if flagDeadVolume: + # Plot colors + colorsForPlot = ["#FE7F2D","#B56938","#6C5342","#233D4D"] # File name of the experiments - rawFileName = ['ZLC_DeadVolume_Exp20A_Output.mat', - 'ZLC_DeadVolume_Exp20B_Output.mat', - 'ZLC_DeadVolume_Exp20C_Output.mat', - 'ZLC_DeadVolume_Exp20E_Output.mat',] - + rawFileName = ['ZLC_DeadVolume_Exp23A_Output.mat', + 'ZLC_DeadVolume_Exp23B_Output.mat', + 'ZLC_DeadVolume_Exp23C_Output.mat', + 'ZLC_DeadVolume_Exp23D_Output.mat',] + # Dead volume parameter model path parameterPath = os.path.join('..','simulationResults',fileParameter) @@ -185,13 +185,13 @@ # Linear scale ax1.plot(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.1,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax1.plot(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$t$ [s]', ylabel='$y_1$ [-]', - xlim = [0,300], ylim = [0, 1]) + xlim = [0,150], ylim = [0, 1]) ax1.locator_params(axis="x", nbins=5) ax1.locator_params(axis="y", nbins=5) ax1.legend() @@ -199,12 +199,12 @@ # Log scale ax2.semilogy(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.05,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.2,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax2.semilogy(timeElapsedExp,moleFracSim, color=colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', - xlim = [0,300], ylim = [1e-3, 1]) + xlim = [0,150], ylim = [1e-2, 1]) ax2.locator_params(axis="x", nbins=5) @@ -266,18 +266,24 @@ from simulateCombinedModel import simulateCombinedModel # File name of the experiments - rawFileName = ['ZLC_ActivatedCarbon_Exp60A_Output.mat', - 'ZLC_ActivatedCarbon_Exp62A_Output.mat', - 'ZLC_ActivatedCarbon_Exp64A_Output.mat', - 'ZLC_ActivatedCarbon_Exp60B_Output.mat', - 'ZLC_ActivatedCarbon_Exp62B_Output.mat', - 'ZLC_ActivatedCarbon_Exp64B_Output.mat'] - + rawFileName = ['ZLC_ActivatedCarbon_Exp72A_Output.mat', + 'ZLC_ActivatedCarbon_Exp74A_Output.mat', + 'ZLC_ActivatedCarbon_Exp76A_Output.mat', + 'ZLC_ActivatedCarbon_Exp72B_Output.mat', + 'ZLC_ActivatedCarbon_Exp74B_Output.mat', + 'ZLC_ActivatedCarbon_Exp76B_Output.mat', + 'ZLC_ActivatedCarbon_Exp73A_Output.mat', + 'ZLC_ActivatedCarbon_Exp75A_Output.mat', + 'ZLC_ActivatedCarbon_Exp77A_Output.mat', + 'ZLC_ActivatedCarbon_Exp73B_Output.mat', + 'ZLC_ActivatedCarbon_Exp75B_Output.mat', + 'ZLC_ActivatedCarbon_Exp77B_Output.mat',] + # ZLC parameter model path parameterPath = os.path.join('..','simulationResults',fileParameter) # Temperature (for each experiment) - temperatureExp = [306.44, 325.98, 345.17]*2 + temperatureExp = [344.69, 325.39, 306.15]*4 # Legend flag useFlow = False @@ -319,7 +325,6 @@ downsampleInt = numPointsExp/np.min(numPointsExp) # Multiply the paremeters by the reference values x = np.multiply(modelNonDim,parameterReference) - # Initialize loadings computedError = 0 numPoints = 0 @@ -380,7 +385,8 @@ deadVolumeFile = deadVolumeFile, volSorbent = volSorbent, volGas = volGas, - temperature = temperatureExp[ii]) + temperature = temperatureExp[ii], + adsorbentDensity = adsorbentDensity) # Print simulation volume print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, moleFracSim), @@ -411,7 +417,7 @@ # y - Linear scale ax1.semilogy(timeElapsedExp,moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.025) # Experimental response + color=colorsForPlot[ii],alpha=0.1) # Experimental response if simulateModel: if useFlow: legendStr = str(round(np.mean(flowRateExp),2))+" ccs" @@ -428,12 +434,12 @@ ylabel='$y_1$ [-]', xlim = [0,250], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) - ax1.legend() + # ax1.legend() # Ft - Log scale ax2.semilogy(np.multiply(flowRateExp,timeElapsedExp),moleFracExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.025) # Experimental response + color=colorsForPlot[ii],alpha=0.1) # Experimental response if simulateModel: ax2.semilogy(np.multiply(resultMat[3,:]*1e6,timeElapsedExp),moleFracSim, color=colorsForPlot[ii],label=str(round(np.mean(resultMat[3,:]*1e6),2))+" ccs") # Simulation response @@ -444,7 +450,7 @@ # Flow rates ax3.plot(timeElapsedExp,flowRateExp, marker = markerForPlot[ii],linewidth = 0, - color=colorsForPlot[ii],alpha=0.025,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response + color=colorsForPlot[ii],alpha=0.1,label=str(round(np.mean(flowRateExp),2))+" ccs") # Experimental response if simulateModel: ax3.plot(timeElapsedExp,resultMat[3,:]*1e6, color=colorsForPlot[ii]) # Simulation response diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py index 799fbb0..b99beae 100644 --- a/plotFunctions/plotIsothermComparisonMultiParam.py +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -45,7 +45,7 @@ saveFileExtension = ".png" # Colors -colorForPlot = ["FF1B6B","A273B5","45CAFF"] +colorForPlot = ["faa307","d00000","03071e"] # colorForPlot = ["E5383B","6C757D"] # Plot text @@ -58,27 +58,21 @@ pressureTotal = np.array([1.e5]); # Define temperature -temperature = [306.44, 325.98, 345.17] +temperature = [303.15, 323.15, 343.15] # AC Isotherm parameters -x_VOL = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3, 100] # (Pini 2020) -# x_VOL = [2.81e-5, 1.25e-7, 2.07e2, 4.12, 7.29e-7, 2.65e4, 100] # (Hassan, QC) +x_VOL = [5.67e-1, 2.61e-5 , 2.21e4, 6.85, 1.05e-7, 2.84e4, 100] # (Hassan, QC) -# 13X Isotherm parameters +# 13X Isotherm parameters (L pellet) # x_VOL = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4, 100] # (Hassan, QC) -# ZLC parameter estimate files -# Experiment 43 and 48 -# zlcFileName = ['zlcParameters_20210618_1837_36d3aa3.npz', -# 'zlcParameters_20210618_2209_36d3aa3.npz', -# 'zlcParameters_20210619_0128_36d3aa3.npz', -# 'zlcParameters_20210619_0447_36d3aa3.npz', -# 'zlcParameters_20210619_0759_36d3aa3.npz',] - -# Experiment 60 - -zlcFileName = ['zlcParameters_20210701_1407_4fd9c19.npz', - 'zlcParameters_20210701_2133_07082e3.npz', - 'zlcParameters_20210702_0207_07082e3.npz'] +# BN Isotherm parameters +# x_VOL = [7.01, 2.32e-07, 2.49e4, 0, 0, 0, 100] # (Hassan, QC) + +# ZLC Parameter estimates +zlcFileName = ['zlcParameters_20210806_2230_c739801.npz', + 'zlcParameters_20210807_2248_c739801.npz', + 'zlcParameters_20210810_2135_eddec53.npz'] # Create the grid for mole fractions y = np.linspace(0,1.,100) From 99b9edd7aa242af71921fc762ea1e28d7b83eb35 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Thu, 12 Aug 2021 15:21:15 +0100 Subject: [PATCH 149/189] Add isotherm fitting tool as submodule --- .gitmodules | 3 +++ IsothermFittingTool | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 IsothermFittingTool diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..246c0cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "IsothermFittingTool"] + path = IsothermFittingTool + url = https://github.com/ImperialCollegeLondon/IsothermFittingTool.git diff --git a/IsothermFittingTool b/IsothermFittingTool new file mode 160000 index 0000000..f817019 --- /dev/null +++ b/IsothermFittingTool @@ -0,0 +1 @@ +Subproject commit f817019259569b0c06eddcfb165a78b492568ad8 From b8df6f8127fad920bb303337ebc6c46eac75f66b Mon Sep 17 00:00:00 2001 From: ha3215 Date: Thu, 12 Aug 2021 15:39:59 +0100 Subject: [PATCH 150/189] Update submodule --- IsothermFittingTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index f817019..7f2ac40 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit f817019259569b0c06eddcfb165a78b492568ad8 +Subproject commit 7f2ac405ae18e78eb9c42ea6abafccaab4bf72c7 From cf36a432a4e7fa464499a6830c12380d99ea8dbd Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 16 Aug 2021 12:34:50 +0100 Subject: [PATCH 151/189] update submodule --- IsothermFittingTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 7f2ac40..cf6745c 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 7f2ac405ae18e78eb9c42ea6abafccaab4bf72c7 +Subproject commit cf6745c4908e31553f3ac0a6590108070e8a0a5e From 0872aa3daeaf65b920bfd15d097661e7c1fd235b Mon Sep 17 00:00:00 2001 From: ha3215 Date: Mon, 16 Aug 2021 13:17:54 +0100 Subject: [PATCH 152/189] Changes to directory structure and remove file --- IsothermFittingTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index cf6745c..15e4049 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit cf6745c4908e31553f3ac0a6590108070e8a0a5e +Subproject commit 15e4049cf38c090a241a6a48efd3ea97ff4be897 From a383841b4bdd580df9d8614c6997ec4bdc85fa23 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 20 Aug 2021 12:41:51 +0100 Subject: [PATCH 153/189] Change kinetic model equations --- experimental/extractZLCParameters.py | 13 ++--- experimental/simulateCombinedModel.py | 23 ++++----- experimental/simulateZLC.py | 44 +++++++++-------- plotFunctions/plotExperimentOutcome.py | 33 +++++-------- .../plotIsothermComparisonMultiParam.py | 47 ++++++++++++------- 5 files changed, 83 insertions(+), 77 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index d9401c9..fd2b089 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -12,10 +12,11 @@ # Find the isotherm parameters and the kinetic rate constant by fitting # the complete response curve from the ZLC experiment. Note that currently # the isotherm can be SSL or DSL model. The rate constant is assumed to be a -# constant in the LDF model +# constant in the LDF model and is analogous to Gleuckauf approximation # Reference: 10.1016/j.ces.2014.12.062 # # Last modified: +# - 2021-08-20, AK: Change definition of rate constants # - 2021-07-21, AK: Add adsorbent density as an input # - 2021-07-02, AK: Remove threshold factor # - 2021-07-01, AK: Add sensitivity analysis @@ -134,7 +135,7 @@ def extractZLCParameters(**kwargs): [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 40e3, 10000, 40e3] # Reference for the isotherm parameters + isoRef = [10, 1e-5, 45e3, 100, 100] # Reference for parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit @@ -146,18 +147,18 @@ def extractZLCParameters(**kwargs): [np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 40e3, 10, 1e-5, 40e3, 10000, 40e3] # Reference for the isotherm parameters + isoRef = [10, 1e-5, 45e3, 10, 1e-5, 45e3, 100, 100] # Reference for the parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit # Kinetic constants only - # Note: This might be buggy for simulations performed before 16.06.21 - # This is because of the addition of activation energy for kinetics + # Note: This might be buggy for simulations performed before 20.08.21 + # This is because of the changes to the structure of the kinetic model elif modelType == 'Kinetic': optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real']) problemDimension = len(optType) - isoRef = [10000, 40e3] # Reference for the parameter (has to be a list) + isoRef = [100, 100] # Reference for the parameter (has to be a list) # File with parameter estimates for isotherm (ZLC) isothermDir = '..' + os.path.sep + 'simulationResults/' modelOutputTemp = load(isothermDir+isothermFile, allow_pickle=True)["modelOutput"] diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py index 11b1e84..9fdce71 100644 --- a/experimental/simulateCombinedModel.py +++ b/experimental/simulateCombinedModel.py @@ -14,6 +14,7 @@ # volume simulator # # Last modified: +# - 2021-08-20, AK: Change definition of rate constants # - 2021-06-16, AK: Add temperature dependence to kinetics # - 2021-06-01, AK: Add temperature as an input # - 2021-05-29, AK: Add a separate MS dead volume @@ -56,17 +57,17 @@ def simulateCombinedModel(**kwargs): # Reference: 10.1007/s10450-020-00268-7 isothermModel = [0.44, 3.17e-6, 28.63e3, 6.10, 3.21e-6, 20.37e3] - # Kinetic rate constants [/s] - if 'rateConstant' in kwargs: - rateConstant = kwargs["rateConstant"] + # Kinetic rate constant 1 (analogous to micropore resistance) [/s] + if 'rateConstant_1' in kwargs: + rateConstant_1 = kwargs["rateConstant_1"] else: - rateConstant = [0.3] + rateConstant_1 = [0.3] - # Kinetic activation energy constants [J/mol] - if 'kineticActEnergy' in kwargs: - kineticActEnergy = np.array(kwargs["kineticActEnergy"]) + # Kinetic rate constant 2 (analogous to macropore resistance) [/s] + if 'rateConstant_2' in kwargs: + rateConstant_2 = np.array(kwargs["rateConstant_2"]) else: - kineticActEnergy = np.array([0]) # To simulate the case with temp. dep. + rateConstant_2 = np.array([0]) # Temperature of the gas [K] if 'temperature' in kwargs: @@ -124,9 +125,9 @@ def simulateCombinedModel(**kwargs): expFlag = False # Call the simulateZLC function to simulate the sorption in a given sorbent - timeZLC, resultMat, _ = simulateZLC(isothermModel=isothermModel, - rateConstant=rateConstant, - kineticActEnergy = kineticActEnergy, + timeZLC, resultMat, _ = simulateZLC(isothermModel = isothermModel, + rateConstant_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, temperature = temperature, flowIn = flowIn, initMoleFrac = initMoleFrac, diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index 3340d09..d960e98 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -14,6 +14,7 @@ # model with mass transfer defined using linear driving force. # # Last modified: +# - 2021-08-20, AK: Change definition of rate constants # - 2021-06-16, AK: Add temperature correction factor to LDF # - 2021-06-15, AK: Add correction factor to LDF # - 2021-05-13, AK: IMPORTANT: Change density from particle to skeletal @@ -49,17 +50,17 @@ def simulateZLC(**kwargs): # Get the current date and time for saving purposes currentDT = auxiliaryFunctions.getCurrentDateTime() - # Kinetic rate constants [/s] - if 'rateConstant' in kwargs: - rateConstant = np.array(kwargs["rateConstant"]) + # Kinetic rate constant 1 (analogous to micropore resistance) [/s] + if 'rateConstant_1' in kwargs: + rateConstant_1 = np.array(kwargs["rateConstant_1"]) else: - rateConstant = np.array([0.1]) + rateConstant_1 = np.array([0.1]) - # Kinetic activation energy constants [J/mol] - if 'kineticActEnergy' in kwargs: - kineticActEnergy = np.array(kwargs["kineticActEnergy"]) + # Kinetic rate constant 2 (analogous to macropore resistance) [/s] + if 'rateConstant_2' in kwargs: + rateConstant_2 = np.array(kwargs["rateConstant_2"]) else: - kineticActEnergy = np.array([0]) # To simulate the case with temp. dep. + rateConstant_2 = np.array([0]) # Feed flow rate [m3/s] if 'flowIn' in kwargs: @@ -150,7 +151,7 @@ def simulateZLC(**kwargs): isothermModel=isothermModel)*adsorbentDensity # [mol/m3] # Prepare tuple of input parameters for the ode solver - inputParameters = (adsorbentDensity, isothermModel, rateConstant, kineticActEnergy, + inputParameters = (adsorbentDensity, isothermModel, rateConstant_1, rateConstant_2, flowIn, feedMoleFrac, initMoleFrac, pressureTotal, temperature, volSorbent, volGas) @@ -202,7 +203,7 @@ def solveSorptionEquation(t, f, *inputParameters): Rg = 8.314; # [J/mol K] # Unpack the tuple of input parameters used to solve equations - adsorbentDensity, isothermModel, rateConstant, kineticActEnergy, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters + adsorbentDensity, isothermModel, rateConstant_1, rateConstant_2, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters # Initialize the derivatives to zero df = np.zeros([2]) @@ -224,18 +225,21 @@ def solveSorptionEquation(t, f, *inputParameters): temperature=temperature, moleFrac=moleFractionUp, isothermModel=isothermModel)*adsorbentDensity # [mol/m3] - # Compute the gradient (delp/delq*) - dPbydq = delP/(equilibriumLoadingUp-equilibriumLoading) - # Compute the correction factor for kinetic constant - # Darken factor for concentration dependence - correctionFactor_Conc = (equilibriumLoading/partialPressure)*dPbydq - # Arrhenius factor for temperature dependence - correctionFactor_Temp = np.exp(-kineticActEnergy/(Rg*temperature)) - # Correction factor - correctionFactor = correctionFactor_Conc*correctionFactor_Temp + + # Compute the gradient (delq*/dc) + dqbydc = (equilibriumLoadingUp-equilibriumLoading)/(delP/(Rg*temperature)) # [-] + + # Rate constant 1 (analogous to micropore resistance) + k1 = rateConstant_1 + + # Rate constant 2 (analogous to macropore resistance) + k2 = rateConstant_2/dqbydc + + # Overall rate constant + rateConstant = 1/(1/k1 + 1/k2) # Linear driving force model (derivative of solid phase loadings) - df[1] = correctionFactor*rateConstant*(equilibriumLoading-f[1]) + df[1] = rateConstant*(equilibriumLoading-f[1]) # Total mass balance # Assumes constant pressure, so flow rate evalauted diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 6058d5a..7fb5de9 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -12,6 +12,7 @@ # Plots for the experimental outcome (along with model) # # Last modified: +# - 2021-08-20, AK: Change definition of rate constants # - 2021-07-03, AK: Remove threshold factor # - 2021-07-01, AK: Cosmetic changes # - 2021-05-14, AK: Fixes and structure changes @@ -38,7 +39,6 @@ import os import matplotlib.pyplot as plt import auxiliaryFunctions -from datetime import datetime plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -54,7 +54,7 @@ saveFileExtension = ".png" # File with parameter estimates -fileParameter = 'zlcParameters_20210806_2230_c739801.npz' +fileParameter = 'zlcParameters_20210820_0106_ea32ed7.npz' # Flag to plot dead volume results # Dead volume files have a certain name, use that to find what to plot @@ -284,7 +284,6 @@ # Temperature (for each experiment) temperatureExp = [344.69, 325.39, 306.15]*4 - # Legend flag useFlow = False @@ -359,29 +358,19 @@ print("Experiment",str(ii+1),round(np.trapz(np.multiply(flowRateExp,moleFracExp),timeElapsedExp),2)) if simulateModel: - # Get the date time of the parameter estimates - parameterDateTime = datetime.strptime(fileParameter[14:27], '%Y%m%d_%H%M') - # Date time when the kinetic model shifted from 1 parameter to 2 parameter - parameterSwitchTime = datetime.strptime('20210616_0800', '%Y%m%d_%H%M') - # When 2 parameter model absent - if parameterDateTime Date: Fri, 20 Aug 2021 13:56:10 +0100 Subject: [PATCH 154/189] Incorporate kinetic model changes to sensitivity analysis --- experimental/sensitivityAnalysis.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/experimental/sensitivityAnalysis.py b/experimental/sensitivityAnalysis.py index de98fee..83102ec 100644 --- a/experimental/sensitivityAnalysis.py +++ b/experimental/sensitivityAnalysis.py @@ -15,6 +15,7 @@ # provided in the code # # Last modified: +# - 2021-08-20, AK: Change definition of rate constants # - 2021-07-05, AK: Bug fix # - 2021-07-01, AK: Change structure (to call from extractZLCParameters) # - 2021-06-28, AK: Initial creation @@ -235,17 +236,17 @@ def computeObjectiveFunction(mainDir, zlcParameterPath, pOpt, pRef): # Parse out the xOpt to the isotherm model and kinetic parameters isothermModel = xOpt[0:-2] - rateConstant = xOpt[-2] - kineticActEnergy = xOpt[-1] + rateConstant_1 = xOpt[-2] + rateConstant_2 = xOpt[-1] # Compute the model response using the optimized parameters _ , moleFracSim , resultMat = simulateCombinedModel(timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) expFlag = True, - isothermModel=isothermModel, - rateConstant=rateConstant, - kineticActEnergy = kineticActEnergy, + isothermModel = isothermModel, + rateConstant_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, deadVolumeFile = deadVolumeFile, volSorbent = volSorbent, volGas = volGas, From 927d7e5e8a95e2674fd2b76ccf04dad1d4cdcbca Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 20 Aug 2021 14:53:37 +0100 Subject: [PATCH 155/189] Small bug fixes --- experimental/extractZLCParameters.py | 10 +++++----- experimental/simulateZLC.py | 11 ++++++++++- plotFunctions/plotIsothermComparisonMultiParam.py | 11 ++++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index fd2b089..47f8980 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -135,7 +135,7 @@ def extractZLCParameters(**kwargs): [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 45e3, 100, 100] # Reference for parameters + isoRef = [10, 1e-5, 45e3, 1000, 1000] # Reference for parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit @@ -147,7 +147,7 @@ def extractZLCParameters(**kwargs): [np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 45e3, 10, 1e-5, 45e3, 100, 100] # Reference for the parameters + isoRef = [10, 1e-5, 45e3, 10, 1e-5, 45e3, 1000, 1000] # Reference for the parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit @@ -158,7 +158,7 @@ def extractZLCParameters(**kwargs): optBounds = np.array(([np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real']) problemDimension = len(optType) - isoRef = [100, 100] # Reference for the parameter (has to be a list) + isoRef = [1000, 1000] # Reference for the parameter (has to be a list) # File with parameter estimates for isotherm (ZLC) isothermDir = '..' + os.path.sep + 'simulationResults/' modelOutputTemp = load(isothermDir+isothermFile, allow_pickle=True)["modelOutput"] @@ -297,8 +297,8 @@ def ZLCObjectiveFunction(x): # Compute the composite response using the optimizer parameters _ , moleFracSim , _ = simulateCombinedModel(isothermModel = isothermModel, - rateConstant = x[-2]*isoRef[-2], # Last but one element is rate constant (Arrhenius constant) - kineticActEnergy = x[-1]*isoRef[-1], # Last element is activation energy + rateConstant_1 = x[-2]*isoRef[-2], # Last but one element is rate constant (Arrhenius constant) + rateConstant_2 = x[-1]*isoRef[-1], # Last element is activation energy temperature = temperature[ii], # Temperature [K] timeInt = timeInt, initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py index d960e98..de13623 100644 --- a/experimental/simulateZLC.py +++ b/experimental/simulateZLC.py @@ -236,7 +236,16 @@ def solveSorptionEquation(t, f, *inputParameters): k2 = rateConstant_2/dqbydc # Overall rate constant - rateConstant = 1/(1/k1 + 1/k2) + # The following conditions are done for purely numerical reasons + # If pure (analogous) macropore + if k1<1e-12: + rateConstant = k2 + # If pure (analogous) micropore + elif k2<1e-12: + rateConstant = k1 + # If both resistances are present + else: + rateConstant = 1/(1/k1 + 1/k2) # Linear driving force model (derivative of solid phase loadings) df[1] = rateConstant*(equilibriumLoading-f[1]) diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py index d79edab..4391108 100644 --- a/plotFunctions/plotIsothermComparisonMultiParam.py +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -148,7 +148,16 @@ k2 = rateConstant_2/dqbydc # Overall rate constant - rateConstant = 1/(1/k1 + 1/k2) + # The following conditions are done for purely numerical reasons + # If pure (analogous) macropore + if k1<1e-12: + rateConstant = k2 + # If pure (analogous) micropore + elif k2<1e-12: + rateConstant = k1 + # If both resistances are present + else: + rateConstant = 1/(1/k1 + 1/k2) # Rate constant (overall) kineticConstant_ZLC[kk,ii,jj] = rateConstant From c8173b18102d7047268a1e919c6e7e896253571b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sun, 22 Aug 2021 09:21:35 +0100 Subject: [PATCH 156/189] Fix for reference --- experimental/extractZLCParameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/extractZLCParameters.py b/experimental/extractZLCParameters.py index 47f8980..286d5bf 100644 --- a/experimental/extractZLCParameters.py +++ b/experimental/extractZLCParameters.py @@ -135,7 +135,7 @@ def extractZLCParameters(**kwargs): [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 45e3, 1000, 1000] # Reference for parameters + isoRef = [10, 1e-5, 40e3, 1000, 1000] # Reference for parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit @@ -147,7 +147,7 @@ def extractZLCParameters(**kwargs): [np.finfo(float).eps,1], [np.finfo(float).eps,1])) optType=np.array(['real','real','real','real','real','real','real','real']) problemDimension = len(optType) - isoRef = [10, 1e-5, 45e3, 10, 1e-5, 45e3, 1000, 1000] # Reference for the parameters + isoRef = [10, 1e-5, 40e3, 10, 1e-5, 40e3, 1000, 1000] # Reference for the parameters isothermFile = [] # Isotherm file is empty as it is fit paramIso = [] # Isotherm parameters is empty as it is fit From 41044a6f1ae615667e582d1d6b93102d94d7b73f Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Sun, 22 Aug 2021 10:05:18 +0100 Subject: [PATCH 157/189] Small fix in plot function --- plotFunctions/plotIsothermComparisonMultiParam.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py index 4391108..0740b13 100644 --- a/plotFunctions/plotIsothermComparisonMultiParam.py +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -102,6 +102,7 @@ modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) modelNonDim = modelOutputTemp[()]["variable"] + adsorbentDensity = load(parameterPath, allow_pickle=True)["adsorbentDensity"] # Print names of files used for the parameter estimation (sanity check) fileNameList = load(parameterPath, allow_pickle=True)["fileName"] print(fileNameList) @@ -112,6 +113,7 @@ parameterSwitchTime = datetime.strptime('20210616_0800', '%Y%m%d_%H%M') # Multiply the paremeters by the reference values x_ZLC = np.multiply(modelNonDim,parameterReference) + print(x_ZLC) # When 2 parameter model absent if parameterDateTime Date: Mon, 23 Aug 2021 10:33:44 +0100 Subject: [PATCH 158/189] Fix generateExperimentalResponse to reflect new kinetic model --- experimental/generateExperimentalResponse.py | 45 ++++++++++++-------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/experimental/generateExperimentalResponse.py b/experimental/generateExperimentalResponse.py index 774a495..54da29f 100644 --- a/experimental/generateExperimentalResponse.py +++ b/experimental/generateExperimentalResponse.py @@ -15,6 +15,7 @@ # .mat file that can then be fed to the parameter estimator. # # Last modified: +# - 2021-08-23, AK: Structure changes to reflect new kinetics # - 2021-08-11, AK: Initial creation # # Input arguments: @@ -52,19 +53,33 @@ fileName = ['ZLC_ActivatedCarbon_Sim01', 'ZLC_ActivatedCarbon_Sim02'] -# Material isotherm parameters and kinetic rate constants +# Material isotherm parameters, kinetic rate constants, sorbent mass, density, +# and poroisty # Note that the material isotherm parameters is obtained from the Quantachrome # measurements -# Activated Carbon (non dimensional) -x = [0.0567, 2.61, 0.5525, 0.685, 0.0105, 0.71, 0.033, 0.533]; - -# Skeletal density, mass and porosity of the material used -adsorbentDensity = 1680 # [kg/m3] -massSorbent = 0.0625 # [g] -particleEpsilon = 0.61 # [-] +#### Activated Carbon (dimensional) #### +x = [4.65e-1, 1.02e-5, 2.51e4, 6.51, 3.51e-7, 2.57e4, 1.019, 16.787]; +adsorbentDensity = 1680 # Skeletal density [kg/m3] +massSorbent = 0.0625 # Mass of sorbent [g] +particleEpsilon = 0.61 # Particle porosity [-] +####################################### + +#### Boron Nitride (dimensional) #### +x = [7.01, 2.32e-07, 2.49e4, 0.082, 302.962]; +adsorbentDensity = 3400 # Skeletal density [kg/m3] +massSorbent = 0.0797 # Mass of sorbent [g] +particleEpsilon = 0.88 # Particle porosity [-] +####################################### + +#### Zeolite 13X (dimensional) #### +x = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4, 1.019, 16.787]; +adsorbentDensity = 4100 # Skeletal density [kg/m3] +massSorbent = 0.0594 # Mass of sorbent [g] +particleEpsilon = 0.79 # Particle porosity [-] +####################################### # Temperature of the simulate experiment [K] -temperature = 303.15 +temperature = 308.15 # Inlet flow rate [ccm] flowRate = [10, 60] @@ -77,9 +92,6 @@ ############ -# Reference isotherm parameters (DSL) -isoRef = [10, 1e-5, 40e3, 10, 1e-5, 40e3, 10000, 40e3] - # Integration time (set to 1000 s, default) timeInt = (0.0,1000.0) @@ -103,9 +115,9 @@ # Initialize the output dictionary experimentOutput = {} # Compute the composite response using the optimizer parameters - timeElapsedSim , moleFracSim , resultMat = simulateCombinedModel(isothermModel = np.multiply(x[0:-2],isoRef[0:-2]), - rateConstant = x[-2]*isoRef[-2], # Last but one element is rate constant (Arrhenius constant) - kineticActEnergy = x[-1]*isoRef[-1], # Last element is activation energy + timeElapsedSim , moleFracSim , resultMat = simulateCombinedModel(isothermModel = x[0:-2], + rateConstant_1 = x[-2], # Last but one element is rate constant (analogous to micropore) + rateConstant_2 = x[-1], # Last element is activation energy (analogous to macropore) temperature = temperature, # Temperature [K] timeInt = timeInt, initMoleFrac = [initMoleFrac[ii,jj]], # Initial mole fraction assumed to be the first experimental point @@ -142,8 +154,7 @@ 'temperature': temperature, 'flowRate': flowRate, 'initMoleFrac': initMoleFrac, - 'deadVolumeFile': deadVolumeFile, - 'isoRef': isoRef}) + 'deadVolumeFile': deadVolumeFile}) # Plot the responses for sanity check # y - Linear scale From 6b8850526a58f9c84db7050d783f9b0364a07db8 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Tue, 24 Aug 2021 12:32:47 +0100 Subject: [PATCH 159/189] Update submodule (change isotherm reference values) --- IsothermFittingTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 15e4049..9eed157 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 15e4049cf38c090a241a6a48efd3ea97ff4be897 +Subproject commit 9eed1577edc8dccf25379d29281cdd829191947a From 2b47da7fdc85e43d4a7e725f4028a5c14a40dc56 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 25 Aug 2021 18:22:19 +0100 Subject: [PATCH 160/189] Cosmetic changes to the plotFunctions to provide better sanity checks --- experimental/generateExperimentalResponse.py | 16 ++-- plotFunctions/plotExperimentOutcome.py | 13 ++- .../plotIsothermComparisonMultiParam.py | 83 ++++++++++++------- 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/experimental/generateExperimentalResponse.py b/experimental/generateExperimentalResponse.py index 54da29f..25eae04 100644 --- a/experimental/generateExperimentalResponse.py +++ b/experimental/generateExperimentalResponse.py @@ -65,17 +65,17 @@ ####################################### #### Boron Nitride (dimensional) #### -x = [7.01, 2.32e-07, 2.49e4, 0.082, 302.962]; -adsorbentDensity = 3400 # Skeletal density [kg/m3] -massSorbent = 0.0797 # Mass of sorbent [g] -particleEpsilon = 0.88 # Particle porosity [-] +# x = [7.01, 2.32e-07, 2.49e4, 0.082, 302.962]; +# adsorbentDensity = 3400 # Skeletal density [kg/m3] +# massSorbent = 0.0797 # Mass of sorbent [g] +# particleEpsilon = 0.88 # Particle porosity [-] ####################################### #### Zeolite 13X (dimensional) #### -x = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4, 1.019, 16.787]; -adsorbentDensity = 4100 # Skeletal density [kg/m3] -massSorbent = 0.0594 # Mass of sorbent [g] -particleEpsilon = 0.79 # Particle porosity [-] +# x = [3.83, 1.33e-08, 40.0e3, 2.57, 4.88e-06, 35.16e3, 6.64e2, 7.61e1]; +# adsorbentDensity = 4100 # Skeletal density [kg/m3] +# massSorbent = 0.0594 # Mass of sorbent [g] +# particleEpsilon = 0.79 # Particle porosity [-] ####################################### # Temperature of the simulate experiment [K] diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index 7fb5de9..ea18161 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -54,7 +54,7 @@ saveFileExtension = ".png" # File with parameter estimates -fileParameter = 'zlcParameters_20210820_0106_ea32ed7.npz' +fileParameter = 'zlcParameters_20210822_0926_c8173b1.npz' # Flag to plot dead volume results # Dead volume files have a certain name, use that to find what to plot @@ -284,6 +284,7 @@ # Temperature (for each experiment) temperatureExp = [344.69, 325.39, 306.15]*4 + # Legend flag useFlow = False @@ -324,6 +325,7 @@ downsampleInt = numPointsExp/np.min(numPointsExp) # Multiply the paremeters by the reference values x = np.multiply(modelNonDim,parameterReference) + # Initialize loadings computedError = 0 numPoints = 0 @@ -467,6 +469,15 @@ downsampleData = downsampleData,) print("Sanity check objective function: ",round(computedError,0)) + # Print model data for sanity checks + print("\nFurther Sanity Checks (from the parameter estimate file): ") + print("Dead Volume File: ",str(deadVolumeFile)) + print("Adsorbent Density: ",str(adsorbentDensity)," kg/m3") + print("Mass Sorbent: ",str(massSorbent)," g") + print("Particle Porosity: ",str(particleEpsilon)) + print("File name list: ",load(parameterPath)["fileName"]) + print("Temperature: ",load(parameterPath)["temperature"]) + # Remove all the .npy files genereated from the .mat # Loop over all available files for ii in range(len(fileName)): diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py index 0740b13..c421441 100644 --- a/plotFunctions/plotIsothermComparisonMultiParam.py +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -13,6 +13,7 @@ # different fits from ZLC # # Last modified: +# - 2021-08-20, AK: Introduce macropore diffusivity (for sanity check) # - 2021-08-20, AK: Change definition of rate constants # - 2021-07-01, AK: Cosmetic changes # - 2021-06-15, AK: Initial creation @@ -29,7 +30,6 @@ from computeEquilibriumLoading import computeEquilibriumLoading import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator -from datetime import datetime import os from numpy import load import auxiliaryFunctions @@ -61,23 +61,35 @@ # Define temperature temperature = [308.15, 328.15, 348.15] +# CO2 molecular diffusivity +molDiffusivity = 1.6e-5 # m2/s + +# Particle Tortuosity +tortuosity = 3 + +# Particle Radius +particleRadius = 2e-3 + # AC Isotherm parameters -x_VOL = [4.65e-1, 1.02e-5 , 2.51e4, 6.51, 3.51e-7, 2.57e4, 100] # (Hassan, QC) +x_VOL = [4.65e-1, 1.02e-5 , 2.51e4, 6.51, 3.51e-7, 2.57e4] # (Hassan, QC) # 13X Isotherm parameters (L pellet) -# x_VOL = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4, 100] # (Hassan, QC) +# x_VOL = [2.50, 2.05e-7, 4.29e4, 4.32, 3.06e-7, 3.10e4] # (Hassan, QC) +# x_VOL = [3.83, 1.33e-08, 40.0e3, 2.57, 4.88e-06, 35.16e3] # (Hassan, QC - Bound delU to 40e3) # BN Isotherm parameters -# x_VOL = [7.01, 2.32e-07, 2.49e4, 0, 0, 0, 100] # (Hassan, QC) +# x_VOL = [7.01, 2.32e-07, 2.49e4, 0, 0, 0] # (Hassan, QC) # ZLC Parameter estimates +# New kinetic model +# Both k1 and k2 present + # Activated Carbon Experiments -# Pressure and temperature and temperature dependence -zlcFileName = ['zlcParameters_20210812_0905_ea32ed7.npz', - 'zlcParameters_20210812_1850_ea32ed7.npz', - 'zlcParameters_20210813_0348_ea32ed7.npz', - 'zlcParameters_20210813_1321_ea32ed7.npz', - 'zlcParameters_20210813_2133_ea32ed7.npz'] +zlcFileName = ['zlcParameters_20210822_0926_c8173b1.npz', + 'zlcParameters_20210822_1733_c8173b1.npz', + 'zlcParameters_20210823_0133_c8173b1.npz', + 'zlcParameters_20210823_1007_c8173b1.npz', + 'zlcParameters_20210823_1810_c8173b1.npz'] # Create the grid for mole fractions y = np.linspace(0,1.,100) @@ -85,13 +97,14 @@ isoLoading_VOL = np.zeros([len(y),len(temperature)]) isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) kineticConstant_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) +kineticConstant_Macro = np.zeros([len(zlcFileName),len(y),len(temperature)]) objectiveFunction = np.zeros([len(zlcFileName)]) # Loop over all the mole fractions # Volumetric data for jj in range(len(temperature)): for ii in range(len(y)): - isoLoading_VOL[ii,jj] = computeEquilibriumLoading(isothermModel=x_VOL[0:-1], + isoLoading_VOL[ii,jj] = computeEquilibriumLoading(isothermModel=x_VOL, moleFrac = y[ii], temperature = temperature[jj]) # Loop over all available ZLC files @@ -102,26 +115,21 @@ modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + print(x_ZLC) + adsorbentDensity = load(parameterPath, allow_pickle=True)["adsorbentDensity"] + particleEpsilon = load(parameterPath)["particleEpsilon"] + # Print names of files used for the parameter estimation (sanity check) fileNameList = load(parameterPath, allow_pickle=True)["fileName"] print(fileNameList) - - # Get the date time of the parameter estimates - parameterDateTime = datetime.strptime(zlcFileName[kk][14:27], '%Y%m%d_%H%M') - # Date time when the kinetic model shifted from 1 parameter to 2 parameter - parameterSwitchTime = datetime.strptime('20210616_0800', '%Y%m%d_%H%M') - # Multiply the paremeters by the reference values - x_ZLC = np.multiply(modelNonDim,parameterReference) - print(x_ZLC) - # When 2 parameter model absent - if parameterDateTime Date: Tue, 5 Oct 2021 19:35:14 +0100 Subject: [PATCH 161/189] Add plots for experimental manuscript --- plotFunctions/doubleColumn.mplstyle | 17 +- plotFunctions/doubleColumn2Row.mplstyle | 45 +++ plotFunctions/plotsForArticle_Experiment.py | 387 ++++++++++++++++++++ 3 files changed, 441 insertions(+), 8 deletions(-) create mode 100644 plotFunctions/doubleColumn2Row.mplstyle create mode 100644 plotFunctions/plotsForArticle_Experiment.py diff --git a/plotFunctions/doubleColumn.mplstyle b/plotFunctions/doubleColumn.mplstyle index 8a5f6bc..59238ea 100755 --- a/plotFunctions/doubleColumn.mplstyle +++ b/plotFunctions/doubleColumn.mplstyle @@ -2,23 +2,24 @@ # https://matplotlib.org/3.3.2/tutorials/introductory/customizing.html ## Figure property -figure.figsize : 7, 3 # width, height in inches +figure.figsize : 7, 2.5 # 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.titlesize : 8 +axes.labelsize : 8 axes.formatter.limits : -5, 3 ## Grid axes.grid : true -grid.color : cccccc -grid.linewidth : 0.5 +grid.color : e0e0e0 +grid.linewidth : 0.25 +axes.grid.which : both ## Lines & Scatter -lines.linewidth : 1.5 -lines.markersize : 4 +lines.linewidth : 1 +lines.markersize : 2 scatter.marker: o ## Ticks @@ -34,7 +35,7 @@ font.size : 10 ## Legends legend.frameon : true -legend.fontsize : 10 +legend.fontsize : 8 legend.edgecolor : 1 legend.framealpha : 0.6 diff --git a/plotFunctions/doubleColumn2Row.mplstyle b/plotFunctions/doubleColumn2Row.mplstyle new file mode 100644 index 0000000..599f244 --- /dev/null +++ b/plotFunctions/doubleColumn2Row.mplstyle @@ -0,0 +1,45 @@ +# matplotlib configuration file for the ERASE project +# https://matplotlib.org/3.3.2/tutorials/introductory/customizing.html + +## Figure property +figure.figsize : 7, 5 # width, height in inches +figure.dpi : 600 # dpi +figure.autolayout : true # for labels not being cut out + +## Axes +axes.titlesize : 8 +axes.labelsize : 8 +axes.formatter.limits : -5, 3 + +## Grid +axes.grid : true +grid.color : e0e0e0 +grid.linewidth : 0.25 +axes.grid.which : both + +## Lines & Scatter +lines.linewidth : 1 +lines.markersize : 2 +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 : 8 +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_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py new file mode 100644 index 0000000..bdcf6c3 --- /dev/null +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -0,0 +1,387 @@ +############################################################################ +# +# 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 experiment manuscript +# +# Last modified: +# - 2021-10-04, AK: Add N2/MIP plots +# - 2021-10-01, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def plotsForArticle_Experiment(**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 dead volume plot needs to be plotted + if 'figureMat' in kwargs: + if kwargs["figureMat"]: + plotForArticle_figureMat(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If dead volume plot needs to be plotted + if 'figureDV' in kwargs: + if kwargs["figureDV"]: + plotForArticle_figureDV(gitCommitID, currentDT, + saveFlag, saveFileExtension) + +# fun: plotForArticle_figureMat +# Plots the Figure DV of the manuscript: Material characterization (N2/MIP and QC) +def plotForArticle_figureMat(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + import scipy.io as sio + import os + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers + colorsForPlot = ["0fa3b1","f17300"] + markersForPlot = ["^","v"] + + # Length of arrow + dx1 = [-13,-24,-30] # (for N2 porosity) + dx2 = [27,60,60] # (for MIP porosity) + + # Head length of arrow + hl1 = [2.5,5,6,] # (for N2 porosity) + hl2 = [10,20,20] # (for N2 porosity) + + # Interval for plots + numIntPorosity = 4 + + # Main folder for material characterization + mainDir = os.path.join('..','experimental','materialCharacterization') + + # Porosity folder + porosityDir = os.path.join('porosityData','porosityResults') + + # File with pore characterization data + porosityALL = ['AC_20nm_interp.mat', + 'BNp_39nm_poreVolume_interp.mat', + '13X_H_50nm_poreVolume.mat',] + + # Loop over all the porosity files + for kk in range(len(porosityALL)): + # Path of the file name + fileToLoad = os.path.join(mainDir,porosityDir,porosityALL[kk]) + + # Load .mat file + rawData = sio.loadmat(fileToLoad)["poreVolume"] + # Convert the nDarray to list + nDArrayToList = np.ndarray.tolist(rawData) + + # Get the porosity options + porosityOptionsTemp = nDArrayToList[0][0][5] + + # Unpack another time (due to the structure of loadmat) + porosityOptions = np.ndarray.tolist(porosityOptionsTemp[0]) + + # Indicies for N2 and MIP data + QCindexLast = porosityOptions[0][1][0][0] # Last index for N2 sorption + + # Get the porosity options + combinedPorosityData = nDArrayToList[0][0][6] + + # Create the instance for the plots + ax = plt.subplot(2,3,kk+1) + + # Plot horizontal line for total pore volume + ax.axhline(combinedPorosityData[-2,2], + linestyle = ':', linewidth = 0.75, color = '#7d8597') + + # Plot vertical line to distinguish N2 and MIP + ax.axvline(combinedPorosityData[QCindexLast-1,0], + linestyle = ':', linewidth = 0.75, color = '#7d8597') + + # Plot N2 sorption + ax.semilogx(combinedPorosityData[0:QCindexLast-1:numIntPorosity,0], + combinedPorosityData[0:QCindexLast-1:numIntPorosity,2], + linewidth = 0.25,linestyle = ':', + marker = markersForPlot[0], + color='#'+colorsForPlot[0],) + # Plot MIP + ax.semilogx(combinedPorosityData[QCindexLast:-1:numIntPorosity,0], + combinedPorosityData[QCindexLast:-1:numIntPorosity,2], + linewidth = 0.25,linestyle = ':', + marker = markersForPlot[1], + color='#'+colorsForPlot[1],) + + # N2 sorption measurements + ax.arrow(combinedPorosityData[QCindexLast-1,0], 0.3, dx1[kk], 0, + length_includes_head = True, head_length = hl1[kk], head_width = 0.04, + color = '#'+colorsForPlot[0]) + ax.text(combinedPorosityData[QCindexLast-1,0]+1.2*dx1[kk], 0.15, "N$_2$", fontsize=8, + color = '#'+colorsForPlot[0]) + + # MIP measurements + ax.arrow(combinedPorosityData[QCindexLast-1,0], 0.7, dx2[kk], 0, + length_includes_head = True, head_length = hl2[kk], head_width = 0.04, + color = '#'+colorsForPlot[1]) + ax.text(combinedPorosityData[QCindexLast-1,0]+0.25*dx2[kk], 0.77, "Hg", fontsize=8, + color = '#'+colorsForPlot[1]) + + # Material specific text labels + if kk == 0: + ax.set(xlabel='$D$ [nm]', + ylabel='$V$ [mL g$^{-1}$]', + xlim = [0.1,1e6], ylim = [0, 2]) + ax.text(0.2, 1.82, "(a)", fontsize=8,) + ax.text(1.4e5, 0.1, "AC", fontsize=8, fontweight = 'bold',color = '#e71d36') + ax.text(2e3, combinedPorosityData[-2,2]+0.07, + str(round(combinedPorosityData[-2,2],2))+' mL g$^{-1}$', + fontsize=8,color = '#7d8597') + elif kk == 1: + ax.set(xlabel='$D$ [nm]', + xlim = [0.1,1e6], ylim = [0, 2]) + ax.text(0.2, 1.82, "(b)", fontsize=8,) + ax.text(1.4e5, 0.1, "BN", fontsize=8, fontweight = 'bold',color = '#e71d36') + ax.text(5, combinedPorosityData[-2,2]-0.15, + str(round(combinedPorosityData[-2,2],2))+' mL g$^{-1}$', + fontsize=8,color = '#7d8597') + elif kk == 2: + ax.set(xlabel='$D$ [nm]', + xlim = [0.1,1e6], ylim = [0, 2]) + ax.text(0.2, 1.82, "(c)", fontsize=8,) + ax.text(1e5, 0.1, "13X", fontsize=8, fontweight = 'bold',color = '#e71d36') + ax.text(2e3, combinedPorosityData[-2,2]+0.07, + str(round(combinedPorosityData[-2,2],2))+' mL g$^{-1}$', + fontsize=8,color = '#7d8597') + + ax.locator_params(axis="y", nbins=5) + + # Save the figure + if saveFlag: + # FileName: figureMat___ + saveFileName = "figureMat_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + +# fun: plotForArticle_figureDV +# Plots the Figure DV of the manuscript: Dead volume characterization +def plotForArticle_figureDV(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + from deadVolumeWrapper import deadVolumeWrapper + from extractDeadVolume import filesToProcess # File processing script + from numpy import load + import os + import matplotlib.pyplot as plt + import auxiliaryFunctions + 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() + + # File with parameter estimates + fileParameterALL = ['deadVolumeCharacteristics_20210810_1323_eddec53.npz', # MS + 'deadVolumeCharacteristics_20210810_1653_eddec53.npz', # With ball + 'deadVolumeCharacteristics_20210817_2330_ea32ed7.npz',] # Without ball + + # Flag to plot simulations + simulateModel = True + + # Plot colors and markers + colorsForPlot = ["03045e","0077b6","00b4d8","90e0ef"] + markersForPlot = ["^",">","v","<"] + + for kk in range(len(fileParameterALL)): + fileParameter = fileParameterALL[kk] # Parse out the parameter estimate name + # Dead volume parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + # Load file names and the model + fileNameList = load(parameterPath, allow_pickle=True)["fileName"] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),fileNameList,'DV') + # Get the processed file names + fileName = filesToProcess(False,[],[],'DV') + # Load the model + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + x = modelOutputTemp[()]["variable"] + + # Get the MS fit flag, flow rates and msDeadVolumeFile (if needed) + # Check needs to be done to see if MS file available or not + # Checked using flagMSDeadVolume in the saved file + dvFileLoadTemp = load(parameterPath) + if 'flagMSDeadVolume' in dvFileLoadTemp.files: + flagMSFit = dvFileLoadTemp["flagMSFit"] + msFlowRate = dvFileLoadTemp["msFlowRate"] + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + else: + flagMSFit = False + msFlowRate = -np.inf + flagMSDeadVolume = False + msDeadVolumeFile = [] + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Print the objective function and volume from model parameters + print("Model Volume",round(sum(x[0:2]),2)) + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # Create the instance for the plots + ax1 = plt.subplot(1,3,1) + ax2 = plt.subplot(1,3,2) + ax3 = plt.subplot(1,3,3) + + # Initialize error for objective function + # Loop over all available files + for ii in range(len(fileName)): + # Initialize outputs + moleFracSim = [] + # Path of the file name + fileToLoad = fileName[ii] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + # Get the flow rates from the fit file + # When MS used + if flagMSFit: + flowRateDV = msFlowRate + else: + flowRateDV = np.mean(flowRateExp[-1:-10:-1]) + + # Integration and ode evaluation time + timeInt = timeElapsedExp + + if simulateModel: + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + moleFracSim = deadVolumeWrapper(timeInt, flowRateDV, x, flagMSDeadVolume, msDeadVolumeFile) + + # Stack mole fraction from experiments and simulation for error + # computation + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - minExp) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) + + # Plot the expreimental and model output + # Log scale + if kk == 0: + ax1.semilogy(timeElapsedExp,moleFracExp, + marker = markersForPlot[ii],linewidth = 0, + color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" ccs") # Experimental response + ax1.semilogy(timeElapsedExp,moleFracSim, + color='#'+colorsForPlot[ii]) # Simulation response + ax1.set(xlabel='$t$ [s]', + ylabel='$y$ [-]', + xlim = [0,15], ylim = [1e-2, 1]) + ax1.locator_params(axis="x", nbins=5) + ax1.legend(handletextpad=0.0,loc='center right') + ax1.text(7, 1.3, "(a)", fontsize=8,) + ax1.text(12.2, 0.64, "MS", fontsize=8, fontweight = 'bold', + backgroundcolor = 'w', color = '#e71d36') + ax1.text(8.2, 0.39, "$V_\mathrm{d}$ = 0.02 cc", fontsize=8, + backgroundcolor = 'w', color = '#7d8597') + + elif kk == 1: + ax2.semilogy(timeElapsedExp,moleFracExp, + marker = markersForPlot[ii],linewidth = 0, + color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" ccs") # Experimental response + ax2.semilogy(timeElapsedExp,moleFracSim, + color='#'+colorsForPlot[ii]) # Simulation response + ax2.set(xlabel='$t$ [s]', + xlim = [0,150], ylim = [1e-2, 1]) + ax2.locator_params(axis="x", nbins=5) + ax2.legend(handletextpad=0.0,loc='center right') + ax2.text(70, 1.3, "(b)", fontsize=8,) + ax2.text(67, 0.64, "Column w/ Ball", fontsize=8, fontweight = 'bold', + backgroundcolor = 'w', color = '#e71d36') + ax2.text(83, 0.39, "$V_\mathrm{d}$ = 3.76 cc", fontsize=8, + backgroundcolor = 'w', color = '#7d8597') + + elif kk == 2: + ax3.semilogy(timeElapsedExp,moleFracExp, + marker = markersForPlot[ii],linewidth = 0, + color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" ccs") # Experimental response + ax3.semilogy(timeElapsedExp,moleFracSim, + color='#'+colorsForPlot[ii]) # Simulation response + ax3.set(xlabel='$t$ [s]', + xlim = [0,150], ylim = [1e-2, 1]) + ax3.locator_params(axis="x", nbins=5) + ax3.legend(handletextpad=0.0,loc='center right') + ax3.text(70, 1.3, "(c)", fontsize=8,) + ax3.text(61, 0.64, "Column w/o Ball", fontsize=8, fontweight = 'bold', + backgroundcolor = 'w', color = '#e71d36') + ax3.text(83, 0.39, "$V_\mathrm{d}$ = 3.93 cc", fontsize=8, + backgroundcolor = 'w', color = '#7d8597') + + # Save the figure + if saveFlag: + # FileName: figureDV___ + saveFileName = "figureDV_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + + # Remove all the .npy files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) \ No newline at end of file From 8bac0aa4cb8a3bb26b765299d0aff29416c6c514 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Wed, 6 Oct 2021 10:25:21 +0100 Subject: [PATCH 162/189] Update isothermFittingTool submodule --- IsothermFittingTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 9eed157..3c05e89 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 9eed1577edc8dccf25379d29281cdd829191947a +Subproject commit 3c05e891db8e43fae99b531c93efaab96178816e From 84930b91b87d739c4d757562dff243d176835aa1 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Wed, 6 Oct 2021 12:07:39 +0100 Subject: [PATCH 163/189] Update isothermFittingTool submodule --- IsothermFittingTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 3c05e89..2788340 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 3c05e891db8e43fae99b531c93efaab96178816e +Subproject commit 27883404c3602d1b365bbc7a34c0f7f179a21601 From bc502b34fa6945d2aa5ada9d0833966f5124670f Mon Sep 17 00:00:00 2001 From: m86972ar Date: Wed, 6 Oct 2021 18:44:07 +0100 Subject: [PATCH 164/189] Add plots for experimental and computational data (iso) --- plotFunctions/plotsForArticle_Experiment.py | 764 +++++++++++++++++++- 1 file changed, 738 insertions(+), 26 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index bdcf6c3..f70327a 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2021-10-06, AK: Add plots for experimental and computational data (iso) # - 2021-10-04, AK: Add N2/MIP plots # - 2021-10-01, AK: Initial creation # @@ -46,7 +47,7 @@ def plotsForArticle_Experiment(**kwargs): else: saveFileExtension = ".png" - # If dead volume plot needs to be plotted + # If material characterization plot needs to be plotted if 'figureMat' in kwargs: if kwargs["figureMat"]: plotForArticle_figureMat(gitCommitID, currentDT, @@ -57,6 +58,24 @@ def plotsForArticle_Experiment(**kwargs): if kwargs["figureDV"]: plotForArticle_figureDV(gitCommitID, currentDT, saveFlag, saveFileExtension) + + # If ZLC plot needs to be plotted + if 'figureZLC' in kwargs: + if kwargs["figureZLC"]: + plotForArticle_figureZLC(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If ZLC and QC plot needs to be plotted + if 'figureComp' in kwargs: + if kwargs["figureComp"]: + plotForArticle_figureComp(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If ZLC simulation plot needs to be plotted + if 'figureZLCSim' in kwargs: + if kwargs["figureZLCSim"]: + plotForArticle_figureZLCSim(gitCommitID, currentDT, + saveFlag, saveFileExtension) # fun: plotForArticle_figureMat # Plots the Figure DV of the manuscript: Material characterization (N2/MIP and QC) @@ -75,9 +94,13 @@ def plotForArticle_figureMat(gitCommitID, currentDT, # Get the current date and time for saving purposes currentDT = auxiliaryFunctions.getCurrentDateTime() - # Plot colors and markers - colorsForPlot = ["0fa3b1","f17300"] - markersForPlot = ["^","v"] + # Plot colors and markers (porosity) + colorsForPlot_P = ["0fa3b1","f17300"] + markersForPlot_P = ["^","v"] + + # Plot colors and markers (isotherm) + colorsForPlot_I = ["ffba08","d00000","03071e"] + markersForPlot_I = ["^","d","v"] # Length of arrow dx1 = [-13,-24,-30] # (for N2 porosity) @@ -101,27 +124,27 @@ def plotForArticle_figureMat(gitCommitID, currentDT, 'BNp_39nm_poreVolume_interp.mat', '13X_H_50nm_poreVolume.mat',] + # Isotherm folder + isothermDir = os.path.join('isothermData','isothermResults') + + # File with pore characterization data + isothermALL = ['AC_S1_DSL_100621.mat', + 'BNp_SSL_100621.mat', + 'Z13X_H_DSL_100621.mat',] + # Loop over all the porosity files for kk in range(len(porosityALL)): # Path of the file name fileToLoad = os.path.join(mainDir,porosityDir,porosityALL[kk]) - - # Load .mat file - rawData = sio.loadmat(fileToLoad)["poreVolume"] - # Convert the nDarray to list - nDArrayToList = np.ndarray.tolist(rawData) - + # Get the porosity options - porosityOptionsTemp = nDArrayToList[0][0][5] - - # Unpack another time (due to the structure of loadmat) - porosityOptions = np.ndarray.tolist(porosityOptionsTemp[0]) - + porosityOptions = sio.loadmat(fileToLoad)["poreVolume"]["options"][0][0] + # Indicies for N2 and MIP data - QCindexLast = porosityOptions[0][1][0][0] # Last index for N2 sorption + QCindexLast = porosityOptions["QCindexLast"][0][0][0][0] # Last index for N2 sorption # Get the porosity options - combinedPorosityData = nDArrayToList[0][0][6] + combinedPorosityData = sio.loadmat(fileToLoad)["poreVolume"]["combined"][0][0] # Create the instance for the plots ax = plt.subplot(2,3,kk+1) @@ -138,28 +161,28 @@ def plotForArticle_figureMat(gitCommitID, currentDT, ax.semilogx(combinedPorosityData[0:QCindexLast-1:numIntPorosity,0], combinedPorosityData[0:QCindexLast-1:numIntPorosity,2], linewidth = 0.25,linestyle = ':', - marker = markersForPlot[0], - color='#'+colorsForPlot[0],) + marker = markersForPlot_P[0], + color='#'+colorsForPlot_P[0],) # Plot MIP ax.semilogx(combinedPorosityData[QCindexLast:-1:numIntPorosity,0], combinedPorosityData[QCindexLast:-1:numIntPorosity,2], linewidth = 0.25,linestyle = ':', - marker = markersForPlot[1], - color='#'+colorsForPlot[1],) + marker = markersForPlot_P[1], + color='#'+colorsForPlot_P[1],) # N2 sorption measurements ax.arrow(combinedPorosityData[QCindexLast-1,0], 0.3, dx1[kk], 0, length_includes_head = True, head_length = hl1[kk], head_width = 0.04, - color = '#'+colorsForPlot[0]) + color = '#'+colorsForPlot_P[0]) ax.text(combinedPorosityData[QCindexLast-1,0]+1.2*dx1[kk], 0.15, "N$_2$", fontsize=8, - color = '#'+colorsForPlot[0]) + color = '#'+colorsForPlot_P[0]) # MIP measurements ax.arrow(combinedPorosityData[QCindexLast-1,0], 0.7, dx2[kk], 0, length_includes_head = True, head_length = hl2[kk], head_width = 0.04, - color = '#'+colorsForPlot[1]) + color = '#'+colorsForPlot_P[1]) ax.text(combinedPorosityData[QCindexLast-1,0]+0.25*dx2[kk], 0.77, "Hg", fontsize=8, - color = '#'+colorsForPlot[1]) + color = '#'+colorsForPlot_P[1]) # Material specific text labels if kk == 0: @@ -189,6 +212,62 @@ def plotForArticle_figureMat(gitCommitID, currentDT, fontsize=8,color = '#7d8597') ax.locator_params(axis="y", nbins=5) + + # Loop over all the isotherm files + for kk in range(len(isothermALL)): + # Create the instance for the plots + ax = plt.subplot(2,3,kk+4) + + # Path of the file name + fileToLoad = os.path.join(mainDir,isothermDir,isothermALL[kk]) + + # Get the experimental points + experimentALL = sio.loadmat(fileToLoad)["isothermData"]["experiment"][0][0] + + # Get the isotherm fits + isothermFitALL = sio.loadmat(fileToLoad)["isothermData"]["isothermFit"][0][0] + + # Find temperatures + temperature = np.unique(experimentALL[:,2]) + + # Find indices corresponding to each temperature + for ll in range(len(temperature)): + indexFirst = int(np.argwhere(experimentALL[:,2]==temperature[ll])[0]) + indexLast = int(np.argwhere(experimentALL[:,2]==temperature[ll])[-1]) + + # Plot experimental isotherm + ax.plot(experimentALL[indexFirst:indexLast,0], + experimentALL[indexFirst:indexLast,1], + linewidth = 0, marker = markersForPlot_I[ll], + color='#'+colorsForPlot_I[ll], + label = str(temperature[ll])) + + # Plot fitted isotherm + ax.plot(isothermFitALL[1:-1,0],isothermFitALL[1:-1,ll+1], + linewidth = 1,color='#'+colorsForPlot_I[ll],alpha=0.5) + ax.legend(loc='best') + # Material specific text labels + if kk == 0: + ax.set(xlabel='$P$ [bar]', + ylabel='$q^*$ [mol kg$^{-1}$]', + xlim = [0,1], ylim = [0, 3]) + ax.text(0.89, 2.75, "(d)", fontsize=8,) + ax.text(0.87, 0.13, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + + elif kk == 1: + ax.set(xlabel='$P$ [bar]', + xlim = [0,1], ylim = [0, 2]) + ax.text(0.89, 1.82, "(e)", fontsize=8,) + ax.text(0.87, 0.09, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + + elif kk == 2: + ax.set(xlabel='$P$ [bar]', + xlim = [0,1], ylim = [0, 8]) + ax.text(0.89, 7.25, "(f)", fontsize=8,) + ax.text(0.85, 0.35, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + + ax.locator_params(axis="x", nbins=4) + ax.locator_params(axis="y", nbins=4) # Save the figure if saveFlag: @@ -384,4 +463,637 @@ def plotForArticle_figureDV(gitCommitID, currentDT, # Remove all the .npy files genereated from the .mat # Loop over all available files for ii in range(len(fileName)): - os.remove(fileName[ii]) \ No newline at end of file + os.remove(fileName[ii]) + +# fun: plotForArticle_figureZLC +# Plots the Figure ZLC of the manuscript: ZLC parameter estimates +def plotForArticle_figureZLC(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + from numpy import load + import os + from computeEquilibriumLoading import computeEquilibriumLoading + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers (isotherm) + colorsForPlot = ["ffba08","d00000","03071e"] + + # Universal gas constant + Rg = 8.314 + + # Total pressure + pressureTotal = np.array([1.e5]); + + # Define temperature + temperature = [308.15, 328.15, 348.15] + + # Parameter estimate files + # Activated Carbon Experiments + zlcFileNameALL = [['zlcParameters_20210822_0926_c8173b1.npz', + 'zlcParameters_20210822_1733_c8173b1.npz', + # 'zlcParameters_20210823_0133_c8173b1.npz', # DSL BAD (but lowest J) + # 'zlcParameters_20210823_1007_c8173b1.npz', # DSL BAD (but lowest J) + 'zlcParameters_20210823_1810_c8173b1.npz'], + # Boron Nitride Experiments + ['zlcParameters_20210823_1731_c8173b1.npz', + 'zlcParameters_20210824_0034_c8173b1.npz', + 'zlcParameters_20210824_0805_c8173b1.npz', + 'zlcParameters_20210824_1522_c8173b1.npz', + 'zlcParameters_20210824_2238_c8173b1.npz',], + # Zeolite 13X Experiments + ['zlcParameters_20210824_1552_6b88505.npz', + 'zlcParameters_20210825_0559_6b88505.npz', + 'zlcParameters_20210825_1854_6b88505.npz', + 'zlcParameters_20210826_0847_6b88505.npz', + 'zlcParameters_20210827_0124_6b88505.npz',]] + + # Create the grid for mole fractions + y = np.linspace(0,1.,100) + + for pp in range(len(zlcFileNameALL)): + zlcFileName = zlcFileNameALL[pp] + + # Initialize isotherms + isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + kineticConstant_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + objectiveFunction = np.zeros([len(zlcFileName)]) + + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # ZLC Data + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + parameterReference = load(parameterPath)["parameterReference"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + adsorbentDensity = load(parameterPath, allow_pickle=True)["adsorbentDensity"] + + # Parse out the isotherm parameter + isothermModel = x_ZLC[0:-2] + rateConstant_1 = x_ZLC[-2] + rateConstant_2 = x_ZLC[-1] + + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, + moleFrac = y[ii], + temperature = temperature[jj]) # [mol/kg] + # Partial pressure of the gas + partialPressure = y[ii]*pressureTotal + # delta pressure to compute gradient + delP = 1e-3 + # Mole fraction (up) + moleFractionUp = (partialPressure + delP)/pressureTotal + # Compute the loading [mol/m3] @ moleFractionUp + equilibriumLoadingUp = computeEquilibriumLoading(temperature=temperature[jj], + moleFrac=moleFractionUp, + isothermModel=isothermModel) # [mol/kg] + + # Compute the gradient (delq*/dc) + dqbydc = (equilibriumLoadingUp-isoLoading_ZLC[kk,ii,jj])*adsorbentDensity/(delP/(Rg*temperature[jj])) # [-] + + # Rate constant 1 (analogous to micropore resistance) + k1 = rateConstant_1 + + # Rate constant 2 (analogous to macropore resistance) + k2 = rateConstant_2/dqbydc + + # Overall rate constant + # The following conditions are done for purely numerical reasons + # If pure (analogous) macropore + if k1<1e-12: + rateConstant = k2 + # If pure (analogous) micropore + elif k2<1e-12: + rateConstant = k1 + # If both resistances are present + else: + rateConstant = 1/(1/k1 + 1/k2) + + # Rate constant (overall) + kineticConstant_ZLC[kk,ii,jj] = rateConstant + + # Plot the isotherms + ax1 = plt.subplot(2,3,pp+1) + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + + for jj in range(len(temperature)): + for kk in range(len(zlcFileName)): + if kk == minJ[0]: + ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Lowest J + else: + ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.2) # ALL + + # Plot the kinetic constants + ax2 = plt.subplot(2,3,pp+4) + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + + for jj in range(len(temperature)): + for kk in range(len(zlcFileName)): + if kk == minJ[0]: + ax2.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Lowest J + else: + ax2.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.2) # ALL + + if pp == 0: + # Isotherm + ax1.set(ylabel='$q^*$ [mol kg$^{-1}$]', + xlim = [0,1], ylim = [0, 3]) + ax1.text(0.04, 2.75, "(a)", fontsize=8,) + ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.30, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.12, "REP", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.axes.xaxis.set_ticklabels([]) + ax1.legend() + # Kinetics + ax2.set(xlabel='$P$ [bar]', + ylabel='$k$ [s$^{-1}$]', + xlim = [0,1], ylim = [0, 1]) + ax2.text(0.04, 0.9, "(d)", fontsize=8,) + # ax2.text(0.87, 0.9, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax2.text(0.53, 0.83, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + elif pp == 1: + # Isotherm + ax1.set(xlim = [0,1], ylim = [0, 1.5]) + ax1.text(0.04, 1.35, "(b)", fontsize=8,) + ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.15, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.06, "REP", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.axes.xaxis.set_ticklabels([]) + ax1.legend() + # Kinetics + ax2.set(xlabel='$P$ [bar]', + xlim = [0,1], ylim = [0, 1]) + ax2.text(0.04, 0.9, "(e)", fontsize=8,) + # ax2.text(0.87, 0.9, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax2.text(0.53, 0.83, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + elif pp == 2: + # Isotherm + ax1.set(xlim = [0,1], ylim = [0, 8]) + ax1.text(0.04, 7.3, "(c)", fontsize=8,) + ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.86, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.32, "REP", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.axes.xaxis.set_ticklabels([]) + ax1.legend(loc='upper right') + # Kinetics + ax2.set(xlabel='$P$ [bar]', + xlim = [0,1], ylim = [0, 2]) + ax2.text(0.04, 1.8, "(f)", fontsize=8,) + # ax2.text(0.84, 1.8, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax2.text(0.53, 1.66, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + + # Save the figure + if saveFlag: + # FileName: figureZLC___ + saveFileName = "figureZLC_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + +# fun: plotForArticle_figureComp +# Plots the Figure Comp of the manuscript: ZLC and QC comparison +def plotForArticle_figureComp(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + from numpy import load + import scipy.io as sio + import os + from computeEquilibriumLoading import computeEquilibriumLoading + 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() + + # Plot colors and markers (isotherm) + colorsForPlot = ["ffba08","d00000","03071e"] + + # Define temperature + temperature = [308.15, 328.15, 348.15] + + # QC parameter estimates + # Main folder for material characterization + mainDir = os.path.join('..','experimental','materialCharacterization') + + # Isotherm folder + isothermDir = os.path.join('isothermData','isothermResults') + + # File with pore characterization data + isothermALL = ['AC_S1_DSL_100621.mat', + 'BNp_SSL_100621.mat', + 'Z13X_H_DSL_100621.mat',] + + # Parameter estimate files + # Activated Carbon Experiments + zlcFileNameALL = [['zlcParameters_20210822_0926_c8173b1.npz', + 'zlcParameters_20210822_1733_c8173b1.npz', + # 'zlcParameters_20210823_0133_c8173b1.npz', # DSL BAD (but lowest J) + # 'zlcParameters_20210823_1007_c8173b1.npz', # DSL BAD (but lowest J) + 'zlcParameters_20210823_1810_c8173b1.npz'], + # Boron Nitride Experiments + ['zlcParameters_20210823_1731_c8173b1.npz', + 'zlcParameters_20210824_0034_c8173b1.npz', + 'zlcParameters_20210824_0805_c8173b1.npz', + 'zlcParameters_20210824_1522_c8173b1.npz', + 'zlcParameters_20210824_2238_c8173b1.npz',], + # Zeolite 13X Experiments + ['zlcParameters_20210824_1552_6b88505.npz', + 'zlcParameters_20210825_0559_6b88505.npz', + 'zlcParameters_20210825_1854_6b88505.npz', + 'zlcParameters_20210826_0847_6b88505.npz', + 'zlcParameters_20210827_0124_6b88505.npz',]] + + # Create the grid for mole fractions + y = np.linspace(0,1.,100) + + # Compute the ZLC loadings + for pp in range(len(zlcFileNameALL)): + # Initialize volumetric loading + isoLoading_VOL = np.zeros([len(y),len(temperature)]) + # Path of the file name + fileToLoad = os.path.join(mainDir,isothermDir,isothermALL[pp]) + # Load isotherm parameters from QC data + isothermParameters = sio.loadmat(fileToLoad)["isothermData"]["isothermParameters"][0][0] + + # Prepare x_VOL + x_VOL = list(isothermParameters[0:-1:2,0]) + list(isothermParameters[1::2,0]) + + # Loop through all the temperature and mole fraction + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_VOL[ii,jj] = computeEquilibriumLoading(isothermModel=x_VOL, + moleFrac = y[ii], + temperature = temperature[jj]) + + zlcFileName = zlcFileNameALL[pp] + + # Initialize isotherms + isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + objectiveFunction = np.zeros([len(zlcFileName)]) + + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # ZLC Data + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + parameterReference = load(parameterPath)["parameterReference"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + + # Parse out the isotherm parameter + isothermModel = x_ZLC[0:-2] + + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, + moleFrac = y[ii], + temperature = temperature[jj]) # [mol/kg] + + + # Plot the isotherms + ax1 = plt.subplot(1,3,pp+1) + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + + for jj in range(len(temperature)): + ax1.plot(y,isoLoading_VOL[:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # QC + for kk in range(len(zlcFileName)): + if kk == minJ[0]: + ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha = 0.2) # Lowest J + + if pp == 0: + # Isotherm + ax1.set(xlabel='$P$ [bar]', + ylabel='$q^*$ [mol kg$^{-1}$]', + xlim = [0,1], ylim = [0, 3]) + ax1.text(0.04, 2.75, "(a)", fontsize=8,) + ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.32, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.12, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.legend() + elif pp == 1: + # Isotherm + ax1.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 1.5]) + ax1.text(0.04, 1.35, "(b)", fontsize=8,) + ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.16, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.06, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.legend() + elif pp == 2: + # Isotherm + ax1.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 8]) + ax1.text(0.04, 7.3, "(c)", fontsize=8,) + ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.86, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.84, 0.32, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.legend(loc='upper right') + + # Save the figure + if saveFlag: + # FileName: figureComp___ + saveFileName = "figureComp_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + +# fun: plotForArticle_figureZLCSim +# Plots the Figure ZLC of the manuscript: ZLC parameter estimates (simulated) +def plotForArticle_figureZLCSim(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + from numpy import load + import scipy.io as sio + import os + from computeEquilibriumLoading import computeEquilibriumLoading + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers (isotherm) + colorsForPlot = ["ffba08","d00000","03071e"] + + # Universal gas constant + Rg = 8.314 + + # Total pressure + pressureTotal = np.array([1.e5]); + + # Define temperature + temperature = [308.15, 328.15, 348.15] + + # .mat files with genereated simuation data + # Main folder for material characterization + mainDir = os.path.join('..','experimental','runData') + + # File with pore characterization data + simData = ['ZLC_ActivatedCarbon_Sim01A_Output.mat', + 'ZLC_BoronNitride_Sim01A_Output.mat', + 'ZLC_Zeolite13X_Sim01A_Output.mat',] + + # Parameter estimate files + # Activated Carbon Simulations + zlcFileNameALL = [['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'], + # Boron Nitride Simulations + ['zlcParameters_20210823_1907_03c82f4.npz', + 'zlcParameters_20210824_0555_03c82f4.npz', + 'zlcParameters_20210824_2105_03c82f4.npz', + 'zlcParameters_20210825_0833_03c82f4.npz', + 'zlcParameters_20210825_2214_03c82f4.npz'], + # Zeolite 13X Simulations + ['zlcParameters_20210824_1102_c8173b1.npz', + 'zlcParameters_20210825_0243_c8173b1.npz', + 'zlcParameters_20210825_1758_c8173b1.npz', + 'zlcParameters_20210826_1022_c8173b1.npz', + 'zlcParameters_20210827_0104_c8173b1.npz',]] + + # Create the grid for mole fractions + y = np.linspace(0,1.,100) + + for pp in range(len(zlcFileNameALL)): + # Initialize simulated loading + isoLoading_SIM = np.zeros([len(y),len(temperature)]) + # Path of the file name + fileToLoad = os.path.join(mainDir,simData[pp]) + # Load isotherm parameters from simulated data + isothermParameters = sio.loadmat(fileToLoad)["modelParameters"][0] + + # Prepare x_VOL + if len(isothermParameters) == 8: + x_SIM = isothermParameters[0:6] + else: + x_SIM = isothermParameters[0:3] + + # Loop through all the temperature and mole fraction + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_SIM[ii,jj] = computeEquilibriumLoading(isothermModel=x_SIM, + moleFrac = y[ii], + temperature = temperature[jj]) + + + # Go through the ZLC files + zlcFileName = zlcFileNameALL[pp] + + # Initialize isotherms + isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + kineticConstant_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + kineticConstant_SIM = np.zeros([len(y),len(temperature)]) # For simulated data + objectiveFunction = np.zeros([len(zlcFileName)]) + + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # ZLC Data + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + parameterReference = load(parameterPath)["parameterReference"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + adsorbentDensity = load(parameterPath, allow_pickle=True)["adsorbentDensity"] + + # Parse out the isotherm parameter + isothermModel = x_ZLC[0:-2] + rateConstant_1 = x_ZLC[-2] + rateConstant_2 = x_ZLC[-1] + + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, + moleFrac = y[ii], + temperature = temperature[jj]) # [mol/kg] + # Partial pressure of the gas + partialPressure = y[ii]*pressureTotal + # delta pressure to compute gradient + delP = 1e-3 + # Mole fraction (up) + moleFractionUp = (partialPressure + delP)/pressureTotal + # Compute the loading [mol/m3] @ moleFractionUp + equilibriumLoadingUp = computeEquilibriumLoading(temperature=temperature[jj], + moleFrac=moleFractionUp, + isothermModel=isothermModel) # [mol/kg] + + # Compute the gradient (delq*/dc) + dqbydc = (equilibriumLoadingUp-isoLoading_ZLC[kk,ii,jj])*adsorbentDensity/(delP/(Rg*temperature[jj])) # [-] + + # Rate constant 1 (analogous to micropore resistance) + k1 = rateConstant_1 + + # Rate constant 2 (analogous to macropore resistance) + k2 = rateConstant_2/dqbydc + + # Overall rate constant + # The following conditions are done for purely numerical reasons + # If pure (analogous) macropore + if k1<1e-12: + rateConstant = k2 + # If pure (analogous) micropore + elif k2<1e-12: + rateConstant = k1 + # If both resistances are present + else: + rateConstant = 1/(1/k1 + 1/k2) + + # Rate constant (overall) + kineticConstant_ZLC[kk,ii,jj] = rateConstant + + # Compute the simulated "true" kinetic constant + if kk == 0: + # Rate constant 1 (analogous to micropore resistance) + k1 = isothermParameters[-2] + + # Rate constant 2 (analogous to macropore resistance) + k2 = isothermParameters[-1]/dqbydc + + # Overall rate constant + # The following conditions are done for purely numerical reasons + # If pure (analogous) macropore + if k1<1e-12: + rateConstant = k2 + # If pure (analogous) micropore + elif k2<1e-12: + rateConstant = k1 + # If both resistances are present + else: + rateConstant = 1/(1/k1 + 1/k2) + + # Rate constant (overall) + kineticConstant_SIM[ii,jj] = rateConstant + + # Plot the isotherms + ax1 = plt.subplot(2,3,pp+1) + for jj in range(len(temperature)): + ax1.plot(y,isoLoading_SIM[:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Simulated "True" Data + for kk in range(len(zlcFileName)): + ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.1) # ALL + + # Plot the kinetic constants + ax2 = plt.subplot(2,3,pp+4) + for jj in range(len(temperature)): + ax2.plot(y,kineticConstant_SIM[:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Simulated "True" Data + for kk in range(len(zlcFileName)): + ax2.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.1) # ALL + + if pp == 0: + # Isotherm + ax1.set(ylabel='$q^*$ [mol kg$^{-1}$]', + xlim = [0,1], ylim = [0, 3]) + ax1.text(0.04, 2.75, "(a)", fontsize=8,) + ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.79, 0.30, "TRUE", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.83, 0.12, "EST.", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.axes.xaxis.set_ticklabels([]) + ax1.legend() + # Kinetics + ax2.set(xlabel='$P$ [bar]', + ylabel='$k$ [s$^{-1}$]', + xlim = [0,1], ylim = [0, 1]) + ax2.text(0.04, 0.9, "(d)", fontsize=8,) + # ax2.text(0.87, 0.9, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax2.text(0.53, 0.83, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + elif pp == 1: + # Isotherm + ax1.set(xlim = [0,1], ylim = [0, 1.5]) + ax1.text(0.04, 1.35, "(b)", fontsize=8,) + ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.79, 0.15, "TRUE", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.83, 0.06, "EST.", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.axes.xaxis.set_ticklabels([]) + ax1.legend() + # Kinetics + ax2.set(xlabel='$P$ [bar]', + xlim = [0,1], ylim = [0, 1]) + ax2.text(0.04, 0.9, "(e)", fontsize=8,) + # ax2.text(0.87, 0.9, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax2.text(0.53, 0.83, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + elif pp == 2: + # Isotherm + ax1.set(xlim = [0,1], ylim = [0, 8]) + ax1.text(0.04, 7.3, "(c)", fontsize=8,) + ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.79, 0.86, "TRUE", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.83, 0.32, "EST.", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + ax1.axes.xaxis.set_ticklabels([]) + ax1.legend(loc='upper right') + # Kinetics + ax2.set(xlabel='$P$ [bar]', + xlim = [0,1], ylim = [0, 2]) + ax2.text(0.04, 1.8, "(f)", fontsize=8,) + # ax2.text(0.84, 1.8, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax2.text(0.53, 1.66, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + + # Save the figure + if saveFlag: + # FileName: figureZLCSim___ + saveFileName = "figureZLCSim_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() \ No newline at end of file From 0826603e7a8e2635fd6c3d93aed4b4cbdf1b0bde Mon Sep 17 00:00:00 2001 From: m86972ar Date: Wed, 6 Oct 2021 18:59:25 +0100 Subject: [PATCH 165/189] Small color fix --- plotFunctions/plotsForArticle_Experiment.py | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index f70327a..793a0d3 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -858,8 +858,8 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, currentDT = auxiliaryFunctions.getCurrentDateTime() # Plot colors and markers (isotherm) - colorsForPlot = ["ffba08","d00000","03071e"] - + colorsForPlot = ["0091ad","5c4d7d","b7094c"] + # Universal gas constant Rg = 8.314 @@ -1031,9 +1031,9 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, ax1.set(ylabel='$q^*$ [mol kg$^{-1}$]', xlim = [0,1], ylim = [0, 3]) ax1.text(0.04, 2.75, "(a)", fontsize=8,) - ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.79, 0.30, "TRUE", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.83, 0.12, "EST.", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = 'k') + ax1.text(0.79, 0.30, "TRUE", fontsize=8, fontweight = 'bold',color = 'k') + ax1.text(0.83, 0.12, "EST.", fontsize=8, fontweight = 'bold',color = 'k', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.axes.xaxis.set_ticklabels([]) @@ -1051,9 +1051,9 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, # Isotherm ax1.set(xlim = [0,1], ylim = [0, 1.5]) ax1.text(0.04, 1.35, "(b)", fontsize=8,) - ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.79, 0.15, "TRUE", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.83, 0.06, "EST.", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = 'k') + ax1.text(0.79, 0.15, "TRUE", fontsize=8, fontweight = 'bold',color = 'k') + ax1.text(0.83, 0.06, "EST.", fontsize=8, fontweight = 'bold',color = 'k', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.axes.xaxis.set_ticklabels([]) @@ -1070,9 +1070,9 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, # Isotherm ax1.set(xlim = [0,1], ylim = [0, 8]) ax1.text(0.04, 7.3, "(c)", fontsize=8,) - ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.79, 0.86, "TRUE", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.83, 0.32, "EST.", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = 'k') + ax1.text(0.79, 0.86, "TRUE", fontsize=8, fontweight = 'bold',color = 'k') + ax1.text(0.83, 0.32, "EST.", fontsize=8, fontweight = 'bold',color = 'k', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.axes.xaxis.set_ticklabels([]) From e3a47444b363528d0b365ea0e5a3e83697f43e7c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 8 Oct 2021 13:04:20 +0100 Subject: [PATCH 166/189] Add plot for experimental fit --- plotFunctions/plotsForArticle_Experiment.py | 327 +++++++++++++++++++- 1 file changed, 317 insertions(+), 10 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 793a0d3..16fd459 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2021-10-08, AK: Add plots for experimental fits # - 2021-10-06, AK: Add plots for experimental and computational data (iso) # - 2021-10-04, AK: Add N2/MIP plots # - 2021-10-01, AK: Initial creation @@ -77,6 +78,15 @@ def plotsForArticle_Experiment(**kwargs): plotForArticle_figureZLCSim(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If ZLC fits needs to be plotted + if 'figureZLCFit' in kwargs: + if kwargs["figureZLCFit"]: + plotForArticle_figureZLCFit(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + + + # fun: plotForArticle_figureMat # Plots the Figure DV of the manuscript: Material characterization (N2/MIP and QC) def plotForArticle_figureMat(gitCommitID, currentDT, @@ -448,6 +458,11 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax3.text(83, 0.39, "$V_\mathrm{d}$ = 3.93 cc", fontsize=8, backgroundcolor = 'w', color = '#7d8597') + # Remove all the .npz files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) + # Save the figure if saveFlag: # FileName: figureDV___ @@ -459,11 +474,6 @@ def plotForArticle_figureDV(gitCommitID, currentDT, plt.savefig (savePath) plt.show() - - # Remove all the .npy files genereated from the .mat - # Loop over all available files - for ii in range(len(fileName)): - os.remove(fileName[ii]) # fun: plotForArticle_figureZLC # Plots the Figure ZLC of the manuscript: ZLC parameter estimates @@ -910,10 +920,7 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, isothermParameters = sio.loadmat(fileToLoad)["modelParameters"][0] # Prepare x_VOL - if len(isothermParameters) == 8: - x_SIM = isothermParameters[0:6] - else: - x_SIM = isothermParameters[0:3] + x_SIM = isothermParameters[0:-2] # Loop through all the temperature and mole fraction for jj in range(len(temperature)): @@ -1096,4 +1103,304 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) plt.savefig (savePath) - plt.show() \ No newline at end of file + plt.show() + +# fun: plotForArticle_figureZLCFit +# Plots the Figure ZLC Fit of the manuscript: ZLC goodness of fit for AC +def plotForArticle_figureZLCFit(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + from numpy import load + import os + from simulateCombinedModel import simulateCombinedModel + from deadVolumeWrapper import deadVolumeWrapper + from extractDeadVolume import filesToProcess # File processing script + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers + colorsForPlot = ["ffba08","d00000","03071e"] + markersForPlot = ["^","d","v"] + + # X limits for the different materials + XLIM_L = [[0, 200],[0, 150],[0, 600]] + XLIM_H = [[0, 100],[0, 60],[0, 200]] + + # Label positions for the different materials + panelLabel_L = [185, 150/200*185, 600/200*185] + panelLabel_H = [185/2, 60/100*185/2, 200/100*185/2] + materialLabel_L = [182, 150/200*182, 600/200*180] + materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] + flowLabel_L = [147, 150/200*147, 600/200*147] + flowLabel_H = [147/2, 60/100*147/2, 200/100*147/2] + materialText = ["AC", "BN", "13X"] + + # Parameter estimate files + # Activated Carbon Experiments + zlcFileNameALL = [['zlcParameters_20210822_0926_c8173b1.npz', + 'zlcParameters_20210822_1733_c8173b1.npz', + # 'zlcParameters_20210823_0133_c8173b1.npz', # DSL BAD (but lowest J) + # 'zlcParameters_20210823_1007_c8173b1.npz', # DSL BAD (but lowest J) + 'zlcParameters_20210823_1810_c8173b1.npz'], + # Boron Nitride Experiments + ['zlcParameters_20210823_1731_c8173b1.npz', + 'zlcParameters_20210824_0034_c8173b1.npz', + 'zlcParameters_20210824_0805_c8173b1.npz', + 'zlcParameters_20210824_1522_c8173b1.npz', + 'zlcParameters_20210824_2238_c8173b1.npz',], + # Zeolite 13X Experiments + ['zlcParameters_20210824_1552_6b88505.npz', + 'zlcParameters_20210825_0559_6b88505.npz', + 'zlcParameters_20210825_1854_6b88505.npz', + 'zlcParameters_20210826_0847_6b88505.npz', + 'zlcParameters_20210827_0124_6b88505.npz',]] + + for pp in range(len(zlcFileNameALL)): + zlcFileName = zlcFileNameALL[pp] + objectiveFunction = np.zeros([len(zlcFileName)]) + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # Obtain the onjective function values + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + + # Find the experiment with the min objective function + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + fileParameter = zlcFileName[int(minJ[0])] + + # ZLC parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + + # Parse out experiments names and temperature used for the fitting + rawFileName = load(parameterPath)["fileName"] + temperatureExp = load(parameterPath)["temperature"] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'ZLC') + # Get the processed file names + fileName = filesToProcess(False,[],[],'ZLC') + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Parse out all the necessary quantities to obtain model fit + # Mass of sorbent and particle epsilon + adsorbentDensity = load(parameterPath)["adsorbentDensity"] + particleEpsilon = load(parameterPath)["particleEpsilon"] + massSorbent = load(parameterPath)["massSorbent"] + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + # Dead volume model + deadVolumeFile = str(load(parameterPath)["deadVolumeFile"]) + # Isotherm parameter reference + parameterReference = load(parameterPath)["parameterReference"] + # Load the model + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x = np.multiply(modelNonDim,parameterReference) + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Initialize loadings + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # Loop over all available files + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + + # Initialize outputs + moleFracSim = [] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Parse out parameter values + isothermModel = x[0:-2] + rateConstant_1 = x[-2] + rateConstant_2 = x[-1] + + # Compute the dead volume response using the optimizer parameters + _ , moleFracSim , resultMat = simulateCombinedModel(timeInt = timeInt, + initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point + flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = True, + isothermModel = isothermModel, + rateConstant_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, + deadVolumeFile = deadVolumeFile, + volSorbent = volSorbent, + volGas = volGas, + temperature = temperatureExp[ii], + adsorbentDensity = adsorbentDensity) + # Print simulation volume + print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, + moleFracSim), + timeElapsedExp),2)) + + # Stack mole fraction from experiments and simulation for error + # computation + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) + + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + deadVolumePath = os.path.join('..','simulationResults',deadVolumeFile) + modelOutputTemp = load(deadVolumePath, allow_pickle=True)["modelOutput"] + pDV = modelOutputTemp[()]["variable"] + dvFileLoadTemp = load(deadVolumePath) + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + moleFracDV = deadVolumeWrapper(timeInt, resultMat[3,:]*1e6, pDV, flagMSDeadVolume, msDeadVolumeFile, initMoleFrac = [moleFracExp[0]]) + + if 300__ + saveFileName = "figureZLCFit_" + materialText[pp] + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + + # Remove all the .npz files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) \ No newline at end of file From c427e11dddbad40ca0d6e9b44c02e19f555aff38 Mon Sep 17 00:00:00 2001 From: m86972ar Date: Fri, 15 Oct 2021 18:10:35 +0100 Subject: [PATCH 167/189] Add plots for SI --- plotFunctions/doubleColumn2Row.mplstyle | 2 +- plotFunctions/plotsForArticle_Experiment.py | 742 +++++++++++++++++++- 2 files changed, 708 insertions(+), 36 deletions(-) diff --git a/plotFunctions/doubleColumn2Row.mplstyle b/plotFunctions/doubleColumn2Row.mplstyle index 599f244..3d8dc87 100644 --- a/plotFunctions/doubleColumn2Row.mplstyle +++ b/plotFunctions/doubleColumn2Row.mplstyle @@ -9,7 +9,7 @@ figure.autolayout : true # for labels not being cut out ## Axes axes.titlesize : 8 axes.labelsize : 8 -axes.formatter.limits : -5, 3 +axes.formatter.limits : -5, 5 ## Grid axes.grid : true diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 16fd459..a0d82d6 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2021-10-15, AK: Add plots for SI # - 2021-10-08, AK: Add plots for experimental fits # - 2021-10-06, AK: Add plots for experimental and computational data (iso) # - 2021-10-04, AK: Add N2/MIP plots @@ -74,19 +75,34 @@ def plotsForArticle_Experiment(**kwargs): # If ZLC simulation plot needs to be plotted if 'figureZLCSim' in kwargs: - if kwargs["figureZLCSim"]: + if kwargs["figureZLCSim"]: plotForArticle_figureZLCSim(gitCommitID, currentDT, saveFlag, saveFileExtension) # If ZLC fits needs to be plotted if 'figureZLCFit' in kwargs: if kwargs["figureZLCFit"]: - plotForArticle_figureZLCFit(gitCommitID, currentDT, + plotForArticle_figureZLCFit(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If ZLC simulation fits needs to be plotted + if 'figureZLCSimFit' in kwargs: + if kwargs["figureZLCSimFit"]: + plotForArticle_figureZLCSimFit(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If ZLC experimental repeats needs to be plotted + if 'figureZLCRep' in kwargs: + if kwargs["figureZLCRep"]: + plotForArticle_figureZLCRep(gitCommitID, currentDT, saveFlag, saveFileExtension) - - - + # If ZLC objective functions + if 'figureZLCObj' in kwargs: + if kwargs["figureZLCObj"]: + plotForArticle_figureZLCObj(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # fun: plotForArticle_figureMat # Plots the Figure DV of the manuscript: Material characterization (N2/MIP and QC) def plotForArticle_figureMat(gitCommitID, currentDT, @@ -197,12 +213,12 @@ def plotForArticle_figureMat(gitCommitID, currentDT, # Material specific text labels if kk == 0: ax.set(xlabel='$D$ [nm]', - ylabel='$V$ [mL g$^{-1}$]', + ylabel='$V$ [cm$^{3}$ g$^{-1}$]', xlim = [0.1,1e6], ylim = [0, 2]) ax.text(0.2, 1.82, "(a)", fontsize=8,) ax.text(1.4e5, 0.1, "AC", fontsize=8, fontweight = 'bold',color = '#e71d36') - ax.text(2e3, combinedPorosityData[-2,2]+0.07, - str(round(combinedPorosityData[-2,2],2))+' mL g$^{-1}$', + ax.text(1.6e3, combinedPorosityData[-2,2]+0.07, + str(round(combinedPorosityData[-2,2],2))+' cm$^{3}$ g$^{-1}$', fontsize=8,color = '#7d8597') elif kk == 1: ax.set(xlabel='$D$ [nm]', @@ -210,15 +226,15 @@ def plotForArticle_figureMat(gitCommitID, currentDT, ax.text(0.2, 1.82, "(b)", fontsize=8,) ax.text(1.4e5, 0.1, "BN", fontsize=8, fontweight = 'bold',color = '#e71d36') ax.text(5, combinedPorosityData[-2,2]-0.15, - str(round(combinedPorosityData[-2,2],2))+' mL g$^{-1}$', + str(round(combinedPorosityData[-2,2],2))+' cm$^{3}$ g$^{-1}$', fontsize=8,color = '#7d8597') elif kk == 2: ax.set(xlabel='$D$ [nm]', xlim = [0.1,1e6], ylim = [0, 2]) ax.text(0.2, 1.82, "(c)", fontsize=8,) ax.text(1e5, 0.1, "13X", fontsize=8, fontweight = 'bold',color = '#e71d36') - ax.text(2e3, combinedPorosityData[-2,2]+0.07, - str(round(combinedPorosityData[-2,2],2))+' mL g$^{-1}$', + ax.text(1.6e3, combinedPorosityData[-2,2]+0.07, + str(round(combinedPorosityData[-2,2],2))+' cm$^{3}$ g$^{-1}$', fontsize=8,color = '#7d8597') ax.locator_params(axis="y", nbins=5) @@ -255,7 +271,7 @@ def plotForArticle_figureMat(gitCommitID, currentDT, # Plot fitted isotherm ax.plot(isothermFitALL[1:-1,0],isothermFitALL[1:-1,ll+1], linewidth = 1,color='#'+colorsForPlot_I[ll],alpha=0.5) - ax.legend(loc='best') + ax.legend(loc='best', handletextpad=0.0) # Material specific text labels if kk == 0: ax.set(xlabel='$P$ [bar]', @@ -336,6 +352,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, # Load the model modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] x = modelOutputTemp[()]["variable"] + print(x) # Get the MS fit flag, flow rates and msDeadVolumeFile (if needed) # Check needs to be done to see if MS file available or not @@ -535,7 +552,7 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) kineticConstant_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) objectiveFunction = np.zeros([len(zlcFileName)]) - + # Loop over all available ZLC files for a given material for kk in range(len(zlcFileName)): # ZLC Data @@ -598,22 +615,22 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) for jj in range(len(temperature)): - for kk in range(len(zlcFileName)): - if kk == minJ[0]: - ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Lowest J + for qq in range(len(zlcFileName)): + if qq == minJ[0]: + ax1.plot(y,isoLoading_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Lowest J else: - ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.2) # ALL + ax1.plot(y,isoLoading_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha=0.2) # ALL # Plot the kinetic constants ax2 = plt.subplot(2,3,pp+4) minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) for jj in range(len(temperature)): - for kk in range(len(zlcFileName)): - if kk == minJ[0]: - ax2.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Lowest J + for qq in range(len(zlcFileName)): + if qq == minJ[0]: + ax2.plot(y,kineticConstant_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Lowest J else: - ax2.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.2) # ALL + ax2.plot(y,kineticConstant_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha=0.2) # ALL if pp == 0: # Isotherm @@ -799,9 +816,9 @@ def plotForArticle_figureComp(gitCommitID, currentDT, for jj in range(len(temperature)): ax1.plot(y,isoLoading_VOL[:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # QC - for kk in range(len(zlcFileName)): - if kk == minJ[0]: - ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha = 0.2) # Lowest J + for qq in range(len(zlcFileName)): + if qq == minJ[0]: + ax1.plot(y,isoLoading_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha = 0.2) # Lowest J if pp == 0: # Isotherm @@ -849,7 +866,7 @@ def plotForArticle_figureComp(gitCommitID, currentDT, plt.show() # fun: plotForArticle_figureZLCSim -# Plots the Figure ZLC of the manuscript: ZLC parameter estimates (simulated) +# Plots the Figure ZLC Sim of the manuscript: ZLC parameter estimates (simulated) def plotForArticle_figureZLCSim(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np @@ -1023,15 +1040,15 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, ax1 = plt.subplot(2,3,pp+1) for jj in range(len(temperature)): ax1.plot(y,isoLoading_SIM[:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Simulated "True" Data - for kk in range(len(zlcFileName)): - ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.1) # ALL + for qq in range(len(zlcFileName)): + ax1.plot(y,isoLoading_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha=0.1) # ALL # Plot the kinetic constants ax2 = plt.subplot(2,3,pp+4) for jj in range(len(temperature)): ax2.plot(y,kineticConstant_SIM[:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # Simulated "True" Data - for kk in range(len(zlcFileName)): - ax2.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorsForPlot[jj],alpha=0.1) # ALL + for qq in range(len(zlcFileName)): + ax2.plot(y,kineticConstant_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha=0.1) # ALL if pp == 0: # Isotherm @@ -1106,7 +1123,7 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, plt.show() # fun: plotForArticle_figureZLCFit -# Plots the Figure ZLC Fit of the manuscript: ZLC goodness of fit for AC +# Plots the Figure ZLC Fit of the manuscript: ZLC goodness of fit for experimental results def plotForArticle_figureZLCFit(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np @@ -1213,6 +1230,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, modelNonDim = modelOutputTemp[()]["variable"] # Multiply the paremeters by the reference values x = np.multiply(modelNonDim,parameterReference) + print(x) # Downsample intervals downsampleInt = numPointsExp/np.min(numPointsExp) @@ -1383,10 +1401,6 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, ax4.text(panelLabel_H[pp], 0.67, "(d)", fontsize=8,) ax4.text(materialLabel_H[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') ax4.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 ccm", fontsize=8, fontweight = 'bold',color = '#4895EF') - - - # ax2.text(0.04, 0.9, "(d)", fontsize=8,) - # ax2.text(0.04, 0.9, "(d)", fontsize=8,) # Save the figure if saveFlag: @@ -1403,4 +1417,662 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, # Remove all the .npz files genereated from the .mat # Loop over all available files for ii in range(len(fileName)): - os.remove(fileName[ii]) \ No newline at end of file + os.remove(fileName[ii]) + +# fun: plotForArticle_figureZLCSimFit +# Plots the Figure ZLC Fit of the manuscript: ZLC goodness for computational results +def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + from numpy import load + import os + from simulateCombinedModel import simulateCombinedModel + from deadVolumeWrapper import deadVolumeWrapper + from extractDeadVolume import filesToProcess # File processing script + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers + colorsForPlot = ["0091ad","5c4d7d","b7094c"] + markersForPlot = ["^","d","v"] + + # X limits for the different materials + XLIM_L = [[0, 200],[0, 150],[0, 600]] + XLIM_H = [[0, 100],[0, 60],[0, 200]] + + # Label positions for the different materials + panelLabel_L = [185, 150/200*185, 600/200*185] + panelLabel_H = [185/2, 60/100*185/2, 200/100*185/2] + materialLabel_L = [182, 150/200*182, 600/200*180] + materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] + flowLabel_L = [147, 150/200*147, 600/200*147] + flowLabel_H = [147/2, 60/100*147/2, 200/100*147/2] + materialText = ["AC", "BN", "13X"] + + # Parameter estimate files + # Activated Carbon Simulations + zlcFileNameALL = [['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'], + # Boron Nitride Simulations + ['zlcParameters_20210823_1907_03c82f4.npz', + 'zlcParameters_20210824_0555_03c82f4.npz', + 'zlcParameters_20210824_2105_03c82f4.npz', + 'zlcParameters_20210825_0833_03c82f4.npz', + 'zlcParameters_20210825_2214_03c82f4.npz'], + # Zeolite 13X Simulations + ['zlcParameters_20210824_1102_c8173b1.npz', + 'zlcParameters_20210825_0243_c8173b1.npz', + 'zlcParameters_20210825_1758_c8173b1.npz', + 'zlcParameters_20210826_1022_c8173b1.npz', + 'zlcParameters_20210827_0104_c8173b1.npz']] + + for pp in range(len(zlcFileNameALL)): + zlcFileName = zlcFileNameALL[pp] + objectiveFunction = np.zeros([len(zlcFileName)]) + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # Obtain the onjective function values + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + + # Find the experiment with the min objective function + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + fileParameter = zlcFileName[int(minJ[0])] + + # ZLC parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + + # Parse out experiments names and temperature used for the fitting + rawFileName = load(parameterPath)["fileName"] + temperatureExp = load(parameterPath)["temperature"] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'ZLC') + # Get the processed file names + fileName = filesToProcess(False,[],[],'ZLC') + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Parse out all the necessary quantities to obtain model fit + # Mass of sorbent and particle epsilon + adsorbentDensity = load(parameterPath)["adsorbentDensity"] + particleEpsilon = load(parameterPath)["particleEpsilon"] + massSorbent = load(parameterPath)["massSorbent"] + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + # Dead volume model + deadVolumeFile = str(load(parameterPath)["deadVolumeFile"]) + # Isotherm parameter reference + parameterReference = load(parameterPath)["parameterReference"] + # Load the model + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x = np.multiply(modelNonDim,parameterReference) + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Initialize loadings + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # Loop over all available files + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + + # Initialize outputs + moleFracSim = [] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Parse out parameter values + isothermModel = x[0:-2] + rateConstant_1 = x[-2] + rateConstant_2 = x[-1] + + # Compute the dead volume response using the optimizer parameters + _ , moleFracSim , resultMat = simulateCombinedModel(timeInt = timeInt, + initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point + flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = True, + isothermModel = isothermModel, + rateConstant_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, + deadVolumeFile = deadVolumeFile, + volSorbent = volSorbent, + volGas = volGas, + temperature = temperatureExp[ii], + adsorbentDensity = adsorbentDensity) + # Print simulation volume + print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, + moleFracSim), + timeElapsedExp),2)) + + # Stack mole fraction from experiments and simulation for error + # computation + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) + + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + deadVolumePath = os.path.join('..','simulationResults',deadVolumeFile) + modelOutputTemp = load(deadVolumePath, allow_pickle=True)["modelOutput"] + pDV = modelOutputTemp[()]["variable"] + dvFileLoadTemp = load(deadVolumePath) + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + moleFracDV = deadVolumeWrapper(timeInt, resultMat[3,:]*1e6, pDV, flagMSDeadVolume, msDeadVolumeFile, initMoleFrac = [moleFracExp[0]]) + + if 300__ + saveFileName = "figureZLCSimFit_" + materialText[pp] + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + + # Remove all the .npz files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) + +# fun: plotForArticle_figureZLCRep +# Plots the Figure repetition of the manuscript: ZLC experimental repetitions +def plotForArticle_figureZLCRep(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + from numpy import load + import os + from extractDeadVolume import filesToProcess # File processing script + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers + colorsForPlot = ["ffba08","d00000","03071e"] + markersForPlot = ["^","d","v"] + + # X limits for the different materials + XLIM_L = [[0, 200],[0, 150],[0, 600]] + XLIM_H = [[0, 100],[0, 60],[0, 200]] + + # Label positions for the different materials + panelLabel_L = [175, 150/200*175, 600/200*175] + panelLabel_H = [175/2, 60/100*175/2, 200/100*175/2] + materialLabel_L = [172, 150/200*172, 600/200*165] + materialLabel_H = [172/2, 60/100*172/2, 200/100*165/2] + flowLabel_L = [113, 150/200*113, 600/200*113] + flowLabel_H = [113/2, 60/100*113/2, 200/100*113/2] + materialText = ["AC", "BN", "13X"] + panelLabel = ["(a)","(b)","(c)","(d)","(e)","(f)"] + repLabel_L = [10, 10*150/200, 10*600/200] + repLabel_H = [80, 80*60/100, 80*200/100] + + # Names of experiment and their corresponding temperature + # Activated Carbon Experiments + rawFileNameALL = [['ZLC_ActivatedCarbon_Exp76B_Output.mat', + 'ZLC_ActivatedCarbon_Exp74B_Output.mat', + 'ZLC_ActivatedCarbon_Exp72B_Output.mat', + 'ZLC_ActivatedCarbon_Exp77B_Output.mat', + 'ZLC_ActivatedCarbon_Exp75B_Output.mat', + 'ZLC_ActivatedCarbon_Exp73B_Output.mat', + 'ZLC_ActivatedCarbon_Exp78B_Output.mat', + 'ZLC_ActivatedCarbon_Exp80B_Output.mat', + 'ZLC_ActivatedCarbon_Exp82B_Output.mat', + 'ZLC_ActivatedCarbon_Exp79B_Output.mat', + 'ZLC_ActivatedCarbon_Exp81B_Output.mat', + 'ZLC_ActivatedCarbon_Exp83B_Output.mat'], + # # Boron Nitride Experiments + ['ZLC_BoronNitride_Exp38B_Output.mat', + 'ZLC_BoronNitride_Exp36B_Output.mat', + 'ZLC_BoronNitride_Exp34B_Output.mat', + 'ZLC_BoronNitride_Exp39B_Output.mat', + 'ZLC_BoronNitride_Exp37B_Output.mat', + 'ZLC_BoronNitride_Exp35B_Output.mat', + 'ZLC_BoronNitride_Exp40B_Output.mat', + 'ZLC_BoronNitride_Exp42B_Output.mat', + 'ZLC_BoronNitride_Exp44B_Output.mat', + 'ZLC_BoronNitride_Exp41B_Output.mat', + 'ZLC_BoronNitride_Exp43B_Output.mat', + 'ZLC_BoronNitride_Exp45B_Output.mat',], + # Zeolite 13X Experiments + ['ZLC_Zeolite13X_Exp62B_Output.mat', + 'ZLC_Zeolite13X_Exp58B_Output.mat', + 'ZLC_Zeolite13X_Exp54B_Output.mat', + 'ZLC_Zeolite13X_Exp63B_Output.mat', + 'ZLC_Zeolite13X_Exp59B_Output.mat', + 'ZLC_Zeolite13X_Exp55B_Output.mat', + 'ZLC_Zeolite13X_Exp66B_Output.mat', + 'ZLC_Zeolite13X_Exp70B_Output.mat', + 'ZLC_Zeolite13X_Exp68B_Output.mat', + 'ZLC_Zeolite13X_Exp67B_Output.mat', + 'ZLC_Zeolite13X_Exp71B_Output.mat', + 'ZLC_Zeolite13X_Exp69B_Output.mat',]] + + temperatureALL = [[306,325,345]*4, # Activated carbon + [306,325,345]*4, # Boron Nitrode + [306,326,345]*4,] # Zeolite 13X + + for pp in range(len(rawFileNameALL)): + rawFileName = rawFileNameALL[pp] + + # Parse out temperature used for the fitting + temperatureExp = temperatureALL[pp] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'ZLC') + # Get the processed file names + fileName = filesToProcess(False,[],[],'ZLC') + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Loop over all available files + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + + if 300__ + saveFileName = "figureZLCRep" + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + + # Remove all the .npz files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) + +# fun: plotForArticle_figureZLCObj +# Plots the Figure ZLC Fit of the manuscript: ZLC goodness of fit for experimental results +def plotForArticle_figureZLCObj(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + from numpy import load + import os + from matplotlib.ticker import MaxNLocator + plt.style.use('doubleColumn2Row.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() + + # Minimum J for all the materials (computational) - HARD CODED + minJ_True = [-1894, -1077, -10592] + + # X limits for the different materials + YLIM_L = [[180, 200],[380, 400],[-80, -50]] + YLIM_H = [[-2000, -1400],[-1200, -400],[-12000, -4000]] + + # Label positions for the different materials + minJLabel = [-1875,-1045, -10300] + materialText = ["AC", "BN", "13X"] + materialLabel_X = [4.45, 4.45, 4.32] + materialLabel_L = [196.75, 396.75, -55] + materialLabel_H = [-1500, -535, -5400] + panelLabel = ["(a)","(b)","(c)","(d)","(e)","(f)"] + panelLabel_L = [198.5, 398.5, -52.5] + panelLabel_H = [-1450, -470, -4750] + + # Parameter estimate files (Experimental) + # Activated Carbon Experiments + zlcFileNameExpALL = [['zlcParameters_20210822_0926_c8173b1.npz', + 'zlcParameters_20210822_1733_c8173b1.npz', + # 'zlcParameters_20210823_0133_c8173b1.npz', # DSL BAD (but lowest J) + # 'zlcParameters_20210823_1007_c8173b1.npz', # DSL BAD (but lowest J) + 'zlcParameters_20210823_1810_c8173b1.npz'], + # Boron Nitride Experiments + ['zlcParameters_20210823_1731_c8173b1.npz', + 'zlcParameters_20210824_0034_c8173b1.npz', + 'zlcParameters_20210824_0805_c8173b1.npz', + 'zlcParameters_20210824_1522_c8173b1.npz', + 'zlcParameters_20210824_2238_c8173b1.npz',], + # Zeolite 13X Experiments + ['zlcParameters_20210824_1552_6b88505.npz', + 'zlcParameters_20210825_0559_6b88505.npz', + 'zlcParameters_20210825_1854_6b88505.npz', + 'zlcParameters_20210826_0847_6b88505.npz', + 'zlcParameters_20210827_0124_6b88505.npz',]] + + # Parameter estimate files (Computational) + # Activated Carbon Simulations + zlcFileNameSimALL = [['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'], + # Boron Nitride Simulations + ['zlcParameters_20210823_1907_03c82f4.npz', + 'zlcParameters_20210824_0555_03c82f4.npz', + 'zlcParameters_20210824_2105_03c82f4.npz', + 'zlcParameters_20210825_0833_03c82f4.npz', + 'zlcParameters_20210825_2214_03c82f4.npz'], + # Zeolite 13X Simulations + ['zlcParameters_20210824_1102_c8173b1.npz', + 'zlcParameters_20210825_0243_c8173b1.npz', + 'zlcParameters_20210825_1758_c8173b1.npz', + 'zlcParameters_20210826_1022_c8173b1.npz', + 'zlcParameters_20210827_0104_c8173b1.npz']] + + for pp in range(len(zlcFileNameExpALL)): + # Experiments + zlcFileName = zlcFileNameExpALL[pp] + objectiveFunction = np.zeros([len(zlcFileName)]) + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # Obtain the onjective function values + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + + ax1 = plt.subplot(2,3,pp+1) + xx = range(1,len(zlcFileName)+1) + ax1.plot(xx,objectiveFunction, + linestyle = ':', linewidth = 0.5, + color = '#7d8597', marker = '^', markersize = 3, + markerfacecolor = '#d00000', + markeredgecolor = '#d00000') # ALL + + if pp == 0: + ax1.set(ylabel='$J$ [-]', + xlim = [1,5],ylim = YLIM_L[pp]) + else: + ax1.set(xlim = [1,5],ylim = YLIM_L[pp]) + ax1.locator_params(axis="y", nbins=4) + ax1.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax1.axes.xaxis.set_ticklabels([]) + + # Simulation + zlcFileName = zlcFileNameSimALL[pp] + objectiveFunction = np.zeros([len(zlcFileName)]) + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # Obtain the onjective function values + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + + ax2 = plt.subplot(2,3,pp+4) + xx = range(1,len(zlcFileName)+1) + ax2.plot(xx,objectiveFunction, + linestyle = ':', linewidth = 0.5, + color = '#7d8597', marker = 'v', markersize = 3, + markerfacecolor = '#5c4d7d', + markeredgecolor = '#5c4d7d') # ALL + ax2.axhline(minJ_True[pp], + linestyle = ':', linewidth = 1, color = '#7d8597') + ax2.text(1.25, minJLabel[pp], '$J_\mathregular{true}$ = ' + str(minJ_True[pp]), + fontsize=8,color = '#7d8597') + + if pp == 0: + ax2.set(xlabel='Iteration [-]', + ylabel='$J$ [-]', + xlim = [1,5],ylim = YLIM_H[pp]) + else: + ax2.set(xlabel='Iteration [-]', + xlim = [1,5],ylim = YLIM_H[pp]) + ax2.locator_params(axis="y", nbins=4) + ax2.xaxis.set_major_locator(MaxNLocator(integer=True)) + + # Put other text entries + ax1.text(1.15, panelLabel_L[pp], panelLabel[pp], fontsize=8,) + ax2.text(1.15, panelLabel_H[pp], panelLabel[pp+3], fontsize=8,) + + ax1.text(3.1,materialLabel_L[pp], "Experimental", fontsize=8, fontweight = 'bold',color = '#7d8597') + ax1.text(materialLabel_X[pp],panelLabel_L[pp], materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') + + ax2.text(2.9,materialLabel_H[pp], "Computational", fontsize=8, fontweight = 'bold',color = '#7d8597') + ax2.text(materialLabel_X[pp],panelLabel_H[pp], materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') + + # Save the figure + if saveFlag: + # FileName: figureZLCObj___ + saveFileName = "figureZLCObj" + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() \ No newline at end of file From c934599f5f23333f28a6e1204819cfaf50d64aa7 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 18 Oct 2021 18:56:48 +0100 Subject: [PATCH 168/189] Cosmetic changes for experimental plots --- plotFunctions/plotsForArticle_Experiment.py | 52 ++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index a0d82d6..409b558 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -213,7 +213,7 @@ def plotForArticle_figureMat(gitCommitID, currentDT, # Material specific text labels if kk == 0: ax.set(xlabel='$D$ [nm]', - ylabel='$V$ [cm$^{3}$ g$^{-1}$]', + ylabel='$V_\mathregular{pore}$ [cm$^{3}$ g$^{-1}$]', xlim = [0.1,1e6], ylim = [0, 2]) ax.text(0.2, 1.82, "(a)", fontsize=8,) ax.text(1.4e5, 0.1, "AC", fontsize=8, fontweight = 'bold',color = '#e71d36') @@ -429,7 +429,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, if kk == 0: ax1.semilogy(timeElapsedExp,moleFracExp, marker = markersForPlot[ii],linewidth = 0, - color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" ccs") # Experimental response + color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" cm$^3$ s$^{-1}$") # Experimental response ax1.semilogy(timeElapsedExp,moleFracSim, color='#'+colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$t$ [s]', @@ -438,15 +438,15 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax1.locator_params(axis="x", nbins=5) ax1.legend(handletextpad=0.0,loc='center right') ax1.text(7, 1.3, "(a)", fontsize=8,) - ax1.text(12.2, 0.64, "MS", fontsize=8, fontweight = 'bold', + ax1.text(12.7, 0.64, "MS", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax1.text(8.2, 0.39, "$V_\mathrm{d}$ = 0.02 cc", fontsize=8, + ax1.text(7.4, 0.39, "$V_\mathrm{D}$ = 0.02 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') elif kk == 1: ax2.semilogy(timeElapsedExp,moleFracExp, marker = markersForPlot[ii],linewidth = 0, - color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" ccs") # Experimental response + color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" cm$^3$ s$^{-1}$") # Experimental response ax2.semilogy(timeElapsedExp,moleFracSim, color='#'+colorsForPlot[ii]) # Simulation response ax2.set(xlabel='$t$ [s]', @@ -454,15 +454,15 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax2.locator_params(axis="x", nbins=5) ax2.legend(handletextpad=0.0,loc='center right') ax2.text(70, 1.3, "(b)", fontsize=8,) - ax2.text(67, 0.64, "Column w/ Ball", fontsize=8, fontweight = 'bold', + ax2.text(70, 0.64, "Column w/ Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax2.text(83, 0.39, "$V_\mathrm{d}$ = 3.76 cc", fontsize=8, + ax2.text(78, 0.39, "$V_\mathrm{D}$ = 3.78 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') elif kk == 2: ax3.semilogy(timeElapsedExp,moleFracExp, marker = markersForPlot[ii],linewidth = 0, - color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" ccs") # Experimental response + color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" cm$^3$ s$^{-1}$") # Experimental response ax3.semilogy(timeElapsedExp,moleFracSim, color='#'+colorsForPlot[ii]) # Simulation response ax3.set(xlabel='$t$ [s]', @@ -470,9 +470,9 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax3.locator_params(axis="x", nbins=5) ax3.legend(handletextpad=0.0,loc='center right') ax3.text(70, 1.3, "(c)", fontsize=8,) - ax3.text(61, 0.64, "Column w/o Ball", fontsize=8, fontweight = 'bold', + ax3.text(64, 0.64, "Column w/o Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax3.text(83, 0.39, "$V_\mathrm{d}$ = 3.93 cc", fontsize=8, + ax3.text(78, 0.39, "$V_\mathrm{D}$ = 3.95 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') # Remove all the .npz files genereated from the .mat @@ -1155,8 +1155,8 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, panelLabel_H = [185/2, 60/100*185/2, 200/100*185/2] materialLabel_L = [182, 150/200*182, 600/200*180] materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] - flowLabel_L = [147, 150/200*147, 600/200*147] - flowLabel_H = [147/2, 60/100*147/2, 200/100*147/2] + flowLabel_L = [123, 150/200*123, 600/200*123] + flowLabel_H = [123/2, 60/100*123/2, 200/100*123/2] materialText = ["AC", "BN", "13X"] # Parameter estimate files @@ -1315,7 +1315,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax1.semilogy(timeElapsedExp,moleFracDV, - color='#1697a6',alpha = 0.2, linestyle=':') # Dead volume response + color='#76c893',alpha = 0.2, linestyle='-') # Dead volume response ax1.set(ylabel='$y$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) @@ -1332,7 +1332,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax2.semilogy(timeElapsedExp,moleFracDV, - color='#1697a6',alpha = 0.2, linestyle=':') # Dead volume response + color='#76c893',alpha = 0.2, linestyle='-') # Dead volume response ax2.set(xlabel='$t$ [s]',ylabel='$y$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax2.locator_params(axis="x", nbins=4) @@ -1348,7 +1348,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax3.semilogy(timeElapsedExp,moleFracDV, - color='#1697a6',alpha = 0.2, linestyle=':') # Dead volume response + color='#76c893',alpha = 0.2, linestyle='-') # Dead volume response ax3.set(xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax3.locator_params(axis="x", nbins=4) ax3.axes.xaxis.set_ticklabels([]) @@ -1367,7 +1367,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax4.semilogy(timeElapsedExp,moleFracDV, - color='#1697a6',alpha = 0.2, linestyle=':') # Dead volume response + color='#76c893',alpha = 0.2, linestyle='-') # Dead volume response ax4.set(xlabel='$t$ [s]', xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax4.locator_params(axis="x", nbins=4) @@ -1388,19 +1388,19 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, # Put other text entries ax1.text(panelLabel_L[pp], 0.67, "(a)", fontsize=8,) ax1.text(materialLabel_L[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 ccm", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#4895EF') ax2.text(panelLabel_L[pp], 0.67, "(c)", fontsize=8,) ax2.text(materialLabel_L[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') - ax2.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 ccm", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax2.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#4895EF') ax3.text(panelLabel_H[pp], 0.67, "(b)", fontsize=8,) ax3.text(materialLabel_H[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') - ax3.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 ccm", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax3.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#4895EF') ax4.text(panelLabel_H[pp], 0.67, "(d)", fontsize=8,) ax4.text(materialLabel_H[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') - ax4.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 ccm", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax4.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#4895EF') # Save the figure if saveFlag: @@ -1452,8 +1452,8 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, panelLabel_H = [185/2, 60/100*185/2, 200/100*185/2] materialLabel_L = [182, 150/200*182, 600/200*180] materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] - flowLabel_L = [147, 150/200*147, 600/200*147] - flowLabel_H = [147/2, 60/100*147/2, 200/100*147/2] + flowLabel_L = [123, 150/200*123, 600/200*123] + flowLabel_H = [123/2, 60/100*123/2, 200/100*123/2] materialText = ["AC", "BN", "13X"] # Parameter estimate files @@ -1611,7 +1611,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax1.semilogy(timeElapsedExp,moleFracDV, - color='k',alpha = 0.2, linestyle=':') # Dead volume response + color='k',alpha = 0.2, linestyle='-') # Dead volume response ax1.set(ylabel='$y$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) @@ -1628,7 +1628,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax2.semilogy(timeElapsedExp,moleFracDV, - color='k',alpha = 0.2, linestyle=':') # Dead volume response + color='k',alpha = 0.2, linestyle='-') # Dead volume response ax2.set(xlabel='$t$ [s]',ylabel='$y$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax2.locator_params(axis="x", nbins=4) @@ -1644,7 +1644,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax3.semilogy(timeElapsedExp,moleFracDV, - color='k',alpha = 0.2, linestyle=':') # Dead volume response + color='k',alpha = 0.2, linestyle='-') # Dead volume response ax3.set(xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax3.locator_params(axis="x", nbins=4) ax3.axes.xaxis.set_ticklabels([]) @@ -1663,7 +1663,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, color='#'+colorTemp,label=legendStr) # Simulation response if ii%3 == 0: ax4.semilogy(timeElapsedExp,moleFracDV, - color='k',alpha = 0.2, linestyle=':') # Dead volume response + color='k',alpha = 0.2, linestyle='-') # Dead volume response ax4.set(xlabel='$t$ [s]', xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax4.locator_params(axis="x", nbins=4) From 20543381974fc7e19f6a9b4ec795d258059d85be Mon Sep 17 00:00:00 2001 From: ha3215 Date: Fri, 22 Oct 2021 10:44:26 +0100 Subject: [PATCH 169/189] Add subfunctions to plot raw textural characterization data, and repeats of MS calibration --- IsothermFittingTool | 2 +- plotFunctions/plotsForArticle_Experiment.py | 311 ++++++++++++++++++++ 2 files changed, 312 insertions(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 2788340..147e08c 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 27883404c3602d1b365bbc7a34c0f7f179a21601 +Subproject commit 147e08cda9c9c4788d0a130c39c51ca85739e620 diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 409b558..ebc0ac5 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -102,6 +102,18 @@ def plotsForArticle_Experiment(**kwargs): if kwargs["figureZLCObj"]: plotForArticle_figureZLCObj(gitCommitID, currentDT, saveFlag, saveFileExtension) + + # If raw textural characterization + if 'figureRawTex' in kwargs: + if kwargs["figureRawTex"]: + plotForArticle_figureRawTex(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If MS calibration comparison + if 'figureMSCal' in kwargs: + if kwargs["figureMSCal"]: + plotForArticle_figureMSCal(gitCommitID, currentDT, + saveFlag, saveFileExtension) # fun: plotForArticle_figureMat # Plots the Figure DV of the manuscript: Material characterization (N2/MIP and QC) @@ -2075,4 +2087,303 @@ def plotForArticle_figureZLCObj(gitCommitID, currentDT, os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) plt.savefig (savePath) + plt.show() + + # fun: plotForArticle_figureRawTex + # Plots the Figure SX of the manuscript: Raw Textural Characterization (MIP, N2 and XRD) +def plotForArticle_figureRawTex(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + import scipy.io as sio + import os + 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() + + # Plot colors and markers (isotherm) + colorsForPlot_I = ["ffba08","d00000","03071e"] + markersForPlot_I = ["^","d","v"] + + # Folder for material characterization + mainDir = os.path.join('..','experimental','materialCharacterization') + + + # File with pore characterization data + rawDataALL = ['rawTexData.mat'] + + # Loop over all the raw files + for kk in range(len(rawDataALL)): + # Create the instance for the plots + ax1 = plt.subplot(1,3,2) + + # Path of the file name + fileToLoad = os.path.join(mainDir,rawDataALL[kk]) + + # Get the MIP points + MIPALL = sio.loadmat(fileToLoad)["rawTexturalData"]["MIP"][0][0] + + # Get the QC points + QCALL = sio.loadmat(fileToLoad)["rawTexturalData"]["QC"][0][0] + + # Get the XRD points + XRDALL = sio.loadmat(fileToLoad)["rawTexturalData"]["XRD"][0][0] + + adsorbentName = ['AC', 'BN', '13X'] + + # Find indices corresponding to each material + for ll in range(3): + + # Plot MIP data + ax1.semilogx(MIPALL[:,0+2*ll], + MIPALL[:,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + color='#'+colorsForPlot_I[ll], + label = str(adsorbentName[ll])) + + # Text labels + ax1.set(xlabel='$P$ [psia]', + ylabel='$V_{\mathrm{Hg}}$ [cm$^{3}$ g$^{-1}$]', + xlim = [1e-1,1e5], ylim = [0, 2]) + # ax1.text(70, 1.3, "(a)", fontsize=8,) + ax1.legend(loc='upper right', handletextpad=0.0) + + ax1.locator_params(axis="y", nbins=4) + ax1.text(0.2, 1.82, "(b)", fontsize=8,) + + + # Create the instance for the plots + ax2 = plt.subplot(1,3,3) + + adsorbentName = [' AC', ' BN', ' 13X'] + + # Find indices corresponding to each material + for ll in range(3): + + # Plot XRD data + ax2.plot(XRDALL[:,0+2*ll], + XRDALL[:,1+2*ll]/np.max(XRDALL[:,1+2*ll])+ll, + linewidth = 0.5, + color='#'+colorsForPlot_I[ll], + label = str(adsorbentName[ll])) + + # Text labels + ax2.set(xlabel='2\u03B8 [deg]', + ylabel='$I$ [-]', + xlim = [5,60], ylim = [0, 3.5]) + # ax2.text(70, 1.3, "(b)", fontsize=8,) + ax2.legend(loc='best', handletextpad=0.0) + + ax2.locator_params(axis="x", nbins=6) + ax2.locator_params(axis="y", nbins=1) + ax2.yaxis.set_ticklabels([]) + ax2.yaxis.set_ticks([]) + ax2.text(7.5, 3.2, "(c)", fontsize=8,) + + # Create the instance for the plots + ax3 = plt.subplot(1,3,1) + + adsorbentName = [' AC', ' BN', ' 13X'] + + # Find indices corresponding to each material + for ll in range(3): + + # Plot N2 77 K isotherm data + ax3.plot(QCALL[:,0+2*ll], + QCALL[:,1+2*ll], + linewidth = 0.5, + linestyle = ':', + marker = markersForPlot_I[ll], + color='#'+colorsForPlot_I[ll], + label = str(adsorbentName[ll])) + + # Text labels + ax3.set(xlabel='$P/P_0$', + ylabel='$q^*_{\mathrm{N}_2}$ [cm$^{3}$(STP) g$^{-1}$]', + xlim = [0,1], ylim = [0, 600]) + # ax3.text(70, 1.3, "(c)", fontsize=8,) + ax3.legend(loc='upper right', handletextpad=0.0) + + ax3.locator_params(axis="x", nbins=4) + ax3.locator_params(axis="y", nbins=4) + ax3.text(0.05, 550, "(a)", fontsize=8,) + + # Save the figure + if saveFlag: + # FileName: figureRawTex___ + saveFileName = "figureRawTex_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + + # fun: plotForArticle_figureMSCal + # Plots the Figure SX of the manuscript: Repeats of MS calibration over the course of 83 days +def plotForArticle_figureMSCal(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import matplotlib.pyplot as plt + import auxiliaryFunctions + import scipy.io as sio + import os + 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() + + # Plot colors and markers (isotherm) + colorsForPlot_I = ["ffba08","03071e"] + markersForPlot_I = ["^","^","^"] + + # Folder for MS Calibration + mainDir = os.path.join('..','experimental','msCalibrationData') + + + # File with MS data + msDataNEW = ['msData_072621.mat'] + msDataOLD = ['msData_050521.mat'] + + # Create the instance for the plots + ax1 = plt.subplot(1,2,1) + ax2 = plt.subplot(1,2,2) + + + # Loop over all the flowrate files + for kk in range(len(msDataOLD)): + # Path of the file name + fileToLoad = os.path.join(mainDir,msDataOLD[kk]) + + # Get the MIP points + msDataOLDALL = sio.loadmat(fileToLoad)["msData_050521"] + + # Find indices corresponding to each flowrate + for ll in range(3): + + # Plot MS calibration plot for old repeat + if ll == 1: + ax1.plot(msDataOLDALL[:,0+2*ll], + msDataOLDALL[:,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[1], + label = str('Day 1')) + + ax2.semilogy(msDataOLDALL[1:-1,0+2*ll], + 1-msDataOLDALL[1:-1,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[1], + label = str('Day 1')) + else: + ax1.plot(msDataOLDALL[:,0+2*ll], + msDataOLDALL[:,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[1]) + ax2.semilogy(msDataOLDALL[1:-1,0+2*ll], + 1-msDataOLDALL[1:-1,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[1]) + + # Text labels + ax1.set(xlabel='$I_{\mathrm{He}}$ [-]', + ylabel='$y_{\mathrm{He}}$ [-]', + xlim = [0,1], ylim = [0,1]) + # ax1.text(70, 1.3, "(a)", fontsize=8,) + ax1.legend(loc='best', handletextpad=0.25) + ax1.text(0.48, 1.05, "(a)", fontsize=8,) + + ax2.set(xlabel='$I_{\mathrm{He}}$ [-]', + ylabel='$1-y_{\mathrm{He}}$ [-]', + xlim = [0.8,1], ylim = [1e-3,0.1]) + # ax1.text(70, 1.3, "(a)", fontsize=8,) + ax1.legend(loc='best', handletextpad=0.25) + ax2.legend(loc='best', handletextpad=0.25) + ax2.locator_params(axis="x", nbins=4) + ax2.text(0.895, 0.13, "(b)", fontsize=8,) + + markersForPlot_I = ["v","v","v"] + # Loop over all the flowrate files + for kk in range(len(msDataNEW)): + # Path of the file name + fileToLoad = os.path.join(mainDir,msDataNEW[kk]) + + # Get the MIP points + msDataNEWALL = sio.loadmat(fileToLoad)["msData_072621"] + + + # Find indices corresponding to each flowrate + for ll in range(3): + + # Plot MS calibration plot for new repeat + if ll == 1: + ax1.plot(msDataNEWALL[:,0+2*ll], + msDataNEWALL[:,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[0], + label = str('Day 83')) + ax2.semilogy(msDataNEWALL[1:-1,0+2*ll], + 1-msDataNEWALL[1:-1,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[0], + label = str('Day 83')) + else: + ax1.plot(msDataNEWALL[:,0+2*ll], + msDataNEWALL[:,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[0]) + ax2.semilogy(msDataNEWALL[1:-1,0+2*ll], + 1-msDataNEWALL[1:-1,1+2*ll], + linewidth = 0.5, + linestyle =':', + marker = markersForPlot_I[ll], + markersize = 2, + color='#'+colorsForPlot_I[0]) + + # Text labels + ax1.legend(loc='best', handletextpad=0.25) + ax2.legend(loc='best', handletextpad=0.25) + + + + # Save the figure + if saveFlag: + # FileName: figureMSCal___ + saveFileName = "figureMSCal_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + plt.show() \ No newline at end of file From 3663c99fe6a3d6481ff01fc0e596eb01877f3626 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 25 Oct 2021 17:51:39 +0100 Subject: [PATCH 170/189] Merge conflict --- IsothermFittingTool | 2 +- plotFunctions/plotExperimentOutcome.py | 64 ++++++++++- .../plotIsothermComparisonMultiParam.py | 108 +++++++++++++++--- plotFunctions/plotsForArticle_Experiment.py | 26 ++--- 4 files changed, 168 insertions(+), 32 deletions(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 147e08c..9eed157 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 147e08cda9c9c4788d0a130c39c51ca85739e620 +Subproject commit 9eed1577edc8dccf25379d29281cdd829191947a diff --git a/plotFunctions/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py index ea18161..6b1bd4e 100644 --- a/plotFunctions/plotExperimentOutcome.py +++ b/plotFunctions/plotExperimentOutcome.py @@ -54,7 +54,7 @@ saveFileExtension = ".png" # File with parameter estimates -fileParameter = 'zlcParameters_20210822_0926_c8173b1.npz' +fileParameter = 'zlcParameters_20211012_1247_c8173b1.npz' # Flag to plot dead volume results # Dead volume files have a certain name, use that to find what to plot @@ -278,13 +278,69 @@ 'ZLC_ActivatedCarbon_Exp73B_Output.mat', 'ZLC_ActivatedCarbon_Exp75B_Output.mat', 'ZLC_ActivatedCarbon_Exp77B_Output.mat',] - + + # rawFileName = ['ZLC_ActivatedCarbon_Sim01A_Output.mat', + # 'ZLC_ActivatedCarbon_Sim03A_Output.mat', + # 'ZLC_ActivatedCarbon_Sim05A_Output.mat', + # 'ZLC_ActivatedCarbon_Sim01B_Output.mat', + # 'ZLC_ActivatedCarbon_Sim03B_Output.mat', + # 'ZLC_ActivatedCarbon_Sim05B_Output.mat', + # 'ZLC_ActivatedCarbon_Sim02A_Output.mat', + # 'ZLC_ActivatedCarbon_Sim04A_Output.mat', + # 'ZLC_ActivatedCarbon_Sim06A_Output.mat', + # 'ZLC_ActivatedCarbon_Sim02B_Output.mat', + # 'ZLC_ActivatedCarbon_Sim04B_Output.mat', + # 'ZLC_ActivatedCarbon_Sim06B_Output.mat',] + + # rawFileName = ['ZLC_BoronNitride_Exp34A_Output.mat', + # 'ZLC_BoronNitride_Exp36A_Output.mat', + # 'ZLC_BoronNitride_Exp38A_Output.mat', + # 'ZLC_BoronNitride_Exp34B_Output.mat', + # 'ZLC_BoronNitride_Exp36B_Output.mat', + # 'ZLC_BoronNitride_Exp38B_Output.mat', + # 'ZLC_BoronNitride_Exp35A_Output.mat', + # 'ZLC_BoronNitride_Exp37A_Output.mat', + # 'ZLC_BoronNitride_Exp39A_Output.mat', + # 'ZLC_BoronNitride_Exp35B_Output.mat', + # 'ZLC_BoronNitride_Exp37B_Output.mat', + # 'ZLC_BoronNitride_Exp39B_Output.mat',] + + # rawFileName = ['ZLC_BoronNitride_Sim01A_Output.mat', + # 'ZLC_BoronNitride_Sim03A_Output.mat', + # 'ZLC_BoronNitride_Sim05A_Output.mat', + # 'ZLC_BoronNitride_Sim01B_Output.mat', + # 'ZLC_BoronNitride_Sim03B_Output.mat', + # 'ZLC_BoronNitride_Sim05B_Output.mat', + # 'ZLC_BoronNitride_Sim02A_Output.mat', + # 'ZLC_BoronNitride_Sim04A_Output.mat', + # 'ZLC_BoronNitride_Sim06A_Output.mat', + # 'ZLC_BoronNitride_Sim02B_Output.mat', + # 'ZLC_BoronNitride_Sim04B_Output.mat', + # 'ZLC_BoronNitride_Sim06B_Output.mat',] + + # rawFileName = ['ZLC_Zeolite13X_Sim01A_Output.mat', + # 'ZLC_Zeolite13X_Sim03A_Output.mat', + # 'ZLC_Zeolite13X_Sim05A_Output.mat', + # 'ZLC_Zeolite13X_Sim01B_Output.mat', + # 'ZLC_Zeolite13X_Sim03B_Output.mat', + # 'ZLC_Zeolite13X_Sim05B_Output.mat', + # 'ZLC_Zeolite13X_Sim02A_Output.mat', + # 'ZLC_Zeolite13X_Sim04A_Output.mat', + # 'ZLC_Zeolite13X_Sim06A_Output.mat', + # 'ZLC_Zeolite13X_Sim02B_Output.mat', + # 'ZLC_Zeolite13X_Sim04B_Output.mat', + # 'ZLC_Zeolite13X_Sim06B_Output.mat',] + # ZLC parameter model path parameterPath = os.path.join('..','simulationResults',fileParameter) # Temperature (for each experiment) - temperatureExp = [344.69, 325.39, 306.15]*4 - + temperatureExp = [344.69, 325.39, 306.15]*4 # AC Experiments + # temperatureExp = [308.15, 328.15, 348.15]*4 # AC Simulations + + # temperatureExp = [344.6, 325.49, 306.17,]*4 # BN (2 pellets) Experiments + # temperatureExp = [308.15, 328.15, 348.15]*4 # BN (2 pellets) Simulations + # Legend flag useFlow = False diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py index c421441..9875ec0 100644 --- a/plotFunctions/plotIsothermComparisonMultiParam.py +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -85,11 +85,91 @@ # Both k1 and k2 present # Activated Carbon Experiments -zlcFileName = ['zlcParameters_20210822_0926_c8173b1.npz', - 'zlcParameters_20210822_1733_c8173b1.npz', - 'zlcParameters_20210823_0133_c8173b1.npz', - 'zlcParameters_20210823_1007_c8173b1.npz', - 'zlcParameters_20210823_1810_c8173b1.npz'] +# zlcFileName = ['zlcParameters_20210822_0926_c8173b1.npz', +# 'zlcParameters_20210822_1733_c8173b1.npz', +# 'zlcParameters_20210823_0133_c8173b1.npz', +# 'zlcParameters_20210823_1007_c8173b1.npz', +# 'zlcParameters_20210823_1810_c8173b1.npz'] + +# Activated Carbon Experiments - dqbydc = Henry's constant +# zlcFileName = ['zlcParameters_20211002_0057_c8173b1.npz', +# 'zlcParameters_20211002_0609_c8173b1.npz', +# 'zlcParameters_20211002_1119_c8173b1.npz', +# 'zlcParameters_20211002_1638_c8173b1.npz', +# 'zlcParameters_20211002_2156_c8173b1.npz'] + +# Activated Carbon Experiments - Dead volume +# zlcFileName = ['zlcParameters_20211011_1334_c8173b1.npz', +# 'zlcParameters_20211011_2058_c8173b1.npz', +# 'zlcParameters_20211012_0437_c8173b1.npz', +# 'zlcParameters_20211012_1247_c8173b1.npz', + # 'zlcParameters_20211012_2024_c8173b1.npz'] + +# Activated Carbon Simulations (Main) +zlcFileName = ['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'] + +# Activated Carbon Simulations (Effect of porosity) +# 0.90 +# zlcFileName = ['zlcParameters_20210922_2242_c8173b1.npz', +# 'zlcParameters_20210923_0813_c8173b1.npz', +# 'zlcParameters_20210923_1807_c8173b1.npz', +# 'zlcParameters_20210924_0337_c8173b1.npz', +# 'zlcParameters_20210924_1314_c8173b1.npz'] + +# 0.35 +# zlcFileName = ['zlcParameters_20210923_0816_c8173b1.npz', +# 'zlcParameters_20210923_2040_c8173b1.npz', +# 'zlcParameters_20210924_0952_c8173b1.npz', +# 'zlcParameters_20210924_2351_c8173b1.npz', +# 'zlcParameters_20210925_1243_c8173b1.npz'] + +# Activated Carbon Simulations (Effect of mass) +# 1.05 +# zlcFileName = ['zlcParameters_20210925_1104_c8173b1.npz', +# 'zlcParameters_20210925_2332_c8173b1.npz', +# 'zlcParameters_20210926_1132_c8173b1.npz', +# 'zlcParameters_20210926_2248_c8173b1.npz', +# 'zlcParameters_20210927_0938_c8173b1.npz'] + +# 0.95 +# zlcFileName = ['zlcParameters_20210926_2111_c8173b1.npz', +# 'zlcParameters_20210927_0817_c8173b1.npz', +# 'zlcParameters_20210927_1933_c8173b1.npz', +# 'zlcParameters_20210928_0647_c8173b1.npz', +# 'zlcParameters_20210928_1809_c8173b1.npz'] + +# Activated Carbon Simulations (Effect of dead volume) +# TIS + MS +# zlcFileName = ['zlcParameters_20211015_0957_c8173b1.npz', +# 'zlcParameters_20211015_1744_c8173b1.npz', +# 'zlcParameters_20211016_0148_c8173b1.npz', +# 'zlcParameters_20211016_0917_c8173b1.npz', +# 'zlcParameters_20211016_1654_c8173b1.npz'] + +# Boron Nitride Experiments +# zlcFileName = ['zlcParameters_20210823_1731_c8173b1.npz', +# 'zlcParameters_20210824_0034_c8173b1.npz', +# 'zlcParameters_20210824_0805_c8173b1.npz', +# 'zlcParameters_20210824_1522_c8173b1.npz', +# 'zlcParameters_20210824_2238_c8173b1.npz',] + +# Boron Nitride Simulations +# zlcFileName = ['zlcParameters_20210823_1907_03c82f4.npz', +# 'zlcParameters_20210824_0555_03c82f4.npz', +# 'zlcParameters_20210824_2105_03c82f4.npz', +# 'zlcParameters_20210825_0833_03c82f4.npz', +# 'zlcParameters_20210825_2214_03c82f4.npz'] + +# Zeolite 13X Simulations +# zlcFileName = ['zlcParameters_20210824_1102_c8173b1.npz', +# 'zlcParameters_20210825_0243_c8173b1.npz', +# 'zlcParameters_20210825_1758_c8173b1.npz', +# 'zlcParameters_20210826_1022_c8173b1.npz', +# 'zlcParameters_20210827_0104_c8173b1.npz'] # Create the grid for mole fractions y = np.linspace(0,1.,100) @@ -171,16 +251,16 @@ # Rate constant (overall) kineticConstant_ZLC[kk,ii,jj] = rateConstant - # Macropore resistance from QC data - # Compute dqbydc for QC isotherm - equilibriumLoadingUp = computeEquilibriumLoading(temperature=temperature[jj], - moleFrac=moleFractionUp, - isothermModel=x_VOL) # [mol/kg] - dqbydc_True = (equilibriumLoadingUp-isoLoading_VOL[ii,jj])*adsorbentDensity/(delP/(Rg*temperature[jj])) # [-] + # # Macropore resistance from QC data + # # Compute dqbydc for QC isotherm + # equilibriumLoadingUp = computeEquilibriumLoading(temperature=temperature[jj], + # moleFrac=moleFractionUp, + # isothermModel=x_VOL) # [mol/kg] + # dqbydc_True = (equilibriumLoadingUp-isoLoading_VOL[ii,jj])*adsorbentDensity/(delP/(Rg*temperature[jj])) # [-] - # Macropore resistance - kineticConstant_Macro[kk,ii,jj] = (15*particleEpsilon*molDiffusivity - /(tortuosity*(particleRadius)**2)/dqbydc_True) + # # Macropore resistance + # kineticConstant_Macro[kk,ii,jj] = (15*particleEpsilon*molDiffusivity + # /(tortuosity*(particleRadius)**2)/dqbydc_True) # Plot the isotherms fig = plt.figure diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index ebc0ac5..4cfffe8 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -452,7 +452,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax1.text(7, 1.3, "(a)", fontsize=8,) ax1.text(12.7, 0.64, "MS", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax1.text(7.4, 0.39, "$V_\mathrm{D}$ = 0.02 cm$^3$", fontsize=8, + ax1.text(7.4, 0.39, "$V^\mathrm{MS}$ = 0.02 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') elif kk == 1: @@ -466,9 +466,9 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax2.locator_params(axis="x", nbins=5) ax2.legend(handletextpad=0.0,loc='center right') ax2.text(70, 1.3, "(b)", fontsize=8,) - ax2.text(70, 0.64, "Column w/ Ball", fontsize=8, fontweight = 'bold', + ax2.text(80, 0.64, "Setup w/ Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax2.text(78, 0.39, "$V_\mathrm{D}$ = 3.78 cm$^3$", fontsize=8, + ax2.text(78, 0.39, "$V^\mathrm{S}$ = 3.76 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') elif kk == 2: @@ -482,9 +482,9 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax3.locator_params(axis="x", nbins=5) ax3.legend(handletextpad=0.0,loc='center right') ax3.text(70, 1.3, "(c)", fontsize=8,) - ax3.text(64, 0.64, "Column w/o Ball", fontsize=8, fontweight = 'bold', + ax3.text(74, 0.64, "Setup w/o Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax3.text(78, 0.39, "$V_\mathrm{D}$ = 3.95 cm$^3$", fontsize=8, + ax3.text(78, 0.39, "$V^\mathrm{S}$ = 3.93 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') # Remove all the .npz files genereated from the .mat @@ -1696,19 +1696,19 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, # Put other text entries ax1.text(panelLabel_L[pp], 0.67, "(a)", fontsize=8,) ax1.text(materialLabel_L[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#7d8597') - ax1.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 ccm", fontsize=8, fontweight = 'bold',color = '#7d8597') + ax1.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#7d8597') ax2.text(panelLabel_L[pp], 0.67, "(c)", fontsize=8,) ax2.text(materialLabel_L[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#7d8597') - ax2.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 ccm", fontsize=8, fontweight = 'bold',color = '#7d8597') + ax2.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#7d8597') ax3.text(panelLabel_H[pp], 0.67, "(b)", fontsize=8,) ax3.text(materialLabel_H[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#7d8597') - ax3.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 ccm", fontsize=8, fontweight = 'bold',color = '#7d8597') + ax3.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#7d8597') ax4.text(panelLabel_H[pp], 0.67, "(d)", fontsize=8,) ax4.text(materialLabel_H[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#7d8597') - ax4.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 ccm", fontsize=8, fontweight = 'bold',color = '#7d8597') + ax4.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#7d8597') # Save the figure if saveFlag: @@ -1758,8 +1758,8 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, panelLabel_H = [175/2, 60/100*175/2, 200/100*175/2] materialLabel_L = [172, 150/200*172, 600/200*165] materialLabel_H = [172/2, 60/100*172/2, 200/100*165/2] - flowLabel_L = [113, 150/200*113, 600/200*113] - flowLabel_H = [113/2, 60/100*113/2, 200/100*113/2] + flowLabel_L = [75, 150/200*75, 600/200*72] + flowLabel_H = [75/2, 60/100*75/2, 200/100*72/2] materialText = ["AC", "BN", "13X"] panelLabel = ["(a)","(b)","(c)","(d)","(e)","(f)"] repLabel_L = [10, 10*150/200, 10*600/200] @@ -1905,13 +1905,13 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, # # Put other text entries ax1.text(panelLabel_L[pp], 0.67, panelLabel[pp], fontsize=8,) ax1.text(materialLabel_L[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 ccm", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(flowLabel_L[pp], 0.33, "$F^\mathregular{in}$ = 10 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#4895EF') ax1.text(repLabel_L[pp], 0.015, "REP1", fontsize=8, fontweight = 'bold',color = 'k') ax1.text(repLabel_L[pp], 0.011, "REP2", fontsize=8, fontweight = 'bold',color = 'k', alpha = 0.25) ax3.text(panelLabel_H[pp], 0.67, panelLabel[pp+3], fontsize=8,) ax3.text(materialLabel_H[pp], 0.45, materialText[pp], fontsize=8, fontweight = 'bold',color = '#4895EF') - ax3.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 ccm", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax3.text(flowLabel_H[pp], 0.33, "$F^\mathregular{in}$ = 60 cm$^3$ min$^{-1}$", fontsize=8, fontweight = 'bold',color = '#4895EF') ax3.text(repLabel_H[pp], 0.015, "REP1", fontsize=8, fontweight = 'bold',color = 'k') ax3.text(repLabel_H[pp], 0.011, "REP2", fontsize=8, fontweight = 'bold',color = 'k', alpha = 0.25) From 9a6188f9b1d64613a4d5caa63e3455264a69d686 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 27 Oct 2021 16:37:24 +0100 Subject: [PATCH 171/189] Add plots for sensitivity analysis --- plotFunctions/plotsForArticle_Experiment.py | 523 +++++++++++++++++++- plotFunctions/singleColumn.mplstyle | 15 +- 2 files changed, 504 insertions(+), 34 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 4cfffe8..d059fae 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2021-10-27, AK: Add plots for sensitivity analysis # - 2021-10-15, AK: Add plots for SI # - 2021-10-08, AK: Add plots for experimental fits # - 2021-10-06, AK: Add plots for experimental and computational data (iso) @@ -114,6 +115,18 @@ def plotsForArticle_Experiment(**kwargs): if kwargs["figureMSCal"]: plotForArticle_figureMSCal(gitCommitID, currentDT, saveFlag, saveFileExtension) + + # If sensitivity plots + if 'figureSensitivity' in kwargs: + if kwargs["figureSensitivity"]: + plotForArticle_figureSensitivity(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If DV sensitivity plots + if 'figureDVSensitivity' in kwargs: + if kwargs["figureDVSensitivity"]: + plotForArticle_figureDVSensitivity(gitCommitID, currentDT, + saveFlag, saveFileExtension) # fun: plotForArticle_figureMat # Plots the Figure DV of the manuscript: Material characterization (N2/MIP and QC) @@ -454,7 +467,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, backgroundcolor = 'w', color = '#e71d36') ax1.text(7.4, 0.39, "$V^\mathrm{MS}$ = 0.02 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') - + ax1.grid(which='minor', linestyle=':') elif kk == 1: ax2.semilogy(timeElapsedExp,moleFracExp, marker = markersForPlot[ii],linewidth = 0, @@ -470,7 +483,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, backgroundcolor = 'w', color = '#e71d36') ax2.text(78, 0.39, "$V^\mathrm{S}$ = 3.76 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') - + ax2.grid(which='minor', linestyle=':') elif kk == 2: ax3.semilogy(timeElapsedExp,moleFracExp, marker = markersForPlot[ii],linewidth = 0, @@ -486,6 +499,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, backgroundcolor = 'w', color = '#e71d36') ax3.text(78, 0.39, "$V^\mathrm{S}$ = 3.93 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') + ax3.grid(which='minor', linestyle=':') # Remove all the .npz files genereated from the .mat # Loop over all available files @@ -1332,6 +1346,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) ax1.axes.xaxis.set_ticklabels([]) + ax1.grid(which='minor', linestyle=':') if ii in range(3,6): # Plot the experimental data with model output @@ -1348,7 +1363,8 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, ax2.set(xlabel='$t$ [s]',ylabel='$y$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax2.locator_params(axis="x", nbins=4) - + ax2.grid(which='minor', linestyle=':') + if ii in range(6,9): # Plot the experimental data with model output legendStr = str(int(round(temperatureExp[ii],0)))+" K" @@ -1366,7 +1382,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, ax3.axes.xaxis.set_ticklabels([]) ax3.locator_params(axis="x", nbins=4) ax3.axes.xaxis.set_ticklabels([]) - # ax3.axes.yaxis.set_ticklabels([]) + ax3.grid(which='minor', linestyle=':') if ii in range(9,12): # Plot the experimental data with model output @@ -1383,7 +1399,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, ax4.set(xlabel='$t$ [s]', xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax4.locator_params(axis="x", nbins=4) - # ax4.axes.yaxis.set_ticklabels([]) + ax4.grid(which='minor', linestyle=':') # Get the order of temperatures for each plot temperatureOrder = np.argsort(temperatureExp[0:3]) @@ -1628,6 +1644,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) ax1.axes.xaxis.set_ticklabels([]) + ax1.grid(which='minor', linestyle=':') if ii in range(3,6): # Plot the experimental data with model output @@ -1644,6 +1661,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, ax2.set(xlabel='$t$ [s]',ylabel='$y$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax2.locator_params(axis="x", nbins=4) + ax2.grid(which='minor', linestyle=':') if ii in range(6,9): # Plot the experimental data with model output @@ -1662,7 +1680,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, ax3.axes.xaxis.set_ticklabels([]) ax3.locator_params(axis="x", nbins=4) ax3.axes.xaxis.set_ticklabels([]) - # ax3.axes.yaxis.set_ticklabels([]) + ax3.grid(which='minor', linestyle=':') if ii in range(9,12): # Plot the experimental data with model output @@ -1679,7 +1697,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, ax4.set(xlabel='$t$ [s]', xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax4.locator_params(axis="x", nbins=4) - # ax4.axes.yaxis.set_ticklabels([]) + ax4.grid(which='minor', linestyle=':') # Get the order of temperatures for each plot temperatureOrder = np.argsort(temperatureExp[0:3]) @@ -1866,6 +1884,7 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, ax1.set(xlabel='$t$ [s]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) + ax1.grid(which='minor', linestyle=':') if ii in range(6,9): ax2 = plt.subplot(2,3,pp+1) @@ -1873,6 +1892,7 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, marker = markersForPlot[0],linewidth = 0, color='#'+colorTemp, alpha = 0.25, markevery = 4) # Experimental response + ax2.grid(which='minor', linestyle=':') if ii in range(3,6): legendStr = str(int(round(temperatureExp[ii],0)))+" K" @@ -1893,6 +1913,7 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, ax3.set(xlabel='$t$ [s]', xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax3.locator_params(axis="x", nbins=4) + ax3.grid(which='minor', linestyle=':') if ii in range(9,12): # Plot the experimental data with model output @@ -1901,6 +1922,7 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, marker = markersForPlot[0],linewidth = 0, color='#'+colorTemp, alpha = 0.25, markevery = 4) # Experimental response + ax4.grid(which='minor', linestyle=':') # # Put other text entries ax1.text(panelLabel_L[pp], 0.67, panelLabel[pp], fontsize=8,) @@ -2089,8 +2111,8 @@ def plotForArticle_figureZLCObj(gitCommitID, currentDT, plt.show() - # fun: plotForArticle_figureRawTex - # Plots the Figure SX of the manuscript: Raw Textural Characterization (MIP, N2 and XRD) +# fun: plotForArticle_figureRawTex +# Plots the Figure SX of the manuscript: Raw Textural Characterization (MIP, N2 and XRD) def plotForArticle_figureRawTex(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np @@ -2107,7 +2129,7 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, currentDT = auxiliaryFunctions.getCurrentDateTime() # Plot colors and markers (isotherm) - colorsForPlot_I = ["ffba08","d00000","03071e"] + colorsForPlot_I = ["E4572E","76B041","FFC914"] markersForPlot_I = ["^","d","v"] # Folder for material characterization @@ -2153,7 +2175,7 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, ylabel='$V_{\mathrm{Hg}}$ [cm$^{3}$ g$^{-1}$]', xlim = [1e-1,1e5], ylim = [0, 2]) # ax1.text(70, 1.3, "(a)", fontsize=8,) - ax1.legend(loc='upper right', handletextpad=0.0) + ax1.legend(loc='upper right', handletextpad=0.2) ax1.locator_params(axis="y", nbins=4) ax1.text(0.2, 1.82, "(b)", fontsize=8,) @@ -2161,9 +2183,6 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, # Create the instance for the plots ax2 = plt.subplot(1,3,3) - - adsorbentName = [' AC', ' BN', ' 13X'] - # Find indices corresponding to each material for ll in range(3): @@ -2179,7 +2198,7 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, ylabel='$I$ [-]', xlim = [5,60], ylim = [0, 3.5]) # ax2.text(70, 1.3, "(b)", fontsize=8,) - ax2.legend(loc='best', handletextpad=0.0) + ax2.legend(loc='best', handletextpad=0.2) ax2.locator_params(axis="x", nbins=6) ax2.locator_params(axis="y", nbins=1) @@ -2188,10 +2207,7 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, ax2.text(7.5, 3.2, "(c)", fontsize=8,) # Create the instance for the plots - ax3 = plt.subplot(1,3,1) - - adsorbentName = [' AC', ' BN', ' 13X'] - + ax3 = plt.subplot(1,3,1) # Find indices corresponding to each material for ll in range(3): @@ -2205,11 +2221,11 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, label = str(adsorbentName[ll])) # Text labels - ax3.set(xlabel='$P/P_0$', + ax3.set(xlabel='$P/P_0$ [-]', ylabel='$q^*_{\mathrm{N}_2}$ [cm$^{3}$(STP) g$^{-1}$]', xlim = [0,1], ylim = [0, 600]) # ax3.text(70, 1.3, "(c)", fontsize=8,) - ax3.legend(loc='upper right', handletextpad=0.0) + ax3.legend(loc='upper right', handletextpad=0.2) ax3.locator_params(axis="x", nbins=4) ax3.locator_params(axis="y", nbins=4) @@ -2227,8 +2243,8 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, plt.show() - # fun: plotForArticle_figureMSCal - # Plots the Figure SX of the manuscript: Repeats of MS calibration over the course of 83 days +# fun: plotForArticle_figureMSCal +# Plots the Figure SX of the manuscript: Repeats of MS calibration over the course of 83 days def plotForArticle_figureMSCal(gitCommitID, currentDT, saveFlag, saveFileExtension): import matplotlib.pyplot as plt @@ -2244,11 +2260,11 @@ def plotForArticle_figureMSCal(gitCommitID, currentDT, currentDT = auxiliaryFunctions.getCurrentDateTime() # Plot colors and markers (isotherm) - colorsForPlot_I = ["ffba08","03071e"] + colorsForPlot_I = ["797d62","d08c60"] markersForPlot_I = ["^","^","^"] # Folder for MS Calibration - mainDir = os.path.join('..','experimental','msCalibrationData') + mainDir = os.path.join('..','experimental','materialCharacterization') # File with MS data @@ -2373,8 +2389,9 @@ def plotForArticle_figureMSCal(gitCommitID, currentDT, # Text labels ax1.legend(loc='best', handletextpad=0.25) ax2.legend(loc='best', handletextpad=0.25) - - + + ax1.grid(which='minor', linestyle=':') + ax2.grid(which='minor', linestyle=':') # Save the figure if saveFlag: @@ -2386,4 +2403,456 @@ def plotForArticle_figureMSCal(gitCommitID, currentDT, os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) plt.savefig (savePath) + plt.show() + +# fun: plotForArticle_figureZLC +# Plots the Figure ZLC of the manuscript: ZLC parameter estimates +def plotForArticle_figureSensitivity(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.pyplot import figure + from matplotlib.lines import Line2D + import auxiliaryFunctions + from numpy import load + import os + from computeEquilibriumLoading import computeEquilibriumLoading + 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() + + # Plot colors and markers (isotherm) + colorsForPlot = ["0091ad","5c4d7d","b7094c"] + alphaForPlot = [0.5, 1., 0.5] + lineForPlot = [':', '-', '--'] + + # DV alpha and lines + alphaForPlot_DV = [0.5, 0.5, 1.0] + lineForPlot_DV = [':', '--', '-'] + + # Porosity label + porosityLabel = ['$\epsilon_\mathregular{T}$ = 0.35', + '$\epsilon_\mathregular{T}$ = 0.61', + '$\epsilon_\mathregular{T}$ = 0.90'] + + # Mass label + massLabel = ['$m_\mathregular{ads}$ = 59.38 mg', + '$m_\mathregular{ads}$ = 62.50 mg', + '$m_\mathregular{ads}$ = 65.63 mg'] + + # Dead Volume + deadLabel = ['TIS', + 'TIS + D/M', + 'TIS + D/M + MS'] + + # Custom Legend Lines + custom_lines = [Line2D([0], [0], linestyle=lineForPlot[0], lw=1, dash_capstyle = 'round', alpha = alphaForPlot[0], color = 'k'), + Line2D([0], [0], linestyle=lineForPlot[1], lw=1, dash_capstyle = 'round', alpha = alphaForPlot[1], color = 'k'), + Line2D([0], [0], linestyle=lineForPlot[2], lw=1, dash_capstyle = 'round', alpha = alphaForPlot[2], color = 'k')] + + # Custom Legend Lines (DV) + custom_linesDV = [Line2D([0], [0], linestyle=lineForPlot_DV[0], lw=1, dash_capstyle = 'round', alpha = alphaForPlot_DV[0], color = 'k'), + Line2D([0], [0], linestyle=lineForPlot_DV[1], lw=1, dash_capstyle = 'round', alpha = alphaForPlot_DV[1], color = 'k'), + Line2D([0], [0], linestyle=lineForPlot_DV[2], lw=1, dash_capstyle = 'round', alpha = alphaForPlot_DV[2], color = 'k')] + + + + # Define temperature + temperature = [308.15, 328.15, 348.15] + + # Parameter estimate files + # Effect of porosity + + porosityALL = [# Porosity - 0.35 + ['zlcParameters_20210923_0816_c8173b1.npz', + 'zlcParameters_20210923_2040_c8173b1.npz', + 'zlcParameters_20210924_0952_c8173b1.npz', + 'zlcParameters_20210924_2351_c8173b1.npz', + 'zlcParameters_20210925_1243_c8173b1.npz'], + # Activated Carbon Simulation (Base) + ['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'], + # Porosity - 0.90 + ['zlcParameters_20210922_2242_c8173b1.npz', + 'zlcParameters_20210923_0813_c8173b1.npz', + 'zlcParameters_20210923_1807_c8173b1.npz', + 'zlcParameters_20210924_0337_c8173b1.npz', + 'zlcParameters_20210924_1314_c8173b1.npz']] + + # Effect of mass + massALL = [ # Mass - 0.95 + ['zlcParameters_20210926_2111_c8173b1.npz', + 'zlcParameters_20210927_0817_c8173b1.npz', + 'zlcParameters_20210927_1933_c8173b1.npz', + 'zlcParameters_20210928_0647_c8173b1.npz', + 'zlcParameters_20210928_1809_c8173b1.npz'], + # Activated Carbon Simulation (Base) + ['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'], + # Mass - 1.05 + ['zlcParameters_20210925_1104_c8173b1.npz', + 'zlcParameters_20210925_2332_c8173b1.npz', + 'zlcParameters_20210926_1132_c8173b1.npz', + 'zlcParameters_20210926_2248_c8173b1.npz', + 'zlcParameters_20210927_0938_c8173b1.npz']] + + # Effect of DV Model + deadALL = [ # TIS + ['zlcParameters_20211018_1029_c8173b1.npz', + 'zlcParameters_20211018_1648_c8173b1.npz', + 'zlcParameters_20211018_2358_c8173b1.npz', + 'zlcParameters_20211019_0625_c8173b1.npz', + 'zlcParameters_20211019_1303_c8173b1.npz'], + # TIS + D/M + ['zlcParameters_20211026_1152_c8173b1.npz', + 'zlcParameters_20211026_2220_c8173b1.npz',], + # Activated Carbon Simulation (Base) + ['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'],] + + # Create the grid for mole fractions + y = np.linspace(0,1.,100) + fig = figure(figsize=(7,2.65)) + # Effect of porosity + for pp in range(len(porosityALL)): + zlcFileName = porosityALL[pp] + + # Initialize isotherms + isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + objectiveFunction = np.zeros([len(zlcFileName)]) + + # Loop over all available ZLC files for a given porosity + for kk in range(len(zlcFileName)): + # ZLC Data + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + parameterReference = load(parameterPath)["parameterReference"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + + # Parse out the isotherm parameter + isothermModel = x_ZLC[0:-2] + + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, + moleFrac = y[ii], + temperature = temperature[jj]) # [mol/kg] + + # Plot the isotherms + ax1 = plt.subplot(1,3,1) + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + for jj in range(len(temperature)): + ax1.plot(y,isoLoading_ZLC[int(minJ[0]),:,jj],color='#'+colorsForPlot[jj], + linestyle = lineForPlot[pp],alpha = alphaForPlot[pp], + dash_capstyle = 'round',) # Lowest J + if pp == 0: + # Isotherm + ax1.set(xlabel = '$P$ [bar]', + ylabel='$q^*$ [mol kg$^{-1}$]', + xlim = [0,1], ylim = [0, 3]) + ax1.text(0.04, 2.75, "(a)", fontsize=8,) + ax1.text(0.72, 0.15, "Porosity", fontsize=8, fontweight = 'bold',color = 'k') + ax1.locator_params(axis="x", nbins=4) + ax1.locator_params(axis="y", nbins=4) + # ax1.axes.xaxis.set_ticklabels([]) + ax1.legend(custom_lines, porosityLabel) + + # Effect of mass + for pp in range(len(massALL)): + zlcFileName = massALL[pp] + + # Initialize isotherms + isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + objectiveFunction = np.zeros([len(zlcFileName)]) + + # Loop over all available ZLC files for a given mass + for kk in range(len(zlcFileName)): + # ZLC Data + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + parameterReference = load(parameterPath)["parameterReference"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + + # Parse out the isotherm parameter + isothermModel = x_ZLC[0:-2] + + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, + moleFrac = y[ii], + temperature = temperature[jj]) # [mol/kg] + + # Plot the isotherms + ax2 = plt.subplot(1,3,2) + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + for jj in range(len(temperature)): + if pp == 1: + ax2.plot(y,isoLoading_ZLC[int(minJ[0]),:,jj],color='#'+colorsForPlot[jj], + linestyle = lineForPlot[pp],alpha = alphaForPlot[pp], + dash_capstyle = 'round', + label = str(temperature[jj]) + ' K') # Lowest J + else: + ax2.plot(y,isoLoading_ZLC[int(minJ[0]),:,jj],color='#'+colorsForPlot[jj], + linestyle = lineForPlot[pp],alpha = alphaForPlot[pp], + dash_capstyle = 'round',) # Lowest J + if pp == 0: + # Isotherm + ax2.set(xlabel = '$P$ [bar]', + xlim = [0,1], ylim = [0, 3]) + ax2.text(0.04, 2.75, "(b)", fontsize=8,) + ax2.text(0.81, 0.15, "Mass", fontsize=8, fontweight = 'bold',color = 'k') + ax2.locator_params(axis="x", nbins=4) + ax2.locator_params(axis="y", nbins=4) + ax2.legend(custom_lines, massLabel) + + # Effect of dead volume + for pp in range(len(deadALL)): + zlcFileName = deadALL[pp] + + # Initialize isotherms + isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) + objectiveFunction = np.zeros([len(zlcFileName)]) + + # Loop over all available ZLC files for a given DV model + for kk in range(len(zlcFileName)): + # ZLC Data + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + parameterReference = load(parameterPath)["parameterReference"] + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x_ZLC = np.multiply(modelNonDim,parameterReference) + + # Parse out the isotherm parameter + isothermModel = x_ZLC[0:-2] + + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, + moleFrac = y[ii], + temperature = temperature[jj]) # [mol/kg] + + # Plot the isotherms + ax3 = plt.subplot(1,3,3) + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + for jj in range(len(temperature)): + ax3.plot(y,isoLoading_ZLC[int(minJ[0]),:,jj],color='#'+colorsForPlot[jj], + linestyle = lineForPlot_DV[pp],alpha = alphaForPlot_DV[pp], + dash_capstyle = 'round',) # Lowest J + if pp == 0: + # Isotherm + ax3.set(xlabel = '$P$ [bar]', + xlim = [0,1], ylim = [0, 3]) + ax3.text(0.04, 2.75, "(c)", fontsize=8,) + ax3.text(0.58, 0.15, "Dead Volume", fontsize=8, fontweight = 'bold',color = 'k') + ax3.locator_params(axis="x", nbins=4) + ax3.locator_params(axis="y", nbins=4) + ax3.legend(custom_linesDV, deadLabel, loc = 'upper right') + + # Temperature legend + if pp == 1: + fig.legend(bbox_to_anchor=(0.3,0.93,0.4,0.1), mode="expand", ncol=3, borderaxespad=0) + + # Save the figure + if saveFlag: + # FileName: figureSensitivity___ + saveFileName = "figureSensitivity_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + + +# fun: plotForArticle_figureDVSensitivity +# Plots the Figure DV Sensitivity of the manuscript: Dead volume characterization +def plotForArticle_figureDVSensitivity(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + from deadVolumeWrapper import deadVolumeWrapper + from extractDeadVolume import filesToProcess # File processing script + from numpy import load + import os + import matplotlib.pyplot as plt + from matplotlib.pyplot import figure + from matplotlib.lines import Line2D + import auxiliaryFunctions + plt.style.use('singleColumn.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() + + # File with parameter estimates + fileParameterALL = ['deadVolumeCharacteristics_20211002_1307_c8173b1.npz', # TIS + 'deadVolumeCharacteristics_20211026_0025_c8173b1.npz', # TIS + M/D + 'deadVolumeCharacteristics_20210810_1653_eddec53.npz',] # TIS + M/D + MS + + # Flag to plot simulations + simulateModel = True + + # Plot colors and markers + colorsForPlot = ["03045e","0077b6","00b4d8","90e0ef"] + markersForPlot = ["^",">","v","<"] + + # Line style and alpha + alphaForPlot_DV = [0.5, 0.5, 1.0] + lineForPlot_DV = [':', '--', '-'] + + # Dead Volume + deadLabel = ['TIS', + 'TIS + D/M', + 'TIS + D/M + MS'] + + # Custom Legend Lines + custom_lines = [Line2D([0], [0], linestyle=lineForPlot_DV[0], lw=1, dash_capstyle = 'round', alpha = alphaForPlot_DV[0], color = 'k'), + Line2D([0], [0], linestyle=lineForPlot_DV[1], lw=1, dash_capstyle = 'round', alpha = alphaForPlot_DV[1], color = 'k'), + Line2D([0], [0], linestyle=lineForPlot_DV[2], lw=1, dash_capstyle = 'round', alpha = alphaForPlot_DV[2], color = 'k')] + + fig = figure(figsize=(3.3,2.65)) + # Loop over all the files + for kk in range(len(fileParameterALL)): + fileParameter = fileParameterALL[kk] # Parse out the parameter estimate name + # Dead volume parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + # Load file names and the model + fileNameList = load(parameterPath, allow_pickle=True)["fileName"] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),fileNameList,'DV') + # Get the processed file names + fileName = filesToProcess(False,[],[],'DV') + # Load the model + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + x = modelOutputTemp[()]["variable"] + print(x) + + # Get the MS fit flag, flow rates and msDeadVolumeFile (if needed) + # Check needs to be done to see if MS file available or not + # Checked using flagMSDeadVolume in the saved file + dvFileLoadTemp = load(parameterPath) + if 'flagMSDeadVolume' in dvFileLoadTemp.files: + flagMSFit = dvFileLoadTemp["flagMSFit"] + msFlowRate = dvFileLoadTemp["msFlowRate"] + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + else: + flagMSFit = False + msFlowRate = -np.inf + flagMSDeadVolume = False + msDeadVolumeFile = [] + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Print the objective function and volume from model parameters + print("Model Volume",round(sum(x[0:2]),2)) + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # Create the instance for the plots + ax1 = plt.subplot(1,1,1) + + # Initialize error for objective function + # Loop over all available files + for ii in range(len(fileName)): + # Initialize outputs + moleFracSim = [] + # Path of the file name + fileToLoad = fileName[ii] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + # Get the flow rates from the fit file + # When MS used + if flagMSFit: + flowRateDV = msFlowRate + else: + flowRateDV = np.mean(flowRateExp[-1:-10:-1]) + + # Integration and ode evaluation time + timeInt = timeElapsedExp + + if simulateModel: + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + moleFracSim = deadVolumeWrapper(timeInt, flowRateDV, x, flagMSDeadVolume, msDeadVolumeFile) + + # Stack mole fraction from experiments and simulation for error + # computation + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - minExp) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) + + # Plot the expreimental and model output + # Log scale + if kk == 0: + ax1.semilogy(timeElapsedExp,moleFracExp, + marker = markersForPlot[ii],linewidth = 0, + color='#'+colorsForPlot[ii],alpha=0.25,label=str(round(abs(np.mean(flowRateExp)),1))+" cm$^3$ s$^{-1}$") # Experimental response + ax1.semilogy(timeElapsedExp,moleFracSim, + color='#'+colorsForPlot[ii], + linestyle = lineForPlot_DV[kk], + alpha = alphaForPlot_DV[kk], + dash_capstyle = 'round',) # Simulation response + ax1.set(xlabel='$t$ [s]', + ylabel='$y$ [-]', + xlim = [0,150], ylim = [1e-2, 1]) + ax1.locator_params(axis="x", nbins=5) + ax1.legend(handletextpad=0.0) + ax1.grid(which='minor', linestyle=':') + + # Model legend + fig.legend(custom_lines,deadLabel,bbox_to_anchor=(0.07,0.93,0.9,0.1), mode="expand", ncol=3, borderaxespad=0) + + # Remove all the .npz files genereated from the .mat + # Loop over all available files + for ii in range(len(fileName)): + os.remove(fileName[ii]) + + # Save the figure + if saveFlag: + # FileName: figureDVSensitivity__ + saveFileName = "figureDVSensitivity" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + plt.show() \ No newline at end of file diff --git a/plotFunctions/singleColumn.mplstyle b/plotFunctions/singleColumn.mplstyle index ea278cf..104b685 100755 --- a/plotFunctions/singleColumn.mplstyle +++ b/plotFunctions/singleColumn.mplstyle @@ -7,18 +7,19 @@ figure.dpi : 600 # dpi figure.autolayout : true # for labels not being cut out ## Axes -axes.titlesize : 10 -axes.labelsize : 10 +axes.titlesize : 8 +axes.labelsize : 8 axes.formatter.limits : -5, 4 ## Grid axes.grid : true -grid.color : cccccc -grid.linewidth : 0.5 +grid.color : e0e0e0 +grid.linewidth : 0.25 +axes.grid.which : both ## Lines & Scatter -lines.linewidth : 1.5 -lines.markersize : 4 +lines.linewidth : 1 +lines.markersize : 2 scatter.marker: o ## Ticks @@ -34,7 +35,7 @@ font.size : 10 ## Legends legend.frameon : true -legend.fontsize : 10 +legend.fontsize : 8 legend.edgecolor : 1 legend.framealpha : 0.6 From 73804e254955223e283eaee4930468fe4dd6f43e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Wed, 27 Oct 2021 17:24:13 +0100 Subject: [PATCH 172/189] Cosmetic changes --- plotFunctions/plotsForArticle_Experiment.py | 34 ++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index d059fae..6e226be 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -465,7 +465,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax1.text(7, 1.3, "(a)", fontsize=8,) ax1.text(12.7, 0.64, "MS", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax1.text(7.4, 0.39, "$V^\mathrm{MS}$ = 0.02 cm$^3$", fontsize=8, + ax1.text(7.2, 0.39, "$V^\mathrm{MS}$ = 0.02 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax1.grid(which='minor', linestyle=':') elif kk == 1: @@ -1154,6 +1154,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np import matplotlib.pyplot as plt + from matplotlib.pyplot import figure import auxiliaryFunctions from numpy import load import os @@ -1181,8 +1182,8 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, panelLabel_H = [185/2, 60/100*185/2, 200/100*185/2] materialLabel_L = [182, 150/200*182, 600/200*180] materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] - flowLabel_L = [123, 150/200*123, 600/200*123] - flowLabel_H = [123/2, 60/100*123/2, 200/100*123/2] + flowLabel_L = [118, 150/200*118, 600/200*118] + flowLabel_H = [118/2, 60/100*118/2, 200/100*118/2] materialText = ["AC", "BN", "13X"] # Parameter estimate files @@ -1206,6 +1207,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, 'zlcParameters_20210827_0124_6b88505.npz',]] for pp in range(len(zlcFileNameALL)): + fig = figure(figsize=(6.5,5)) zlcFileName = zlcFileNameALL[pp] objectiveFunction = np.zeros([len(zlcFileName)]) # Loop over all available ZLC files for a given material @@ -1453,6 +1455,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np import matplotlib.pyplot as plt + from matplotlib.pyplot import figure import auxiliaryFunctions from numpy import load import os @@ -1480,8 +1483,8 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, panelLabel_H = [185/2, 60/100*185/2, 200/100*185/2] materialLabel_L = [182, 150/200*182, 600/200*180] materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] - flowLabel_L = [123, 150/200*123, 600/200*123] - flowLabel_H = [123/2, 60/100*123/2, 200/100*123/2] + flowLabel_L = [118, 150/200*118, 600/200*118] + flowLabel_H = [118/2, 60/100*118/2, 200/100*118/2] materialText = ["AC", "BN", "13X"] # Parameter estimate files @@ -1503,8 +1506,9 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, 'zlcParameters_20210825_1758_c8173b1.npz', 'zlcParameters_20210826_1022_c8173b1.npz', 'zlcParameters_20210827_0104_c8173b1.npz']] - - for pp in range(len(zlcFileNameALL)): + + for pp in range(len(zlcFileNameALL)): + fig = figure(figsize=(6.5,5)) zlcFileName = zlcFileNameALL[pp] objectiveFunction = np.zeros([len(zlcFileName)]) # Loop over all available ZLC files for a given material @@ -1627,7 +1631,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, elif 340 Date: Mon, 1 Nov 2021 19:36:06 +0000 Subject: [PATCH 173/189] Add files for final estimation runs --- plotFunctions/plotsForArticle_Experiment.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 6e226be..3e91f78 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -2521,7 +2521,10 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, 'zlcParameters_20211019_1303_c8173b1.npz'], # TIS + D/M ['zlcParameters_20211026_1152_c8173b1.npz', - 'zlcParameters_20211026_2220_c8173b1.npz',], + 'zlcParameters_20211026_2220_c8173b1.npz', + 'zlcParameters_20211027_0918_c8173b1.npz', + 'zlcParameters_20211027_2016_c8173b1.npz', + 'zlcParameters_20211028_0645_c8173b1.npz'], # Activated Carbon Simulation (Base) ['zlcParameters_20210823_1104_03c82f4.npz', 'zlcParameters_20210824_0000_03c82f4.npz', @@ -2661,6 +2664,7 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, # Plot the isotherms ax3 = plt.subplot(1,3,3) minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + for jj in range(len(temperature)): ax3.plot(y,isoLoading_ZLC[int(minJ[0]),:,jj],color='#'+colorsForPlot[jj], linestyle = lineForPlot_DV[pp],alpha = alphaForPlot_DV[pp], From fe3614d6718314e79c7194d425683d5b30aee16e Mon Sep 17 00:00:00 2001 From: m86972ar Date: Tue, 16 Nov 2021 19:37:48 +0000 Subject: [PATCH 174/189] Add Ft plot and make cosmetic changes --- plotFunctions/plotsForArticle_Experiment.py | 302 +++++++++++++++++--- 1 file changed, 263 insertions(+), 39 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 3e91f78..eca17bf 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -127,7 +127,13 @@ def plotsForArticle_Experiment(**kwargs): if kwargs["figureDVSensitivity"]: plotForArticle_figureDVSensitivity(gitCommitID, currentDT, saveFlag, saveFileExtension) - + + # If Ft plots + if 'figureFt' in kwargs: + if kwargs["figureFt"]: + plotForArticle_figureFt(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # fun: plotForArticle_figureMat # Plots the Figure DV of the manuscript: Material characterization (N2/MIP and QC) def plotForArticle_figureMat(gitCommitID, currentDT, @@ -137,6 +143,8 @@ def plotForArticle_figureMat(gitCommitID, currentDT, import auxiliaryFunctions import scipy.io as sio import os + from matplotlib.ticker import FormatStrFormatter + plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -241,28 +249,31 @@ def plotForArticle_figureMat(gitCommitID, currentDT, ylabel='$V_\mathregular{pore}$ [cm$^{3}$ g$^{-1}$]', xlim = [0.1,1e6], ylim = [0, 2]) ax.text(0.2, 1.82, "(a)", fontsize=8,) - ax.text(1.4e5, 0.1, "AC", fontsize=8, fontweight = 'bold',color = '#e71d36') + # ax.text(1.4e5, 0.1, "AC", fontsize=8, fontweight = 'bold',color = '#e71d36') ax.text(1.6e3, combinedPorosityData[-2,2]+0.07, str(round(combinedPorosityData[-2,2],2))+' cm$^{3}$ g$^{-1}$', fontsize=8,color = '#7d8597') + ax.text(2.5e2, 2.15, "AC", fontsize=8, fontweight = 'bold',color = 'k') elif kk == 1: ax.set(xlabel='$D$ [nm]', xlim = [0.1,1e6], ylim = [0, 2]) ax.text(0.2, 1.82, "(b)", fontsize=8,) - ax.text(1.4e5, 0.1, "BN", fontsize=8, fontweight = 'bold',color = '#e71d36') + # ax.text(1.4e5, 0.1, "BN", fontsize=8, fontweight = 'bold',color = '#e71d36') ax.text(5, combinedPorosityData[-2,2]-0.15, str(round(combinedPorosityData[-2,2],2))+' cm$^{3}$ g$^{-1}$', fontsize=8,color = '#7d8597') + ax.text(2.5e2, 2.15, "BN", fontsize=8, fontweight = 'bold',color = 'k') elif kk == 2: ax.set(xlabel='$D$ [nm]', xlim = [0.1,1e6], ylim = [0, 2]) ax.text(0.2, 1.82, "(c)", fontsize=8,) - ax.text(1e5, 0.1, "13X", fontsize=8, fontweight = 'bold',color = '#e71d36') + # ax.text(1e5, 0.1, "13X", fontsize=8, fontweight = 'bold',color = '#e71d36') ax.text(1.6e3, combinedPorosityData[-2,2]+0.07, str(round(combinedPorosityData[-2,2],2))+' cm$^{3}$ g$^{-1}$', fontsize=8,color = '#7d8597') - + ax.text(2.5e2, 2.15, "13X", fontsize=8, fontweight = 'bold',color = 'k') ax.locator_params(axis="y", nbins=5) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) # Loop over all the isotherm files for kk in range(len(isothermALL)): @@ -300,25 +311,26 @@ def plotForArticle_figureMat(gitCommitID, currentDT, # Material specific text labels if kk == 0: ax.set(xlabel='$P$ [bar]', - ylabel='$q^*$ [mol kg$^{-1}$]', + ylabel='$q^*_\mathregular{CO_2}$ [mol kg$^{-1}$]', xlim = [0,1], ylim = [0, 3]) ax.text(0.89, 2.75, "(d)", fontsize=8,) - ax.text(0.87, 0.13, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax.text(0.87, 0.13, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') elif kk == 1: ax.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 2]) ax.text(0.89, 1.82, "(e)", fontsize=8,) - ax.text(0.87, 0.09, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax.text(0.87, 0.09, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') elif kk == 2: ax.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 8]) ax.text(0.89, 7.25, "(f)", fontsize=8,) - ax.text(0.85, 0.35, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax.text(0.85, 0.35, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') ax.locator_params(axis="x", nbins=4) ax.locator_params(axis="y", nbins=4) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) # Save the figure if saveFlag: @@ -458,14 +470,14 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax1.semilogy(timeElapsedExp,moleFracSim, color='#'+colorsForPlot[ii]) # Simulation response ax1.set(xlabel='$t$ [s]', - ylabel='$y$ [-]', + ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = [0,15], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=5) ax1.legend(handletextpad=0.0,loc='center right') ax1.text(7, 1.3, "(a)", fontsize=8,) ax1.text(12.7, 0.64, "MS", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax1.text(7.2, 0.39, "$V^\mathrm{MS}$ = 0.02 cm$^3$", fontsize=8, + ax1.text(7.2, 0.385, "$V^\mathrm{MS}$ = 0.02 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax1.grid(which='minor', linestyle=':') elif kk == 1: @@ -481,7 +493,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax2.text(70, 1.3, "(b)", fontsize=8,) ax2.text(80, 0.64, "Setup w/ Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax2.text(78, 0.39, "$V^\mathrm{S}$ = 3.76 cm$^3$", fontsize=8, + ax2.text(78, 0.385, "$V^\mathrm{S}$ = 3.76 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax2.grid(which='minor', linestyle=':') elif kk == 2: @@ -497,7 +509,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax3.text(70, 1.3, "(c)", fontsize=8,) ax3.text(74, 0.64, "Setup w/o Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax3.text(78, 0.39, "$V^\mathrm{S}$ = 3.93 cm$^3$", fontsize=8, + ax3.text(78, 0.385, "$V^\mathrm{S}$ = 3.93 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax3.grid(which='minor', linestyle=':') @@ -528,6 +540,7 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, from numpy import load import os from computeEquilibriumLoading import computeEquilibriumLoading + from matplotlib.ticker import FormatStrFormatter plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -660,10 +673,10 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, if pp == 0: # Isotherm - ax1.set(ylabel='$q^*$ [mol kg$^{-1}$]', + ax1.set(ylabel='$q^*_\mathregular{CO_2}$ [mol kg$^{-1}$]', xlim = [0,1], ylim = [0, 3]) ax1.text(0.04, 2.75, "(a)", fontsize=8,) - ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = 'k') ax1.text(0.84, 0.30, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF') ax1.text(0.84, 0.12, "REP", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) @@ -672,7 +685,7 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, ax1.legend() # Kinetics ax2.set(xlabel='$P$ [bar]', - ylabel='$k$ [s$^{-1}$]', + ylabel='$k\mathregular{_{CO_2}}$ [s$^{-1}$]', xlim = [0,1], ylim = [0, 1]) ax2.text(0.04, 0.9, "(d)", fontsize=8,) # ax2.text(0.87, 0.9, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') @@ -683,7 +696,7 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, # Isotherm ax1.set(xlim = [0,1], ylim = [0, 1.5]) ax1.text(0.04, 1.35, "(b)", fontsize=8,) - ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = 'k') ax1.text(0.84, 0.15, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF') ax1.text(0.84, 0.06, "REP", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) @@ -702,7 +715,7 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, # Isotherm ax1.set(xlim = [0,1], ylim = [0, 8]) ax1.text(0.04, 7.3, "(c)", fontsize=8,) - ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = 'k') ax1.text(0.84, 0.86, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF') ax1.text(0.84, 0.32, "REP", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) @@ -717,7 +730,8 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, # ax2.text(0.53, 1.66, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') ax2.locator_params(axis="x", nbins=4) ax2.locator_params(axis="y", nbins=4) - + ax1.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) # Save the figure if saveFlag: # FileName: figureZLC___ @@ -741,6 +755,7 @@ def plotForArticle_figureComp(gitCommitID, currentDT, import scipy.io as sio import os from computeEquilibriumLoading import computeEquilibriumLoading + from matplotlib.ticker import FormatStrFormatter plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -849,10 +864,10 @@ def plotForArticle_figureComp(gitCommitID, currentDT, if pp == 0: # Isotherm ax1.set(xlabel='$P$ [bar]', - ylabel='$q^*$ [mol kg$^{-1}$]', + ylabel='$q^*_\mathregular{CO_2}$ [mol kg$^{-1}$]', xlim = [0,1], ylim = [0, 3]) ax1.text(0.04, 2.75, "(a)", fontsize=8,) - ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = 'k') ax1.text(0.84, 0.32, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') ax1.text(0.84, 0.12, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) @@ -862,7 +877,7 @@ def plotForArticle_figureComp(gitCommitID, currentDT, # Isotherm ax1.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 1.5]) ax1.text(0.04, 1.35, "(b)", fontsize=8,) - ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = 'k') ax1.text(0.84, 0.16, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') ax1.text(0.84, 0.06, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) @@ -872,13 +887,13 @@ def plotForArticle_figureComp(gitCommitID, currentDT, # Isotherm ax1.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 8]) ax1.text(0.04, 7.3, "(c)", fontsize=8,) - ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') + ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = 'k') ax1.text(0.84, 0.86, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') ax1.text(0.84, 0.32, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.legend(loc='upper right') - + ax1.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) # Save the figure if saveFlag: # FileName: figureComp___ @@ -902,6 +917,7 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, import scipy.io as sio import os from computeEquilibriumLoading import computeEquilibriumLoading + from matplotlib.ticker import FormatStrFormatter plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -1078,7 +1094,7 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, if pp == 0: # Isotherm - ax1.set(ylabel='$q^*$ [mol kg$^{-1}$]', + ax1.set(ylabel='$q^*_\mathregular{CO_2}$ [mol kg$^{-1}$]', xlim = [0,1], ylim = [0, 3]) ax1.text(0.04, 2.75, "(a)", fontsize=8,) ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = 'k') @@ -1090,7 +1106,7 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, ax1.legend() # Kinetics ax2.set(xlabel='$P$ [bar]', - ylabel='$k$ [s$^{-1}$]', + ylabel='$k\mathregular{_{CO_2}}$ [s$^{-1}$]', xlim = [0,1], ylim = [0, 1]) ax2.text(0.04, 0.9, "(d)", fontsize=8,) # ax2.text(0.87, 0.9, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') @@ -1135,7 +1151,8 @@ def plotForArticle_figureZLCSim(gitCommitID, currentDT, # ax2.text(0.53, 1.66, "Experimental", fontsize=8, fontweight = 'bold',color = '#4895EF') ax2.locator_params(axis="x", nbins=4) ax2.locator_params(axis="y", nbins=4) - + ax1.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) # Save the figure if saveFlag: # FileName: figureZLCSim___ @@ -1344,7 +1361,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, if ii%3 == 0: ax1.semilogy(timeElapsedExp,moleFracDV, color='#76c893',alpha = 0.2, linestyle='-') # Dead volume response - ax1.set(ylabel='$y$ [-]', + ax1.set(ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) ax1.axes.xaxis.set_ticklabels([]) @@ -1362,7 +1379,7 @@ def plotForArticle_figureZLCFit(gitCommitID, currentDT, if ii%3 == 0: ax2.semilogy(timeElapsedExp,moleFracDV, color='#76c893',alpha = 0.2, linestyle='-') # Dead volume response - ax2.set(xlabel='$t$ [s]',ylabel='$y$ [-]', + ax2.set(xlabel='$t$ [s]',ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax2.locator_params(axis="x", nbins=4) ax2.grid(which='minor', linestyle=':') @@ -1644,7 +1661,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, if ii%3 == 0: ax1.semilogy(timeElapsedExp,moleFracDV, color='k',alpha = 0.2, linestyle='-') # Dead volume response - ax1.set(ylabel='$y$ [-]', + ax1.set(ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) ax1.axes.xaxis.set_ticklabels([]) @@ -1662,7 +1679,7 @@ def plotForArticle_figureZLCSimFit(gitCommitID, currentDT, if ii%3 == 0: ax2.semilogy(timeElapsedExp,moleFracDV, color='k',alpha = 0.2, linestyle='-') # Dead volume response - ax2.set(xlabel='$t$ [s]',ylabel='$y$ [-]', + ax2.set(xlabel='$t$ [s]',ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax2.locator_params(axis="x", nbins=4) ax2.grid(which='minor', linestyle=':') @@ -1881,7 +1898,7 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, label = legendStr) # Experimental response ax1.legend(loc = "center right", handletextpad=0.0) if pp == 0: - ax1.set(xlabel='$t$ [s]',ylabel='$y$ [-]', + ax1.set(xlabel='$t$ [s]',ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = XLIM_L[pp], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=4) else: @@ -1910,7 +1927,7 @@ def plotForArticle_figureZLCRep(gitCommitID, currentDT, label = legendStr) # Experimental response ax3.legend(loc = "center right", handletextpad=0.0) if pp == 0: - ax3.set(xlabel='$t$ [s]',ylabel='$y$ [-]', + ax3.set(xlabel='$t$ [s]',ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = XLIM_H[pp], ylim = [1e-2, 1]) ax3.locator_params(axis="x", nbins=4) else: @@ -2423,6 +2440,7 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, from numpy import load import os from computeEquilibriumLoading import computeEquilibriumLoading + from matplotlib.ticker import FormatStrFormatter plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -2573,10 +2591,10 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, if pp == 0: # Isotherm ax1.set(xlabel = '$P$ [bar]', - ylabel='$q^*$ [mol kg$^{-1}$]', + ylabel='$q^*_\mathregular{CO_2}$ [mol kg$^{-1}$]', xlim = [0,1], ylim = [0, 3]) ax1.text(0.04, 2.75, "(a)", fontsize=8,) - ax1.text(0.72, 0.15, "Porosity", fontsize=8, fontweight = 'bold',color = 'k') + ax1.text(0.70, 0.15, "Porosity", fontsize=8, fontweight = 'bold',color = 'k') ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) # ax1.axes.xaxis.set_ticklabels([]) @@ -2628,7 +2646,7 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, ax2.set(xlabel = '$P$ [bar]', xlim = [0,1], ylim = [0, 3]) ax2.text(0.04, 2.75, "(b)", fontsize=8,) - ax2.text(0.81, 0.15, "Mass", fontsize=8, fontweight = 'bold',color = 'k') + ax2.text(0.79, 0.15, "Mass", fontsize=8, fontweight = 'bold',color = 'k') ax2.locator_params(axis="x", nbins=4) ax2.locator_params(axis="y", nbins=4) ax2.legend(custom_lines, massLabel) @@ -2674,7 +2692,7 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, ax3.set(xlabel = '$P$ [bar]', xlim = [0,1], ylim = [0, 3]) ax3.text(0.04, 2.75, "(c)", fontsize=8,) - ax3.text(0.58, 0.15, "Dead Volume", fontsize=8, fontweight = 'bold',color = 'k') + ax3.text(0.56, 0.15, "Dead Volume", fontsize=8, fontweight = 'bold',color = 'k') ax3.locator_params(axis="x", nbins=4) ax3.locator_params(axis="y", nbins=4) ax3.legend(custom_linesDV, deadLabel, loc = 'upper right') @@ -2683,6 +2701,10 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, if pp == 1: fig.legend(bbox_to_anchor=(0.3,0.93,0.4,0.1), mode="expand", ncol=3, borderaxespad=0) + ax1.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + ax3.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + # Save the figure if saveFlag: # FileName: figureSensitivity___ @@ -2841,7 +2863,7 @@ def plotForArticle_figureDVSensitivity(gitCommitID, currentDT, alpha = alphaForPlot_DV[kk], dash_capstyle = 'round',) # Simulation response ax1.set(xlabel='$t$ [s]', - ylabel='$y$ [-]', + ylabel='$y\mathregular{_{CO_2}}$ [-]', xlim = [0,150], ylim = [1e-2, 1]) ax1.locator_params(axis="x", nbins=5) ax1.legend(handletextpad=0.0) @@ -2865,4 +2887,206 @@ def plotForArticle_figureDVSensitivity(gitCommitID, currentDT, os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) plt.savefig (savePath, bbox_inches = "tight") - plt.show() \ No newline at end of file + plt.show() + + +# fun: plotForArticle_figureFt +# Plots the Figure Ftof the manuscript: Ft plots for parameter estimates +def plotForArticle_figureFt(gitCommitID, currentDT, + saveFlag, saveFileExtension): + from simulateCombinedModel import simulateCombinedModel + import numpy as np + import os + import matplotlib.pyplot as plt + from matplotlib.pyplot import figure + import scipy.io as sio + from numpy import load + from matplotlib.lines import Line2D + + os.chdir(".."+os.path.sep+"plotFunctions") + plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file + os.chdir(".."+os.path.sep+"experimental") + + # Move to top level folder (to avoid path issues) + os.chdir("..") + import auxiliaryFunctions + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + os.chdir("experimental") + + # Get the current date and time for saving purposes + currentDT = auxiliaryFunctions.getCurrentDateTime() + + # Temperature of the simulate experiment [K] + temperature = 308.15 + + # Inlet flow rate [ccm] + flowRate = [10, 60] + + # Saturation mole fraction (works for a binary system) + initMoleFrac = np.array(([0.11, 0.94], [0.11, 0.94])) + + # Parameter estimate files + # Activated Carbon Experiments + zlcFileNameALL = [['zlcParameters_20210822_0926_c8173b1.npz', + 'zlcParameters_20210822_1733_c8173b1.npz', + # 'zlcParameters_20210823_0133_c8173b1.npz', # DSL BAD (but lowest J) + # 'zlcParameters_20210823_1007_c8173b1.npz', # DSL BAD (but lowest J) + 'zlcParameters_20210823_1810_c8173b1.npz'], + # Boron Nitride Experiments + ['zlcParameters_20210823_1731_c8173b1.npz', + 'zlcParameters_20210824_0034_c8173b1.npz', + 'zlcParameters_20210824_0805_c8173b1.npz', + 'zlcParameters_20210824_1522_c8173b1.npz', + 'zlcParameters_20210824_2238_c8173b1.npz',], + # Zeolite 13X Experiments + ['zlcParameters_20210824_1552_6b88505.npz', + 'zlcParameters_20210825_0559_6b88505.npz', + 'zlcParameters_20210825_1854_6b88505.npz', + 'zlcParameters_20210826_0847_6b88505.npz', + 'zlcParameters_20210827_0124_6b88505.npz',]] + + # Create the instance for the plots + fig = plt.figure + ax1 = plt.subplot(1,3,1) + ax2 = plt.subplot(1,3,2) + ax3 = plt.subplot(1,3,3) + + # Plot colors + colorsForPlot = ["#ef233c","#8d99ae"]*2 + styleForPlot = [":","-"]*2 + alphaForPlot = [0.4,1.0]*2 + + # Flow labels + flowStr = [str(int(flowRate[0]))+ " cm$^3$ min$^{-1}$", + str(int(flowRate[1]))+ " cm$^3$ min$^{-1}$"] + + # Legend labels + legendStr = ["$y^\mathregular{in}$ = " + str(initMoleFrac[0,0]), + "$y^\mathregular{in}$ = " + str(initMoleFrac[0,1])] + + # Custom Legend Lines + custom_lines = [Line2D([0], [0], linestyle=':', lw=1, dash_capstyle = 'round', color = 'k'), + Line2D([0], [0], linestyle='-', lw=1, dash_capstyle = 'round', color = 'k')] + + + # Loop over all materials + for pp in range(len(zlcFileNameALL)): + fig = figure(figsize=(6.5,5)) + zlcFileName = zlcFileNameALL[pp] + objectiveFunction = np.zeros([len(zlcFileName)]) + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # Obtain the onjective function values + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + + # Find the experiment with the min objective function + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + fileParameter = zlcFileName[int(minJ[0])] + + # ZLC parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + + # Parse out experiments names and temperature used for the fitting + rawFileName = load(parameterPath)["fileName"] + temperatureExp = load(parameterPath)["temperature"] + + # Parse out all the necessary quantities to obtain model fit + # Mass of sorbent and particle epsilon + adsorbentDensity = load(parameterPath)["adsorbentDensity"] + particleEpsilon = load(parameterPath)["particleEpsilon"] + massSorbent = load(parameterPath)["massSorbent"] + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + # Dead volume model + deadVolumeFile = str(load(parameterPath)["deadVolumeFile"]) + # Isotherm parameter reference + parameterReference = load(parameterPath)["parameterReference"] + # Load the model + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x = np.multiply(modelNonDim,parameterReference) + # Integration time (set to 1000 s, default) + timeInt = (0.0,1000.0) + + # Loop over all the conditions + for ii in range(len(flowRate)): + for jj in range(np.size(initMoleFrac,1)): + # Initialize the output dictionary + experimentOutput = {} + # Compute the composite response using the optimizer parameters + timeElapsedSim , _ , resultMat = simulateCombinedModel(isothermModel = x[0:-2], + rateConstant_1 = x[-2], # Last but one element is rate constant (analogous to micropore) + rateConstant_2 = x[-1], # Last element is activation energy (analogous to macropore) + temperature = temperature, # Temperature [K] + timeInt = timeInt, + initMoleFrac = [initMoleFrac[ii,jj]], # Initial mole fraction assumed to be the first experimental point + flowIn = flowRate[ii]*1e-6/60, # Flow rate [m3/s] for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = False, + deadVolumeFile = str(deadVolumeFile), + volSorbent = volSorbent, + volGas = volGas, + adsorbentDensity = adsorbentDensity) + + # Find the index that corresponds to 1e-2 (to be consistent with the + # experiments) + lastIndThreshold = int(np.argwhere(resultMat[0,:]<=1e-2)[0]) + # Cut the time, mole fraction and the flow rate to the last index + # threshold + timeExp = timeElapsedSim[0:lastIndThreshold] # Time elapsed [s] + moleFrac = resultMat[0,0:lastIndThreshold] # Mole fraction [-] + totalFlowRate = resultMat[3,0:lastIndThreshold]*1e6 # Total flow rate[ccs] + + # Acticated Carbon + if pp == 0: + # Ft - Log scale + ax1.semilogy(np.multiply(totalFlowRate,timeExp),moleFrac, + color=colorsForPlot[ii],linestyle=styleForPlot[jj], + dash_capstyle = 'round',alpha=alphaForPlot[jj]) + ax1.set(xlabel='$Ft$ [cm$^3$]', ylabel='$y\mathregular{_{CO_2}}$ [-]', + xlim = [0,60], ylim = [1e-2, 1]) + ax1.locator_params(axis="x", nbins=4) + ax1.legend(custom_lines, legendStr, loc = 'upper right') + ax1.text(2, 0.67, "(a)", fontsize=8,) + if ii == 0: + ax1.text(5, 0.2, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) + if ii == 1: + ax1.text(30, 0.03, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) + ax1.text(28, 1.3, "AC", fontsize=8, fontweight = 'bold',color = 'k') + # Boron Nitride + if pp == 1: + # Ft - Log scale + ax2.semilogy(np.multiply(totalFlowRate,timeExp),moleFrac, + color=colorsForPlot[ii],linestyle=styleForPlot[jj], + dash_capstyle = 'round',alpha=alphaForPlot[jj]) + ax2.set(xlabel='$Ft$ [cm$^3$]', + xlim = [0,60], ylim = [1e-2, 1]) + ax2.locator_params(axis="x", nbins=4) + ax2.legend(custom_lines, legendStr, loc = 'upper right') + ax2.text(2, 0.67, "(b)", fontsize=8,) + if ii == 0: + ax2.text(5, 0.2, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) + if ii == 1: + ax2.text(22, 0.03, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) + ax2.text(28, 1.3, "BN", fontsize=8, fontweight = 'bold',color = 'k') + # Zeolite 13X + if pp == 2: + # Ft - Log scale + ax3.semilogy(np.multiply(totalFlowRate,timeExp),moleFrac, + color=colorsForPlot[ii],linestyle=styleForPlot[jj], + dash_capstyle = 'round',alpha=alphaForPlot[jj]) + ax3.set(xlabel='$Ft$ [cm$^3$]', + xlim = [0,150], ylim = [1e-2, 1]) + ax3.locator_params(axis="x", nbins=4) + ax3.legend(custom_lines, legendStr, loc = 'upper right') + ax3.text(2*150/60, 0.67, "(c)", fontsize=8,) + if ii == 0: + ax3.text(10, 0.2, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) + if ii == 1: + ax3.text(65, 0.03, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) + ax3.text(67, 1.3, "13X", fontsize=8, fontweight = 'bold',color = 'k') From 6cf05272cd730e5a4e58d871038007eb67cd211f Mon Sep 17 00:00:00 2001 From: m86972ar Date: Tue, 16 Nov 2021 19:41:36 +0000 Subject: [PATCH 175/189] Save Ft plot figure --- plotFunctions/plotsForArticle_Experiment.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index eca17bf..3790f08 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2021-11-16, AK: Add Ft plot and minor fixes # - 2021-10-27, AK: Add plots for sensitivity analysis # - 2021-10-15, AK: Add plots for SI # - 2021-10-08, AK: Add plots for experimental fits @@ -3090,3 +3091,15 @@ def plotForArticle_figureFt(gitCommitID, currentDT, if ii == 1: ax3.text(65, 0.03, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) ax3.text(67, 1.3, "13X", fontsize=8, fontweight = 'bold',color = 'k') + + # Save the figure + if saveFlag: + # FileName: figureFt___ + saveFileName = "figureFt" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath, bbox_inches = "tight") + + plt.show() From f1367dae7220aa2368aa9cca677de1998fef0d78 Mon Sep 17 00:00:00 2001 From: m86972ar Date: Tue, 16 Nov 2021 20:00:31 +0000 Subject: [PATCH 176/189] More plot bug fixes --- plotFunctions/plotsForArticle_Experiment.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 3790f08..cb77cee 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -2881,7 +2881,7 @@ def plotForArticle_figureDVSensitivity(gitCommitID, currentDT, # Save the figure if saveFlag: # FileName: figureDVSensitivity__ - saveFileName = "figureDVSensitivity" + currentDT + "_" + gitCommitID + saveFileExtension + saveFileName = "figureDVSensitivity_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): @@ -2902,8 +2902,7 @@ def plotForArticle_figureFt(gitCommitID, currentDT, from matplotlib.pyplot import figure import scipy.io as sio from numpy import load - from matplotlib.lines import Line2D - + from matplotlib.lines import Line2D os.chdir(".."+os.path.sep+"plotFunctions") plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file os.chdir(".."+os.path.sep+"experimental") @@ -2956,7 +2955,7 @@ def plotForArticle_figureFt(gitCommitID, currentDT, # Plot colors colorsForPlot = ["#ef233c","#8d99ae"]*2 styleForPlot = [":","-"]*2 - alphaForPlot = [0.4,1.0]*2 + alphaForPlot = [0.5,1.0]*2 # Flow labels flowStr = [str(int(flowRate[0]))+ " cm$^3$ min$^{-1}$", @@ -2967,13 +2966,12 @@ def plotForArticle_figureFt(gitCommitID, currentDT, "$y^\mathregular{in}$ = " + str(initMoleFrac[0,1])] # Custom Legend Lines - custom_lines = [Line2D([0], [0], linestyle=':', lw=1, dash_capstyle = 'round', color = 'k'), - Line2D([0], [0], linestyle='-', lw=1, dash_capstyle = 'round', color = 'k')] + custom_lines = [Line2D([0], [0], linestyle=':', lw=1, dash_capstyle = 'round', alpha = alphaForPlot[0], color = 'k'), + Line2D([0], [0], linestyle='-', lw=1, dash_capstyle = 'round', alpha = alphaForPlot[1], color = 'k')] # Loop over all materials - for pp in range(len(zlcFileNameALL)): - fig = figure(figsize=(6.5,5)) + for pp in range(len(zlcFileNameALL)): zlcFileName = zlcFileNameALL[pp] objectiveFunction = np.zeros([len(zlcFileName)]) # Loop over all available ZLC files for a given material @@ -3095,7 +3093,7 @@ def plotForArticle_figureFt(gitCommitID, currentDT, # Save the figure if saveFlag: # FileName: figureFt___ - saveFileName = "figureFt" + currentDT + "_" + gitCommitID + saveFileExtension + saveFileName = "figureFt_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): From 5fee652cb10ee2cb1678f8dddb9f23f1c4c52623 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Thu, 18 Nov 2021 14:53:02 +0000 Subject: [PATCH 177/189] Update isotherm fitting tool --- IsothermFittingTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 9eed157..a4125ef 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 9eed1577edc8dccf25379d29281cdd829191947a +Subproject commit a4125efb67c3456947dc6e0a1688d3f8ed9fcb94 From 5400511ebc8d8bdea8462318c9e2d0984220b463 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Tue, 23 Nov 2021 15:48:31 +0000 Subject: [PATCH 178/189] Add background color to material characterization plot in plot function --- IsothermFittingTool | 2 +- plotFunctions/plotsForArticle_Experiment.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index a4125ef..8a7c704 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit a4125efb67c3456947dc6e0a1688d3f8ed9fcb94 +Subproject commit 8a7c7042a54958abfd0868e4d7f865c3bc5eda58 diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index cb77cee..3539d94 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -217,6 +217,16 @@ def plotForArticle_figureMat(gitCommitID, currentDT, ax.axvline(combinedPorosityData[QCindexLast-1,0], linestyle = ':', linewidth = 0.75, color = '#7d8597') + # Set background color for micropore region + ax.axvspan(0,2, facecolor='#F3CA40', alpha=0.3) + ax.text(0.13, 1.5, "micro", fontsize=8, color = 'k') + # Set background color for mesopore region + ax.axvspan(2, 50, facecolor='#F2A541', alpha=0.3) + ax.text(2.8, 1.5, "meso", fontsize=8, color = 'k') + # Set background color for macropore region + ax.axvspan(50, 1e6, facecolor='#F08A4B', alpha=0.3) + ax.text(2e3, 1.5, "macro", fontsize=8, color = 'k') + # Plot N2 sorption ax.semilogx(combinedPorosityData[0:QCindexLast-1:numIntPorosity,0], combinedPorosityData[0:QCindexLast-1:numIntPorosity,2], From acca4fef8fa2e381d85060a056006b67502778f6 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Wed, 1 Dec 2021 12:37:40 +0000 Subject: [PATCH 179/189] Update panel colors in plotForArticle_figureMat --- IsothermFittingTool | 2 +- plotFunctions/plotsForArticle_Experiment.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 8a7c704..321b0c2 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 8a7c7042a54958abfd0868e4d7f865c3bc5eda58 +Subproject commit 321b0c22686cbeb016cb57475d59210e7e635a65 diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 3539d94..eba199d 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -218,13 +218,13 @@ def plotForArticle_figureMat(gitCommitID, currentDT, linestyle = ':', linewidth = 0.75, color = '#7d8597') # Set background color for micropore region - ax.axvspan(0,2, facecolor='#F3CA40', alpha=0.3) + ax.axvspan(0,2, facecolor='#EEE0CB', alpha=0.3) ax.text(0.13, 1.5, "micro", fontsize=8, color = 'k') # Set background color for mesopore region - ax.axvspan(2, 50, facecolor='#F2A541', alpha=0.3) + ax.axvspan(2, 50, facecolor='#BAA898', alpha=0.3) ax.text(2.8, 1.5, "meso", fontsize=8, color = 'k') # Set background color for macropore region - ax.axvspan(50, 1e6, facecolor='#F08A4B', alpha=0.3) + ax.axvspan(50, 1e6, facecolor='#848586', alpha=0.3) ax.text(2e3, 1.5, "macro", fontsize=8, color = 'k') # Plot N2 sorption From 9f20eda2847d84ce6e123abfe775bfb631168212 Mon Sep 17 00:00:00 2001 From: ha3215 Date: Thu, 2 Dec 2021 15:39:18 +0000 Subject: [PATCH 180/189] Change plotForArticle_figureRawTex N2 sorption plots to log x-scale --- IsothermFittingTool | 2 +- plotFunctions/plotsForArticle_Experiment.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/IsothermFittingTool b/IsothermFittingTool index 321b0c2..bda2502 160000 --- a/IsothermFittingTool +++ b/IsothermFittingTool @@ -1 +1 @@ -Subproject commit 321b0c22686cbeb016cb57475d59210e7e635a65 +Subproject commit bda2502acbb94858c151cce65bd50749df981cfe diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index eba199d..22f6981 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -2244,7 +2244,7 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, for ll in range(3): # Plot N2 77 K isotherm data - ax3.plot(QCALL[:,0+2*ll], + ax3.semilogx(QCALL[:,0+2*ll], QCALL[:,1+2*ll], linewidth = 0.5, linestyle = ':', @@ -2255,13 +2255,14 @@ def plotForArticle_figureRawTex(gitCommitID, currentDT, # Text labels ax3.set(xlabel='$P/P_0$ [-]', ylabel='$q^*_{\mathrm{N}_2}$ [cm$^{3}$(STP) g$^{-1}$]', - xlim = [0,1], ylim = [0, 600]) + xlim = [1e-7,1], + ylim = [0, 600]) # ax3.text(70, 1.3, "(c)", fontsize=8,) ax3.legend(loc='upper right', handletextpad=0.2) - ax3.locator_params(axis="x", nbins=4) + # ax3.locator_params(axis="x", nbins=4) ax3.locator_params(axis="y", nbins=4) - ax3.text(0.05, 550, "(a)", fontsize=8,) + ax3.text(2e-7, 550, "(a)", fontsize=8,) # Save the figure if saveFlag: From cac45210e0eb085e732791e7a7a123236d8c5d56 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 22 Feb 2022 12:46:29 +0100 Subject: [PATCH 181/189] Minor cosmetic changes in plots --- plotFunctions/plotsForArticle_Experiment.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 22f6981..3135c80 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2022-02-22, AK: Minor fix for plots # - 2021-11-16, AK: Add Ft plot and minor fixes # - 2021-10-27, AK: Add plots for sensitivity analysis # - 2021-10-15, AK: Add plots for SI @@ -486,9 +487,9 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax1.locator_params(axis="x", nbins=5) ax1.legend(handletextpad=0.0,loc='center right') ax1.text(7, 1.3, "(a)", fontsize=8,) - ax1.text(12.7, 0.64, "MS", fontsize=8, fontweight = 'bold', + ax1.text(8.9, 0.64, "Segment II", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax1.text(7.2, 0.385, "$V^\mathrm{MS}$ = 0.02 cm$^3$", fontsize=8, + ax1.text(7.2, 0.385, "$V^\mathrm{SII}$ = 0.02 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax1.grid(which='minor', linestyle=':') elif kk == 1: @@ -502,9 +503,9 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax2.locator_params(axis="x", nbins=5) ax2.legend(handletextpad=0.0,loc='center right') ax2.text(70, 1.3, "(b)", fontsize=8,) - ax2.text(80, 0.64, "Setup w/ Ball", fontsize=8, fontweight = 'bold', + ax2.text(57, 0.64, "Segment I w/ Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax2.text(78, 0.385, "$V^\mathrm{S}$ = 3.76 cm$^3$", fontsize=8, + ax2.text(75, 0.385, "$V^\mathrm{SI}$ = 3.76 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax2.grid(which='minor', linestyle=':') elif kk == 2: @@ -518,9 +519,9 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax3.locator_params(axis="x", nbins=5) ax3.legend(handletextpad=0.0,loc='center right') ax3.text(70, 1.3, "(c)", fontsize=8,) - ax3.text(74, 0.64, "Setup w/o Ball", fontsize=8, fontweight = 'bold', + ax3.text(51, 0.64, "Segment I w/o Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax3.text(78, 0.385, "$V^\mathrm{S}$ = 3.93 cm$^3$", fontsize=8, + ax3.text(75, 0.385, "$V^\mathrm{SI}$ = 3.93 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax3.grid(which='minor', linestyle=':') @@ -3068,6 +3069,7 @@ def plotForArticle_figureFt(gitCommitID, currentDT, if ii == 1: ax1.text(30, 0.03, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) ax1.text(28, 1.3, "AC", fontsize=8, fontweight = 'bold',color = 'k') + ax1.grid(which='minor', linestyle=':') # Boron Nitride if pp == 1: # Ft - Log scale @@ -3083,7 +3085,8 @@ def plotForArticle_figureFt(gitCommitID, currentDT, ax2.text(5, 0.2, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) if ii == 1: ax2.text(22, 0.03, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) - ax2.text(28, 1.3, "BN", fontsize=8, fontweight = 'bold',color = 'k') + ax2.text(28, 1.3, "BN", fontsize=8, fontweight = 'bold',color = 'k') + ax2.grid(which='minor', linestyle=':') # Zeolite 13X if pp == 2: # Ft - Log scale @@ -3100,6 +3103,7 @@ def plotForArticle_figureFt(gitCommitID, currentDT, if ii == 1: ax3.text(65, 0.03, flowStr[ii], fontsize=8, color=colorsForPlot[ii]) ax3.text(67, 1.3, "13X", fontsize=8, fontweight = 'bold',color = 'k') + ax3.grid(which='minor', linestyle=':') # Save the figure if saveFlag: From 10cd11798d55d67496a2064ba39723a0579835cb Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 22 Feb 2022 12:50:14 +0100 Subject: [PATCH 182/189] More cosmetic changes in plots --- plotFunctions/plotsForArticle_Experiment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 3135c80..b3ca1d4 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -489,7 +489,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax1.text(7, 1.3, "(a)", fontsize=8,) ax1.text(8.9, 0.64, "Segment II", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax1.text(7.2, 0.385, "$V^\mathrm{SII}$ = 0.02 cm$^3$", fontsize=8, + ax1.text(7.2, 0.385, "$V^\mathrm{S_{II}}$ = 0.02 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax1.grid(which='minor', linestyle=':') elif kk == 1: @@ -505,7 +505,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax2.text(70, 1.3, "(b)", fontsize=8,) ax2.text(57, 0.64, "Segment I w/ Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax2.text(75, 0.385, "$V^\mathrm{SI}$ = 3.76 cm$^3$", fontsize=8, + ax2.text(75, 0.385, "$V^\mathrm{S_{I}}$ = 3.76 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax2.grid(which='minor', linestyle=':') elif kk == 2: @@ -521,7 +521,7 @@ def plotForArticle_figureDV(gitCommitID, currentDT, ax3.text(70, 1.3, "(c)", fontsize=8,) ax3.text(51, 0.64, "Segment I w/o Ball", fontsize=8, fontweight = 'bold', backgroundcolor = 'w', color = '#e71d36') - ax3.text(75, 0.385, "$V^\mathrm{SI}$ = 3.93 cm$^3$", fontsize=8, + ax3.text(75, 0.385, "$V^\mathrm{S_{I}}$ = 3.93 cm$^3$", fontsize=8, backgroundcolor = 'w', color = '#7d8597') ax3.grid(which='minor', linestyle=':') From 269319e07b3df9f22aa438e2b2e6df782428b658 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 11 Apr 2022 12:44:00 +0100 Subject: [PATCH 183/189] Changes to plots and add confidence regions --- plotFunctions/plotsForArticle_Experiment.py | 463 +++++++++++++++++++- 1 file changed, 439 insertions(+), 24 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index b3ca1d4..de4e87e 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2022-04-11, AK: Minor fix for plots (RPv1) # - 2022-02-22, AK: Minor fix for plots # - 2021-11-16, AK: Add Ft plot and minor fixes # - 2021-10-27, AK: Add plots for sensitivity analysis @@ -88,6 +89,12 @@ def plotsForArticle_Experiment(**kwargs): plotForArticle_figureZLCFit(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If ZLC fits needs to be plotted + if 'figureZLCFitALL' in kwargs: + if kwargs["figureZLCFitALL"]: + plotForArticle_figureZLCFitALL(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # If ZLC simulation fits needs to be plotted if 'figureZLCSimFit' in kwargs: if kwargs["figureZLCSimFit"]: @@ -146,6 +153,7 @@ def plotForArticle_figureMat(gitCommitID, currentDT, import scipy.io as sio import os from matplotlib.ticker import FormatStrFormatter + from computeEquilibriumLoading import computeEquilibriumLoading plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file @@ -303,7 +311,7 @@ def plotForArticle_figureMat(gitCommitID, currentDT, # Find temperatures temperature = np.unique(experimentALL[:,2]) - + # Find indices corresponding to each temperature for ll in range(len(temperature)): indexFirst = int(np.argwhere(experimentALL[:,2]==temperature[ll])[0]) @@ -315,11 +323,43 @@ def plotForArticle_figureMat(gitCommitID, currentDT, linewidth = 0, marker = markersForPlot_I[ll], color='#'+colorsForPlot_I[ll], label = str(temperature[ll])) - - # Plot fitted isotherm - ax.plot(isothermFitALL[1:-1,0],isothermFitALL[1:-1,ll+1], - linewidth = 1,color='#'+colorsForPlot_I[ll],alpha=0.5) + # Removed isotherm fit from MATLAB code (bug) + # ax.plot(isothermFitALL[1:-1,0],isothermFitALL[1:-1,ll+1], + # linewidth = 1,color='#'+colorsForPlot_I[ll],alpha=0.5) ax.legend(loc='best', handletextpad=0.0) + + + # Obtain the confidence bounds for the QC data + # Load isotherm parameters from QC data + isothermParameters = sio.loadmat(fileToLoad)["isothermData"]["isothermParameters"][0][0] + + # Create the grid for mole fractions + y = np.linspace(0,1.,100) + + # Prepare x_VOL + x_VOL = list(isothermParameters[0:-1:2,0]) + list(isothermParameters[1::2,0]) + x_VOL_CI = list(isothermParameters[0:-1:2,1]) + list(isothermParameters[1::2,1]) + + # Initialize volumetric loading + isoLoading_VOL = np.zeros([len(y),len(temperature)]) + + # Loop through all the temperature and mole fraction + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_VOL[ii,jj] = computeEquilibriumLoading(isothermModel=x_VOL, + moleFrac = y[ii], + temperature = temperature[jj]) + + # Get the confidence bounds + isoLoading_VOL_LowerBound, isoLoading_VOL_UpperBound = computeConfidenceBounds(x_VOL, x_VOL_CI, temperature, y) + + + # Plot fitted isotherm and confidence bounds + for jj in range(len(temperature)): + ax.plot(y,isoLoading_VOL[:,jj],color='#'+colorsForPlot_I[jj],alpha=1.,linestyle=':') # QC + ax.fill_between(y, isoLoading_VOL_LowerBound[:,jj], isoLoading_VOL_UpperBound[:,jj], + color='#'+colorsForPlot_I[jj],alpha = 0.1,linewidth=0.) # Lowest J + # Material specific text labels if kk == 0: ax.set(xlabel='$P$ [bar]', @@ -761,13 +801,15 @@ def plotForArticle_figureZLC(gitCommitID, currentDT, def plotForArticle_figureComp(gitCommitID, currentDT, saveFlag, saveFileExtension): import numpy as np + from matplotlib.pyplot import figure import matplotlib.pyplot as plt import auxiliaryFunctions from numpy import load import scipy.io as sio import os from computeEquilibriumLoading import computeEquilibriumLoading - from matplotlib.ticker import FormatStrFormatter + from matplotlib.ticker import FormatStrFormatter + from matplotlib.lines import Line2D plt.style.use('doubleColumn.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -813,10 +855,21 @@ def plotForArticle_figureComp(gitCommitID, currentDT, 'zlcParameters_20210825_1854_6b88505.npz', 'zlcParameters_20210826_0847_6b88505.npz', 'zlcParameters_20210827_0124_6b88505.npz',]] + + # Dead Volume + methodLabel = ['VOL','OPT',] + + # Custom Legend Lines + custom_lines = [Line2D([0], [0], linestyle=':', lw=1, color = '#4895EF'), + Line2D([0], [0], linestyle='-', lw=1, color = '#4895EF'),] + # Create the grid for mole fractions y = np.linspace(0,1.,100) + # Get the figure handle + fig = figure() + # Compute the ZLC loadings for pp in range(len(zlcFileNameALL)): # Initialize volumetric loading @@ -828,7 +881,8 @@ def plotForArticle_figureComp(gitCommitID, currentDT, # Prepare x_VOL x_VOL = list(isothermParameters[0:-1:2,0]) + list(isothermParameters[1::2,0]) - + x_VOL_CI = list(isothermParameters[0:-1:2,1]) + list(isothermParameters[1::2,1]) + # Loop through all the temperature and mole fraction for jj in range(len(temperature)): for ii in range(len(y)): @@ -836,8 +890,11 @@ def plotForArticle_figureComp(gitCommitID, currentDT, moleFrac = y[ii], temperature = temperature[jj]) + # Get the confidence bounds + isoLoading_VOL_LowerBound, isoLoading_VOL_UpperBound = computeConfidenceBounds(x_VOL, x_VOL_CI, temperature, y) + + # Get the ZLC Isotherms zlcFileName = zlcFileNameALL[pp] - # Initialize isotherms isoLoading_ZLC = np.zeros([len(zlcFileName),len(y),len(temperature)]) objectiveFunction = np.zeros([len(zlcFileName)]) @@ -855,23 +912,26 @@ def plotForArticle_figureComp(gitCommitID, currentDT, # Parse out the isotherm parameter isothermModel = x_ZLC[0:-2] - + for jj in range(len(temperature)): for ii in range(len(y)): isoLoading_ZLC[kk,ii,jj] = computeEquilibriumLoading(isothermModel=isothermModel, moleFrac = y[ii], temperature = temperature[jj]) # [mol/kg] - - + # Plot the isotherms ax1 = plt.subplot(1,3,pp+1) minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) for jj in range(len(temperature)): - ax1.plot(y,isoLoading_VOL[:,jj],color='#'+colorsForPlot[jj],label=str(temperature[jj])+' K') # QC + ax1.plot(y,isoLoading_VOL[:,jj],color='#'+colorsForPlot[jj],linestyle=':',alpha=0.5) # QC + # Get the confidence bounds + ax1.fill_between(y, isoLoading_VOL_LowerBound[:,jj], isoLoading_VOL_UpperBound[:,jj], + color='#'+colorsForPlot[jj],alpha = 0.25,linewidth=0.) # Lowest J for qq in range(len(zlcFileName)): if qq == minJ[0]: - ax1.plot(y,isoLoading_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha = 0.2) # Lowest J + ax1.plot(y,isoLoading_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha = 1, + label=str(temperature[jj])+' K') # Lowest J if pp == 0: # Isotherm @@ -879,9 +939,9 @@ def plotForArticle_figureComp(gitCommitID, currentDT, ylabel='$q^*_\mathregular{CO_2}$ [mol kg$^{-1}$]', xlim = [0,1], ylim = [0, 3]) ax1.text(0.04, 2.75, "(a)", fontsize=8,) - ax1.text(0.45, 3.2, "AC", fontsize=8, fontweight = 'bold',color = 'k') - ax1.text(0.84, 0.32, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.84, 0.12, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.text(0.15, 2.74, "AC", fontsize=8, fontweight = 'bold',color = 'k') + # ax1.text(0.84, 0.32, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax1.text(0.84, 0.12, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.legend() @@ -889,9 +949,9 @@ def plotForArticle_figureComp(gitCommitID, currentDT, # Isotherm ax1.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 1.5]) ax1.text(0.04, 1.35, "(b)", fontsize=8,) - ax1.text(0.45, 1.6, "BN", fontsize=8, fontweight = 'bold',color = 'k') - ax1.text(0.84, 0.16, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.84, 0.06, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.text(0.15, 1.345, "BN", fontsize=8, fontweight = 'bold',color = 'k') + # ax1.text(0.84, 0.16, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax1.text(0.84, 0.06, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.legend() @@ -899,13 +959,16 @@ def plotForArticle_figureComp(gitCommitID, currentDT, # Isotherm ax1.set(xlabel='$P$ [bar]', xlim = [0,1], ylim = [0, 8]) ax1.text(0.04, 7.3, "(c)", fontsize=8,) - ax1.text(0.44, 8.5, "13X", fontsize=8, fontweight = 'bold',color = 'k') - ax1.text(0.84, 0.86, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') - ax1.text(0.84, 0.32, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) + ax1.text(0.15, 7.25, "13X", fontsize=8, fontweight = 'bold',color = 'k') + # ax1.text(0.84, 0.86, "VOL", fontsize=8, fontweight = 'bold',color = '#4895EF') + # ax1.text(0.84, 0.32, "OPT", fontsize=8, fontweight = 'bold',color = '#4895EF', alpha = 0.3) ax1.locator_params(axis="x", nbins=4) ax1.locator_params(axis="y", nbins=4) ax1.legend(loc='upper right') ax1.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + fig.legend(custom_lines,methodLabel,bbox_to_anchor=(0.07,0.93,0.55,0.1), + ncol=2, borderaxespad=0, labelcolor = '#4895EF') + # Save the figure if saveFlag: # FileName: figureComp___ @@ -2113,11 +2176,11 @@ def plotForArticle_figureZLCObj(gitCommitID, currentDT, fontsize=8,color = '#7d8597') if pp == 0: - ax2.set(xlabel='Iteration [-]', + ax2.set(xlabel='Repetition [-]', ylabel='$J$ [-]', xlim = [1,5],ylim = YLIM_H[pp]) else: - ax2.set(xlabel='Iteration [-]', + ax2.set(xlabel='Repetition [-]', xlim = [1,5],ylim = YLIM_H[pp]) ax2.locator_params(axis="y", nbins=4) ax2.xaxis.set_major_locator(MaxNLocator(integer=True)) @@ -3116,3 +3179,355 @@ def plotForArticle_figureFt(gitCommitID, currentDT, plt.savefig (savePath, bbox_inches = "tight") plt.show() + +# fun: plotForArticle_figureZLCFitALL +# Plots the Figure ZLC Fit of the manuscript: ZLC goodness of fit for experimental results +def plotForArticle_figureZLCFitALL(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.pyplot import figure + import auxiliaryFunctions + from numpy import load + import os + from simulateCombinedModel import simulateCombinedModel + from deadVolumeWrapper import deadVolumeWrapper + from extractDeadVolume import filesToProcess # File processing script + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers + colorsForPlot = ["ffba08","d00000","03071e"] + markersForPlot = ["^","d","v"] + + # X limits for the different materials + XLIM_L = [[0, 200],[0, 150],[0, 600]] + XLIM_H = [[0, 100],[0, 60],[0, 200]] + + # Label positions for the different materials + panelLabel_L = [170, 150/200*170, 600/200*170] + panelLabel_H = [170/2, 60/100*170/2, 200/100*170/2] + materialLabel_L = [182, 150/200*182, 600/200*180] + materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] + flowLabel_L = [250] + flowLabel_H = [250/2] + materialText = ["AC", "BN", "13X"] + + # Parameter estimate files + # Activated Carbon Experiments + zlcFileNameALL = [['zlcParameters_20210822_0926_c8173b1.npz', + 'zlcParameters_20210822_1733_c8173b1.npz', + # 'zlcParameters_20210823_0133_c8173b1.npz', # DSL BAD (but lowest J) + # 'zlcParameters_20210823_1007_c8173b1.npz', # DSL BAD (but lowest J) + 'zlcParameters_20210823_1810_c8173b1.npz'], + # Boron Nitride Experiments + ['zlcParameters_20210823_1731_c8173b1.npz', + 'zlcParameters_20210824_0034_c8173b1.npz', + 'zlcParameters_20210824_0805_c8173b1.npz', + 'zlcParameters_20210824_1522_c8173b1.npz', + 'zlcParameters_20210824_2238_c8173b1.npz',], + # Zeolite 13X Experiments + ['zlcParameters_20210824_1552_6b88505.npz', + 'zlcParameters_20210825_0559_6b88505.npz', + 'zlcParameters_20210825_1854_6b88505.npz', + 'zlcParameters_20210826_0847_6b88505.npz', + 'zlcParameters_20210827_0124_6b88505.npz',]] + + fig = figure(figsize=(6.5,5)) + for pp in range(len(zlcFileNameALL)): + + zlcFileName = zlcFileNameALL[pp] + objectiveFunction = np.zeros([len(zlcFileName)]) + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # Obtain the onjective function values + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + + # Find the experiment with the min objective function + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + fileParameter = zlcFileName[int(minJ[0])] + + # ZLC parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + + # Parse out experiments names and temperature used for the fitting + rawFileName = load(parameterPath)["fileName"] + temperatureExp = load(parameterPath)["temperature"] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'ZLC') + # Get the processed file names + fileName = filesToProcess(False,[],[],'ZLC') + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Parse out all the necessary quantities to obtain model fit + # Mass of sorbent and particle epsilon + adsorbentDensity = load(parameterPath)["adsorbentDensity"] + particleEpsilon = load(parameterPath)["particleEpsilon"] + massSorbent = load(parameterPath)["massSorbent"] + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + # Dead volume model + deadVolumeFile = str(load(parameterPath)["deadVolumeFile"]) + # Isotherm parameter reference + parameterReference = load(parameterPath)["parameterReference"] + # Load the model + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x = np.multiply(modelNonDim,parameterReference) + print(x) + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Initialize loadings + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # Loop over all available files + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + + # Initialize outputs + moleFracSim = [] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Parse out parameter values + isothermModel = x[0:-2] + rateConstant_1 = x[-2] + rateConstant_2 = x[-1] + + # Compute the dead volume response using the optimizer parameters + _ , moleFracSim , resultMat = simulateCombinedModel(timeInt = timeInt, + initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point + flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = True, + isothermModel = isothermModel, + rateConstant_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, + deadVolumeFile = deadVolumeFile, + volSorbent = volSorbent, + volGas = volGas, + temperature = temperatureExp[ii], + adsorbentDensity = adsorbentDensity) + # Print simulation volume + print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, + moleFracSim), + timeElapsedExp),2)) + + # Stack mole fraction from experiments and simulation for error + # computation + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) + + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + deadVolumePath = os.path.join('..','simulationResults',deadVolumeFile) + modelOutputTemp = load(deadVolumePath, allow_pickle=True)["modelOutput"] + pDV = modelOutputTemp[()]["variable"] + dvFileLoadTemp = load(deadVolumePath) + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + moleFracDV = deadVolumeWrapper(timeInt, resultMat[3,:]*1e6, pDV, flagMSDeadVolume, msDeadVolumeFile, initMoleFrac = [moleFracExp[0]]) + + if 300__ + saveFileName = "figureZLCFitALL_" + materialText[pp] + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath,bbox_inches='tight') + + plt.show() +# fun: computeConfidenceBounds +# Generate LHS sampled isotherms using the confidence bounds +def computeConfidenceBounds(isoParam, ciParam, temperature, y): + from smt.sampling_methods import LHS + import numpy as np + from computeEquilibriumLoading import computeEquilibriumLoading + + # Generate numIso isotherm parameters in the confidence region + numIso = 1000 + + # Initialize the bounds for the parameter values + if len(isoParam) == 3: + isoBound = np.zeros([6,2]) + # Lower Bound + isoBound[0:2,0] = np.array(isoParam[0:2]) - np.array(ciParam[0:2]) + isoBound[2,0] = np.array(isoParam[2]) + np.array(ciParam[2]) + + # Upper Bound + isoBound[0:2,1] = np.array(isoParam[0:2]) + np.array(ciParam[0:2]) + isoBound[2,1] = np.array(isoParam[2]) - np.array(ciParam[2]) + + else: + isoBound = np.zeros([6,2]) + # Lower Bound + isoBound[0:2,0] = np.array(isoParam[0:2]) - np.array(ciParam[0:2]) + isoBound[3:5,0] = np.array(isoParam[3:5]) - np.array(ciParam[3:5]) + isoBound[2:6:3,0] = np.array(isoParam[2:6:3]) + np.array(ciParam[2:6:3]) + + # Upper Bound + isoBound[0:2,1] = np.array(isoParam[0:2]) + np.array(ciParam[0:2]) + isoBound[3:5,1] = np.array(isoParam[3:5]) + np.array(ciParam[3:5]) + isoBound[2:6:3,1] = np.array(isoParam[2:6:3]) - np.array(ciParam[2:6:3]) + + # Generate a LHS method with the isotherm parameter bounds + lhsPopulation = LHS(xlimits=isoBound) + + # Generate numIso isotherm parameters + ciIsothermParameters = lhsPopulation(numIso) + + # Initialize isoLoading + isoLoading_VOL = np.zeros([len(ciIsothermParameters),len(y),len(temperature)]) + # Loop through all the isotherm parameters, temperature and mole fraction + for kk in range(len(ciIsothermParameters)): + ciIsothermParametersTemp = [np.float64(qq) for qq in ciIsothermParameters[kk,:]] + for jj in range(len(temperature)): + for ii in range(len(y)): + isoLoading_VOL[kk,ii,jj] = computeEquilibriumLoading(isothermModel=ciIsothermParametersTemp, + moleFrac = y[ii], + temperature = temperature[jj]) + + # Find the maximum and minimum loading at each partial pressure & temperature + isoLoadingLowerBound = isoLoading_VOL.min(axis = 0) + isoLoadingUpperBound = isoLoading_VOL.max(axis = 0) + + return isoLoadingLowerBound, isoLoadingUpperBound \ No newline at end of file From d24d47aed211a46f1f91965685f1326183e76a28 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 11 Apr 2022 13:18:05 +0100 Subject: [PATCH 184/189] Minor fix to time resolved plot --- plotFunctions/plotsForArticle_Experiment.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index de4e87e..146e095 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -3193,6 +3193,7 @@ def plotForArticle_figureZLCFitALL(gitCommitID, currentDT, from simulateCombinedModel import simulateCombinedModel from deadVolumeWrapper import deadVolumeWrapper from extractDeadVolume import filesToProcess # File processing script + from matplotlib.lines import Line2D plt.style.use('doubleColumn2Row.mplstyle') # Custom matplotlib style file # Get the commit ID of the current repository @@ -3464,11 +3465,21 @@ def plotForArticle_figureZLCFitALL(gitCommitID, currentDT, plt.figtext(-0.02, 0.83, "AC", fontsize=8, fontweight = 'bold',color = '#4895EF') plt.figtext(-0.02, 0.51, "BN", fontsize=8, fontweight = 'bold',color = '#4895EF') plt.figtext(-0.02, 0.19, "13X", fontsize=8, fontweight = 'bold',color = '#4895EF') - + + # Dead Volume + tempLabel = ['306 K','325 K', '345 K'] + + # Custom Legend Lines + custom_lines = [Line2D([0], [0], linestyle='-', lw=1, color = '#ffba08'), + Line2D([0], [0], linestyle='-', lw=1, color = '#d00000'), + Line2D([0], [0], linestyle='-', lw=1, color = '#03071e'),] + + fig.legend(custom_lines,tempLabel,bbox_to_anchor=(0.04,0.95,0.675,0.1), + ncol=3, borderaxespad=0) # Save the figure if saveFlag: # FileName: figureZLCALL___ - saveFileName = "figureZLCFitALL_" + materialText[pp] + "_" + currentDT + "_" + gitCommitID + saveFileExtension + saveFileName = "figureZLCFitALL_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): From e81a19e6e2967bc1c4c8ce3f7adcf3a493932688 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Mon, 25 Apr 2022 19:44:32 +0100 Subject: [PATCH 185/189] Cosmetic change for plots --- plotFunctions/plotsForArticle_Experiment.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 146e095..e878d45 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -977,7 +977,7 @@ def plotForArticle_figureComp(gitCommitID, currentDT, # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) - plt.savefig (savePath) + plt.savefig (savePath,bbox_inches='tight') plt.show() @@ -3208,16 +3208,11 @@ def plotForArticle_figureZLCFitALL(gitCommitID, currentDT, # X limits for the different materials XLIM_L = [[0, 200],[0, 150],[0, 600]] - XLIM_H = [[0, 100],[0, 60],[0, 200]] + XLIM_H = [[0, 60],[0, 40],[0, 150]] # Label positions for the different materials panelLabel_L = [170, 150/200*170, 600/200*170] - panelLabel_H = [170/2, 60/100*170/2, 200/100*170/2] - materialLabel_L = [182, 150/200*182, 600/200*180] - materialLabel_H = [182/2, 60/100*182/2, 200/100*180/2] - flowLabel_L = [250] - flowLabel_H = [250/2] - materialText = ["AC", "BN", "13X"] + panelLabel_H = [60/200*170, 60/200*40/60*170, 60/200*150/60*170] # Parameter estimate files # Activated Carbon Experiments From e072f61a9dd9e3e50e7bbf76b7f795c056c8b7a6 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 May 2022 09:34:53 +0100 Subject: [PATCH 186/189] Minor fix for time-resolved plots --- plotFunctions/plotsForArticle_Experiment.py | 313 +++++++++++++++++++- 1 file changed, 300 insertions(+), 13 deletions(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index e878d45..c81727b 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -12,6 +12,7 @@ # Plots for the experiment manuscript # # Last modified: +# - 2022-05-10, AK: Minor fix for time-resolved plots # - 2022-04-11, AK: Minor fix for plots (RPv1) # - 2022-02-22, AK: Minor fix for plots # - 2021-11-16, AK: Add Ft plot and minor fixes @@ -101,6 +102,12 @@ def plotsForArticle_Experiment(**kwargs): plotForArticle_figureZLCSimFit(gitCommitID, currentDT, saveFlag, saveFileExtension) + # If ZLC fits needs to be plotted + if 'figureZLCSimFitALL' in kwargs: + if kwargs["figureZLCSimFitALL"]: + plotForArticle_figureZLCSimFitALL(gitCommitID, currentDT, + saveFlag, saveFileExtension) + # If ZLC experimental repeats needs to be plotted if 'figureZLCRep' in kwargs: if kwargs["figureZLCRep"]: @@ -3430,19 +3437,7 @@ def plotForArticle_figureZLCFitALL(gitCommitID, currentDT, ax4.locator_params(axis="x", nbins=4) ax4.grid(which='minor', linestyle=':') ax4.axes.yaxis.set_ticklabels([]) - - # Get the order of temperatures for each plot - # temperatureOrder = np.argsort(temperatureExp[0:3]) - # Get the axis handles and labels and order it - # handles,labels = ax1.get_legend_handles_labels() - # ax1.legend([handles[idx] for idx in temperatureOrder],[labels[idx] for idx in temperatureOrder], loc = "upper center", ncol=3, columnspacing=1) - # handles,labels = ax2.get_legend_handles_labels() - # ax2.legend([handles[idx] for idx in temperatureOrder],[labels[idx] for idx in temperatureOrder], loc = "upper center", ncol=3, columnspacing=1) - # handles,labels = ax3.get_legend_handles_labels() - # ax3.legend([handles[idx] for idx in temperatureOrder],[labels[idx] for idx in temperatureOrder], loc = "upper center", ncol=3, columnspacing=1) - # handles,labels = ax4.get_legend_handles_labels() - # ax4.legend([handles[idx] for idx in temperatureOrder],[labels[idx] for idx in temperatureOrder], loc = "upper center", ncol=3, columnspacing=1) - + # Put panel labels ax1.text(panelLabel_L[pp], 0.6, '('+chr(96+4*pp+1)+')', fontsize=8,) ax2.text(panelLabel_L[pp], 0.6, '('+chr(96+4*pp+2)+')', fontsize=8,) @@ -3471,6 +3466,7 @@ def plotForArticle_figureZLCFitALL(gitCommitID, currentDT, fig.legend(custom_lines,tempLabel,bbox_to_anchor=(0.04,0.95,0.675,0.1), ncol=3, borderaxespad=0) + # Save the figure if saveFlag: # FileName: figureZLCALL___ @@ -3482,6 +3478,297 @@ def plotForArticle_figureZLCFitALL(gitCommitID, currentDT, plt.savefig (savePath,bbox_inches='tight') plt.show() + +# fun: plotForArticle_figureZLCSimFit +# Plots the Figure ZLC Fit of the manuscript: ZLC goodness for computational results +def plotForArticle_figureZLCSimFitALL(gitCommitID, currentDT, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.pyplot import figure + import auxiliaryFunctions + from numpy import load + import os + from simulateCombinedModel import simulateCombinedModel + from deadVolumeWrapper import deadVolumeWrapper + from extractDeadVolume import filesToProcess # File processing script + from matplotlib.lines import Line2D + plt.style.use('doubleColumn2Row.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() + + # Plot colors and markers + colorsForPlot = ["0091ad","5c4d7d","b7094c"] + markersForPlot = ["^","d","v"] + + # X limits for the different materials + XLIM_L = [[0, 200],[0, 150],[0, 600]] + XLIM_H = [[0, 60],[0, 40],[0, 150]] + + # Label positions for the different materials + panelLabel_L = [170, 150/200*170, 600/200*170] + panelLabel_H = [60/200*170, 60/200*40/60*170, 60/200*150/60*170] + + # Parameter estimate files + # Activated Carbon Simulations + zlcFileNameALL = [['zlcParameters_20210823_1104_03c82f4.npz', + 'zlcParameters_20210824_0000_03c82f4.npz', + 'zlcParameters_20210824_1227_03c82f4.npz', + 'zlcParameters_20210825_0017_03c82f4.npz', + 'zlcParameters_20210825_1151_03c82f4.npz'], + # Boron Nitride Simulations + ['zlcParameters_20210823_1907_03c82f4.npz', + 'zlcParameters_20210824_0555_03c82f4.npz', + 'zlcParameters_20210824_2105_03c82f4.npz', + 'zlcParameters_20210825_0833_03c82f4.npz', + 'zlcParameters_20210825_2214_03c82f4.npz'], + # Zeolite 13X Simulations + ['zlcParameters_20210824_1102_c8173b1.npz', + 'zlcParameters_20210825_0243_c8173b1.npz', + 'zlcParameters_20210825_1758_c8173b1.npz', + 'zlcParameters_20210826_1022_c8173b1.npz', + 'zlcParameters_20210827_0104_c8173b1.npz']] + + fig = figure(figsize=(6.5,5)) + for pp in range(len(zlcFileNameALL)): + zlcFileName = zlcFileNameALL[pp] + objectiveFunction = np.zeros([len(zlcFileName)]) + # Loop over all available ZLC files for a given material + for kk in range(len(zlcFileName)): + # Obtain the onjective function values + parameterPath = os.path.join('..','simulationResults',zlcFileName[kk]) + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + objectiveFunction[kk] = round(modelOutputTemp[()]["function"],0) + + # Find the experiment with the min objective function + minJ = np.argwhere(objectiveFunction == min(objectiveFunction)) + fileParameter = zlcFileName[int(minJ[0])] + + # ZLC parameter model path + parameterPath = os.path.join('..','simulationResults',fileParameter) + + # Parse out experiments names and temperature used for the fitting + rawFileName = load(parameterPath)["fileName"] + temperatureExp = load(parameterPath)["temperature"] + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'ZLC') + # Get the processed file names + fileName = filesToProcess(False,[],[],'ZLC') + + numPointsExp = np.zeros(len(fileName)) + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + # Load experimental molefraction + timeElapsedExp = load(fileToLoad)["timeElapsed"].flatten() + numPointsExp[ii] = len(timeElapsedExp) + + # Parse out all the necessary quantities to obtain model fit + # Mass of sorbent and particle epsilon + adsorbentDensity = load(parameterPath)["adsorbentDensity"] + particleEpsilon = load(parameterPath)["particleEpsilon"] + massSorbent = load(parameterPath)["massSorbent"] + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas chamber (dead volume) [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + # Dead volume model + deadVolumeFile = str(load(parameterPath)["deadVolumeFile"]) + # Isotherm parameter reference + parameterReference = load(parameterPath)["parameterReference"] + # Load the model + modelOutputTemp = load(parameterPath, allow_pickle=True)["modelOutput"] + modelNonDim = modelOutputTemp[()]["variable"] + # Multiply the paremeters by the reference values + x = np.multiply(modelNonDim,parameterReference) + # Downsample intervals + downsampleInt = numPointsExp/np.min(numPointsExp) + + # Initialize loadings + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # Loop over all available files + for ii in range(len(fileName)): + fileToLoad = fileName[ii] + + # Initialize outputs + moleFracSim = [] + # Load experimental time, molefraction and flowrate (accounting for downsampling) + timeElapsedExpTemp = load(fileToLoad)["timeElapsed"].flatten() + moleFracExpTemp = load(fileToLoad)["moleFrac"].flatten() + flowRateTemp = load(fileToLoad)["flowRate"].flatten() + timeElapsedExp = timeElapsedExpTemp[::int(np.round(downsampleInt[ii]))] + moleFracExp = moleFracExpTemp[::int(np.round(downsampleInt[ii]))] + flowRateExp = flowRateTemp[::int(np.round(downsampleInt[ii]))] + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Parse out parameter values + isothermModel = x[0:-2] + rateConstant_1 = x[-2] + rateConstant_2 = x[-1] + + # Compute the dead volume response using the optimizer parameters + _ , moleFracSim , resultMat = simulateCombinedModel(timeInt = timeInt, + initMoleFrac = [moleFracExp[0]], # Initial mole fraction assumed to be the first experimental point + flowIn = np.mean(flowRateExp[-1:-10:-1]*1e-6), # Flow rate for ZLC considered to be the mean of last 10 points (equilibrium) + expFlag = True, + isothermModel = isothermModel, + rateConstant_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, + deadVolumeFile = deadVolumeFile, + volSorbent = volSorbent, + volGas = volGas, + temperature = temperatureExp[ii], + adsorbentDensity = adsorbentDensity) + # Print simulation volume + print("Simulation",str(ii+1),round(np.trapz(np.multiply(resultMat[3,:]*1e6, + moleFracSim), + timeElapsedExp),2)) + + # Stack mole fraction from experiments and simulation for error + # computation + minExp = np.min(moleFracExp) # Compute the minimum from experiment + normalizeFactor = np.max(moleFracExp - np.min(moleFracExp)) # Compute the max from normalized data + moleFracExpALL = np.hstack((moleFracExpALL, (moleFracExp-minExp)/normalizeFactor)) + moleFracSimALL = np.hstack((moleFracSimALL, (moleFracSim-minExp)/normalizeFactor)) + + # Call the deadVolume Wrapper function to obtain the outlet mole fraction + deadVolumePath = os.path.join('..','simulationResults',deadVolumeFile) + modelOutputTemp = load(deadVolumePath, allow_pickle=True)["modelOutput"] + pDV = modelOutputTemp[()]["variable"] + dvFileLoadTemp = load(deadVolumePath) + flagMSDeadVolume = dvFileLoadTemp["flagMSDeadVolume"] + msDeadVolumeFile = dvFileLoadTemp["msDeadVolumeFile"] + moleFracDV = deadVolumeWrapper(timeInt, resultMat[3,:]*1e6, pDV, flagMSDeadVolume, msDeadVolumeFile, initMoleFrac = [moleFracExp[0]]) + + if 300__ + saveFileName = "figureZLCSimFit_" + materialText[pp] + "_" + currentDT + "_" + gitCommitID + saveFileExtension + savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) + # Check if inputResources directory exists or not. If not, create the folder + if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): + os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) + plt.savefig (savePath) + + plt.show() + # fun: computeConfidenceBounds # Generate LHS sampled isotherms using the confidence bounds def computeConfidenceBounds(isoParam, ciParam, temperature, y): From 3d2e0ec385fca6c3ba7aa5cdcad9b0780c032b79 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 May 2022 10:41:56 +0100 Subject: [PATCH 187/189] Minor fix --- plotFunctions/plotsForArticle_Experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index c81727b..e2bd4c9 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -3760,7 +3760,7 @@ def plotForArticle_figureZLCSimFitALL(gitCommitID, currentDT, # Save the figure if saveFlag: # FileName: figureZLCSimFit___ - saveFileName = "figureZLCSimFit_" + materialText[pp] + "_" + currentDT + "_" + gitCommitID + saveFileExtension + saveFileName = "figureZLCSimFitALL_" + currentDT + "_" + gitCommitID + saveFileExtension savePath = os.path.join('..','simulationFigures','experimentManuscript',saveFileName) # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): From c7d32a4d63f8bc576ccea6f78b8d040832b52012 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Tue, 10 May 2022 12:26:05 +0100 Subject: [PATCH 188/189] Minor fix --- plotFunctions/plotsForArticle_Experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index e2bd4c9..713c8a9 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -3765,7 +3765,7 @@ def plotForArticle_figureZLCSimFitALL(gitCommitID, currentDT, # Check if inputResources directory exists or not. If not, create the folder if not os.path.exists(os.path.join('..','simulationFigures','experimentManuscript')): os.mkdir(os.path.join('..','simulationFigures','experimentManuscript')) - plt.savefig (savePath) + plt.savefig (savePath,bbox_inches='tight') plt.show() From 3cdf3968d37efe3c51c901aaeaadacab43afce1c Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Rajagopalan Date: Fri, 13 May 2022 14:18:42 +0100 Subject: [PATCH 189/189] Cosmetic change to plot --- plotFunctions/plotsForArticle_Experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py index 713c8a9..33a7b63 100644 --- a/plotFunctions/plotsForArticle_Experiment.py +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -2775,7 +2775,7 @@ def plotForArticle_figureSensitivity(gitCommitID, currentDT, ax3.set(xlabel = '$P$ [bar]', xlim = [0,1], ylim = [0, 3]) ax3.text(0.04, 2.75, "(c)", fontsize=8,) - ax3.text(0.56, 0.15, "Dead Volume", fontsize=8, fontweight = 'bold',color = 'k') + ax3.text(0.53, 0.15, "Blank Volume", fontsize=8, fontweight = 'bold',color = 'k') ax3.locator_params(axis="x", nbins=4) ax3.locator_params(axis="y", nbins=4) ax3.legend(custom_linesDV, deadLabel, loc = 'upper right')