diff --git a/.gitignore b/.gitignore index d1d4543..d1db017 100755 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ __pycache__/ #*.bib *.xlsx *.txt +*.zip # Python related outputs *.npy @@ -25,6 +26,9 @@ __pycache__/ # C extensions *.so +# MATLAB extensions +*.mat + # Distribution / packaging .Python build/ 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..9eed157 --- /dev/null +++ b/IsothermFittingTool @@ -0,0 +1 @@ +Subproject commit 9eed1577edc8dccf25379d29281cdd829191947a diff --git a/experimental/README.md b/experimental/README.md new file mode 100644 index 0000000..29e74ef --- /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 diff --git a/experimental/analysis/analyzeCalibration.m b/experimental/analysis/analyzeCalibration.m new file mode 100644 index 0000000..97f4527 --- /dev/null +++ b/experimental/analysis/analyzeCalibration.m @@ -0,0 +1,226 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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-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 +% - 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 +% - 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 +% polynomial fitting +% - 2021-03-18, AK: Fix variable names +% - 2021-03-17, AK: Change structure +% - 2021-03-17, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function analyzeCalibration(parametersFlow,parametersMS) +% Find the directory of the file and move to the top folder +filePath = which('analyzeCalibration'); +cd(filePath(1:end-21)); + +% Get the git commit ID +gitCommitID = getGitCommit; + +% Load the file that contains the flow meter calibration +if ~isempty(parametersFlow) + flowData = load(parametersFlow); + % 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]; % 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_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 + + % 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 + if exist(['..',filesep,'experimentalData',filesep,... + 'calibrationData'],'dir') == 7 + % Save the calibration data for further use + save(['..',filesep,'experimentalData',filesep,... + 'calibrationData',filesep,parametersFlow,'_Model'],'calibrationFlow',... + 'gitCommitID'); + else + % Create the calibration data folder if it does not exist + mkdir(['..',filesep,'experimentalData',filesep,'calibrationData']) + % Save the calibration data for further use + save(['..',filesep,'experimentalData',filesep,... + 'calibrationData',filesep,parametersFlow,'_Model'],'calibrationFlow',... + 'gitCommitID'); + end + + % Plot the raw and the calibrated data (for pure gases at MFC) + figure + MFC1Set = 0:80; + subplot(1,2,1) + hold on + scatter(volFlow_MFC1_PureHe,volFlow_UMFM_PureHe,'or') + plot(MFC1Set,calibrationFlow.MFC_He*MFC1Set,'b') + 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_MFC2_PureCO2,volFlow_UMFM_PureCO2,'or') + plot(MFC1Set,calibrationFlow.MFC_CO2*MFC1Set,'b') + 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 + 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 + [reconciledData, expInfo] = concatenateData(parametersMS); + % Find the index that corresponds to the last time for a given set + % point + setPtMFC = unique(reconciledData.flow(:,5)); + % 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(numRepetitions*length(setPtMFC),2); + % Loop over all the set points + 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 + % 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 + if exist(['..',filesep,'experimentalData',filesep,... + 'calibrationData'],'dir') == 7 + % Save the calibration data for further use + save(['..',filesep,'experimentalData',filesep,... + 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... + 'gitCommitID','parametersMS'); + else + % Create the calibration data folder if it does not exist + mkdir(['..',filesep,'experimentalData',filesep,'calibrationData']) + % Save the calibration data for further use + save(['..',filesep,'experimentalData',filesep,... + 'calibrationData',filesep,parametersMS.flow,'_Model'],'calibrationMS',... + 'gitCommitID','parametersMS'); + end + + % Plot the raw and the calibrated data + figure(1) + 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 [-]') + set(gca,'FontSize',8) +end +end \ No newline at end of file diff --git a/experimental/analysis/analyzeExperiment.m b/experimental/analysis/analyzeExperiment.m new file mode 100644 index 0000000..c863a49 --- /dev/null +++ b/experimental/analysis/analyzeExperiment.m @@ -0,0 +1,115 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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-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 +% - 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 +% Python script +% - 2021-03-18, AK: Updates to structure +% - 2021-03-18, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function analyzeExperiment(experimentStruct,flagCalibration,flagFlowMeter) + % 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); + + % 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 = min([find(outputStruct.moleFrac(:,2)=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 +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(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','southeast'); +xlabel('{\it{D}} [nm]'); ylabel('{\it{V}} [mL/g]'); +set(gca,'FontSize',14) +box on;grid on; + +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 diff --git a/experimental/analysis/concatenateData.m b/experimental/analysis/concatenateData.m new file mode 100644 index 0000000..abb9ac8 --- /dev/null +++ b/experimental/analysis/concatenateData.m @@ -0,0 +1,219 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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-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 +% - 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 +% - 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 +% - 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 +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function [reconciledData, expInfo] = concatenateData(fileToLoad) + % Load flow data + flowMS = load(fileToLoad.flow); + 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 + load(fileToLoad.calibrationFlow); + % Return expInfo + expInfo = flowMS.expInfo; + % Analyse flow data + MFC1 = [flowMS.outputStruct.MFC1]; % MFC1 - He + 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 + % 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 + 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(rawMSData{1,4},... + 'InputFormat','MM/dd/yyyy hh:mm:ss.SSS a'); + % Get the date time for He + dateTimeCO2 = datetime(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_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_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 # + % 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); + reconciledData.raw.volFlow_MFM = volFlow_MFM(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 = 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 + for ii=indexInitial_MS:concantenateLastInd + % He + % If negative element, initialize to eps + if str2num(cell2mat(rawMSData{1,6}(ii))) < 0 + reconciledData.raw.signalHe(ii-indexInitial_MS+1) = eps; + % If not, use the actual value + else + 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-indexInitial_MS+1) = eps; + % If not, use the actual value + else + reconciledData.raw.signalCO2(ii-indexInitial_MS+1) = str2num(cell2mat(rawMSData{1,3}(ii))); + end + end + + % Reconciled data (with interpolation) + % 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] + % 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] + 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] + % 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 + % This will be used in the analyzeCalibration script + 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); + % If actual experiment is analyzed, loads the calibration MS file + else + % Find number of calibration files being used + numCalibrationFiles = length(fileToLoad.calibrationMS); + % Loop through all calibration files and obtain the compositions + % for every possible calibration + moleFracTemp = zeros(length(reconciledData.MS(:,2)),numCalibrationFiles); + for ii = 1:numCalibrationFiles + % MS Calibration File + load([fileToLoad.calibrationMS{ii},'_Model']); + % Convert the raw signal to concentration + % Parse out the fitting parameters + paramFit = calibrationMS.ratioHeCO2; + % Use a fourier series model to obtain the mole fraction + 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(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 new file mode 100644 index 0000000..9d787c2 --- /dev/null +++ b/experimental/analyzeExperimentWrapper.m @@ -0,0 +1,137 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Ashwin Kumar Rajagopalan (AK) +% Hassan Azzan (HA) +% +% Purpose: +% A wrapper function that generates the calibration data to be used for +% experiments and subsequently analyze experimental data using calibrated +% MS model to generate the response curve from a "ZLC/Breakthrough" +% 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 +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% List the flow meter calibration file (this is usually in calibraiton folder) +flowMeterCalibration = 'ZLCCalibrateMeters_20210419_Model'; + +%%%% 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_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_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_Exp23'; % Raw MS data file name +% Flow rate files for experiments +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; +msRawFileALL={}; +% Loop over the number of experiments per calibration ms raw file +% Generate the name of the MS file that will be used for each flow rate +% .mat file +for ii = 1:length(numExpForEachRawFile) + for jj = startInd:startInd+numExpForEachRawFile(ii)-1 + msRawFileALL{jj} = msRawFiles{ii}; + end + startInd = length(msRawFileALL) + 1; +end + +% % Loop through all the MS calibration files +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 = 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 + % Syntax: analyzeExperiment(experimentStruct,calibrationMode,calibrationFlowMeter) + analyzeExperiment(calibrationStruct,true,false); % Calibrate MS +end + +% Loop through all the experimental files +if ~isempty(experimentFiles) + for ii = 1:length(experimentFiles) + experimentStruct.calibrationFlow = flowMeterCalibration; % Calibration file for meters (.mat) + 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 = false; % Flag for interpolating flow data, to have a higher resolution for actual experiments + 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 + % 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',... + '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','b'); + hold on + box on;grid on; + 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,10]); ylim([0,1]); + 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/auxillaryEquipments/checkGasName.m b/experimental/auxillaryEquipments/checkGasName.m new file mode 100644 index 0000000..a826fa7 --- /dev/null +++ b/experimental/auxillaryEquipments/checkGasName.m @@ -0,0 +1,38 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% Function to generate the gas ID required for alicat devices +% +% Last modified: +% - 2021-03-01, HA: Initial creation +% +% Input arguments: +% - Gas name : Name of the gas used in Alicat equipment (CO2, He, CH4, H2, N2) +% +% Output arguments: +% - gasID : ID of the gas needed for 'controlAuxiliaryEquipments.m' +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function gasID = checkGasName(gasName) +% 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..97a49bc --- /dev/null +++ b/experimental/auxillaryEquipments/checkManufacturer.m @@ -0,0 +1,34 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% 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 +% +% Input arguments: +% - portProperty : Structure containing the properties of the comms +% device +% +% Output arguments: +% - flagAlicat : Determines whether or not the equipment is from Alicat +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function flagAlicat = checkManufacturer(portProperty) +flagAlicat = 0; +switch portProperty.portName + 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 new file mode 100644 index 0000000..1a62192 --- /dev/null +++ b/experimental/auxillaryEquipments/controlAuxiliaryEquipments.m @@ -0,0 +1,61 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% Year: 2021 +% MATLAB: R2020a +% Authors: Hassan Azzan (HA) +% +% Purpose: +% +% Last modified: +% - 2021-03-11, HA: Add UMFM +% - 2021-03-01, HA: Remove gas selection (hard coded) +% - 2021-03-01, HA: Initial creation +% +% Input arguments: +% - portProperty : Structure containing the properties of the comms +% device +% - serialCommand : Command that would be issued to the microcontoller. +% - varargin : Variable arguments for the device type and gas +% +% Output arguments: +% - controllerOutput: variable output from the controller +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +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); + +% If using Alicat (flow meter or controller) +% varargin(1): Device type - Alicat: True +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(1); % Pause to ensure proper read +end + +%% SEND THE COMMAND AND CLOSE THE CONNCETION +% Send command to controller +% 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 +% 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/defineSetPtManual.m b/experimental/auxillaryEquipments/defineSetPtManual.m new file mode 100644 index 0000000..53020b1 --- /dev/null +++ b/experimental/auxillaryEquipments/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/auxillaryEquipments/generateSerialCommand.m b/experimental/auxillaryEquipments/generateSerialCommand.m new file mode 100644 index 0000000..2295734 --- /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 +% - varargin: Arguments to determine the device and the set pt +% +% Output arguments: +% - serialCommand : Command that would be issued to the device +% +% 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, varargin) +% Generate serial command for the device +% If Alicat is used: variable argument 1 is true! +if varargin{1} + switch commandToBeAnalyzed + case 'setPoint' + % Set the set point value for the controller + setPointValue = round(varargin{2},2); + serialCommand = ['as',num2str(setPointValue)]; + case 'pollData' + serialCommand = 'a??d'; + end +end +end \ No newline at end of file diff --git a/experimental/auxillaryFunctions/getGitCommit.m b/experimental/auxillaryFunctions/getGitCommit.m new file mode 100644 index 0000000..bec7dff --- /dev/null +++ b/experimental/auxillaryFunctions/getGitCommit.m @@ -0,0 +1,50 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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. + +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 + 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/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/calibrationFunctions/calibrateMS.m b/experimental/calibrationFunctions/calibrateMS.m new file mode 100644 index 0000000..d9c042e --- /dev/null +++ b/experimental/calibrationFunctions/calibrateMS.m @@ -0,0 +1,71 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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-04-30, AK: Add flow rate sweep +% - 2021-04-15, AK: Modify function for mixture experiments +% - 2021-03-16, AK: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function calibrateMS(varargin) + % 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 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) + 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 + 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); + expInfo.MFC2_SP = MFC2_SP(ii); + % 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 = 300; + else + % Else use 5 min + expInfo.maxTime = 300; + end + % Run the setup for different calibrations + runZLC(expInfo) + end +end diff --git a/experimental/calibrationFunctions/calibrateMeters.m b/experimental/calibrationFunctions/calibrateMeters.m new file mode 100644 index 0000000..d22386d --- /dev/null +++ b/experimental/calibrationFunctions/calibrateMeters.m @@ -0,0 +1,71 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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-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 +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function calibrateMeters + % Experiment name + expInfo.expName = ['ZLCCalibrateMeters','_',... + datestr(datetime('now'),'yyyymmdd')]; + % Maximum time of the experiment + expInfo.maxTime = 30; + % Sampling time for the device + expInfo.samplingTime = 5; + % Define gas for MFM + expInfo.gasName_MFM = 'He'; + % Define gas for MFC1 + 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 + % 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 + % 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 + 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(totalFlowRate) + for jj=1:length(moleFracCO2) + % Set point for MFC1 + 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 + runZLC(expInfo) + end + end +end diff --git a/experimental/calibrationFunctions/runMultipleMSCalibration.m b/experimental/calibrationFunctions/runMultipleMSCalibration.m new file mode 100644 index 0000000..a8d06f6 --- /dev/null +++ b/experimental/calibrationFunctions/runMultipleMSCalibration.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: +% 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 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. 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) + % Change the flow rate of CO2 to 0 and of He to the next set point to + % equilibrate + if iinp.median(sortedData[:,0]))[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]) + 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 (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]))] + + # 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)) + # 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 + # 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 diff --git a/experimental/deadVolumeWrapper.py b/experimental/deadVolumeWrapper.py new file mode 100644 index 0000000..8b96932 --- /dev/null +++ b/experimental/deadVolumeWrapper.py @@ -0,0 +1,85 @@ +############################################################################ +# +# 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-29, AK: Add optional arguments for combind model +# - 2021-05-28, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +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], + deadVolume_2M = DV_p[1], + deadVolume_2D = DV_p[2], + numTanks_1 = int(DV_p[3]), + flowRate_2D = DV_p[4], + initMoleFrac = initMoleFrac, + feedMoleFrac = feedMoleFrac, + 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_2D = 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 new file mode 100644 index 0000000..b5cc4fb --- /dev/null +++ b/experimental/extractDeadVolume.py @@ -0,0 +1,296 @@ +############################################################################ +# +# 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 +# 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-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 +# - 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 +# - 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 +# - 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 +# - 2021-03-25, AK: Estimate parameters using experimental data +# - 2021-03-17, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def extractDeadVolume(**kwargs): + 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 + + # 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() + + # 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 = 5 + + # Directory of raw data + mainDir = 'runData' + # File name of the experiments + 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 = 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 = 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_20210612_2100_8313a04.npz' + + # Downsample the data at different compositions (this is done on + # normalized data) + downsampleData = True + + ##################################### + ##################################### + + # Save the parameters to be used for fitting to a dummy file (to pass + # through GA - IDIOTIC) + savez ('tempFittingParametersDV.npz', + downsampleData=downsampleData, flagMSFit = flagMSFit, msFlowRate = msFlowRate, + flagMSDeadVolume = flagMSDeadVolume, msDeadVolumeFile = msDeadVolumeFile) + + # 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 + # 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':30, + 'population_size':200, + 'mutation_probability':0.25, + '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 = deadVolObjectiveFunction, dimension=len(optType), + variable_type_mixed = optType, + variable_boundaries = optBounds, + algorithm_parameters=algorithm_param, + function_timeout = 300) + + # 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) + + # 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 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')) + + # Save the output into a .npz file + 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 + 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 + downsampleFlag = downsampleData, # Flag for downsampling data [-] + 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 + +# func: deadVolObjectiveFunction +# For use with GA, the function accepts only one input (parameters from the +# optimizer) +def deadVolObjectiveFunction(x): + import numpy as np + 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 + # file + downsampleData = load ('tempFittingParametersDV.npz')["downsampleData"] + 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') + + 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 # Total error + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + expVolume = 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]))] + + # 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(flowRateDV, timeElapsedExp))]) + + # 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 + # 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 - 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 + penaltyObj = 0 + 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 (using MLE) + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData) + return computedError + penaltyObj + +# func: filesToProcess +# Loads .mat experimental file and processes it for python +def filesToProcess(initFlag,mainDir,fileName,expType): + 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 + 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 + 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 new file mode 100644 index 0000000..286d5bf --- /dev/null +++ b/experimental/extractZLCParameters.py @@ -0,0 +1,360 @@ +############################################################################ +# +# 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 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 +# - 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 +# - 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) +# - 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 +# - 2021-04-27, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +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 + from numpy import load + 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") + + # 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 ###### + # If not passed to the function, default values used + # Isotherm model type + if 'modelType' in kwargs: + modelType = kwargs["modelType"] + else: + modelType = 'SSL' + + # Number of times optimization repeated + numOptRepeat = 5 + + # Directory of raw data + mainDir = 'runData' + # File name of the experiments + 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) + if 'temperature' in kwargs: + temperature = kwargs["temperature"] + else: + temperature = [306.47,317.18,339.14] + + # Dead volume model + deadVolumeFile = 'deadVolumeCharacteristics_20210613_0847_8313a04.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 + adsorbentDensity = 2000 # Activated carbon skeletal density [kg/m3] + # Particle porosity + particleEpsilon = 0.61 + # Particle mass [g] + massSorbent = 0.0625 + + # Downsample the data at different compositions (this is done on + # normalized data) + downsampleData = True + + # Confidence interval for the sensitivity analysis + alpha = 0.95 + + ##################################### + ##################################### + + # 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 + # 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], + [np.finfo(float).eps,1])) + optType=np.array(['real','real','real','real','real']) + problemDimension = len(optType) + 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 + + # Dual-site Langmuir + elif modelType == 'DSL': + 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], [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, 1000, 1000] # 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 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 = [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"] + 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,temperature,deadVolumeFile,adsorbentDensity,particleEpsilon, + massSorbent,isoRef,downsampleData,paramIso) + + # Algorithm parameters for GA + algorithm_param = {'max_num_iteration':15, + 'population_size':400, + 'mutation_probability':0.25, + 'crossover_probability': 0.55, + 'parents_portion': 0.15, + 'elit_ratio': 0.01, + 'max_iteration_without_improv':None} + + # 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, + 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, + 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 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 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')) + + # Save the output into a .npz file + 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 + 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] + particleEpsilon = particleEpsilon, # Particle voidage [-] + massSorbent = massSorbent, # Mass of sorbent [g] + parameterReference = isoRef, # Parameter references [-] + downsampleFlag = downsampleData, # Flag for downsampling data [-] + 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]) + + # Perform the sensitivity analysis with the optimized parameters + sensitivityAnalysis(saveFileName, alpha) + + # 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 + from computeMLEError import computeMLEError + + # Get the zlc parameters needed for the solver + temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, paramIso = fittingParameters(False,[],[],[],[],[],[],[],[]) + + # Volume of sorbent material [m3] + volSorbent = (massSorbent/1000)/adsorbentDensity + # Volume of gas in pores [m3] + volGas = volSorbent/(1-particleEpsilon)*particleEpsilon + + # Prepare isotherm model (the first n-1 parameters are for the isotherm model) + if len(paramIso) != 0: + isothermModel = paramIso[0:-2] # Use this if isotherm parameter provided (for kinetics only) + else: + 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') + + # 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 + moleFracExpALL = np.array([]) + moleFracSimALL = np.array([]) + + # 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]))] # [cc/s] + + # Integration and ode evaluation time (check simulateZLC/simulateDeadVolume) + timeInt = timeElapsedExp + + # Compute the composite response using the optimizer parameters + _ , moleFracSim , _ = simulateCombinedModel(isothermModel = isothermModel, + 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 + 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 = str(deadVolumeFile), + volSorbent = volSorbent, + volGas = volGas, + adsorbentDensity = adsorbentDensity) + + # 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) + 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)) + + # Compute the sum of the error for the difference between exp. and sim. + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData) + 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,downsampleData, + 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, temperature = temperature, + deadVolumeFile = deadVolumeFile, + adsorbentDensity=adsorbentDensity, + particleEpsilon=particleEpsilon, + massSorbent=massSorbent, + isoRef=isoRef, + downsampleData=downsampleData, + paramIso = paramIso) + # Returns the path of the .npz file to be used + else: + # 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"] + massSorbent = load (dummyFileName)["massSorbent"] + isoRef = load (dummyFileName)["isoRef"] + downsampleData = load (dummyFileName)["downsampleData"] + paramIso = load (dummyFileName)["paramIso"] + return temperature, deadVolumeFile, adsorbentDensity, particleEpsilon, massSorbent, isoRef, downsampleData, paramIso \ No newline at end of file diff --git a/experimental/generateExperimentalResponse.py b/experimental/generateExperimentalResponse.py new file mode 100644 index 0000000..25eae04 --- /dev/null +++ b/experimental/generateExperimentalResponse.py @@ -0,0 +1,187 @@ +############################################################################ +# +# 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-23, AK: Structure changes to reflect new kinetics +# - 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, kinetic rate constants, sorbent mass, density, +# and poroisty +# Note that the material isotherm parameters is obtained from the Quantachrome +# measurements +#### 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 = [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] +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.73])) + +# Dead volume file for the setup +deadVolumeFile = 'deadVolumeCharacteristics_20210810_1653_eddec53.npz' + +############ + +# 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 = 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(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}) + + # 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/processExpMatFile.py b/experimental/processExpMatFile.py new file mode 100644 index 0000000..4d5b121 --- /dev/null +++ b/experimental/processExpMatFile.py @@ -0,0 +1,59 @@ +############################################################################ +# +# 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: Make it a function +# - 2021-03-24, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +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 = os.path.join(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/runMultipleZLC.m b/experimental/runMultipleZLC.m new file mode 100644 index 0000000..f5607ee --- /dev/null +++ b/experimental/runMultipleZLC.m @@ -0,0 +1,84 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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-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 +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function runMultipleZLC + run('C:\Users\QCPML\Desktop\Ashwin\ERASE\setPathERASE.m') + % Series name for the experiments + expSeries = {'ZLC_Zeolite13X_Exp27',... + 'ZLC_Zeolite13X_Exp28'}; + % Maximum time of the experiment + expInfo.maxTime = 300; + % Sampling time for the device + expInfo.samplingTime = 1; + % Intervals for collecting MFC data + expInfo.MFCInterval = 100; + % 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 = [10, 10; + 60, 60]; + % Fraction CO2 + 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) + MFC1_SP = round(expTotalFlowRate,1); + % 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); + % Start delay (used for adsorbent equilibration) + 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 + expInfo.runMixtures = true; + % 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); + 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 diff --git a/experimental/runZLC.m b/experimental/runZLC.m new file mode 100644 index 0000000..d06ea8a --- /dev/null +++ b/experimental/runZLC.m @@ -0,0 +1,345 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Imperial College London, United Kingdom +% Multifunctional Nanomaterials Laboratory +% +% Project: ERASE +% 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-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 +% - 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 +% - 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 +% - 2021-03-11, HA: Add data logger, set points, and refine code +% - 2021-03-10, HA: Initial creation +% +% Input arguments: +% +% Output arguments: +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +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 = 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 + expInfo.gasName_MFC1 = 'He'; + % Define gas for MFC2 + expInfo.gasName_MFC2 = 'CO2'; + % Define set point for MFC1 + 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'); + 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({'FT4U1GABA'}); + if ~isempty(portText{1}) + [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}) + [startInd, stopInd] = regexp(portText{1},'COM(\d+)'); + portMFC1 = portText{1}(startInd(1):stopInd(1)); + end + % Find COM port for MFC2 + portText = matchUSBport({'FT1EQDD6A'}); + if ~isempty(portText{1}) + [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}) + [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'); + 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); + + %% Initialize timer + timerDevice = timer; + timerDevice.ExecutionMode = 'fixedRate'; + timerDevice.BusyMode = 'drop'; + timerDevice.Period = expInfo.samplingTime; % [s] + timerDevice.StartDelay = expInfo.equilibrationTime; % [s] + timerDevice.TasksToExecute = floor((expInfo.maxTime)/expInfo.samplingTime); + + % Specify timer callbacks + timerDevice.StartFcn = {@initializeTimerDevice,expInfo,serialObj}; + timerDevice.TimerFcn = {@executeTimerDevice, expInfo, 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) + % Block the command line + wait(timerDevice) + + % Load the experimental data and add a few more things + % Load the output .mat file + load(['experimentalData',filesep,expInfo.expName]) + % Get the git commit ID + gitCommitID = getGitCommit; + % Load the output .mat file + save(['experimentalData',filesep,expInfo.expName],... + 'gitCommitID','outputStruct','expInfo') +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 + % MFM + if ~isempty(serialObj.MFM.portName) + [~] = controlAuxiliaryEquipments(serialObj.MFM, gasID_MFM,1); % Set gas for MFM + end + % 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 + [~] = 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 + % 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) + if round(str2double(outputMFC1Temp(6)),1) ~= round(expInfo.MFC1_SP,1) + error("You should not be here!!!") + end + end + % 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 + [~] = 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 + % 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 + % 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!!']) +end +%% executeTimerDevice: Execute function for the timer at each instant +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 && ~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 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 + 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 + end + % Get the sampling date/time + 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) + 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 + % 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) && flagCollect + 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.portName) && flagCollect + 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 = 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) + 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(~, expInfo, currentDateTime, ... + MFM, MFC1, MFC2, UMFM) +% 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,... + '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(['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 diff --git a/experimental/sensitivityAnalysis.py b/experimental/sensitivityAnalysis.py new file mode 100644 index 0000000..83102ec --- /dev/null +++ b/experimental/sensitivityAnalysis.py @@ -0,0 +1,267 @@ +############################################################################ +# +# 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-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 +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +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 + from numpy.linalg import multi_dot # Performs (multiple) matrix multiplication + from numpy.linalg import inv + import os + from scipy.stats import chi2 + import socket + + # Get the commit ID of the current repository + gitCommitID = auxiliaryFunctions.getCommitID() + + # Directory of raw data + mainDir = 'runData' + # File with parameter estimates + simulationDir = os.path.join('..','simulationResults') + # ZLC parameter path + 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"] + 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:-8] + "_" + 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 + + # 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 +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"] + + # 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_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_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, + 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) + + # Return the objective function value, experimental and simulated output + return computedError, moleFracExpALL, moleFracSimALL \ No newline at end of file diff --git a/experimental/simulateCombinedModel.py b/experimental/simulateCombinedModel.py new file mode 100644 index 0000000..9fdce71 --- /dev/null +++ b/experimental/simulateCombinedModel.py @@ -0,0 +1,216 @@ +############################################################################ +# +# 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-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 +# - 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 +# +# Input arguments: +# +# +# Output arguments: +# +############################################################################ + +def simulateCombinedModel(**kwargs): + from simulateZLC import simulateZLC + 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("..") + 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: + 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] + + # Kinetic rate constant 1 (analogous to micropore resistance) [/s] + if 'rateConstant_1' in kwargs: + rateConstant_1 = kwargs["rateConstant_1"] + else: + rateConstant_1 = [0.3] + + # Kinetic rate constant 2 (analogous to macropore resistance) [/s] + if 'rateConstant_2' in kwargs: + rateConstant_2 = np.array(kwargs["rateConstant_2"]) + else: + rateConstant_2 = np.array([0]) + + # 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: + flowIn = kwargs["flowIn"] + else: + flowIn = [5e-7] + + # Initial Gas Mole Fraction [-] + if 'initMoleFrac' in 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) + + # 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 = 2000 # 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"] + else: + expFlag = False + + # Call the simulateZLC function to simulate the sorption in a given sorbent + timeZLC, resultMat, _ = simulateZLC(isothermModel = isothermModel, + rateConstant_1 = rateConstant_1, + rateConstant_2 = rateConstant_2, + temperature = temperature, + flowIn = flowIn, + initMoleFrac = initMoleFrac, + timeInt = timeInt, + expFlag=expFlag, + volSorbent = volSorbent, + volGas = volGas, + adsorbentDensity = adsorbentDensity) + + # 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"] + + # 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: + 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 + 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.]) + ax3.locator_params(axis="x", nbins=4) + ax3.legend() + plt.show() + os.chdir(".."+os.path.sep+"experimental") diff --git a/experimental/simulateDeadVolume.py b/experimental/simulateDeadVolume.py new file mode 100644 index 0000000..bf8e86d --- /dev/null +++ b/experimental/simulateDeadVolume.py @@ -0,0 +1,241 @@ +############################################################################ +# +# 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 +# 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-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 +# - 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 +# - 2021-03-25, AK: Fix for plot +# - 2021-03-18, AK: Fix for inlet concentration +# - 2021-03-17, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def simulateDeadVolume(**kwargs): + import numpy as np + from scipy.integrate import solve_ivp + import os + + # Move to top level folder (to avoid path issues) + os.chdir("..") + + # Plot flag + plotFlag = False + + # Flow rate of the gas [cc/s] + if 'flowRate' in kwargs: + flowRate = kwargs["flowRate"] + else: + flowRate = np.array([0.25]) + # Dead Volume of the first volume [cc] + if 'deadVolume_1' in kwargs: + deadVolume_1 = kwargs["deadVolume_1"] + else: + deadVolume_1 = 4.25 + # Number of tanks of the first volume [-] + if 'numTanks_1' in kwargs: + numTanks_1 = kwargs["numTanks_1"] + else: + 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.59 + # Dead Volume of the second volume (diffusive) [cc] + if 'deadVolume_2D' in kwargs: + deadVolume_2D = kwargs["deadVolume_2D"] + else: + deadVolume_2D = 5.93e-1 + # Flow rate in the diffusive volume [-] + if 'flowRate_2D' in kwargs: + flowRate_2D = kwargs["flowRate_2D"] + else: + flowRate_2D = 1.35e-2 + # 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"]) + else: + feedMoleFrac = np.array([0.]) + # Time span for integration [tuple with t0 and tf] + if 'timeInt' in kwargs: + timeInt = kwargs["timeInt"] + 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 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)) + + # Prepare tuple of input parameters for the ode solver + inputParameters = (t_eval,flowRate, deadVolume_1,deadVolume_2M, + deadVolume_2D, numTanks_1, flowRate_2D, + feedMoleFrac) + + # Total number of tanks[-] + numTanksTotal = numTanks_1 + 2 + + # Prepare initial conditions vector + # The first element is the inlet composition and the rest is the dead + # volume + initialConditions = np.ones([numTanksTotal])*initMoleFrac + # Solve the system of equations + outputSol = solve_ivp(solveTanksInSeries, timeInt, initialConditions, + method='Radau', t_eval = t_eval, + rtol = 1e-8, args = inputParameters) + + # Parse out the time + timeSim = outputSol.t + + # Inlet concentration + moleFracIn = np.ones((len(outputSol.t),1))*feedMoleFrac + + # Mole fraction at the outlet + # Mixing volume + moleFracMix = outputSol.y[numTanks_1] + # Diffusive volume + moleFracDiff = outputSol.y[-1] + + # Composition after mixing + flowRate_M = flowRate - flowRate_2D + moleFracOut = np.divide(np.multiply(flowRate_M,moleFracMix) + + np.multiply(flowRate_2D,moleFracDiff),flowRate) + + # 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 + +# 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 + 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_2D, feedMoleFracALL = inputParameters + + # Check if experimental data available + # 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) + flowRate = interpFlow(t) + 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_1 + 2 + + # Initialize the derivatives to zero + df = np.zeros([numTanksTotal]) + + # 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_2D + residenceTime_2M = volTank_2M/(flowRate_M) + residenceTime_2D = volTank_2D/(flowRate_2D) + + # Solve the odes + # 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 + +# 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("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.semilogy(timeSim, moleFracOut, + linewidth=1.5,color='r', + label = 'Out') + ax.set(xlabel='$t$ [s]', + ylabel='$y$ [-]', + xlim = [timeSim[0], 1000], ylim = [1e-4, 1.1*np.max(moleFracOut)]) + ax.legend() + plt.show() + os.chdir("..") \ No newline at end of file diff --git a/experimental/simulateZLC.py b/experimental/simulateZLC.py new file mode 100644 index 0000000..de13623 --- /dev/null +++ b/experimental/simulateZLC.py @@ -0,0 +1,326 @@ +############################################################################ +# +# 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. 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-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 +# - 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 +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +def simulateZLC(**kwargs): + import numpy as np + from scipy.integrate import solve_ivp + from computeEquilibriumLoading import computeEquilibriumLoading + 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() + + # Kinetic rate constant 1 (analogous to micropore resistance) [/s] + if 'rateConstant_1' in kwargs: + rateConstant_1 = np.array(kwargs["rateConstant_1"]) + else: + rateConstant_1 = np.array([0.1]) + + # Kinetic rate constant 2 (analogous to macropore resistance) [/s] + if 'rateConstant_2' in kwargs: + rateConstant_2 = np.array(kwargs["rateConstant_2"]) + else: + rateConstant_2 = np.array([0]) + + # 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([0.]) + + # Initial Gas Mole Fraction [-] + if 'initMoleFrac' in kwargs: + initMoleFrac = np.array(kwargs["initMoleFrac"]) + else: + # Equilibrium process + initMoleFrac = np.array([1.]) + + # Time span for integration [tuple with t0 and tf] + if 'timeInt' in 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.2) + 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: + 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 + + # 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] + # This has to be the skeletal density + if 'adsorbentDensity' in kwargs: + adsorbentDensity = kwargs["adsorbentDensity"] + else: + adsorbentDensity = 2000 # Activated carbon skeletal density [kg/m3] + + # 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 + equilibriumLoading = computeEquilibriumLoading(pressureTotal=pressureTotal, + temperature=temperature, + moleFrac=initMoleFrac, + isothermModel=isothermModel)*adsorbentDensity # [mol/m3] + + # Prepare tuple of input parameters for the ode solver + inputParameters = (adsorbentDensity, isothermModel, rateConstant_1, rateConstant_2, + 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 0.1 s + # Solves the model assuming constant/negligible pressure across the sensor + # Prepare initial conditions vector + 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 = t_eval, + rtol = 1e-8, args = inputParameters) + + # Presure vector in output + pressureVec = pressureTotal * np.ones(len(outputSol.t)) # Constant pressure + + # Compute the outlet flow rate + 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 + resultMat = np.row_stack((outputSol.y,pressureVec,flowOut)) + + # Parse out the time + timeSim = outputSol.t + + # Call the plotting function + if plotFlag: + 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 + +# func: solveSorptionEquation - Constant pressure model +# Solves the system of ODEs to evaluate the gas composition and loadings +def solveSorptionEquation(t, f, *inputParameters): + import numpy as np + from computeEquilibriumLoading import computeEquilibriumLoading + + # Gas constant + Rg = 8.314; # [J/mol K] + + # Unpack the tuple of input parameters used to solve equations + adsorbentDensity, isothermModel, rateConstant_1, rateConstant_2, flowIn, feedMoleFrac, _ , pressureTotal, temperature, volSorbent, volGas = inputParameters + + # Initialize the derivatives to zero + df = np.zeros([2]) + + # 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 (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 + # 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]) + + # Total mass balance + # Assumes constant pressure, so flow rate evalauted + flowOut = flowIn - (volSorbent*(Rg*temperature)/pressureTotal)*df[1] + + # Component mass balance + term1 = 1/volGas + 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, inputParameters, + gitCommitID, currentDT): + import numpy as np + import os + import matplotlib.pyplot as plt + + # Save settings + saveFlag = False + saveFileExtension = ".png" + + # Unpack the tuple of input parameters used to solve equations + adsorbentDensity , _ , _ , _ , flowIn, _ , _ , _ , temperature, _ , _ = inputParameters + + os.chdir("plotFunctions") + # 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 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 index d71b19b..3d8dc87 100644 --- a/plotFunctions/doubleColumn2Row.mplstyle +++ b/plotFunctions/doubleColumn2Row.mplstyle @@ -2,23 +2,24 @@ # https://matplotlib.org/3.3.2/tutorials/introductory/customizing.html ## Figure property -figure.figsize : 7, 6 # width, height in inches +figure.figsize : 7, 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.formatter.limits : -5, 4 +axes.titlesize : 8 +axes.labelsize : 8 +axes.formatter.limits : -5, 5 ## 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/plotExperimentOutcome.py b/plotFunctions/plotExperimentOutcome.py new file mode 100644 index 0000000..6b1bd4e --- /dev/null +++ b/plotFunctions/plotExperimentOutcome.py @@ -0,0 +1,540 @@ +############################################################################ +# +# 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-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 +# - 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 +# - 2021-04-20, AK: Implement time-resolved experimental flow rate for DV +# - 2021-04-16, AK: Initial creation +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from computeMLEError import computeMLEError +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() + +# Save flag for plot +saveFlag = False + +# Save file extension +saveFileExtension = ".png" + +# File with parameter estimates +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 +if fileParameter[0:10] == 'deadVolume': + flagDeadVolume = True +else: + flagDeadVolume = False + +# Flag to plot simulations +simulateModel = True + +# Flag to plot dead volume results +plotFt = False + +# Total pressure of the gas [Pa] +pressureTotal = np.array([1.e5]); + +# Plot colors +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_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) + + # Generate .npz file for python processing of the .mat file + filesToProcess(True,os.path.join('..','experimental','runData'),rawFileName,'DV') + # Get the processed file names + fileName = filesToProcess(False,[],[],'DV') + # 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(parameterPath)["downsampleFlag"] + + # 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("Objective Function",round(modelOutputTemp[()]["function"],0)) + 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,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 = 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 + + # Print experimental volume + print("Experiment",str(ii+1),round(np.trapz(moleFracExp, + np.multiply(flowRateExp,timeElapsedExp)),2)) + 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, + 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 - 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: + # 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 + if simulateModel: + ax1.plot(timeElapsedExp,moleFracSim, + color=colorsForPlot[ii]) # Simulation response + ax1.set(xlabel='$t$ [s]', + ylabel='$y_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 + 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 + if simulateModel: + 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) + + + # Save the figure + if saveFlag: + # FileName: deadVolumeCharacteristics___ + saveFileName = "deadVolumeCharacteristics_" + currentDT + "_" + gitCommitID + "_" + fileParameter[-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) + else: + # Linear scale + ax1.plot(np.multiply(flowRateDV,timeElapsedExp),moleFracExp, + marker = markerForPlot[ii],linewidth = 0, + 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 + ax1.set(xlabel='$Ft$ [cc]', + ylabel='$y_1$ [-]', + xlim = [0,0.1], ylim = [0, 1]) + ax1.legend() + + # Log scale + ax2.semilogy(np.multiply(flowRateDV,timeElapsedExp),moleFracExp, + marker = markerForPlot[ii],linewidth = 0, + 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 + ax2.set(xlabel='$Ft$ [cc]', + xlim = [0,0.1], ylim = [1e-3, 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 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() + # Print the MLE error + if simulateModel: + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + downsampleData=downsampleData,) + print("Sanity check objective function: ",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 + + # File name of the experiments + 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',] + + # 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 # 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 + + # 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') + # 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"] + + # This was added on 12.06 (not back compatible for error computation) + downsampleData = load(parameterPath)["downsampleFlag"] + print("Objective Function",round(modelOutputTemp[()]["function"],0)) + + 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) + # 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),3)) + + # 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 = 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 + + # Print experimental volume + print("Experiment",str(ii+1),round(np.trapz(np.multiply(flowRateExp,moleFracExp),timeElapsedExp),2)) + + if simulateModel: + # 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)) + + # 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) + - volGas*moleFracExp[0])*(pressureTotal/(8.314*temperatureExp[ii]))/(massSorbent/1000)) + massBalanceALL[ii,2] = volGas*moleFracExp[0]*(pressureTotal/(8.314*temperatureExp[ii]))/(massSorbent/1000) + + # 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]]) + + # y - Linear scale + ax1.semilogy(timeElapsedExp,moleFracExp, + marker = markerForPlot[ii],linewidth = 0, + color=colorsForPlot[ii],alpha=0.1) # Experimental response + if simulateModel: + if useFlow: + legendStr = str(round(np.mean(flowRateExp),2))+" ccs" + else: + legendStr = str(temperatureExp[ii])+" K" + ax1.plot(timeElapsedExp,moleFracSim, + color=colorsForPlot[ii],label=legendStr) # Simulation response + # if ii==len(fileName)-1: + # ax1.plot(timeElapsedExp,moleFracDV, + # color='#118ab2',label="DV",alpha=0.025, + # linestyle = '-') # Dead volume simulation response + + ax1.set(xlabel='$t$ [s]', + ylabel='$y_1$ [-]', + xlim = [0,200], ylim = [1e-2, 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.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 + ax2.set(xlabel='$Ft$ [cc]', + xlim = [0,60], ylim = [1e-2, 1]) + ax2.locator_params(axis="x", nbins=4) + + # Flow rates + ax3.plot(timeElapsedExp,flowRateExp, + marker = markerForPlot[ii],linewidth = 0, + 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 + ax3.set(xlabel='$t$ [s]', + ylabel='$F$ [ccs]', + xlim = [0,250], ylim = [0, 1.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 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() + + # Print the MLE error + if simulateModel: + computedError = computeMLEError(moleFracExpALL,moleFracSimALL, + 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)): + os.remove(fileName[ii]) \ No newline at end of file diff --git a/plotFunctions/plotIsothermComparisonMultiParam.py b/plotFunctions/plotIsothermComparisonMultiParam.py new file mode 100644 index 0000000..9875ec0 --- /dev/null +++ b/plotFunctions/plotIsothermComparisonMultiParam.py @@ -0,0 +1,325 @@ +############################################################################ +# +# 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-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 +# +# Input arguments: +# +# +# Output arguments: +# +# +############################################################################ + +import numpy as np +from computeEquilibriumLoading import computeEquilibriumLoading +import matplotlib.pyplot as plt +from matplotlib.ticker import MaxNLocator +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 = ["faa307","d00000","03071e"] +# colorForPlot = ["E5383B","6C757D"] + +# Plot text +plotText = 'DSL' + +# Universal gas constant +Rg = 8.314 + +# Total pressure +pressureTotal = np.array([1.e5]); + +# 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] # (Hassan, QC) + +# 13X Isotherm parameters (L pellet) +# 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] # (Hassan, QC) + +# ZLC Parameter estimates +# New kinetic model +# 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'] + +# 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) +# Initialize isotherms +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, + moleFrac = y[ii], + temperature = temperature[jj]) +# Loop over all available ZLC files +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) + 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) + + # 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 + + # # 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) + +# Plot the isotherms +fig = plt.figure +ax1 = plt.subplot(1,2,1) +for jj in range(len(temperature)): + ax1.plot(y,isoLoading_VOL[:,jj],color='#'+colorForPlot[jj],label=str(temperature[jj])+' K') # Ronny's isotherm + for kk in range(len(zlcFileName)): + ax1.plot(y,isoLoading_ZLC[kk,:,jj],color='#'+colorForPlot[jj],alpha=0.2) # ALL + +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() + +# Plot the objective function +fig = plt.figure +ax2 = plt.subplot(1,2,2) +for kk in range(len(zlcFileName)): + ax2.scatter(kk+1,objectiveFunction[kk]) # ALL + +ax2.set(xlabel='Iteration [-]', +ylabel='$J$ [-]', +xlim = [0,len(zlcFileName)]) +ax2.locator_params(axis="y", nbins=4) +ax2.xaxis.set_major_locator(MaxNLocator(integer=True)) +ax2.locator_params(axis="x", nbins=4) +ax2.yaxis.set_major_locator(MaxNLocator(integer=True)) +ax2.legend() + +# Save the figure +if saveFlag: + # FileName: isothermComparison___ + 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() + +# 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_Macro[kk,:,jj],color='#'+colorForPlot[jj]) # Macropore resistance + ax1.plot(y,kineticConstant_ZLC[kk,:,jj],color='#'+colorForPlot[jj],alpha=0.2, + label=labelText) # ALL + +ax1.set(xlabel='$P$ [bar]', +ylabel='$k$ [s$^\mathregular{-1}$]', +xlim = [0,1], ylim = [0, 1]) +ax1.locator_params(axis="x", nbins=4) +ax1.locator_params(axis="y", nbins=4) +ax1.legend() \ No newline at end of file diff --git a/plotFunctions/plotsForArticle_Experiment.py b/plotFunctions/plotsForArticle_Experiment.py new file mode 100644 index 0000000..33a7b63 --- /dev/null +++ b/plotFunctions/plotsForArticle_Experiment.py @@ -0,0 +1,3826 @@ +############################################################################ +# +# 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: +# - 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 +# - 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) +# - 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 material characterization 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) + + # 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) + + # If ZLC fits needs to be plotted + if 'figureZLCFit' in kwargs: + if kwargs["figureZLCFit"]: + 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"]: + 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"]: + plotForArticle_figureZLCRep(gitCommitID, currentDT, + saveFlag, saveFileExtension) + + # If ZLC objective functions + if 'figureZLCObj' in 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) + + # 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) + + # 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, + saveFlag, saveFileExtension): + import numpy as np + import matplotlib.pyplot as plt + import auxiliaryFunctions + 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 + + # 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 (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) + 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',] + + # 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]) + + # Get the porosity options + porosityOptions = sio.loadmat(fileToLoad)["poreVolume"]["options"][0][0] + + # Indicies for N2 and MIP data + QCindexLast = porosityOptions["QCindexLast"][0][0][0][0] # Last index for N2 sorption + + # Get the porosity options + combinedPorosityData = sio.loadmat(fileToLoad)["poreVolume"]["combined"][0][0] + + # 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') + + # Set background color for micropore region + 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='#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='#848586', 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], + linewidth = 0.25,linestyle = ':', + 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_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_P[0]) + ax.text(combinedPorosityData[QCindexLast-1,0]+1.2*dx1[kk], 0.15, "N$_2$", fontsize=8, + 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_P[1]) + ax.text(combinedPorosityData[QCindexLast-1,0]+0.25*dx2[kk], 0.77, "Hg", fontsize=8, + color = '#'+colorsForPlot_P[1]) + + # Material specific text labels + if kk == 0: + ax.set(xlabel='$D$ [nm]', + 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.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(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(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)): + # 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])) + # 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]', + 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') + + 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) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + + # 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"] + 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,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))+" cm$^3$ s$^{-1}$") # Experimental response + ax1.semilogy(timeElapsedExp,moleFracSim, + color='#'+colorsForPlot[ii]) # Simulation response + ax1.set(xlabel='$t$ [s]', + 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(8.9, 0.64, "Segment II", fontsize=8, fontweight = 'bold', + backgroundcolor = 'w', color = '#e71d36') + 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: + ax2.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 + 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(57, 0.64, "Segment I w/ Ball", fontsize=8, fontweight = 'bold', + backgroundcolor = 'w', color = '#e71d36') + 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: + ax3.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 + 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(51, 0.64, "Segment I w/o Ball", fontsize=8, fontweight = 'bold', + backgroundcolor = 'w', color = '#e71d36') + ax3.text(75, 0.385, "$V^\mathrm{S_{I}}$ = 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 + for ii in range(len(fileName)): + os.remove(fileName[ii]) + + # 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() + +# 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 + from matplotlib.ticker import FormatStrFormatter + 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 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[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 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[qq,:,jj],color='#'+colorsForPlot[jj],alpha=0.2) # ALL + + if pp == 0: + # Isotherm + 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') + 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\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') + # 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 = '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) + 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 = '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) + 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) + ax1.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) + # 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 + 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.lines import Line2D + 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',]] + + # 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 + 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]) + 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)): + 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) + + # Get the ZLC Isotherms + 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],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 = 1, + label=str(temperature[jj])+' K') # Lowest J + + if pp == 0: + # Isotherm + ax1.set(xlabel='$P$ [bar]', + 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.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() + 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.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() + 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.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___ + 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,bbox_inches='tight') + + plt.show() + +# fun: plotForArticle_figureZLCSim +# Plots the Figure ZLC Sim 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 + from matplotlib.ticker import FormatStrFormatter + 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 = ["0091ad","5c4d7d","b7094c"] + + # 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 + x_SIM = isothermParameters[0:-2] + + # 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 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 qq in range(len(zlcFileName)): + ax2.plot(y,kineticConstant_ZLC[qq,:,jj],color='#'+colorsForPlot[jj],alpha=0.1) # ALL + + if pp == 0: + # Isotherm + 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') + 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([]) + ax1.legend() + # Kinetics + ax2.set(xlabel='$P$ [bar]', + 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') + # 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 = '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([]) + 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 = '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([]) + 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) + ax1.yaxis.set_major_formatter(FormatStrFormatter('%.1f')) + ax2.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) + # 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() + +# fun: plotForArticle_figureZLCFit +# 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 + 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 = [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 = [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 + # 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)): + 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"] + + # 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 = "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]) + +# 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 + 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 = ["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 = [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 + # 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)): + 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"] + + # 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 = [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] + 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='Repetition [-]', + ylabel='$J$ [-]', + xlim = [1,5],ylim = YLIM_H[pp]) + else: + 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)) + + # 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() + +# 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 = ["E4572E","76B041","FFC914"] + 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.2) + + 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) + # 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.2) + + 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) + # Find indices corresponding to each material + for ll in range(3): + + # Plot N2 77 K isotherm data + ax3.semilogx(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 = [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="y", nbins=4) + ax3.text(2e-7, 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 + from matplotlib.pyplot import figure + 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 = ["797d62","d08c60"] + markersForPlot_I = ["^","^","^"] + + # Folder for MS Calibration + mainDir = os.path.join('..','experimental','materialCharacterization') + + + # File with MS data + msDataNEW = ['msData_072621.mat'] + msDataOLD = ['msData_050521.mat'] + + # Create the instance for the plots + fig = figure(figsize=(6,3)) + 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) + + ax1.grid(which='minor', linestyle=':') + ax2.grid(which='minor', linestyle=':') + + # 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() + +# fun: plotForArticle_figureSensitivity +# Plots the Figure Sensitivity of the manuscript: Variable sensitivity analysis +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 + from matplotlib.ticker import FormatStrFormatter + 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', + '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', + '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^*_\mathregular{CO_2}$ [mol kg$^{-1}$]', + xlim = [0,1], ylim = [0, 3]) + ax1.text(0.04, 2.75, "(a)", fontsize=8,) + 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([]) + 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.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) + + # 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.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') + + # Temperature legend + 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___ + 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, bbox_inches = "tight") + + plt.show() + + +# fun: plotForArticle_figureDVSensitivity +# Plots the Figure DV Sensitivity of the manuscript: Dead volume characterization different models +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\mathregular{_{CO_2}}$ [-]', + 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, bbox_inches = "tight") + + 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.5,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', 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)): + 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') + ax1.grid(which='minor', linestyle=':') + # 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') + ax2.grid(which='minor', linestyle=':') + # 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') + ax3.grid(which='minor', linestyle=':') + + # 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() + +# 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 + 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 = ["ffba08","d00000","03071e"] + 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 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_" + 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: 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 = "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')): + 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 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 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