diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c075ad2..aee5eaa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,4 +16,3 @@ Release notes * Support scikit-package Level 5 standard (https://scikit-package.github.io/scikit-package/). * Port legacy Python 2 code to support Python 3. - diff --git a/docs/source/api/diffpy.srxplanargui.rst b/docs/source/api/diffpy.srxplanargui.rst new file mode 100644 index 0000000..7dab4cd --- /dev/null +++ b/docs/source/api/diffpy.srxplanargui.rst @@ -0,0 +1,30 @@ +:tocdepth: -1 + +|title| +======= + +.. |title| replace:: diffpy.srxplanargui package + +.. automodule:: diffpy.srxplanargui + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + diffpy.srxplanargui.example_package + +Submodules +---------- + +|module| +-------- + +.. |module| replace:: diffpy.srxplanargui.example_submodule module + +.. automodule:: diffpy.srxplanargui.example_submodule + :members: + :undoc-members: + :show-inheritance: diff --git a/news/add-planargui.rst b/news/add-planargui.rst new file mode 100644 index 0000000..cfe6c01 --- /dev/null +++ b/news/add-planargui.rst @@ -0,0 +1,23 @@ +**Added:** + +* Add the diffpy.srxplanargui package to this repository. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/doc.rst b/news/doc.rst new file mode 100644 index 0000000..7677ce5 --- /dev/null +++ b/news/doc.rst @@ -0,0 +1,24 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Change "diffpy.srxplanargui" from python 2 to python 3 architecture. +* Support ``scikit-package`` Level 5 standard (https://scikit-package.github.io/scikit-package/). + +**Security:** + +* diff --git a/pyproject.toml b/pyproject.toml index 7e597c0..c80026b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ namespaces = false # to disable scanning PEP 420 namespaces (true by default) [project.scripts] diffpy-srxconfutils = "diffpy.srxconfutils.srxconfutils_app:main" +diffpy-srxplanargui = "diffpy.srxplanargui.srxplanargui_app:main" [tool.setuptools.dynamic] dependencies = {file = ["requirements/pip.txt"]} diff --git a/requirements/conda.txt b/requirements/conda.txt index 73b057e..abf0e60 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -1,3 +1,8 @@ numpy traits traitsui +fabio +pyface +scipy +pyfai +matplotlib diff --git a/requirements/pip.txt b/requirements/pip.txt index 73b057e..c72b04d 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,3 +1,10 @@ numpy traits traitsui +chaco +pyface +scipy +fabio +pyfai +matplotlib +diffpy.srxplanar diff --git a/src/diffpy/srxplanargui/__init__.py b/src/diffpy/srxplanargui/__init__.py new file mode 100644 index 0000000..19cf2a0 --- /dev/null +++ b/src/diffpy/srxplanargui/__init__.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +############################################################################## +# +# (c) 2012-2025 The Trustees of Columbia University in the City of New York. +# All rights reserved. +# +# File coded by: Xiaohao Yang, Billinge Group members. +# +# See GitHub contributions for a more detailed list of contributors. +# https://github.com/diffpy/diffpy.srxplanargui/graphs/contributors +# +# See LICENSE.rst for license information. +# +############################################################################## +"""XPDFsuite, a software for PDF transformation and visualization.""" + +# package version +from diffpy.srxplanargui.version import __version__ # noqa + +# silence the pyflakes syntax checker +assert __version__ or True + +# End of file diff --git a/src/diffpy/srxplanargui/calibration.py b/src/diffpy/srxplanargui/calibration.py new file mode 100644 index 0000000..b2bca01 --- /dev/null +++ b/src/diffpy/srxplanargui/calibration.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanargui by Simon J. L. Billinge group +# (c) 2012-2025 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## + +import os +import re +import sys + +from pyface.api import ImageResource +from traits.api import ( + Bool, + DelegatesTo, + Directory, + Enum, + File, + Float, + HasTraits, + Instance, + Int, + Str, + on_trait_change, +) +from traits.etsconfig.api import ETSConfig +from traitsui.api import Group, Handler, HGroup, Item, VGroup, View +from traitsui.menu import CancelButton, OKButton + +from diffpy.srxconfutils.tools import module_exists_lower +from diffpy.srxplanar.selfcalibrate import selfCalibrate +from diffpy.srxplanar.srxplanar import SrXplanar +from diffpy.srxplanar.srxplanarconfig import checkMax +from diffpy.srxplanargui.srxconfig import SrXconfig + +ETSConfig.toolkit = "qt" + +if module_exists_lower("pyfai"): + import pyFAI + + missingpyFAI = False +else: + missingpyFAI = True + +# determine option name for calibrant used in pyFAI-calib +# The current option is "-c", but it was "-S" in 0.9.3. +pyFAIcalib_opt_calibrant = "-c" +if not missingpyFAI: + from pkg_resources import parse_version + + if parse_version(pyFAI.version) <= parse_version("0.9.3"): + pyFAIcalib_opt_calibrant = "-S" + del parse_version + + +class CalibrationHandler(Handler): + + def closed(self, info, is_ok): + """Notify main gui to delete current plot in plots list.""" + if is_ok: + info.object.calibration() + return True + + +class Calibration(HasTraits): + image = File + dspacefile = File + srx = Instance(SrXplanar) + srxconfig = Instance(SrXconfig) + pythonbin = File + pyFAIdir = Directory + caliscript = File + missingpyFAI = Bool(False) + + xpixelsize = DelegatesTo("srxconfig") + ypixelsize = DelegatesTo("srxconfig") + wavelength = DelegatesTo("srxconfig") + xbeamcenter = DelegatesTo("srxconfig") + ybeamcenter = DelegatesTo("srxconfig") + xdimension = DelegatesTo("srxconfig") + ydimension = DelegatesTo("srxconfig") + distance = DelegatesTo("srxconfig") + rotationd = DelegatesTo("srxconfig") + tiltd = DelegatesTo("srxconfig") + configmode = DelegatesTo("srxconfig") + xpixelsizetem = DelegatesTo("srxconfig") + ypixelsizetem = DelegatesTo("srxconfig") + + def __init__(self, *args, **kwargs): + super(Calibration, self).__init__(*args, **kwargs) + self.locatePyFAI() + self.missingpyFAI = missingpyFAI + return + + def locatePyFAI(self): + pythonbin = sys.executable + if sys.platform == "win32": + pyFAIdir = os.path.join(sys.exec_prefix, "Scripts") + elif sys.platform.startswith("linux"): + pyFAIdir = os.path.join(sys.exec_prefix, "bin") + else: + pyFAIdir = os.path.join(sys.exec_prefix, "bin") + self.pythonbin = pythonbin + self.pyFAIdir = pyFAIdir + return + + @on_trait_change("pyFAIdir") + def _pyFAIdirChanged(self): + if sys.platform == "win32": + caliscript = os.path.join(self.pyFAIdir, "pyFAI-calib.py") + intescript = os.path.join(self.pyFAIdir, "pyFAI-waxs.py") + elif sys.platform.startswith("linux"): + caliscript = os.path.join(self.pyFAIdir, "pyFAI-calib") + intescript = os.path.join(self.pyFAIdir, "pyFAI-waxs") + else: + caliscript = os.path.join(self.pyFAIdir, "pyFAI-calib") + intescript = os.path.join(self.pyFAIdir, "pyFAI-waxs") + self.caliscript = caliscript + self.intescript = intescript + return + + def callPyFAICalibration(self, image=None, dspacefile=None): + if image is None: + image = self.image + else: + self.image = image + if dspacefile is None: + dspacefile = self.dspacefile + else: + self.dspacefile = dspacefile + + flag = False + if os.path.exists(image) and os.path.isfile(image): + if os.path.exists(dspacefile) and os.path.isfile(dspacefile): + flag = True + + if flag: + image = os.path.abspath(image) + dspacefile = os.path.abspath(dspacefile) + + # remove .npt and .azim + for f in [ + os.path.splitext(image)[0] + ".npt", + os.path.splitext(image)[0] + ".azim", + ]: + if os.path.exists(f): + os.remove(f) + + ps = [self.xpixelsize * 1000, self.ypixelsize * 1000] + + calicmd = [self.pythonbin, self.caliscript] + calicmd.extend(["-w", str(self.wavelength)]) + calicmd.extend([pyFAIcalib_opt_calibrant, str(dspacefile)]) + calicmd.extend(["-p", str(ps[0]) + "," + str(ps[1])]) + calicmd.extend([str(image)]) + + import subprocess + + try: + os.environ.pop("QT_API") + except KeyError: + pass + subprocess.call(calicmd) + + # integrate image + ponifile = os.path.splitext(str(image))[0] + ".poni" + intecmd = [ + self.pythonbin, + self.intescript, + "-p", + ponifile, + str(image), + ] + subprocess.call(intecmd) + self.parsePyFAIoutput(image) + print("Calibration finished!") + return + + def parsePyFAIoutput(self, image=None): + if image is None: + image = self.image + + filename = os.path.splitext(image)[0] + ".xy" + if os.path.exists(filename): + f = open(filename, "r") + lines = f.readlines() + f.close() + else: + raise ValueError("pyFAI results file does not exist.") + for line in lines: + if re.search("# Distance Sample-beamCenter", line): + distance = findFloat(line)[0] + elif re.search("# Center", line): + x, y = findFloat(line) + elif re.search("# Tilt", line): + tiltd, rotationd = findFloat(line) + + self.distance = distance + self.xbeamcenter = x # - 0.5 + self.ybeamcenter = y # - y - 0.5 + self.tiltd = tiltd + self.rotationd = rotationd # + 180 + self.srxconfig.flipvertical = False + self.srxconfig.fliphorizontal = False + return + + def selfCalibration(self, image=None): + # self.addfiles.selected[0].fullname + if image is None: + image = self.image + + if os.path.exists(image) and os.path.isfile(image): + for mode, showresults in zip( + ["x", "y", "x", "y"], [False, False, False, True] + ): + selfCalibrate( + self.srx, + image, + mode=mode, + cropedges=self.slice, + showresults=showresults, + xywidth=self.xywidth, + ) + return + + slice = Enum(["auto", "x", "y", "box", "full"]) + calibrationmode = Enum(["self", "calibrant"]) + + def calibration(self, image=None, dspacefile=None): + if self.calibrationmode == "calibrant": + self.callPyFAICalibration(image, dspacefile) + elif self.calibrationmode == "self": + self.selfCalibration(image) + else: + raise ValueError("calibration mode error") + return + + xywidth = Int(6) + qmincali = Float(0.5) + qmaxcali = Float(10.0) + + @on_trait_change( + "srxconfig.[xpixelsize, ypixelsize, distance," + " wavelength, xdimension, ydimension]" + ) + def _qmaxChanged(self): + tthmax, qmax = checkMax(self.srxconfig) + self.qmincali = min(1.25, qmax / 10) + self.qmaxcali = qmax / 2 + return + + inst1 = Str( + "Please install pyFAI and FabIO to use" + " the calibration function (refer to help)." + ) + inst2 = Str( + "(http://github.com/kif/pyFAI," + " https://forge.epn-campus.eu/projects/azimuthal/files)" + ) + main_View = View( + # Item('calibrationmode', style='custom', label='Calibration mode'), + Item("image", label="Image file"), + Group( + Item("inst1", style="readonly"), + Item("inst2", style="readonly"), + visible_when='missingpyFAI and calibrationmode=="calibrant"', + show_border=True, + show_labels=False, + ), + Group( + Item("dspacefile", label="D-space file"), + Item("pyFAIdir", label="pyFAI dir."), + show_border=True, + visible_when='calibrationmode=="calibrant"', + enabled_when="not missingpyFAI", + label="Please specify the d-space file and" + + " the location of pyFAI executable", + ), + HGroup( + Item( + "xpixelsize", + label="Pixel size x (mm)", + visible_when='configmode == "normal"', + ), + Item( + "xpixelsizetem", + label="Pixel size x (A^-1)", + visible_when='configmode == "TEM"', + ), + Item( + "ypixelsize", + label="Pixel size y (mm)", + visible_when='configmode == "normal"', + ), + Item( + "ypixelsizetem", + label="Pixel size y (A^-1)", + visible_when='configmode == "TEM"', + ), + visible_when='calibrationmode=="calibrant"', + enabled_when="not missingpyFAI", + show_border=True, + label="Please specify the size of pixel", + ), + HGroup( + Item("wavelength", label="Wavelength (A)"), + visible_when='calibrationmode=="calibrant"', + enabled_when="not missingpyFAI", + show_border=True, + label="Please specify the wavelength", + ), + HGroup( + Item( + "wavelength", + visible_when='integrationspace == "qspace"', + label="Wavelength(Angstrom)", + ), + Item( + "distance", + label="Distance(mm)", + visible_when='configmode == "normal"', + ), + Item( + "distance", + label="Camera Length(mm)", + visible_when='configmode == "TEM"', + ), + label="Please specify the wavelength and" + + " distance between sample and detector:", + show_border=True, + visible_when='calibrationmode=="self"', + ), + HGroup( + VGroup( + Item("xbeamcenter", label="x beamcenter (pixel)"), + Item("rotationd", label="Rotation (degree)"), + ), + VGroup( + Item("ybeamcenter", label="y beamcenter (pixel)"), + Item("tiltd", label="Tilt rotation (degree)"), + ), + show_border=True, + label="Plasee specify the initial value of following parameters:", + visible_when='calibrationmode=="self"', + ), + HGroup( + VGroup( + Item("xdimension", label="x dimension (pixel)"), + Item( + "xpixelsize", + label="Pixel size x (mm)", + visible_when='configmode == "normal"', + ), + Item( + "xpixelsizetem", + label="Pixel size x (A^-1)", + visible_when='configmode == "TEM"', + ), + ), + VGroup( + Item("ydimension", label="y dimension (pixel)"), + Item( + "ypixelsize", + label="Pixel size y (mm)", + visible_when='configmode == "normal"', + ), + Item( + "ypixelsizetem", + label="Pixel size y (A^-1)", + visible_when='configmode == "TEM"', + ), + ), + show_border=True, + label="Please specify the dimension of detector" + + " and size of pixel:", + visible_when='calibrationmode=="self"', + ), + HGroup( + VGroup( + Item("xywidth", label="(x,y) center searching range, +/-"), + Item("slice", label="Refining using slab along"), + ), + VGroup( + Item("qmincali", label="Qmin in calibration"), + Item("qmaxcali", label="Qmax in calibration"), + ), + show_border=True, + label="Others", + visible_when='calibrationmode=="self"', + ), + title="Calibration", + width=600, + height=450, + resizable=True, + buttons=[OKButton, CancelButton], + handler=CalibrationHandler(), + icon=ImageResource("icon.png"), + ) + + +def findFloat(line): + pattern = r"[-+]?(?:\d+\.\d*|\.\d+|\d+)" + return [float(x) for x in re.findall(pattern, line)] + + +if __name__ == "__main__": + srxconfig = SrXconfig() + cali = Calibration(srxconfig=srxconfig) + # cali.callPyFAICalibration('ceo2.tif', 'ceo2.d') + # cali.parsePyFAIoutput() + cali.configure_traits(view="main_View") diff --git a/src/diffpy/srxplanargui/datacontainer.py b/src/diffpy/srxplanargui/datacontainer.py new file mode 100644 index 0000000..e64af8f --- /dev/null +++ b/src/diffpy/srxplanargui/datacontainer.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanargui by Simon J. L. Billinge group +# (c) 2012-2025 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## + +import os + +from traits.api import File, HasTraits, Property, property_depends_on + + +class DataContainer(HasTraits): + + # The full path and file name of the file: + fullname = File + # The base file name of the source file: + basename = Property # Str + + @property_depends_on("fullname") + def _get_basename(self): + return os.path.basename(self.fullname) + + +if __name__ == "__main__": + test = DataContainer() diff --git a/src/diffpy/srxplanargui/gitarchive.cfg b/src/diffpy/srxplanargui/gitarchive.cfg new file mode 100644 index 0000000..43b349f --- /dev/null +++ b/src/diffpy/srxplanargui/gitarchive.cfg @@ -0,0 +1,5 @@ +[DEFAULT] +commit = $Format:%H$ +date = $Format:%ai$ +timestamp = $Format:%at$ +refnames = $Format:%D$ diff --git a/src/diffpy/srxplanargui/help.py b/src/diffpy/srxplanargui/help.py new file mode 100644 index 0000000..076a0c0 --- /dev/null +++ b/src/diffpy/srxplanargui/help.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanargui by Simon J. L. Billinge group +# (c) 2012-2025 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## +"""Provide help for SrXgui.""" + +import sys + +from pyface.api import ImageResource +from traits.api import HasTraits, Int, Property, property_depends_on +from traits.etsconfig.api import ETSConfig +from traitsui.api import Action, Handler, ImageEditor, Item, View +from traitsui.menu import OKButton + + +class HelpHandler(Handler): + + def _qsnext(self, info): + info.object.qsindex += 1 + return + + def _qsprevious(self, info): + info.object.qsindex -= 1 + return + + def _cpReftext(self, info): + info.object.cpReftext() + return + + +class SrXguiHelp(HasTraits): + + if sys.platform.startswith("win"): + if ETSConfig.toolkit == "qt": + hheight = 510 + hwidth = 960 + else: + hheight = 556 + hwidth = 980 + else: + hheight = 524 + hwidth = 964 + + ####################### + # quick start + ####################### + + imgs = [ImageResource("%02d.png" % i) for i in range(1, 23)] + + qslen = Int(len(imgs) - 1) + + next_action = Action( + name="Next", + action="_qsnext", + enabled_when="object.qsindex 2: + mask = points_in_polygon(self.pts, points) + mask = mask.reshape(self.staticmask.shape) + if remove: + self.staticmask = np.logical_and( + self.staticmask, np.logical_not(mask) + ) + else: + self.staticmask = np.logical_or(self.staticmask, mask) + self.refreshMask(staticmask=self.staticmask) + return + + def addPointMask(self, ndx, remove=None): + """Param ndx -- (x,y) float.""" + x, y = ndx + r = self.pts - np.array((x, y)) + r = np.sum(r**2, axis=1) + mask = r < ((self.pointmaskradius + 1) ** 2) + mask = mask.reshape(self.staticmask.shape) + if remove: + self.staticmask = np.logical_and( + self.staticmask, np.logical_not(mask) + ) + else: + self.staticmask = np.logical_or(self.staticmask, mask) + self.refreshMask(self.staticmask) + return + + def clearMask(self): + self.staticmask = self.staticmask * 0 + self.refreshMask(self.staticmask) + return + + def invertMask(self): + self.staticmask = np.logical_not(self.staticmask) + self.refreshMask(self.staticmask) + return + + def refreshMask(self, staticmask=None, draw=True): + self.staticmask = ( + self.srx.mask.staticMask() if staticmask is None else staticmask + ) + self.dynamicmask = self.srx.mask.dynamicMask( + self.imageorg, dymask=self.staticmask + ) + self.dynamicmask = np.logical_or( + self.dynamicmask, self.srx.mask.edgeMask() + ) + self.mask = np.logical_or(self.staticmask, self.dynamicmask) + if draw: + self.refreshImage() + return + + maskaboveint = Int(10e10) + maskbelowint = Int(1) + + def maskabove(self): + mask = self.imageorg > self.maskaboveint + self.staticmask = np.logical_or(self.staticmask, mask) + self.refreshMask(self.staticmask) + return + + def maskbelow(self): + mask = self.imageorg < self.maskbelowint + self.staticmask = np.logical_or(self.staticmask, mask) + self.refreshMask(self.staticmask) + return + + def _appendTools(self): + """Append xy position, zoom, pan tools to plot. + + :param plot: the plot object to append on + """ + plot = self.plot + img_plot = self.img_plot + + # tools + self.pan = PanTool(plot) + self.zoom = ZoomTool(component=plot, tool_mode="box", always_on=False) + self.lstool = MasklineDrawer(self.plot, imageplot=self) + self.xyseltool = MaskPointInspector(img_plot, imageplot=self) + # self.lstool.imageplot = self + + img_plot.tools.append(self.xyseltool) + overlay = ImageInspectorOverlay( + component=img_plot, + image_inspector=self.xyseltool, + bgcolor="white", + border_visible=True, + ) + img_plot.overlays.append(overlay) + + plot.tools.append(self.pan) + plot.overlays.append(self.zoom) + return + + def _enableMaskEditing(self): + """Enable mask tool and disable pan tool.""" + self.maskediting = True + for i in range(self.plot.tools.count(self.pan)): + self.plot.tools.remove(self.pan) + self.plot.overlays.append(self.lstool) + self.titlebak = self.plot.title + self.plot.title = ( + "Click: add a vertex; " + "+Click: remove a vertex; \n " + ": finish the selection" + ) + return + + def _disableMaskEditing(self): + """Disable mask tool and enable pan tool.""" + self.plot.overlays.remove(self.lstool) + self.plot.tools.append(self.pan) + self.plot.title = self.titlebak + self.maskediting = False + return + + def _enablePointMaskEditing(self): + self.maskediting = True + for i in range(self.plot.tools.count(self.pan)): + self.plot.tools.remove(self.pan) + self.titlebak = self.plot.title + self.plot.title = ( + "Click: add a point; : exit the point selection" + ) + return + + def _disablePointMaskEditing(self): + self.plot.tools.append(self.pan) + self.plot.title = self.titlebak + self.maskediting = False + return + + def refreshImage(self, mask=None, draw=True): + """Recalculate the image using self.mask or mask and refresh + display.""" + mask = self.mask if mask is None else mask + image = self.applyScale() + image = image * np.logical_not(mask) + image.max() * mask + self.pd.set_data("imagedata", image) + if draw: + self.plot.invalidate_draw() + return + + scalemode = Enum("linear", ["linear", "log"], desc="Scale the image") + scalepowder = Float(0.5, desc="gamma value to control the contrast") + + def applyScale(self, image=None): + """Apply the scale to increase/decrease contrast.""" + if self.scalemode == "linear": + if image is None: + image = self.imageorg + intmax = self.imageorgmax + else: + image = image + intmax = image.max() + elif self.scalemode == "log": + if image is None: + image = self.imageorglog + intmax = self.imageorglogmax + else: + image = np.log(image) + image[image < 0] = 0 + intmax = image.max() + else: + image = image + intmax = image.max() + + image = intmax * ((image / intmax) ** self.scalepowder) + return image + + splb = Float(0.0) + spub = Float(1.0) + + def _scalemode_changed(self): + if self.scalemode == "linear": + self.scalepowder = 0.5 + self.splb = 0.0 + self.spub = 1.0 + elif self.scalemode == "log": + self.scalepowder = 1.0 + self.splb = 0.0 + self.spub = 4.0 + self.refreshImage() + return + + def _scalepowder_changed(self, old, new): + if np.round(old, 1) != np.round(new, 1): + self.refreshImage() + return + + def _add_notifications(self): + self.on_trait_change(self.refreshMaskFile, "srxconfig.maskfile") + return + + def _del_notifications(self): + self.on_trait_change( + self.refreshMaskFile, "srxconfig.maskfile", remove=True + ) + return + + addpolygon_bb = Button("Add polygon mask") + removepolygon_bb = Button("Remove polygon mask") + addpoint_bb = Button("Add point mask") + clearmask_bb = Button("Clear mask", desc="Clear mask") + invertmask_bb = Button("Invert mask", desc="Invert mask") + advancedmask_bb = Button( + "Dynamic mask", + desc="The dynamic mask is dynamically generated for each image.", + ) + maskabove_bb = Button("Mask intensity above") + maskbelow_bb = Button("Mask intensity below") + loadmaskfile_bb = Button("Load mask") + savemaskfile_bb = Button("Save mask") + + def _addpolygon_bb_fired(self): + self.removepolygonmask = False + self._enableMaskEditing() + return + + def _removepolygon_bb_fired(self): + self.removepolygonmask = True + self._enableMaskEditing() + return + + def _addpoint_bb_fired(self): + self._enablePointMaskEditing() + self.xyseltool.enablemaskselect = True + return + + def _clearmask_bb_fired(self): + self.clearMask() + return + + def _invertmask_bb_fired(self): + self.invertMask() + return + + def _advancedmask_bb_fired(self): + self.edit_traits("advancedmask_view") + # if not hasattr(self, 'advhint'): + # self.advhint = AdvHint() + # self.advhint.edit_traits('advhint_view') + return + + def _maskabove_bb_fired(self): + self.maskabove() + return + + def _maskbelow_bb_fired(self): + self.maskbelow() + return + + def _loadmaskfile_bb_fired(self): + self.edit_traits("loadmaskfile_view") + return + + def _savemaskfile_bb_fired(self): + if self.maskfile == "": + self.maskfile = os.path.join( + self.srxconfig.savedirectory, "mask.npy" + ) + else: + self.maskfile = os.path.splitext(self.maskfile)[0] + ".npy" + self.edit_traits("savemaskfile_view") + return + + def __init__(self, **kwargs): + """Init the object and create notification.""" + HasTraits.__init__(self, **kwargs) + self.createPlot() + # self._loadMaskPar() + self._add_notifications() + return + + hinttext = Str( + "Zoom: ; Reset: ;" + " Pan: ; Toggle XY coordinates:

" + ) + traits_view = View( + Group( + Item( + "plot", + editor=ComponentEditor(size=(550, 550)), + show_label=False, + ), + HGroup( + spring, + Item("scalemode", label="Scale mode"), + Item( + "scalepowder", + label="Gamma", + editor=RangeEditor( + auto_set=False, + low_name="splb", + high_name="spub", + format="%.1f", + ), + ), + spring, + ), + VGroup( + HGroup( + Item("addpolygon_bb", enabled_when="not maskediting"), + Item("removepolygon_bb", enabled_when="not maskediting"), + spring, + Item("maskabove_bb", enabled_when="not maskediting"), + Item("maskaboveint", enabled_when="not maskediting"), + show_labels=False, + ), + HGroup( + Item("addpoint_bb", enabled_when="not maskediting"), + Item("pointmaskradius", label="Size:", show_label=True), + spring, + Item("maskbelow_bb", enabled_when="not maskediting"), + Item("maskbelowint", enabled_when="not maskediting"), + show_labels=False, + ), + HGroup( + Item("clearmask_bb", enabled_when="not maskediting"), + Item("invertmask_bb", enabled_when="not maskediting"), + Item("advancedmask_bb", enabled_when="not maskediting"), + spring, + Item("loadmaskfile_bb"), + Item("savemaskfile_bb"), + show_labels=False, + ), + show_labels=False, + show_border=True, + label="Mask", + ), + orientation="vertical", + ), + resizable=True, + title="2D image", + statusbar=["hinttext"], + width=600, + height=700, + icon=ImageResource("icon.png"), + ) + + savemaskfile_action = Action(name="OK ", action="_save") + loadmaskfile_action = Action(name="OK ", action="_load") + applydymask_action = Action(name="Apply ", action="_applyDymask") + + savemaskfile_view = View( + Item("maskfile"), + buttons=[savemaskfile_action, CancelButton], + title="Save mask file", + width=500, + resizable=True, + handler=SaveLoadMaskHandler(), + icon=ImageResource("icon.png"), + ) + + loadmaskfile_view = View( + Item("maskfile"), + buttons=[loadmaskfile_action, CancelButton], + title="Load mask file", + width=500, + resizable=True, + handler=SaveLoadMaskHandler(), + icon=ImageResource("icon.png"), + ) + + advancedmask_view = View( + Group( + VGroup( + Item( + "cropedges", + label="Mask edges", + editor=ArrayEditor(width=-50), + ), + label="Edge mask", + show_border=True, + ), + VGroup( + Item("darkpixelmask", label="Enable"), + Item( + "darkpixelr", + label="Threshold", + enabled_when="darkpixelmask", + ), + label="Dark pixel mask", + show_border=True, + ), + VGroup( + Item("brightpixelmask", label="Enable"), + Item( + "brightpixelsize", + label="Testing size", + enabled_when="brightpixelmask", + ), + Item( + "brightpixelr", + label="Threshold", + enabled_when="brightpixelmask", + ), + label="Bright pixel mask", + show_border=True, + ), + VGroup( + Item("avgmask", label="Enable"), + Item("avgmaskhigh", label="High", enabled_when="avgmask"), + Item("avgmasklow", label="Low", enabled_when="avgmask"), + label="Average mask", + show_border=True, + ), + ), + title="Dynamic mask", + width=320, + handler=AdvMaskHandler(), + resizable=True, + buttons=[applydymask_action, OKButton, CancelButton], + icon=ImageResource("icon.png"), + ) + + +class MasklineDrawer(LineSegmentTool): + """""" + + imageplot = Any + + def _finalize_selection(self): + self.imageplot._disableMaskEditing() + self.imageplot.mergeMask(self.points) + return + + def __init__(self, *args, **kwargs): + LineSegmentTool.__init__(self, *args, **kwargs) + self.line.line_color = "red" + self.line.vertex_color = "white" + return + + +class MaskPointInspector(ImageInspectorTool): + exitmask_key = KeySpec("Enter") + imageplot = Any + enablemaskselect = Bool(False) + + def normal_key_pressed(self, event): + if self.inspector_key.match(event): + self.visible = not self.visible + event.handled = True + if self.exitmask_key.match(event): + self.enablemaskselect = False + self.imageplot._disablePointMaskEditing() + return + + def normal_left_down(self, event): + if self.enablemaskselect: + ndx = self.component.map_index((event.x, event.y)) + self.imageplot.addPointMask(ndx) + return + + +class AdvHint(HasTraits): + advhinttext = str( + """Notes: Advanced Masks are generated during the integration + and refreshed for each image. + You can preview the masks here or apply the current masks + to the static mask permanently. + +Edge mask: mask the pixels around the image edge. +(left, right, top, bottom) + +Dark pixel mask: mask the pixels too dark +compared to their local environment + +Bright pixel mask: mask the pixels too bright +compared to their local environment +Average mask: Mask the pixels too bright or too dark +compared to the average intensity at the similar diffraction angle. +Correct calibration information is required.""" + ) + + advhint_view = View( + Group( + Item("advhinttext", style="readonly", show_label=False), + show_border=True, + ), + title="Advanced mask hints", + width=640, + resizable=False, + buttons=[OKButton], + icon=ImageResource("icon.png"), + ) diff --git a/src/diffpy/srxplanargui/images/01.png b/src/diffpy/srxplanargui/images/01.png new file mode 100644 index 0000000..38a360b Binary files /dev/null and b/src/diffpy/srxplanargui/images/01.png differ diff --git a/src/diffpy/srxplanargui/images/02.png b/src/diffpy/srxplanargui/images/02.png new file mode 100644 index 0000000..8c39885 Binary files /dev/null and b/src/diffpy/srxplanargui/images/02.png differ diff --git a/src/diffpy/srxplanargui/images/03.png b/src/diffpy/srxplanargui/images/03.png new file mode 100644 index 0000000..d23cc9d Binary files /dev/null and b/src/diffpy/srxplanargui/images/03.png differ diff --git a/src/diffpy/srxplanargui/images/04.png b/src/diffpy/srxplanargui/images/04.png new file mode 100644 index 0000000..0d84cbc Binary files /dev/null and b/src/diffpy/srxplanargui/images/04.png differ diff --git a/src/diffpy/srxplanargui/images/05.png b/src/diffpy/srxplanargui/images/05.png new file mode 100644 index 0000000..0874af2 Binary files /dev/null and b/src/diffpy/srxplanargui/images/05.png differ diff --git a/src/diffpy/srxplanargui/images/06.png b/src/diffpy/srxplanargui/images/06.png new file mode 100644 index 0000000..b465f08 Binary files /dev/null and b/src/diffpy/srxplanargui/images/06.png differ diff --git a/src/diffpy/srxplanargui/images/07.png b/src/diffpy/srxplanargui/images/07.png new file mode 100644 index 0000000..aafa077 Binary files /dev/null and b/src/diffpy/srxplanargui/images/07.png differ diff --git a/src/diffpy/srxplanargui/images/08.png b/src/diffpy/srxplanargui/images/08.png new file mode 100644 index 0000000..5f5363f Binary files /dev/null and b/src/diffpy/srxplanargui/images/08.png differ diff --git a/src/diffpy/srxplanargui/images/09.png b/src/diffpy/srxplanargui/images/09.png new file mode 100644 index 0000000..57954bc Binary files /dev/null and b/src/diffpy/srxplanargui/images/09.png differ diff --git a/src/diffpy/srxplanargui/images/10.png b/src/diffpy/srxplanargui/images/10.png new file mode 100644 index 0000000..c51c8bc Binary files /dev/null and b/src/diffpy/srxplanargui/images/10.png differ diff --git a/src/diffpy/srxplanargui/images/11.png b/src/diffpy/srxplanargui/images/11.png new file mode 100644 index 0000000..94742b8 Binary files /dev/null and b/src/diffpy/srxplanargui/images/11.png differ diff --git a/src/diffpy/srxplanargui/images/12.png b/src/diffpy/srxplanargui/images/12.png new file mode 100644 index 0000000..2b5ab13 Binary files /dev/null and b/src/diffpy/srxplanargui/images/12.png differ diff --git a/src/diffpy/srxplanargui/images/13.png b/src/diffpy/srxplanargui/images/13.png new file mode 100644 index 0000000..b578e7c Binary files /dev/null and b/src/diffpy/srxplanargui/images/13.png differ diff --git a/src/diffpy/srxplanargui/images/14.png b/src/diffpy/srxplanargui/images/14.png new file mode 100644 index 0000000..c518ff9 Binary files /dev/null and b/src/diffpy/srxplanargui/images/14.png differ diff --git a/src/diffpy/srxplanargui/images/15.png b/src/diffpy/srxplanargui/images/15.png new file mode 100644 index 0000000..7a13b9a Binary files /dev/null and b/src/diffpy/srxplanargui/images/15.png differ diff --git a/src/diffpy/srxplanargui/images/16.png b/src/diffpy/srxplanargui/images/16.png new file mode 100644 index 0000000..c8cf22e Binary files /dev/null and b/src/diffpy/srxplanargui/images/16.png differ diff --git a/src/diffpy/srxplanargui/images/17.png b/src/diffpy/srxplanargui/images/17.png new file mode 100644 index 0000000..8f8a47a Binary files /dev/null and b/src/diffpy/srxplanargui/images/17.png differ diff --git a/src/diffpy/srxplanargui/images/18.png b/src/diffpy/srxplanargui/images/18.png new file mode 100644 index 0000000..1da1629 Binary files /dev/null and b/src/diffpy/srxplanargui/images/18.png differ diff --git a/src/diffpy/srxplanargui/images/19.png b/src/diffpy/srxplanargui/images/19.png new file mode 100644 index 0000000..24fc4c4 Binary files /dev/null and b/src/diffpy/srxplanargui/images/19.png differ diff --git a/src/diffpy/srxplanargui/images/20.png b/src/diffpy/srxplanargui/images/20.png new file mode 100644 index 0000000..97409e4 Binary files /dev/null and b/src/diffpy/srxplanargui/images/20.png differ diff --git a/src/diffpy/srxplanargui/images/21.png b/src/diffpy/srxplanargui/images/21.png new file mode 100644 index 0000000..7b4daac Binary files /dev/null and b/src/diffpy/srxplanargui/images/21.png differ diff --git a/src/diffpy/srxplanargui/images/22.png b/src/diffpy/srxplanargui/images/22.png new file mode 100644 index 0000000..78704c1 Binary files /dev/null and b/src/diffpy/srxplanargui/images/22.png differ diff --git a/src/diffpy/srxplanargui/images/gitarchive.cfg b/src/diffpy/srxplanargui/images/gitarchive.cfg new file mode 100644 index 0000000..43b349f --- /dev/null +++ b/src/diffpy/srxplanargui/images/gitarchive.cfg @@ -0,0 +1,5 @@ +[DEFAULT] +commit = $Format:%H$ +date = $Format:%ai$ +timestamp = $Format:%at$ +refnames = $Format:%D$ diff --git a/src/diffpy/srxplanargui/images/icon.ico b/src/diffpy/srxplanargui/images/icon.ico new file mode 100644 index 0000000..c87fb66 Binary files /dev/null and b/src/diffpy/srxplanargui/images/icon.ico differ diff --git a/src/diffpy/srxplanargui/images/icon.png b/src/diffpy/srxplanargui/images/icon.png new file mode 100644 index 0000000..8919ccc Binary files /dev/null and b/src/diffpy/srxplanargui/images/icon.png differ diff --git a/src/diffpy/srxplanargui/live.py b/src/diffpy/srxplanargui/live.py new file mode 100644 index 0000000..9c05fc5 --- /dev/null +++ b/src/diffpy/srxplanargui/live.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanargui by Simon J. L. Billinge group +# (c) 2012-2025 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## +"""Provide UI for srxplanar.""" + +import os +import sys + +from pyface.api import GUI, ImageResource, SplashScreen +from traits.api import Any, on_trait_change +from traits.etsconfig.api import ETSConfig +from traitsui.api import ( + Action, + Group, + HGroup, + InstanceEditor, + Item, + VGroup, + View, + spring, +) +from traitsui.menu import OKButton + +from diffpy.srxplanar.srxplanar import SrXplanar +from diffpy.srxplanargui.calibration import Calibration +from diffpy.srxplanargui.help import SrXguiHelp +from diffpy.srxplanargui.selectfiles import AddFiles +from diffpy.srxplanargui.srxconfig import SrXconfig +from diffpy.srxplanargui.srxgui import SrXgui, SrXguiHandler + +ETSConfig.toolkit = "qt" + + +class SrXguiLive(SrXgui): + + getxgui = Any + + def __init__(self, configfile=None, args=None, **kwargs): + + # init the object, createt the notifications + + self.splash = SplashScreen( + image=ImageResource("01.png"), show_log_messages=False + ) + self.splash.open() + + super(SrXgui, self).__init__(**kwargs) + configfile = self.detectConfigfile(configfile) + if not os.path.exists(configfile): + configfile = self.detectConfigfile("default") + self.configfile = configfile + + if not kwargs.has_key("srxconfig"): + self.srxconfig = SrXconfig( + filename=configfile, args=args, **kwargs + ) + + self.addfiles = AddFiles(srxconfig=self.srxconfig) + self.srx = SrXplanar(self.srxconfig) + self.addfiles.srx = self.srx + self.help = SrXguiHelp() + self.calibration = Calibration(srx=self.srx, srxconfig=self.srxconfig) + self.splash.close() + return + + @on_trait_change("srxconfig.savedirectory") + def _changedir(self): + newdir = self.srxconfig.savedirectory + if os.path.exists(newdir): + self.getxgui.getxconfig.inputdir = os.path.abspath(newdir) + self.getxgui.getxconfig.savedir = os.path.abspath(newdir) + else: + self.getxgui.getxconfig.inputdir = os.path.abspath(os.path.curdir) + self.getxgui.getxconfig.savedir = os.path.abspath(os.path.curdir) + return + + def processSelected(self, summation=False): + if self.addfiles.selected: + self.srx.updateConfig() + filelist = [f.fullname for f in self.addfiles.selected] + self.srx.prepareCalculation(filelist) + rvlist = self.srx.integrateFilelist(filelist, summation=summation) + newchifilelist = [rv["filename"] for rv in rvlist] + GUI.invoke_later(self.addNewImagesToGetXgui, newchifilelist) + return + + def addNewImagesToGetXgui(self, filelist): + """Add new images to getxgui, if images are already there, + refresh them. + + :param filelist: list of full path of new images + """ + self.addfiles.refreshdatalist = True + newdatacontainers = self.getxgui.selectfiles.addFiles(filelist) + self.getxgui.createNewPlot(newdatacontainers) + return + + helpbutton_action = Action(name="Help ", action="_helpView") + saveconfig_action = Action( + name="Save Config", + action="_saveconfigView", + enabled_when="not capturing", + ) + loadconfig_action = Action( + name="Load Config", + action="_loadconfigView", + enabled_when="not capturing", + ) + + traits_view = View( + HGroup( + Item( + "addfiles", + editor=InstanceEditor(view="traits_view"), + style="custom", + label="Files", + width=0.4, + ), + VGroup( + Group( + Item( + "srxconfig", + editor=InstanceEditor(view="main_view"), + style="custom", + label="Basic", + show_label=False, + ), + springy=True, + ), + HGroup( + spring, + Item("selfcalibratebb", enabled_when="not capturing"), + Item("integratbb", enabled_when="not capturing"), + spring, + show_labels=False, + ), + ), + layout="split", + springy=True, + dock="tab", + show_labels=False, + ), + resizable=True, + title="SrXgui", + width=700, + height=650, + kind="live", + icon=ImageResource("icon.png"), + handler=SrXguiHandler(), + buttons=[ + helpbutton_action, + saveconfig_action, + loadconfig_action, + OKButton, + ], + ) + + +def main(): + gui = SrXguiLive() + gui.configure_traits(view="traits_view") + return + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/diffpy/srxplanargui/selectfiles.py b/src/diffpy/srxplanargui/selectfiles.py new file mode 100644 index 0000000..7b3d1e8 --- /dev/null +++ b/src/diffpy/srxplanargui/selectfiles.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanargui by Simon J. L. Billinge group +# (c) 2012 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## + + +import os + +from pyface.api import ImageResource +from traits.api import ( + Any, + Bool, + Button, + Directory, + Enum, + Event, + HasTraits, + Instance, + Property, + Str, + on_trait_change, + property_depends_on, +) +from traitsui.api import ( + Group, + Handler, + HGroup, + Item, + TableEditor, + TextEditor, + TitleEditor, + VGroup, + View, + spring, +) +from traitsui.editors.table_editor import TableEditor as TableEditorBE +from traitsui.menu import CancelButton, OKButton +from traitsui.table_column import ObjectColumn + +try: + from diffpy.pdfgetx.functs import sortKeyNumericString +except ImportError: + from diffpy.pdfgete.functs import sortKeyNumericString + +from diffpy.srxplanar.loadimage import openImage, saveImage +from diffpy.srxplanargui.datacontainer import DataContainer +from diffpy.srxplanargui.imageplot import ImagePlot +from diffpy.srxplanargui.srxconfig import SrXconfig + +# -- The Live Search table editor definition ------------------------------ + + +class AddFilesHandler(Handler): + + def object_selectallbb_changed(self, info): + """Select all files.""" + # FIXME + try: + editor = [ + aa for aa in info.ui._editors if isinstance(aa, TableEditorBE) + ][0] + info.object.selected = [ + info.object.datafiles[i] for i in editor.filtered_indices + ] + editor.refresh() + except (AttributeError, IndexError): + pass + return + + def object_dclick_changed(self, info): + info.object._plotbb_fired() + return + + +class SaveImageHandler(Handler): + + def closed(self, info, is_ok): + if is_ok: + info.object._sumImgs() + return + + +class AddFiles(HasTraits): + + srxconfig = Instance(SrXconfig) + + # The currently inputdir directory being searched: + # inputdir = DelegatesTo('srxconfig') + inputdir = Directory() # , entries = 10 ) + + def _inputdir_default(self): + return self.srxconfig.opendirectory + + # Should sub directories be included in the search: + recursive = Bool(False) + # The file types to include in the search: + filetype = Enum("tif", "npy", "all") + # The current search string: + search = Str + # Is the search case sensitive? + casesensitive = Bool(False) + # The live search table filter: + filter = Property # Instance( TableFilter ) + # The current list of source files being searched: + datafiles = Property # List( string ) + # The currently selected source file: + selected = Any # list( DtaContainer) + dclick = Event + # Summary of current number of files: + summary = Property # Str + # some meta data + _filetypedict = { + "tif": [".tif", ".tiff", ".tif.bz2"], + "npy": [".npy"], + "all": "all", + } + + # -- Property Implementations --------------------------------------------- + + @property_depends_on("search, casesensitive") + def _get_filter(self): + """Get filename filter.""" + return _createFileNameFilter(self.search, self.casesensitive) + + refreshdatalist = Event + + @property_depends_on("inputdir, recursive, filetype, refreshdatalist") + def _get_datafiles(self): + """Create a datacontainer list, all files under inputdir is + filtered using filetype.""" + inputdir = self.inputdir + if inputdir == "": + inputdir = os.getcwd() + if not os.path.exists(inputdir): + self.srxconfig.opendirectory = os.getcwd() + inputdir = os.getcwd() + + filetypes = self._filetypedict[self.filetype] + if self.recursive: + rv = [] + for dirpath, dirnames, filenames in os.walk(inputdir): + for filename in filenames: + if (os.path.splitext(filename)[1] in filetypes) or ( + filetypes == "all" + ): + rv.append(os.path.join(dirpath, filename)) + else: + rv = [ + os.path.join(inputdir, filename) + for filename in os.listdir(inputdir) + if (os.path.splitext(filename)[1] in filetypes) + or (filetypes == "all") + ] + + rv.sort(key=sortKeyNumericString) + rvlist = [DataContainer(fullname=fn) for fn in rv] + return rvlist + + @property_depends_on("datafiles, search, casesensitive, selected") + def _get_summary(self): + """Get summary of file.""" + if self.selected and self.datafiles: + rv = "%d files selected in a total of %d files." % ( + len(self.selected), + len(self.datafiles), + ) + else: + rv = "0 files selected in a total of 0 files." + return rv + + @on_trait_change("srxconfig.opendirectory") + def _changeInputdir(self): + """Change inputdir of getxconfig.""" + self.inputdir = self.srxconfig.opendirectory + return + + def _plotbb_fired(self): + try: + imagefile = self.selected[0].fullname + except IndexError: + imagefile = None + if imagefile is not None: + if os.path.exists(imagefile): + imageplot = ImagePlot( + imagefile=imagefile, srx=self.srx, srxconfig=self.srxconfig + ) + # imageplot.createPlot() + imageplot.edit_traits() + return + + def _refreshbb_fired(self): + self.refreshdatalist = True + return + + sumname = Str + + def _sumbb_fired(self): + self.sumname = ( + os.path.splitext(self.selected[0].fullname)[0] + "_sum.tif" + ) + self.edit_traits(view="saveimage_view") + return + + def _sumImgs(self): + if len(self.selected) > 1: + sel = self.selected + img = openImage(sel[0].fullname) + for im in sel[1:]: + img += openImage(im.fullname) + img /= len(sel) + saveImage(self.sumname, img) + self.refreshdatalist = True + return + + saveimage_view = View( + Group( + Item("sumname", springy=True, label="File name"), + ), + buttons=[OKButton, CancelButton], + title="Save image", + width=500, + # height = 400, + resizable=True, + handler=SaveImageHandler(), + icon=ImageResource("icon.png"), + ) + + # -- Traits UI Views ------------------------------------------------------ + tableeditor = TableEditor( + columns=[ + ObjectColumn( + name="basename", + label="Name", + # width=0.70, + editable=False, + ), + ], + auto_size=True, + # show_toolbar = True, + deletable=True, + # reorderable = True, + edit_on_first_click=False, + filter_name="filter", + selection_mode="rows", + selected="selected", + dclick="dclick", + label_bg_color="(244, 243, 238)", + cell_bg_color="(234, 233, 228)", + ) + + selectallbb = Button("Select all") + refreshbb = Button("Refresh") + plotbb = Button("Mask") + sumbb = Button("Sum") + + traits_view = View( + VGroup( + VGroup( + HGroup( + Item( + "search", + id="search", + springy=True, + editor=TextEditor(auto_set=False), + ), + ), + HGroup( + spring, + Item("selectallbb", show_label=False), + Item("refreshbb", show_label=False), + spring, + Item("filetype", label="Type"), + ), + Item("datafiles", id="datafiles", editor=tableeditor), + Item("summary", editor=TitleEditor()), + HGroup( + spring, + Item("plotbb", show_label=False), + Item("sumbb", show_label=False), + spring, + ), + dock="horizontal", + show_labels=False, + ), + ), + # title = 'Add files', + # width = 500, + height=600, + resizable=True, + handler=AddFilesHandler(), + ) + + +def _createFileNameFilter(pattern, casesensitive): + """Build function that returns True for matching files. + + pattern -- string pattern to be matched + casesensitive -- flag for case-sensitive file matching + + Return callable object. + """ + try: + from diffpy.pdfgetx.multipattern import MultiPattern + except ImportError: + from diffpy.pdfgete.multipattern import MultiPattern + # MultiPattern always matches for an empty pattern, thus there + # is no need to handle empty search string in a special way. + patterns = pattern.split() + if not casesensitive: + patterns = [p.lower() for p in patterns] + + mp = MultiPattern(patterns) + + def rv(x): + name = x.basename + if not casesensitive: + name = name.lower() + return mp.match(name) + + return rv + + +# Run the demo (if invoked from the command line): +if __name__ == "__main__": + addfiles = AddFiles() + addfiles.configure_traits() diff --git a/src/diffpy/srxplanargui/srxconfig.py b/src/diffpy/srxplanargui/srxconfig.py new file mode 100644 index 0000000..a83a517 --- /dev/null +++ b/src/diffpy/srxplanargui/srxconfig.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanar by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2010-2025 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSENOTICE.txt for license information. +# +############################################################################## + + +import os + +import numpy as np +from pyface.api import ImageResource +from traits.api import Bool, Enum, Property, on_trait_change +from traits.etsconfig.api import ETSConfig +from traitsui.api import Group, Item, View + +from diffpy.srxconfutils.configtraits import ConfigBaseTraits +from diffpy.srxplanar.srxplanarconfig import ( + _description, + _epilog, + _optdatalist, + checkMax, +) + +ETSConfig.toolkit = "qt" + + +_optdatalist.append( + [ + "xpixelsizetem", + { + "sec": "Beamline", + "h": "detector pixel size in x axis, in A^-1", + "d": 0.02, + }, + ] +) +_optdatalist.append( + [ + "ypixelsizetem", + { + "sec": "Beamline", + "h": "detector pixel size in y axis, in A^-1", + "d": 0.02, + }, + ] +) + +for i in _optdatalist: + if i[0] == "polcorrectionenable": + i[1] = { + "sec": "Others", + "args": "n", + "config": "n", + "header": "n", + "s": "polarcorr", + "h": "enable polarization correction", + "n": "?", + "co": False, + "d": False, + } + elif i[0] == "polcorrectf": + i[1] = { + "sec": "Others", + "args": "n", + "config": "n", + "header": "n", + "s": "polarf", + "h": "polarization correction factor", + "d": 0.99, + } + elif i[0] == "xpixelsize": + i[1] = { + "sec": "Beamline", + "args": "n", + "config": "n", + "header": "n", + "s": "xp", + "h": "detector pixel size in x axis, in mm", + "d": 0.2, + } + elif i[0] == "ypixelsize": + i[1] = { + "sec": "Beamline", + "args": "n", + "config": "n", + "header": "n", + "s": "yp", + "h": "detector pixel size in y axis, in mm", + "d": 0.2, + } + + +class SrXconfig(ConfigBaseTraits): + """Config class, based on ConfigBase class in diffpy.confutils.""" + + # Text to display before the argument help + _description = _description + + # Text to display after the argument help + _epilog = _epilog + + _optdatalist = _optdatalist + + _defaultdata = {"configfile": [], "headertitle": "SrXgui configuration"} + + rotation = Property( + depends_on="rotationd", fget=lambda self: np.radians(self.rotationd) + ) + tilt = Property( + depends_on="tiltd", fget=lambda self: np.radians(self.tiltd) + ) + tthstep = Property( + depends_on="tthstepd", fget=lambda self: np.radians(self.tthstepd) + ) + tthmax = Property( + depends_on="tthmaxd", fget=lambda self: np.radians(self.tthmaxd) + ) + + tthorqmax = Property( + depends_on="integrationspace, tthmaxd, qmax", + fget=lambda self: ( + self.tthmax if self.integrationspace == "twotheta" else self.qmax + ), + ) + tthorqstep = Property( + depends_on="integrationspace, tthmaxd, qmax", + fget=lambda self: ( + self.tthstep if self.integrationspace == "twotheta" else self.qstep + ), + ) + + def _preUpdateSelf(self, **kwargs): + """Additional process called in self._updateSelf, this method is + called before self._copySelftoConfig(), i.e. before copy options + value to self.config (config file) + + check the tthmaxd and qmax, and set tthorqmax, tthorqstep + according to integration space + + :param kwargs: optional kwargs + """ + self.tthmaxd, self.qmax = checkMax(self) + """Addmask = [b for b in self.addmask if not (b in + ['brightpixel', 'darkpixel'])] if len(addmask) > 0: + + self.maskfile = addmask[0] + """ + return + + def _opendirectory_changed(self): + if os.path.exists(self.opendirectory): + self.savedirectory = os.path.abspath(self.opendirectory) + else: + self.opendirectory = os.path.abspath(os.curdir) + self.savedirectory = os.path.abspath(os.curdir) + return + + def _savedirectory_changed(self): + if not os.path.exists(self.savedirectory): + self.savedirectory = os.path.abspath(os.curdir) + return + + configmode = Enum(["TEM", "normal"]) + + @on_trait_change("distance, wavelength, xpixelsizetem, ypixelsizetem") + def _refreshPSsize(self, obj, name, new): + self.updateConfig( + xpixelsize=self.xpixelsizetem * self.wavelength * self.distance, + ypixelsize=self.ypixelsizetem * self.wavelength * self.distance, + ) + return + + directory_group = Group( + Item( + "opendirectory", label="Input dir.", help="directory of 2D images" + ), + Item( + "savedirectory", + label="Output dir.", + help="directory of saved files", + ), + show_border=True, + label="Files", + ) + mask_group = Group( + Item("maskfile", label="Mask file"), + show_border=True, + label="Masks", + ) + + geometry_visible = Bool(False) + geometry_group = Group( + Item("integrationspace", label="Integration grid"), + Item( + "wavelength", + visible_when='integrationspace == "qspace"', + label="Wavelength", + ), + Item("xbeamcenter", label="X beamcenter"), + Item("ybeamcenter", label="Y beamcenter"), + Item( + "distance", + label="Camera length", + visible_when='configmode == "TEM"', + ), + Item( + "distance", label="Distance", visible_when='configmode == "normal"' + ), + Item("rotationd", label="Rotation"), + Item("tiltd", label="Tilt rotation"), + Item( + "tthstepd", + label="Integration step", + visible_when='integrationspace == "twotheta"', + ), + Item( + "qstep", + label="Integration step", + visible_when='integrationspace == "qspace"', + ), + show_border=True, + # label='Geometry parameters', + visible_when="geometry_visible", + ) + + correction_visible = Bool(False) + correction_group = Group( + Item("uncertaintyenable", label="Uncertainty"), + Item("sacorrectionenable", label="solid angle corr."), + # Item('polcorrectionenable', label='polarization corr.'), + # Item('polcorrectf', label='polarization factor'), + # Item('brightpixelmask', label='Bright pixel mask'), + # Item('darkpixelmask', label='Dark pixel mask'), + # Item('avgmask', label='Average mask'), + # Item('cropedges', label='Crop edges', editor=ArrayEditor(width=-50)), + show_border=True, + # label='Corrections' + visible_when="correction_visible", + ) + + detector_visible = Bool(False) + detector_group = ( + Group( + Item("fliphorizontal", label="Flip horizontally"), + Item("flipvertical", label="Flip vertically"), + Item("xdimension", label="x dimension"), + Item("ydimension", label="y dimension"), + Item( + "xpixelsizetem", + label="x pixel size (A^-1)", + tooltip="x pixel size, in A^-1", + visible_when='configmode == "TEM"', + ), + Item( + "ypixelsizetem", + label="y pixel size (A^-1)", + tooltip="y pixel size, in A^-1", + visible_when='configmode == "TEM"', + ), + show_border=True, + # label='Detector parameters' + visible_when="detector_visible", + ), + ) + + main_view = View( + Group( + directory_group, + mask_group, + Group( + # Item('configmode'), + Group( + Item("geometry_visible", label="Geometry parameters"), + geometry_group, + ), + Group( + Item("correction_visible", label="Corrections"), + correction_group, + ), + Group( + Item("detector_visible", label="Detector parameters"), + detector_group, + ), + # label = 'Basic' + show_border=True, + ), + ), + resizable=True, + scrollable=True, + # handler = handler, + icon=ImageResource("icon.png"), + ) + + +SrXconfig.initConfigClass() + +if __name__ == "__main__": + a = SrXconfig() + # a.updateConfig() + a.configure_traits(view="main_view") diff --git a/src/diffpy/srxplanargui/srxgui.py b/src/diffpy/srxplanargui/srxgui.py new file mode 100644 index 0000000..76ecaa0 --- /dev/null +++ b/src/diffpy/srxplanargui/srxgui.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanargui by Simon J. L. Billinge group +# (c) 2012-2025 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## +"""Provide UI for srxplanar.""" + +import os +import sys + +from pyface.api import ImageResource +from traits.api import Any, Button, File, HasTraits, Instance +from traits.etsconfig.api import ETSConfig +from traitsui.api import ( + Action, + Group, + Handler, + HGroup, + InstanceEditor, + Item, + VGroup, + View, + spring, +) +from traitsui.menu import CancelButton, OKButton + +from diffpy.srxplanar.srxplanar import SrXplanar +from diffpy.srxplanargui.calibration import Calibration +from diffpy.srxplanargui.help import SrXguiHelp +from diffpy.srxplanargui.selectfiles import AddFiles +from diffpy.srxplanargui.srxconfig import SrXconfig + +ETSConfig.toolkit = "qt" + + +class SrXguiHandler(Handler): + + def closed(self, info, is_ok): + """Notify main gui to delete current plot in plots list.""" + configfile = info.object.detectConfigfile("default") + info.object.saveConfig(configfile) + return True + + def _saveconfigView(self, info): + info.object._saveconfigView() + return + + def _loadconfigView(self, info): + info.object._loadconfigView() + return + + def _helpView(self, info): + info.object._helpbb_changed() + return + + +class SaveHandler(Handler): + + def closed(self, info, is_ok): + if is_ok: + info.object.saveConfig(info.object.configfile) + return True + + +class LoadHandler(Handler): + def closed(self, info, is_ok): + if is_ok: + info.object.loadConfig(info.object.configfile) + return + + +class SrXgui(HasTraits): + + addfiles = Instance(AddFiles) + srxconfig = Instance(SrXconfig) + help = Instance(SrXguiHelp) + splash = Any + calibration = Instance(Calibration) + + def __init__(self, configfile=None, args=None, **kwargs): + """Init the object, createt the notifications.""" + super(SrXgui, self).__init__(**kwargs) + configfile = self.detectConfigfile(configfile) + if not os.path.exists(configfile): + configfile = self.detectConfigfile("default") + self.configfile = configfile + + if not kwargs.has_key("srxconfig"): + self.srxconfig = SrXconfig( + filename=configfile, args=args, **kwargs + ) + + self.addfiles = AddFiles(srxconfig=self.srxconfig) + self.srx = SrXplanar(self.srxconfig) + self.addfiles.srx = self.srx + self.help = SrXguiHelp() + self.calibration = Calibration(srx=self.srx, srxconfig=self.srxconfig) + + # self.loadConfig(configfile) + self.splash.close() + return + + def saveConfig(self, filename=None): + """Save config.""" + if filename == "default": + filename = self.detectConfigfile(filename) + self.srxconfig.writeConfig(filename, mode="full") + self.configfile = filename + return + + def loadConfig(self, filename=None): + """Load config.""" + configfile = self.detectConfigfile(filename) + if os.path.exists(configfile): + self.srxconfig.updateConfig(filename=configfile) + self.configfile = configfile + return + + def processSelected(self, summation=False): + if self.addfiles.selected: + self.srx.updateConfig() + filelist = [f.fullname for f in self.addfiles.selected] + self.srx.prepareCalculation(filelist) + self.srx.integrateFilelist(filelist, summation=summation) + return + + def detectConfigfile(self, filename): + """Current directory > home directory, if none, then return the + curdir+filename if 'default', then return home+filename.""" + if filename is None: + configfile = os.path.join(os.path.curdir, "srxconfig.cfg") + elif filename == "default": + configfile = os.path.join(os.path.expanduser("~"), "srxconfig.cfg") + else: + if os.path.abspath(filename): + if os.path.exists(filename): + configfile = filename + else: + filename = os.path.split(filename)[1] + configfile = os.path.join(os.path.curdir, filename) + else: + configfile = os.path.join(os.path.curdir, filename) + return configfile + + ########################################################### + def _saveconfigView(self): + self.edit_traits(view="saveconfig_view") + return + + def _loadconfigView(self): + self.edit_traits(view="loadconfig_view") + return + + configfile = File() + helpbutton_action = Action(name="Help ", action="_helpView") + saveconfig_action = Action(name="Save Config", action="_saveconfigView") + loadconfig_action = Action(name="Load Config", action="_loadconfigView") + + saveconfig_view = View( + Item("configfile"), + resizable=True, + title="Save config", + width=500, + buttons=[OKButton, CancelButton], + handler=SaveHandler(), + icon=ImageResource("icon.png"), + ) + loadconfig_view = View( + Item("configfile"), + resizable=True, + title="Load config", + width=500, + buttons=[OKButton, CancelButton], + handler=LoadHandler(), + icon=ImageResource("icon.png"), + ) + ############################################################# + + def _integratbb_changed(self): + self.processSelected(False) + return + + def _integratessbb_changed(self): + self.processSelected(True) + return + + def _helpbb_changed(self): + self.help.edit_traits(view="quickstart_view") + return + + def _selfcalibratebb_changed(self): + image = None + if self.addfiles.selected is not None: + if len(self.addfiles.selected) == 1: + image = self.addfiles.selected[0].fullname + + if image is not None: + self.calibration.image = image + self.calibration.edit_traits(view="main_View") + return + + integratbb = Button("Integrate") + integratessbb = Button("Sum and Integrate") + selfcalibratebb = Button("Calibrate") + helpbb = Button("Help") + + traits_view = View( + HGroup( + Item( + "addfiles", + editor=InstanceEditor(view="traits_view"), + style="custom", + label="Files", + width=0.4, + ), + VGroup( + Group( + Item( + "srxconfig", + editor=InstanceEditor(view="main_view"), + style="custom", + label="Basic", + show_label=False, + ), + springy=True, + ), + HGroup( + spring, + Item("selfcalibratebb"), + Item("integratbb"), + spring, + show_labels=False, + ), + ), + layout="split", + springy=True, + dock="tab", + show_labels=False, + ), + resizable=True, + title="SrXgui", + width=700, + height=650, + kind="live", + buttons=[ + helpbutton_action, + saveconfig_action, + loadconfig_action, + OKButton, + ], + icon=ImageResource("icon.png"), + handler=SrXguiHandler(), + ) + + +if __name__ == "__main__": + sys.exit() diff --git a/src/diffpy/srxplanargui/srxguiapp.py b/src/diffpy/srxplanargui/srxguiapp.py new file mode 100644 index 0000000..c9c2fe5 --- /dev/null +++ b/src/diffpy/srxplanargui/srxguiapp.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.srxplanargui by Simon J. L. Billinge group +# (c) 2012-2025 Trustees of the Columbia University +# in the City of New York. All rights reserved. +# +# File coded by: Xiaohao Yang +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## +"""Provide UI for srxplanar.""" + +import logging +import os +import sys +import warnings + +from pyface.api import ImageResource, SplashScreen +from traits.etsconfig.api import ETSConfig + +from diffpy.srxplanargui.srxgui import SrXgui + +warnings.filterwarnings("ignore") + + +logging.disable("CRITICAL") + +# break if help passed to the args +sysargv = sys.argv[1:] +if ("--help" in sysargv) or ("-h" in sysargv): + from diffpy.srxplanargui.srxconfig import SrXconfig + + SrXconfig(args=sysargv) + + +os.environ["QT_API"] = "pyside" +ETSConfig.toolkit = "qt" + + +# open splash screen +splash = SplashScreen(image=ImageResource("01.png"), show_log_messages=False) +if not any([aa == "-h" or aa == "--help" for aa in sysargv]): + splash.open() + + +def main(): + gui = SrXgui(splash=splash) + gui.configure_traits(view="traits_view") + return + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/diffpy/srxplanargui/srxplanargui_app.py b/src/diffpy/srxplanargui/srxplanargui_app.py new file mode 100644 index 0000000..653c323 --- /dev/null +++ b/src/diffpy/srxplanargui/srxplanargui_app.py @@ -0,0 +1,33 @@ +import argparse + +from diffpy.srxplanargui.version import __version__ # noqa + + +def main(): + parser = argparse.ArgumentParser( + prog="diffpy.srxplanargui", + description=( + "xPDFsuite, a software for PDF transformation" + " and visualization.\n\n For more information, visit: " + "https://github.com/diffpy/diffpy.srxplanargui/" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", + action="store_true", + help="Show the program's version number and exit", + ) + + args = parser.parse_args() + + if args.version: + print(f"diffpy.srxplanargui {__version__}") + else: + # Default behavior when no arguments are given + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/src/diffpy/srxplanargui/version.py b/src/diffpy/srxplanargui/version.py new file mode 100644 index 0000000..d278817 --- /dev/null +++ b/src/diffpy/srxplanargui/version.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +############################################################################## +# +# (c) 2012-2025 The Trustees of Columbia University in the City of New York. +# All rights reserved. +# +# File coded by: Xiaohao Yang, Billinge Group members. +# +# See GitHub contributions for a more detailed list of contributors. +# https://github.com/diffpy/diffpy.srxplanargui/graphs/contributors # noqa: E501 +# +# See LICENSE.rst for license information. +# +############################################################################## +"""Definition of __version__.""" + +# We do not use the other three variables, but can be added back if needed. +# __all__ = ["__date__", "__git_commit__", "__timestamp__", "__version__"] + +# obtain version information +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("diffpy.srxplanargui") +except PackageNotFoundError: + __version__ = "unknown" diff --git a/tests/test_version.py b/tests/test_version_confutils.py similarity index 100% rename from tests/test_version.py rename to tests/test_version_confutils.py diff --git a/tests/test_version_planargui.py b/tests/test_version_planargui.py new file mode 100644 index 0000000..008dab2 --- /dev/null +++ b/tests/test_version_planargui.py @@ -0,0 +1,10 @@ +"""Unit tests for __version__.py.""" + +import diffpy.srxplanargui # noqa + + +def test_package_version(): + """Ensure the package version is defined and not set to the initial + placeholder.""" + assert hasattr(diffpy.srxplanargui, "__version__") + assert diffpy.srxplanargui.__version__ != "0.0.0"