Ejemplo n.º 1
0
def detector(pmt_radius=14000.0, sphere_radius=14500.0, spiral_step=350.0):
    pmt = build_8inch_pmt_with_lc()
    geo = Detector(water)

    geo.add_solid(Solid(sphere(sphere_radius,nsteps=200), 
                        water, water, 
                        surface=black_surface,
                        color=0xBBFFFFFF))

    for position in spherical_spiral(pmt_radius, spiral_step):
        direction = -normalize(position)

        # Orient PMT that starts facing Y axis
        y_axis = np.array((0.0,1.0,0.0))
        axis = np.cross(direction, y_axis)
        angle = np.arccos(np.dot(y_axis, direction))
        rotation = make_rotation_matrix(angle, axis)

        # Place PMT (note that position is front face of PMT)
        geo.add_pmt(pmt, rotation, position)
        
    
    time_rms = 1.5 # ns
    charge_mean = 1.0
    charge_rms = 0.1 # Don't I wish!
    
    geo.set_time_dist_gaussian(time_rms, -5 * time_rms, 5*time_rms)
    geo.set_charge_dist_gaussian(charge_mean, charge_rms, 0.0, charge_mean + 5*charge_rms)

    logger.info('Demo detector: %d PMTs' % geo.num_channels())
    logger.info('               %1.1f ns time RMS' % time_rms)
    logger.info('               %1.1f%% charge RMS' % (100.0*charge_rms/charge_mean))
    return geo
Ejemplo n.º 2
0
    def setUp(self):
        # Setup geometry
        cube = Detector(vacuum)
        cube.add_pmt(Solid(box(10.0,10,10), vacuum, vacuum, surface=r7081hqe_photocathode))
        cube.set_time_dist_gaussian(1.2, -6.0, 6.0)
        cube.set_charge_dist_gaussian(1.0, 0.1, 0.5, 1.5)

        geo = create_geometry_from_obj(cube, update_bvh_cache=False)

        self.geo = geo
        self.sim = Simulation(self.geo, geant4_processes=0)
Ejemplo n.º 3
0
    def setUp(self):
        # Setup geometry
        cube = Detector(vacuum)
        cube.add_pmt(
            Solid(box(10.0, 10, 10),
                  vacuum,
                  vacuum,
                  surface=r7081hqe_photocathode))
        cube.set_time_dist_gaussian(1.2, -6.0, 6.0)
        cube.set_charge_dist_gaussian(1.0, 0.1, 0.5, 1.5)

        geo = create_geometry_from_obj(cube, update_bvh_cache=False)

        self.geo = geo
        self.sim = Simulation(self.geo, geant4_processes=0)
Ejemplo n.º 4
0
 def build_detector(self, detector=None, volume_classifier=_default_volume_classifier):
     '''
     Add the meshes defined by this GDML to the detector. If detector is not
     specified, a new detector will be created.
     
     The volume_classifier should be a function that returns a classification
     of the volume ('pmt','solid','omit') and kwargs passed to the Solid
     constructor for that volume: material1, material2, color, surface
     
     The different classifications have different behaviors:
     'pmt' should specify channel_type in the kwargs to identify the channel, calls add_pmt
     'solid' will add a normal solid to the Chroma geometry, calls add_solid
     'omit' will not add the Solid to the Chroma geometry
     '''
     if detector is None:
         detector = Detector(vacuum)
     q = deque()
     q.append([self.world, np.zeros(3), np.identity(3), None])
     while len(q):
         v, pos, rot, parent_material_ref = q.pop()
         for child, c_pos, c_rot in zip(v.children, v.child_pos, v.child_rot):
             c_pos = self.get_vals(c_pos) if c_pos is not None else np.zeros(3)
             c_rot = self.get_vals(c_rot) if c_rot is not None else np.identity(3)
             c_pos = np.matmul(c_pos,rot)+pos
             x_rot = make_rotation_matrix(c_rot[0], [1, 0, 0])
             y_rot = make_rotation_matrix(c_rot[1], [0, 1, 0])
             z_rot = make_rotation_matrix(c_rot[2], [0, 0, 1])
             c_rot = np.matmul(rot, np.matmul(x_rot, np.matmul(y_rot, z_rot))) #FIXME verify this order
             q.append([child, c_pos, c_rot, v.material_ref])
         m = self.get_mesh(v.solid_ref)
         mesh = Mesh(m.vertices, m.faces) # convert PyMesh mesh to Chroma mesh
         classification, kwargs = volume_classifier(v.name, v.material_ref, parent_material_ref)
         if classification == 'pmt':
             channel_type = kwargs.pop('channel_type',None)
             solid = Solid(mesh, **kwargs)
             detector.add_pmt(solid, displacement=pos, rotation=rot, channel_type=channel_type)   
         elif classification == 'solid':
             solid = Solid(mesh, **kwargs)
             detector.add_solid(solid, displacement=pos, rotation=rot)   
         elif classification == 'omit':
             pass
         else:
             raise Exception('Unknown volume classification: '+classification)
     return detector
