__init__.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 '''
3 Functions related to responses.
4 '''
5 from wirecell import units
6 
7 from . import schema
8 
9 import math
10 import numpy
11 import collections
12 
13 
14 def electronics_no_gain_scale(time, gain, shaping=2.0*units.us):
15  '''
16  This version takes gain parameter already scaled such that the
17  gain actually desired is obtained.
18  '''
19  domain=(0, 10*units.us)
20  if time <= domain[0] or time >= domain[1]:
21  return 0.0
22 
23  time = time/units.us
24  st = shaping/units.us
25 
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)
40  ret *= gain
41  return ret
42 
43 def electronics(time, peak_gain=14*units.mV/units.fC, shaping=2.0*units.us):
44  '''
45  Electronics response function.
46 
47  - gain :: the peak gain value in [voltage]/[charge]
48 
49  - shaping :: the shaping time in Wire Cell system of units
50 
51  - domain :: outside this pair, the response is identically zero
52  '''
53  # see wirecell.sigproc.plots.electronics() for these magic numbers.
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
60  else:
61  gain = peak_gain*10.120179
62  return electronics_no_gain_scale(time, gain, shaping)
63 
64 electronics = numpy.vectorize(electronics)
65 
66 def convolve(f1, f2):
67  '''
68  Return the simple convolution of the two arrays using FFT+mult+invFFT method.
69  '''
70  # fftconvolve adds an unwanted time shift
71  #from scipy.signal import fftconvolve
72  #return fftconvolve(field, elect, "same")
73  s1 = numpy.fft.fft(f1)
74  s2 = numpy.fft.fft(f2)
75  sig = numpy.fft.ifft(s1*s2)
76 
77  return numpy.real(sig)
78 
79 def _convolve(f1, f2):
80  '''
81  Return the simple convolution of the two arrays using FFT+mult+invFFT method.
82  '''
83  from scipy.signal import fftconvolve
84  return fftconvolve(f1, f2, "same")
85 
86 
87 
88 class ResponseFunction(object):
89  '''
90  A response function object holds the response wave function and metadata.
91 
92  Note: time is assumed to be in Wire Cell system of units (ns). This is NOT seconds.
93  '''
94  def __init__(self, plane, region, pos, domainls, response, impact=None):
95  plane = plane.lower()
96  assert plane in 'uvw'
97  self.plane = plane
98  self.region = region
99  self.pos = tuple(pos)
100  self.domainls = domainls
101  self.response = response
102  self.times = numpy.linspace(*self.domainls)
103  self.impact = impact
104 
105 
106  def __call__(self, time):
107  return numpy.interp(time, self.times, self.response)
108 
109  def resample(self, nbins):
110  newls = (self.times[0], self.times[-1], nbins)
111  newtimes = numpy.linspace(*newls)
112  newresps = numpy.interp(newtimes, self.times, self.response)
113  return self.dup(domainls=newls, response=newresps)
114 
115  def dup(self, **kwds):
116  '''
117  Return a new ResponseFunction which is a copy of this one and
118  with any values in kwds overriding.
119  '''
120  return ResponseFunction(**dict(self.asdict, **kwds))
121 
122  @property
123  def nbins(self):
124  return self.domainls[2]
125 
126  @property
127  def asdict(self):
128  '''
129  Object as a dictionary.
130  '''
131  return dict(plane=self.plane, region=self.region, pos=self.pos,
132  domainls=self.domainls, response=self.response.tolist(),
133  impact=self.impact)
134 
135  def shaped(self, gain=14*units.mV/units.fC, shaping=2.0*units.us, nbins=None):
136  '''
137  Convolve electronics shaping/peaking response, returning a new
138  ResponseFunction.
139  '''
140  # use version defined above
141  # from scipy.signal import fftconvolve
142  if nbins is None:
143  newfr = self.dup()
144  else:
145  newfr = self.resample(nbins)
146  # integrate the current over the sample to get charge
147  dt = newfr.times[1]-newfr.times[0]
148  newfr.response = [r*dt for r in newfr.response]
149  elecr = electronics(newfr.times, gain, shaping)
150  newfr.response = convolve(elecr, newfr.response)
151  return newfr
152 
153  def __str__(self):
154  blah = "<ResponseFunction plane=%s region=%d domainls=%s pos=%s" % \
155  (self.plane, self.region, self.domainls, self.pos)
156  if self.impact is not None:
157  blah += " impact=%f" % self.impact
158  blah += ">"
159  return blah
160 
161 
162 def group_by(rflist, field):
163  '''
164  Return a list of lists grouping by like values of the field.
165  '''
166  ret = list()
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]
169  ret.append(bything)
170  return ret
171 
172 def by_region(rflist, region=0):
173  ret = [rf for rf in rflist if rf.region == region]
174  ret.sort(key=lambda x: x.plane)
175  return ret
176 
177 
178 def total_charge(rf):
179  '''
180  Integrate total charge in a current response.
181  '''
182  dt = (rf.times[1] - rf.times[0])
183  if dt == 0.0:
184  raise ValueError("Corrupt response function for plane %s, region %d" % (rf.plane, rf.region))
185  itot = numpy.sum(rf.response)
186  return dt*itot
187 
188 def normalize(rflist, plane='w', region=0, impact=None):
189  '''
190  Return new rflist with all responses normalized to be in Ampere
191  and assuming a single electron was drifting.
192 
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.
197  '''
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):
201  impact = [impact]
202  toaverage = [rf for rf in toaverage if rf.impact in impact]
203 
204  num = len(toaverage)
205  if 0 == num:
206  msg = "No fields to average out of %d for nomalize(%s, %d, %s)" % (len(rflist), plane, region, impact)
207  raise ValueError(msg)
208 
209  qtot = sum([total_charge(rf) for rf in toaverage])
210  qavg = qtot/num
211  scale = -units.eplus/qavg
212 
213  out = list()
214  for rf in rflist:
215  newrf = rf.dup(response = rf.response*scale)
216  out.append(newrf)
217  return out
218 
219 
220 def _average(fine):
221  '''
222  Average fine-grained response functions over multiple impact
223  positions in the same plane and wire region.
224 
225  Return list of new response.ResponseFunction objects ordered by
226  plane, region.
227  '''
228  ret = list()
229  for inplane in group_by(fine, 'plane'):
230  byregion = group_by(inplane, 'region')
231  noigeryb = list(byregion)
232  noigeryb.reverse()
233 
234  regions = [rflist[0].region for rflist in byregion]
235  center = max(regions)
236 
237  # warning: this makes detailed assumptions about where the impact points are!
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)
241 
242  tot = numpy.zeros_like(regp[0].response)
243  count = 0
244  for one in regp + regm: # sums 2 regions!
245  tot += one.response
246  count += 1
247  tot *= 2.0 # reflect across wire region
248  count *= 2
249  tot -= regp[0].response + regm[0].response # share region boundary path
250  count -= 2
251  tot -= regp[-1].response + regm[-1].response # don't double count impact=0
252  count -= 2
253  tot /= count
254  dat = regp[0].dup(response=tot, impact=None)
255  ret.append(dat)
256  continue
257  return ret
258 
259 def average(fine):
260  '''
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.
266 
267  Return list of new response.ResponseFunction objects ordered by
268  plane, region which cover the same regions.
269  '''
270  coarse = list()
271  for inplane in group_by(fine, 'plane'):
272  byregion = group_by(inplane, 'region')
273 
274  # for each region, we need to take impacts from the region on the other
275  # side of center. So march down the reverse while we march up the
276  # original.
277  noigeryb = list(byregion)
278  noigeryb.reverse()
279 
280  for regp, regm in zip(byregion, noigeryb):
281  # Assure each region is sorted so first is impact=0
282  regp.sort(key=lambda x: abs(x.impact))
283  regm.sort(key=lambda x: abs(x.impact))
284 
285  tot = numpy.zeros_like(regp[0].response)
286 
287  nimpacts = len(regp)
288  binsize = [1.0]*nimpacts # unit impact bin size
289  binsize[0] = 0.5; # each center impact only covers 1/2 impact bin
290  binsize[-1] = 0.5; # same for the edge impacts
291 
292  for impact in range(nimpacts):
293  rp = regp[impact]
294  rm = regm[impact]
295  tot += binsize[impact]*(rp.response + rm.response)
296 
297  # normalize by total number of impact bins
298  tot /= 2*(nimpacts-1)
299  dat = regp[0].dup(response=tot, impact=None)
300  coarse.append(dat)
301  continue
302  return coarse
303 
304 
305 
306 
308  '''
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.
312  '''
313  impacts = set([rf.impact for rf in rflist])
314  if len(impacts) > 1:
315  rflist = average(rflist)
316  rflist = normalize(rflist)
317 
318  ret = list()
319 
320  byplane = group_by(rflist, 'plane')
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)
325  rows.reverse() # mirror
326  rows += responses[1:] # don't double add region==0
327  mat = numpy.asarray(rows)
328  spect = numpy.fft.fft2(mat, axes=(0,1))
329  ret.append(spect)
330  return tuple(ret)
331 
332 def plane_impact_blocks(rflist, eresp = None):
333  '''
334  Return a field responses as a number of matrices blocked by impacts.
335 
336  Returns a triple of arrays of shape (Nimpacts, Nregion, Ntbins).
337 
338  If eresp is given, it is convolved with each response function.
339  '''
340 
341  # symmetry: Response on wire 0 due to path at wire region i,
342  # impact j is same as Response on wire 0 due to path at wire
343  # region -i, impact -j.
344 
345  # symmetry: Response on Wire 0 due to path at wire region i,
346  # impact j is same as response on wire i, due to path at wire
347  # region 0, impact j.
348 
349  ret = list()
350  byplane = group_by(rflist, 'plane')
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]))
356  regions.sort()
357  nregions = len(regions)
358  ntbins = len(inplane[0].response)
359  pib_shape = (nimpacts, nregions, ntbins)
360 
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
367  ret.append(pib)
368  return ret
369 
370 
371 # pibs
372 class PlaneImpactBlocks(object):
373  '''
374  Organize responses into per (plane,impact) and make available as
375  array blocks of shape (Nregion,Ntbins).
376 
377  There are two symmetries for response Resp(w,r,i) on wire w for
378  path near wire region r on impact i.
379 
380  - Due to reciprocity: Resp(w=0,r=N,i) = R(w=N,r=0,i)
381 
382  - Due to geometry: R(w=0,r=N,i=M) = R(w=0,r=-N,i=-M)
383 
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
390  and -1.
391  '''
392  def __init__(self, rflist, xstart = 0.0*units.cm):
393 
394  onerf = rflist[0]
395  self.ntbins = len(onerf.response)
396  self.tmin = onerf.times[0]
397  self.tbin = onerf.times[1]-onerf.times[0]
398  self.trange = self.tbin*self.ntbins
399  self.tmax = self.trange + self.tmin
400 
401  self.xstart = xstart # x position at start of field response drift
402 
403 
404  self.plane_keys = sorted(set([rf.plane for rf in rflist]))
405  self.region_keys = sorted(set([rf.region for rf in rflist]))
406  self.impact_keys = sorted(set([rf.impact for rf in rflist] + [-rf.impact for rf in rflist]))
407 
408  # Organize flat rflist into tree [plane][impact][region]
409  tree = dict()
410  byplane = group_by(rflist, 'plane') # uvw
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:
416  # WARNING: Garfield seems to measure either wire
417  # region number xor impact position in a different
418  # direction than is assumed here. Garfield impact
419  # positions are always positive.
420  impact = -1*inimpact[0].impact
421  assert impact <= 0.0
422 
423  tree_plane[-impact] = tree_impact_pos = dict()
424  tree_plane[impact] = tree_impact_neg = dict()
425 
426  byregion = group_by(inimpact, 'region')
427  for inregion in byregion:
428  assert len(inregion) == 1
429  rf = inregion[0]
430  region = rf.region
431  tree_impact_pos[region] = rf
432  tree_impact_neg[-region] = rf
433  self._tree = tree
434  self._by_plane_impact = dict()
435 
436  def region_block(self, plane, impact):
437  '''
438  Return an array shaped (Nregions, Ntbins) for the given plane
439  and impact. Row=0 corresponds to the highest region (wire 10).
440  '''
441  key = (plane,impact) # cache the built array
442  try:
443  return self._by_plane_impact[key]
444  except KeyError:
445  pass
446  ppi = self._tree[plane][impact]
447  rfs = [ppi[r] for r in self.region_keys]
448  mat = numpy.zeros((len(self.region_keys), len(rfs[0].response)))
449  for row,rf in enumerate(rfs):
450  mat[row] = rf.response
451  self._by_plane_impact[key] = mat
452  return mat
453 
454  def response(self, plane, impact, region):
455  return self._tree[plane][impact][region].response
456 
457 class foo():
458 
459  def __init__(self):
460 
461 
462  self.region_center = self.nregions // 2
463 
464  def __call__(self, plane, impact, region=None):
465  '''
466  Return a response block by plane (0,1,2) and by impact
467  (-4,...,0,...,5) and optionally by region (-10,...,0,...,10).
468 
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.
472  '''
473  pib = self.pibs[plane]
474  if region is None: # 2D
475  if impact >= 0:
476  return pib[impact]
477  return numpy.flipud(pib[-impact])
478  # 1D
479  impact,region = self.impact_region_numbers_to_indices(impact, region)
480  return pib[impact, region]
481 
482  def impact_region_numbers_to_indices(self, impact, region):
483  if impact < 0: # must reflect
484  impact *= -1
485  region *= -1
486  region + self.region_center
487  return (impact,region)
488 
489 
490  @property
491  def impact_range(self):
492  max_impact = len(self.pibs[0]) - 1
493  return range(-max_impact, max_impact) # skip highest as it's shared with lowest of lower region
494  @property
495  def region_range(self):
496  nhalf_regions = self.nregions//2
497  return range(-nhalf_regions, nhalf_regions+1) # inclusive
498 
499  @property
500  def nregions(self):
501  return self.pibs[0][0].shape[0]
502  @property
503  def ntbins(self):
504  return self.pibs[0][0].shape[1]
505 
506 
507 
508 def response_spect_nominal(rflist, gain, shaping, tick=0.5*units.us):
509  '''
510  Return the a response matrix such as passed to `deconvolve()`.
511 
512  Only the frequencies corresponding to a sampling period of `tick`
513  are retained.
514  '''
515  first = rflist[0]
516  frm = field_response_spectra(rflist)
517 
518  elect = electronics(first.times, gain, shaping)
519  elesp = numpy.fft.fft(elect)
520 
521  # number of frequencies to keep to correspond to downsampled tick
522  Nhave = first.nbins
523  Nkeep = int(round(first.nbins/(2.0*tick/(first.times[1]-first.times[0]))))
524 
525  # chop out the "middle" frequencies around the nominal Nyquist freq.
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))
528 
529  resp = [ele_chopped*f for f in frm_chopped]
530  return resp
531 
532 
533 def filter_expower(sig, power, nbins, nyquist):
534  '''
535  Return a Fourier space filter function:
536 
537  filter = exp(-(freq/sig)^(power))
538 
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`
541  "frequency".
542 
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.
547  '''
548  freqs = numpy.linspace(0, nyquist, nbins)
549  def filt(f):
550  return math.exp(-0.5*((f/sig)**power))
551  filt = numpy.vectorize(filt)
552  return filt(freqs)
553 
554 
555 def filters(nticks=9600, tick=0.5*units.us, npitches=3000, pitch=1.0):
556  '''
557  Return (fu,fv,fw,fc) filters.
558 
559  See `filter_expower()` for details.
560  '''
561  tick_seconds = tick/units.s
562  nyquist_hz = 1.0/(2*tick_seconds)
563 
564  # note, these parameters are scaled from what is in the prototype
565  # to be implicitly in Hz instead of MHz.
566  #
567  # These parameters were found by applying a Weiner-inspired filter
568  # to a toy simulation using 2D microboone response and electronics
569  # functions and noise model. They may need to be re-evaluated for
570  # other detectors.
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)
574 
575  nyquist_pp = 1.0/(2*pitch) # pp = per pitch
576 
577  # note, this parameter is scaled to move the somewhat bogus 0.5
578  # (us) number that was used in the prototype out of the "freq" and
579  # into the "sig". In microboone, this filter appears to be only
580  # needed for simulation.
581  fc = filter_expower((1.4*0.5)/math.sqrt(math.pi), 2.0, npitches, nyquist_pp)
582 
583  return (fu, fv, fw, fc)
584 
585 
586 def deconvolve(Mct, Rpf, Ff, Fp):
587  '''
588  Return a matrix like Mct which is deconvolved by Rpf and filtered
589  with the Ff and Fp.
590 
591  Indices are c=channel, t=time, p=periodicity, f=frequency.
592 
593  Mct is the measured ADC in a plane as a matrix of Nchannel rows
594  and Nticks columns.
595 
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.
600 
601  Ff is a frequency space filter.
602 
603  Fp is the channel periodicity space filter.
604  '''
605 
606  nchan,ntick = Mct.shape
607  nperi,nfreq = Rpf.shape
608 
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)
612 
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,))
617  return Sct
618 
619 
620 def schematorf1d(fr):
621  '''
622  Convert response.schema objects to 1D ResponseFunction objects.
623 
624  Fixme: this has not yet been validated.
625  '''
626  ret = list()
627  for pr in fr.planes:
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
634  rf = ResponseFunction(pr.planeid, region, pos, times, path.current, impact)
635  ret.append(rf)
636  return ret
637 
638 
639 
640 def rf1dtoschema(rflist, origin=10*units.cm, speed = 1.114*units.mm/units.us):
641  '''
642  Convert the list of 1D ResponseFunction objects into
643  response.schema objects.
644 
645  The "1D" refers to the drift paths starting on a line in 2D space.
646 
647  Because it is 1D, all the pitch and wire directions are the same.
648  '''
649  #rflist = normalize(rflist)
650 
651  anti_drift_axis = (1.0, 0.0, 0.0)
652  one = rflist[0] # get sample times
653  period = (one.times[1] - one.times[0])
654  tstart = one.times[0]
655 
656  planes = list()
657  byplane = group_by(rflist, 'plane')
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)
665 
666  paths = list()
667  for rf in inplane:
668  pitchpos = (rf.region*pitch + rf.impact)
669  wirepos = 0.0
670  par = schema.PathResponse(rf.response, pitchpos, wirepos)
671  paths.append(par)
672 
673  plr = schema.PlaneResponse(paths, planeid, location, pitch)
674  planes.append(plr)
675  return schema.FieldResponse(planes, anti_drift_axis, origin, tstart, period, speed)
676 
677 
678 
679 
680 
681 def write(rflist, outputfile = "wire-cell-garfield-response.json.bz2"):
682  '''
683  Write a list of response functions to file.
684  '''
685  import json
686  text = json.dumps([rf.asdict for rf in rflist])
687  if outputfile.endswith(".json"):
688  open(outputfile,'w').write(text)
689  return
690  if outputfile.endswith(".json.bz2"):
691  import bz2
692  bz2.BZ2File(outputfile, 'w').write(text)
693  return
694  if outputfile.endswith(".json.gz"):
695  import gzip
696  gzip.open(outputfile, "wb").write(text)
697  return
698  raise ValueError("unknown file format: %s" % outputfile)
699 # fixme: implement read()
700 
701 def line(rflist, normalization=13700*units.eplus):
702  '''
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
708  value if nonzero.
709  '''
710 
711  impacts = set([rf.impact for rf in rflist])
712  if len(impacts) > 1:
713  rflist = average(rflist)
714  byplane = group_by(rflist, 'plane')
715 
716  # sum across all impact positions assuming a single point source is
717  # equivalent to summing across a perpendicular line source for a single,
718  # central wire.
719  ret = list()
720  for inplane in byplane:
721  first = inplane[0]
722  tot = numpy.zeros_like(first.response)
723  for rf in inplane:
724  tot += rf.response
725  dat = first.dup(response=tot, impact=None, region=None)
726  ret.append(dat)
727 
728  # normalize to user's amount of charge
729  if normalization > 0.0:
730  for rf in ret:
731  rf.response *= normalization
732 
733  return ret
734 
def line(rflist, normalization=13700 *units.eplus)
Definition: __init__.py:701
def impact_region_numbers_to_indices(self, impact, region)
Definition: __init__.py:482
def region_block(self, plane, impact)
Definition: __init__.py:436
def field_response_spectra(rflist)
Definition: __init__.py:307
def response_spect_nominal(rflist, gain, shaping, tick=0.5 *units.us)
Definition: __init__.py:508
int open(const char *, int)
Opens a file descriptor.
def rf1dtoschema(rflist, origin=10 *units.cm, speed=1.114 *units.mm/units.us)
Definition: __init__.py:640
def __call__(self, plane, impact, region=None)
Definition: __init__.py:464
auto enumerate(Iterables &&...iterables)
Range-for loop helper tracking the number of iteration.
Definition: enumerate.h:69
def filter_expower(sig, power, nbins, nyquist)
Definition: __init__.py:533
def by_region(rflist, region=0)
Definition: __init__.py:172
T abs(T value)
def response(self, plane, impact, region)
Definition: __init__.py:454
def shaped(self, gain=14 *units.mV/units.fC, shaping=2.0 *units.us, nbins=None)
Definition: __init__.py:135
def convolve(f1, f2)
Definition: __init__.py:66
def electronics_no_gain_scale(time, gain, shaping=2.0 *units.us)
Definition: __init__.py:14
static int max(int a, int b)
def __init__(self, plane, region, pos, domainls, response, impact=None)
Definition: __init__.py:94
def plane_impact_blocks(rflist, eresp=None)
Definition: __init__.py:332
def group_by(rflist, field)
Definition: __init__.py:162
auto zip(Iterables &&...iterables)
Range-for loop helper iterating across many collections at the same time.
Definition: zip.h:295
def write(rflist, outputfile="wire-cell-garfield-response.json.bz2")
Definition: __init__.py:681
def __init__(self, rflist, xstart=0.0 *units.cm)
Definition: __init__.py:392
def filters(nticks=9600, tick=0.5 *units.us, npitches=3000, pitch=1.0)
Definition: __init__.py:555
def deconvolve(Mct, Rpf, Ff, Fp)
Definition: __init__.py:586
def _convolve(f1, f2)
Definition: __init__.py:79
def normalize(rflist, plane='w', region=0, impact=None)
Definition: __init__.py:188