Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read and Write GDML colour from/to BDSIM and generally. #228

Merged
merged 19 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pyg4ometry/fluka/Writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Writer:
Class to write FLUKA input files from a fluka registry object.

>>> f = Writer()
>>> f.addDetectro(flukaRegObject)
>>> f.addDetector(flukaRegObject)
>>> f.write("model.inp")
"""

Expand Down
29 changes: 26 additions & 3 deletions src/pyg4ometry/gdml/Reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from . import Defines as _defines
import logging as _log
from .. import geant4 as _g4
from ..visualisation import VisualisationOptions as _VisOptions
import os as _os


Expand All @@ -23,6 +24,8 @@ class Reader:
:type registryOn: bool
:param reduceNISTMaterialsToPredefined: change NIST-named materials to predefined ones
:type reduceNISTMaterialsToPredefined: bool
:param makeAllVisible: loaded volumes with aux info to make them invisible will be ignored and made visible
:type makeAllVisible: bool

When loading a GDML file that was exported by Geant4, the NIST materials may be
fully expanded to include their full element / isotope composition. With the
Expand All @@ -32,12 +35,19 @@ class Reader:
"""

def __init__(
self, fileName, registryOn=True, skipMaterials=False, reduceNISTMaterialsToPredefined=False
self,
fileName,
registryOn=True,
skipMaterials=False,
reduceNISTMaterialsToPredefined=False,
makeAllVisible=False,
):
super().__init__()
self.filename = fileName
self.registryOn = registryOn
self._reduceNISTMaterialsToPredefined = reduceNISTMaterialsToPredefined
self._makeAllVisible = makeAllVisible
self._forcedVisibleOptions = _VisOptions(alpha=0.1)
self._skipMaterials = skipMaterials

if self.registryOn:
Expand Down Expand Up @@ -1862,11 +1872,16 @@ def extractStructureNodeData(self, node, materialSubstitutionNames=None):
mat = _g4.MaterialArbitrary(material)

aux_list = []
visOptions = None
try:
for aux_node in node.childNodes:
try:
if aux_node.tagName == "auxiliary":
aux = self._parseAuxiliary(aux_node, register=False)
if aux.auxtype == "bds_vrgba":
visOptions = _BDSIM_VRGBA(aux.auxvalue)
if not visOptions.visible and self._makeAllVisible:
visOptions = self._forcedVisibleOptions
aux_list.append(aux)
except AttributeError:
pass # probably a comment
Expand All @@ -1880,10 +1895,10 @@ def extractStructureNodeData(self, node, materialSubstitutionNames=None):
registry=self._registry,
auxiliary=aux_list,
)
if visOptions:
vol.visOptions = visOptions
self.parsePhysicalVolumeChildren(node, vol)

# vol.checkOverlaps()

elif node_name == "assembly":
name = node.attributes["name"].value
vol = _g4.AssemblyVolume(name, self._registry, True)
Expand Down Expand Up @@ -2940,3 +2955,11 @@ def _StripPointer(name):
pattern = r"(0x\w{7,})"
rNameToObject = _re.sub(pattern, "", name)
return rNameToObject


def _BDSIM_VRGBA(s):
sl = s.split()
visible = bool(int(sl[0]))
rgb = list(map(float, sl[1:4]))
a = float(sl[4])
return _VisOptions(colour=rgb, alpha=a, visible=visible)
28 changes: 25 additions & 3 deletions src/pyg4ometry/gdml/Writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@


class Writer:
def __init__(self, prepend=""):
"""
:param writeColour: whether to write the VisOptions of each LogicalVolume.
:type writeColour: bool

"""

def __init__(self, prepend="", writeColour=True):
super().__init__()
self.prepend = prepend
self._writeColour = writeColour

self.imp = getDOMImplementation()
self.doc = self.imp.createDocument(None, "gdml", None)
Expand Down Expand Up @@ -372,9 +379,19 @@ def writeLogicalVolume(self, lv):
sr.setAttribute("ref", f"{self.prepend}{lv.solid.name}")
we.appendChild(sr)

aux = []
# lv.auxiliary could be None, Auxiliary or list(Auxiliary) or tuple(Auxiliary)
# ensure it's a list, even if empty
if lv.auxiliary:
for aux in lv.auxiliary:
self.writeAuxiliary(aux, parent=we)
if type(lv.auxiliary) in (list, tuple):
aux = list(lv.auxiliary) # ensure it's of type list
else:
aux = [lv.auxiliary]
if self._writeColour and lv.visOptions:
cAux = VisOptionsToAuxiliary(lv.visOptions)
aux.append(cAux)
for aux in lv.auxiliary:
self.writeAuxiliary(aux, parent=we)

for dv in lv.daughterVolumes:
if dv.type == "placement":
Expand Down Expand Up @@ -1373,3 +1390,8 @@ def writeScaled(self, instance):
oe.appendChild(scl)