Ejemplo n.º 5
0
def build_detector():
    """Returns a cubic detector made of cubic photodetectors."""
    world_size = 1000000 # 1 km

    d = Detector(ice)

    #add DOMs at locations x,y,z
    
    channel_id = 0

    for x in np.arange(-500000,500001,100000):
        for y in np.arange(-500000,500001,100000):
            for z in np.arange(-500000,500001,100000):
                d.add_pmt(build_dom(),displacement=(x,y,z),channel_id=channel_id)
                channel_id += 1

    world = Solid(make.box(world_size,world_size,world_size),ice,vacuum,color=0x33ffffff)
    d.add_solid(world)

    return d
Ejemplo n.º 6
0
    def setUp(self):
        # Setup geometry
        cube = Detector(vacuum)
        cube.add_pmt(
            Solid(box(10.0, 10.0, 10.0),
                  vacuum,
                  vacuum,
                  surface=r7081hqe_photocathode))
        cube.set_time_dist_gaussian(1.2, -6.0, 6.0)
        cube.set_charge_dist_gaussian(1.0, 0.1, 0.5, 1.5)

        geo = create_geometry_from_obj(cube,
                                       update_bvh_cache=True,
                                       read_bvh_cache=False)
        print "Number of channels in detector: ", geo.num_channels()
        self.geo = geo
        self.sim = Simulation(self.geo,
                              geant4_processes=0,
                              nthreads_per_block=1,
                              max_blocks=1)

        self.rfile = rt.TFile("output_test_detector.root", "recreate")
        self.htime = rt.TH1D("htime", "Time;ns", 120, 80, 120)
        self.hcharge = rt.TH1D("hcharge", "Charge;pe", 100, 0.5, 1.5)
