def plotter(caller, dir_path, self, dryf, cdt, sdt, min_size, max_size, csbn, p_rho): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # self - reference to GUI # dryf - whether particles dried (0) or not (1) # cdt - particle number concentration detection limit (particles/cm3) # sdt - particle size at 50 % detection efficiency (nm), # width factor for detection efficiency dependence on particle size # min_size - minimum size measure by counter (nm) # max_size - maximum size measure by counter (nm) # csbn - number of size bins for counter # p_rho - assumed density of particles (g/cm3) # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, Nwet, _, y_MV, _, wall_on, space_mode, indx_plot, comp0, _, PsatPa, OC, H2Oi, _, siz_str, _, _, _, _) = retr_out.retr_out(dir_path) # number of actual particle size bins num_asb = (num_sb-wall_on) if (caller == 0): plt.ion() # show results to screen and turn on interactive mode # prepare figure ------------------------------------------- fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) par1 = ax0.twinx() # first parasite axis par2 = ax0.twinx() # second parasite axis # Offset the right spine of par2. The ticks and label have already been # placed on the right by twinx above. par2.spines["right"].set_position(("axes", 1.2)) # Having been created by twinx, par2 has its frame off, so the line of its # detached spine is invisible. First, activate the frame but make the patch # and spines invisible. make_patch_spines_invisible(par2) # Second, show the right spine. par2.spines["right"].set_visible(True) # --------------------------------------------------------------- # affect whether particle number concentration is for dry or wet inlet to instrument (# particles/cm3) if (dryf == 0): Nuse = Ndry else: Nuse = Nwet # if just one size bin, ensure two dimensional if (num_sb-wall_on) == 1: Nuse = Nuse.reshape(-1, 1) x = x.reshape(-1, 1) # account for minimum particle size detectable by instrument (um) Nuse[x<(min_size*1.e-3)] = 0. # if moving centre used rather than full moving then # change Nuse, x and rbou_rec to a two-point moving average if (siz_str[0] == 0): # two point moving average number concentration Nuse = (Nuse[:, 0:-1]+Nuse[:, 1::])/2. if (space_mode == 'log'): # two point moving average size (radius) bin centres (um) x = 10.**(np.log10(x[:, 0:-1])+(np.log10(x[:, 1::])-np.log10(x[:, 0:-1]))/2.) # two point moving average size (radius) bin bounds (um) rbou_rec = 10.**(np.log10(rbou_rec[:, 0:-1])+(np.log10(rbou_rec[:, 1::])-np.log10(rbou_rec[:, 0:-1]))/2.) # fixed point centre (radius) of size bins (um) xf = 10.**(np.log10(rbou_rec[:, 0:-1])+(np.log10(rbou_rec[:, 1::])-np.log10(rbou_rec[:, 0:-1]))/2.) if (space_mode == 'lin'): # two point moving average size (radius) bin centres (um) x = x[:, 0:-1]+(x[:, 1::]-x[:, 0:-1])/2. # two point moving average size (radius) bin bounds (um) rbou_rec = rbou_rec[:, 0:-1]+(rbou_rec[:, 1::]-rbou_rec[:, 0:-1])/2. # fixed point centre (radius) of size bins (um) xf = rbou_rec[:, 0:-1]+(rbou_rec[:, 1::]-rbou_rec[:, 0:-1])/2. else: # if using full-moving then set the size bin centre radius (um) as the known xf = x # difference in the log10 of size (diameter) bin widths of model (um) # (could vary with time depending on size structure) sbwm = (np.log10(rbou_rec[:, 1::]*2.))-(np.log10(rbou_rec[:, 0:-1]*2.)) # normalise number concentration by size (diameter) bin width (# particles/cm3/um) Nuse = Nuse/sbwm # interpolate particle number concentration to the counter size bins -------------------- Nint = np.zeros((len(timehr), csbn)) # empty array to hold interpolated results (#/m3) # size bin bounds of SMPS (diameter) (um) csbb = (np.logspace(np.log10(min_size), np.log10(max_size), csbn+1, base = 10.))*1.e-3 # size bin centres of instrument (diameter) (um) csbc = (10.**(np.log10(csbb[0:-1])+(np.log10(csbb[1::])-np.log10(csbb[0:-1]))/2.)) # difference in log10 of size bins (diameter) bin widths of instrument (um) sbwc = np.log10(csbb[1::])-np.log10(csbb[0:-1]) # interpolation based on fixed size bin centres - note that this is preferred over changeable # size bin centres when calculating number size distribution, total particle number # concentration and total mass concentration, since the area under the # number vs size line is comparable between times with changeable size bin centres, note when # this is not the case, even when a particle grows when using moving centre size structure # the area beneath the curve may decrease because it becomes more jagged, thereby # unrealistically affecting particle number concentration during interpolation for ti in range(len(timehr)): Nint[ti, :] = np.interp(csbc, xf[ti, :]*2., Nuse[ti, :]) # (# particles/cm3/difference in log 10(size(um))) Nint[ti, :] = Nint[ti, :]*sbwc # correct for size bin width (# particles/cm3) # account for minimum detectable particle concentration (# particles/cm3) Nint[Nint<cdt] = 0. # get detection efficiency as a function of particle size (nm) [Dp, ce] = count_eff_plot(3, 0, self, sdt) # interpolate detection efficiency (fraction) to instrument size bin centres ce = np.interp(csbc, Dp, ce) Nint = Nint*ce # correct for detection efficiency # plotting number size distribution -------------------------------------- # take log10 of instrument size (diameter) bin boundaries log10D = np.log10(csbb) # take difference of log 10 of diameter at size bin boundaries dlog10D = log10D[1::]-log10D[0:-1] dlog10D = np.tile(dlog10D, [len(timehr), 1]) # tile over times # number size distribution contours ((# particle/cm3)) dNdlog10D = np.zeros((Nint.shape[0], Nint.shape[1])) dNdlog10D[:, :] = Nint/dlog10D # transpose ready for contour plot dNdlog10D = np.transpose(dNdlog10D) # customised colormap (https://www.rapidtables.com/web/color/RGB_Color.html) colors = [(0.6, 0., 0.7), (0, 0, 1), (0, 1., 1.), (0, 1., 0.), (1., 1., 0.), (1., 0., 0.)] # R -> G -> B n_bin = 100 # discretizes the colormap interpolation into bins cmap_name = 'my_list' # create the colormap cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin) # set contour levels levels = (MaxNLocator(nbins = 100).tick_values(np.min(dNdlog10D), np.max(dNdlog10D))) # associate colours and contour levels norm1 = BoundaryNorm(levels, ncolors=cm.N, clip=True) # contour plot with times (hours) along x axis and # particle diameters (nm) along y axis for ti in range(len(timehr)-1): # loop through times p1 = ax0.pcolormesh(timehr[ti:ti+2], (csbb*1e3), dNdlog10D[:, ti].reshape(-1, 1), cmap=cm, norm=norm1) # plot vertical axis logarithmically ax0.set_yscale("log") # set tick format for vertical axis ax0.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.0e')) ax0.set_ylabel('Diameter (nm)', size = 14) ax0.xaxis.set_tick_params(labelsize = 14, direction = 'in', which= 'both') ax0.yaxis.set_tick_params(labelsize = 14, direction = 'in', which= 'both') ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) cb = plt.colorbar(p1, format=ticker.FuncFormatter(fmt), pad=0.25) cb.ax.tick_params(labelsize=14) # colour bar label cb.set_label('dN (#$\,$$\mathrm{cm^{-3}}$)/d$\,$log$_{10}$(D ($\mathrm{\mu m}$))', size=14, rotation=270, labelpad=20) # total particle number concentration (# particles/cm3) ------------------------- # include total number concentration (# particles/cm3 (air)) on contour plot Nvs_time = Nint.sum(axis = 1) p3, = par1.plot(timehr, Nvs_time, '-+k', label = 'N') par1.set_ylabel('N (#$\,$ $\mathrm{cm^{-3})}$', size=14, rotation=270, labelpad=20) # vertical axis label par1.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) # set tick format for vertical axis par1.yaxis.set_tick_params(labelsize=14) # mass concentration of particles (ug/m3) --------------------------------------------------------------- MCvst = np.zeros((len(timehr))) # empty array for total mass concentration (ug/m3) # assumed volumes of particles per size bin (um3) Vn = ((4./3.)*np.pi)*((csbc/2.)**3.) Vn = Vn*1.e-12 # convert to cm3 Vn = np.tile(Vn, [len(timehr), 1]) # tile over times # convert number concentration to volume concentration (cm3 (particle)/cm3 (air)) Vc = Nint*Vn # convert volume concentration to total mass concentration (ug/m3) MCvst = ((Vc*p_rho)*1.e12).sum(axis=1) # log10 of maximum in mass concentration if (max(MCvst[:]) > 0): MCmax = int(np.log10(max(MCvst[:]))) else: MCmax = 0. p5, = par2.plot(timehr, MCvst[:], '-xk', label = 'Total Particle Mass Concentration') par2.set_ylabel(str('Mass Concentration ($\mathrm{\mu g\, m^{-3}})$'), rotation=270, size=16, labelpad=25) # set colour of label, tick font and corresponding vertical axis to match scatter plot presentation par2.yaxis.label.set_color('black') par2.tick_params(axis='y', colors='black') par2.spines['right'].set_color('black') par2.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) # set tick format for vertical axis par2.yaxis.set_tick_params(labelsize=16) plt.legend(fontsize=14, handles=[p3, p5] ,loc=4) if (caller == 2): # display when in test mode plt.show()
def cpc_plotter(caller, dir_path, self, dryf, cdt, max_dt, sdt, max_size, uncert, delays, wfuncs, Hz, loss_func_str, losst, av_int, Q, tau, coi_maxD): import rad_resp_hum import inlet_loss # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # self - reference to GUI # dryf - relative humidity of aerosol at entrance to condensing unit of CPC (fraction 0-1) # cdt - false background counts (# particles/cm3) # max_dt - maximum detectable actual concentration (# particles/cm3) # sdt - particle size at 50 % detection efficiency (nm), # width factor for detection efficiency dependence on particle size # max_size - maximum size measure by counter (nm) # uncert - uncertainty (%) around counts by counter # delays - the significant response times for counter # wfuncs - the weighting as a function of time for particles of different age # Hz - temporal frequency of output # loss_func_str - string stating loss rate (fraction/s) as a # function of particle size (um) # losst - time of passage through inlet (s) # av_int - the averaging interval (s) # Q - volumetric flow rate through counting unit (cm3/s) # tau - instrument dead time (s) # coi_maxDp - maximum actual concentration that # coincidence convolution applies to (# particles/cm3) # -------------------------------------------------------------------------- # required outputs --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, Nwet, _, y_MV, _, wall_on, space_mode, indx_plot, comp0, _, PsatPa, OC, H2Oi, _, siz_str, _, _, _, _) = retr_out.retr_out(dir_path) # ------------------------------------------------------------------------------ # condition wet particles assuming equilibrium with relative humidity at # entrance to condensing unit of CPC. Get new radius at size bin centre (um) [xn, yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)]] = rad_resp_hum.rad_resp_hum(yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)], x, dryf, H2Oi, num_comp, (num_sb-wall_on), Nwet, y_MV) # remove particles lost during transit through inlet (# particles/cm3) [Nwet, yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)]]= inlet_loss.inlet_loss(0, Nwet, xn, yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)], loss_func_str, losst, num_comp) # all CPC output times, assuming first report is at 0 s through experiment times = np.arange(0, timehr[-1]*3600., 1./Hz) # empty array for holding corrected concentrations (# particles/cm3) Nwetn = np.zeros((len(times), Nwet.shape[1])) xnn = np.zeros((len(times), Nwet.shape[1])) # empty array for holding corrected diameters (um) # interpolate simulation output to instrument output frequency (# particles/cm3) # loop through size bins for sbi in range(num_sb-wall_on): Nwetn[:, sbi] = np.interp(times, timehr*3600., Nwet[:, sbi]) xnn[:, sbi] = np.interp(times, timehr*3600., xn[:, sbi]) Nwet = Nwetn # rename Nwet (# particles/cm3) xn = xnn # rename xn (um) # number of simulation outputs within the instrument response time rt_num = delays[2]/(times[1]-times[0]) # if more than one output within response time, then loop through times to correct # for response time and any mixing of ages of particle # an explanation of response time and mixing of particles of different ages # due to the parabolic speed distribution in the CPC tubing is # given by Enroth et al. (2018) in: https://doi.org/10.1080/02786826.2018.1460458 if (rt_num >= 1): # account for response time and mixing of particles of different ages [weight, weightt] = resp_time_func(3, delays, wfuncs) # empty array for holding corrected concentrations (# particles/cm3) Nwetn = np.zeros((Nwet.shape[0], Nwet.shape[1])) xnn = np.zeros((Nwet.shape[0], Nwet.shape[1])) for it in range(1, len(times)): # loop through times # number of time points to consider trel = (times >= (times[it]-delays[2]))*(times <= times[it]) tsim = times[trel] # extract relevant time points (s) tsim = np.abs(tsim-tsim[-1]) # time difference with present (s) Nsim = Nwet[trel, :] # extract relevant number concentrations (# particles/cm3) xsim = xn[trel, :] # interpolate weights, use flip to align times weightn = np.flip(np.interp(np.flip(tsim), weightt, weight)) if (np.diff(weightt) == 0).all(): # if weight is all on one time weightn[:] = 0 # identify time closest to response time t_diff = np.abs(tsim - weightt[0]) tindx = t_diff == np.min(t_diff) weightn[tindx] = 1. # tile across size bins weightn = np.tile(weightn.reshape(-1, 1), [1, num_sb-wall_on]) # corrected concentration Nwetn[it, :] = np.sum(Nsim*weightn, axis=0) xnn[it, :] = np.sum(xsim*weightn, axis=0) Nwet = Nwetn # rename Nwet xn = xnn # size bin radii (um) # correct for coincidence (only relevant at relatively moderate # concentrations (# particles/cm3)), using eq. 11 of # https://doi.org/10.1080/02786826.2012.737049 # where Q is the volumetric flow (cm3/s) rate and tau is the instrument # dead time (s) # bypass if coincidence flagged to not be considered if ((Q == -1)*(tau == -1)*(coi_maxD == -1) != 1): from scipy.special import lambertw # product of actual concentration with volumetric flow rate and instrument dead time Ca = Nwet.sum(axis=1) # cannot invert the Lambert function (eq. 9 of https://doi.org/10.1080/02786826.2012.737049) # directly as do not know the imaginary part, but can identify closest point to real part as we # we know that measure count must lie between blank counts and actual concentration for it in range(len(times)): # time loop # bypass if actual total particle concentration (# particles/cm3) # exceeds maximum that coincidence applicable to or is less than # blank concentration if (Ca[it] > cdt and Ca[it] < coi_maxD): # the possible measured counts (# particles/cm3) x_poss = np.logspace(np.log10(cdt), np.log10(coi_maxD), int(1e3)) # account for volumetric flow rate and dead time x_possn = -x_poss*(Q*tau) # take the Lambert function and obtain just the real part x_possn = (-lambertw(x_possn).real)/(Q*tau) # zero any negatives as these are useless x_possn[x_possn<0] = 0 # find point closest to actual concentration (# particles/cm3) # if all possibilities fall below the actual concentration, then # the instrument will have marked this as a maximum if all(x_possn < Ca[it]): Cm = coi_maxD else: # linear interpolation diff = (Ca[it]-x_possn) indx1 = (diff == np.max(diff[diff<=0.])) indx0 = (diff == np.min(diff[diff>0.])) # the measured concentration (# particles/cm3) diff[indx1] = -1*diff[indx1] # make absolute Cm = (x_poss[indx0]*(diff[indx1])+x_poss[indx1]*(diff[indx0]))/(diff[indx1]+diff[indx0]) # get the fraction underestimation due to coincidence frac_un = Cm/Ca[it] # correct across all size bins (# particles/cm3) Nwet[it, :] = Nwet[it, :]*(frac_un) # moving-average over averaging interval # number of outputs within averaging interval # note that using int here means rounding down, which is sensible av_num = int(av_int/times[1]-times[0]) if (av_num > 1): # empty array to hold moving averages (# particles/cm3) Nwetn = np.zeros((int(Nwet.shape[0]-(av_num-1)), Nwet.shape[1])) # empty array to hold moving average diameters (um) xnn = np.zeros((int(Nwet.shape[0]-(av_num-1)), Nwet.shape[1])) for avi in range(av_num): # (# particles/cm3) Nwetn[:, :] += Nwet[avi:Nwet.shape[0]-(av_num-avi-1), :]/av_num # (um) xnn[:, :] += xn[avi:Nwet.shape[0]-(av_num-avi-1), :]/av_num # correct time (s) times = times[av_num-1::] # return to working variable names Nwet = Nwetn xn = xnn # account for size dependent detection efficiency below one # get detection efficiency as a function of particle size (nm) [Dp, ce] = count_eff_plot(3, 0, self, sdt) # empty array to hold detection efficiencies across times and simulation size bins # Dp is in um ce_t = np.zeros((len(times), xn.shape[1])) # loop through times for it in range(len(times)): # interpolate detection efficiency (fraction) to simulation size bin centres # Dp is in um ce_t[it, :] = np.interp(xn[it, :]*2., Dp, ce) # correct for upper size range of instrument, note conversion of # upper size from nm to um if (max_size != -1): size_indx = (xn[it, :]*2. > max_size*1.e-3) Nwet[it, size_indx] = 0. Nwet = Nwet*ce_t # correct for detection efficiency Nwet = Nwet.sum(axis=1) # sum particle concentrations (# particles/cm3) # account for false background counts # (minimum detectable particle concentration) (# particles/cm3) Nwet[Nwet < cdt] = cdt # account for maximum particle concentration (# particles/cm3) if (max_dt != -1): # if maximum particle concentration to be considered Nwet[Nwet > max_dt] = max_dt if (caller == 0): # when called from gui plt.ion() # show results to screen and turn on interactive mode # plot temporal profile of total particle number concentration (# particles/cm3) # prepare figure ------------------------------------------- fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) ax0.plot(times/3600.0, Nwet, label = 'uncertainty mid-point') # plot vertical axis logarithmically ax0.set_yscale("log") # include uncertainty region, note conversion of uncertainty from percentage to fraction ax0.fill_between(times/3600., Nwet-Nwet*uncert/100., Nwet+Nwet*uncert/100., alpha=0.3, label = 'uncertainty bounds') # set tick format for vertical axis ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.set_ylabel('Total Number Concentration (#$\mathrm{particles\, cm^{-3}}$)', size = 14) ax0.xaxis.set_tick_params(labelsize = 14, direction = 'in', which= 'both') ax0.yaxis.set_tick_params(labelsize = 14, direction = 'in', which= 'both') ax0.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) ax0.set_title('Simulated total particle concentration convolved to represent \ncondensation particle counter (CPC) measurements') ax0.legend() if (caller == 2): # display when in test mode plt.show() return()
def RO2_av_molec(caller, dir_path, comp_names_to_plot, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0 for ug/m3 or 1 for ppb, 3 for # molecules/cm3) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # comp_names_to_plot - chemical scheme names of components to plot # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, rel_SMILES, y_MW, _, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, H2Oi, _, _, _, group_indx, _, _) = retr_out.retr_out(dir_path) y_MW = np.array(y_MW) # convert to numpy array from list Cfac = (np.array(Cfac)).reshape(-1, 1) # convert to numpy array from list if (caller == 0): plt.ion() # show results to screen and turn on interactive mode # prepare plot fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) # get RO2 indices indx_plot = (np.array((group_indx['RO2i']))) # gas-phase concentration (# molecules/cm3) conc = yrec[:, indx_plot].reshape(yrec.shape[0], (indx_plot).shape[0]) * Cfac # sum of concentrations (# molecules/cm3) conc_sum = np.sum(conc, axis=1) # get SMILES of RO2 componenets rel_SMILES = rel_SMILES[indx_plot] # number of carbons and oxygens in each component Ccnt = [] Ocnt = [] Hcnt = [] for i in rel_SMILES: Ccnt.append(i.count('C') + i.count('c')) Ocnt.append(i.count('O')) # generate pybel object Pybel_object = pybel.readstring('smi', i) try: Hi = (Pybel_objects[indx].formula).index('H') Hcnt = -1 except: Hcnt = 0.0 if (Hcnt == -1): try: Hcnt.append(float(Pybel_objects[indx].formula[Hi + 1:Hi + 3])) except: Hcnt.append(float(Pybel_objects[indx].formula[Hi + 1:Hi + 2])) Ccnt = np.tile(((np.array((Ccnt))).reshape(1, -1)), (conc.shape[0], 1)) Ocnt = np.tile(((np.array((Ocnt))).reshape(1, -1)), (conc.shape[0], 1)) Hcnt = np.tile(((np.array((Hcnt))).reshape(1, -1)), (conc.shape[0], 1)) # average carbon number of organic peroxy radicals (RO2) in gas-phase at each time step Cav_cnt = ((np.sum(Ccnt * conc, axis=1)) / conc_sum) # average oxygen number of organic peroxy radicals (RO2) in gas-phase at each time step Oav_cnt = ((np.sum(Ocnt * conc, axis=1)) / conc_sum) # average hydrogen number of organic peroxy radicals (RO2) in gas-phase at each time step Hav_cnt = ((np.sum(Hcnt * conc, axis=1)) / conc_sum) ax0.plot(timehr, Cav_cnt, '-+', linewidth=4., label='Carbon number') ax0.plot(timehr, Oav_cnt, '-+', linewidth=4., label='Oxygen number') ax0.plot(timehr, Hav_cnt, '-+', linewidth=4., label='Hydrogen number') ax0.set_ylabel(r'Average number of atoms per RO2 molecule', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14)
def plotter(caller, dir_path, atom_name, atom_num, self): # define function # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # atom_name - SMILE string names of atom/functional group to target # atom_num - number of top contributors to plot # self - reference to GUI # -------------------------------------------------------------------------- (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, SMILES, y_mw, _, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, _, _, _, _, RO2i, _, _) = retr_out.retr_out(dir_path) # empty lists to contain results cnt_list = [] ind_list = [] if (atom_name != 'RO2'): # if not organic peroxy radicals cn = 0 # count on components for ci in SMILES: # loop through component names at_cnt = ci.count(atom_name) if (at_cnt > 0): # if a contributor cnt_list.append(at_cnt) ind_list.append(int(cn)) cn += 1 else: # if organic peroxy radicals cnt_list = [1] * len(RO2i) ind_list = RO2i # empty results array for contributing component index and number of occurrences res = np.zeros((len(cnt_list), 2)) res[:, 0] = np.array((ind_list)) # index in first column res[:, 1] = np.array((cnt_list)) # count in second column res = res.astype(int) # reshape so that time in rows and components per size bin in columns yrec = yrec.reshape(len(timehr), num_comp * (num_sb + 1)) # convert gas-phase concentrations to molecules/cm3 from ppb yrec[:, 0:num_comp] = yrec[:, 0:num_comp] * (np.array( (Cfac)).reshape(-1, 1)) # extract just the relevant concentrations yrel = np.zeros((len(timehr), res.shape[0])) # molecules/cm3 nam_rel = [] cin = 0 # count on the relevant components for ci in res[:, 0]: # loop through relevant components yrel[:, cin] = np.sum(yrec[:, ci::num_comp], axis=1) * res[cin, 1] nam_rel.append(comp_names[ci]) cin += 1 # identify the components to be plotted ytot = np.sum(yrel, axis=0) ytot_asc = np.sort(ytot) cutoff = ytot_asc[ -atom_num] # the cutoff for the requested number of contributors yreln = yrel[:, ytot >= cutoff] nam_rel_indx = (np.where(ytot >= cutoff))[0] nam_up = [] # empty list for nri in nam_rel_indx: nam_up.append(nam_rel[nri]) fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) # prepare figure for ci in range(len(yreln[0, :])): # loop through relevant components if (any(yreln[:, ci] > 0.)): ax0.semilogy(timehr, yreln[:, ci] / np.sum(yrel, axis=1), label=nam_up[ci]) # details of plot ax0.set_ylabel(r'Fraction contribution', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.set_title(str('Fraction contribution to the atom/functional group ' + str(atom_name)), fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14, loc='lower right') plt.show() return ()
def plotter(caller, dir_path, comp_names_to_plot, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # comp_names_to_plot - chemical scheme names of components to plot # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_MW, _, comp_names, y_MV, _, wall_on, space_mode, _, _, yrec_p2w, PsatPa, OC, H2Oi, _, _, _, _, _, _) = retr_out.retr_out(dir_path) # number of actual particle size bins num_asb = (num_sb - wall_on) if (caller == 0): plt.ion() # show results to screen and turn on interactive mode # prepare plot fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) if (comp_names_to_plot): # if component names specified # plotting section --------------------------------------------- for i in range(len(comp_names_to_plot)): if comp_names_to_plot[i].strip() == 'H2O': indx_plot = H2Oi else: try: # will work if provided components were in simulation chemical scheme # get index of this specified component, removing any white space indx_plot = comp_names.index(comp_names_to_plot[i].strip()) except: self.l203a.setText( str('Component ' + comp_names_to_plot[i] + ' not found in chemical scheme used for this simulation' )) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return () if (wall_on == 1): # total concentration on wall (from particle deposition to wall) (molecules/cc) conc = (yrec_p2w[:, indx_plot::num_comp]).sum(axis=1) else: self.l203a.setText( str('Wall not considered in this simulation')) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 if (self.bd_pl >= 2): self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return () # concentration in ug/m3 conc = ((conc / si.N_A) * y_MW[indx_plot]) * 1.e12 # plot this component ax0.plot(timehr, conc, '+', linewidth=4., label=str( str(comp_names[indx_plot] + ' (wall (from particle deposition to wall))'))) ax0.set_ylabel(r'Concentration ($\rm{\mu}$g$\,$m$\rm{^{-3}}$)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) # end of gas-phase concentration sub-plot --------------------------------------- # display if (caller == 2): plt.show() return ()
def plotter_2DVBS(caller, dir_path, self, t_thro): # inputs: ------------------------------- # caller - the module calling (0 for gui) # dir_path - path to results # self - reference to GUI # t_thro - time (s) through experiment at which to pot the 2D-VBS # ----------------------------------------- # ---------------------------------------------------------------------------------------- if (caller == 0): # if calling function is gui plt.ion() # show figure # prepare plot fig, (ax1) = plt.subplots(1, 1, figsize=(10, 7)) fig.subplots_adjust(hspace=0.7) # prepare plot data -------------------------------------- # required outputs from full-moving (num_sb, num_comp, Cfac, y, Ndry, rbou_rec, xfm, t_array, rel_SMILES, y_mw, N, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, H2Oi, seedi, _, _, _, _, _) = retr_out.retr_out(dir_path) # subtract recorded times from requested time and absolute t_diff = np.abs(t_thro - (t_array * 3600.)) # find closest recorded time to requested time to plot t_indx = (np.where(t_diff == np.min(t_diff)))[0][0] # temperature for vapour pressures TEMP = 298.15 # convert lists to numpy array y_mw = np.array((y_mw)) PsatPa = np.array((PsatPa)) # convert standard (at 298.15 K) vapour pressures in Pa to # saturation concentrations in ug/m3 # using eq. 1 of O'Meara et al. 2014 Psat_Cst = (1.e6 * y_mw) * (PsatPa / 101325.) / (8.2057e-5 * TEMP) # get particle concentrations at this time (molecules/cc) pc = y[t_indx, num_comp:num_comp * (num_sb - wall_on)] # zero water and seed components pc[H2Oi[0]::num_comp] = 0. for seed_indxs in seedi: pc[seed_indxs::num_comp] = 0. # tile molecular weights over size bins y_mw = np.tile(y_mw, (num_sb - wall_on - 1)) # convert concentrations from molecules/cc to ug/m3 pc = ((pc / si.N_A) * y_mw) * 1.e12 # sum particle concentrations (ug/m3) at this time, without water and seed tot_pc = pc.sum() OC_range = np.arange(0., 2., 0.2) # the O:C range VP_range = np.arange( -2.5, 7.5, 1.) # the log10 of the vapour pressure (ug/m3) at 298.15 K range # empty mass fractions matrix mf = np.zeros((len(OC_range), len(VP_range))) # convert list to array PsatPa = np.array((PsatPa)) OC = np.array((OC)) # loop over size bins to get total particle-phase # concentration of each component (ug/m3) for sbi in range(1, (num_sb - wall_on) - 1): pc[0:num_comp] += pc[num_comp * sbi:num_comp * (sbi + 1)] # forget all excess size bins (ug/m3) pc = pc[0:num_comp] # loop through O:C ratios and vapour pressures to estimate mass fractions for OCi in range(len(OC_range) - 1): VPo = 0. # reset lower vapour pressure limit (ug/m3) for VPi in range(len(VP_range)): # upper vapour pressure limit now (ug/m3) VPn = 10**(VP_range[VPi] + 0.5) if (VPi == len(VP_range) - 1): # final upper vapour pressure limit (ug/m3) VPn = np.inf if (OCi == len(OC_range) - 1): # final upper O:C OC_up = np.inf else: OC_up = OC_range[OCi + 1] # index of all components with this vapour pressure and O:C ratio compi = ((Psat_Cst >= VPo) * (Psat_Cst < VPn)) * ((OC >= OC_range[OCi]) * (OC < OC_up)) # mass fractions of components with this combination of properties if (tot_pc > 0): mf[OCi, VPi] = sum(pc[compi]) / tot_pc VPo = VPn # reset lower vapour pressure limit (ug/m3) # do the plotting ------------------------ # customised colormap (https://www.rapidtables.com/web/color/RGB_Color.html) colors = [(0.6, 0., 0.7), (0, 0, 1), (0, 1., 1.), (0, 1., 0.), (1., 1., 0.), (1., 0., 0.)] # R -> G -> B n_bin = 100 # discretizes the colormap interpolation into bins cmap_name = 'my_list' # create the colormap cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin) # set contour levels levels = (MaxNLocator(nbins=100).tick_values(0., 1.)) # associate colours and contour levels norm1 = BoundaryNorm(levels, ncolors=cm.N, clip=True) p0 = ax1.pcolormesh(VP_range, OC_range, mf, cmap=cm, norm=norm1, shading='auto') cax = plt.axes([0.875, 0.40, 0.02, 0.18]) # specify colour bar position cb = plt.colorbar(p0, cax=cax, ticks=[0.00, 0.25, 0.50, 0.75, 1.00], orientation='vertical') cb.ax.tick_params(labelsize=12) cb.set_label('mass fraction', size=12, rotation=270, labelpad=10.) ax1.set_xlabel( r'$\rm{log_{10}(}$$C*_{\mathrm{298.15 K}}$$\rm{\, (\mu g\, m^{-3}))}$', fontsize=14) ax1.set_ylabel(r'O:C ratio', fontsize=14, labelpad=10.) ax1.set_title( str('Mass fraction of non-water and non-seed components at ' + str(t_thro) + str(' s through experiment')), fontsize=14) ax1.yaxis.set_tick_params(labelsize=14, direction='in', which='both') ax1.xaxis.set_tick_params(labelsize=14, direction='in', which='both') # array containing the location of tick labels xtloc = VP_range ax1.set_xticks(xtloc) ytloc = OC_range ax1.set_yticks(ytloc) # prepare list of strings for the tick labels xtl = [] for i in xtloc: if (i == np.min(xtloc)): # if the minimum include less than sign xtl.append(str('$\less$' + str(i + 0.5))) continue if (i == np.max(xtloc) ): # if the maximum include the greater than or equal to sign xtl.append(str('$\geq$' + str(i + 0.5))) continue xtl.append(str(i + 0.5)) # otherwise just state number ax1.set_xticklabels(xtl) if (caller != 0): plt.show() # show figure return () # end function
def plotter_ind(caller, dir_path, comp_names_to_plot, top_num, uc, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # comp_names_to_plot - chemical scheme names of components to plot # top_num - top number of chemical reactions to plot # uc - units to use for change tendency # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, _, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, _, _, _, _, _, _, ro_obj) = retr_out.retr_out(dir_path) # loop through components to plot to check they are available for comp_name in (comp_names_to_plot): fname = str(dir_path + '/' + comp_name + '_rate_of_change') try: # try to open dydt = np.loadtxt(fname, delimiter=',', skiprows=0) # skiprows = 0 skips first header except: mess = str( 'Please note, a change tendency record for the component ' + str(comp_name) + ' was not found, was it specified in the tracked_comp input of the model variables file? Please see README for more information.' ) self.l203a.setText(mess) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return () # if all files are available, then proceed without error message mess = str('') self.l203a.setText(mess) if (self.bd_pl < 3): self.l203a.setStyleSheet(0., '0px solid red', 0., 0.) self.bd_pl == 3 # prepare figure plt.ion() # display figure in interactive mode fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) for comp_name in (comp_names_to_plot): # loop through components to plot if (comp_name != 'HOMRO2' and comp_name != 'RO2'): ci = comp_names.index(comp_name) # get index of this component if (comp_name == 'HOMRO2'): # get mean molecular weight of HOMRO2 (g/mol) counter = 0 # count on HOMRO2 components mw_extra = 0. # count on HOMRO2 molecular weights (g/mol) for cni in range(len(comp_names)): # loop through all components if 'API_' in comp_names[cni] or 'api_' in comp_names[cni]: if 'RO2' in comp_names[cni]: mw_extra += y_mw[cni] counter += 1 y_mw = np.concatenate( (y_mw, (np.array(mw_extra / counter)).reshape(1))) ci = len(y_mw) - 1 if (comp_name == 'RO2'): # get indices of RO2 RO2indx = (np.array((ro_obj.gi['RO2i']))) # get mean molecular weight of RO2 (g/mol) mw_extra = np.sum((np.array((y_mw)))[RO2indx]) / len(RO2indx) y_mw = np.concatenate((y_mw, (np.array(mw_extra)).reshape(1))) ci = len(y_mw) - 1 # note that penultimate column in dydt is gas-particle # partitioning and final column is gas-wall partitioning, whilst # the first row contains chemical reaction numbers # prepare to store results of change tendency due to chemical reactions res = np.zeros((dydt.shape[0], dydt.shape[1] - 2)) res[:, :] = dydt[:, 0: -2] # get chemical reaction numbers and change tendencies if (uc == 0): Cfaca = (np.array(Cfac)).reshape( -1, 1) # convert to numpy array from list # convert change tendencies from # molecules/cm3/s to ppb res[1::, :] = (res[1::, :] / Cfaca[1::]) ct_units = str('(ppb/s)') if (uc == 1): # convert change tendencies from # molecules/cm3/s to ug/m3/s res[1::, :] = ((res[1::, :] / si.N_A) * y_mw[ci]) * 1.e12 ct_units = str('(' + u'\u03BC' + 'g/m' + u'\u00B3' + '/s)') if (uc == 2): # keep change tendencies as # molecules/cm3/s res[1::, :] = res[1::, :] ct_units = str('\n(' + u'\u0023' + ' molecules/cm' + u'\u00B3' + '/s)') # identify most active chemical reactions # first sum total change tendency over time (ug/m3/s) res_sum = np.abs(np.sum(res[1::, :], axis=0)) # sort in ascending order res_sort = np.sort(res_sum) if (len(res_sort) < top_num[0]): # if less reactions are present than the number requested inform user mess = str( 'Please note that ' + str(len(res_sort)) + ' relevant reactions were found, although a maximum of ' + str(top_num[0]) + ' were requested by the user.') self.l203a.setText(mess) # get all reactions out of the used chemical scheme -------------------------------------------------------------------- import sch_interr # for interpeting chemical scheme import re # for parsing chemical scheme import scipy.constants as si sch_name = ro_obj.sp inname = ro_obj.vp f_open_eqn = open(sch_name, mode='r') # open model variables file # read the file and store everything into a list total_list_eqn = f_open_eqn.readlines() f_open_eqn.close() # close file inputs = open(inname, mode='r') # open model variables file in_list = inputs.readlines( ) # read file and store everything into a list inputs.close() # close file for i in range(len(in_list) ): # loop through supplied model variables to interpret # ---------------------------------------------------- # if commented out continue to next line if (in_list[i][0] == '#'): continue key, value = in_list[i].split('=') # split values from keys # model variable name - a string with bounding white space removed key = key.strip() # ---------------------------------------------------- if key == 'chem_scheme_markers' and ( value.strip()): # formatting for chemical scheme chem_sch_mrk = [str(i).strip() for i in (value.split(','))] # interrogate scheme to list equations [eqn_list, aqeqn_list, eqn_num, rrc, rrc_name, RO2_names] = sch_interr.sch_interr(total_list_eqn, chem_sch_mrk) for cnum in range(np.min([top_num[0], len(res_sort) ])): # loop through chemical reactions # identify this chemical reaction cindx = np.where((res_sort[-(cnum + 1)] == res_sum) == 1)[0] for indx_two in (cindx): reac_txt = str(eqn_list[int( res[0, indx_two])]) # get equation text # plot, note the +1 in the label to bring label into MCM index ax0.plot(timehr[0:-1], res[1::, indx_two], label=str(' Eq. # ' + str(int(res[0, indx_two]) + 1) + ': ' + reac_txt)) ax0.yaxis.set_tick_params(direction='in') ax0.set_title( 'Change tendencies, where a tendency to decrease \ngas-phase concentrations is negative' ) ax0.set_xlabel('Time through experiment (hours)') ax0.set_ylabel(str('Change tendency ' + ct_units)) ax0.yaxis.set_tick_params(direction='in') ax0.xaxis.set_tick_params(direction='in') ax0.legend() return ()
fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(12, 6.5), sharex='all') fig.subplots_adjust(hspace=0.05) # ------------------------------------------------------------------------------ # results - note that all results for table 1 saved in fig11_data cwd = os.getcwd() # get current working directory try: # if calling from the PyCHAM home directory output_by_sim = str( cwd + '/PyCHAM/output/GMD_paper_plotting_scripts/fig11_data/nuc_vsobs_output_fm_n1_2e4_n2_-4e2_n3_1e2' ) # required outputs (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, xfm, time_array, comp_names, _, N, _, y_MV, _, wall_on, space_mode, _) = retr_out.retr_out(output_by_sim) except: # if calling from the GMD paper Results folder output_by_sim = str( cwd + '/fig11_data/nuc_vsobs_output_fm_n1_2e4_n2_-4e2_n3_1e2') # required outputs (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, xfm, time_array, comp_names, _, N, _, y_MV, _, wall_on, space_mode, _) = retr_out.retr_out(output_by_sim) dlog10D = np.log10(rbou_rec[:, 1::] * 2.0) - np.log10(rbou_rec[:, 0:-1] * 2.0) dNdD = Ndry / dlog10D # normalised number size distribution (#/cc (air)) # observation part --------------------------------------------------------------- import xlrd # for opening xlsx file
def plotter_CIMS(dir_path, res_in, tn, iont, sens_func): # inputs: ----------------- # dir_path - path to folder containing results files to plot # res_in - inputs for resolution of molar mass to charge ratio (g/mol/charge) # tn - time through experiment to plot at (s) # iont - type of ionisation # sens_func - sensitivity to molar mass function # --------------------------- # retrieve results, note that num_sb (number of size bins) # includes wall if wall turned on (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_MW, _, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, H2Oi, _, _, _, RO2i, _, _) = retr_out.retr_out(dir_path) # convert to 2D numpy array y_MW = np.array((y_MW)).reshape(-1, 1) # get index of time wanted ti = (np.where( np.abs(timehr - tn / 3600.) == np.min(np.abs(timehr - tn / 3600.))))[0][0] # convert yrec from 1D to 2D with times in rows, then select time wanted yrec = (yrec.reshape(len(timehr), num_comp * (num_sb + 1)))[ti, :] # get gas-phase concentrations (ppt, note starting concentration is ppb) gp = yrec[ 0: num_comp] * 1.e3 # conversion to ug/m3 (if wanted): /Cfac[ti]/si.N_A*y_MW[:, 0]*1.e12 # get particle-phase concentrations (molecules/cm3) pp = yrec[num_comp:num_comp * (num_sb + 1 - wall_on)] # sum each component over size bins (molecules/cm3) pp = np.sum(pp.reshape(num_sb - wall_on, num_comp), axis=0) # convert to ppt pp = (pp / si.N_A) * Cfac[ti] * 1.e6 # or convert to ug/m3: *y_MW[:, 0]*1.e12 # correct for sensitivity to molar mass fac_per_comp = write_sens2mm(0, sens_func, y_MW) gp = gp * fac_per_comp[:] pp = pp * fac_per_comp[:] # if ionisation source molar mass to be added # (e.g. because not corrected for in measurment software), then add if (int(iont[1]) == 1): if (iont[0] == 'I' ): # (https://pubchem.ncbi.nlm.nih.gov/compound/Iodide-ion) y_MW += 126.9045 if (iont[0] == 'N' ): # (https://pubchem.ncbi.nlm.nih.gov/compound/nitrate) y_MW += 62.005 # remove water gp = np.append(gp[0:H2Oi], gp[H2Oi + 1::]) pp = np.append(pp[0:H2Oi], pp[H2Oi + 1::]) y_MW = np.append(y_MW[0:H2Oi, 0], y_MW[H2Oi + 1::, 0]) # account for mass to charge resolution [pdf, comp_indx, comp_prob, mm_all] = write_mzres(1, res_in, y_MW) gpres = np.zeros((len(comp_indx))) ppres = np.zeros((len(comp_indx))) for pdfi in range(len(comp_indx)): # loop through resolution intervals gpres[pdfi] = np.sum(gp[comp_indx[pdfi]] * comp_prob[pdfi]) ppres[pdfi] = np.sum(pp[comp_indx[pdfi]] * comp_prob[pdfi]) plt.ion() # disply plot in interactive mode # prepare plot fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) ax0.semilogy(mm_all, gpres, '+m', markersize=14, markeredgewidth=5, label=str('gas-phase')) ax0.semilogy(mm_all, ppres, 'xb', markersize=14, markeredgewidth=5, label=str('particle-phase')) ax0.set_title(str('Mass spectrum at ' + str(timehr[ti]) + ' hours'), fontsize=14) ax0.set_xlabel(r'Mass/charge (Th)', fontsize=14) ax0.set_ylabel(r'Concentration (ppt)', fontsize=14) ax0.xaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.yaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.legend(fontsize=14) return ()
def plotter(caller, dir_path, comp_names_to_plot, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # comp_names_to_plot - chemical scheme names of components to plot # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, _, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, _, _, _, _, _, _, _) = retr_out.retr_out(dir_path) # no record of change tendency for final experiment time point timehr = timehr[0:-1] # loop through components to plot to check they are available for comp_name in (comp_names_to_plot): fname = str(dir_path + '/' + comp_name + '_rate_of_change') try: # try to open dydt = np.loadtxt(fname, delimiter=',', skiprows=1) # skiprows = 1 omits header except: mess = str( 'Please note, a change tendency record for the component ' + str(comp_name) + ' was not found, was it specified in the tracked_comp input of the model variables file? Please see README for more information.' ) self.l203a.setText(mess) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return () # if all files are available, then proceed without error message mess = str('') self.l203a.setText(mess) if (self.bd_pl < 3): self.l203a.setStyleSheet(0., '0px solid red', 0., 0.) self.bd_pl == 3 # prepare figure plt.ion() # display figure in interactive mode fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) for comp_name in (comp_names_to_plot): # loop through components to plot if (comp_name != 'HOMRO2' and comp_name != 'RO2'): ci = comp_names.index(comp_name) # get index of this component if (comp_name == 'HOMRO2'): # get mean molecular weight of HOMRO2 (g/mol) counter = 0 # count on HOMRO2 components mw_extra = 0. # count on HOMRO2 molecular weights (g/mol) for cni in range(len(comp_names)): # loop through all components if 'API_' in comp_names[cni] or 'api_' in comp_names[cni]: if 'RO2' in comp_names[cni]: mw_extra += y_mw[cni] counter += 1 y_mw = np.concatenate( (y_mw, (np.array(mw_extra / counter)).reshape(1))) ci = len(y_mw) - 1 if (comp_name == 'RO2'): # get indices of RO2 RO2indx = (np.array((ro_obj.gi['RO2i']))) # get mean molecular weight of RO2 (g/mol) mw_extra = np.sum((np.array((y_mw)))[RO2indx]) / len(RO2indx) y_mw = np.concatenate((y_mw, (np.array(mw_extra)).reshape(1))) ci = len(y_mw) - 1 # note that penultimate column in dydt is gas-particle # partitioning and final column is gas-wall partitioning, whilst # the first row contains chemical reaction numbers # extract the change tendency due to gas-particle partitioning gpp = dydt[1::, -2] # extract the change tendency due to gas-wall partitioning gwp = dydt[1::, -1] # sum chemical reaction gains crg = np.zeros((dydt.shape[0] - 1, 1)) # sum chemical reaction losses crl = np.zeros((dydt.shape[0] - 1, 1)) for ti in range(dydt.shape[0] - 1): # loop through times indx = dydt[ ti + 1, 0:-2] > 0 # indices of reactions that produce component crg[ti] = dydt[ti + 1, 0:-2][indx].sum() indx = dydt[ti + 1, 0:-2] < 0 # indices of reactions that lose component crl[ti] = dydt[ti + 1, 0:-2][indx].sum() # convert change tendencies from molecules/cc/s to ug/m3/s gpp = ((gpp / si.N_A) * y_mw[ci]) * 1.e12 gwp = ((gwp / si.N_A) * y_mw[ci]) * 1.e12 crg = ((crg / si.N_A) * y_mw[ci]) * 1.e12 crl = ((crl / si.N_A) * y_mw[ci]) * 1.e12 # plot temporal profiles of change tendencies due to chemical # reaction production and loss, gas-particle partitioning and gas-wall partitioning ax0.plot(timehr, gpp, label=str('gas-particle partitioning ' + comp_name)) ax0.plot(timehr, gwp, label=str('gas-wall partitioning ' + comp_name)) ax0.plot(timehr, crg, label=str('chemical reaction gain ' + comp_name)) ax0.plot(timehr, crl, label=str('chemical reaction loss ' + comp_name)) ax0.yaxis.set_tick_params(direction='in') ax0.set_title( 'Change tendencies, where a tendency to decrease \ngas-phase concentrations is treated as negative' ) ax0.set_xlabel('Time through experiment (hours)') ax0.set_ylabel('Change tendency ($\mathrm{\mu g\, m^{-3}\, s^{-1}}$)') ax0.yaxis.set_tick_params(direction='in') ax0.xaxis.set_tick_params(direction='in') ax0.legend() return ()
# best particle and gas loss to wall #mass_trans_coeff = 1.e-6 #eff_abs_wall_massC = 1.e0 #inflectDp = 1.e-6 #Grad_pre_inflect = 1. #Grad_post_inflect = 1. #Rate_at_inflect = 6.e-6 output_by_sim = str( cwd + '/PyCHAM/output/fig11_full_scheme/nuc_vsobs_output_mc_24sb_gpm') # required outputs (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, xfm, time_array, comp_names, _, N, _, y_MV, _, wall_on, space_mode, indx_plot, comp0, yrec_p2w, PsatPa, OC, H2Oi, seedi, siz_str, cham_env, group_indx, tot_in_res) = retr_out.retr_out(output_by_sim) dlog10D = np.log10(rbou_rec[:, 1::] * 2.) - np.log10(rbou_rec[:, 0:-1] * 2.) dNdD = Ndry / dlog10D # normalised number size distribution (# particles/cm3 (air)) # observation part --------------------------------------------------------------- import openpyxl # for opening xlsx file # if calling from the PyCHAM home folder wb = openpyxl.load_workbook( str(cwd + '/PyCHAM/output/GMD_paper_plotting_scripts/obs_fig11.xlsx')) wb = wb['20190628_Dark_limonene_LowNOx'] # take just the required sheet sr = 14 # starting row for desired time since O3 injection obst = np.zeros((46 - sr, 1)) # empty array for observation times (s)
def plotter(caller): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # -------------------------------------------------------------------------- # retrieve useful information from pickle file [sav_name, sch_name, indx_plot, Comp0] = ui.share(1) if (sav_name == 'default_res_name'): print( 'Default results name was used, therefore results not saved and nothing to plot' ) return () dir_path = os.getcwd() # current working directory # obtain just part of the path up to PyCHAM home directory for i in range(len(dir_path)): if dir_path[i:i + 7] == 'PyCHAM': dir_path = dir_path[0:i + 7] break # isolate the scheme name from path to scheme for i in range(len(sch_name) - 1, 0, -1): if sch_name[i] == '/': sch_name = sch_name[i + 1::] break # remove any file formats for i in range(len(sch_name) - 1, 0, -1): if sch_name[i] == '.': sch_name = sch_name[0:i] break dir_path = str(dir_path + '/PyCHAM/output/' + sch_name + '/' + sav_name) # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, comp_names, _, _, _, y_MV, _, wall_on, space_mode) = retr_out.retr_out(dir_path) # number of actual particle size bins num_asb = num_sb - wall_on if caller == 0: plt.ion() # show results to screen and turn on interactive mode # prepare sub-plots depending on whether particles present if (num_asb) == 0: # no particle size bins if not (indx_plot ): # check whether there are any gaseous components to plot print( 'Please note no initial gas-phase concentrations were received and no particle size bins were present, therefore there is nothing for the standard plot to show' ) return () fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) if (num_asb > 0): if not (indx_plot): print( 'Please note, no initial gas-phase concentrations were registered, therefore the gas-phase standard plot will not be shown' ) fig, (ax1) = plt.subplots(1, 1, figsize=(14, 7)) else: fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(14, 7)) par1 = ax1.twinx() # first parasite axis par2 = ax1.twinx() # second parasite axis # Offset the right spine of par2. The ticks and label have already been # placed on the right by twinx above. par2.spines["right"].set_position(("axes", 1.2)) # Having been created by twinx, par2 has its frame off, so the line of its # detached spine is invisible. First, activate the frame but make the patch # and spines invisible. make_patch_spines_invisible(par2) # Second, show the right spine. par2.spines["right"].set_visible(True) if (indx_plot): # gas-phase concentration sub-plot --------------------------------------------- for i in range(len(indx_plot)): ax0.semilogy(timehr, yrec[:, indx_plot[i]], '+', linewidth=4.0, label=str(str(Comp0[i]))) ax0.set_ylabel(r'Gas-phase concentration (ppb)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) # find maximum and minimum of plotted concentrations for sub-plot label maxy = max(yrec[:, indx_plot].flatten()) miny = min(yrec[:, indx_plot].flatten()) ax0.text(x=timehr[0] - (timehr[-1] - timehr[0]) / 10., y=maxy + ((maxy - miny) / 10.), s='a)', size=14) # end of gas-phase concentration sub-plot --------------------------------------- # particle properties sub-plot -------------------------------------------------- if (num_asb > 0): # if particles present if timehr.ndim == 0: # occurs if only one time step saved Ndry = np.array(Ndry.reshape(1, num_asb)) if num_asb == 1: # just one particle size bin (wall included in num_sb) Ndry = np.array(Ndry.reshape(len(timehr), num_asb)) if timehr.ndim == 0: # occurs if only one time step saved x = np.array(x.reshape(1, num_asb)) if num_asb == 1: # just one particle size bin (wall included in num_sb) x = np.array(x.reshape(len(timehr), num_asb)) if timehr.ndim == 0: # occurs if only one time step saved rbou_rec = np.array(rbou_rec.reshape(1, num_sb)) # plotting number size distribution -------------------------------------- # don't use the first boundary as it's zero, so will error when log10 taken log10D = np.log10(rbou_rec[:, 1::] * 2.0) if (num_asb > 1): # note, can't append zero to start of log10D to cover first size bin as the log10 of the # non-zero boundaries give negative results due to the value being below 1, so instead # assume same log10 distance as the next pair log10D = np.append( (log10D[:, 0] - (log10D[:, 1] - log10D[:, 0])).reshape(-1, 1), log10D, axis=1) # radius distance covered by each size bin (log10(um)) dlog10D = (log10D[:, 1::] - log10D[:, 0:-1]).reshape( log10D.shape[0], log10D.shape[1] - 1) if (num_asb == 1): # single particle size bin # assume lower radius bound is ten times smaller than upper dlog10D = (log10D[:, 0] - np.log10( (rbou_rec[:, 1] / 10.) * 2.)).reshape(log10D.shape[0], 1) # number size distribution contours (/cc (air)) dNdlog10D = np.zeros((Ndry.shape[0], Ndry.shape[1])) dNdlog10D[:, :] = Ndry[:, :] / dlog10D[:, :] # transpose ready for contour plot dNdlog10D = np.transpose(dNdlog10D) # mask the nan values so they're not plotted z = np.ma.masked_where(np.isnan(dNdlog10D), dNdlog10D) # customised colormap (https://www.rapidtables.com/web/color/RGB_Color.html) colors = [(0.60, 0.0, 0.70), (0, 0, 1), (0, 1.0, 1.0), (0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0)] # R -> G -> B n_bin = 100 # Discretizes the colormap interpolation into bins cmap_name = 'my_list' # Create the colormap cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin) # set contour levels levels = (MaxNLocator(nbins=100).tick_values(np.min(z[~np.isnan(z)]), np.max(z[~np.isnan(z)]))) # associate colours and contour levels norm1 = BoundaryNorm(levels, ncolors=cm.N, clip=True) # contour plot with times (hours) along x axis and # particle diameters (nm) along y axis for ti in range(len(timehr) - 1): # loop through times p1 = ax1.pcolormesh(timehr[ti:ti + 2], (rbou_rec[ti, :] * 2 * 1e3), z[:, ti].reshape(-1, 1), cmap=cm, norm=norm1) # if logarithmic spacing of size bins specified, plot vertical axis # logarithmically if space_mode == 'log': ax1.set_yscale("log") ax1.set_ylabel('Diameter (nm)', size=14) ax1.xaxis.set_tick_params(labelsize=14, direction='in') ax1.yaxis.set_tick_params(labelsize=14, direction='in') # label according to whether gas-phase plot also displayed if (indx_plot): ax1.text(x=timehr[0] - (timehr[-1] - timehr[0]) / 11., y=np.amax(rbou_rec * 2 * 1e3) * 1.05, s='b)', size=14) else: ax1.text(x=timehr[0] - (timehr[-1] - timehr[0]) / 11., y=np.amax(rbou_rec * 2 * 1e3) * 1.3, s='a)', size=14) ax1.set_xlabel(r'Time through simulation (hours)', fontsize=14) cb = plt.colorbar(p1, format=ticker.FuncFormatter(fmt), pad=0.25) cb.ax.tick_params(labelsize=14) # colour bar label cb.set_label('dN/dlog10(D) $\mathrm{(cm^{-3})}$', size=14, rotation=270, labelpad=20) # ---------------------------------------------------------------------------------------- # total particle number concentration #/cm3 # include total number concentration (# particles/cc (air)) on contour plot # first identify size bins with radius exceeding 3nm # empty array for holding total number of particles Nvs_time = np.zeros((Ndry.shape[0])) for i in range(num_asb): # size bin loop # get the times when bin exceeds 3nm - might be wanted to deal with particle counter detection limits # ish = x[:, i]>3.0e-3 # Nvs_time[ish] += Ndry[ish, i] # sum number Nvs_time[:] += Ndry[:, i] # sum number p3, = par1.plot(timehr, Nvs_time, '+k', label='N') par1.set_ylabel('N (# $\mathrm{cm^{-3})}$', size=14, rotation=270, labelpad=20) # vertical axis label par1.yaxis.set_major_formatter(ticker.FormatStrFormatter( '%.0e')) # set tick format for vertical axis par1.yaxis.set_tick_params(labelsize=14) # SOA mass concentration --------------------------------------------------------------- # array for SOA sum with time SOAvst = np.zeros((1, len(timehr))) final_i = 0 # check whether water and/or core is present if comp_names[-2] == 'H2O': # if both present final_i = 2 if comp_names[-1] == 'H2O': # if just water final_i = 1 # note that the seed component is only registered in init_conc_func if initial # particle concentration (pconc) exceeds zero, therefore particle-phase material # must be present at start of experiment (row 0 in y) if final_i == 0 and y[0, num_speci:(num_speci * (num_asb + 1))].sum() > 1.0e-10: final_i = 1 for i in range(num_asb): # particle size bin loop # sum of organics in condensed-phase at end of simulation (ug/m3 (air)) # to replicate the SMPS results, find the volume of particles then # assume a density of 1.0 g/cm3 SOAvst[0, :] += np.sum( (yrec[:, ((i + 1) * num_comp):((i + 2) * num_comp - final_i)] / si.N_A * (y_MV[0:-final_i]) * 1.0e12), axis=1) # log10 of maximum in SOA if (max(SOAvst[0, :]) > 0): SOAmax = int(np.log10(max(SOAvst[0, :]))) else: SOAmax = 0. # transform SOA so no standard notation required SOAvst[0, :] = SOAvst[0, :] p5, = par2.plot(timehr, SOAvst[0, :], 'xk', label='[secondary]') par2.set_ylabel(str('[secondary] ($\mathrm{\mu g\, m^{-3}})$'), rotation=270, size=16, labelpad=25) # set label, tick font and [SOA] vertical axis to red to match scatter plot presentation par2.yaxis.label.set_color('black') par2.tick_params(axis='y', colors='black') par2.spines['right'].set_color('black') par2.yaxis.set_major_formatter(ticker.FormatStrFormatter( '%.0e')) # set tick format for vertical axis par2.yaxis.set_tick_params(labelsize=16) par2.text((timehr)[0], max(SOAvst[0, :]) / 2.0, 'assumed particle density = 1.0 $\mathrm{g\, cm^{-3}}$') plt.legend(fontsize=14, handles=[p3, p5], loc=4) # end of particle properties sub-plot ----------------------------------- # save and display plt.savefig(str(dir_path + '/' + sav_name + '_output_plot.png')) if caller == 2: plt.show() return ()
def cpc_plotter(caller, dir_path, self, dryf, cdt, max_dt, sdt, max_size, uncert, delays, wfuncs, Hz, loss_func_str, losst): import rad_resp_hum import inlet_loss # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # self - reference to GUI # dryf - relative humidity of aerosol at entrance to condensing unit of CPC (fraction 0-1) # cdt - false background counts (# particles/cm3) # max_dt - maximum detectable concentration (# particles/cm3) # sdt - particle size at 50 % detection efficiency (nm), # width factor for detection efficiency dependence on particle size # max_size - maximum size measure by counter (nm) # uncert - uncertainty (%) around counts by counter # delays - the significant response times for counter # wfuncs - the weighting as a function of time for particles of different age # Hz - temporal frequency of output # loss_func_str - string stating loss rate (fraction/s) as a # function of particle size (um) # losst - time of passage through inlet (s) # -------------------------------------------------------------------------- # required outputs --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, Nwet, _, y_MV, _, wall_on, space_mode, indx_plot, comp0, _, PsatPa, OC, H2Oi, _, siz_str, _) = retr_out.retr_out(dir_path) # ------------------------------------------------------------------------------ # condition wet particles assuming equilibrium with relative humidity at # entrance to condensing unit of CPC. Get new radius at size bin centre (um) [xn, yrec[:, num_comp:(num_sb - wall_on + 1) * (num_comp)] ] = rad_resp_hum.rad_resp_hum( yrec[:, num_comp:(num_sb - wall_on + 1) * (num_comp)], x, dryf, H2Oi, num_comp, (num_sb - wall_on), Nwet, y_MV) # remove particles lost during transit through inlet (# particles/cm3) [Nwet, yrec[:, num_comp:(num_sb - wall_on + 1) * (num_comp)] ] = inlet_loss.inlet_loss( Nwet, xn, yrec[:, num_comp:(num_sb - wall_on + 1) * (num_comp)], loss_func_str, losst, num_comp) # all CPC output times, assuming first report is at 0 s through experiment times = np.arange(0, timehr[-1] * 3600., 1. / Hz) # empty array for holding corrected concentrations (# particles/cm3) Nwetn = np.zeros((len(times), Nwet.shape[1])) xnn = np.zeros((len(times), Nwet.shape[1])) # interpolate simulation output to instrument output frequency (# particles/cm3) # loop through size bins for sbi in range(num_sb - wall_on): Nwetn[:, sbi] = np.interp(times, timehr * 3600., Nwet[:, sbi]) xnn[:, sbi] = np.interp(times, timehr * 3600., xn[:, sbi]) Nwet = Nwetn # rename Nwet (# particles/cm3) xn = xnn # rename xn (um) # number of simulation outputs within the instrument response time rt_num = (times[1] - times[0]) / delays[2] # if more than one output within response time, then loop through times to correct # for response time and any mixing of ages of particle if (rt_num > 1): # account for response time and mixing of particles of different ages [weight, weightt] = resp_time_func(3, delays, wfuncs) # empty array for holding corrected concentrations (# particles/cm3) Nwetn = np.zeros((Nwet.shape[0], Nwet.shape[1])) xnn = np.zeros((Nwet.shape[0], Nwet.shape[1])) for it in range(1, len(times)): # loop through times # number of time points to consider trel = (times >= (times[it] - delays[2])) * (times <= times[it]) tsim = times[trel] # extract relevant time points (s) tsim = np.abs(tsim - tsim[-1]) # time difference with present (s) Nsim = Nwet[ trel, :] # extract relevant number concentrations (# particles/cm3) xsim = xn[trel, :] # interpolate weights, use flip to align times weightn = np.flip(np.interp(np.flip(tsim), weightt, weight)) # tile across size bins weightn = np.tile(weightn.reshape(-1, 1), [1, num_sb - wall_on]) # corrected concentration Nwetn[it, :] = np.sum(Nsim * weightn, axis=0) xnn[it, :] = np.sum(xsim * weightn, axis=0) Nwet = Nwetn # rename Nwet xn = xnn # size bin radii (um) # account for size dependent detection efficiency below one # get detection efficiency as a function of particle size (nm) [Dp, ce] = count_eff_plot(3, 0, self, sdt) # empty array to hold detection efficiencies across times and simulation size bins # Dp is in um ce_t = np.zeros((len(times), xn.shape[1])) # loop through times for it in range(len(times)): # interpolate detection efficiency (fraction) to simulation size bin centres # Dp is in um ce_t[it, :] = np.interp(xn[it, :] * 2., Dp, ce) # correct for upper size range of instrument, note conversion of # upper size from nm to um size_indx = (xn[it, :] * 2. > max_size * 1.e-3) Nwet[it, size_indx] = 0. Nwet = Nwet * ce_t # correct for detection efficiency # ------------ # sum particle number concentration across size bins Nwet = Nwet.sum(axis=1) plt.ion() # show results to screen and turn on interactive mode fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) ax0.plot(times / 3600., Nwet, label='uncertainty mid-point') # plot vertical axis logarithmically ax0.set_yscale("log") # include uncertainty region, note conversion of uncertainty from percentage to fraction ax0.fill_between(times / 3600., Nwet - Nwet * uncert / 100., Nwet + Nwet * uncert / 100., alpha=0.3, label='uncertainty bounds') # set tick format for vertical axis ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.set_ylabel( 'Total Number Concentration (#$\mathrm{particles\, cm^{-3}}$)', size=14) ax0.xaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.yaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) ax0.set_title( 'Simulated total particle concentration convolved to represent \ncondensation particle counter (CPC) measurements' ) ax0.legend() return () # ------------ Nwet = Nwet.sum(axis=1) # sum particle concentrations (# particles/cm3) # account for false background counts # (minimum detectable particle concentration) (# particles/cm3) Nwet[Nwet < cdt] = cdt # account for maximum particle concentration (# particles/cm3) Nwet[Nwet > max_dt] = max_dt if (caller == 0): # when called from gui plt.ion() # show results to screen and turn on interactive mode # plot temporal profile of total particle number concentration (# particles/cm3) # prepare figure ------------------------------------------- fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) ax0.plot(times / 3600.0, Nwet, label='uncertainty mid-point') # plot vertical axis logarithmically ax0.set_yscale("log") # include uncertainty region, note conversion of uncertainty from percentage to fraction ax0.fill_between(times / 3600., Nwet - Nwet * uncert / 100., Nwet + Nwet * uncert / 100., alpha=0.3, label='uncertainty bounds') # set tick format for vertical axis ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.set_ylabel( 'Total Number Concentration (#$\mathrm{particles\, cm^{-3}}$)', size=14) ax0.xaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.yaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) ax0.set_title( 'Simulated total particle concentration convolved to represent \ncondensation particle counter (CPC) measurements' ) ax0.legend() if (caller == 2): # display when in test mode plt.show() return ()
def smps_plotter(caller, dir_path, self, dryf, cdt, max_dt, sdt, max_size, uncert, delays, wfuncs, Hz, loss_func_str, losst, av_int, Q, tau, coi_maxD, csbn): import rad_resp_hum import inlet_loss # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # self - reference to GUI # dryf - relative humidity of aerosol at entrance to condensing unit of CPC (fraction 0-1) # cdt - false background counts (# particles/cm3) # max_dt - maximum detectable actual concentration (# particles/cm3) # sdt - particle size at 50 % detection efficiency (nm), # width factor for detection efficiency dependence on particle size # max_size - minimum and maximum size measured (nm) # uncert - uncertainty (%) around counts by counter # delays - the significant response times for counter # wfuncs - the weighting as a function of time for particles of different age # Hz - temporal frequency of output # loss_func_str - string stating loss rate (fraction/s) as a # function of particle size (um) # losst - time of passage through inlet (s) # av_int - the averaging interval (s) # Q - volumetric flow rate through counting unit (cm3/s) # tau - instrument dead time (s) # coi_maxD - maximum actual concentration that # coincidence convolution applies to (# particles/cm3) # csbn - the number of channels per decade of particle size # -------------------------------------------------------------------------- # required outputs --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, Nwet, _, y_MV, _, wall_on, space_mode, indx_plot, comp0, _, PsatPa, OC, H2Oi, _, siz_str, _, _, _, _) = retr_out.retr_out(dir_path) # ------------------------------------------------------------------------------ # condition wet particles assuming equilibrium with relative humidity inside instrument. # Get new radius at size bin centre (um) [xn, yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)]] = rad_resp_hum.rad_resp_hum(yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)], x, dryf, H2Oi, num_comp, (num_sb-wall_on), Nwet, y_MV) # remove particles lost during transit through instrument (# particles/cm3) [Nwet, yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)]]= inlet_loss.inlet_loss(0, Nwet, xn, yrec[:, num_comp:(num_sb-wall_on+1)*(num_comp)], loss_func_str, losst, num_comp) # all instrument output times, assuming first report is at 0 s through experiment times = np.arange(0, timehr[-1]*3600., 1./Hz) # empty array for holding corrected concentrations (# particles/cm3) Nwetn = np.zeros((len(times), Nwet.shape[1])) xnn = np.zeros((len(times), Nwet.shape[1])) # empty array for holding corrected radii (um) rbou_recn = np.zeros((len(times), Nwet.shape[1]+1))# empty array for holding corrected size bin boundaries # interpolate simulation output to instrument output frequency (# particles/cm3) # loop through size bins for sbi in range(num_sb-wall_on+1): if (sbi == (num_sb-wall_on+1)-1): # only consider size bin boundary for final step rbou_recn[:, sbi] = np.interp(times, timehr*3600., rbou_rec[:, sbi]) else: # otherwise consider all arrays Nwetn[:, sbi] = np.interp(times, timehr*3600., Nwet[:, sbi]) xnn[:, sbi] = np.interp(times, timehr*3600., xn[:, sbi]) rbou_recn[:, sbi] = np.interp(times, timehr*3600., rbou_rec[:, sbi]) Nwet = Nwetn # rename Nwet (# particles/cm3) xn = xnn # rename xn (um) rbou_rec = rbou_recn # rename size bin boundary (um) array # number of simulation outputs within the instrument response time rt_num = delays[2]/(times[1]-times[0]) # if more than one output within response time, then loop through times to correct # for response time and any mixing of ages of particle # an explanation of response time and mixing of particles of different ages # due to the parabolic speed distribution in instrument tubing is # given by Enroth et al. (2018) in: https://doi.org/10.1080/02786826.2018.1460458 if (rt_num >= 1): # account for response time and mixing of particles of different ages [weight, weightt] = resp_time_func(3, delays, wfuncs) # empty array for holding corrected concentrations (# particles/cm3) Nwetn = np.zeros((Nwet.shape[0], Nwet.shape[1])) xnn = np.zeros((Nwet.shape[0], Nwet.shape[1])) for it in range(1, len(times)): # loop through times # number of time points to consider trel = (times >= (times[it]-delays[2]))*(times <= times[it]) tsim = times[trel] # extract relevant time points (s) tsim = np.abs(tsim-tsim[-1]) # time difference with present (s) Nsim = Nwet[trel, :] # extract relevant number concentrations (# particles/cm3) xsim = xn[trel, :] # interpolate weights, use flip to align times weightn = np.flip(np.interp(np.flip(tsim), weightt, weight)) if (np.diff(weightt) == 0).all(): # if weight is all on one time weightn[:] = 0 # identify time closest to response time t_diff = np.abs(tsim - weightt[0]) tindx = t_diff == np.min(t_diff) weightn[tindx] = 1. # tile across size bins weightn = np.tile(weightn.reshape(-1, 1), [1, num_sb-wall_on]) # corrected concentration Nwetn[it, :] = np.sum(Nsim*weightn, axis=0) xnn[it, :] = np.sum(xsim*weightn, axis=0) Nwet = Nwetn # rename Nwet xn = xnn # size bin radii (um) # correct for coincidence (only relevant at relatively moderate # concentrations (# particles/cm3)), using eq. 11 of # https://doi.org/10.1080/02786826.2012.737049 # where Q is the volumetric flow (cm3/s) rate and tau is the instrument # dead time (s) # bypass if coincidence flagged to not be considered if ((Q == -1)*(tau == -1)*(coi_maxD == -1) != 1): from scipy.special import lambertw # product of actual concentration with volumetric flow rate and instrument dead time Ca = Nwet.sum(axis=1) # cannot invert the Lambert function (eq. 9 of https://doi.org/10.1080/02786826.2012.737049) # directly as do not know the imaginary part, but can identify closest point to real part as we # we know that measure count must lie between blank counts and actual concentration for it in range(len(times)): # time loop # bypass if actual total particle concentration (# particles/cm3) # exceeds maximum that coincidence applicable to or is less than # blank concentration if (Ca[it] > cdt and Ca[it] < coi_maxD): # the possible measured counts (# particles/cm3) x_poss = np.logspace(np.log10(cdt), np.log10(coi_maxD), int(1e3)) # account for volumetric flow rate and dead time x_possn = -x_poss*(Q*tau) # take the Lambert function and obtain just the real part x_possn = (-lambertw(x_possn).real)/(Q*tau) # zero any negatives as these are useless x_possn[x_possn<0] = 0 # find point closest to actual concentration (# particles/cm3) # if all possibilities fall below the actual concentration, then # the instrument will have marked this as a maximum if all(x_possn < Ca[it]): Cm = coi_maxD else: # linear interpolation diff = (Ca[it]-x_possn) indx1 = (diff == np.max(diff[diff<=0.])) indx0 = (diff == np.min(diff[diff>0.])) # the measured concentration (# particles/cm3) diff[indx1] = -1*diff[indx1] # make absolute Cm = (x_poss[indx0]*(diff[indx1])+x_poss[indx1]*(diff[indx0]))/(diff[indx1]+diff[indx0]) # get the fraction underestimation due to coincidence frac_un = Cm/Ca[it] # correct across all size bins (# particles/cm3) Nwet[it, :] = Nwet[it, :]*(frac_un) # moving-average over averaging interval # number of outputs within averaging interval # note that using int here means rounding down, which is sensible av_num = int(av_int/times[1]-times[0]) if (av_num > 1): # empty array to hold moving averages (# particles/cm3) Nwetn = np.zeros((int(Nwet.shape[0]-(av_num-1)), Nwet.shape[1])) # empty array to hold moving average radii (um) xnn = np.zeros((int(Nwet.shape[0]-(av_num-1)), Nwet.shape[1])) for avi in range(av_num): # (# particles/cm3) Nwetn[:, :] += Nwet[avi:Nwet.shape[0]-(av_num-avi-1), :]/av_num # radii (um) xnn[:, :] += xn[avi:Nwet.shape[0]-(av_num-avi-1), :]/av_num # correct time (s) times = times[av_num-1::] # return to working variable names Nwet = Nwetn xn = xnn # prepare for interpolating concentrations of simulated sizes to instrument size bins --------------------------- # if no maximum or minimum size given, then assume same limits as simulation if (max_size[0] == -1): # no minimum diameter given (nm) max_size[0] = np.min(np.min(xn[xn>0.]*2.e3)) if (max_size[1] == -1): # no maximum diameter given (nm) max_size[1] = np.max(np.max(xn*2.e3)) # total number of decades of particle size for instrument dec = (np.log10(max_size[1])-np.log10(max_size[0])) # total number of size bins nsb_ins = int(dec*csbn) # instrument size bin bounds (for diameters) (nm) ins_sizbb = np.logspace(np.log10(max_size[0]), np.log10(max_size[1]), num = (nsb_ins+1), base = 10.) # instrument size bin centres (diameter) (nm) ins_sizc = ins_sizbb[0:-1]+np.diff(ins_sizbb) # difference (nm) between simulated size bins (diameter) sim_diff = np.diff(rbou_rec*2.e3, axis = 1) # difference (nm) between measurement size bins (diameter) ins_diff = np.diff(ins_sizbb) # normalise simulated particle number concentrations by size bin width (diameters) (# particles/cm3/nm) Nwet = Nwet/sim_diff # empty array for holding particle concentrations in instrument size bins (# particles/cm3) Nwetn = np.zeros((Nwet.shape[0], nsb_ins)) # account for size dependent detection efficiency below one # get detection efficiency as a function of particle size (diameter) (um) [Dp, ce] = count_eff_plot(3, 0, self, sdt) # empty array to hold detection efficiencies across times and simulation size bins # Dp is in um ce_t = np.zeros((len(times), nsb_ins)) # loop through times for it in range(len(times)): # distribute normalised simulated particle concentrations across instrument size array (# particles/cm3/nm) insNwet = np.interp(ins_sizc, xn[it, :]*2.e3, Nwet[it, :]) # correct to width of instrument size bins (# particles/cm3) Nwetn[it, :] = insNwet*ins_diff # interpolate detection efficiency (fraction) to instrument size bin centres # Dp is in um, so convert to nm ce_t[it, :] = np.interp(ins_sizc, Dp*1.e3, ce) # correct for upper size range of instrument, note conversion of # upper size from nm to um if (max_size[-1] != -1): size_indx = (ins_sizc > max_size[-1]) Nwetn[it, size_indx] = 0. Nwetn = Nwetn*ce_t # correct for detection efficiency # rename Nwetn variable Nwet = np.zeros((Nwetn.shape[0], Nwetn.shape[1])) Nwet[:, :] = Nwetn[:, :] # account for false background counts # (minimum detectable particle concentration) (# particles/cm3) Nwet[Nwet < cdt] = cdt # account for maximum particle concentration (# particles/cm3) if (max_dt != -1): # if maximum particle concentration to be considered Nwet[Nwet > max_dt] = max_dt if (caller == 0): # when called from gui plt.ion() # show results to screen and turn on interactive mode # plot temporal profile of particle number size distribution (# particles/cm3/log10(Dp)) # prepare figure ------------------------------------------- fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) # the difference in log10 of size bin boundaries (diameters (nm)) dlog10D = (np.diff(np.log10(ins_sizbb))).reshape(1, -1) # number size distribution contours (/cc (air)) dNdlog10D = np.zeros((Nwet.shape[0], Nwet.shape[1])) dNdlog10D[:, :] = Nwet[:, :]/dlog10D[:, :] # transpose ready for contour plot dNdlog10D = np.transpose(dNdlog10D) # mask any nan values so they are not plotted z = np.ma.masked_where(np.isnan(dNdlog10D), dNdlog10D) # customised colormap (https://www.rapidtables.com/web/color/RGB_Color.html) colors = [(0.6, 0., 0.7), (0, 0, 1), (0, 1., 1.), (0, 1., 0.), (1., 1., 0.), (1., 0., 0.)] # R -> G -> B n_bin = 100 # discretizes the colormap interpolation into bins cmap_name = 'my_list' # create the colormap cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin) # set contour levels levels = (MaxNLocator(nbins = 100).tick_values(np.min(z[~np.isnan(z)]), np.max(z[~np.isnan(z)]))) # fix upper value of contours, e.g. when trying to compare plots #levels = (MaxNLocator(nbins = 100).tick_values(np.min(z[~np.isnan(z)]), # 1.89e5)) # associate colours and contour levels norm1 = BoundaryNorm(levels, ncolors=cm.N, clip=True) # contour plot with times (hours) along x axis and # particle diameters (nm) along y axis p1 = ax0.pcolormesh(times/3600.0, ins_sizbb, z, cmap = cm, norm = norm1, shading = 'auto') ax0.set_yscale("log") # set vertical axis to logarithmic spacing # set tick format for vertical axis ax0.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) cb = plt.colorbar(p1, format=ticker.FuncFormatter(fmt)) cb.ax.tick_params(labelsize=14) # colour bar label cb.set_label('dN (#$\,$$\mathrm{cm^{-3}}$)/d$\,$log$_{10}$(D$\mathrm{_p}$ ($\mathrm{nm}$))', size=14, rotation=270, labelpad=20) # set tick format for vertical axis ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.set_ylabel('Diameter (nm)', size = 14) ax0.xaxis.set_tick_params(labelsize = 14, direction = 'in', which= 'both') ax0.yaxis.set_tick_params(labelsize = 14, direction = 'in', which= 'both') ax0.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) ax0.set_title('Simulated particle number concentration convolved to represent \nscanning mobility particle spectrometer (SMPS) measurements') if (caller == 2): # display when in test mode plt.show() return()
def plotter_prod(caller, dir_path, comp_names_to_plot, tp, uc, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # comp_names_to_plot - chemical scheme names of components to plot # tp - times to calculate between (hours) # uc - units to use for change tendency # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, _, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, _, _, _, _, _, _, ro_obj) = retr_out.retr_out(dir_path) # loop through components due to be plotted, to check they are available for comp_name in (comp_names_to_plot): fname = str(dir_path + '/' + comp_name + '_rate_of_change') try: # try to open dydt = np.loadtxt(fname, delimiter=',', skiprows=0) # skiprows = 0 skips first header except: mess = str( 'Please note, a change tendency record for the component ' + str(comp_name) + ' was not found, was it specified in the tracked_comp input of the model variables file? Please see README for more information.' ) self.l203a.setText(mess) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return () # if all files are available, then proceed without error message mess = str('') self.l203a.setText(mess) if (self.bd_pl < 3): self.l203a.setStyleSheet(0., '0px solid red', 0., 0.) self.bd_pl == 3 for comp_name in (comp_names_to_plot): # loop through components to plot if (comp_name != 'HOMRO2' and comp_name != 'RO2'): ci = comp_names.index(comp_name) # get index of this component if (comp_name == 'HOMRO2'): # get mean molecular weight of HOMRO2 (g/mol) counter = 0 # count on HOMRO2 components mw_extra = 0. # count on HOMRO2 molecular weights (g/mol) for cni in range(len(comp_names)): # loop through all components if 'API_' in comp_names[cni] or 'api_' in comp_names[cni]: if 'RO2' in comp_names[cni]: mw_extra += y_mw[cni] counter += 1 y_mw = np.concatenate( (y_mw, (np.array(mw_extra / counter)).reshape(1))) ci = len(y_mw) - 1 if (comp_name == 'RO2'): # get indices of RO2 RO2indx = (np.array((ro_obj.gi['RO2i']))) # get mean molecular weight of RO2 (g/mol) mw_extra = np.sum((np.array((y_mw)))[RO2indx]) / len(RO2indx) y_mw = np.concatenate((y_mw, (np.array(mw_extra)).reshape(1))) ci = len(y_mw) - 1 # note that penultimate column in dydt is gas-particle # partitioning and final column is gas-wall partitioning, whilst # the first row contains chemical reaction numbers # prepare to store results of change tendency due to chemical reactions res = np.zeros((dydt.shape[0], dydt.shape[1] - 2)) res[:, :] = dydt[:, 0: -2] # get chemical reaction numbers and change tendencies if (uc == 0): Cfaca = (np.array(Cfac)).reshape( -1, 1) # convert to numpy array from list # convert change tendencies from # molecules/cm3/s to ppb/s res[1::, :] = (res[1::, :] / Cfaca[1::]) ct_units = str('ppb') if (uc == 1): # convert change tendencies from # molecules/cm3/s to ug/m3/s res[1::, :] = ((res[1::, :] / si.N_A) * y_mw[ci]) * 1.e12 ct_units = str(u'\u03BC' + 'g/m' + u'\u00B3') if (uc == 2): # keep change tendencies as # molecules/cm3/s res[1::, :] = res[1::, :] ct_units = str(u'\u0023' + ' molecules/cm' + u'\u00B3') # remove reactions that destroy component res[1::, :][res[1::, :] < 0] = 0. # sum production rates over production reactions res_sum = np.sum(res[1::, :], axis=1) # retain only the required time period tindx = (timehr >= tp[0]) * (timehr < tp[1]) res_sum = res_sum[tindx[0:-1]] # time intervals over this period (s) tindx = (timehr >= tp[0]) * (timehr <= tp[1]) tint = (timehr[tindx][1::] - timehr[tindx][0:-1]) * 3600. # if one short then concatenate assuming same interval if len(tint) < (len(res_sum)): tint = np.concatenate((tint, np.reshape(np.array(tint[-1]), 1))) # integrate production rate to get total production res_sum = np.sum(res_sum * tint) # display to message board mess = str(mess + 'Total production of ' + str(comp_name) + ': ' + str(res_sum) + ' ' + ct_units) self.l203a.setText(mess) # set border around message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed magenta', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid magenta', 0., 0.) self.bd_pl = 1 return () # return now return ()
# create figure to plot results fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(12, 6.5), sharex='all') fig.subplots_adjust(hspace=0.05) # ------------------------------------------------------------------------------ # results - note that all results for table 1 saved in fig11_data cwd = os.getcwd() # get current working directory try: # if calling from the PyCHAM home directory output_by_sim = str( cwd + '/PyCHAM/output/GMD_paper_plotting_scripts/fig11_data/nuc_vsobs_output_fm_n1_2e4_n2_-4e2_n3_1e2' ) # required outputs (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, xfm, time_array, comp_names, _, N, _, y_MV, _, wall_on, space_mode) = retr_out.retr_out(output_by_sim) except: # if calling from the GMD paper Results folder output_by_sim = str( cwd + '/fig11_data/nuc_vsobs_output_fm_n1_2e4_n2_-4e2_n3_1e2') # required outputs (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, xfm, time_array, comp_names, _, N, _, y_MV, _, wall_on, space_mode) = retr_out.retr_out(output_by_sim) dlog10D = np.log10(rbou_rec[:, 1::] * 2.0) - np.log10(rbou_rec[:, 0:-1] * 2.0) dNdD = Ndry / dlog10D # normalised number size distribution (#/cc (air)) # observation part --------------------------------------------------------------- import xlrd # for opening xlsx file try: # if calling from the PyCHAM home folder
def plotter(caller, dir_path, comp_names_to_plot, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0 for individual components, 3 for # total excluding seed and water, 4 for top contributors to # particle-phase, 5 for particle surface area concentration, # 6 for seed surface area concentration, 7 for top contributors # to particle-phase excluding seed and water) # or tests (2) are the calling module # dir_path - path to folder containing results files to plot # comp_names_to_plot - chemical scheme names of components to plot # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, rel_SMILES, y_MW, Nwet, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, H2Oi, seedi, _, _, group_indx, _, ro_obj) = retr_out.retr_out(dir_path) # number of actual particle size bins num_asb = (num_sb - wall_on) if (caller == 0 or caller >= 3): plt.ion() # show results to screen and turn on interactive mode # prepare plot fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) if (comp_names_to_plot): # if component names specified # concentration plot --------------------------------------------- for i in range(len(comp_names_to_plot)): if (comp_names_to_plot[i].strip() == 'H2O'): indx_plot = [H2Oi] indx_plot = np.array((indx_plot)) if (comp_names_to_plot[i].strip() == 'RO2'): indx_plot = (np.array((group_indx['RO2i']))) if (comp_names_to_plot[i].strip() == 'RO'): indx_plot = (np.array((group_indx['ROi']))) if (comp_names_to_plot[i].strip() != 'H2O' and comp_names_to_plot[i].strip() != 'RO2' and comp_names_to_plot[i].strip() != 'RO'): try: # will work if provided components were in simulation chemical scheme # get index of this specified component, removing any white space indx_plot = [ comp_names.index(comp_names_to_plot[i].strip()) ] indx_plot = np.array((indx_plot)) except: self.l203a.setText( str('Component ' + comp_names_to_plot[i] + ' not found in chemical scheme used for this simulation' )) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return () # particle-phase concentrations of all components (# molecules/cm3) if (wall_on == 1): # wall on ppc = yrec[:, num_comp:-num_comp] if (wall_on == 0): # wall off ppc = yrec[:, num_comp::] # particle-phase concentration of this component over all size bins (# molecules/cm3) conc = np.zeros((ppc.shape[0], num_sb - wall_on)) for indxn in indx_plot: # loop through the indices concf = ppc[:, indxn::num_comp] # particle-phase concentration (ug/m3) conc[:, :] += ((concf / si.N_A) * y_MW[indxn]) * 1.e12 if (comp_names_to_plot[i].strip() != 'RO2' and comp_names_to_plot[i].strip() != 'RO'): # if not a sum # plot this component ax0.plot(timehr, conc.sum(axis=1), '+', linewidth=4., label=str( str(comp_names[indx_plot[0]]) + ' (particle-phase)')) if (comp_names_to_plot[i].strip() == 'RO2' ): # if is the sum of organic peroxy radicals ax0.plot(timehr, conc.sum(axis=1), '-+', linewidth=4., label=str(r'$\Sigma$RO2 (particle-phase)')) if (comp_names_to_plot[i].strip() == 'RO' ): # if is the sum of organic alkoxy radicals ax0.plot(timehr, conc.sum(axis=1), '-+', linewidth=4., label=str(r'$\Sigma$RO (particle-phase)')) ax0.set_ylabel(r'Concentration ($\rm{\mu}$g$\,$m$\rm{^{-3}}$)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) # end of gas-phase concentration sub-plot --------------------------------------- # if called by button to plot temporal profile of total particle-phase concentration # excluding water and seed if (caller == 3): import scipy.constants as si # particle-phase concentrations of all components (# molecules/cm3) if (wall_on == 1): # wall on ppc = yrec[:, num_comp:-num_comp] if (wall_on == 0): # wall off ppc = yrec[:, num_comp::] # zero water and seed ppc[:, H2Oi::num_comp] = 0. for i in seedi: # loop through seed components ppc[:, i::num_comp] = 0. # tile molar weights over size bins and times y_mwt = np.tile(np.array((y_MW)).reshape(1, -1), (1, num_sb - wall_on)) y_mwt = np.tile(y_mwt, (ppc.shape[0], 1)) # convert from # molecules/cm3 to ug/m3 ppc = (ppc / si.N_A) * y_mwt * 1.e12 # sum over components and size bins ppc = np.sum(ppc, axis=1) # plot ax0.plot(timehr, ppc, '+', linewidth=4., label='total particle-phase excluding seed and water') ax0.set_ylabel(r'Concentration ($\rm{\mu}$g$\,$m$\rm{^{-3}}$)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) # if called by button to plot top contributors to particle-phase if (caller == 4): import scipy.constants as si # particle-phase concentrations of all components (# molecules/cm3) if (wall_on == 1): # wall on ppc = yrec[:, num_comp:-num_comp] if (wall_on == 0): # wall off ppc = yrec[:, num_comp::] # tile molar weights over size bins and times y_mwt = np.tile( np.array((y_MW)).reshape(1, -1), (1, (num_sb - wall_on))) y_mwt = np.tile(y_mwt, (ppc.shape[0], 1)) # convert to ug/m3 from # molecules/cm3 ppc = ((ppc / si.N_A) * y_mwt) * 1.e12 # sum particle-phase concentration per component over time (ug/m3) ppc_t = (np.sum(ppc, axis=0)).reshape(1, -1) # sum particle-phase concentrations over size bins (ug/m3), # but keeping components separate ppc_t = np.sum(ppc_t.reshape(num_sb - wall_on, num_comp), axis=0) # convert to mass contributions ppc_t = ((ppc_t / np.sum(ppc_t)) * 100.).reshape(-1, 1) # rank in ascending order ppc_ts = np.flip(np.sort(np.squeeze(ppc_t))) # take just top number as supplied by user ppc_ts = ppc_ts[0:self.e300r] # sum concentrations over size bins and components ppc_sbc = np.sum(ppc, axis=1) # loop through to plot for i in range(self.e300r): # get index indx = np.where(ppc_t == ppc_ts[i])[0][0] # get name namei = comp_names[indx] # sum for this component over size bins (ug/m3) ppci = np.sum(ppc[:, indx::num_comp], axis=1) # get contribution (%) ppci = (ppci[ppc_sbc > 0.] / ppc_sbc[ppc_sbc > 0.]) * 100. ax0.plot(timehr[ppc_sbc > 0.], ppci, '-+', linewidth=4., label=namei) ax0.set_ylabel( r'Contribution to particle-phase mass concentration (%)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) # if called by button to plot top contributors to particle-phase excluding seed and water if (caller == 7): import scipy.constants as si # particle-phase concentrations of all components (# molecules/cm3) if (wall_on == 1): # wall on ppc = yrec[:, num_comp:-num_comp] if (wall_on == 0): # wall off ppc = yrec[:, num_comp::] for i in seedi: # loop through seed components ppc[:, i::num_comp] = 0. # zero seed ppc[:, H2Oi::num_comp] = 0. # zero water # tile molar weights over size bins and times y_mwt = np.tile( np.array((y_MW)).reshape(1, -1), (1, (num_sb - wall_on))) y_mwt = np.tile(y_mwt, (ppc.shape[0], 1)) # convert to ug/m3 from # molecules/cm3 ppc = ((ppc / si.N_A) * y_mwt) * 1.e12 # sum particle-phase concentration per component over time (ug/m3) ppc_t = (np.sum(ppc, axis=0)).reshape(1, -1) # sum particle-phase concentrations over size bins (ug/m3), # but keeping components separate ppc_t = np.sum(ppc_t.reshape(num_sb - wall_on, num_comp), axis=0) # convert to mass contributions ppc_t = ((ppc_t / np.sum(ppc_t)) * 100.).reshape(-1, 1) # rank in ascending order ppc_ts = np.flip(np.sort(np.squeeze(ppc_t))) # take just top number as supplied by user ppc_ts = ppc_ts[0:self.e300r] # sum concentrations over size bins and components ppc_sbc = np.sum(ppc, axis=1) # loop through to plot for i in range(self.e300r): # get index indx = np.where(ppc_t == ppc_ts[i])[0][0] # get name namei = comp_names[indx] # sum for this component over size bins (ug/m3) ppci = np.sum(ppc[:, indx::num_comp], axis=1) # get contribution (%) ppci = (ppci[ppc_sbc > 0.] / ppc_sbc[ppc_sbc > 0.]) * 100. ax0.plot(timehr[ppc_sbc > 0.], ppci, '-+', linewidth=4., label=namei) ax0.set_ylabel( r'Contribution to particle-phase mass concentration (%)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) # if called by button to plot total particle surface area concentration (m2/m3) if (caller == 5): # get surface area of single particles per size bin # at all times (m2), note radius converted from um # to m asp = 4. * np.pi * (x * 1.e-6)**2. # integrate over all particles in a size bin, # note conversion from /cm3 to /m3 (m2/m3) asp = asp * (Nwet * 1.e6) # sum over size bins (m2/m3) asp = np.sum(asp, axis=1) # plot ax0.plot(timehr, asp, '-+', linewidth=4.) ax0.set_ylabel( r'Total particle-phase surface area concentration ($\rm{m^{2}\,m^{-3}}$)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') # if called by button to plot seed particle surface area concentration (m2/m3) if (caller == 6): import scipy.constants as si # particle-phase concentrations of all components (# molecules/cm3) if (wall_on == 1): # wall on ppc = yrec[:, num_comp:-num_comp] if (wall_on == 0): # wall off ppc = yrec[:, num_comp::] # empty results array for seed particle volume concentrations per size bin ppcs = np.zeros((ppc.shape[0], (num_sb - wall_on))) for i in seedi: # loop through seed indices # get just seed component particle-phase volume concentration # (cm3/cm3) ppcs[:, :] += (ppc[:, i::num_comp] / si.N_A) * (np.array( (y_MV))[i]) # convert total volume to volume per particle (cm3) ppcs[Nwet > 0] = ppcs[Nwet > 0] / Nwet[Nwet > 0] # convert volume to radius (cm) ppcs = ((3. * ppcs) / (4. * np.pi))**(1. / 3.) # convert cm to m ppcs = ppcs * 1.e-2 # get surface area over all particles in a size bin (m2/m3), # note conversion from m2/cm3 to m2/m3 ppcs = (4. * np.pi * ppcs**2.) * Nwet * 1.e6 # sum over all size bins (m2/m3) ppcs = np.sum(ppcs, axis=1) # plot ax0.plot(timehr, ppcs, '-+', linewidth=4.) ax0.set_ylabel( r'Seed particle-phase surface area concentration ($\rm{m^{2}\,m^{-3}}$)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') if ( caller == 8 ): # if called by button to plot generational contribution to particle-phase mass import sch_interr # for interpeting chemical scheme import re # for parsing chemical scheme import scipy.constants as si gen_num = [] # empty results list ci = 0 # component count sch_name = ro_obj.sp inname = ro_obj.vp sch_name = str(dir_path + '/inputs/api_iso_ch4_mechHOM_scheme.txt') inname = str(dir_path + '/inputs/api_iso_ch4_mechHOM_model_var.txt') f_open_eqn = open(sch_name, mode='r') # open the chemical scheme file # read the file and store everything into a list total_list_eqn = f_open_eqn.readlines() f_open_eqn.close() # close file inputs = open(inname, mode='r') # open model variables file in_list = inputs.readlines( ) # read file and store everything into a list inputs.close() # close file for i in range(len(in_list) ): # loop through supplied model variables to interpret # ---------------------------------------------------- # if commented out continue to next line if (in_list[i][0] == '#'): continue key, value = in_list[i].split('=') # split values from keys # model variable name - a string with bounding white space removed key = key.strip() # ---------------------------------------------------- if key == 'chem_scheme_markers' and ( value.strip()): # formatting for chemical scheme chem_sch_mrk = [str(i).strip() for i in (value.split(','))] # interrogate scheme to list equations [eqn_list, aqeqn_list, eqn_num, rrc, rrc_name, RO2_names] = sch_interr.sch_interr(total_list_eqn, chem_sch_mrk) # loop through components to identify their generation for compi in comp_names[0:-2]: # don't include core and water comp_fin = 0 # flag for when component generation number found gen_num.append(0) # assume zero generation by default #print(ci, compi, rel_SMILES[ci], len(comp_names), len(rel_SMILES)) # if an organic molecule if ('C' in rel_SMILES[ci] or 'c' in rel_SMILES[ci]): # loop through reactions to identify where this first occurs, # assuming that if first occurrence is as a reactant # it is zero generation and if as a product is >zero generation for eqn_step in range(len(eqn_list)): line = eqn_list[eqn_step] # extract this line # work out whether equation or reaction rate coefficient part comes first eqn_start = str('.*\\' + chem_sch_mrk[10]) rrc_start = str('.*\\' + chem_sch_mrk[9]) # get index of these markers, note span is the property of the match object that # gives the location of the marker eqn_start_indx = (re.match(eqn_start, line)).span()[1] rrc_start_indx = (re.match(rrc_start, line)).span()[1] if (eqn_start_indx > rrc_start_indx): eqn_sec = 1 # equation is second part else: eqn_sec = 0 # equation is first part # split the line into 2 parts: equation and rate coefficient # . means match with anything except a new line character., when followed by a * # means match zero or more times (so now we match with all characters in the line # except for new line characters, so final part is stating the character(s) we # are specifically looking for, \\ ensures the marker is recognised if eqn_sec == 1: eqn_markers = str('\\' + chem_sch_mrk[10] + '.*\\' + chem_sch_mrk[11]) else: # end of equation part is start of reaction rate coefficient part eqn_markers = str('\\' + chem_sch_mrk[10] + '.*\\' + chem_sch_mrk[9]) # extract the equation as a string ([0] extracts the equation section and # [1:-1] removes the bounding markers) eqn = re.findall(eqn_markers, line)[0][1:-1].strip() eqn_split = eqn.split() eqmark_pos = eqn_split.index('=') # reactants with stoichiometry number and omit any photon reactants = [ i for i in eqn_split[:eqmark_pos] if i != '+' and i != 'hv' ] # products with stoichiometry number products = [ t for t in eqn_split[eqmark_pos + 1:] if t != '+' ] for ri in reactants: # note that no spaces or other punctuation included around # the component name as extracted from the equation if compi in ri and len(compi) == len(ri): # assuming that components appearing first as a reactant # must be zero generation gen_num[ci] = 0 # finished with this component, break out of reactant loop comp_fin = 1 break for pi in products: # note that no spaces or other punctuation included around # the component name as extracted from the equation if compi in pi and len(compi) == len(pi): # check which reactant has earliest generation rcheck = [] for ri in reactants: # get its index rindx = comp_names.index(ri) # if an organic, note if inorgnic then ignore if ('C' in rel_SMILES[rindx] or 'c' in rel_SMILES[rindx]): if ( rindx < ci ): # if generation number of recatant already known rcheck.append(gen_num[rindx]) # identify earliest generation reactant egen = np.min(rcheck) # check whether this species has a charge, i.e. is open shell, note that according # to DAYLIGHT, the square brackets in a SMILES string deontes when an atom has a # valence other than normal, i.e. is in an excited (radical) state: # https://daylight.com/dayhtml/doc/theory/theory.smiles.html if ('[' in rel_SMILES[ci] or ']' in rel_SMILES[ci]): # if open-shell gen_num[ci] = egen else: # if closed-shell gen_num[ci] = egen + 1 # finished with this component, break out of reactant loop comp_fin = 1 break # finished with this component, break out of equation loop, # move onto next component if (comp_fin == 1): break ci += 1 # component count # convert list to numpy array gen_num = np.array(gen_num) # append two zeros for core and water gen_num = np.concatenate((gen_num, np.zeros(2)), axis=0) # in case we want to see generation number per component ------------- #ax0.plot(np.arange(len(comp_names)), gen_num, '+') #ax0.set_xticks(np.arange(len(comp_names))) #ax0.set_xticklabels(comp_names, rotation = 90) # -------------------------------------------------------------------- # empty results matrix for contribution (%) to particle-phase mass per # generation per time step res = np.zeros((len(timehr), int(np.max(gen_num)))) # particle-phase concentrations of all components (# molecules/cm3) if (wall_on == 1): # wall on ppc = yrec[:, num_comp:-num_comp] if (wall_on == 0): # wall off ppc = yrec[:, num_comp::] y_MW = np.tile(y_MW, (len(timehr), num_sb - wall_on)) # convert particle-phase concentrations into mass # concentration (ug/m3) from # molecules/cm3 ppc[:, :] += ((ppc / si.N_A) * y_MW) * 1.e12 # zero water and seed contribution ppc[:, int(H2Oi)::int(num_comp)] = 0. for seei in seedi: ppc[:, int(seei)::int(num_comp)] = 0. # sum concentrations over all size bins # and components per time step (ug/m3) ppc_sum = np.sum(ppc, axis=1) # tile generation number over size bins gen_num = np.tile(gen_num, (num_sb - wall_on)) # loop through generations for gi in range(int(np.max(gen_num))): # get percentage contribution from this generation res[:, gi] = (np.sum(ppc[:, gen_num == gi], axis=1) / ppc_sum) * 100. ax0.plot(timehr, res[:, gi], label=str('Generation ' + str(gi))) ax0.set_ylabel( r'% Contribution to organic particle-phase mass concentration ($\%$)', fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) return () # display if (caller == 2): plt.show() return ()
def plotter(caller, dir_path, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # self - reference to GUI # -------------------------------------------------------------------------- # retrieve results (_, _, _, _, _, _, _, timehr, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, cham_env, _, _, _) = retr_out.retr_out(dir_path) # check whether chamber environment variables were saved and therefore # retrieved if (cham_env == []): mess = str('Please note, no chamber environmental variables were found, perhaps the simulation was completed in a PyCHAM version predating this functionality') self.l203a.setText(mess) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return() # prepare figure plt.ion() # display figure in interactive mode fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) # ensure that all axes can be seen plt.subplots_adjust(left=0.1, right=0.8) # parasite axis part --------------------------------------------------- par1 = ax0.twinx() # first parasite axis par2 = ax0.twinx() # second parasite axis # Offset the right spine of par2. The ticks and label have already been # placed on the right by twinx above. par2.spines["right"].set_position(("axes", 1.11)) # Having been created by twinx, par2 has its frame off, so the line of its # detached spine is invisible. First, activate the frame but make the patch # and spines invisible. make_patch_spines_invisible(par2) # second, show the right spine par2.spines['right'].set_visible(True) # ------------------------------------------------------------------------- # plot temporal profiles # temperature on original vertical axis p0, = ax0.plot(timehr, cham_env[:, 0], 'k', label = 'temperature (K)') ax0.set_ylabel('Temperature (K)', size=16) ax0.yaxis.label.set_color('black') ax0.tick_params(axis='y', colors='black') ax0.spines['left'].set_color('black') ax0.yaxis.set_tick_params(direction = 'in', which = 'both') # pressure on first parasite axis p1, = par1.plot(timehr, cham_env[:, 1], '--m', label = 'pressure (Pa)') par1.set_ylabel('Pressure (Pa)', rotation=270, size=16, labelpad = 15) par1.yaxis.label.set_color('magenta') par1.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.2e')) # set tick format for vertical axis par1.tick_params(axis='y', colors='magenta') par1.spines['right'].set_color('magenta') par1.yaxis.set_tick_params(direction = 'in', which = 'both') # relative humidity on second parasite axis p2, = par2.plot(timehr, cham_env[:, 2], '-.b', label = 'relative humidity (fraction)') par2.set_ylabel('Relative Humidity (0-1)', rotation=270, size=16, labelpad = 15) par2.yaxis.label.set_color('blue') par2.tick_params(axis='y', colors='blue') par2.spines['right'].set_color('blue') par2.yaxis.set_tick_params(direction = 'in', which = 'both') ax0.xaxis.set_tick_params(direction = 'in', which = 'both') ax0.set_xlabel('Time through experiment (hours)', size=16) plt.legend(fontsize=14, handles=[p0, p1, p2], loc=4) return()
def plotter(caller, dir_path, uc, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # uc - number representing the units to be used for gas-phase concentrations # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, Nwet, _, y_MV, _, wall_on, space_mode, indx_plot, comp0, _, PsatPa, OC, _, _, _, _, _, _, _) = retr_out.retr_out(dir_path) # number of actual particle size bins num_asb = (num_sb - wall_on) if (caller == 0): plt.ion() # show results to screen and turn on interactive mode # prepare sub-plots depending on whether particles present if (num_asb == 0): # no particle size bins if not (indx_plot ): # check whether there are any gaseous components to plot mess = str( 'Please note, no initial gas-phase concentrations were received and no particle size bins were present, therefore there is nothing for the standard plot to show' ) self.l203a.setText(mess) return () # if there are gaseous components to plot, then prepare figure fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) mess = str( 'Please note, no particle size bins were present, therefore the particle-phase standard plot will not be shown' ) self.l203a.setText(mess) if (num_asb > 0): if not (indx_plot): mess = str( 'Please note, no initial gas-phase concentrations were registered, therefore the gas-phase standard plot will not be shown' ) self.l203a.setText(mess) # if there are no gaseous components then prepare figure fig, (ax1) = plt.subplots(1, 1, figsize=(14, 7)) else: # if there are both gaseous components and particle size bins then prepare figure fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(14, 7)) # parasite axis setup -------------------------------------------------------------- par1 = ax1.twinx() # first parasite axis par2 = ax1.twinx() # second parasite axis # Offset the right spine of par2. The ticks and label have already been # placed on the right by twinx above. par2.spines["right"].set_position(("axes", 1.2)) # Having been created by twinx, par2 has its frame off, so the line of its # detached spine is invisible. First, activate the frame but make the patch # and spines invisible. make_patch_spines_invisible(par2) # second, show the right spine par2.spines["right"].set_visible(True) # ---------------------------------------------------------------------------------------- if (indx_plot): ymax = 0. # start tracking maximum value for plot label # action units for gas-phase concentrations if (uc == 0): # ppb gp_conc = yrec[:, 0:num_comp] # ppb is original units gpunit = '(ppb)' if (uc == 1 or uc == 2): # ug/m3 or # molecules/cm3 y_MW = np.array(y_mw) # convert to numpy array from list Cfaca = (np.array(Cfac)).reshape( -1, 1) # convert to numpy array from list gp_conc = yrec[:, 0:num_comp] # # molecules/cm3 gp_conc = gp_conc.reshape(yrec.shape[0], num_comp) * Cfaca gpunit = str('\n(' + u'\u0023' + ' molecules/cm' + u'\u00B3' + ')') if (uc == 1): # ug/m3 gp_conc = ((gp_conc / si.N_A) * y_MW) * 1.e12 gpunit = str('(' + u'\u03BC' + 'g/m' + u'\u00B3' + ')') # gas-phase concentration sub-plot --------------------------------------------- for i in range(len(indx_plot)): ax0.semilogy(timehr, gp_conc[:, indx_plot[i]], '+', linewidth=4.0, label=str(str(comp0[i]).strip())) ymax = max(ymax, max(yrec[:, indx_plot[i]])) if (uc == 1 or uc == 2): # ug/m3 or # molecules/cm3 ax0.set_ylabel(r'Gas-phase concentration ' + gpunit, fontsize=14) if (uc == 0): # ppb ax0.set_ylabel(r'Gas-phase mixing ratio ' + gpunit, fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.xaxis.set_tick_params(labelsize=14, direction='in', which='both') ax0.legend(fontsize=14) # find maximum and minimum of plotted concentrations for sub-plot label maxy = max(yrec[:, indx_plot].flatten()) miny = min(yrec[:, indx_plot].flatten()) if (num_asb > 0): # if more than one plot # get the location of ticks locs = ax0.get_yticks() maxloc = max(locs) ax0.text(x=timehr[0] - (timehr[-1] - timehr[0]) / 9.5, y=ymax * 1.05, s='a)', size=14) # end of gas-phase concentration sub-plot --------------------------------------- # particle properties sub-plot -------------------------------------------------- if (num_asb > 0): # if particles present if (timehr.ndim == 0): # occurs if only one time step saved Ndry = np.array(Ndry.reshape(1, num_asb)) x = np.array(x.reshape(1, num_asb)) rbou_rec = np.array(rbou_rec.reshape(1, num_sb)) if (num_asb == 1 ): # just one particle size bin (wall included in num_sb) Ndry = np.array(Ndry.reshape(len(timehr), num_asb)) x = np.array(x.reshape(len(timehr), num_asb)) # plotting number size distribution -------------------------------------- # don't use the first boundary as it could be zero, which will error when log10 taken log10D = np.log10(rbou_rec[:, 1::] * 2.) if (num_asb > 1): # note, can't append zero to start of log10D to cover first size bin as the log10 of the # non-zero boundaries give negative results due to the value being below 1, so instead # assume same log10 distance as the next pair log10D = np.append( (log10D[:, 0] - (log10D[:, 1] - log10D[:, 0])).reshape(-1, 1), log10D, axis=1) # radius distance covered by each size bin (log10(um)) dlog10D = (log10D[:, 1::] - log10D[:, 0:-1]).reshape( log10D.shape[0], log10D.shape[1] - 1) if (num_asb == 1): # single particle size bin # assume lower radius bound is ten times smaller than upper dlog10D = (log10D[:, 0] - np.log10( (rbou_rec[:, 1] / 10.) * 2.)).reshape(log10D.shape[0], 1) # number size distribution contours (/cc (air)) dNdlog10D = np.zeros((Nwet.shape[0], Nwet.shape[1])) dNdlog10D[:, :] = Nwet[:, :] / dlog10D[:, :] # transpose ready for contour plot dNdlog10D = np.transpose(dNdlog10D) # mask any nan values so they are not plotted z = np.ma.masked_where(np.isnan(dNdlog10D), dNdlog10D) # customised colormap (https://www.rapidtables.com/web/color/RGB_Color.html) colors = [(0.6, 0., 0.7), (0, 0, 1), (0, 1., 1.), (0, 1., 0.), (1., 1., 0.), (1., 0., 0.)] # R -> G -> B n_bin = 100 # discretizes the colormap interpolation into bins cmap_name = 'my_list' # create the colormap cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin) # set contour levels levels = (MaxNLocator(nbins=100).tick_values(np.min(z[~np.isnan(z)]), np.max(z[~np.isnan(z)]))) # associate colours and contour levels norm1 = BoundaryNorm(levels, ncolors=cm.N, clip=True) # contour plot with times (hours) along x axis and # particle diameters (nm) along y axis for ti in range(len(timehr) - 1): # loop through times p1 = ax1.pcolormesh(timehr[ti:ti + 2], (rbou_rec[ti, :] * 2 * 1e3), z[:, ti].reshape(-1, 1), cmap=cm, norm=norm1) # if logarithmic spacing of size bins specified, plot vertical axis # logarithmically if space_mode == 'log': ax1.set_yscale("log") # set tick format for vertical axis ax1.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.1e')) ax1.set_ylabel('Diameter (nm)', size=14) ax1.xaxis.set_tick_params(labelsize=14, direction='in', which='both') ax1.yaxis.set_tick_params(labelsize=14, direction='in', which='both') # label according to whether gas-phase plot also displayed if (indx_plot): ax1.text(x=timehr[0] - (timehr[-1] - timehr[0]) / 11., y=np.amax(rbou_rec * 2 * 1e3) * 1.05, s='b)', size=14) ax1.set_xlabel(r'Time through simulation (hours)', fontsize=14) cb = plt.colorbar(p1, format=ticker.FuncFormatter(fmt), pad=0.25, ax=ax1) cb.ax.tick_params(labelsize=14) # colour bar label cb.set_label( 'dN (#$\,$$\mathrm{cm^{-3}}$)/d$\,$log$_{10}$(D$\mathrm{_p}$ ($\mathrm{\mu m}$))', size=14, rotation=270, labelpad=20) # ---------------------------------------------------------------------------------------- # total particle number concentration # particles/cm3 # include total number concentration (# particles/cm3 (air)) on contour plot # first identify size bins with radius exceeding 3nm # empty array for holding total number of particles Nvs_time = np.zeros((Nwet.shape[0])) for i in range(num_asb): # size bin loop Nvs_time[:] += Nwet[:, i] # sum number p3, = par1.plot(timehr, Nvs_time, '+k', label='N') par1.set_ylabel('N (#$\,$ $\mathrm{cm^{-3})}$', size=14, rotation=270, labelpad=20) # vertical axis label par1.yaxis.set_major_formatter(ticker.FormatStrFormatter( '%.1e')) # set tick format for vertical axis par1.yaxis.set_tick_params(labelsize=14) # mass concentration of particles --------------------------------------------------------------- # array for mass concentration with time MCvst = np.zeros((1, len(timehr))) # first obtain just the particle-phase concentrations (molecules/cm3) yrp = yrec[:, num_comp:num_comp * (num_asb + 1)] # loop through size bins to convert to ug/m3 for sbi in range(num_asb): yrp[:, sbi * num_comp:(sbi + 1) * num_comp] = ( (yrp[:, sbi * num_comp:(sbi + 1) * num_comp] / si.N_A) * y_mw) * 1.e12 MCvst[0, :] = yrp.sum(axis=1) # log10 of maximum in mass concentration if (max(MCvst[0, :]) > 0): MCmax = int(np.log10(max(MCvst[0, :]))) else: MCmax = 0. p5, = par2.plot(timehr, MCvst[0, :], 'xk', label='Total Particle Mass Concentration') par2.set_ylabel(str('Mass Concentration ($\mathrm{\mu g\, m^{-3}})$'), rotation=270, size=16, labelpad=25) # set colour of label, tick font and corresponding vertical axis to match scatter plot presentation par2.yaxis.label.set_color('black') par2.tick_params(axis='y', colors='black') par2.spines['right'].set_color('black') par2.yaxis.set_major_formatter(ticker.FormatStrFormatter( '%.1e')) # set tick format for vertical axis par2.yaxis.set_tick_params(labelsize=16) plt.legend(fontsize=14, handles=[p3, p5], loc=4, fancybox=True, framealpha=0.5) # end of particle properties sub-plot ----------------------------------- if (caller == 2): # display when in test mode plt.show() return ()
def cons(comp_chem_schem_name, dir_path, self, caller): # inputs: ------------------------------- # comp_chem_schem_name - chemical scheme name of component # dir_path - path to results # self - reference to GUI # caller - flag for calling function # --------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_mw, Nwet, comp_names, y_MV, _, wall_on, space_mode, indx_plot, comp0, _, PsatPa, OC, H2Oi, seedi, _, _, _, tot_in_res, _) = retr_out.retr_out(dir_path) try: # get index of component of interest compi = comp_names.index(comp_chem_schem_name) except: self.l203a.setText( str('Error - could not find component ' + comp_chem_schem_name + ' in the simulated system. Please check whether the name matches that in the chemical scheme.' )) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 return () # return now # get consumption try: indx_int = (np.where(tot_in_res[0, :].astype('int') == compi))[0][0] except: self.l203a.setText( str('Error - could not find a consumption value for component ' + comp_chem_schem_name + '. Please check whether the name matches that in the chemical scheme and that it had gas-phase influx through the model variables file.' )) # set border around message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 return () # return now # indices for supplied time indxt = (timehr >= self.tmin) * (timehr <= self.tmax) # cumulative influxed over all times (ug/m3) tot_in = tot_in_res[1::, indx_int] # influxes over interested time period tot_in = tot_in[indxt] # gas-phase concentration (ppb) over all times yrecn = yrec[:, compi] # gas-phase concentration (ppb) over interested time period (ppb) yrecn = yrecn[indxt] # change in gas-phase concentration (ug/m3) yrecn = (( (yrecn[-1] - yrecn[0]) * Cfac[-1]) / si.N_A) * y_mw[compi] * 1.e12 # total influx over this time (ug/m3) tot_in = tot_in[-2] - tot_in[0] # total consumed (ug/m3) cons = tot_in - yrecn if (caller == 0): # call from the consumption button self.l203a.setText( str('Consumption of ' + comp_chem_schem_name + ': ' + str(cons) + ' ' + u'\u03BC' + 'g/m' + u'\u00B3')) # set border around message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed magenta', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid magenta', 0., 0.) self.bd_pl = 1 return () # return now if (caller == 1): # call from the yield button # concentrations of components in particle phase at end of simulation (# molecules/cm3) SOA = yrec[-2, num_comp:num_comp * (num_sb - wall_on + 1)] # remove seed and water in all size bins SOA[seedi[0]::num_comp] = 0. SOA[H2Oi::num_comp] = 0. # convert from # molecules/cm3 to ug/m3 SOA = ((SOA / si.N_A) * np.tile(y_mw, (num_sb - wall_on))) * 1.e12 # sum for total (ug/m3) SOA = np.sum(SOA) yld = SOA / cons self.l203a.setText( str('Yield of ' + comp_chem_schem_name + ': ' + str(yld))) # set border around message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed magenta', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid magenta', 0., 0.) self.bd_pl = 1 return () # return now return ()
import os import matplotlib.pyplot as plt # ---------------------------------------------------------------------------------------- # get current working directory cwd = os.getcwd() import retr_out try: # if calling from the GMD paper results folder # 60 s intervals # file name Pyfname = str(cwd + '/fig03_data/PyCHAM_time_res/PyCHAM_time_res60s') (num_sb, num_comp, Cfac, y0, Ndry, rbou_rec, xfm, thr0, PyCHAM_names, _, N, _, y_MV, _, wall_on, space_mode) = retr_out.retr_out(Pyfname) # 600 s intervals Pyfname = str(cwd + '/fig03_data/PyCHAM_time_res/PyCHAM_time_res600s') (num_sb, num_comp, Cfac, y1, Ndry, rbou_rec, xfm, thr1, PyCHAM_names, _, N, _, y_MV, _, wall_on, space_mode) = retr_out.retr_out(Pyfname) # 6000 s intervals Pyfname = str(cwd + '/fig03_data/PyCHAM_time_res/PyCHAM_time_res6000s') (num_sb, num_comp, Cfac, y2, Ndry, rbou_rec, xfm, thr2, PyCHAM_names, _, N, _, y_MV, _, wall_on, space_mode) = retr_out.retr_out(Pyfname) except: # if calling from the PyCHAM home folder # 60 s intervals Pyfname = str(
def plotter_wiw(caller, dir_path, self, now): # define function # inputs: ------------------------------- # caller - the module calling (0 for gui) # dir_path - path to results # self - reference to GUI # now - whether to include (0) or exclude water (1) # ----------------------------------------- # ---------------------------------------------------------------------------------------- if (caller == 0): # if calling function is gui plt.ion() # show figure # prepare plot fig, (ax1) = plt.subplots(1, 1, figsize=(10, 7)) fig.subplots_adjust(hspace=0.7) # ---------------------------------------------------------------------------------------- # prepare the volatility basis set interpretation # of particle-phase concentrations # required outputs from full-moving (num_sb, num_comp, Cfac, y, Ndry, rbou_rec, xfm, t_array, rel_SMILES, y_mw, N, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, H2Oi, seedi, _, _, _, _, _) = retr_out.retr_out(dir_path) # number of particle size bins without wall num_asb = (num_sb - wall_on) # convert from list to array y_mw = (np.array((y_mw))).reshape(1, -1) PsatPa = (np.array((PsatPa))).reshape(1, -1) # repeat molecular weights over size bins and times y_mw_rep = np.tile(y_mw, (1, num_asb)) y_mw_rep = np.tile(y_mw_rep, (len(t_array), 1)) # particulate concentrations of individual components (*1.e-12 to convert from g/cc (air) to ug/m3 (air)) # including any water and core pc = (y[:, num_comp:num_comp * (num_asb + 1)] / si.N_A) * y_mw_rep * 1.e12 if (now == 1): # if water to be excluded, zero its contribution pc[:, H2Oi::num_comp] = 0. # total particulate concentrations (ug/m3) tpc = pc.sum(axis=1) # standard temperature for pure component saturation vapour pressures (K) TEMP = 298.15 # convert standard (at 298.15 K) vapour pressures in Pa to # saturation concentrations in ug/m3 # using eq. 1 of O'Meara et al. 2014 Psat_Cst = (1.e6 * y_mw) * (PsatPa / 101325.) / (8.2057e-5 * TEMP) # tile over size bins Psat_Cst = np.tile(Psat_Cst, num_asb) # remove excess dimension Psat_Cst = Psat_Cst.squeeze() # the saturation concentrations to consider (log10(C* (ug/m3))) # note these will be values at the centre of the volatility size bins # setting the final input argument to 1 means decadal bins of vapour pressure sc = np.arange(-2.5, 7.5, 1.) # empty array for normalised mass contributions nmc = np.zeros((len(sc), len(t_array))) for it in range(len(t_array)): # loop through times # loop through saturation concentrations and find normalised mass contributions # to particulate loading for i in range(len(sc)): if (i == 0): indx = Psat_Cst < 10**(sc[i] + 0.5) if (i > 0 and i < len(sc) - 1): indx = (Psat_Cst >= 10**(sc[i - 1] + 0.5)) * (Psat_Cst < 10** (sc[i] + 0.5)) if (i == len(sc) - 1): indx = (Psat_Cst >= 10**(sc[i - 1] + 0.5)) if (tpc[it] > 0.): nmc[i, it] = (pc[it, indx].sum()) / tpc[it] # customised colormap (https://www.rapidtables.com/web/color/RGB_Color.html) colors = [(0.6, 0., 0.7), (0, 0, 1), (0, 1., 1.), (0, 1., 0.), (1., 1., 0.), (1., 0., 0.)] # R -> G -> B n_bin = 100 # discretizes the colormap interpolation into bins cmap_name = 'my_list' # create the colormap cm = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin) # set contour levels levels = (MaxNLocator(nbins=100).tick_values(np.min(nmc), np.max(nmc))) # associate colours and contour levels norm1 = BoundaryNorm(levels, ncolors=cm.N, clip=True) ptindx = (tpc > 0) # indices of times where secondary material present p0 = ax1.pcolormesh(t_array[ptindx], sc, nmc[:, ptindx], cmap=cm, norm=norm1, shading='auto') cax = plt.axes([0.875, 0.40, 0.02, 0.18]) # specify colour bar position cb = plt.colorbar(p0, cax=cax, ticks=[0.00, 0.25, 0.50, 0.75, 1.00], orientation='vertical') cb.ax.tick_params(labelsize=12) cb.set_label('mass fraction', size=12, rotation=270, labelpad=10.) ax1.set_xlabel(r'Time through experiment (hours)', fontsize=14) ax1.set_ylabel( r'$\rm{log_{10}(}$$C*_{\mathrm{298.15 K}}$$\rm{\, (\mu g\, m^{-3}))}$', fontsize=14, labelpad=10.) ax1.yaxis.set_tick_params(labelsize=14, direction='in', which='both') ax1.xaxis.set_tick_params(labelsize=14, direction='in', which='both') # array containing the location of tick labels ytloc = sc ax1.set_yticks(ytloc) # prepare list of strings for the tick labels ytl = [] for i in ytloc: if (i == np.min(ytloc)): # if the minimum include less than sign ytl.append(str('$\less$' + str(i + 0.5))) continue if (i == np.max(ytloc) ): # if the maximum include the greater than or equal to sign ytl.append(str('$\geq$' + str(i + 0.5))) continue ytl.append(str(i + 0.5)) # otherwise just state number ax1.set_yticklabels(ytl) if (caller != 0): plt.show() # show figure return () # end function
def plotter(caller, dir_path, comp_names_to_plot, self): # inputs: ------------------------------------------------------------------ # caller - marker for whether PyCHAM (0 for ug/m3 or 1 for ppb, 3 for # molecules/cm3) or tests (2) are the calling module # dir_path - path to folder containing results files to plot # comp_names_to_plot - chemical scheme names of components to plot # self - reference to GUI # -------------------------------------------------------------------------- # chamber condition --------------------------------------------------------- # retrieve results (num_sb, num_comp, Cfac, yrec, Ndry, rbou_rec, x, timehr, _, y_MW, _, comp_names, y_MV, _, wall_on, space_mode, _, _, _, PsatPa, OC, H2Oi, _, _, _, group_indx, _, _) = retr_out.retr_out(dir_path) y_MW = np.array(y_MW) # convert to numpy array from list Cfac = (np.array(Cfac)).reshape(-1, 1) # convert to numpy array from list # number of actual particle size bins num_asb = (num_sb - wall_on) if (caller == 0 or caller == 1 or caller == 3): plt.ion() # show results to screen and turn on interactive mode # prepare plot fig, (ax0) = plt.subplots(1, 1, figsize=(14, 7)) if (comp_names_to_plot): # if component names specified # gas-phase concentration sub-plot --------------------------------------------- for i in range(len(comp_names_to_plot)): if (comp_names_to_plot[i].strip() == 'H2O'): indx_plot = [H2Oi] indx_plot = np.array((indx_plot)) if (comp_names_to_plot[i].strip() == 'RO2'): indx_plot = (np.array((group_indx['RO2i']))) if (comp_names_to_plot[i].strip() == 'RO'): indx_plot = (np.array((group_indx['ROi']))) if (comp_names_to_plot[i].strip() == 'HOMRO2'): indx_plot = [] cindn = 0 # number of components for cind in comp_names: if 'API_' in cind or 'api_' in cind: if 'RO2' in cind: indx_plot.append(cindn) cindn += 1 indx_plot = np.array(indx_plot) if (comp_names_to_plot[i].strip() != 'H2O' and comp_names_to_plot[i].strip() != 'RO2' and comp_names_to_plot[i].strip() != 'RO' and comp_names_to_plot[i].strip() != 'HOMRO2'): try: # will work if provided components were in simulation chemical scheme # get index of this specified component, removing any white space indx_plot = [ comp_names.index(comp_names_to_plot[i].strip()) ] indx_plot = np.array((indx_plot)) except: self.l203a.setText( str('Component ' + comp_names_to_plot[i] + ' not found in chemical scheme used for this simulation' )) # set border around error message if (self.bd_pl == 1): self.l203a.setStyleSheet(0., '2px dashed red', 0., 0.) self.bd_pl = 2 else: self.l203a.setStyleSheet(0., '2px solid red', 0., 0.) self.bd_pl = 1 plt.ioff() # turn off interactive mode plt.close() # close figure window return () if (caller == 0): # ug/m3 plot # gas-phase concentration (# molecules/cm3) conc = yrec[:, indx_plot].reshape(yrec.shape[0], (indx_plot).shape[0]) * Cfac # gas-phase concentration (ug/m3) conc = ((conc / si.N_A) * y_MW[indx_plot]) * 1.e12 if (caller == 1): # ppb plot # gas-phase concentration (ppb) conc = yrec[:, indx_plot].reshape(yrec.shape[0], (indx_plot).shape[0]) if (caller == 3): # # molecules/cm3 plot # gas-phase concentration (# molecules/cm3) conc = yrec[:, indx_plot].reshape(yrec.shape[0], (indx_plot).shape[0]) * Cfac if (len(indx_plot) > 1): conc = np.sum(conc, axis=1) # sum multiple components # plot this component if (comp_names_to_plot[i].strip() != 'RO2' and comp_names_to_plot[i].strip() != 'RO' and comp_names_to_plot[i].strip() != 'HOMRO2'): # if not the sum of organic peroxy radicals # log10 y axis ax0.semilogy(timehr, conc, '-+', linewidth=4., label=str( str(comp_names[int(indx_plot)] + ' (gas-phase)'))) # linear y axis #ax0.plot(timehr, conc, '-+', linewidth = 4., label = str(str(comp_names[int(indx_plot)]+' (gas-phase)'))) if (comp_names_to_plot[i].strip() == 'RO2' ): # if is the sum of organic peroxy radicals ax0.semilogy(timehr, conc, '-+', linewidth=4., label=str(r'$\Sigma$RO2 (gas-phase)')) if (comp_names_to_plot[i].strip() == 'RO' ): # if is the sum of organic alkoxy radicals ax0.semilogy(timehr, conc, '-+', linewidth=4., label=str(r'$\Sigma$RO (gas-phase)')) if (comp_names_to_plot[i].strip() == 'HOMRO2' ): # if is the sum of HOM organic peroxy radicals ax0.semilogy(timehr, conc, '-+', linewidth=4., label=str(r'$\Sigma$HOMRO2 (gas-phase)')) if (caller == 0): # ug/m3 plot ax0.set_ylabel(r'Concentration ($\rm{\mu}$g$\,$m$\rm{^{-3}}$)', fontsize=14) if (caller == 1): # ppb plot ax0.set_ylabel(r'Mixing ratio (ppb)', fontsize=14) if (caller == 3): # # molecules/cm3 plot gpunit = str('\n(' + u'\u0023' + ' molecules/cm' + u'\u00B3' + ')') ax0.set_ylabel(r'Concentration ' + gpunit, fontsize=14) ax0.set_xlabel(r'Time through simulation (hours)', fontsize=14) ax0.yaxis.set_tick_params(labelsize=14, direction='in') ax0.xaxis.set_tick_params(labelsize=14, direction='in') ax0.legend(fontsize=14) # end of gas-phase concentration sub-plot --------------------------------------- if (caller == 2): # display plt.show() return ()