self.solids.appendChild(oe)


def VisOptionsToAuxiliary(visOptions):
result = _Defines.Auxiliary("bds_vrgba", visOptions.getBDSIMVRGBA())
return result
13 changes: 8 additions & 5 deletions src/pyg4ometry/geant4/LogicalVolume.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class LogicalVolume:
:param addRegistry:
:type addRegistry: bool

Acceptable kwargs: "auxiliary", "visOptions".

"""

def __init__(self, solid, material, name, registry=None, addRegistry=True, **kwargs):
Expand Down Expand Up @@ -106,7 +108,7 @@ def __init__(self, solid, material, name, registry=None, addRegistry=True, **kwa
self.registry = registry

# physical visualisation options
self.visOptions = _VisOptions()
self.visOptions = kwargs.get("visOptions", None)

# efficient overlap checking
self.overlapChecked = False
Expand Down Expand Up @@ -857,18 +859,19 @@ def makeSolidTessellated(self):

def addAuxiliaryInfo(self, auxiliary):
"""
Add auxilary information to logical volume
Add auxiliary information to logical volume
:param auxiliary: auxiliary information for the logical volume
:type auxiliary: tuple or list
:type auxiliary: pyg4ometry.gdml.Defines.Auxiliary, list(pyg4ometry.gdml.Defines.Auxiliary), tuple(pyg4ometry.gdml.Defines.Auxiliary)
"""
# if auxiliary is not None and not isinstance(auxiliary, _Auxiliary):
# raise ValueError("Auxiliary information must be a gdml.Defines.Auxiliary instance.")
if auxiliary == None:
return
if isinstance(auxiliary, list) or isinstance(auxiliary, tuple):
for aux in auxiliary:
self.addAuxiliaryInfo(aux)
else:
if auxiliary:
self.auxiliary.append(auxiliary)
self.auxiliary.append(auxiliary)

def extent(self, includeBoundingSolid=False):
"""
Expand Down
5 changes: 3 additions & 2 deletions src/pyg4ometry/visualisation/UsdViewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ def visOptions2MaterialPrim(stage, visOptions, materialPrim):


class UsdViewer(_ViewerHierarchyBase):

def __init__(self, filePath="./test.usd"):
super().__init__()
if Usd is None:
msg = "Failed to import open usd"
raise RuntimeError(msg)
Expand Down Expand Up @@ -119,7 +119,8 @@ def traverseHierarchy(self, volume=None, motherPrim=None, scale=1000.00):
self.stage, self.materialRootPath + "/" + volume.name + "_mat"
)

visOptions2MaterialPrim(self.stage, volume.visOptions, materialPrim)
vo = self.getVisOptionsLV(volume)
visOptions2MaterialPrim(self.stage, vo, materialPrim)
UsdShade.MaterialBindingAPI(meshPrim).Bind(materialPrim)

# loop over all daughters
Expand Down
70 changes: 45 additions & 25 deletions src/pyg4ometry/visualisation/ViewerBase.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64 as _base64
import copy as _copy
import numpy as _np
import random as _random
from .. import pycgal as _pycgal
Expand Down Expand Up @@ -47,7 +48,7 @@ def __init__(self):
self.bSubtractDaughters = False

# default vis options
self.defaultVisOptions = None
self.defaultVisOptions = _VisOptions()
self.defaultOverlapVisOptions = None
self.defaultCoplanarVisOptions = None
self.defaultProtusionVisOptions = None
Expand All @@ -66,6 +67,40 @@ def clear(self):
self.instanceVisOptions = {} # instance vis options
self.instancePbrOptions = {} # instance pbr options

def _getMaterialVis(self, materialName):
materialVis = None
# a dict evaluates to True if not empty
if self.materialVisOptions:
# if 0x is in name, strip the appended pointer (common in exported GDML)
if "0x" in materialName:
materialName = materialName[0 : materialName.find("0x")]
# get with default
materialVis = v = self.materialVisOptions.get(materialName, self.defaultVisOptions)
return materialVis

def getVisOptions(self, pv):
"""
Return a set of vis options according to the precedence of pv, lv, material, default.
"""
materialVis = self._getMaterialVis(pv.logicalVolume.material.name)
# take the first non-None set of visOptions
orderOfPrecedence = [
pv.visOptions,
pv.logicalVolume.visOptions,
materialVis,
self.defaultVisOptions,
]
return next(item for item in orderOfPrecedence if item is not None)

def getVisOptionsLV(self, lv):
"""
Return a set of vis options according to the precedence of pv, lv, material, default.
"""
materialVis = self._getMaterialVis(lv.material.name)
# take the first non-None set of visOptions
orderOfPrecedence = [lv.visOptions, materialVis, self.defaultVisOptions]
return next(item for item in orderOfPrecedence if item is not None)