Ejemplo n.º 7
0
class ColladaToChroma(object):
    secs = {}
    surface_props = "detect absorb reemit reflect_diffuse reflect_specular eta k reemission_cdf".split(
    )

    def __init__(self, nodecls, bvh=False, dump_node_info=False):
        """
        :param nodecls: typically DAENode
        """
        log.debug("ColladaToChroma")
        self.dump_node_info = dump_node_info  # for debug
        self.nodecls = nodecls
        self.bvh = bvh
        #self.chroma_geometry = Geometry(detector_material=None)    # bialkali ?
        self.chroma_geometry = Detector(detector_material=None)
        pass
        self.vcount = 0

        self.surfaces = {}
        self.materials = {}  # dict of chroma.geometry.Material
        self._materialmap = {}  # dict with short name keys
        self._surfacemap = {}  # dict with short name keys

        # idmap checking
        self.channel_count = 0
        self.channel_ids = set()

    def convert_opticalsurfaces(self, debug=False):
        """
        """
        log.info("convert_opticalsurfaces")
        for dsurf in self.nodecls.extra.opticalsurface:
            surface = self.make_opticalsurface(dsurf, debug=debug)
            self.surfaces[surface.name] = surface
        pass
        #assert len(self.surfaces) == len(self.nodecls.extra.opticalsurface), "opticalsurface with duplicate names ? "
        log.info("convert_opticalsurfaces creates %s from %s  " %
                 (len(self.surfaces), len(self.nodecls.extra.opticalsurface)))

    def make_opticalsurface(self, dsurf, debug=False):
        """
        :param dsurf: G4DAE surface
        :return: Chroma surface 

        * name
        * model ? defaults to 0

        G4DAE Optical Surface properties

        * REFLECTIVITY (the only property to be widely defined)
        * RINDEX (seems odd for a surface, looks to always be zero) 
        * SPECULARLOBECONSTANT  (set to 0.85 for a few surface)
        * BACKSCATTERCONSTANT,SPECULARSPIKECONSTANT (often present, always zero)


        `chroma/geometry_types.h`::

           enum { SURFACE_DEFAULT, SURFACE_COMPLEX, SURFACE_WLS };

        Potentially wavelength dependent props all default to zero.
        Having values for these is necessary to get SURFACE_DETECT, SURFACE_ABSORB

        * detect
        * absorb
        * reflect_diffuse
        * reflect_specular

        * reemit
        * eta
        * k
        * reemission_cdf


        `chroma/cuda/photon.h`::

            701 __device__ int
            702 propagate_at_surface(Photon &p, State &s, curandState &rng, Geometry *geometry,
            703                      bool use_weights=false)
            704 {
            705     Surface *surface = geometry->surfaces[s.surface_index];
            706 
            707     if (surface->model == SURFACE_COMPLEX)
            708         return propagate_complex(p, s, rng, surface, use_weights);
            709     else if (surface->model == SURFACE_WLS)
            710         return propagate_at_wls(p, s, rng, surface, use_weights);
            711     else
            712     {
            713         // use default surface model: do a combination of specular and
            714         // diffuse reflection, detection, and absorption based on relative
            715         // probabilties
            716 
            717         // since the surface properties are interpolated linearly, we are
            718         // guaranteed that they still sum to 1.0.
            719         float detect = interp_property(surface, p.wavelength, surface->detect);
            720         float absorb = interp_property(surface, p.wavelength, surface->absorb);
            721         float reflect_diffuse = interp_property(surface, p.wavelength, surface->reflect_diffuse);
            722         float reflect_specular = interp_property(surface, p.wavelength, surface->reflect_specular);
            723 
        """
        if debug:
            print "%-75s %s " % (dsurf.name, dsurf)
        surface = Surface(dsurf.name)

        finish_map = {
            OpticalSurfaceFinish.polished: 'reflect_specular',
            OpticalSurfaceFinish.ground: 'reflect_diffuse',
        }

        if 'EFFICIENCY' in dsurf.properties:
            EFFICIENCY = dsurf.properties.get('EFFICIENCY', None)
            surface.set('detect',
                        EFFICIENCY[:, 1],
                        wavelengths=EFFICIENCY[:, 0])
            pass
        elif 'REFLECTIVITY' in dsurf.properties:
            REFLECTIVITY = dsurf.properties.get('REFLECTIVITY', None)
            key = finish_map.get(int(dsurf.finish), None)
            if key is None or REFLECTIVITY is None:
                log.warn(
                    "miss REFLECTIVITY key : not setting REFLECTIVITY for %s "
                    % surface.name)
            else:
                log.debug("setting prop %s for surface %s " %
                          (key, surface.name))
                surface.set(key,
                            REFLECTIVITY[:, 1],
                            wavelengths=REFLECTIVITY[:, 0])
            pass
        else:
            log.warn(" no REFLECTIVITY/EFFICIENCY in dsurf.properties %s " %
                     repr(dsurf.properties))
        pass
        return surface

    def collada_materials_summary(self,
                                  names=['GdDopedLS', 'LiquidScintillator']):
        collada = self.nodecls.orig
        find_ = lambda name: filter(lambda m: m.id.find(name) > -1, collada.
                                    materials)
        for name in names:
            mats = find_(name)
            assert len(mats) == 1, "name is ambiguous or missing"
            self.dump_collada_material(mats[0])
        pass

    def dump_collada_material(self, mat):
        extra = getattr(mat, 'extra', None)
        keys = extra.properties.keys() if extra else []
        print mat.id
        keys = sorted(keys,
                      key=lambda k: extra.properties[k].shape[0],
                      reverse=True)
        for k in keys:
            xy = extra.properties[k]
            x = xy[:, 0]
            y = xy[:, 1]
            print "%30s %10s    %10.3f %10.3f   %10.3f %10.3f   " % (
                k, repr(xy.shape), x.min(), x.max(), y.min(), y.max())

    def convert_materials(self, debug=False):
        """
        #. creates chroma Material instances for each collada material 
        #. fills in properties from the collada extras
        #. records materials in a map keyed by material.name

        Chroma materials default to None, 3 settings:

        * refractive_index
        * absorption_length
        * scattering_length

        And defaults to zero, 2 settings:

        * reemission_prob
        * reemission_cdf

        G4DAE materials have an extra attribute dict that 
        contains keys such as

        * RINDEX
        * ABSLENGTH
        * RAYLEIGH

        Uncertain of key correspondence, especially REEMISSIONPROB
        and what about reemission_cdf ? Possibly the many Scintillator
        keys can provide that ?

        Probably many the scintillator keys are only relevant to photon 
        production rather than photon propagation, so they are irrelevant 
        to Chroma.

        Which materials have each::

             EFFICIENCY                     [1 ] Bialkali 

             -------------- assumed to not apply to optical photons ---------

             FASTTIMECONSTANT               [2 ] GdDopedLS,LiquidScintillator 
             SLOWTIMECONSTANT               [2 ] GdDopedLS,LiquidScintillator 
             YIELDRATIO                     [2 ] GdDopedLS,LiquidScintillator 

             GammaFASTTIMECONSTANT          [2 ] GdDopedLS,LiquidScintillator 
             GammaSLOWTIMECONSTANT          [2 ] GdDopedLS,LiquidScintillator 
             GammaYIELDRATIO                [2 ] GdDopedLS,LiquidScintillator 

             AlphaFASTTIMECONSTANT          [2 ] GdDopedLS,LiquidScintillator 
             AlphaSLOWTIMECONSTANT          [2 ] GdDopedLS,LiquidScintillator 
             AlphaYIELDRATIO                [2 ] GdDopedLS,LiquidScintillator 

             NeutronFASTTIMECONSTANT        [2 ] GdDopedLS,LiquidScintillator 
             NeutronSLOWTIMECONSTANT        [2 ] GdDopedLS,LiquidScintillator 
             NeutronYIELDRATIO              [2 ] GdDopedLS,LiquidScintillator 

             SCINTILLATIONYIELD             [2 ] GdDopedLS,LiquidScintillator 
             RESOLUTIONSCALE                [2 ] GdDopedLS,LiquidScintillator 

             ---------------------------------------------------------------------

             ReemissionFASTTIMECONSTANT     [2 ] GdDopedLS,LiquidScintillator      for opticalphoton
             ReemissionSLOWTIMECONSTANT     [2 ] GdDopedLS,LiquidScintillator 
             ReemissionYIELDRATIO           [2 ] GdDopedLS,LiquidScintillator 

             FASTCOMPONENT                  [2 ] GdDopedLS,LiquidScintillator     "Fast_Intensity"
             SLOWCOMPONENT                  [2 ] GdDopedLS,LiquidScintillator     "Slow_Intensity"
             REEMISSIONPROB                 [2 ] GdDopedLS,LiquidScintillator     "Reemission_Prob"

             ------------------------------------------------------------------------

             RAYLEIGH                       [5 ] GdDopedLS,Acrylic,Teflon,LiquidScintillator,MineralOil 
             RINDEX                         [14] Air,GdDopedLS,Acrylic,Teflon,LiquidScintillator,Bialkali,
                                                 Vacuum,Pyrex,MineralOil,Water,NitrogenGas,IwsWater,OwsWater,DeadWater 
             ABSLENGTH                      [20] PPE,Air,GdDopedLS,Acrylic,Teflon,LiquidScintillator,Bialkali,
                                                 Vacuum,Pyrex,UnstStainlessSteel,StainlessSteel,
                                                 ESR,MineralOil,Water,NitrogenGas,IwsWater,ADTableStainlessSteel,Tyvek,OwsWater,DeadWater 

        Observations:
 
        #. no RAYLEIGH for water 


        """
        keymap = {
            "RINDEX": 'refractive_index',
            "ABSLENGTH": 'absorption_length',
            "RAYLEIGH": 'scattering_length',
            "REEMISSIONPROB": 'reemission_prob',
        }

        keymat = {}
        collada = self.nodecls.orig
        for dmaterial in collada.materials:
            material = Material(dmaterial.id)
            if DEBUG:
                material.dae = dmaterial

            # vacuum like defaults ? is that appropriate ? what is the G4 equivalent ?
            material.set('refractive_index', 1.0)
            material.set('absorption_length', 1e6)
            material.set('scattering_length', 1e6)

            if dmaterial.extra is not None:
                props = dmaterial.extra.properties
                for dkey, dval in props.items():
                    if dkey not in keymat:
                        keymat[dkey] = []
                    keymat[dkey].append(
                        material.name
                    )  # record of materials that have each key

                    if dkey in keymap:
                        key = keymap[dkey]
                        material.set(key, dval[:, 1], wavelengths=dval[:, 0])
                        log.debug(
                            "for material %s set Chroma prop %s from G4DAE prop %s vals %s "
                            % (material.name, key, dkey, len(dval)))
                    else:
                        log.debug(
                            "for material %s skipping G4DAE prop %s vals %s " %
                            (material.name, dkey, len(dval)))
                    pass
                    self.setup_cdf(material, props)
                pass
            pass
            self.materials[material.name] = material
        pass
        log.debug("convert_materials G4DAE keys encountered : %s " %
                  len(keymat))
        if debug:
            for dkey in sorted(keymat, key=lambda _: len(keymat[_])):
                mats = keymat[dkey]
                print " %-30s [%-2s] %s " % (dkey, len(mats), ",".join(
                    map(matshorten, mats)))

    def setup_cdf(self, material, props):
        """
        Chroma uses "reemission_cdf" cumulative distribution function 
        to generate the wavelength of reemission photons. 

        Currently think that the name "reemission_cdf" is misleading, 
        as it is the RHS normalized CDF obtained from an intensity distribution
        (photon intensity as function of wavelength) 

        NB REEMISSIONPROB->reemission_prob is handled as a 
        normal keymapped property, no need to integrate to construct 
        the cdf for that.
    
        Compare this with the C++

           DsChromaG4Scintillation::BuildThePhysicsTable()  

        """
        fast = props.get('FASTCOMPONENT', None)
        slow = props.get('SLOWCOMPONENT', None)
        reem = props.get('REEMISSIONPROB', None)

        if fast is None or slow is None or reem is None:
            return

        assert not fast is None
        assert not slow is None
        assert not reem is None

        assert np.all(fast == slow)  # CURIOUS, that these are the same

        reemission_cdf = construct_cdf_energywise(fast)

        ## yep "fast" : need intensity distribution
        #
        #   reem_cdf = construct_cdf( reem )
        #
        #   Nope the CDF are used to generate wavelengths
        #   following the desired slow/fast intensity distribution
        #   [ie number of photons in wavelength ranges]
        #   (which happen to be the same)
        #
        #   conversely the reemission probability gives the
        #   fraction that reemit at the wavelength
        #   that value can be used directly by random uniform throws
        #   to decide whether to reemit no cdf gymnastics needed
        #   as are just determining whether somethinh happens not
        #   the wavelength distribution  of photons
        #
        #

        log.debug("setting reemission_cdf for %s to %s " %
                  (material.name, repr(reemission_cdf)))

        #material.set('reemission_cdf', reemission_cdf[:,1], wavelengths=reemission_cdf[:,0])
        material.setraw('reemission_cdf', reemission_cdf)

    def _get_materialmap(self):
        """
        Dict of chroma.geometry.Material instances with short name keys   
        """
        if len(self._materialmap) == 0:
            prefix = '__dd__Materials__'
            for name, mat in self.materials.items():
                if name.startswith(prefix):
                    name = name[len(prefix):]
                if name[-9:-7] == '0x':
                    name = name[:-9]
                pass
                self._materialmap[name] = mat
            pass
        return self._materialmap

    materialmap = property(_get_materialmap)

    def _get_surfacemap(self):
        """
        Dict of chroma.geometry.Surface instances with short name keys   
        """
        postfix = 'Surface'
        if len(self._surfacemap) == 0:
            for name, surf in self.surfaces.items():
                prefix = "__".join(name.split("__")[:-1]) + "__"
                if name.startswith(prefix):
                    nam = name[len(prefix):]
                if nam.endswith(postfix):
                    nam = nam[:-len(postfix)]
                pass
                self._surfacemap[nam] = surf
            pass
        return self._surfacemap

    surfacemap = property(_get_surfacemap)

    def property_plot(self, matname, propname):
        import matplotlib.pyplot as plt
        mat = self.materialmap[matname]
        xy = mat.daeprops[propname]
        #plt.plot( xy[:,0], xy[:,1] )
        plt.plot(*xy.T)

    def convert_geometry_traverse(self, nodes=None):
        log.debug("convert_geometry_traverse")
        if nodes is None:
            self.nodecls.vwalk(self.visit)
        else:
            for node in nodes:
                self.visit(node)
        pass
        self.dump_channel_info()

    def convert_flatten(self):
        log.debug("ColladaToChroma convert_geometry flattening %s " %
                  len(self.chroma_geometry.solids))
        self.chroma_geometry.flatten()

    def convert_make_maps(self):
        self.cmm = self.make_chroma_material_map(self.chroma_geometry)
        self.csm = self.make_chroma_surface_map(self.chroma_geometry)

    def convert_geometry(self, nodes=None):
        """
        :param nodes: list of DAENode instances or None

        Converts DAENode/pycollada geometry into Chroma geometry.

        When `nodes=None` the entire DAENode tree is visited and converted, 
        otherwise just the listed nodes.
        """
        log.debug("convert_geometry")

        self.convert_materials()
        self.convert_opticalsurfaces()
        self.convert_geometry_traverse(nodes)
        self.convert_flatten()
        self.convert_make_maps()

        if self.bvh:
            self.add_bvh()
        return self.chroma_geometry

    def convert_geometry_partial(self, nodes=None):
        """ 
        splitting the conversion in order to provide a point at which to hack the geometry.
        this includes removing sibling overlapping triangles or defining wire meshes.
        """
        log.debug("convert_geometry")

        self.convert_materials()
        self.convert_opticalsurfaces()
        self.convert_geometry_traverse(nodes)

        return self.chroma_geometry

    def finish_converting_geometry(self):
        self.convert_flatten()
        self.convert_make_maps()

        if self.bvh:
            self.add_bvh()
        return self.chroma_geometry

    def make_chroma_material_map(self, chroma_geometry):
        """
        Curiously the order of chroma_geometry.unique_materials on different invokations is 
        "fairly constant" but not precisely so. 
        How is that possible ? Perfect or random would seem more likely outcomes. 
        """
        unique_materials = chroma_geometry.unique_materials
        material_lookup = dict(
            zip(unique_materials, range(len(unique_materials))))
        cmm = dict([(material_lookup[m], m.name)
                    for m in filter(None, unique_materials)])
        cmm[-1] = "ANY"
        cmm[999] = "UNKNOWN"
        return cmm

    def make_chroma_surface_map(self, chroma_geometry):
        unique_surfaces = chroma_geometry.unique_surfaces
        surface_lookup = dict(zip(unique_surfaces,
                                  range(len(unique_surfaces))))
        csm = dict([(surface_lookup[s], s.name)
                    for s in filter(None, unique_surfaces)])
        csm[-1] = "ANY"
        csm[999] = "UNKNOWN"
        return csm

    def add_bvh(self,
                bvh_name="default",
                auto_build_bvh=True,
                read_bvh_cache=True,
                update_bvh_cache=True,
                cache_dir=None,
                cuda_device=None):
        """
        As done by chroma.loader
        """
        log.debug("ColladaToChroma adding BVH")
        self.chroma_geometry.bvh = load_bvh(self.chroma_geometry,
                                            bvh_name=bvh_name,
                                            auto_build_bvh=auto_build_bvh,
                                            read_bvh_cache=read_bvh_cache,
                                            update_bvh_cache=update_bvh_cache,
                                            cache_dir=cache_dir,
                                            cuda_device=cuda_device)
        log.debug("completed adding BVH")

    def find_outer_inner_materials(self, node):
        """
        :param node: DAENode instance
        :return: Chroma Material instances for outer and inner materials

        #. Parent node material regarded as outside
        #. Current node material regarded as inside        

        Think about a leaf node to see the sense of that.

        Caveat, the meanings of "inner" and "outer" depend on 
        the orientation of the triangles that make up the surface...  
        So just adopt a convention and try to verify it later.
        """
        assert node.__class__.__name__ == 'DAENode'
        this_material = self.materials[node.matid]
        if node.parent is None:
            parent_material = this_material
            log.warning(
                "setting parent_material to %s as parent is None for node %s "
                % (parent_material.name, node.id))
        else:
            parent_material = self.materials[node.parent.matid]

        log.debug("find_outer_inner_materials node %s %s %s" %
                  (node, this_material, parent_material))
        return parent_material, this_material

    def find_skinsurface(self, node):
        """
        :param node: DAENode instance
        :return: G4DAE Surface instance corresponding to G4LogicalSkinSurface if one is available for the LV of the current node


        * ambiguous skin for lvid __dd__Geometry__PMT__lvPmtHemiCathode0xc2cdca0 found 672 
        * if the properties are the same then ambiguity not a problem ?

        """
        assert node.__class__.__name__ == 'DAENode'
        assert self.nodecls.extra.__class__.__name__ == 'DAEExtra'

        ssid = self.nodecls.sensitive_surface_id(node)

        if not ssid is None:
            skin = self.nodecls.extra.skinmap.get(ssid, None)
            log.debug("ssid %s skin %s " % (ssid, repr(skin)))
            if skin is not None:
                if len(skin) > 0:  # expected for sensitives
                    skin = skin[0]
            pass
        else:
            lvid = node.lv.id
            skin = self.nodecls.extra.skinmap.get(lvid, None)
            if skin is not None:
                assert len(
                    skin) == 1, "ambiguous skin for lvid %s found %s  " % (
                        lvid, len(skin))
                ##log.warn("ambiguous skin for lvid %s found %s : USING FIRST  " % (lvid, len(skin)))
                skin = skin[0]
            pass

        return skin

    def find_bordersurface(self, node):
        """
        :param node: DAENode instance
        :return: G4DAE Surface instance corresponding to G4LogicalBorderSurface 
                 if one is available for the PVs of the current node and its parent

        Ambiguity bug makes this difficult
        """
        assert node.__class__.__name__ == 'DAENode'
        pass
        #pvid = node.pv.id
        #ppvid = node.parent.pv.id
        #border = self.nodecls.extra.bordermap.get(pvid, None)
        return None

    def find_surface(self, node):
        """
        :param node: DAENode instance
        :return Chroma Surface instance or None:

        G4DAE persists the below surface elements which 
        both reference "opticalsurface" containing the keyed properties
        
        * "skinsurface" (single volumeref, ref by lv.id)
        * "boundarysurface" (physvolref ordered pair, identified by pv1.id,pv2.id) 
          
        The boundary pairs are always parent/child nodes in dyb Near geometry, 
        they could in principal be siblings.
        """
        assert node.__class__.__name__ == 'DAENode'

        skin = self.find_skinsurface(node)
        border = self.find_bordersurface(node)

        dsurf = filter(None, [skin, border])
        assert len(
            dsurf
        ) < 2, "Not expecting both skin %s and border %s surface for the same node %s " % (
            skin, border, node)
        if len(dsurf) == 1:
            dsurface = dsurf[0]
            log.debug("found dsurface %s for node %s " % (dsurface, node))
            surface = self.surfaces.get(dsurface.name, None)
            assert surface is not None, "dsurface %s without corresponding chroma surface of name %s for node %s " % (
                dsurface, dsurface.name, node.id)
        else:
            surface = None
        pass
        return surface

    def visit(self, node, debug=False):
        """
        :param node: DAENode instance

        DAENode instances and their pycollada underpinnings meet chroma here

        Chroma needs sensitive detectors to have an associated surface 
        with detect property ...
        """
        #assert node.__class__.__name__ == 'DAENode'
        self.vcount += 1
        if self.vcount < 10:
            log.debug("visit : vcount %s node.index %s node.id %s " %
                      (self.vcount, node.index, node.id))

        bps = list(node.boundgeom.primitives())

        bpl = bps[0]

        assert len(bps) == 1 and bpl.__class__.__name__ == 'BoundPolylist'

        tris = bpl.triangleset()

        vertices = tris._vertex

        triangles = tris._vertex_index

        mesh = Mesh(vertices, triangles, remove_duplicate_vertices=False)

        material2, material1 = self.find_outer_inner_materials(node)

        surface = self.find_surface(
            node)  # lookup Chroma surface corresponding to the node
        if surface == None:
            surfacename = "NOT SPECIFIED"
        else:
            surfacename = surface.name
        if self.dump_node_info:
            print "[NODE %05d:%s]" % (
                node.index, node.lv.id
            ), " NTriangles=%d OuterMat=%s InnerMat=%s Surface=%s" % (len(
                mesh.triangles), material2.name, material1.name, surfacename)

        color = 0x33ffffff

        solid = Solid(mesh, material1, material2, surface, color)
        solid.node = node

        #
        # hmm a PMT is comprised of several volumes all of which
        # have the same associated channel_id
        #
        channel_id = getattr(node, 'channel_id', None)
        if not channel_id is None and channel_id > 0:
            self.channel_count += 1  # nodes with associated non zero channel_id
            self.channel_ids.add(channel_id)
            self.chroma_geometry.add_pmt(solid, channel_id=channel_id)
        else:
            self.chroma_geometry.add_solid(solid)
        pass

        if debug and self.vcount % 1000 == 0:
            print node.id
            print self.vcount, bpl, tris, tris.material
            print mesh
            #print mesh.assemble()
            bounds = mesh.get_bounds()
            extent = bounds[1] - bounds[0]
            print extent

    def dump_channel_info(self):
        log.info(
            "channel_count (nodes with channel_id > 0) : %s  uniques %s " %
            (self.channel_count, len(set(self.channel_ids))))
        log.debug("channel_ids %s " % repr(self.channel_ids))

    def surface_props_table(self):
        """
        ::

            plt.plot(*cc.surfacemap['NearOWSLiner'].reflect_diffuse.T)
            plt.plot(*cc.surfacemap['NearDeadLiner'].reflect_diffuse.T)
            plt.plot(*cc.surfacemap['NearIWSCurtain'].reflect_diffuse.T)  ## all three the same, up to plateau

            plt.plot(*cc.surfacemap['RSOil'].reflect_diffuse.T)    ## falloff 

            plt.plot(*cc.surfacemap['ESRAirSurfaceBot'].reflect_specular.T) 
            plt.plot(*cc.surfacemap['ESRAirSurfaceTop'].reflect_specular.T)  ## Bot and Top the same, cliff

        """
        def smry(spt, suppress="60.0:800.0 =0.0"):
            x, y = spt.T
            xsmr = "%3.1f:%3.1f" % (x.min(), x.max())
            ymin, ymax = y.min(), y.max()
            ysmr = "=%3.1f" % ymin if ymin == ymax else "%3.1f:%3.1f" % (ymin,
                                                                         ymax)
            s = "%s %s" % (xsmr, ysmr)
            return "-" if s == suppress else s

        pass
        lfmt_ = lambda _: "%-23s" % _
        bfmt_ = lambda _: " ".join(["%-25s" % s for s in _])
        print lfmt_("surf") + bfmt_(self.surface_props)
        for nam, surf in self.surfacemap.items():
            sprop = map(lambda prop: smry(getattr(surf, prop)),
                        self.surface_props)
            print lfmt_(nam) + bfmt_(sprop)