3 Functions related to responses. 5 from wirecell
import units
16 This version takes gain parameter already scaled such that the 17 gain actually desired is obtained. 19 domain=(0, 10*units.us)
20 if time <= domain[0]
or time >= domain[1]:
26 from math
import sin, cos, exp
27 ret = 4.31054*exp(-2.94809*time/st) \
28 -2.6202*exp(-2.82833*time/st)*cos(1.19361*time/st) \
29 -2.6202*exp(-2.82833*time/st)*cos(1.19361*time/st)*cos(2.38722*time/st) \
30 +0.464924*exp(-2.40318*time/st)*cos(2.5928*time/st) \
31 +0.464924*exp(-2.40318*time/st)*cos(2.5928*time/st)*cos(5.18561*time/st) \
32 +0.762456*exp(-2.82833*time/st)*sin(1.19361*time/st) \
33 -0.762456*exp(-2.82833*time/st)*cos(2.38722*time/st)*sin(1.19361*time/st) \
34 +0.762456*exp(-2.82833*time/st)*cos(1.19361*time/st)*sin(2.38722*time/st) \
35 -2.6202*exp(-2.82833*time/st)*sin(1.19361*time/st)*sin(2.38722*time/st) \
36 -0.327684*exp(-2.40318*time/st)*sin(2.5928*time/st) + \
37 +0.327684*exp(-2.40318*time/st)*cos(5.18561*time/st)*sin(2.5928*time/st) \
38 -0.327684*exp(-2.40318*time/st)*cos(2.5928*time/st)*sin(5.18561*time/st) \
39 +0.464924*exp(-2.40318*time/st)*sin(2.5928*time/st)*sin(5.18561*time/st)
43 def electronics(time, peak_gain=14*units.mV/units.fC, shaping=2.0*units.us):
45 Electronics response function. 47 - gain :: the peak gain value in [voltage]/[charge] 49 - shaping :: the shaping time in Wire Cell system of units 51 - domain :: outside this pair, the response is identically zero 54 if shaping <= 0.5*units.us:
55 gain = peak_gain*10.146826
56 elif shaping <= 1.0*units.us:
57 gain = peak_gain*10.146828
58 elif shaping <= 2.0*units.us:
59 gain = peak_gain*10.122374
61 gain = peak_gain*10.120179
64 electronics = numpy.vectorize(electronics)
68 Return the simple convolution of the two arrays using FFT+mult+invFFT method. 73 s1 = numpy.fft.fft(f1)
74 s2 = numpy.fft.fft(f2)
75 sig = numpy.fft.ifft(s1*s2)
77 return numpy.real(sig)
81 Return the simple convolution of the two arrays using FFT+mult+invFFT method. 83 from scipy.signal
import fftconvolve
84 return fftconvolve(f1, f2,
"same")
90 A response function object holds the response wave function and metadata. 92 Note: time is assumed to be in Wire Cell system of units (ns). This is NOT seconds. 94 def __init__(self, plane, region, pos, domainls, response, impact=None):
110 newls = (self.
times[0], self.
times[-1], nbins)
111 newtimes = numpy.linspace(*newls)
113 return self.
dup(domainls=newls, response=newresps)
117 Return a new ResponseFunction which is a copy of this one and 118 with any values in kwds overriding. 129 Object as a dictionary. 132 domainls=self.
domainls, response=self.response.tolist(),
135 def shaped(self, gain=14*units.mV/units.fC, shaping=2.0*units.us, nbins=None):
137 Convolve electronics shaping/peaking response, returning a new 147 dt = newfr.times[1]-newfr.times[0]
148 newfr.response = [r*dt
for r
in newfr.response]
150 newfr.response =
convolve(elecr, newfr.response)
154 blah =
"<ResponseFunction plane=%s region=%d domainls=%s pos=%s" % \
156 if self.
impact is not None:
157 blah +=
" impact=%f" % self.
impact 164 Return a list of lists grouping by like values of the field. 167 for thing
in sorted(set(getattr(d, field)
for d
in rflist)):
168 bything = [d
for d
in rflist
if getattr(d, field) == thing]
173 ret = [rf
for rf
in rflist
if rf.region == region]
174 ret.sort(key=
lambda x: x.plane)
180 Integrate total charge in a current response. 182 dt = (rf.times[1] - rf.times[0])
184 raise ValueError(
"Corrupt response function for plane %s, region %d" % (rf.plane, rf.region))
185 itot = numpy.sum(rf.response)
188 def normalize(rflist, plane='w', region=0, impact=None):
190 Return new rflist with all responses normalized to be in Ampere 191 and assuming a single electron was drifting. 193 The collection signal on the given plane and region is used to 194 normalize. If impact is an impact distance, or a list of them 195 then the average collection signal is used. If not given, all 196 impacts for the given plane/region are used. 198 toaverage = [rf
for rf
in rflist
if rf.plane == plane
and rf.region == region]
199 if impact
is not None:
200 if not isinstance(impact, collections.Sequence):
202 toaverage = [rf
for rf
in toaverage
if rf.impact
in impact]
206 msg =
"No fields to average out of %d for nomalize(%s, %d, %s)" % (len(rflist), plane, region, impact)
207 raise ValueError(msg)
211 scale = -units.eplus/qavg
215 newrf = rf.dup(response = rf.response*scale)
222 Average fine-grained response functions over multiple impact 223 positions in the same plane and wire region. 225 Return list of new response.ResponseFunction objects ordered by 229 for inplane
in group_by(fine,
'plane'):
230 byregion =
group_by(inplane,
'region')
231 noigeryb = list(byregion)
234 regions = [rflist[0].region
for rflist
in byregion]
235 center =
max(regions)
238 for regp,regm
in zip(byregion[center:], noigeryb[center:]):
239 regp.sort(key=
lambda x: x.impact)
240 regm.sort(key=
lambda x: x.impact)
242 tot = numpy.zeros_like(regp[0].response)
244 for one
in regp + regm:
249 tot -= regp[0].response + regm[0].response
251 tot -= regp[-1].response + regm[-1].response
254 dat = regp[0].dup(response=tot, impact=
None)
261 Average fine-grained response functions over multiple impact 262 positions in the same plane and wire region. It assumes an odd 263 number of regions and a half-populated impact positions per region 264 such that the first impact is exactly on a wire and the impact is 265 exactly on a half-way line between neighboring wires. 267 Return list of new response.ResponseFunction objects ordered by 268 plane, region which cover the same regions. 271 for inplane
in group_by(fine,
'plane'):
272 byregion =
group_by(inplane,
'region')
277 noigeryb = list(byregion)
280 for regp, regm
in zip(byregion, noigeryb):
282 regp.sort(key=
lambda x:
abs(x.impact))
283 regm.sort(key=
lambda x:
abs(x.impact))
285 tot = numpy.zeros_like(regp[0].response)
288 binsize = [1.0]*nimpacts
292 for impact
in range(nimpacts):
295 tot += binsize[impact]*(rp.response + rm.response)
298 tot /= 2*(nimpacts-1)
299 dat = regp[0].dup(response=tot, impact=
None)
309 Return a tuple of response spectra as collection of per-plane 310 matrices in channel periodicity vs frequency. The rflist is both 311 averaged over impacts (if needed) and normalized. 313 impacts = set([rf.impact
for rf
in rflist])
321 for inplane
in byplane:
322 inplane.sort(key=
lambda x: x.region)
323 responses = [rf.response
for rf
in inplane]
324 rows = list(responses)
326 rows += responses[1:]
327 mat = numpy.asarray(rows)
328 spect = numpy.fft.fft2(mat, axes=(0,1))
334 Return a field responses as a number of matrices blocked by impacts. 336 Returns a triple of arrays of shape (Nimpacts, Nregion, Ntbins). 338 If eresp is given, it is convolved with each response function. 351 for inplane
in byplane:
352 byimpact =
group_by(inplane,
'impact')
353 impacts = [d[0].impact
for d
in byimpact]
354 nimpacts = len(impacts)
355 regions = list(set([rf.region
for rf
in inplane]))
357 nregions = len(regions)
358 ntbins = len(inplane[0].response)
359 pib_shape = (nimpacts, nregions, ntbins)
361 pib = numpy.zeros(pib_shape)
362 for inimpact
in byimpact:
363 impact_index = impacts.index(inimpact[0].impact)
364 for inregion
in inimpact:
365 region_index = regions.index(inregion.region)
366 pib[impact_index, region_index] = inregion.response
374 Organize responses into per (plane,impact) and make available as 375 array blocks of shape (Nregion,Ntbins). 377 There are two symmetries for response Resp(w,r,i) on wire w for 378 path near wire region r on impact i. 380 - Due to reciprocity: Resp(w=0,r=N,i) = R(w=N,r=0,i) 382 - Due to geometry: R(w=0,r=N,i=M) = R(w=0,r=-N,i=-M) 384 See the functions `plots.plane_impact_blocks_full()` and 385 `plots.plane_impact_blocks()` for visualizing this data. In 386 particular check for continuous patterns of responses across 387 different impact positions in the first and check that U-plane, 388 high-positive impact position puts most response on wires 0 and 1 389 and high-negative impact positions puts most response on wires 0 397 self.
tbin = onerf.times[1]-onerf.times[0]
406 self.
impact_keys = sorted(set([rf.impact
for rf
in rflist] + [-rf.impact
for rf
in rflist]))
411 for inplane
in byplane:
412 plane_letter = inplane[0].plane
413 tree[plane_letter] = tree_plane = dict()
414 byimpact =
group_by(inplane,
'impact')
415 for inimpact
in byimpact:
420 impact = -1*inimpact[0].impact
423 tree_plane[-impact] = tree_impact_pos = dict()
424 tree_plane[impact] = tree_impact_neg = dict()
426 byregion =
group_by(inimpact,
'region')
427 for inregion
in byregion:
428 assert len(inregion) == 1
431 tree_impact_pos[region] = rf
432 tree_impact_neg[-region] = rf
438 Return an array shaped (Nregions, Ntbins) for the given plane 439 and impact. Row=0 corresponds to the highest region (wire 10). 446 ppi = self.
_tree[plane][impact]
448 mat = numpy.zeros((len(self.
region_keys), len(rfs[0].response)))
450 mat[row] = rf.response
455 return self.
_tree[plane][impact][region].response
466 Return a response block by plane (0,1,2) and by impact 467 (-4,...,0,...,5) and optionally by region (-10,...,0,...,10). 469 If region is None, return corresponding (Nregions,Ntbins) 470 array. If region is given, return (Ntbins) array. The impact 471 (and region) numbers may be negative. 473 pib = self.pibs[plane]
477 return numpy.flipud(pib[-impact])
480 return pib[impact, region]
487 return (impact,region)
492 max_impact = len(self.pibs[0]) - 1
493 return range(-max_impact, max_impact)
497 return range(-nhalf_regions, nhalf_regions+1)
501 return self.pibs[0][0].shape[0]
504 return self.pibs[0][0].shape[1]
510 Return the a response matrix such as passed to `deconvolve()`. 512 Only the frequencies corresponding to a sampling period of `tick` 519 elesp = numpy.fft.fft(elect)
523 Nkeep =
int(round(first.nbins/(2.0*tick/(first.times[1]-first.times[0]))))
526 frm_chopped = [numpy.delete(f, range(Nkeep, Nhave-Nkeep), axis=1)
for f
in frm]
527 ele_chopped = numpy.delete(elesp, range(Nkeep, Nhave-Nkeep))
529 resp = [ele_chopped*f
for f
in frm_chopped]
535 Return a Fourier space filter function: 537 filter = exp(-(freq/sig)^(power)) 539 The filter function is returned as an array `nbins` long covering 540 the low half of the Fourier domain up to the given `nyquist` 543 The `sig` and `nyquist` parameters are in units of the relevant 544 "frequency" for the given Fourier domain. For time-domain Fourier 545 this is likely Hz. For channel-domain this is likely in units of 546 per pitch (unitless). Caller assures this consistency. 548 freqs = numpy.linspace(0, nyquist, nbins)
550 return math.exp(-0.5*((f/sig)**power))
551 filt = numpy.vectorize(filt)
555 def filters(nticks=9600, tick=0.5*units.us, npitches=3000, pitch=1.0):
557 Return (fu,fv,fw,fc) filters. 559 See `filter_expower()` for details. 561 tick_seconds = tick/units.s
562 nyquist_hz = 1.0/(2*tick_seconds)
571 fu =
filter_expower(2*1.43555e+07/200.0, 4.95096e+00, nticks, nyquist_hz)
572 fv =
filter_expower(2*1.47404e+07/200.0, 4.97667e+00, nticks, nyquist_hz)
573 fw =
filter_expower(2*1.45874e+07/200.0, 5.02219e+00, nticks, nyquist_hz)
575 nyquist_pp = 1.0/(2*pitch)
581 fc =
filter_expower((1.4*0.5)/math.sqrt(math.pi), 2.0, npitches, nyquist_pp)
583 return (fu, fv, fw, fc)
588 Return a matrix like Mct which is deconvolved by Rpf and filtered 591 Indices are c=channel, t=time, p=periodicity, f=frequency. 593 Mct is the measured ADC in a plane as a matrix of Nchannel rows 596 Rpf is the wire periodicity and frequency space 2D Fourier 597 transform of the field response * electronics response. It is 598 assumed to contain only the four corners up the time and wire 599 Nyquist frequencies of Mct. 601 Ff is a frequency space filter. 603 Fp is the channel periodicity space filter. 606 nchan,ntick = Mct.shape
607 nperi,nfreq = Rpf.shape
609 Mpf = numpy.fft.fft2(Mct, axes=(0,1))
610 Mpf = numpy.delete(Mpf, range(nperi/2, nchan-nperi/2), axis=0)
611 Mpf = numpy.delete(Mpf, range(nfreq/2, ntick-nfreq/2), axis=1)
613 Spf = Mpf/Rpf * Fp[:nperi].reshape(1,nperi).T
614 Scf = numpy.fft.ifft2(Spf, axes=(1,))
615 Scf = Scf * Ff[:nfreq]
616 Sct = numpy.fft.ifft2(Scf, axes=(0,))
622 Convert response.schema objects to 1D ResponseFunction objects. 624 Fixme: this has not yet been validated. 628 for path
in pr.paths:
629 region =
int(round(path.pitchpos/pr.pitch))
630 pos = (pr.wirepos, pr.pitchpos)
631 nsamples = len(path.current)
632 times = (fr.tstart, nsamples*fr.period, nsamples)
633 impact = pathc.pitchpos - region*pr.pitch
640 def rf1dtoschema(rflist, origin=10*units.cm, speed = 1.114*units.mm/units.us):
642 Convert the list of 1D ResponseFunction objects into 643 response.schema objects. 645 The "1D" refers to the drift paths starting on a line in 2D space. 647 Because it is 1D, all the pitch and wire directions are the same. 651 anti_drift_axis = (1.0, 0.0, 0.0)
653 period = (one.times[1] - one.times[0])
654 tstart = one.times[0]
658 for inplane
in byplane:
659 letter = inplane[0].plane
660 planeid =
"uvw".
index(letter)
661 onetwo = [rf
for rf
in inplane
if rf.impact == 0.0
and (rf.region == 0
or rf.region==1)]
662 pitch =
abs(onetwo[0].pos[0] - onetwo[1].pos[0])
663 location = inplane[0].pos[1]
664 inplane.sort(key=
lambda x: x.region*10000+x.impact)
668 pitchpos = (rf.region*pitch + rf.impact)
681 def write(rflist, outputfile = "wire-cell-garfield-response.json.bz2"):
683 Write a list of response functions to file. 686 text = json.dumps([rf.asdict
for rf
in rflist])
687 if outputfile.endswith(
".json"):
690 if outputfile.endswith(
".json.bz2"):
692 bz2.BZ2File(outputfile,
'w').
write(text)
694 if outputfile.endswith(
".json.gz"):
696 gzip.open(outputfile,
"wb").
write(text)
698 raise ValueError(
"unknown file format: %s" % outputfile)
701 def line(rflist, normalization=13700*units.eplus):
703 Assuming an infinite track of `normalization` ionization electrons 704 per pitch which runs along the starting points of the response 705 function paths, calculate the average response on the central wire 706 of each plane. The returned responses will be normalized such 707 that the collection response integrates to the given normalization 711 impacts = set([rf.impact
for rf
in rflist])
720 for inplane
in byplane:
722 tot = numpy.zeros_like(first.response)
725 dat = first.dup(response=tot, impact=
None, region=
None)
729 if normalization > 0.0:
731 rf.response *= normalization
def line(rflist, normalization=13700 *units.eplus)
def impact_region_numbers_to_indices(self, impact, region)
def region_block(self, plane, impact)
def field_response_spectra(rflist)
def response_spect_nominal(rflist, gain, shaping, tick=0.5 *units.us)
int open(const char *, int)
Opens a file descriptor.
def rf1dtoschema(rflist, origin=10 *units.cm, speed=1.114 *units.mm/units.us)
def __call__(self, plane, impact, region=None)
def resample(self, nbins)
auto enumerate(Iterables &&...iterables)
Range-for loop helper tracking the number of iteration.
def filter_expower(sig, power, nbins, nyquist)
def by_region(rflist, region=0)
def response(self, plane, impact, region)
def shaped(self, gain=14 *units.mV/units.fC, shaping=2.0 *units.us, nbins=None)
def electronics_no_gain_scale(time, gain, shaping=2.0 *units.us)
static int max(int a, int b)
def __init__(self, plane, region, pos, domainls, response, impact=None)
def plane_impact_blocks(rflist, eresp=None)
def group_by(rflist, field)
auto zip(Iterables &&...iterables)
Range-for loop helper iterating across many collections at the same time.
def write(rflist, outputfile="wire-cell-garfield-response.json.bz2")
def __init__(self, rflist, xstart=0.0 *units.cm)
def filters(nticks=9600, tick=0.5 *units.us, npitches=3000, pitch=1.0)
def deconvolve(Mct, Rpf, Ff, Fp)
def normalize(rflist, plane='w', region=0, impact=None)