def setSubtractDaughters(self, subtractDaughters=True):
self.bSubtractDaughters = subtractDaughters

Expand Down Expand Up @@ -111,20 +146,8 @@ def addLogicalVolume(
name = "world"
self.addInstance(lv.name, mtra, tra, name)

materialName = lv.material.name

pointerLoc = materialName.find("0x")
if pointerLoc != -1:
materialName = materialName[0 : materialName.find("0x")]

# add vis options
if materialName in self.materialVisOptions:
visOptions = self.materialVisOptions[materialName]
visOptions.depth = depth
self.addVisOptions(lv.name, visOptions)
else:
visOptions.depth = depth
self.addVisOptions(lv.name, visOptions)
vo = self.getVisOptionsLV(lv)
self.addVisOptions(lv.name, vo)

# add overlap meshes
for [overlapmesh, overlaptype], i in zip(
Expand All @@ -145,6 +168,7 @@ def addLogicalVolume(
print("Unknown logical volume type or null mesh")

for pv in lv.daughterVolumes:
vo = self.getVisOptions(pv)
if pv.type == "placement":
# pv transform
pvmrot = _np.linalg.inv(_transformation.tbxyz2matrix(pv.rotation.eval()))
Expand All @@ -159,12 +183,6 @@ def addLogicalVolume(
mtra_new = mtra @ pvmrot @ pvmsca
tra_new = mtra @ pvtra + tra

if not pv.visOptions:
vo = pv.logicalVolume.visOptions
else:
vo = pv.visOptions

# pv.visOptions.colour = [_random.random(), _random.random(), _random.random()]
self.addLogicalVolume(
pv.logicalVolume,
mtra_new,
Expand All @@ -183,11 +201,12 @@ def addLogicalVolume(
new_mtra = mtra @ pvmrot
new_tra = mtra @ pvtra + tra

pv.visOptions.depth = depth + 2
vo2 = _copy.deepcopy(vo)
vo2.depth += 1

self.addMesh(pv.name, mesh.localmesh)
self.addInstance(pv.name, new_mtra, new_tra, pv.name)
self.addVisOptions(pv.name, pv.visOptions)
self.addVisOptions(pv.name, vo2)
elif pv.type == "parametrised":
for mesh, trans, i in zip(pv.meshes, pv.transforms, range(0, len(pv.meshes), 1)):
pv_name = pv.name + "_param_" + str(i)
Expand All @@ -200,11 +219,12 @@ def addLogicalVolume(
new_mtra = mtra @ pvmrot
new_tra = mtra @ pvtra + tra

pv.visOptions.depth = depth + 2
vo2 = _copy.deepcopy(vo)
vo2.depth += 1

self.addMesh(pv_name, mesh.localmesh)
self.addInstance(pv_name, new_mtra, new_tra, pv_name)
self.addVisOptions(pv_name, pv.visOptions)
self.addVisOptions(pv_name, vo2)

def addFlukaRegions(self, fluka_registry, max_region=1000000, debugIO=False):
icount = 0
Expand Down
21 changes: 20 additions & 1 deletion src/pyg4ometry/visualisation/ViewerHierarchyBase.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from . import VisualisationOptions as _VisOptions


class ViewerHierarchyBase:
"""
Base class for all viewers and exporters. Handles unique meshes and their instances
"""

def __init__(self):
pass
self.defaultVisOptions = _VisOptions()

def addWorld(self, worldLV):
self.worldLV = worldLV
Expand All @@ -17,3 +20,19 @@ def traverseHierarchy(self, LV=None):

for daughter in LV.daughterVolumes:
self.traverseHierarchy(daughter.logicalVolume)

def getVisOptions(self, pv):
"""
Return a set of vis options according to the precedence of pv, lv, default.
"""
# take the first non-None set of visOptions
orderOfPrecedence = [pv.visOptions, pv.logicalVolume.visOptions, self.defaultVisOptions]
return next(item for item in orderOfPrecedence if item is not None)

def getVisOptionsLV(self, lv):
"""
Return a set of vis options according to the precedence of lv, default.
"""
# take the first non-None set of visOptions
orderOfPrecedence = [lv.visOptions, self.defaultVisOptions]
return next(item for item in orderOfPrecedence if item is not None)
4 changes: 4 additions & 0 deletions src/pyg4ometry/visualisation/VisualisationOptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ def __repr__(self):
f"vis={self.visible}, linewidth={self.lineWidth}, depth={self.depth}>"
)

def getBDSIMVRGBA(self):
c = self.colour
return f"{int(self.visible)} {c[0]} {c[1]} {c[2]} {self.alpha}"

def getColour(self):
"""
Return the colour and generate a random colour if flagged.
Expand Down
Loading
Loading