# <<BEGIN-copyright>>
# <<END-copyright>>
"""
This module contains the product class.
GNDS classes store reaction data in a hierarchical format. At the top is the product class. Next is the
channel, representing the reaction output channels. An output channel contains the Q-value and a list
of outgoing products (some of which may decay into other particles).
The channel is stored inside the reaction class, which consists of an input and output channel together,
(e.g. n_1 + O_16 --> H_1 + N_16). Finally, the reactionSuite class is used to store a list of reaction
for a given input channel (that is, for the incoming particles that come together in the reaction).
The following outline summarizes the classes.
:product:
Outgoing particle, including multiplicity and distribution information.
:channel:
A Q-value plus a list of products.
:reaction:
Contains a cross section (defined in the gnds.reactionData.crossSection module), along with
an outgoing channel which must be one of the classes defined in gnds.channels (e.g., 2n + Pu239).
:reactionSuite:
Contains all reactions sharing the same input channel (e.g. n + Pu239)
(e.g., n + Pu239 --> n + Pu239, n + Pu239 --> n + Pu239_e1,
n + Pu239 --> n + Pu239_e2, n + Pu239 --> 2n + Pu239 ... )
"""
from pqu import PQU as PQUModule
from PoPs import IDs as IDsPoPsModule
from PoPs import misc as miscPoPsModule
from PoPs.families import nuclide as nuclidePoPsModule
from fudge.core.utilities import brb
from xData import ancestry as ancestryModule
from xData import physicalQuantity as physicalQuantityModule
from fudge.processing import group as groupModule
from fudge.gnds import suites as suitesModule
from .productData.distributions import distribution as distributionModule
from .productData.distributions import unspecified as unspecifiedModule
from .productData import multiplicity as multiplicityModule
from .productData import energyDeposition as energyDepositionModule
from .productData import momentumDeposition as momentumDepositionModule
from . import channels as channelsModule
__metaclass__ = type
[docs]class product( ancestryModule.ancestry ) :
"""
This is the class for a gnds product. If the product can decay (e.g. for breakup reactions),
the resulting decay information is defined in the product outputChannel
"""
moniker = 'product'
ancestryMembers = ( 'multiplicity', 'distribution', 'outputChannel',
'energyDeposition', 'momentumDeposition' )
def __init__( self, id, label = None, attributes = None, outputChannel = None ) :
"""Creates a new product object."""
ancestryModule.ancestry.__init__( self )
self.__id = id
self.__label = label
self.__particle = None # lazy evaluation
self.attributes = {}
if attributes is not None:
for q in attributes : self.addAttribute( q, attributes[q] )
self.outputChannel = None
if( outputChannel is not None ) : self.addOutputChannel( outputChannel )
self.__multiplicity = multiplicityModule.component( )
self.__multiplicity.setAncestor( self )
self.__energyDeposition = energyDepositionModule.component( )
self.__energyDeposition.setAncestor( self )
self.__momentumDeposition = momentumDepositionModule.component( )
self.__momentumDeposition.setAncestor( self )
self.__distribution = distributionModule.component( )
self.__distribution.setAncestor( self )
[docs] def cullStyles( self, styleList ) :
self.__multiplicity.cullStyles( styleList )
self.__energyDeposition.cullStyles( styleList )
self.__momentumDeposition.cullStyles( styleList )
self.__distribution.cullStyles( styleList )
def __cmp__( self, other ) :
"""Compares self to other."""
if( self.id < other.id ) : return( -1 )
if( self.id > other.id ) : return( 1 )
if( self.outputChannel < other.outputChannel ) : return( -1 )
if( self.outputChannel > other.outputChannel ) : return( 1 )
return( 0 )
def __str__( self ) :
"""Converts product object to a string representation."""
return( self.toString( simpleString = False ) )
@property
def id( self ) :
return( self.__id )
@property
def pid( self ) :
return( self.id )
@property
def particle( self ) :
if( self.__particle is None ) :
pops = self.findAttributeInAncestry( 'PoPs' )
try :
self.__particle = pops[self.id]
except :
baseName, anti, qualifier = miscPoPsModule.baseAntiQualifierFromID( self.id, qualifierAllowed = True )
name = baseName + anti
self.__particle = pops.chemicalElements.getSymbol( name )
return( self.__particle )
@property
def label( self ) :
"""Returns self's label."""
return( self.__label )
@label.setter
def label( self, value ) :
if( not( isinstance( value, str ) ) ) : raise TypeError( 'label must be a string' )
self.__label = value
@property
def multiplicity( self ) :
return( self.__multiplicity )
@property
def energyDeposition( self ) :
return( self.__energyDeposition )
@property
def momentumDeposition( self ) :
return( self.__momentumDeposition )
@property
def distribution( self ) :
return( self.__distribution )
[docs] def addOutputChannel( self, outputChannel ) :
"""Adds outputChannel to particle."""
if( isinstance( outputChannel, channelsModule.channel ) ) :
self.outputChannel = outputChannel
self.outputChannel.setAncestor( self )
else :
raise TypeError( 'Invalid decay channel = %s' % brb.getType( outputChannel ) )
[docs] def addAttribute( self, name, value ) :
"""Add name and value to attribute list."""
self.attributes[name] = value
[docs] def convertUnits( self, unitMap ) :
"See documentation for reactionSuite.convertUnits."
self.multiplicity.convertUnits( unitMap )
self.energyDeposition.convertUnits( unitMap )
self.momentumDeposition.convertUnits( unitMap )
self.distribution.convertUnits( unitMap )
if( self.outputChannel is not None ) : self.outputChannel.convertUnits( unitMap )
[docs] def checkProductFrame( self ) :
"""
Calls checkProductFrame for self's distributions and if present for its outputChannel.
"""
self.distribution.checkProductFrame( )
if( self.outputChannel is not None ) : self.outputChannel.checkProductFrame( )
@property
def domainMin( self ) :
return( self.multiplicity.domainMin )
@property
def domainMax( self ) :
return( self.multiplicity.domainMax )
@property
def domainUnit( self ) :
return( self.multiplicity.domainUnit )
[docs] def thresholdQAs( self, unit, final = True ) :
if( self.outputChannel is not None ) : return( self.outputChannel.thresholdQAs( unit, final = final ) )
return( 0. )
[docs] def getLevelAsFloat( self, unit, default = 0. ) :
if( hasattr( self.particle, 'getLevelAsFloat' ) ) : return( self.particle.getLevelAsFloat( unit, default = default ) )
return( default )
[docs] def getMass( self, unit ) :
"""Returns the mass of the particle if possible, otherwise None is returned."""
particle = self.particle
PoPs = self.findAttributeInAncestry('PoPs')
if particle.id in PoPs.aliases:
particle = PoPs[particle.pid]
return( particle.getMass( unit ) )
[docs] def getAttribute( self, name ) :
"""Returns value for attribute name if it exists; otherwise, returns None."""
if( name in self.attributes ) : return( self.attributes[name] )
return( None )
[docs] def calculateAverageProductData( self, style, indent, **kwargs ) :
verbosity = kwargs['verbosity']
indent2 = indent + kwargs['incrementalIndent']
indent3 = indent2 + kwargs['incrementalIndent']
reactionSuite = kwargs['reactionSuite']
energyUnit = kwargs['incidentEnergyUnit']
momentumDepositionUnit = kwargs['momentumDepositionUnit']
massUnit = kwargs['massUnit']
energyAccuracy = kwargs['energyAccuracy']
momentumAccuracy = kwargs['momentumAccuracy']
kwargs['product'] = self
try :
kwargs['productMass'] = reactionSuite.PoPs[self.id].getMass( massUnit )
except : # Can happend when mass is not needed for evaluation and hence not stored.
kwargs['productMass'] = None
if( verbosity > 1 ) :
print '%s%s: label = %s: calculating average product data' % ( indent, self.id, self.label )
if( len( self.distribution ) > 0 ) :
multiplicity = style.findFormMatchingDerivedStyle( self.multiplicity )
if( not( isinstance( multiplicity, multiplicityModule.branching1d ) ) ) :
kwargs['multiplicity'] = multiplicity.toPointwise_withLinearXYs( accuracy = 1e-5, upperEps = 1e-8 )
energyData, momentumData = self.distribution.calculateAverageProductData( style, indent = indent2, **kwargs )
if( energyData is not None ) :
axes = energyDepositionModule.defaultAxes( energyUnit = energyUnit )
if( len( energyData ) == 1 ) :
averageEnergy = energyDepositionModule.XYs1d( data = energyData[0], axes = axes, label = style.label )
else :
averageEnergy = energyDepositionModule.regions1d( axes = axes, label = style.label )
for energyDataRegion in energyData :
averageEnergyRegion = energyDepositionModule.XYs1d( data = energyDataRegion, axes = axes )
averageEnergy.append( averageEnergyRegion )
self.energyDeposition.add( averageEnergy )
if( momentumData is not None ) :
axes = momentumDepositionModule.defaultAxes( energyUnit = energyUnit, momentumDepositionUnit = momentumDepositionUnit )
if( len( momentumData ) == 1 ) :
averageMomentum = momentumDepositionModule.XYs1d( data = momentumData[0], axes = axes, label = style.label )
else :
averageMomentum = momentumDepositionModule.regions1d( axes = axes, label = style.label )
for momentumDataRegion in momentumData :
averageMomentumRegion = momentumDepositionModule.XYs1d( data = momentumDataRegion, axes = axes )
averageMomentum.append( averageMomentumRegion )
self.momentumDeposition.add( averageMomentum )
if( self.outputChannel is not None ) :
self.outputChannel.calculateAverageProductData( style, indent = indent3, **kwargs )
[docs] def processMC_cdf( self, style, tempInfo, indent = '', incrementalIndent = ' ' ) :
indent2 = indent + tempInfo['incrementalIndent']
verbosity = tempInfo['verbosity']
if( verbosity > 1 ) : print '%s%s: label = %s: MonteCarlo_cdf processing' % ( indent, self.id, self.label )
self.distribution.processMC_cdf( style, tempInfo, indent )
if( self.outputChannel is not None ) : self.outputChannel.processMC_cdf( style, tempInfo, indent2 )
[docs] def processMultiGroup( self, style, tempInfo, indent ) :
indent2 = indent + tempInfo['incrementalIndent']
verbosity = tempInfo['verbosity']
tempInfo['workFile'].append( self.label )
doIt = not( isinstance( self.distribution[0], unspecifiedModule.form ) )
if( doIt and ( self.id in style.transportables ) ) :
if( verbosity > 1 ) :
print '%s%s: label = %s: multiGroup processing' % ( indent, self.id, self.label )
productMass = tempInfo['masses']['Product'] # Save to restore later
tempInfo['masses']['Product'] = self.getMass( tempInfo['massUnit'] )
tempInfo['product'] = self
tempInfo['multiplicity'] = self.multiplicity
self.multiplicity.processMultiGroup( style, tempInfo, indent )
self.energyDeposition.processMultiGroup( style, tempInfo, indent )
self.momentumDeposition.processMultiGroup( style, tempInfo, indent )
try :
self.distribution.processMultiGroup( style, tempInfo, indent )
except :
if( tempInfo['logFile'] is None ) :
raise
else :
import traceback
tempInfo['logFile'].write( '\n' + self.toXLink() + ':\n' + traceback.format_exc( ) + '\n' )
tempInfo['failures'] += 1
tempInfo['masses']['Product'] = productMass
if( self.outputChannel is not None ) : self.outputChannel.processMultiGroup( style, tempInfo, indent2 )
del tempInfo['workFile'][-1]
[docs] def check( self, info ):
""" check product multiplicity, distribution and breakup products (if applicable) """
from fudge.gnds import warning
warnings = []
multWarnings = self.multiplicity.check( info )
if multWarnings:
warnings.append( warning.context("Multiplicity:", multWarnings) )
if( ( self.label in info['transportables'] ) and ( not self.distribution.hasData( ) ) ) :
warnings.append( warning.missingDistribution( self.label, self ) )
distributionWarnings = self.distribution.check( info )
if distributionWarnings:
warnings.append( warning.context("Distribution:", distributionWarnings) )
if self.outputChannel is not None:
parentIsTwoBody = info['isTwoBody']
info['isTwoBody'] = isinstance( self.outputChannel, channelsModule.twoBodyOutputChannel )
for decayProduct in self.outputChannel:
decayWarnings = decayProduct.check( info )
if decayWarnings:
warnings.append( warning.context("Decay product: %s" % decayProduct.label, decayWarnings) )
info['isTwoBody'] = parentIsTwoBody # reset to parent channel
return warnings
[docs] def toXMLList( self, indent = '', **kwargs ) :
indent2 = indent + kwargs.get( 'incrementalIndent', ' ' )
attributeString = ''
for q in sorted(self.attributes) :
if( q in [ 'discrete', 'decayRate', 'primary' ] ) :
attributeString += ' %s="%s"' % ( q, self.attributes[q].toString( keepPeriod = False ) )
else :
attributeString += ' %s="%s"' % ( q, self.attributes[q] )
xmlString = [ '%s<%s pid="%s" label="%s"%s>' % ( indent, self.moniker, self.id, self.label, attributeString ) ]
xmlString += self.multiplicity.toXMLList( indent2, **kwargs )
xmlString += self.distribution.toXMLList( indent2, **kwargs )
xmlString += self.energyDeposition.toXMLList( indent2, **kwargs )
xmlString += self.momentumDeposition.toXMLList( indent2, **kwargs )
if( self.outputChannel is not None ) :
xmlString += self.outputChannel.toXMLList( indent2, **kwargs )
xmlString[-1] += '</%s>' % self.moniker
return( xmlString )
[docs] def toString( self, simpleString = False, exposeGammaMultiplicity = False ) :
"""
Returns a string representation of self. If simpleString is True, the string contains only the final
particles, and not any intermediate particles.
"""
def multiplicityString( ) :
multiplicity = ''
if( ( self.id != IDsPoPsModule.photon ) or exposeGammaMultiplicity ) :
_multiplicity = self.multiplicity.evaluated
if( isinstance( _multiplicity, multiplicityModule.constant1d ) ) :
iValue = int( _multiplicity.constant )
if( iValue == _multiplicity.constant ) :
if( iValue != 1 ) : multiplicity = '%s' % iValue
else :
multiplicity = '%s ' % multiplicity.constant
else :
multiplicity = 'm(E)*'
return( multiplicity )
multiplicity = multiplicityString( )
if( simpleString == True ) :
productName = '%s%s' % ( multiplicity, self.id )
else :
productName = self.id
qs = ''
c = '['
for q in self.attributes :
v = self.attributes[q]
qs += "%s%s:'%s'" % ( c, q, v )
c = ', '
if( len( qs ) > 0 ) : qs += ']'
productName = '%s%s%s' % ( multiplicity, productName, qs )
if( self.outputChannel is not None ) : productName = '(%s -> %s)' % ( productName, self.outputChannel )
return( productName )
[docs] @staticmethod
def parseXMLNode( productElement, xPath, linkData ):
"""Translate a <product> element from xml."""
def parseChildNode( moniker, parser ) :
child = productElement.find( moniker )
if( child is not None ) : parser( child, xPath, linkData )
xPath.append( '%s[@label="%s"]' % ( productElement.tag, productElement.get( 'label' ) ) )
attrs = dict( productElement.items( ) )
prod = product( id = attrs.pop( 'pid' ), label = attrs.pop( 'label' ) )
parseChildNode( multiplicityModule.component.moniker, prod.multiplicity.parseXMLNode )
parseChildNode( distributionModule.component.moniker, prod.distribution.parseXMLNode )
outputChannel = productElement.find( channelsModule.outputChannelToken )
if( outputChannel is not None ) :
prod.addOutputChannel( channelsModule.parseXMLNode( outputChannel, xPath, linkData ) )
for attr in ( 'decayRate', 'primary', 'discrete' ) :
if( attr in attrs ) :
attrs[attr] = PQUModule.PQU( attrs[attr] )
prod.attributes = attrs
parseChildNode( energyDepositionModule.component.moniker, prod.energyDeposition.parseXMLNode )
parseChildNode( momentumDepositionModule.component.moniker, prod.momentumDeposition.parseXMLNode )
xPath.pop( )
return( prod )
[docs]class products( suitesModule.suite ) :
moniker = 'products'
def __init__( self ) :
suitesModule.suite.__init__( self, ( product, ) )
[docs] def uniqueLabel( self, product ) :
"""
If product's label is the same as another product's label in self, construct a new unique label
based on product's name appended with '__' and one or more lower case letters (i.e., 'a' to 'z').
"""
if( product.label is None ) : product.label = product.id
return( suitesModule.suite.uniqueLabel( self, product ) )