apa.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 '''
3 This module provides connectivity information about DUNE APAs
4 (protoDUNE or nominal DUNE FD design, and not 35t prototype)
5 
6 See the Plex class for a convenient one-stop user shop.
7 
8 '''
9 from wirecell import units
10 import wirecell.util.wires.generator as generator
11 
12 from collections import namedtuple, Counter, defaultdict
13 import numpy
14 import networkx
15 
16 # This matrix connects the:
17 #
18 # (chip row, channel column)
19 #
20 # in an FEMBwith the conductor expressed as a pair of values:
21 #
22 # (layer letter, attachement number)
23 #
24 # The layer is expressed as a letter in "uvw", the attachment number
25 # is a one-based count of the logical spot where the conductor
26 # attaches to the top of the APA looking at the face holding the
27 # conductor and ordered left to right.
28 #
29 chip_channel_layer_spot_matrix = numpy.array([
30  [('u', 19), ('u', 17), ('u', 15), ('u', 13), ('u', 11), ('v', 19),
31  ('v', 17), ('v', 15), ('v', 13), ('v', 11), ('w', 23), ('w', 21),
32  ('w', 19), ('w', 17), ('w', 15), ('w', 13)],
33  [('u', 9), ('u', 7), ('u', 5), ('u', 3), ('u', 1), ('v', 9),
34  ('v', 7), ('v', 5), ('v', 3), ('v', 1), ('w', 11), ('w', 9),
35  ('w', 7), ('w', 5), ('w', 3), ('w', 1)],
36  [('w', 14), ('w', 16), ('w', 18), ('w', 20), ('w', 22), ('w', 24),
37  ('v', 12), ('v', 14), ('v', 16), ('v', 18), ('v', 20), ('u', 12),
38  ('u', 14), ('u', 16), ('u', 18), ('u', 20)],
39  [('w', 2), ('w', 4), ('w', 6), ('w', 8), ('w', 10), ('w', 12),
40  ('v', 2), ('v', 4), ('v', 6), ('v', 8), ('v', 10), ('u', 2),
41  ('u', 4), ('u', 6), ('u', 8), ('u', 10)],
42  [('u', 29), ('u', 27), ('u', 25), ('u', 23), ('u', 21), ('v', 29),
43  ('v', 27), ('v', 25), ('v', 23), ('v', 21), ('w', 35), ('w', 33),
44  ('w', 31), ('w', 29), ('w', 27), ('w', 25)],
45  [('u', 39), ('u', 37), ('u', 35), ('u', 33), ('u', 31), ('v', 39),
46  ('v', 37), ('v', 35), ('v', 33), ('v', 31), ('w', 47), ('w', 45),
47  ('w', 43), ('w', 41), ('w', 39), ('w', 37)],
48  [('w', 26), ('w', 28), ('w', 30), ('w', 32), ('w', 34), ('w', 36),
49  ('v', 22), ('v', 24), ('v', 26), ('v', 28), ('v', 30), ('u', 22),
50  ('u', 24), ('u', 26), ('u', 28), ('u', 30)],
51  [('w', 38), ('w', 40), ('w', 42), ('w', 44), ('w', 46), ('w', 48),
52  ('v', 32), ('v', 34), ('v', 36), ('v', 38), ('v', 40), ('u', 32),
53  ('u', 34), ('u', 36), ('u', 38), ('u', 40)]], dtype=object)
54 
55 # split off each half of the tuple.
56 chip_channel_spot = chip_channel_layer_spot_matrix[:,:,1].astype(numpy.int32)
57 chip_channel_layer = numpy.asarray(["uvw".index(i) for i in chip_channel_layer_spot_matrix[:,:,0].flat]).reshape(chip_channel_layer_spot_matrix.shape[:2]).astype(numpy.int32)
58 
59 
60 def flatten_cclsm(mat = chip_channel_layer_spot_matrix):
61  '''
62  Flatten an ASIC channel X number matrix to a dictionary keyed by
63  (plane letter, local wire attachment number (1-48 or 1-40). Value
64  is a tuple (ichip, ich) with ichip:{1-8} and ich:{1-16}
65  '''
66  ret = dict()
67  for ichip, row in enumerate(mat):
68  for ich, cell in enumerate(row):
69  cell = tuple(cell)
70  ret[cell] = (ichip+1, ich+1)
71  return ret
72 
73 
74 FaceParams = namedtuple("FaceParams", ["nlayers", "nboards"]);
75 BoardParams = namedtuple("BoardParams", ["nchips", "nchanperchip"])
76 DaqParams = namedtuple("DaqParams", ["nwibs", "nconnperwib"])
77 GeomParams = namedtuple("GeomParams", ["width", "height", "pitch","angle","offset","planex"])
78 Params = namedtuple("ApaParams", ["nfaces", "anode_loc", "crate_addr",
79  "face", "board", "daq", "geom"])
80 
81 plane_separation = 4.71*units.mm
82 default_params = Params(
83  nfaces = 2,
84  anode_loc = (1,1,1), # layer, column, row
85  crate_addr = 10101, # arbitrary, take: layer*10000+column*100+row
86  face = FaceParams(3, 10),
87  board = BoardParams(8, 16),
88  daq = DaqParams(5, 4),
89  geom = [
90  GeomParams(
91  width = 2295*units.mm,
92  height = 5920*units.mm,
93  pitch = 4.669*units.mm,
94  angle = +35.707*units.deg,
95  offset = 0.3923*units.mm,
96  planex = 3*plane_separation
97  ),
98  GeomParams(
99  width = 2295*units.mm,
100  height = 5920*units.mm,
101  pitch = 4.669*units.mm,
102  angle = -35.707*units.deg,
103  offset = 0.3923*units.mm,
104  planex = 2*plane_separation
105  ),
106  GeomParams(
107  width = 2295*units.mm,
108  height = 5920*units.mm,
109  pitch = 4.790*units.mm,
110  angle = 0.0,
111  offset = 0.295*units.mm,
112  planex = plane_separation
113  ),
114  ]
115 )
116 
117 GeomPoint = namedtuple("GeomPoint", ["x","y","z"]) # face-specific coordinates
118 GeomWire = namedtuple("GeomWire", ["ploc", "wip", "spot", "seg", "p1" ,"p2"])
119 
120 class Description(object):
121  '''
122  Provide data methods to describe an APA and enumerate its connectivity.
123  '''
124  def __init__(self, params = default_params):
125  self.p = params
126 
127  # Total numbers of things in one APA.
128  # Some of this is just a copy in order to present a flat namespace
129  self.nfaces = self.p.nfaces
130  self.nplanes = self.nfaces * self.p.face.nlayers
131  self.nwibs = self.p.daq.nwibs
132  self.nboards = self.p.face.nboards*self.nfaces
133  self.nchips = self.nboards * self.p.board.nchips
134  self.nchannels = self.nchips*self.p.board.nchanperchip
135  self.nconductors = self.nchannels
136  #self.nwires, see below
137 
138  # List of indicies to boards in two ways: [face,board] and WIB
139  # [conn,slot]. A very smart layout convention adopted by the
140  # engineers let us do this so cleanly!
141  bi = numpy.array(range(self.nboards))
142  self.iboard_by_face_board = bi.reshape(self.p.nfaces, self.p.face.nboards)
143  self.iboard_by_conn_slot = bi.reshape(self.p.daq.nconnperwib, self.p.daq.nwibs)
144 
145  # List of indices to chips, accessed by [face,board_in_face,chip_on_board]
146  ci = numpy.array(range(self.nchips))
147  self.ichip_by_face_board_chip = ci.reshape(self.p.nfaces, self.p.face.nboards, self.p.board.nchips)
148 
149 
150  # List of indices to all conductors (or all channels) in an APA
151  # accessed by [face, board_in_face, chip_in_board, chan_in_chip]
152  # nominal: 2x10x8x16
153  ci = numpy.array(range(self.nchannels))
154  self.iconductor_by_face_board_chip_chan = ci.reshape(self.p.nfaces, self.p.face.nboards,
155  self.p.board.nchips, self.p.board.nchanperchip)
156 
157  # Flattened (layer-conductor)->(chip,channel) dictionary
159  counter = Counter()
160  for k,v in self.ccls:
161  counter[k[0]] += 1
162  # nominal: (40,40,48)
163  self.nch_in_board_by_layer = tuple([kv[1] for kv in sorted(counter.items())])
164 
165 
166  self.points = list()
167 
168  ### wires ###
169  #
170  # Caution: wires are a bit tricky. While each wire is
171  # physically unique, each also shares a conceptual twin on the
172  # other side of the APA, given rotational symmetry about the Y
173  # axis. These twins share the same points but these points
174  # are expressed in two different coordinate systems, one for
175  # each face! When drawing a 2D representation of wires using
176  # these points a rotation must be taken into account.
177  self.wires_by_face_plane = [list(), list()]
178  for iplane, geom in enumerate(self.p.geom):
179  rect = generator.Rectangle(geom.width, geom.height)
180  # (ap, side, spot, seg, p1, p2)
181  raw_wires = generator.wrapped_from_top_oneside(geom.offset, geom.angle, geom.pitch, rect)
182  raw_wires.sort()
183  gwires_front = list()
184  gwires_back = list()
185  for wip, raw_wire in enumerate(raw_wires):
186  ap, side, spot, seg, zy1, zy2 = raw_wire
187  ip1 = len(self.points)
188  p1 = GeomPoint(geom.planex, zy1[1], zy1[0])
189  self.points.append(p1)
190  ip2 = len(self.points)
191  p2 = GeomPoint(geom.planex, zy2[1], zy2[0])
192  self.points.append(p2)
193  wf = GeomWire(ap, wip, spot, seg, ip1, ip2)
194  wb = GeomWire(ap, wip, spot, seg, ip1, ip2)
195  gwires_front.append(wf)
196  gwires_back.append(wb)
197  self.wires_by_face_plane[0].append(gwires_front)
198  self.wires_by_face_plane[1].append(gwires_back)
199 
200  self.nwires_by_plane = [len(l) for l in self.wires_by_face_plane[0]]
202  self.nwires = self.nfaces * self.nwires_per_face
203  self.npoints = len(self.points)
204 
205  def wire_index_by_wip(self, face, plane, wip):
206  '''
207  Return a tuple of a global wire index and the gwire
208  '''
209  index = face*self.nwires_per_face + sum(self.nwires_by_plane[:plane]) + wip
210  wire = self.wires_by_face_plane[face][plane][wip]
211  return (index,wire)
212 
213  def iconductor_by_face_plane_spot(self, face, plane_in_face, spot_in_plane):
214  '''
215  Return the global conductor index based on the face, plane and spot.
216  '''
217  nch = self.nch_in_board_by_layer[plane_in_face] # 40,40,48
218  board_in_face = spot_in_plane//nch
219  spot_in_layer = spot_in_plane%nch
220  return self.iconductor_chip_chan(face, board_in_face, plane_in_face, spot_in_layer)[0]
221 
222 
223  def iconductor_chip_chan(self, face, board_in_face, layer_in_face, wire_spot_in_layer):
224  '''
225  Given the paramers return information about the associated
226  conductor as a triple:
227 
228  - iconductor :: the apa-global index for the conductor
229 
230  - chip :: the board-local index for the chip
231 
232  - chan :: the chip-local index for the channel
233  '''
234  # must +1 the spot to match the matrix assumption
235  nchip,nch = self.ccls[("uvw"[layer_in_face], wire_spot_in_layer+1)]
236  # must -1 the returns to match our assumption
237  ichip,ich = nchip-1, nch-1
238  icond = self.iconductor_by_face_board_chip_chan[face, board_in_face, ichip, ich]
239  return (icond, ichip, ich)
240 
241  def iface_board(self, iboard):
242  '''
243  Given a global board index, return tuple of:
244 
245  - iface :: the apa-global face index
246  - board : the face-local board index
247  '''
248  if not iboard in range(self.nboards):
249  raise ValueError("iboard is out of range: %d" % iboard)
250  iface = iboard//self.p.face.nboards
251  board = iboard%self.p.face.nboards
252  return (iface,board)
253 
254  def iplane(self, iface, plane_in_face):
255  'Return global plane index given global face and plane in face'
256  # trivial...
257  return iface*self.p.face.nlayers + plane_in_face
258 
259 
260 
261  pass
262 
263 
264 # A set of makers to create objects in the APA heirachy. Each call to
265 # a maker should return an instance of its associated type. This
266 # construction call should take parameters with names taken from by
267 # its connected types which should accept values as returned by other
268 # makers. All construction arguments are optional and may be set as
269 # attributes after an object is created. If an object has a
270 # one-to-many relationship with another object then the paramter name
271 # is pluralized (in all case simply by appending "s"), else it is
272 # singular.
273 
274 Parts = namedtuple("Parts",
275  ["detector", "apa", "face", "plane",
276  "wib", "board", "chip", "conductor",
277  "channel", "wire", "point"
278  ])
279 
280 def maker(G, ac, typename):
281  try:
282  n = getattr(ac, 'n'+typename+'s')
283  except AttributeError:
284  G.add_node(typename, type=typename)
285  return typename
286  ret = list()
287  for ind in range(n):
288  name = "%s%d" % (typename, ind)
289  G.add_node(name, type=typename, index=ind)
290  ret.append(name)
291  return ret
292 
293 
294 def graph(desc, maker = maker):
295  '''
296  Return a directed graph expressing the connectivity and
297  containment given the apa.Description object.
298  '''
299  G = networkx.DiGraph()
300 
301  p = Parts(*[maker(G, desc, typename) for typename in Parts._fields])
302 
303  def join(n1, n2, link, relationship = "containment", **params):
304 
305  if relationship in ["peer", "sibling"]:
306  G.add_edge(n1, n2, relationship=relationship, link=link, **params)
307  G.add_edge(n2, n1, relationship=relationship, link=link, **params)
308  return
309  if relationship in ["direct","simple"]:
310  G.add_edge(n1, n2, relationship=relationship, link=link, **params)
311  return
312  # parent/child
313  G.add_edge(n1, n2, relationship="children", link=link, **params)
314  G.add_edge(n2, n1, relationship="parent", link=link, **params)
315 
316  # FIXME: for now hard-code the single apa connection. This should
317  # be expanded to 6 for protoDUNE or 150 for DUNE SP FD. But, each
318  # APA is identical up to a translation in global coordinates so it
319  # is not yet obvious that it's necessary to exaustively construct
320  # them all.
321  join(p.detector, p.apa, 'submodule', anode_lcr = desc.p.anode_loc)
322 
323  # variable name convention below: gi_ = "global index", "i_" just an index.
324  # everything else an object
325  for gi_wib in range(desc.nwibs):
326  wib = p.wib[gi_wib]
327 
328  join(p.apa, wib, 'slot', slot=gi_wib)
329 
330 
331  for i_wibconn in range(desc.p.daq.nconnperwib):
332  gi_board = desc.iboard_by_conn_slot[i_wibconn, gi_wib]
333  board = p.board[gi_board]
334 
335  join(wib, board, 'cable', connector = i_wibconn)
336 
337  gi_face, iboard_in_face = desc.iface_board(gi_board)
338 
339  for ilayer_in_face, ispots_in_layer in enumerate(desc.nch_in_board_by_layer):
340  for ispot_in_layer in range(ispots_in_layer): # 40,40,48
341  gi_cond, ichip_on_board, ichan_in_chip \
342  = desc.iconductor_chip_chan(gi_face, iboard_in_face,
343  ilayer_in_face, ispot_in_layer)
344 
345  conductor = p.conductor[gi_cond]
346  join(board, conductor, 'trace', layer=ilayer_in_face, spot=ispot_in_layer)
347 
348  gi_chip = desc.ichip_by_face_board_chip[gi_face, iboard_in_face, ichip_on_board]
349  chip = p.chip[gi_chip]
350 
351  # Note: this will be called multiple times. Since
352  # G is not a multigraph subsequent calls are no-ops
353  join(board, chip, 'place', spot=ichip_on_board)
354 
355  # channels and conductors are one-to-one
356  channel = p.channel[gi_cond]
357  join(chip, channel, 'address', address=ichan_in_chip)
358 
359  join(channel, conductor, 'channel', relationship="peer")
360 
361  for ipoint, point in enumerate(p.point):
362  G.node[point]['pos'] = desc.points[ipoint]
363 
364  for gi_face in range(desc.nfaces):
365  face = p.face[gi_face]
366  join(p.apa, face, 'side', side=gi_face)
367 
368  for iboard_in_face in range(desc.p.face.nboards):
369  gi_board = desc.iboard_by_face_board[gi_face, iboard_in_face]
370  board = p.board[gi_board]
371  join(face, board, 'spot', spot = iboard_in_face)
372 
373  for iplane_in_face, wires in enumerate(desc.wires_by_face_plane[gi_face]):
374 
375  gi_plane = desc.iplane(gi_face, iplane_in_face)
376  plane = p.plane[gi_plane]
377  join(face, plane, 'plane', plane=iplane_in_face)
378 
379  for wip in range(desc.nwires_by_plane[iplane_in_face]):
380  gi_wire, gwire = desc.wire_index_by_wip(gi_face, iplane_in_face, wip)
381  wire = p.wire[gi_wire]
382  join(plane, wire, 'wip', wip=wip)
383  G.node[wire]['pitchloc'] = gwire.ploc
384 
385  # odd segments are on the "other" face.
386  spot_face = (gi_face + gwire.seg%2)%2
387 
388  gi_conductor = desc.iconductor_by_face_plane_spot(spot_face, iplane_in_face, gwire.spot)
389  conductor = p.conductor[gi_conductor]
390  join(conductor, wire, 'segment', segment=gwire.seg)
391 
392  tail = p.point[gwire.p1]
393  join(wire, tail, 'pt', endpoint=0)
394  head = p.point[gwire.p2]
395  join(wire, head, 'pt', endpoint=1)
396 
397  return G,p
398 
399 # The (#layers, #columns, #rows) for DUNE "anodes" in different detectors
400 oneapa_lcr = (1,1,1)
401 protodun_lcr = (1,2,3)
402 dune_lcr = (2,25,3)
403 
404 
405 def channel_tuple(G, wire):
406  '''
407  Return a channel address tuple associated with the given wire.
408 
409  The tuple is intended to be directly passed to channel_hash().
410  '''
411 
412  # fixme: some problems with dependencies here:
413  from .graph import parent
414 
415  conductor = parent(G, wire, 'conductor')
416  channel = parent(G, conductor, 'channel')
417  chip = parent(G, channel, 'chip')
418  board = parent(G, chip, 'board')
419  box = parent(G, board, 'face')
420  wib = parent(G, board, 'wib')
421  apa = parent(G, wib, 'apa')
422 
423  islot = G[apa][wib]['slot']
424  iconn = G[wib][board]['connector']
425  ichip = G[board][chip]['spot']
426  iaddr = G[chip][channel]['address']
427  return (iconn, islot, ichip, iaddr)
428 
429 def channel_hash(iconn, islot, ichip, iaddr):
430  '''
431  Hash a channel address tuple into a single integer.
432 
433  See also channel_tuple().
434  '''
435  return int("%d%d%d%02d" % (iconn+1, islot+1, ichip+1, iaddr+1))
436 
437 def channel_unhash(chident):
438  '''
439  Return tuple used to produce the given hash.
440  '''
441  a = str(chident)
442  return tuple([int(n)-1 for n in [a[0], a[1], a[2], a[3:5]]])
443 
444 
445 def channel_ident(G, wire):
446  '''
447  Return an identifier number for the channel attached to the given wire.
448  '''
449  return channel_hash(*channel_tuple(G, wire))
450 
451 
452 
453 class Plex(object):
454  '''
455  Provide an instance of a Plex that provides some convenient
456  mappings on the connection graph.
457 
458  This assumes the given graph was created using conventions
459  expressed in parent module.
460 
461  The term "Plex" for this role was invented by MINOS.
462  '''
463 
464  def __init__(self, G, P):
465  self.G = G
466  self.P = P
467 
468  def channel_plane(self, chanidents):
469  '''
470  Return a generator of plane numbers, one for each chanidents.
471  '''
472  for chid in chanidents:
473  iconn, islot, ichip, iaddr = channel_unhash(chid)
474  yield chip_channel_layer[ichip, iaddr]
475 
def iplane(self, iface, plane_in_face)
Definition: apa.py:254
def graph(desc, maker=maker)
Definition: apa.py:294
int Counter[nCounterTag]
Definition: makeDST.cxx:47
def flatten_cclsm(mat=chip_channel_layer_spot_matrix)
Definition: apa.py:60
auto enumerate(Iterables &&...iterables)
Range-for loop helper tracking the number of iteration.
Definition: enumerate.h:69
def __init__(self, G, P)
Definition: apa.py:464
def wire_index_by_wip(self, face, plane, wip)
Definition: apa.py:205
def __init__(self, params=default_params)
Definition: apa.py:124
def channel_tuple(G, wire)
Definition: apa.py:405
def iconductor_by_face_plane_spot(self, face, plane_in_face, spot_in_plane)
Definition: apa.py:213
def iface_board(self, iboard)
Definition: apa.py:241
def channel_plane(self, chanidents)
Definition: apa.py:468
def maker(G, ac, typename)
Definition: apa.py:280
def iconductor_chip_chan(self, face, board_in_face, layer_in_face, wire_spot_in_layer)
Definition: apa.py:223
def parent(G, child, parent_type)
Definition: graph.py:67
static QCString str
def channel_ident(G, wire)
Definition: apa.py:445
def channel_unhash(chident)
Definition: apa.py:437
def channel_hash(iconn, islot, ichip, iaddr)
Definition: apa.py:429