garfield.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 '''
3 Process Garfield field response output files to produce Wire Cell
4 field response input files.
5 
6 Garfield input is provided as a tar file. Internal directory
7 structure does not matter much but the files are assumed to be spelled
8 in the form:
9 
10  <impact>_<plane>.dat
11 
12 where <impact> spells the impact position in mm and plane is from the
13 set {"U","V","Y"}.
14 
15 Each .dat file may hold many records. See parse_text_record() for
16 details of the assumptions made when parsing.
17 
18 These quantities must be given explicitly:
19 
20  - speed :: the nominal drift speed
21 
22 '''
23 from .. import units
24 from . import response
25 
26 import numpy
27 import tarfile
28 
29 import os.path as osp
30 
31 # fixme: move to some util module
32 def fromtarfile(filename):
33  '''
34  Iterate on tarfile, returning (name,text) pair of each file.
35  '''
36  tf = tarfile.open(filename, 'r')
37  for name,member in sorted([(m.name,m) for m in tf.getmembers()]):
38  if member.isdir():
39  continue
40  yield (member.name, tf.extractfile(member).read().decode('utf-8'))
41 # fixme: move to some util module
42 def asgenerator(source):
43  '''
44  If string, assume file, open proper generator, o.w. just return
45  '''
46  if type(source) not in [type(""), type(u"")]:
47  #print ('Source is not a string: %s' % type(source))
48  return source
49  if osp.splitext(source)[1] in [".tar", ".gz", ".tgz"]:
50  return fromtarfile(source)
51  raise ValueError('unknown garfield data source: "%s"' % source)
52 
53 
55  '''
56  Return a generator that splits text by record separators.
57  '''
58  for maybe in text.split("\n% "):
59  if maybe.startswith("Created"):
60  yield maybe
61 
63  '''
64  Iterate on garfield text, returning one record.
65  '''
66  lines = text.split('\n')
67 
68  ret = dict()
69 
70  # Created 31/07/16 At 19.52.20 < none > SIGNAL "Direct signal, group 1 "
71  created = lines[0].split()
72  ret['created'] = '%s %s' %(created[1], created[3])
73  ret['signal'] = None
74  if 'Direct signal' in lines[0]:
75  ret['signal'] = 'direct'
76  if 'Cross-talk' in lines[0]:
77  ret['signal'] = 'x-talk'
78 
79  # Group 1 consists of:
80  ret['group'] = int(lines[2].split()[1])
81 
82  # Wire 243 with label X at (x,y)=(-3,0.6) and at -110 V
83  wire = lines[3].split()
84  ret['wire_region'] = int(wire[1])
85  ret['label'] = wire[4]
86 
87  pos = map(float, wire[6].split('=')[1][1:-1].split(','))
88  ret['wire_region_pos'] = tuple([p*units.cm for p in pos])
89  ret['bias_voltage'] = float(wire[9])
90 
91  # Number of signal records: 1000
92  ret['nbins'] = nbins = int(lines[4].split()[4])
93 
94  # Units used: time in micro second, current in micro Ampere.
95  xunit, yunit = lines[5].split(":")[1].split(",")
96  xunit = [x.strip() for x in xunit.split("in")]
97  yunit = [y.strip() for y in yunit.split("in")]
98 
99  xscale = 1.0 # float(lines[7].split("=")[1]);
100  if "micro second" in xunit[1]:
101  xscale = units.us
102 
103  yscale = 1.0 # float(lines[8].split("=")[1]);
104  if "micro Ampere" in yunit[1]:
105  yscale = units.microampere
106 
107  ret['xlabel'] = xunit[0]
108  ret['ylabel'] = yunit[0]
109 
110  xdata = list()
111  ydata = list()
112  # + ( 0.00000000E+00 0.00000000E+00
113  # + 0.10000000E+00 0.00000000E+00
114  # ...
115  # + 0.99800003E+02 0.00000000E+00
116  # + 0.99900002E+02 0.00000000E+00 )
117  for line in lines[9:9+nbins]:
118  xy = line[4:].split()
119  xdata.append(float(xy[0]))
120  ydata.append(float(xy[1]))
121  if nbins != len(xdata) or nbins != len(ydata):
122  raise ValueError('parse error for "%s"' % wire)
123  ret['x'] = numpy.asarray(xdata)*xscale
124  ret['y'] = numpy.asarray(ydata)*yscale
125  return ret
126 
127 
128 def parse_filename(filename):
129  '''
130  Try to parse whatever data is encoded into the file name.
131  '''
132  fname = osp.split(filename)[-1]
133  dist, plane = osp.splitext(fname)[0].split('_')
134  plane = plane.lower()
135  if plane == 'y':
136  plane = 'w'
137  return dict(impact=float(dist), plane=plane, filename=filename)
138 
139 def load(source, normalization = None, zero_wire_loc = 0.0):
140  '''Load Garfield data source (eg, tarball).
141 
142  Return list of response.ResponseFunction objects.
143 
144  The responses will be normalized according to the value of
145  `normalization`:
146 
147  none or 0: no normalization, take Garfield as absolutely
148  normalized
149 
150  less than 0: normalize such that the average of the integrals
151  along an impact position of the central collection wire is this
152  many electrons.
153 
154  greater than 0: simply multiply the responses by this number.
155 
156  The `zero_wire_loc` is the transverse location of the wire to be
157  considered the central wire.
158 
159  '''
160  source = asgenerator(source)
161 
162  from collections import defaultdict
163  uniq = defaultdict(dict)
164 
165  for filename, text in source:
166 
167  fnamedat = parse_filename(filename)
168 
169  gen = split_text_records(text)
170  for rec in gen:
171  dat = parse_text_record(rec)
172 
173  key = tuple([filename] + [dat[k] for k in ['group', 'wire_region', 'label']])
174 
175  old = uniq.get(key, None)
176  if old: # sum up both signal types
177  ### debugging: central collection wire has both records but one is miniscule
178  #oldtot = sum(old['y'])
179  #if abs(oldtot) > 0.0:
180  # print ('found key already: "%s", sum=%e+%e"' %(key, sum(dat['y']), sum(old['y'])))
181  old['y'] += dat['y']
182  continue
183 
184  dat.pop('signal')
185  dat.update(fnamedat)
186  uniq[key] = dat
187 
188  ret = list()
189  for plane, zwl in zip('uvw', zero_wire_loc):
190  byplane = [one for one in uniq.values() if one['plane'] == plane]
191  zeros = [one for one in byplane if one['wire_region_pos'][0] == zwl and one['impact'] == 0.0]
192  if len(zeros) != 1:
193  for one in sorted(byplane, key=lambda x: (x['wire_region_pos'][0], x['impact'])):
194  print ("region=%s impact=%s" % (one['wire_region_pos'], one['impact']))
195  raise ValueError("%s-plane, failed to find exactly one zero wire (at %f). Found: %s wires" % (plane.upper(), zwl, zeros))
196  zero_wire_region = zeros[0]['wire_region']
197  this_plane = list()
198  for one in byplane:
199  times = one['x']
200  t0 = int(times[0]) # fix round off
201  tf = int(times[-1]) # fix round off
202  ls = (t0, tf, len(times))
203 
204  relative_region = one['wire_region'] - zero_wire_region
205  fix_garfield_impact_sign = -1
206  impact_number = fix_garfield_impact_sign * one['impact']
207  rf = response.ResponseFunction(plane, relative_region,
208  one['wire_region_pos'],
209  ls, numpy.asarray(one['y']),
210  impact_number)
211  this_plane.append(rf)
212  this_plane.sort(key=lambda x: x.region * 10000 + x.impact)
213  ret += this_plane
214 
215  w0 = [r for r in ret if r.region == 0 and r.plane == 'w']
216  dt = (w0[0].times[1]-w0[0].times[0])
217  itot = sum([sum(w.response) for w in w0])/len(w0)
218  qtot = itot*dt
219 
220 
221  if normalization is None or normalization == 0:
222  print ("No normalizing. But, %d paths (Qavg=%f fC = %f electrons)" % \
223  (len(w0), qtot/units.femtocoulomb, -qtot/units.eplus))
224 
225  return ret
226 
227  if normalization < 0:
228  norm = normalization*units.eplus/qtot
229  print ("Normalizing over %d paths is %f (dt=%.2f us, Qavg=%f fC = %f electrons)" % \
230  (len(w0), norm, dt/units.microsecond, qtot/units.femtocoulomb, -qtot/units.eplus))
231  else:
232  norm = normalization
233  print ("Normalization by scaling with: %f" % norm)
234 
235 
236  for r in ret:
237  r.response *= norm
238 
239  return ret
240 
241 
242 def toarrays(rflist):
243  '''
244  Return field response current waveforms as 3 2D arrays.
245 
246  Return as tuple (u,v,w) where each is a 2D array shape:
247  (#regions, #responses).
248  '''
249  ret = list()
250  for byplane in response.group_by(rflist, 'plane'):
251  this_plane = list()
252  byregion = response.group_by(byplane, 'region')
253  if len(byregion) != 1:
254  raise ValueError("unexpected number of regions: %d" % len(byregion))
255  for region in byregion:
256  this_plane.append(region.response)
257  ret.append(numpy.vstack(this_plane))
258  return tuple(ret)
259 
260 
261 
262 def convert(inputfile, outputfile = "wire-cell-garfield-fine-response.json.bz2", average=False, shaped=False):
263  '''
264  Convert an input Garfield file pack into an output wire cell field response file.
265 
266  See also wirecell.sigproc.response.persist
267  '''
268  rflist = load(inputfile)
269  if shaped:
270  rflist = [d.shaped() for d in rflist]
271  if average:
272  rflist = response.average(rflist)
273  response.write(rflist, outputfile)
274 
275 
276 
def asgenerator(source)
Definition: garfield.py:42
def parse_filename(filename)
Definition: garfield.py:128
def convert(inputfile, outputfile="wire-cell-garfield-fine-response.json.bz2", average=False, shaped=False)
Definition: garfield.py:262
def fromtarfile(filename)
Definition: garfield.py:32
def load(source, normalization=None, zero_wire_loc=0.0)
Definition: garfield.py:139
def parse_text_record(text)
Definition: garfield.py:62
int read(int, char *, size_t)
Read bytes from a file descriptor.
auto zip(Iterables &&...iterables)
Range-for loop helper iterating across many collections at the same time.
Definition: zip.h:295
void decode(std::any const &a, Hep2Vector &result)
Definition: CLHEP_ps.h:12
void split(std::string const &s, char c, OutIter dest)
Definition: split.h:35
def split_text_records(text)
Definition: garfield.py:54