def lineout_along_center(filename, ts=None, field="H_density", cellsz=10, cellsx=10, normfactor=1.742e27): if ts is None: # then try to guess ts = filename.replace('.bp', '').split('_')[-1] with bp.File(filename) as fh: data = fh[f"/data/{ts}/fields/{field}"] if data.ndim == 2: nx, ny = data.shape raise NotImplementedError elif data.ndim == 3: nz, ny, nx = data.shape if cellsz > nz or cellsx > nx: raise ValueError( "Requested to cut out a slice bigger than the whole array" ) startx = nx // 2 - cellsx // 2 startz = nz // 2 - cellsz // 2 dataslice = data[startz:startz + cellsz, :, startx:startx + cellsx] # norm to critical density and project on axis lineout = np.mean(dataslice, axis=( 0, 2)) * data.attrs['unitSI'].value / normfactor return lineout
def angular_spectrum_conic_from_particles(filename, ts=None, Emax=None): if ts is None: # then try to guess ts = filename.replace('.bp', '').split('_')[-1] if Emax is None: Emax = 160 # create the arrays for the energy- and angle-coordinates # definitions: bins_E = np.linspace(0, Emax, 51) # Emax in MeV bins_theta = np.linspace(0, 80, 81) # in deg bins_py = [0., 1e9] # one bin - filter out backward # unit conversions: m_p = 1.6726219e-27 # in SI c = 3e8 # in SI J2MeV = 6.242e+12 # new bins E_0 = m_p * c**2 # in SI bins_E_SI = bins_E / J2MeV # in SI bins_p2_SI = ((bins_E_SI + E_0)**2 - E_0**2) / c**2 # in SI bins_tan2 = np.tan(bins_theta / 180 * np.pi)**2 diffs_E = np.diff(bins_E) values_E = (bins_E[1:] + bins_E[:-1]) / 2 diffs_theta = np.diff(bins_theta) / 180 * np.pi values_theta = (bins_theta[:-1] + bins_theta[1:]) / 360 * np.pi diffs_sterad = 2 * np.pi * np.sin(values_theta) * diffs_theta with bp.File(filename) as fh: px = fh[f'/data/{ts}/particles/H_all/momentum/x'][:] py = fh[f'/data/{ts}/particles/H_all/momentum/y'][:] pz = fh[f'/data/{ts}/particles/H_all/momentum/z'][:] un = fh[f'/data/{ts}/particles/H_all/momentum/y'].unitSI.value we = fh[f'/data/{ts}/particles/H_all/weighting'][:] py1 = py / we py2 = py**2 / we**2 px2 = px**2 / we**2 pz2 = pz**2 / we**2 pt2 = px2 + pz2 bins_p2 = bins_p2_SI / un**2 hist, bin_edges = np.histogramdd((pt2 + py2, pt2 / py2, py1), bins=(bins_p2, bins_tan2, bins_py), weights=we) # norm and remove the last dimension of length 1 hist = hist[:, :, 0] / ( # norm to particles per MeV and sterad diffs_E.reshape((-1, 1)) # per MeV * diffs_sterad.reshape((1, -1)) # per sterad in cyllinder symmetry ) bin_edges = bin_edges[:-1] return hist, bins_E, bins_theta
def slice_from_particles(filename, ts=None, particle="H_all", axis=2, thickness=0.8): """ takes particle output to compute density slice along center """ with bp.File(filename) as fh: x = fh[f'/data/{ts}/particles/{particle}/positionOffset/x'][:] y = fh[f'/data/{ts}/particles/{particle}/positionOffset/y'][:] z = fh[f'/data/{ts}/particles/{particle}/positionOffset/z'][:] hist_raw, bin_edges = np.histogramdd( (p2x + p2y + p2z, tan2t, tan2p, p_x, p_z, p_y), bins=(bins_p2, bins_tan2t, bins_tan2p, bins_sign, bins_sign, bins_sign), weights=we)
def slice_z(filename, ts=None, slicewidth=4): """ returns 2d-array of the central slice """ filename = f'{params.folder.folder}{params.outputs.fields.filename.format(timestep=timestep)}' self.log(f"reading {filename}") with bp.File(filename) as fh: field = fh[f'/data/{timestep}/fields/{fieldname}'] zstart, zend = params.pos_to_cell( -slicewidth / 2, axis=0), params.pos_to_cell(slicewidth / 2, axis=0) self.log( f"doing average in z-direction over {zend-zstart} cells, {zstart}-{zend}" ) rawdata = np.mean(field[zstart:zend, :, :], axis=0) # slice in z unit_factor = field.attrs['unitSI'].value / normfactor extent = [ params.pos_from_cell(*args) for args in [(0, ), (rawdata.shape[0] - 1, ), (0, 2), (rawdata.shape[1] - 1, 2)] ] data = unit_factor * rawdata.transpose() image = axes.imshow( data, origin='upper', aspect=1., extent=extent, norm=plotnorm, ) self.log(f"drawn axes with {whichfield}") if cbar_ax: cbar_ax.figure.colorbar(image, cax=cbar_ax) if title: axes.set_title(title.format(time=time)) return data
def polar_from_particles(filename, histargs={}, ts=None): """ Analyzes the particles-output and returns a histogram with the bins (E, theta, phi), i.e. the usual sperical angles with respect to the laser propagation direction. returns normalized (particles per MeV and sterad) data. """ if ts is None: # then try to guess ts = filename.replace('.bp', '').split('_')[-1] # add default parameters if not in histargs and copy to better readable object if histargs is None: # None should be valid default, too histargs = {} p = SimpleNamespace(**dict( thetamin=0.8, thetamax=20, Emax=240, Nt=24, Np=120, NE=60, )) for k, v in histargs.items(): setattr(p, k, v) Np4 = p.Np // 4 # adjust/correct the number of phi-bins p.Np = Np4 * 4 # it's required to be a multiple of 4 # construct the arrays for binning bins_E = np.linspace(0, p.Emax, p.NE + 1) # Emax in MeV bins_theta = np.linspace(p.thetamin, p.thetamax, p.Nt + 1) # in deg bins_phi = np.linspace(0, 2 * np.pi, p.Np + 1) # in rad bins_sign = [-1e99, 0, 1e99] # find sign # unit conversions: m_p = 1.6726219e-27 # in SI c = 3e8 # in SI J2MeV = 6.242e+12 E_0 = m_p * c**2 # in SI # new bin borders that make it computationally cheaper bins_E_SI = bins_E / J2MeV # in SI bins_p2_SI = ((bins_E_SI + E_0)**2 - E_0**2) / c**2 # in SI bins_tan2t = np.tan(bins_theta / 180 * np.pi)**2 bins_phi_90 = np.linspace(0, 90, Np4, endpoint=False) # quarter of the circle bins_tan2p = 1e99 * np.ones(Np4 + 1) bins_tan2p[:Np4] = np.tan(bins_phi_90 / 180 * np.pi)**2 diffs_E = np.diff(bins_E) # for normalize per MeV # get the data of the particles with bp.File(filename) as fh: px = fh[f'/data/{ts}/particles/H_all/momentum/x'][:] py = fh[f'/data/{ts}/particles/H_all/momentum/y'][:] pz = fh[f'/data/{ts}/particles/H_all/momentum/z'][:] un = fh[f'/data/{ts}/particles/H_all/momentum/y'].unitSI.value we = fh[f'/data/{ts}/particles/H_all/weighting'][:] # compute derived quantities of the particles p_x = px / we p_y = py / we p_z = pz / we p2y = p_y**2 p2x = p_x**2 p2z = p_z**2 tan2t = (p2x + p2z) / p2y tan2p = p2z / p2x bins_p2 = bins_p2_SI / un**2 # do the multidimensional binning hist_raw, bin_edges = np.histogramdd( (p2x + p2y + p2z, tan2t, tan2p, p_x, p_z, p_y), bins=(bins_p2, bins_tan2t, bins_tan2p, bins_sign, bins_sign, bins_sign), weights=we) # project the four quarters back on the whole circle # and remove last dimension of length 1 # and normalize to sterad and MeV costheta = np.cos(bins_theta / 180 * np.pi) diffsterad = (-np.diff(costheta) * 2 * np.pi / p.Np).reshape( (1, p.Nt, 1)) normfactor = diffs_E.reshape((p.NE, 1, 1)) * diffsterad hist = np.empty((p.NE, p.Nt, p.Np)) hist[:, :, 0 * Np4:1 * Np4] = hist_raw[:, :, :, 1, 1, 1] / normfactor hist[:, :, 1 * Np4:2 * Np4] = hist_raw[:, :, ::-1, 0, 1, 1] / normfactor hist[:, :, 2 * Np4:3 * Np4] = hist_raw[:, :, :, 0, 0, 1] / normfactor hist[:, :, 3 * Np4:4 * Np4] = hist_raw[:, :, ::-1, 1, 0, 1] / normfactor return hist, bins_E, bins_theta, bins_phi
def posmom_from_particles( filename, ts=None, vec_ps=[0, 1, 0], center_point=[432, 450, 216], filter_point=None, filter_thickness=24, bins_pos_mu=None, species="H_all", verbose=True, mom_or_energy='mom', ): """ returns phase space position-momentum along arbitrary axis position is projected on vec_ps with respect to center_point momentum is projected on vec_ps the momentum bins are evenly spaced w.r.t. momentum or the corresponding energy returned is the histogram and the arrays of position-bins, momentum-bins and energy-bins (the latter two are synonymic) """ if ts is None: # then try to guess ts = filename.replace('.bp', '').split('_')[-1] # set the directions for the ps and filtering vec_filter1 = np.array( [1, 0, 0]) # those two vectors form the boundary of the lineout vec_filter2 = np.array([0, 0, 1]) # it is perpendicular to both if filter_point is None: filter_point = center_point # and goes through that point # some unit conversions: m_p = 1.6726219e-27 # in SI c = 3e8 # in SI J2MeV = 6.242e+12 E_0 = m_p * c**2 # in SI ### ### compute all bins - we need bins in pic-units for effective binning, and bins for plotting ### # we can define bins w.r.t. momentum or the energy corresponding to the momentum: if mom_or_energy == 'energy': E_max_neg_MeV = 80 E_max_pos_MeV = 150 bins_Epos = np.linspace(0, E_max_pos_MeV, 401) # in MeV bins_Eneg = np.linspace(E_max_neg_MeV, 0, 101) # the positive/abs values bins_Epos_SI = bins_Epos / J2MeV bins_Eneg_SI = bins_Eneg / J2MeV bins_ppos_SI = (( (bins_Epos_SI + E_0)**2 - E_0**2))**0.5 / c # in SI bins_pneg_SI = -(( (bins_Eneg_SI + E_0)**2 - E_0**2))**0.5 / c # in SI bins_mom_SI = np.array( list(bins_pneg_SI)[:-1] + list(bins_ppos_SI) ) # for binning, nearly, need to divide by unit, we get it only later from dataset bins_E = list(-bins_Eneg)[:-1] + list( bins_Epos) # can be used for plotting bins_betagamma = bins_mom_SI / m_p / c # alternatively those for plotting elif mom_or_energy == 'mom': bins_betagamma_neg = np.linspace(-0.4, 0., 201) bins_betagamma_pos = np.linspace(0., 0.8, 401) bins_pneg_SI = bins_betagamma_neg * m_p * c bins_ppos_SI = bins_betagamma_pos * m_p * c bins_mom_SI = np.array( list(bins_pneg_SI)[:-1] + list(bins_ppos_SI) ) # for binning, nearly, need to divide by unit, we get it only later from dataset bins_Epos_SI = (bins_ppos_SI**2 * c**2 + E_0**2)**0.5 - E_0 bins_Eneg_SI = -((bins_pneg_SI**2 * c**2 + E_0**2)**0.5 - E_0) bins_Epos = bins_Epos_SI * J2MeV bins_Eneg = bins_Eneg_SI * J2MeV bins_E = list(bins_Eneg)[:-1] + list( bins_Epos) # can be used for plotting bins_betagamma = list(bins_betagamma_neg)[:-1] + list( bins_betagamma_pos) # for plotting if bins_pos_mu is None: bins_pos_mu = np.linspace(-15, 20, 201) # in mu, for plotting bins_pos_cells = bins_pos_mu * 30 # for pos-binning, in cells bins_filter = [-1e9, -filter_thickness, filter_thickness, 1e9] # for filter, in cells with bp.File(filename) as fh: ds = fh[f"/data/{ts}/particles/{species}/"] Np = ds["weighting"].shape[0] unitmom = ds["momentum/y"].unitSI.value bins_mom_pic = bins_mom_SI / unitmom # divide in chunks of less than 1e8 particles: nrchunks = (Np + 1) // 100000000 + 1 if verbose: print( f"will read the {Np} macroparticles in {nrchunks} chunks") limits = list( np.linspace(0, Np, nrchunks, endpoint=False, dtype=int))[1:] indices = [ slice(i, j, None) for (i, j) in zip([None] + limits, limits + [None]) ] hists = [] for index in indices: # read the data weights = ds["weighting"][index] N = len(weights) pos = np.empty( (N, 3)) # to store the data for taking scalar product later mom = np.empty( (N, 3)) # to store the data for taking scalar product later pos[:, 0] = ds["positionOffset/x"][index] pos[:, 1] = ds["positionOffset/y"][index] pos[:, 2] = ds["positionOffset/z"][index] mom[:, 0] = ds["momentum/x"][index] / weights mom[:, 1] = ds["momentum/y"][index] / weights mom[:, 2] = ds["momentum/z"][index] / weights # compute quantities ps_pos = np.dot(pos - center_point, vec_ps) ps_mom = np.dot(mom, vec_ps) scalar1 = np.dot(pos - filter_point, vec_filter1) scalar2 = np.dot(pos - filter_point, vec_filter2) # do binning hist_raw, bin_edges = np.histogramdd( (ps_pos, ps_mom, scalar1, scalar2), bins=(bins_pos_cells, bins_mom_pic, bins_filter, bins_filter), weights=weights) hists.append(hist_raw[:, :, 1, 1]) if verbose: nrall = np.round(np.sum(hist_raw), 1) nrfilt = np.round(np.sum(hist_raw[:, :, 1, 1]), ) nrchunk = len(hists) print( f"In {nrchunk}. chunk: {nrfilt} particles, i.e. {np.round(nrfilt/nrall*100, 1)}% of the chunk, are inside the filter region" ) hist = sum(hists) return hist, bins_pos_mu, bins_betagamma, bins_E
def lineout_n_over_gamma( filename, ts=None, vec1=[0, 0, 1], vec2=[1, 0, 0], center_point=[432, 450, 216], filter_thickness=15, poslim=[-15, 21], mu_in_cells=30, species='e_all', verbose=True, dividebygamma=True, ): """ filter out a square given by vec1-vec2, return a function/histogram of the density from position along axis vec1 x vec2 """ if ts is None: # then try to guess ts = filename.replace('.bp', '').split('_')[-1] vec3 = np.cross(vec1, vec2) pos1, pos2 = poslim bins_vec3_mu = np.linspace(pos1, pos2, round((pos2 - pos1) * mu_in_cells) + 1) # in mu, for plotting bins_vec3_cells = np.round( bins_vec3_mu * mu_in_cells) # round to avoid strange bin overlaps bins_filter = np.array( [-1e9, -filter_thickness, filter_thickness, 1e9]) slice_filterdim = 1 # hist will have 3 entries in last dimension, pick the middle one with this filter_nrcells = 2 * filter_thickness m_e = 9.1094e-31 # in SI c = 3e8 # in SI with bp.File(filename) as fh: ds = fh[f"/data/{ts}/particles/{species}/"] Np = ds["weighting"].shape[0] unitmom = ds["momentum/y"].unitSI.value m2c2 = (m_e * c / unitmom)**2 # in PIC units # divide in chunks of less than 1e8 particles: nrchunks = (Np + 1) // 50000000 + 1 if verbose: print( f"will read the {Np} macroparticles in {nrchunks} chunks") limits = list( np.linspace(0, Np, nrchunks, endpoint=False, dtype=int))[1:] indices = [ slice(i, j, None) for (i, j) in zip([None] + limits, limits + [None]) ] hists = 0 for i, index in enumerate(indices): # read the data weights = ds["weighting"][index] N = len(weights) pos = np.empty( (N, 3)) # to store the data for taking scalar product later pos[:, 0] = ds["positionOffset/x"][index] pos[:, 1] = ds["positionOffset/y"][index] pos[:, 2] = ds["positionOffset/z"][index] p2 = (ds["momentum/x"][index]**2 + ds["momentum/y"][index]**2 + ds["momentum/z"][index]**2) / weights**2 gamma = 1 if not dividebygamma else (1 + p2 / m2c2)**0.5 # do binning hist_raw, bin_edges = np.histogramdd( (np.dot(pos - center_point, vec3), np.dot(pos - center_point, vec1), np.dot(pos - center_point, vec2)), bins=(bins_vec3_cells, bins_filter, bins_filter), weights=weights / gamma) hists += hist_raw[:, slice_filterdim, slice_filterdim] if verbose: nrall = np.round(np.sum(hist_raw), 1) nrfilt = np.round( np.sum(hist_raw[:, slice_filterdim, slice_filterdim]), ) print( f"In {i+1}. chunk: {nrfilt} particles, i.e. {np.round(nrfilt/nrall*100, 1)}% of the chunk, are inside the filter region" ) print(hists.shape) lastdims = (1, ) if slice_filterdim == 1 else (1, 1) hist = ( hists / np.diff(bins_vec3_mu) / (filter_nrcells / mu_in_cells)**2 * 1e18 # from mu to m - particles per m^3 / 1.742e27 # in n_c ) return hist, bins_vec3_mu
def arbitrary_slice_from_particles( filename, ts=None, vec1=[1, 0, 0], vec2=[0, 1, 0], center_point=[432, 450, 216], poslims=[[-12, 12], [-15, 21]], mu_in_cells=30, filter_point=None, filter_thickness=45, bins_E=np.linspace(0, 240, 241), species='H_all', verbose=True, ): """ returns a vec1-vec2-position-histogram, with energy as additional axis if filter_thickness is a number, filter +-filter_thickness around filter_point in the direction of vec1 x vec2 if it's an iterable, take it as the bins. The histogram has dimension one more in that case """ if ts is None: # then try to guess ts = filename.replace('.bp', '').split('_')[-1] if filter_point is None: filter_point = center_point vec3 = np.cross(vec1, vec2) # some unit conversions: m_p = 1.6726219e-27 # in SI c = 3e8 # in SI J2MeV = 6.242e+12 E_0 = m_p * c**2 # in SI ### ### compute all bins - we need bins in pic-units for effective binning, and bins for plotting ### bins_E_SI = bins_E / J2MeV bins_p2_SI = ((bins_E_SI + E_0)**2 - E_0**2) / c**2 # in SI (pos1_1, pos1_2), (pos2_1, pos2_2) = poslims bins_vec1_mu = np.linspace(pos1_1, pos1_2, round((pos1_2 - pos1_1) * mu_in_cells) + 1) # in mu, for plotting bins_vec2_mu = np.linspace(pos2_1, pos2_2, round((pos2_2 - pos2_1) * mu_in_cells) + 1) # in mu, for plotting bins_vec1_cells = np.round(bins_vec1_mu * mu_in_cells) # for pos-binning, in cells bins_vec2_cells = np.round( bins_vec2_mu * mu_in_cells) # round to avoid strange bin overlaps try: bins_filter = [-np.inf] + list(filter_thickness) + [ np.inf ] # for filter, in cells slice_filterdim = slice( 1, -1, None) # to pick the filtered hist from its last dimension filter_nrcells = np.diff(bins_filter[slice_filterdim]).reshape( (1, 1, 1, -1)) except TypeError: # if it's not iterable bins_filter = [-1e9, -filter_thickness, filter_thickness, 1e9] # for filter, in cells slice_filterdim = 1 # hist will have 3 entries in last dimension, pick the middle one with this filter_nrcells = 2 * filter_thickness with bp.File(filename) as fh: ds = fh[f"/data/{ts}/particles/{species}/"] Np = ds["weighting"].shape[0] unitmom = ds["momentum/y"].unitSI.value bins_p2_pic = bins_p2_SI / unitmom**2 # divide in chunks of less than 1e8 particles: nrchunks = (Np + 1) // 50000000 + 1 if verbose: print( f"will read the {Np} macroparticles in {nrchunks} chunks") limits = list( np.linspace(0, Np, nrchunks, endpoint=False, dtype=int))[1:] indices = [ slice(i, j, None) for (i, j) in zip([None] + limits, limits + [None]) ] hists = 0 for i, index in enumerate(indices): # read the data weights = ds["weighting"][index] N = len(weights) pos = np.empty( (N, 3)) # to store the data for taking scalar product later pos[:, 0] = ds["positionOffset/x"][index] pos[:, 1] = ds["positionOffset/y"][index] pos[:, 2] = ds["positionOffset/z"][index] p2 = (ds["momentum/x"][index]**2 + ds["momentum/y"][index]**2 + ds["momentum/z"][index]**2) / weights**2 # do binning hist_raw, bin_edges = np.histogramdd( (np.dot(pos - center_point, vec1), np.dot(pos - center_point, vec2), p2, np.dot(pos - center_point, vec3)), bins=(bins_vec1_cells, bins_vec2_cells, bins_p2_pic, bins_filter), weights=weights) hists += hist_raw[:, :, :, slice_filterdim] if verbose: nrall = np.round(np.sum(hist_raw), 1) nrfilt = np.round( np.sum(hist_raw[:, :, :, slice_filterdim]), ) print( f"In {i+1}. chunk: {nrfilt} particles, i.e. {np.round(nrfilt/nrall*100, 1)}% of the chunk, are inside the filter region" ) lastdims = (1, ) if slice_filterdim == 1 else (1, 1) hist = ( hists / np.diff(bins_vec1_mu).reshape( (-1, 1, *lastdims)) / np.diff(bins_vec2_mu).reshape( (1, -1, *lastdims)) / (filter_nrcells / mu_in_cells) * 1e18 # from mu to m - particles per m^3 / 1.742e27 # in n_c ) return hist, bins_vec1_mu, bins_vec2_mu, bins_E
def rectangle_from_particles(filename, ts=None): """ Analyzes the particles-output and returns a histogram with the bins (E, thetax, thetaz) Thetax and thetaz are not angles in any usual sperical angle sense, but comes from the p_x,z / p_y ratio, so it would equal theta if pz or px were zero. """ if ts is None: # then try to guess ts = filename.replace('.bp', '').split('_')[-1] # create the arrays for the energy- and angle-coordinates # definitions: Emax = 240 txmax, tzmax = 20, 20 Nx, Nz, NE = 24, 24, 50 bins_E = np.linspace(0, Emax, NE + 1) # Emax in MeV bins_thetax = np.linspace(-txmax, txmax, Nx + 1) # in deg bins_thetaz = np.linspace(-tzmax, tzmax, Nz + 1) # in deg bins_py = [0., 1e9] # one bin - filter out backward # unit conversions: m_p = 1.6726219e-27 # in SI c = 3e8 # in SI J2MeV = 6.242e+12 # new bins E_0 = m_p * c**2 # in SI bins_E_SI = bins_E / J2MeV # in SI bins_p2_SI = ((bins_E_SI + E_0)**2 - E_0**2) / c**2 # in SI bins_tanx = np.tan(bins_thetax / 180 * np.pi) # the tan-values bins_tanz = np.tan(bins_thetaz / 180 * np.pi) # the tan-values diffs_E = np.diff(bins_E) values_E = (bins_E[1:] + bins_E[:-1]) / 2 diffs_thetax = np.diff(bins_thetax) / 180 * np.pi diffs_thetaz = np.diff(bins_thetaz) / 180 * np.pi values_thetax = (bins_thetax[:-1] + bins_thetax[1:]) / 360 * np.pi values_thetaz = (bins_thetaz[:-1] + bins_thetaz[1:]) / 360 * np.pi # !!!!!!!!!! Achtung, das ist eine Näherung die nur gilt wenn thetax oder thetaz klein ist: diffs_thetas = diffs_thetax.reshape((Nx, 1)) * diffs_thetaz.reshape( (1, Nz)) with bp.File(filename) as fh: px = fh[f'/data/{ts}/particles/H_all/momentum/x'][:] py = fh[f'/data/{ts}/particles/H_all/momentum/y'][:] pz = fh[f'/data/{ts}/particles/H_all/momentum/z'][:] un = fh[f'/data/{ts}/particles/H_all/momentum/y'].unitSI.value we = fh[f'/data/{ts}/particles/H_all/weighting'][:] p_y = py / we p_x = px / we p_z = pz / we tx = px / py tz = pz / py bins_p2 = bins_p2_SI / un**2 hist, bin_edges = np.histogramdd( (p_x**2 + p_y**2 + p_z**2, tx, tz, p_y), bins=(bins_p2, bins_tanx, bins_tanz, bins_py), weights=we) # norm and remove the last dimension of length 1 hist = hist[:, :, :, 0] / ( # norm to particles per MeV and sterad diffs_E.reshape((NE, 1, 1)) # per MeV * diffs_thetas.reshape((-1, Nx, Nz)) # per sterad ) bin_edges = bin_edges[:-1] return hist, bins_E, bins_thetax, bins_thetaz