From 20650d2969a3d8c40887cd0f44557b3086422c91 Mon Sep 17 00:00:00 2001 From: Ronald Pandolfi Date: Tue, 30 Apr 2019 11:16:07 -0700 Subject: [PATCH 1/3] Massive merge of NSLS-II branch into latest master. Py3 specific changes were removed. --- client/__init__.py | 12 +- client/dask_active_executor.py | 1 + client/dask_io_loop.py | 5 +- client/dask_local_scheduler.py | 2 +- client/dask_remote_scheduler.py | 2 +- client/databrokerclient.py | 59 ++ client/globus.py | 20 +- client/newt.py | 6 +- client/sftp.py | 1 + client/spot.py | 16 +- client/ssh.py | 1 + daemon/__init__.py | 4 - daemon/daemon.py | 98 --- daemon/process.py | 56 -- modpkgs/collectionsmod.py | 1 + modpkgs/guiinvoker.py | 1 + modpkgs/nonesigmod.py | 3 +- modpkgs/yamlmod.py | 1 + pipeline/Staggered -Plot-Attempt.py | 7 +- pipeline/__init__.py | 6 +- pipeline/daemon.py | 47 ++ pipeline/detectors.py | 4 +- pipeline/formats.py | 66 +- pipeline/integration.py | 9 +- pipeline/loader.py | 178 ++++- pipeline/msg.py | 10 +- pipeline/{pathtools.py => path.py} | 7 +- pipeline/peakfinding.py | 2 +- pipeline/peakfindingrem.py | 2 +- pipeline/saxs_calibration.py | 2 +- pipeline/variation.py | 2 +- pipeline/writer.py | 4 +- python3notes | 5 + setup.py | 4 +- xicam/ROI.py | 2 +- xicam/clientmanager.py | 8 +- xicam/config.py | 4 +- xicam/debugtools.py | 1 + xicam/dialogs.py | 1 + xicam/models.py | 5 +- xicam/plugins/EZPluginTest.py | 4 +- xicam/plugins/MOTD.py | 12 +- xicam/plugins/__init__.py | 9 +- xicam/plugins/base.py | 7 +- xicam/plugins/batch.py | 0 xicam/plugins/cdsaxs/__init1__.py | 449 +++++++++++ xicam/plugins/cdsaxs/__init__.py | 751 ++++++++++++++++++ xicam/plugins/cdsaxs/cdsaxs.py | 217 +++++ xicam/plugins/cdsaxs/fitting.py | 138 ++++ xicam/plugins/cdsaxs/simulation.py | 72 ++ xicam/plugins/hiprmc/obsolete/RmcView.py | 144 ---- xicam/plugins/hiprmc/obsolete/rmc.py | 165 ---- xicam/plugins/library.py | 10 +- xicam/plugins/log.py | 4 +- xicam/plugins/tomography/reconpkg.py | 28 - xicam/plugins/tomography/tomocam/XT_Common.py | 40 - .../tomography/tomocam/XT_ForwardModel.py | 156 ---- xicam/plugins/tomography/tomocam/__init__.py | 3 - xicam/plugins/tomography/tomocam/gnufft.so | Bin 211280 -> 0 bytes xicam/plugins/tomography/tomocam/tomoCam.py | 254 ------ xicam/plugins/viewer.py | 20 +- xicam/plugins/viewer3D/__init__.py | 4 +- xicam/plugins/viewer3D/viewer.py | 2 +- xicam/plugins/widgets.py | 67 +- xicam/plugins/zipythonconsole.py | 4 +- xicam/threads.py | 21 +- xicam/toolbar.py | 1 + xicam/watcher.py | 1 + xicam/widgets/NDTimelinePlotWidget.py | 7 +- xicam/widgets/calibrationpanel.py | 1 + xicam/widgets/customwidgets.py | 4 +- xicam/widgets/explorer.py | 335 ++++++-- xicam/widgets/featurewidgets.py | 1 + xicam/widgets/imageviewers.py | 4 +- xicam/widgets/login.py | 14 +- xicam/widgets/metadatawidget.py | 121 +++ xicam/xglobals.py | 1 + xicam/xicamwindow.py | 61 +- xicamlauncher/main.py | 13 +- xicamlauncher/splash.py | 1 + 80 files changed, 2578 insertions(+), 1233 deletions(-) create mode 100644 client/databrokerclient.py delete mode 100644 daemon/__init__.py delete mode 100644 daemon/daemon.py delete mode 100644 daemon/process.py create mode 100644 pipeline/daemon.py rename pipeline/{pathtools.py => path.py} (84%) create mode 100644 python3notes create mode 100644 xicam/plugins/batch.py create mode 100644 xicam/plugins/cdsaxs/__init1__.py create mode 100644 xicam/plugins/cdsaxs/__init__.py create mode 100644 xicam/plugins/cdsaxs/cdsaxs.py create mode 100644 xicam/plugins/cdsaxs/fitting.py create mode 100644 xicam/plugins/cdsaxs/simulation.py delete mode 100644 xicam/plugins/hiprmc/obsolete/RmcView.py delete mode 100644 xicam/plugins/hiprmc/obsolete/rmc.py delete mode 100644 xicam/plugins/tomography/reconpkg.py delete mode 100644 xicam/plugins/tomography/tomocam/XT_Common.py delete mode 100644 xicam/plugins/tomography/tomocam/XT_ForwardModel.py delete mode 100644 xicam/plugins/tomography/tomocam/__init__.py delete mode 100755 xicam/plugins/tomography/tomocam/gnufft.so delete mode 100644 xicam/plugins/tomography/tomocam/tomoCam.py create mode 100644 xicam/widgets/metadatawidget.py diff --git a/client/__init__.py b/client/__init__.py index dc5a5c14..b425f60f 100644 --- a/client/__init__.py +++ b/client/__init__.py @@ -1,11 +1,13 @@ +from __future__ import absolute_import import pysftp import paramiko -from globus import GLOBUSError -from spot import SPOTError -from newt import NEWTError -import ssh +from .globus import GLOBUSError +from .spot import SPOTError +from .newt import NEWTError +from .databrokerclient import DBError +from . import ssh -__all__ = ['newt', 'spot', 'globus', 'sftp', 'ssh'] +__all__ = ['newt', 'spot', 'globus', 'sftp', 'ssh', 'databrokerclient'] # Exceptions raised by clients that we care to handle EXCEPTIONS = (pysftp.ConnectionException, paramiko.ssh_exception.BadAuthenticationType, diff --git a/client/dask_active_executor.py b/client/dask_active_executor.py index fe441845..8b8eb987 100644 --- a/client/dask_active_executor.py +++ b/client/dask_active_executor.py @@ -1 +1,2 @@ +from __future__ import unicode_literals active_executor = None diff --git a/client/dask_io_loop.py b/client/dask_io_loop.py index 484ec0e9..aff0c27d 100644 --- a/client/dask_io_loop.py +++ b/client/dask_io_loop.py @@ -1,4 +1,4 @@ -import distributed +import distributed from tornado.ioloop import IOLoop from threading import Thread from distributed import Scheduler, Worker, Executor @@ -9,7 +9,8 @@ __ioloop__ = IOLoop() -class DaskLoop(): + +class DaskLoop(object): def __init__(self): self.loop = __ioloop__ self.t = Thread(target=self.loop.start) diff --git a/client/dask_local_scheduler.py b/client/dask_local_scheduler.py index 2c844290..efbe1902 100644 --- a/client/dask_local_scheduler.py +++ b/client/dask_local_scheduler.py @@ -2,7 +2,7 @@ from pipeline import msg -class LocalScheduler(): +class LocalScheduler(object): def __init__(self, dloop): """ Create a local scheduler and worker to run commands diff --git a/client/dask_remote_scheduler.py b/client/dask_remote_scheduler.py index 6f57ae0c..7af05526 100644 --- a/client/dask_remote_scheduler.py +++ b/client/dask_remote_scheduler.py @@ -141,7 +141,7 @@ def serve(self): # client.load_system_host_keys() # client.connect('edison.nersc.gov', username="hkrishna") -class RemoteScheduler(): +class RemoteScheduler(object): """ Create a remote executor """ diff --git a/client/databrokerclient.py b/client/databrokerclient.py new file mode 100644 index 00000000..ebad1590 --- /dev/null +++ b/client/databrokerclient.py @@ -0,0 +1,59 @@ +from xicam import config + + +class DataBrokerClient(object): # replace with databroker client + def __init__(self, host, **kwargs): + import os + import numpy as np + # TODO: handlers will need to be registered + # + # from suitcase.als832 import ALSHDF5Handler, ALSHDF5SinoHandler + # from suitcase.als733 import ALSEDFHandler + super(DataBrokerClient, self).__init__() + self.host = host + + # set up filestore + + # from filestore.utils import install_sentinels + # from filestore.fs import FileStore + from databroker.assets.mongo import Registry + from databroker.headersource.mongo import MDS + from databroker import Broker + # from databroker.core import register_builtin_handlers + + fs_config = {'host': host, 'port': 27017, + 'database': config.settings['Databroker FileStore Name']} + + # try: + # # this only needs to be done once + # install_sentinels(fs_config, 1) + # except RuntimeError: + # pass + + fs = Registry(fs_config, version=1) + + # fs.register_handler('ALS_HDF', ALSHDF5Handler) + # fs.register_handler('ALS_HDF_SINO', ALSHDF5SinoHandler) + # fs.register_handler('ALS_EDF', ALSEDFHandler) + # register_builtin_handlers(fs) + mds_conf = dict(database=config.settings['Databroker MetaDataStore Name'], host=host, + port=27017, timezone='US/Eastern') + + mds = MDS(mds_conf, 1, auth=False) + + db = Broker(mds, fs) + print(db) + self.db = db + + def __getitem__(self, item): + return self.db[item] + + def __getattr__(self, item): + return self.db.__getattr__(item) + + def __call__(self, *args, **kwargs): + return self.db(*args, **kwargs) + + +class DBError(Exception): + pass diff --git a/client/globus.py b/client/globus.py index 200a4340..bf07e709 100644 --- a/client/globus.py +++ b/client/globus.py @@ -177,7 +177,7 @@ def determine_local_endpoint(self): os.utime(test, None) for endpoint in user_endpoints: - params = {'path': unicode(test)} + params = {'path': str(test)} r = self.get(self.TRANSFER_URL + '/endpoint/' + quote(endpoint) + '/ls', headers=self.authentication, params=params) try: @@ -256,11 +256,11 @@ def transfer_file(self, src_endpoint, src_path, dst_enpoint, dst_path=None): transfer_submission = json.load(json_file) transfer_submission["submission_id"] = submission_id["value"] - transfer_submission["source_endpoint"] = unicode(src_endpoint) - transfer_submission["destination_endpoint"] = unicode(dst_enpoint) - transfer_submission["DATA"][0]["source_path"] = unicode(src_path) - transfer_submission["DATA"][0]["destination_path"] = unicode(dst_path) - transfer_submission["label"] = unicode('transfered from SPEW client') + transfer_submission["source_endpoint"] = str(src_endpoint) + transfer_submission["destination_endpoint"] = str(dst_enpoint) + transfer_submission["DATA"][0]["source_path"] = str(src_path) + transfer_submission["DATA"][0]["destination_path"] = str(dst_path) + transfer_submission["label"] = str('transfered from SPEW client') r = self.post(self.TRANSFER_URL + '/transfer', json=transfer_submission, headers=self.authentication) transfer_result = self.check_response(r) @@ -296,9 +296,9 @@ def delete_file(self, endpoint, fpath): delete_submission = json.load(json_file) delete_submission["submission_id"] = submission_id["value"] - delete_submission["endpoint"] = unicode(endpoint) - delete_submission["DATA"][0]["path"] = unicode(fpath) - delete_submission["label"] = unicode('deleted from SPEW client') + delete_submission["endpoint"] = str(endpoint) + delete_submission["DATA"][0]["path"] = str(fpath) + delete_submission["label"] = str('deleted from SPEW client') r = self.post(self.TRANSFER_URL + '/delete', json=delete_submission, headers=self.authentication) @@ -386,7 +386,7 @@ def transfer_generator(self, src_endpoint, src_path, dst_enpoint, dst_path=None) while status['status'] == 'ACTIVE': time.sleep(3) status = self.get_task_status(r['task_id']) - yield float(status['bytes_transferred'])/float(size) + yield old_div(float(status['bytes_transferred']), float(size)) class GLOBUSError(Exception): diff --git a/client/newt.py b/client/newt.py index b3bb92aa..d7ab7572 100644 --- a/client/newt.py +++ b/client/newt.py @@ -206,7 +206,7 @@ def _check_scratch_path(self, path, system): try: contents = self.get_dir_contents(path, system) for item in contents: - if 'user' in item.keys(): + if 'user' in list(item.keys()): if item['user'] == self.username: return True except HTTPError: @@ -234,7 +234,7 @@ def get_home_dir(self, system): self.username) contents = self.get_dir_contents(root_home, system) for item in contents: - if 'user' in item.keys(): + if 'user' in list(item.keys()): if item['user'] == self.username: home_dir = root_home break @@ -437,7 +437,7 @@ def download_file_generator(self, path, system, save_path=None, chunk_size=64*10 if chunk: # filter out keep-alive new chunks f.write(chunk) f.flush() - downloaded += len(chunk)/file_size + downloaded += old_div(len(chunk), file_size) yield downloaded r.close() diff --git a/client/sftp.py b/client/sftp.py index 456a91c1..02a6085c 100644 --- a/client/sftp.py +++ b/client/sftp.py @@ -1,3 +1,4 @@ + import pysftp diff --git a/client/spot.py b/client/spot.py index 4bde3cf8..070397b3 100644 --- a/client/spot.py +++ b/client/spot.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import os from time import sleep -from StringIO import StringIO +from io import StringIO from PIL import Image import numpy as np from client.newt import NewtClient @@ -73,10 +73,10 @@ def search(self, query, **kwargs): 'end_station': ['bl832', 'bl733']} generic_kwargs = ['limitnum', 'skipnum'] for key in kwargs: - if key not in allowed_kwargs.keys() and key not in generic_kwargs: + if key not in list(allowed_kwargs.keys()) and key not in generic_kwargs: raise ValueError('%s keyword not in allowed keywords %s' % - (key, list(allowed_kwargs.keys() + - generic_kwargs))) + (key, list(list(allowed_kwargs.keys()) + + generic_kwargs))) elif key in allowed_kwargs: if kwargs[key] not in allowed_kwargs[key]: raise ValueError('%s keyword value must be on of %s' % @@ -461,7 +461,7 @@ def download_dataset_generator(self, dataset, stage, save_path=None, chunk_size= if chunk: # filter out keep-alive new chunks f.write(chunk) f.flush() - downloaded += len(chunk)/file_size + downloaded += old_div(len(chunk), file_size) yield downloaded r.close() @@ -495,7 +495,7 @@ class SPOTError(Exception): if __name__ == '__main__': import time - from StringIO import StringIO + from io import StringIO from PIL import Image from matplotlib.pyplot import imshow, show, figure s = SpotClient() @@ -505,11 +505,11 @@ class SPOTError(Exception): # print 'Time: ', time.time() - t t = time.time() arr = s.get_image_as('20160630_054009_prefire_3_0amp_scan7', 'raw', ext='tif', index=0) - print arr.shape + print(arr.shape) imshow(arr) show() # for i in range(3): # figure(i) # imshow(arr[:, :, i]) # show() - print 'Time: ', time.time() - t + print('Time: ', time.time() - t) diff --git a/client/ssh.py b/client/ssh.py index c4b48d26..a6b8f1b2 100644 --- a/client/ssh.py +++ b/client/ssh.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import paramiko diff --git a/daemon/__init__.py b/daemon/__init__.py deleted file mode 100644 index b5c98c9e..00000000 --- a/daemon/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__all__ = ['process', 'daemon'] - -from daemon import * - diff --git a/daemon/daemon.py b/daemon/daemon.py deleted file mode 100644 index 47e0e767..00000000 --- a/daemon/daemon.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import time -import process -from PySide import QtCore -import multiprocessing -from xicam import debugtools - - -class daemon(QtCore.QThread): - """ - This starts a daemon running as a QThread which watches a directory for new files and sends them to process.py - Processing is done in separate Processes, splitting tasks between each core. Files that already exist can also - be processed. - """ - - - num_cores = multiprocessing.cpu_count() - def __init__(self, path, experiment, procold=False): - super(daemon, self).__init__() - self.procold = procold - self.experiment = experiment - self.path = path - self.exiting = False - self.childfiles = set(os.listdir(path)) - - - def run(self): - - if self.procold: - self.processfiles(self.path, self.childfiles) - - try: - while not self.exiting: - time.sleep(.1) - self.checkdirectory() # Force update; should not have to do this -.- - except KeyboardInterrupt: - pass - - def stop(self): - self.exiting = True - print ("thread stop - %s" % self.exiting) - - def __del__(self): - self.exiting = True - self.wait() - - @debugtools.timeit - def processfiles(self, path, files): - """ - distribute new files to cores for processing. Ignores .nxs. - """ - - files = [f for f in files if not os.path.splitext(f)[1] == '.hdf'] - if files: - print os.path.splitext(path)[1] - - jobs = [] - p = None - # filter paths - files = list(chunks(files, self.num_cores)) - - for i in range(self.num_cores): - p = multiprocessing.Process(target=process.process, args=(path, files[i], self.experiment)) - jobs.append(p) - p.start() - - while p.is_alive(): # TODO: change to wait for ALL threads? - time.sleep(.1) - - - - def checkdirectory(self): - """ - Checks a directory for new files, comparing what files are there now vs. before - """ - updatedchildren = set(os.listdir(self.path)) - newchildren = updatedchildren - self.childfiles - self.childfiles = updatedchildren - self.processfiles(self.path, list(newchildren)) - - - -def chunks(l, n): - """ - Yield successive n chunks from l. - """ - chunksize = int(len(l) / n) - for i in xrange(0, n, 1): - yield l[i * chunksize:(i + 1) * chunksize] - - -if __name__ == '__main__': - path = '/Users/rp/YL1031' - # files = os.listdir(path) - experiment = None - procold = True - d = daemon(path, experiment, procold) - d.run() diff --git a/daemon/process.py b/daemon/process.py deleted file mode 100644 index 8387197d..00000000 --- a/daemon/process.py +++ /dev/null @@ -1,56 +0,0 @@ - -import pipeline - -import numpy as np -import os -from PIL import Image -import string - - -def process(parent, files, experiment, - options=dict(remesh=False, findcenter=False, refinecenter=False, cachethumbnail=False, variation=True, - savefullres=False)): - """ - Applies a series of processing steps to a list of files; outputs a nexus file containing all results - """ - # print('Processing new file: ' + path) - for f in files: - path = os.path.join(parent, f) - img, _ = pipeline.loader.loadpath(path) - if img is not None: - if options['findcenter']: - cen = pipeline.center_approx.center_approx(img) - experiment.setvalue('Center X', cen[0]) - experiment.setvalue('Center Y', cen[1]) - if options['refinecenter']: - pipeline.center_approx.refinecenter(img, experiment) - - # if False: # log image is needed? - # with np.errstate(invalid='ignore'): - # logimg = np.log(img * (img > 0) + 1) - thumb = None - if options['cachethumbnail']: - thumb = pipeline.writer.thumbnail(img) - - if options['remesh']: - img = pipeline.remesh.remesh(img, path, experiment.getGeometry()) - - variation = None - if options['variation']: - prevpath = pipeline.pathtools.similarframe(path, -1) - nextpath = pipeline.pathtools.similarframe(path, +1) - if prevpath is not None and nextpath is not None: - # print 'comparing:', prevpath, path, nextpath - variation = pipeline.variation.filevariation(1, prevpath, img, nextpath) - # print 'variation:', variation - else: - variation = None - - if img is None: return None - - if not options['savefullres']: - img = None - - pipeline.writer.writenexus(img, thumb, pipeline.pathtools.path2nexus(path), path, variation) - - diff --git a/modpkgs/collectionsmod.py b/modpkgs/collectionsmod.py index 5ede8e9a..3be95200 100644 --- a/modpkgs/collectionsmod.py +++ b/modpkgs/collectionsmod.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict import yaml diff --git a/modpkgs/guiinvoker.py b/modpkgs/guiinvoker.py index d8db42ae..504c218c 100644 --- a/modpkgs/guiinvoker.py +++ b/modpkgs/guiinvoker.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide import QtCore diff --git a/modpkgs/nonesigmod.py b/modpkgs/nonesigmod.py index bf4f4d25..aa27a14e 100644 --- a/modpkgs/nonesigmod.py +++ b/modpkgs/nonesigmod.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals # REF: http://stackoverflow.com/questions/23728401/pyside-crashing-python-when-emitting-none-between-threads _PYSIDE_NONE_SENTINEL = object() @@ -25,7 +26,7 @@ def sentinel_guard(arg): def inner(*args, **kwargs): newargs = map(sentinel_guard, args) - newkwargs = {k: sentinel_guard(v) for k, v in kwargs.iteritems()} + newkwargs = {k: sentinel_guard(v) for k, v in kwargs.items()} return func(*newargs, **newkwargs) return inner \ No newline at end of file diff --git a/modpkgs/yamlmod.py b/modpkgs/yamlmod.py index 626775d7..9d038070 100644 --- a/modpkgs/yamlmod.py +++ b/modpkgs/yamlmod.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import yaml from collections import OrderedDict diff --git a/pipeline/Staggered -Plot-Attempt.py b/pipeline/Staggered -Plot-Attempt.py index e07fca47..eec1abe3 100644 --- a/pipeline/Staggered -Plot-Attempt.py +++ b/pipeline/Staggered -Plot-Attempt.py @@ -1,3 +1,6 @@ +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range from PySide import QtGui, QtCore # (the example applies equally well to PySide) import pyqtgraph as pg import numpy as np @@ -19,10 +22,10 @@ curves.append(plot.plot(TID[:,i], pen=(i,TID.shape[1]*1.3))) def getValue(value): - print value + print(value) for i in range(1, TID.shape[1]): curves[i].setData(TID[:,i]+(i*value)) - print i + print(i) w.connect(slider, QtCore.SIGNAL('valueChanged(int)'), getValue) diff --git a/pipeline/__init__.py b/pipeline/__init__.py index 735248fa..43ea3c16 100644 --- a/pipeline/__init__.py +++ b/pipeline/__init__.py @@ -1,6 +1,6 @@ +from __future__ import unicode_literals __author__ = 'rp' __all__ = ['center_approx', 'integration', 'loader', 'peakfinding', 'remesh', 'saxs_calibration', - 'cosmics', 'variation', 'writer', 'pathtools', 'hig', 'detectors', 'cWarpImage', 'variationoperators'] - - + 'cosmics', 'variation', 'writer', 'pathtools', 'hig', 'detectors', 'cWarpImage', 'variationoperators', + 'daemon'] diff --git a/pipeline/daemon.py b/pipeline/daemon.py new file mode 100644 index 00000000..465332e3 --- /dev/null +++ b/pipeline/daemon.py @@ -0,0 +1,47 @@ +import os +import time +from PySide import QtCore +import multiprocessing +import numpy as np +import glob + +from watchdog.observers import Observer +from watchdog.events import PatternMatchingEventHandler + + +class Watcher(Observer): + def __init__(self, path, filter, newcallback, procold=False, recursive=True): + self.filter = filter + self.newcallback = newcallback + self.procold = procold + + event_handler = PatternMatchingEventHandler(patterns=[filter]) + event_handler.on_created = newcallback + super(Watcher, self).__init__() + self.schedule(event_handler, path, recursive=recursive) + self.start() + + +def chunks(l, n): + """ + Yield successive n chunks from l. + """ + chunksize = int(np.ceil(float(len(l)) / n)) + for i in xrange(n): + yield l[i * chunksize:(i + 1) * chunksize] + + +def test(*args, **kwargs): + print(args, kwargs) + + +if __name__ == '__main__': + path = '/home/rp/data/test' + # files = os.listdir(path) + + procold = True + d = Watcher(path, '*.tif', test, procold=procold) + d.start() + print('here?') + + QtCore.QCoreApplication([]).exec_() diff --git a/pipeline/detectors.py b/pipeline/detectors.py index 4ad6841f..4660a8f7 100644 --- a/pipeline/detectors.py +++ b/pipeline/detectors.py @@ -79,9 +79,9 @@ class PhotonicScience(pyFAI.detectors.Detector): aliases = ["Photonic Science"] force_pixel = True MAX_SHAPE = (1042, 1042) - DEFAULT_PIXEL1 = DEFAULT_PIXEL2 = 200e-6 + DEFAULT_PIXEL1 = DEFAULT_PIXEL2 = 104e-6 - def __init__(self, pixel1=200e-6, pixel2=200e-6): + def __init__(self, pixel1=104e-6, pixel2=104e-6): super(PhotonicScience, self).__init__(pixel1=pixel1, pixel2=pixel2) if (pixel1 != self.DEFAULT_PIXEL1) or (pixel2 != self.DEFAULT_PIXEL2): self._binning = (int(2 * pixel1 / self.DEFAULT_PIXEL1), int(2 * pixel2 / self.DEFAULT_PIXEL2)) diff --git a/pipeline/formats.py b/pipeline/formats.py index f671bad7..96feea0b 100644 --- a/pipeline/formats.py +++ b/pipeline/formats.py @@ -26,8 +26,12 @@ response = msgBox.exec_() if response == QtGui.QMessageBox.Yes: - import pip - failure = pip.main(['install','--upgrade','fabio']) + try: + from pip import main + except ImportError: + from pip._internal import main + + failure = main(['install', '--upgrade', 'fabio']) from xicam.dialogs import infodialog if failure: @@ -239,7 +243,7 @@ def __getitem__(self, item): start = item[n].start if item[n].start is not None else 0 step = item[n].step if item[n].step is not None else 1 stop = item[n].stop if item[n].stop is not None else stop - elif n < len(item) and isinstance(item[n], int): + elif n < len(item) and (isinstance(item[n], int) or 'int' in str(type(item[n]))): if item[n] < 0: start, stop, step = stop + item[n], stop + item[n] + 1, 1 else: @@ -310,8 +314,6 @@ def read(self, f, frame=None): self.currentframe = frame self.data = dfrm - - return self @staticmethod @@ -385,14 +387,14 @@ def __getitem__(self, item): elif n == 2: stop = self.data.shape[1] if n < len(item) and isinstance(item[n], slice): - start = item[n].start if item[n].start is not None else 0 - step = item[n].step if item[n].step is not None else 1 - stop = item[n].stop if item[n].stop is not None else stop - elif n < len(item) and isinstance(item[n], int): - if item[n] < 0: - start, stop, step = stop + item[n], stop + item[n] + 1, 1 - else: - start, stop, step = item[n], item[n] + 1, 1 + start = item[n].start if item[n].start is not None else 0 + step = item[n].step if item[n].step is not None else 1 + stop = item[n].stop if item[n].stop is not None else stop + elif n < len(item) and (isinstance(item[n], int) or 'int' in str(type(item[n]))): + if item[n] < 0: + start, stop, step = stop + item[n], stop + item[n] + 1, 1 + else: + start, stop, step = item[n], item[n] + 1, 1 else: start, step = 0, 1 @@ -401,7 +403,7 @@ def __getitem__(self, item): for n, i in enumerate(range(s[0][0], s[0][1], s[0][2])): _arr = self._dgroup[self.frames[i]][slice(*s[1]), slice(*s[2])] if n == 0: # allocate array - arr = np.empty((len(range(s[0][0], s[0][1], s[0][2])), _arr.shape[0], _arr.shape[1])) + arr = np.empty((len(list(range(s[0][0], s[0][1], s[0][2]))), _arr.shape[0], _arr.shape[1])) arr[n] = _arr if arr.shape[0] == 1: arr = arr[0] @@ -414,7 +416,7 @@ def getframe(self, frame=0): self.data = self._dgroup[self.frames[frame]] return self.data - def next(self): + def __next__(self): if self.currentframe < self.__len__() - 1: self.currentframe += 1 else: @@ -646,10 +648,10 @@ def read(self, f, frame=None): @property def nframes(self): with h5py.File(self.filename, 'r') as h: - dset = h[h.keys()[0]] - ddet = dset[dset.keys()[0]] + dset = h[list(h.keys())[0]] + ddet = dset[list(dset.keys())[0]] if self.isburst: - frames = sum(map(lambda key: '.edf' in key, ddet.keys())) + frames = sum(['.edf' in key for key in list(ddet.keys())]) else: frames = 1 return frames @@ -666,15 +668,15 @@ def getframe(self, frame=None): frame = 0 f = self.filename with h5py.File(f, 'r') as h: - dset = h[h.keys()[0]] - ddet = dset[dset.keys()[0]] + dset = h[list(h.keys())[0]] + ddet = dset[list(dset.keys())[0]] if self.isburst: - frames = [key for key in ddet.keys() if '.edf' in key] + frames = [key for key in list(ddet.keys()) if '.edf' in key] dfrm = ddet[frames[frame]] elif self.istiled: high = ddet[u'high'] low = ddet[u'low'] - frames = [high[high.keys()[0]], low[low.keys()[0]]] + frames = [high[list(high.keys())[0]], low[list(low.keys())[0]]] dfrm = frames[frame] else: dfrm = ddet @@ -685,9 +687,9 @@ def getframe(self, frame=None): def isburst(self): try: with h5py.File(self.filename, 'r') as h: - dset = h[h.keys()[0]] - ddet = dset[dset.keys()[0]] - return not (u'high' in ddet.keys() and u'low' in ddet.keys()) + dset = h[list(h.keys())[0]] + ddet = dset[list(dset.keys())[0]] + return not (u'high' in list(ddet.keys()) and u'low' in list(ddet.keys())) except AttributeError: return False @@ -695,9 +697,9 @@ def isburst(self): def istiled(self): try: with h5py.File(self.filename, 'r') as h: - dset = h[h.keys()[0]] - ddet = dset[dset.keys()[0]] - return u'high' in ddet.keys() and u'low' in ddet.keys() + dset = h[list(h.keys())[0]] + ddet = dset[list(dset.keys())[0]] + return u'high' in list(ddet.keys()) and u'low' in list(ddet.keys()) except AttributeError: return False @@ -886,7 +888,7 @@ def read(self, f, frame=None): return self def _finddatagroup(self, h5object): - exchange_groups = [key for key in h5object.keys() if 'exchange' in key] + exchange_groups = [key for key in list(h5object.keys()) if 'exchange' in key] if len(exchange_groups) > 1: raise RuntimeWarning('More than one exchange group found. Will use \'exchange\'\n' 'Need to use logging for this...') @@ -916,7 +918,7 @@ def getframe(self, frame=0): self.data = self._dgroup['data'][frame] return self.data - def next(self): + def __next__(self): if self.currentframe < self.__len__() - 1: self.currentframe += 1 else: @@ -981,7 +983,7 @@ def __init__(self, path, header=None): super(CondensedTiffStack, self).__init__() self.rawdata = tifffile.imread(path, memmap=True) - self.frames = range(self.rawdata.shape[0]) + self.frames = list(range(self.rawdata.shape[0])) self.header = header self._readheader() @@ -1034,7 +1036,7 @@ def scanparas(path): keylesslines = 0 for line in lines: - cells = filter(None, re.split('[=:]+', line)) + cells = [_f for _f in re.split('[=:]+', line) if _f] key = cells[0].strip() diff --git a/pipeline/integration.py b/pipeline/integration.py index 68019759..c680d06c 100644 --- a/pipeline/integration.py +++ b/pipeline/integration.py @@ -5,7 +5,8 @@ import multiprocessing import time import pyFAI -import msg +from . import remesh +from . import msg pyFAI_method = 'cython' from modpkgs import pyFAImod @@ -322,7 +323,7 @@ def cakexintegrate(data, mask, AIdict, cut=None, color=[255,255,255], requestkey if cut is not None: msg.logMessage(('cut:', cut.shape),msg.DEBUG) - mask &= cut.astype(bool) + mask = (mask & cut.astype(bool)) chi = np.arange(-180,180,360/config.settings['Integration Bins (χ)']) @@ -342,7 +343,7 @@ def cakezintegrate(data, mask, AIdict, cut=None, color=[255,255,255], requestkey if cut is not None: msg.logMessage(('cut:', cut.shape),msg.DEBUG) - mask &= cut.astype(bool) + mask = mask & cut.astype(bool) q = np.arange(config.settings['Integration Bins (q)'])*np.max(qpar)/ 10./config.settings['Integration Bins (q)'] @@ -390,7 +391,7 @@ def remeshqintegrate(data, mask, AIdict, cut=None, color=[255, 255, 255], reques remeshcenter=np.unravel_index(qsquared.argmin(),qsquared.shape) - print 'center?:',remeshcenter + print('center?:', remeshcenter) f2d=AI.getFit2D() f2d['centerX']=remeshcenter[0] diff --git a/pipeline/loader.py b/pipeline/loader.py index e09e3820..ce2ddb3b 100644 --- a/pipeline/loader.py +++ b/pipeline/loader.py @@ -1,4 +1,4 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- import os @@ -34,9 +34,36 @@ def loadsingle(path): return loadimage(path), loadparas(path) - def loadimage(path): data = None + if path.startswith('DB:'): + from xicam import clientmanager + dc = clientmanager.databroker_clients + host, _, uid = path[3:].partition('/') + db = dc[host] + h = db[uid] + + img_names = [k for d in h.descriptors if d['name'] == 'primary' + for k, v in d['data_keys'].items() if v['dtype'] == 'array'] + + # For tiled data + if h.start.get('mode') == 'tiled': + ev1, ev2 = [doc['data'] for name, doc in h.stream('primary', fill=True) if name == 'event'] + data, mask = loadstitched('', '', data1=ev1['image'], data2=ev2['image'], paras1=ev1, paras2=ev2) + return data, mask + else: + data = db.db.get_images(h, img_names[0])[0] + + # converting to np.array takes too long - is it necessary to do this? + # if so, comment out line above and uncomment line below + # data = np.array(db.db.get_images(h, img_names[0]).squeeze() + if data.ndim > 2: + data = np.transpose(data, [1, 2, 0]) + return data + + + + try: ext = os.path.splitext(path)[1] if ext in acceptableexts: @@ -135,6 +162,14 @@ def loadparas(path): fimg = fabio.open(path) return fimg.header + elif path.startswith('DB:'): + from xicam import clientmanager + dc = clientmanager.databroker_clients + host, _, uid = path[3:].partition('/') + db = dc[host] + h = db[uid] + return h + except IOError: msg.logMessage('Unexpected read error in loadparas',msg.ERROR) @@ -300,9 +335,9 @@ def loadpath(path): # Do extra rotations/transposition if config.settings['Image Load Transpose']: - img=img.transpose() + img = img.transpose() if config.settings['Image Load Rotations']: - img=np.rot90(img,config.settings['Image Load Rotations']) + img = np.rot90(img, config.settings['Image Load Rotations']) if not isinstance(img, tuple): mask = finddetectorbyshape(img.shape).calc_mask() @@ -814,9 +849,120 @@ def __getitem__(self, item): return np.array([self.jpegs[i] for i in item]) +class PStack(object): + ndim = 3 + + def __init__(self, projections, dark, flat, sino, hdr, descriptors): + self.primary = projections + self.projections = projections + self.sino = sino + self.darks = dark + self.flats = flat + self.dtype = projections.pixel_type + self.header = hdr + self.descriptors = descriptors + self.frames = [str(j) for j in range(len(self.projections))] + + for pim in (self.sino, self.darks, self.flats, self.primary): + pim.frames = [str(j) for j in range(len(pim))] + + def __len__(self): + return len(self.primary) + + # these are required because this object gets passed + # into pyqtgraph which tries to down sample / scale + @property + def max(self): + return self[0].max() + + @property + def min(self): + return self[0].min() + + @property + def shape(self): + return (len(self.primary),) + self.primary.frame_shape + + @property + def size(self): + return np.prod(self.shape) + + def transpose(self, ax): + return self + + def __getitem__(self, item): + + s = [] + if not isinstance(item, tuple) and not isinstance(item, list): + item = (item,) + for n in range(3): + if n == 0: + stop = len(self) + elif n == 1: + stop = self.data.shape[0] + elif n == 2: + stop = self.data.shape[1] + if n < len(item) and isinstance(item[n], slice): + start = item[n].start if item[n].start is not None else 0 + step = item[n].step if item[n].step is not None else 1 + stop = item[n].stop if item[n].stop is not None else stop + elif n < len(item) and (isinstance(item[n], int) or 'int' in str(type(item[n]))): + if item[n] < 0: + start, stop, step = stop + item[n], stop + item[n] + 1, 1 + else: + start, stop, step = item[n], item[n] + 1, 1 + else: + start, step = 0, 1 + + s.append((start, stop, step)) + + for n, i in enumerate(range(s[0][0], s[0][1], s[0][2])): + _arr = self.primary[i][slice(*s[1]), slice(*s[2])] + if n == 0: # allocate array + arr = np.empty((len(range(s[0][0], s[0][1], s[0][2])), _arr.shape[0], _arr.shape[1])) + arr[n] = _arr + if arr.shape[0] == 1: + arr = arr[0] + return np.squeeze(arr) + + def flatindices(self): + + # looks through databroker descriptor + # TODO: find a better way to search through the doc + i0 = 0 + for descriptor in self.descriptors: + try: + key = list(descriptor['configuration'].keys())[0] + if 'i0cycle' in descriptor['configuration'][key]['data']: + i0 = int(descriptor['configuration'][key]['data']['i0cycle']) + except KeyError: + pass + + nproj = len(self) + if i0 > 0: + indices = list(range(0, nproj, i0)) + if indices[-1] != nproj - 1: + indices.append(nproj - 1) + elif i0 == 0: + indices = [0, nproj - 1] + return indices + + @property + def rawdata(self): + return self[0] + + def getframe(self, frame=0): + self.data = self.primary[frame] + return self.data + + def close(self): + pass + + class StackImage(object): - """ - Class for displaying a Image Stack in a pyqtgraph ImageView and be able to scroll through the various Images + """Class for displaying a Image Stack in a pyqtgraph ImageView and be + able to scroll through the various Images + """ ndim = 3 @@ -875,14 +1021,16 @@ def asVolume(self, level=1): return vol def _getframe(self, frame=None): # keeps 3 frames in cache at most - if frame is None: frame = self.currentframe + if frame is None: + frame = self.currentframe if type(frame) is list and type(frame[0]) is slice: frame = 0 # frame[1].step self.currentframe = frame # print self._framecache if frame not in self._framecache: # del the first cached item - if len(self._framecache) > self._cachesize: del self._framecache[self._framecache.keys()[0]] + if len(self._framecache) > self._cachesize: + del self._framecache[list(self._framecache.keys())[0]] self._framecache[frame] = self._getimage(frame) return self._framecache[frame] @@ -1353,7 +1501,8 @@ class multifilediffimage2(diffimage2): ndim = 3 def __init__(self, filepaths, detector=None, experiment=None): - self.filepaths = sorted(list(filepaths)) + self.filepaths = filepaths + if not filepaths[0].startswith('DB:'): self.filepaths = sorted(list(self.filepaths)) self._currentframe = 0 super(multifilediffimage2, self).__init__(detector=detector, experiment=experiment) self._framecache = dict() @@ -1390,7 +1539,10 @@ def xvals(self, _): if self._xvals is None: timekey = config.activeExperiment.headermap['Timeline Axis'] if timekey: - self._xvals = np.array([float(self.iHeaders(i)[timekey]) for i in range(len(self.filepaths))]) + try: + self._xvals = np.array([float(self.iHeaders(i)[timekey]) for i in range(len(self.filepaths))]) + except KeyError: # TODO: allow DB headers to be keyed without .start + self._xvals = np.array([float(self.iHeaders(i).start[timekey]) for i in range(len(self.filepaths))]) else: self._xvals = np.arange(len(self.filepaths)) return self._xvals @@ -1464,11 +1616,11 @@ def __getitem__(self, item): class datadiffimage2(singlefilediffimage2): ndim = 2 - def __init__(self, data, detector=None, experiment=None): + def __init__(self, data, detector=None, experiment=None, rot90=3): super(datadiffimage2, self).__init__(filepath=None, detector=detector, experiment=experiment) - self._rawdata = np.rot90(data, 3) + self._rawdata = np.rot90(data, rot90) - # False scale rawdata to avoid log issues + # False scale rawdata when given floats less than 1 to avoid log issues if self._rawdata.max() <= 1: self._rawdata *= 2 ** 32 diff --git a/pipeline/msg.py b/pipeline/msg.py index c6303287..f0e0d06d 100644 --- a/pipeline/msg.py +++ b/pipeline/msg.py @@ -56,7 +56,7 @@ def logMessage(stuple,level=INFO,loggername=None,timestamp=None,image=None,suppr try: stdch.setLevel(level) except ValueError: - print stuple,level + print(stuple, level) logger.addHandler(stdch) if timestamp is None: timestamp = time.asctime() @@ -64,10 +64,10 @@ def logMessage(stuple,level=INFO,loggername=None,timestamp=None,image=None,suppr if type(stuple) is not tuple: stuple=[stuple] - stuple = (unicode(s) for s in stuple) + stuple = (str(s) for s in stuple) s = ' '.join(stuple) - m = timestamp +'\t'+unicode(s) + m = timestamp + '\t' + str(s) logger.log(level,m) if guilogcallable: @@ -76,9 +76,9 @@ def logMessage(stuple,level=INFO,loggername=None,timestamp=None,image=None,suppr global logbacklog logbacklog.append({'stuple':s,'level':level,'loggername':loggername,'timestamp':timestamp,'image':image}) try: - if not suppressreprint: print m + if not suppressreprint: print(m) except UnicodeEncodeError: - print 'A unicode string could not be written to console. Some logging will not be displayed.' + print('A unicode string could not be written to console. Some logging will not be displayed.') def flushbacklog(): global logbacklog diff --git a/pipeline/pathtools.py b/pipeline/path.py similarity index 84% rename from pipeline/pathtools.py rename to pipeline/path.py index 22f353b7..246be0bb 100644 --- a/pipeline/pathtools.py +++ b/pipeline/path.py @@ -1,9 +1,12 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals import os import string from PySide import QtGui import sys import re -import msg +from . import msg from appdirs import * user_config_dir=user_config_dir('xicam') @@ -40,6 +43,6 @@ def getRoot(): elif sys.platform == 'win32': return QtGui.QFileSystemModel().myComputer() else: - print 'WARNING: Unknown platform "' + sys.platform + '"' + print('WARNING: Unknown platform "' + sys.platform + '"') return None diff --git a/pipeline/peakfinding.py b/pipeline/peakfinding.py index 1ce26697..183caae9 100644 --- a/pipeline/peakfinding.py +++ b/pipeline/peakfinding.py @@ -84,7 +84,7 @@ def findpeaks(x, y): # TODO: Refactor this class into xicam module so I can get rid of pyside dependency -class peaktooltip: +class peaktooltip(object): def __init__(self, x, y, widget): self.q, self.I, self.width, self.index = findpeaks(x, y) self.scatterPoints = pg.PlotDataItem(self.q, self.I, size=10, pen=pg.mkPen(None), diff --git a/pipeline/peakfindingrem.py b/pipeline/peakfindingrem.py index a5564057..3a67e0f0 100644 --- a/pipeline/peakfindingrem.py +++ b/pipeline/peakfindingrem.py @@ -55,7 +55,7 @@ def peakdet(x, v, delta): ############### -class peaktooltip: +class peaktooltip(object): def __init__(self, x, y, widget): self.q, self.I, self.width, self.index = peakdet(x, y, 10) self.scatterPoints = pg.ScatterPlotItem(self.q, self.I, size=10, pen=pg.mkPen(None), diff --git a/pipeline/saxs_calibration.py b/pipeline/saxs_calibration.py index d924aa65..5fb9584f 100644 --- a/pipeline/saxs_calibration.py +++ b/pipeline/saxs_calibration.py @@ -67,7 +67,7 @@ def _error_function(parameter, arguments): geometry, d_spacings, maxima, selected_parameter = arguments mask = [sel in selected_parameter for sel in FIT_PARAMETER] param = numpy.array(get_fit2d(geometry)) - print 'update:',param + print('update:', param) param[numpy.array(mask)] = parameter[numpy.array(mask)] set_fit2d(geometry,*param) return peak_distance(geometry, d_spacings, maxima) diff --git a/pipeline/variation.py b/pipeline/variation.py index 533c89b2..0f2ad96d 100644 --- a/pipeline/variation.py +++ b/pipeline/variation.py @@ -42,7 +42,7 @@ def variation(operationindex, imga, imgb=None, imgc=None, roi=None): else: roi = 1 with np.errstate(divide='ignore'): - return variationoperators.operations.values()[operationindex](p, c, n, roi, None, None) + return list(variationoperators.operations.values())[operationindex](p, c, n, roi, None, None) except TypeError: msg.logMessage('Variation could not be determined for a frame.',msg.ERROR) else: diff --git a/pipeline/writer.py b/pipeline/writer.py index 6ed802d7..f382811b 100644 --- a/pipeline/writer.py +++ b/pipeline/writer.py @@ -43,7 +43,7 @@ def mergenexus(**kwargs): nxroot.data.thumbnail = kwargs['thumb'] # TODO: merge variation if not hasattr(nxroot.data, 'variation'): - nxroot.data.variation = kwargs['variation'].items() + nxroot.data.variation = list(kwargs['variation'].items()) if newfile: writenexus(nxroot, kwargs['path']) @@ -83,7 +83,7 @@ def jpeg(img): def blockshaped(arr, factor): firstslice = np.array_split(arr, arr.shape[0] // factor) - secondslice = map(lambda x: np.array_split(x, arr.shape[1] // factor, axis=1), firstslice) + secondslice = [np.array_split(x, arr.shape[1] // factor, axis=1) for x in firstslice] return np.array(secondslice) diff --git a/python3notes b/python3notes new file mode 100644 index 00000000..2030ef50 --- /dev/null +++ b/python3notes @@ -0,0 +1,5 @@ +Vispy: + - Change site.py 429 to fp = open(filename, "rU",encoding='utf-8') + +qtconsole: + - Change rich_jupyter_widget.py 64-65 to self._jpg_supported = 'jpeg' in QtGui.QImageReader.supportedImageFormats() \ No newline at end of file diff --git a/setup.py b/setup.py index 0460fee8..5288d2a0 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='1.2.25', + version='1.2.26', description='A synchrotron data analysis interface', long_description=long_description, @@ -88,7 +88,7 @@ install_requires=['scipy', 'Cython', 'pyFAI==0.16.0', 'h5py', 'PySide==1.2.2', 'pyqtgraph', 'QDarkStyle', 'Pillow', 'pyfits', 'PyOpenGL', 'PyYAML', 'qtconsole','tifffile','pysftp', 'requests','dask','distributed','appdirs','futures','scikit-image','imageio','vispy', - 'pypaws>=0.8.4','matplotlib', 'astropy'], + 'pypaws>=0.8.4', 'matplotlib', 'astropy', 'watchdog'], # setup_requires=['numpy', 'cython'], diff --git a/xicam/ROI.py b/xicam/ROI.py index 9048eabd..8ae12e85 100644 --- a/xicam/ROI.py +++ b/xicam/ROI.py @@ -153,7 +153,7 @@ def getArrayRegion(self, data, img, axes=(0, 1), returnMappedCoords=False, **kwd rr, cc = polygon(vecs[0], vecs[1]) mask = np.zeros_like(data) rrcc = [[r, c] for r, c in zip(rr, cc) if r < mask.shape[0] and c < mask.shape[1]] - rr, cc = zip(*rrcc) + rr, cc = list(zip(*rrcc)) mask[rr, cc] = 1 return (data * mask).T.copy() diff --git a/xicam/clientmanager.py b/xicam/clientmanager.py index 2e3ed7f5..d63d7e31 100644 --- a/xicam/clientmanager.py +++ b/xicam/clientmanager.py @@ -1,5 +1,3 @@ - - __author__ = "Luis Barroso-Luque" __copyright__ = "Copyright 2016, CAMERA, LBL, ALS" __credits__ = ["Ronald J Pandolfi", "Dinesh Kumar", "Singanallur Venkatakrishnan", "Luis Luque", "Alexander Hexemer"] @@ -22,6 +20,7 @@ sftp_client = client.sftp.SFTPClient globus_client = client.globus.GlobusClient ssh_client = client.ssh.SSHClient +DB_client = client.databrokerclient.DataBrokerClient # Singleton instance of spot_client spot_client = client.spot.SpotClient() @@ -30,6 +29,7 @@ sftp_clients = {} globus_clients = {} ssh_clients = {} +databroker_clients = {} def login_wrapper(client_login): @@ -69,6 +69,10 @@ def add_globus_client(endpoint, client, callback): callback(client) +def add_DB_client(host, client, callback): + databroker_clients[host] = client + callback(client) + def logout(client_obj, callback=None): """Logout client on a background thread""" # TODO remove clients from their corresponding dictionaries!!! diff --git a/xicam/config.py b/xicam/config.py index 6e036c60..f2e9b35d 100644 --- a/xicam/config.py +++ b/xicam/config.py @@ -84,7 +84,9 @@ def template(): {'name': 'Integration Bins (χ)', 'value': 1000, 'type': 'int','min':1}, {'name':'Image Load Rotations','value':0,'type':'int'}, {'name':'Image Load Transpose','value':False,'type':'bool'}, - {'name':'Ignored Modules','value':[],'type':'list'}]} + {'name': 'Ignored Modules', 'value': [], 'type': 'list'}, + {'name': 'Databroker FileStore Name', 'value': 'filestore-production-v1', 'type': 'str'}, + {'name': 'Databroker MetaDataStore Name', 'value': 'metadatastore-production-v1', 'type': 'str'}, ]} settings=settingstracker() diff --git a/xicam/debugtools.py b/xicam/debugtools.py index 6d7599ac..a36fea7d 100644 --- a/xicam/debugtools.py +++ b/xicam/debugtools.py @@ -1,4 +1,5 @@ # -*- coding: UTF-8 -*- +from __future__ import unicode_literals import time import inspect import pyqtgraph as pg diff --git a/xicam/dialogs.py b/xicam/dialogs.py index c2718492..77f6c07c 100644 --- a/xicam/dialogs.py +++ b/xicam/dialogs.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide import QtGui, QtCore from fabio import edfimage, tifimage import numpy as np diff --git a/xicam/models.py b/xicam/models.py index 880170cf..09ac1d71 100644 --- a/xicam/models.py +++ b/xicam/models.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide.QtCore import Qt from PySide import QtCore @@ -79,9 +80,9 @@ def data(self, index, role=Qt.DisplayRole): if role == Qt.DisplayRole: try: if index.column() == 0: - return self.propdata.keys()[index.row()] + return list(self.propdata.keys())[index.row()] if index.column() == 1: - return self.propdata.values()[index.row()] + return list(self.propdata.values())[index.row()] except Exception: return 0 diff --git a/xicam/plugins/EZPluginTest.py b/xicam/plugins/EZPluginTest.py index e1cce625..877a5ae4 100644 --- a/xicam/plugins/EZPluginTest.py +++ b/xicam/plugins/EZPluginTest.py @@ -1,4 +1,6 @@ -import base +from __future__ import absolute_import +from __future__ import unicode_literals +from . import base def runtest(): diff --git a/xicam/plugins/MOTD.py b/xicam/plugins/MOTD.py index d0483327..bebff4e9 100644 --- a/xicam/plugins/MOTD.py +++ b/xicam/plugins/MOTD.py @@ -1,9 +1,11 @@ -import base +from __future__ import absolute_import +from __future__ import unicode_literals +from . import base from PySide import QtGui import os from xicam import xglobals -import widgets +from . import widgets class MOTDPlugin(base.plugin): @@ -34,10 +36,14 @@ def openfiles(self, *args, **kwargs): xglobals.plugins['Viewer'].instance.activate() xglobals.plugins['Viewer'].instance.openfiles(*args, **kwargs) + def appendfiles(self, files): + xglobals.plugins['Viewer'].instance.activate() + xglobals.plugins['Viewer'].instance.openfiles(files) + def calibrate(self): xglobals.plugins['Viewer'].instance.activate() xglobals.plugins['Viewer'].instance.calibrate() def currentImage(self): xglobals.plugins['Viewer'].instance.activate() - xglobals.plugins['Viewer'].instance.currentImage() \ No newline at end of file + xglobals.plugins['Viewer'].instance.currentImage() diff --git a/xicam/plugins/__init__.py b/xicam/plugins/__init__.py index 1e33dac7..1ae7b3f6 100644 --- a/xicam/plugins/__init__.py +++ b/xicam/plugins/__init__.py @@ -13,18 +13,17 @@ modules = [] plugins = OrderedDict() -disabledatstart = ['FXS', 'SPOTH5', 'Library', 'XAS','EZTest', '3D Viewer', 'HipRMC', ] +disabledatstart = ['FXS', 'SPOTH5', 'Library', 'XAS', 'EZTest', '3D Viewer', 'HipRMC', 'Tomography'] def initplugins(placeholders): - import base + from . import base global plugins, modules packages = pkgutil.iter_modules(__path__) msg.logMessage(('packages:',packages),msg.DEBUG) for importer, modname, ispkg in packages: - msg.logMessage("Found plugin %s (is a package: %s)" % (modname, ispkg),msg.DEBUG) mod=safeimporter.import_module('.' + modname, 'xicam.plugins') if mod: @@ -52,7 +51,7 @@ def initplugins(placeholders): def buildactivatemenu(modewidget): menu = QtGui.QMenu('Plugins') - for pluginlink in plugins.values(): + for pluginlink in list(plugins.values()): if pluginlink.plugin.hidden: continue action = QtGui.QAction(pluginlink.name, menu) @@ -64,7 +63,7 @@ def buildactivatemenu(modewidget): return menu -class pluginlink(): +class pluginlink(object): def __init__(self, module, plugin, placeholders): self.plugin = plugin self.modulename = module.__name__ diff --git a/xicam/plugins/base.py b/xicam/plugins/base.py index 0f11305a..40c9a5a5 100644 --- a/xicam/plugins/base.py +++ b/xicam/plugins/base.py @@ -116,6 +116,9 @@ def openSelected(self, operation=None, operationname=None): def openfiles(self, files, operation=None, operationname=None): pass + def appendfiles(self, files): + pass + @property def isActive(self): return activeplugin == self @@ -143,7 +146,7 @@ def activate(self): global leftwidget, rightwidget # if these will become attributes then the check will need to be different if self.leftwidget is leftwidget: if self.leftwidget.count() > 1: - for idx in reversed(range(self.leftwidget.count() - 1)): + for idx in reversed(list(range(self.leftwidget.count() - 1))): self.leftwidget.removeTab(idx + 1) if hasattr(self, 'leftmodes'): for widget, icon in self.leftmodes: @@ -153,7 +156,7 @@ def activate(self): self.leftwidget.tabBar().hide() if self.rightwidget is rightwidget: - for idx in reversed(range(self.rightwidget.count())): + for idx in reversed(list(range(self.rightwidget.count()))): self.rightwidget.removeTab(idx) if hasattr(self, 'rightmodes'): for widget, icon in self.rightmodes: diff --git a/xicam/plugins/batch.py b/xicam/plugins/batch.py new file mode 100644 index 00000000..e69de29b diff --git a/xicam/plugins/cdsaxs/__init1__.py b/xicam/plugins/cdsaxs/__init1__.py new file mode 100644 index 00000000..6ba0c8c5 --- /dev/null +++ b/xicam/plugins/cdsaxs/__init1__.py @@ -0,0 +1,449 @@ +import __future__ +import os, sys, time +import simulation, fitting, cdsaxs +from scipy.fftpack import * +from numpy.fft import * +from scipy.signal import resample +import platform +import pyqtgraph as pg +from pyqtgraph import parametertree as pt +from pipeline import loader, hig, msg +import numpy as np +from xicam.plugins import base, widgets +import subprocess +from xicam import threads, ROI +from modpkgs import guiinvoker +from operator import itemgetter + +from PySide import QtGui, QtCore +from xicam import debugtools + +from pipeline.spacegroups import spacegroupwidget +from xicam import config + +from xicam.widgets.calibrationpanel import calibrationpanel +from pipeline import integration, center_approx + +import astropy + +from pyevolve import G1DList +from pyevolve import GSimpleGA +from pyevolve import Selectors, Consts +from pyevolve import Statistics +from pyevolve import DBAdapters +import pyevolve + +from random import randrange +import cPickle as pickle +import psutil +import multiprocessing +from collections import deque +from itertools import repeat +# import pandas as pd +import emcee +import deap.base as deap_base +from deap import creator, tools +from deap import cma as cmaes + + +class plugin(base.plugin): + name = "CDSAXS" + + def __init__(self, *args, **kwargs): + + self.centerwidget = QtGui.QTabWidget() + self.rightwidget = self.parametertree = pg.parametertree.ParameterTree() + self.topwidget = QtGui.QTabWidget() + self.centerwidget.setDocumentMode(True) + self.centerwidget.setTabsClosable(True) + self.centerwidget.tabCloseRequested.connect(self.tabClose) + self.centerwidget.currentChanged.connect(self.currentChanged) + self.bottomwidget = pg.ImageView() + + # Setup parametertree + self.param = pg.parametertree.Parameter.create(name='params', type='group', children=[ + {'name': 'test', 'type': 'group', 'children': [ + {'name': 'Phi_min', 'type': 'float'}, + {'name': 'Phi_max', 'type': 'float'}, + {'name': 'Phi_step', 'type': 'float'}, + {'name': 'H', 'type': 'float'}, + {'name': 'w0', 'type': 'float'}, + {'name': 'Beta', 'type': 'float'}, + {'name': 'Num_trap', 'type': 'float'}, + {'name': 'Run1', 'type': 'action'}]}, + {'name': 'test1', 'type': 'group', 'children': [ + {'name': 'H_fit', 'type': 'float', 'readonly': True}, + {'name': 'w0_fit', 'type': 'float', 'readonly': True}, + {'name': 'Beta_fit', 'type': 'float', 'readonly': True}, + {'name': 'f_val', 'type': 'float', 'readonly': True}]}]) + + self.parametertree.setParameters(self.param, showTop=False) + self.param.param('test', 'Run1').sigActivated.connect(self.fit) + + super(plugin, self).__init__(*args, **kwargs) + + def update_model(self, widget): + guiinvoker.invoke_in_main_thread(self.bottomwidget.setImage, widget.modelImage) + + def update_right_widget(self, widget): + H, LL, beta, f_val = widget.modelParameter + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'H_fit').setValue, H) + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'w0_fit').setValue, LL) + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'Beta_fit').setValue, beta) + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'f_val').setValue, f_val) + + def fit(self): + activeSet = self.getCurrentTab() + activeSet.setCurrentWidget(activeSet.CDModelWidget) + H, w0, Beta1, Num_trap = self.param['test', 'H'], self.param['test', 'w0'], self.param['test', 'Beta'], \ + self.param['test', 'Num_trap'] + fitrunnable = threads.RunnableMethod(self.getCurrentTab().fitting_test1, method_args=(H, w0, Beta1, Num_trap)) + threads.add_to_queue(fitrunnable) + + def openfiles(self, files, operation=None, operationname=None): + self.activate() + if type(files) is not list: + files = [files] + widget = widgets.OOMTabItem(itemclass=CDSAXSWidget, src=files, operation=operation, + operationname=operationname, plotwidget=self.bottomwidget, + toolbar=self.toolbar) + + self.centerwidget.addTab(widget, os.path.basename(files[0])) + self.centerwidget.setCurrentWidget(widget) + + Phi_min, Phi_max, Phi_step = self.param['test', 'Phi_min'], self.param['test', 'Phi_max'], self.param[ + 'test', 'Phi_step'] + fitrunnable = threads.RunnableMethod(self.getCurrentTab().loadRAW, method_args=(Phi_min, Phi_max, Phi_step)) + threads.add_to_queue(fitrunnable) + + def currentChanged(self, index): + for tab in [self.centerwidget.widget(i) for i in range(self.centerwidget.count())]: + tab.unload() + self.centerwidget.currentWidget().load() + self.getCurrentTab().sigDrawModel.connect(self.update_model) + self.getCurrentTab().sigDrawParam.connect(self.update_right_widget) + + def tabClose(self, index): + self.centerwidget.widget(index).deleteLater() + + def getCurrentTab(self): + if self.centerwidget.currentWidget() is None: return None + if not hasattr(self.centerwidget.currentWidget(), 'widget'): return None + return self.centerwidget.currentWidget().widget + + +class CDSAXSWidget(QtGui.QTabWidget): + sigDrawModel = QtCore.Signal(object) + sigDrawParam = QtCore.Signal(object) + + def __init__(self, src, *args, **kwargs): + super(CDSAXSWidget, self).__init__() + + self.CDRawWidget = CDRawWidget() + self.CDCartoWidget = CDCartoWidget() + self.CDModelWidget = CDModelWidget() + + self.addTab(self.CDRawWidget, 'RAW') + self.addTab(self.CDCartoWidget, 'Cartography') + self.addTab(self.CDModelWidget, 'Model') + + self.setTabPosition(self.South) + self.setTabShape(self.Triangular) + + self.src = src + + # self.loadRAW() + + def loadRAW(self, Phi_min=-45, Phi_max=45, Phi_step=1): + + # center = center_approx.center_approx(loader.loadimage(self.src[0])) + center = [552, 225] # Center CMS beamline + # center = [730, 488] #Center 733 + maxR, minR = 250, 0 + nb_pixel = maxR - minR + maxy, miny = -40, 40 + + data = [] + angles = [] + profiles = [] + x, y = np.indices(loader.loadimage(self.src[0]).shape) + x_1, y_1 = x - center[0], y - center[1] + rmincut = (y_1 > minR) + rmaxcut = (y_1 < maxR) + thetamincut = (x_1 > -10) + thetamaxcut = (x_1 < 10) + cutmask = (rmincut * rmaxcut * thetamincut * thetamaxcut).T + + # Use for 733 + ''' + for file in self.src: + img = np.rot90(loader.loadimage(file), 1) + self.wavelength = 0.12398 # not contained in the header of 733 + #wavelength = loader.loadparas(file)['Beamline Energy'] + self.SDD = 4 # not contained in the header of 733 + #SDD = loader.loadparas(file)['Beamline Energy'] + self.pixel_size = 172 * 10**-6 + #pixel_size = loader.loadparas(file)['title'] + I_0 = loader.loadparas(file)['Izero'] + angle = loader.loadparas(file)['Sample Rotation Stage'] + + profile = np.sum(cutmask * img, axis=1) + profiles.append(profile[center[1] + minR: center[1] + maxR]) + data.append(img) + angles.append(np.float(angle)) + + angles, data, profiles = zip(*sorted(zip(angles, data, profiles))) + Phi_min, Phi_max = min(angles), max(angles) + Phi_step = (max(angles) - min(angles))/(len(angles)-1) + + print(Phi_min, Phi_step) + ''' + + # Use for CMS beamline + for file in self.src: + img = np.rot90(loader.loadimage(file), 1) + profile = np.sum(cutmask * img, axis=1) + # profile = cutmask * img + profiles.append(profile[center[1] + minR: center[1] + maxR]) + # profiles.append(profile[center[1]:]) + data.append(img) + self.pixel_size, self.SDD, self.wavelength = 172 * 10 ** -6, 4., 0.095372 # CMS beamline + + data = np.stack(data) + data = np.log(data - data.min() + 1.) + + self.maskimage = pg.ImageItem(opacity=.25) + self.CDRawWidget.view.addItem(self.maskimage) + invmask = 1 - cutmask + self.maskimage.setImage( + np.dstack((invmask, np.zeros_like(invmask), np.zeros_like(invmask), invmask)).astype(np.float), opacity=.5) + self.CDRawWidget.setImage(data) + + # 1Conversion to q spqce + self.QxyiData = cdsaxs.generate_carto(profiles, nb_pixel, Phi_min, Phi_step, self.pixel_size, self.SDD, + self.wavelength, center[0]) + + # Intensity correction + substratethickness, substrateattenuation = 750 * 10 ** -6, 200 * 10 ** -6 + self.QxyiDatacor = cdsaxs.correc_Iexp(self.QxyiData, substratethickness, substrateattenuation) + + # interpolation and Plotting carthography + sampling_size = (400, 400) + # img = cdsaxs.inter_carto(self.QxyiData) + self.img, qk_shift = cdsaxs.interpolation(self.QxyiDatacor, sampling_size) + np.save('/Users/guillaumefreychet/Desktop/carto', self.img) + self.CDCartoWidget.setImage(self.img) + + # Definition of the variables + self.q = [] + self.Qxexp = [] + self.Qxfit = [] + self.Q__Z = [] + + # Extraction of nb of profile + their positions + profile_carto = np.mean(np.nan_to_num(self.img), axis=1) + self.q, self.Qxexp, self.Q__Z = cdsaxs.find_peaks(profile_carto, self.QxyiDatacor, self.wavelength, nb_pixel, + self.pixel_size, self.SDD) + + self.update_profile_ini() + self.SL_model1(10, 40, np.array([95])) + + def fitting_test1(self, H=10, LL=20, Beta1=70, Num_trap=5, DW=0.1, I0=1, Bk=0): # these are simp not fittingp + self.number_trapezoid = int(Num_trap) + + # algorithm = 'cmaes' + # algorithm = 'mcmc' + algorithm = 'pyevolve' + + if algorithm == 'cmaes': + self.fix_fitness = self.fix_fitness_cmaes + self.residual(np.zeros(len(initiale_value)), test='True') + self.cmaes(sigma=100, ngen=50, popsize=100, mu=10, N=len(initiale_value), restarts=0, verbose=False, + tolhistfun=5e-5, ftarget=None) + elif algorithm == 'mcmc': + self.fix_fitness = self.fix_fitness_mcmc + self.residual(np.zeros(len(initiale_value)), test='True') + self.mcmc(N=len(initiale_value), sigma=100, nsteps=100, nwalkers=4, use_mh='MH', parallel=False, seed=None, + verbose=True) + + elif algorithm == 'pyevolve': + self.fitting_test(H=10, LL=20, Beta1=92) + + def fitting_test(self, H=10, LL=20, Beta1=92): + + Beta = np.full((self.number_trapezoid), Beta1) + DW, I0, Bkg = 10, 10, 10 + + initiale_value = [] + initiale_value.append(int(DW)), initiale_value.append(int(I0)), initiale_value.append(int(Bkg)) + initiale_value.append(int(H)), initiale_value.append(int(LL)) + + for i in Beta: + initiale_value.append(int(i)) + + self.residual(initiale_value) + self.update_right_widget() + + self.best_score = 20 + self.num_param = int(len(initiale_value)) + + genome = G1DList.G1DList(self.num_param) + genome.setParams(rangemin=0, rangemax=1000) + genome.evaluator.set(self.residual) + + ga = GSimpleGA.GSimpleGA(genome) + ga.selector.set(Selectors.GRouletteWheel) + ga.setGenerations(100) + ga.setMinimax(Consts.minimaxType["minimize"]) + + # ga.terminationCriteria.set(GSimpleGA.ConvergenceCriteria) + # sqlite_adapter = DBAdapters.DBSQLite(identify="ex1", resetDB=True) + # ga.setDBAdapter(sqlite_adapter) + + ga.stepCallback.set(self.evolve_callback) + ga.evolve() + + print ga.bestIndividual() + best = ga.bestIndividual() + print(best.genomeList, best.score) + + self.residual(best.genomeList, 'True') + self.update_model() + self.modelParameter = 5 + 0.02 * best.genomeList[3], 20 + 0.04 * best.genomeList[4], 70 + 0.04 * \ + best.genomeList[5], best.score + self.update_right_widget() + + def evolve_callback(self, ga_engine): + generation = ga_engine.getCurrentGeneration() + if generation % 10 == 0: + print "Current generation: %d" % (generation,) + best = ga_engine.bestIndividual() + if best.score < self.best_score: + self.residual(best.genomeList, 'True') + self.update_profile('True') + self.modelParameter = 5 + 0.02 * best.genomeList[3], 20 + 0.04 * best.genomeList[4], 70 + 0.04 * \ + best.genomeList[5], best.score + H, LL = 5 + 0.02 * best.genomeList[3], 20 + 0.04 * best.genomeList[4] + Beta = np.zeros((self.num_param - 5)) + for i in range(0, self.number_trapezoid, 1): + Beta[i] = 70 + 0.04 * best.genomeList[5 + i] + Obj = np.rot90(simulation.multipyramid(H, LL, Beta, 500, 500), 3) + self.modelImage = Obj[150: 350, 350: 500] + self.update_model() + self.update_right_widget() + self.best_score = best.score + + # Need to be test to stop number points / goodnes of the fit/ convergence of the residual + def ConvergenceCriteria(self, ga_engine): + """ Terminate the evolution when the population have converged + Example: + ga_engine.terminationCriteria.set(GSimpleGA.ConvergenceCriteria) + """ + pop = ga_engine.getPopulation() + return pop[0] == pop[len(pop) - 1] + + def FitnessStatsCriteria(self, ga_engine): + """ Terminate the evoltion based on the fitness stats + Example: + ga_engine.terminationCriteria.set(GSimpleGA.FitnessStatsCriteria) + """ + stats = ga_engine.getStatistics() + if stats["fitMax"] == stats["fitMin"]: + if stats["fitAve"] == stats["fitMax"]: + return True + return False + + def update_model(self): + self.sigDrawModel.emit(self) + + def update_right_widget(self): + self.sigDrawParam.emit(self) + + def update_profile_ini(self): + for order in range(0, len(self.Qxexp), 1): + self.Qxexp[order] -= min(self.Qxexp[order]) + self.Qxexp[order] /= max(self.Qxexp[order]) + self.Qxexp[order] += order + 1 + + guiinvoker.invoke_in_main_thread(self.CDModelWidget.orders[order].setData, self.Q__Z[order], + np.log(self.Qxexp[order])) + + def update_profile(self, plot='False'): + for order in range(0, len(self.Qxexp), 1): + self.Qxexp[order] -= min(self.Qxexp[order]) + self.Qxexp[order] /= max(self.Qxexp[order]) + self.Qxexp[order] += order + 1 + + self.Qxfit[order] -= min(self.Qxfit[order]) + self.Qxfit[order] /= max(self.Qxfit[order]) + self.Qxfit[order] += order + 1 + + if plot == 'True': + guiinvoker.invoke_in_main_thread(self.CDModelWidget.orders[order].setData, self.Q__Z[order], + np.log(self.Qxexp[order])) + guiinvoker.invoke_in_main_thread(self.CDModelWidget.orders1[order].setData, self.Q__Z[order], + np.log(self.Qxfit[order])) + + # @debugtools.timeit + def residual(self, p, test='False', plot_mode=False): + DW = 0.0001 * p[0] + I0 = 0.01 * p[1] + Bkg = 0.01 * p[2] + H = 5 + 0.02 * p[3] + LL = 20 + 0.04 * p[4] + Beta = [] + + for i in range(5, len(p), 1): + Beta.append(70 + 0.04 * p[i]) + Beta = np.array(Beta) + + self.Qxfit = self.SL_model1(H, LL, Beta, DW, I0, Bkg) + self.update_profile(test) + + res = 0 + + for i in range(0, len(self.Qxexp), 1): + res += fitting.log_error(self.Qxexp[i], self.Qxfit[i]) + + return res + + def SL_model1(self, H, LL, Beta, DW_factor=0, I0=1, Bk=0): + qy = self.Q__Z + qz = self.q + langle = np.deg2rad(np.asarray(Beta)) + rangle = np.deg2rad(np.asarray(Beta)) + self.Qxfit = [] + self.Qxfitc = [] + for i in range(len(qz)): + self.Qxfit.append(simulation.stacked_trapezoids(qz[i], qy[i], 0, LL, H, langle, rangle)) + + self.Qxfitc = fitting.corrections_DWI0Bk(self.Qxfit, DW_factor, I0, Bk, self.q, self.Q__Z) + return self.Qxfitc + + +class CDRawWidget(pg.ImageView): + pass + + +class CDCartoWidget(pg.ImageView): + pass + + +class CDModelWidget(pg.PlotWidget): + def __init__(self): + super(CDModelWidget, self).__init__() + self.addLegend() + self.orders = [] + self.orders1 = [] + for i, color in enumerate('gyrbcmkgyr'): + self.orders.append(self.plot([], pen=pg.mkPen(color, width=2), name='Order ' + str(i))) + self.orders1.append( + self.plot([], pen=pg.mkPen(color, width=2, style=QtCore.Qt.DashLine), name='Order ' + str(i))) + + +class CDProfileWidget(pg.ImageView): + pass + +################################################################################################################################# +################################################################################################################################# +################################################################################################################################# diff --git a/xicam/plugins/cdsaxs/__init__.py b/xicam/plugins/cdsaxs/__init__.py new file mode 100644 index 00000000..f5242aa4 --- /dev/null +++ b/xicam/plugins/cdsaxs/__init__.py @@ -0,0 +1,751 @@ +import __future__ +import os, sys, time +from xicam.plugins.cdsaxs import simulation, fitting, cdsaxs +from scipy.fftpack import * +from numpy.fft import * +from scipy.signal import resample +import platform +import pyqtgraph as pg +from pyqtgraph import parametertree as pt +from pipeline import loader, hig, msg +import numpy as np +from xicam.plugins import base, widgets +import subprocess +from xicam import threads, ROI +from modpkgs import guiinvoker +import matplotlib.pyplot as plt + +from PySide import QtGui, QtCore + +from random import randrange +# import cPickle as pickle +import psutil +import multiprocessing +from collections import deque +from itertools import repeat +import emcee +import deap.base as deap_base +from deap import creator, tools +from deap import cma as cmaes + +creator.create('FitnessMin', deap_base.Fitness, weights=(-1.0,)) # want to minimize fitness +creator.create('Individual', list, fitness=creator.FitnessMin) + +exp_data = None +Qxfit = None +Q__z = None +q = None + + +def residual(p, test='False', plot_mode=False): + simp = fittingp_to_simp(p) + if simp is None: + return fix_fitness_cmaes(np.inf) + DW = simp[0] + I0 = simp[1] + Bkg = simp[2] + H = simp[3] + LL = simp[4] + Beta = simp[5:] + + Beta = np.array(Beta) + + print(H, LL, Beta) + + Qxfit = SL_model1(H, LL, Beta, DW, I0, Bkg) + + res = 0 + for i in range(0, len(exp_data), 1): + res += fitting.log_error(exp_data[i], Qxfit[i]) + + return fix_fitness_cmaes(res) + + +def fittingp_to_simp(fittingp): + # DW, I0, Bk, H, LL, *Beta[5] = simp + # values assume initial fittingp centered at 0 and std. dev. of 100 + # multiples = np.array([0.001, 0.1, 0.1, 0.2, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]) + multiples = [0.0001, 0.01, 0.01, 0.02, 0.03] + [0.04] * (len(adds) - 5) + simp = np.asarray(multiples) * np.asarray(fittingp) + adds + if np.any(simp[:5] < 0): + return None + if np.any(simp[5:] < 0) or np.any(simp[5:] > 180): + return None + return simp + + +def SL_model1(H, LL, Beta, DW_factor=0, I0=1, Bk=0): + qy = Q__z + qz = q + langle = np.deg2rad(np.asarray(Beta)) + rangle = np.deg2rad(np.asarray(Beta)) + Qxfit = [] + Qxfitc = [] + for i in range(len(qz)): + Qxfit.append(simulation.stacked_trapezoids(qz[i], qy[i], 0, LL, H, langle, rangle)) + + Qxfitc = fitting.corrections_DWI0Bk(Qxfit, DW_factor, I0, Bk, q, Q__z) + return Qxfitc + + +def fix_fitness_cmaes(fitness): + """cmaes accepts the individuals with the lowest fitness, doesn't matter degree to which they are lower""" + + return fitness + + +def fix_fitness_mcmc(fitness): + """ + Metropolis-Hastings criterion: acceptance probability equal to ratio between P(new)/P(old) + where P is proportional to probability distribution we want to find + for our case we assume that probability of our parameters being the best is proportional to a Gaussian centered at fitness=0 + where fitness can be log, abs, squared error, etc. + emcee expects the fitness function to return ln(P(new)), P(old) is auto-calculated + """ + c = 1e-1 # empirical factor to modify mcmc acceptance rate, makes printed fitness different than actual, higher c increases acceptance rate + return -fitness / c + # return -0.5 * fitness ** 2 / c ** 2 + + +class plugin(base.plugin): + name = "CDSAXS" + + def __init__(self, *args, **kwargs): + + self.centerwidget = QtGui.QTabWidget() + self.rightwidget = self.parametertree = pg.parametertree.ParameterTree() + self.topwidget = QtGui.QTabWidget() + self.centerwidget.setDocumentMode(True) + self.centerwidget.setTabsClosable(True) + self.centerwidget.tabCloseRequested.connect(self.tabClose) + self.centerwidget.currentChanged.connect(self.currentChanged) + self.bottomwidget = pg.ImageView() + + # Setup parametertree + + self.param = pg.parametertree.Parameter.create(name='params', type='group', children=[ + {'name': 'test', 'type': 'group', 'children': [ + {'name': 'Phi_min', 'type': 'float'}, + {'name': 'Phi_max', 'type': 'float'}, + {'name': 'Phi_step', 'type': 'float'}, + {'name': 'H', 'type': 'float'}, + {'name': 'w0', 'type': 'float'}, + {'name': 'Beta', 'type': 'float'}, + {'name': 'Num_trap', 'type': 'float'}, + {'name': 'Run1', 'type': 'action'}]}, + {'name': 'test1', 'type': 'group', 'children': [ + {'name': 'H_fit', 'type': 'float', 'readonly': True}, + {'name': 'w0_fit', 'type': 'float', 'readonly': True}, + {'name': 'Beta_fit', 'type': 'float', 'readonly': True}, + {'name': 'f_val', 'type': 'float', 'readonly': True}]}]) + + self.parametertree.setParameters(self.param, showTop=False) + self.param.param('test', 'Run1').sigActivated.connect(self.fit) + + super(plugin, self).__init__(*args, **kwargs) + + def update_model(self, widget): + guiinvoker.invoke_in_main_thread(self.bottomwidget.setImage, widget.modelImage) + + def update_right_widget(self, widget): + H, LL, beta, f_val = widget.modelParameter + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'H_fit').setValue, H) + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'w0_fit').setValue, LL) + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'Beta_fit').setValue, beta) + guiinvoker.invoke_in_main_thread(self.param.param('test1', 'f_val').setValue, f_val) + + def fit(self): + activeSet = self.getCurrentTab() + activeSet.setCurrentWidget(activeSet.CDModelWidget) + H, w0, Beta1, Num_trap = self.param['test', 'H'], self.param['test', 'w0'], self.param['test', 'Beta'], \ + self.param['test', 'Num_trap'] + fitrunnable = threads.RunnableMethod(self.getCurrentTab().fitting_test1, method_args=(H, w0, Beta1, Num_trap)) + threads.add_to_queue(fitrunnable) + + def openfiles(self, files, operation=None, operationname=None): + self.activate() + if type(files) is not list: + files = [files] + widget = widgets.OOMTabItem(itemclass=CDSAXSWidget, src=files, operation=operation, + operationname=operationname, plotwidget=self.bottomwidget, + toolbar=self.toolbar) + + self.centerwidget.addTab(widget, os.path.basename(files[0])) + self.centerwidget.setCurrentWidget(widget) + + Phi_min, Phi_max, Phi_step = self.param['test', 'Phi_min'], self.param['test', 'Phi_max'], self.param[ + 'test', 'Phi_step'] + fitrunnable = threads.RunnableMethod(self.getCurrentTab().loadRAW, method_args=(Phi_min, Phi_max, Phi_step)) + threads.add_to_queue(fitrunnable) + + def currentChanged(self, index): + for tab in [self.centerwidget.widget(i) for i in range(self.centerwidget.count())]: + tab.unload() + self.centerwidget.currentWidget().load() + self.getCurrentTab().sigDrawModel.connect(self.update_model) + self.getCurrentTab().sigDrawParam.connect(self.update_right_widget) + + def tabClose(self, index): + self.centerwidget.widget(index).deleteLater() + + def getCurrentTab(self): + if self.centerwidget.currentWidget() is None: return None + if not hasattr(self.centerwidget.currentWidget(), 'widget'): return None + return self.centerwidget.currentWidget().widget + + +class CDSAXSWidget(QtGui.QTabWidget): + sigDrawModel = QtCore.Signal(object) + sigDrawParam = QtCore.Signal(object) + + def __init__(self, src, *args, **kwargs): + super(CDSAXSWidget, self).__init__() + + self.CDRawWidget = CDRawWidget() + self.CDCartoWidget = CDCartoWidget() + self.CDModelWidget = CDModelWidget() + + self.addTab(self.CDRawWidget, 'RAW') + self.addTab(self.CDCartoWidget, 'Cartography') + self.addTab(self.CDModelWidget, 'Model') + + self.setTabPosition(self.South) + self.setTabShape(self.Triangular) + + self.src = src + + # self.loadRAW() + + def loadRAW(self, Phi_min=-45, Phi_max=45, Phi_step=1): + center = [552, 225] + maxR, minR = 250, 0 + nb_pixel = maxR - minR + maxy, miny = -40, 40 + + data = [] + profiles = [] + x, y = np.indices(loader.loadimage(self.src[0]).shape) + x_1, y_1 = x - center[0], y - center[1] + rmincut = (y_1 > minR) + rmaxcut = (y_1 < maxR) + thetamincut = (x_1 > -10) + thetamaxcut = (x_1 < 10) + cutmask = (rmincut * rmaxcut * thetamincut * thetamaxcut).T + + for file in self.src: + img = np.rot90(loader.loadimage(file), 1) + profile = np.sum(cutmask * img, axis=1) + # profile = cutmask * img + profiles.append(profile[center[1] + minR: center[1] + maxR]) + # profiles.append(profile[center[1]:]) + data.append(img) + + data = np.stack(data) + data = np.log(data - data.min() + 1.) + + self.maskimage = pg.ImageItem(opacity=.25) + self.CDRawWidget.view.addItem(self.maskimage) + invmask = 1 - cutmask + self.maskimage.setImage( + np.dstack((invmask, np.zeros_like(invmask), np.zeros_like(invmask), invmask)).astype(np.float), opacity=.5) + self.CDRawWidget.setImage(data) + + # 1Conversion to q spqce + pixel_size, sample_detector_distance, wavelength = 172 * 10 ** -6, 5., 0.09184 + self.QxyiData = cdsaxs.generate_carto(profiles, nb_pixel, Phi_min, Phi_step, pixel_size, + sample_detector_distance, wavelength, center[0]) + + # Intensity correction + substratethickness, substrateattenuation = 700 * 10 ** -6, 200 * 10 ** -6 + self.QxyiDatacor = cdsaxs.correc_Iexp(self.QxyiData, substratethickness, substrateattenuation) + + # interpolation and Plotting carthography + sampling_size = (400, 400) + # img = cdsaxs.inter_carto(self.QxyiData) + self.img, qk_shift = cdsaxs.interpolation(self.QxyiDatacor, sampling_size) + np.save('/Users/guillaumefreychet/Desktop/carto_cxro.npy', self.img) + self.CDCartoWidget.setImage(self.img) + + # Definition of the variables + self.q = [] + self.Qxexp = [] + self.Qxfit = [] + self.Q__Z = [] + + # Extraction of nb of profile + their positions + profile_carto = np.mean(np.nan_to_num(self.img), axis=1) + self.q, self.Qxexp, self.Q__Z = cdsaxs.find_peaks(profile_carto, self.QxyiDatacor, wavelength, nb_pixel, + pixel_size, sample_detector_distance) + + np.save('/Users/guillaumefreychet/Desktop/test', self.Qxexp) + self.update_profile_ini() + self.SL_model1(10, 40, np.array([95])) + self.maxres = 0 + + def fitting_test1(self, H=10, LL=20, Beta1=70, Num_trap=5, DW=0.1, I0=1, Bk=0): # these are simp not fittingp + + self.number_trapezoid = int(Num_trap) + beta = [] + for i in range(0, self.number_trapezoid, 1): + beta.append(Beta1) + Beta = np.array(beta) + + initiale_value = [DW, I0, Bk] + initiale_value.append(int(H)) + initiale_value.append(int(LL)) + + for i in Beta: + initiale_value.append(int(i)) + + self.adds = np.asarray(initiale_value) + + # set globals + global q, Q__z, exp_data, adds, Qxfit + + adds = self.adds + exp_data = self.Qxexp + q = self.q + Q__z = self.Q__Z + + self.fix_fitness = fix_fitness_cmaes + self.residual(np.zeros(len(initiale_value)), test='True') + # self.cmaes(sigma=200, ngen=200, popsize=100, mu=10, N=len(initiale_value), restarts=0, verbose=False, tolhistfun=5e-5, ftarget=None) + self.cmaes(sigma=100, ngen=100, popsize=100, mu=10, N=len(initiale_value), restarts=0, verbose=False, + tolhistfun=5e-5, ftarget=None) + self.residual(self.best_uncorr, test='False') + self.residual(self.best_uncorr, test='True') + + print(self.best_uncorr) + print(self.best_corr) + + print('OK') + # initiale_value1 = [self.best_corr[0], self.best_corr[1], self.best_corr[2], self.best_corr[3], self.best_corr[4], self.best_corr[5], self.best_corr[6], self.best_corr[7], self.best_corr[8], self.best_corr[9]] + # self.adds = np.asarray(initiale_value1) + ''' + self.fix_fitness = fix_fitness_mcmc + self.mcmc(N=len(self.best_corr), sigma=1000, nsteps=1000, nwalkers=100, use_mh='MH', parallel=True, seed=None, + verbose=True) + #self.residual(self.best_corr, test='True') + print('Done') + ''' + ''' + elif algorithm == 'mcmc': + self.fix_fitness = self.fix_fitness_mcmc + self.residual(np.zeros(len(initiale_value)), test='True') + self.mcmc(N=len(initiale_value), sigma=100, nsteps=100, nwalkers=4, use_mh='MH', parallel=False, seed=None, verbose=True) + ''' + + def update_model(self): + self.sigDrawModel.emit(self) + + def update_right_widget(self): + self.sigDrawParam.emit(self) + + def update_profile_ini(self): + for order in range(0, len(self.Qxexp), 1): + self.Qxexp[order] -= min(self.Qxexp[order]) + self.Qxexp[order] /= max(self.Qxexp[order]) + self.Qxexp[order] += order + 1 + + guiinvoker.invoke_in_main_thread(self.CDModelWidget.orders[order].setData, self.Q__Z[order], + np.log(self.Qxexp[order])) + + def update_profile(self, plot='False'): + for order in range(0, len(self.Qxexp), 1): + self.Qxexp[order] -= min(self.Qxexp[order]) + self.Qxexp[order] /= max(self.Qxexp[order]) + self.Qxexp[order] += order + 1 + + self.Qxfit[order] -= min(self.Qxfit[order]) + self.Qxfit[order] /= max(self.Qxfit[order]) + self.Qxfit[order] += order + 1 + + if plot == 'True': + guiinvoker.invoke_in_main_thread(self.CDModelWidget.orders[order].setData, self.Q__Z[order], + np.log(self.Qxexp[order])) + guiinvoker.invoke_in_main_thread(self.CDModelWidget.orders1[order].setData, self.Q__Z[order], + np.log(self.Qxfit[order])) + + # @debugtools.timeit + def residual(self, p, test='False', plot_mode=False): + simp = fittingp_to_simp(p) + if simp is None: + return fix_fitness_cmaes(np.inf) + DW = simp[0] + I0 = simp[1] + Bkg = simp[2] + H = simp[3] + LL = simp[4] + Beta = simp[5:] + + Beta = np.array(Beta) + + self.Qxfit = SL_model1(H, LL, Beta, DW, I0, Bkg) + self.update_profile(test) + + res = 0 + + for i in range(0, len(self.Qxexp), 1): + res += fitting.log_error(self.Qxexp[i], self.Qxfit[i]) + + return fix_fitness_cmaes(res) + + def SL_model1(self, H, LL, Beta, DW_factor=0, I0=1, Bk=0): + qy = self.Q__Z + qz = self.q + langle = np.deg2rad(np.asarray(Beta)) + rangle = np.deg2rad(np.asarray(Beta)) + self.Qxfit = [] + self.Qxfitc = [] + for i in range(len(qz)): + self.Qxfit.append(simulation.stacked_trapezoids(qz[i], qy[i], 0, LL, H, langle, rangle)) + + self.Qxfitc = fitting.corrections_DWI0Bk(self.Qxfit, DW_factor, I0, Bk, self.q, self.Q__Z) + return self.Qxfitc + + @staticmethod + def fix_fitness_cmaes(fitness): + """cmaes accepts the individuals with the lowest fitness, doesn't matter degree to which they are lower""" + return fitness, + + def fittingp_to_simp(self, fittingp): + # DW, I0, Bk, H, LL, *Beta[5] = simp + # values assume initial fittingp centered at 0 and std. dev. of 100 + multiples = [0.0001, 0.01, 0.01, 0.02, 0.03] + [0.04] * (len(self.adds) - 5) + simp = np.asarray(multiples) * np.asarray(fittingp) + self.adds + if np.any(simp[:5] < 0): + return None + if np.any(simp[5:] < 0) or np.any(simp[5:] > 180): + return None + return simp + + def cmaes(self, sigma, ngen, popsize, mu, N, restarts, verbose, tolhistfun, ftarget, restart_from_best=False): + """Modified from deap/algorithms.py to return population_list instead of final population and use additional termination criteria + + Returns: + population_list: list of (list of individuals (lists), length popsize), length ngen + logbook: list of dicts, length ngen, contains stats for each generation + """ + toolbox = deap_base.Toolbox() + toolbox.register('evaluate', self.residual) + # parallel = multiprocessing.cpu_count() + # pool = multiprocessing.Pool(parallel) + # toolbox.register('map', pool.map) + # last_time = time.perf_counter() + process = psutil.Process() + print('{} CPUs in node'.format(multiprocessing.cpu_count())) + print('pid:{}'.format(os.getpid())) + print(psutil.virtual_memory()) + halloffame = tools.HallOfFame(1) + stats = tools.Statistics(lambda ind: ind.fitness.values) + stats.register('avg', lambda x: np.mean(np.asarray(x)[np.isfinite(x)]) if np.asarray(x)[np.isfinite( + x)].size != 0 else None) + stats.register('std', lambda x: np.std(np.asarray(x)[np.isfinite(x)]) if np.asarray(x)[np.isfinite( + x)].size != 0 else None) + stats.register('min', lambda x: np.min(np.asarray(x)[np.isfinite(x)]) if np.asarray(x)[np.isfinite( + x)].size != 0 else None) + stats.register('max', lambda x: np.max(np.asarray(x)[np.isfinite(x)]) if np.asarray(x)[np.isfinite( + x)].size != 0 else None) + stats.register('fin', lambda x: np.sum(np.isfinite(x)) / np.size(x)) + # stats.register('cumtime', lambda x: time.perf_counter() - last_time) + stats.register('rss_MB', lambda x: process.memory_info().rss / 1048576) + stats.register('vms_MB', lambda x: process.memory_info().vms / 1048576) + logbook = tools.Logbook() + logbook.header = ['gen', 'nevals'] + (stats.fields if stats else []) + population_list = [] + kwargs = {'lambda_': popsize if popsize is not None else int(4 + 3 * np.log(N))} + if mu is not None: + kwargs['mu'] = mu + initial_individual = [0] * N + morestats = {} + morestats['sigma_gen'] = [] + morestats['axis_ratio'] = [] # ratio of min and max scaling at each generation + morestats['diagD'] = [] # scaling of each parameter at each generation (eigenvalues of covariance matrix) + morestats['ps'] = [] + allbreak = False + checkpoint_num = 0 + + for restart in range(restarts + 1): + if allbreak: + break + if restart != 0: + kwargs['lambda_'] *= 2 + print('Doubled popsize') + if restart_from_best: + initial_individual = halloffame[0] + # type of strategy: (parents, children) = (mu/mu_w, popsize), selection takes place among offspring only + strategy = cmaes.Strategy(centroid=initial_individual, sigma=sigma, **kwargs) + # The CMA-ES One Plus Lambda algorithm takes a initialized parent as argument + # parent = creator.Individual(initial_individual) + # parent.fitness.values = toolbox.evaluate(parent) + # strategy = cmaes.StrategyOnePlusLambda(parent=parent, sigma=sigma, lambda_=popsize) + toolbox.register('generate', strategy.generate, creator.Individual) + toolbox.register('update', strategy.update) + + last_best_fitnesses = deque(maxlen=10 + int(np.ceil(30 * N / kwargs['lambda_']))) + cur_gen = 0 + # fewer generations when popsize is doubled (unless fixed ngen is specified) + ngen_ = ngen if ngen is not None else int(100 + 50 * (N + 3) ** 2 / kwargs['lambda_'] ** 0.5) + while cur_gen < ngen_: + cur_gen += 1 + sys.stdout.flush() + # Generate a new population + population = toolbox.generate() + population_list.append(population) + # Evaluate the individuals + fitnesses = toolbox.map(toolbox.evaluate, population) + for ind, fit in zip(population, fitnesses): + ind.fitness.values = (fit,) # tuple of length 1 + + halloffame.update(population) + # if cur_gen % 10 == 0: # print best every 10 generations + # best = np.copy(halloffame[0]) + # for i in range(len(best)): + # best[i] = self.scaling[i][1] * halloffame[0][i] + self.correction[i] + # print(*['{0}:{1:.3g}'.format(i, j) for i, j in zip(self.labels, best)], sep=', ') + + # Update the strategy with the evaluated individuals + toolbox.update(population) + + record = stats.compile(population) if stats is not None else {} + logbook.record(gen=cur_gen, nevals=len(population), **record) + if verbose: + print(logbook.stream) + morestats['sigma_gen'].append(strategy.sigma) + morestats['axis_ratio'].append(max(strategy.diagD) ** 2 / min(strategy.diagD) ** 2) + morestats['diagD'].append(strategy.diagD ** 2) + morestats['ps'].append(strategy.ps) + + last_best_fitnesses.append(record['min']) + if (ftarget is not None) and record['min'] <= ftarget: + print('Iteration terminated due to ftarget criterion after {} gens'.format(cur_gen)) + allbreak = True + break + if (tolhistfun is not None) and (len(last_best_fitnesses) == last_best_fitnesses.maxlen) and ( + max(last_best_fitnesses) - min(last_best_fitnesses) < tolhistfun): + print('Iteration terminated due to tolhistfun criterion after {} gens'.format(cur_gen)) + break + if os.path.exists('break'): + print('Iteration terminated due to user after {} gens'.format(cur_gen)) + break + if os.path.exists('allbreak'): + print('Iteration terminated due to user after {} gens'.format(cur_gen)) + allbreak = True + break + if os.path.exists('checkpoint{}'.format(checkpoint_num)): + # saves current state of self as pickle and continues + self.logbook, self.morestats, self.strategy = logbook, morestats, strategy + + self.minfitness_each_gen = self.logbook.select('min') + self.best_uncorr = halloffame[0] + self.best_fitness = self.best_uncorr.fitness.values[0] + + # simulate best individual one more time to print fitness and save sim_list + # optionally plot sim and exp, plot trapezoids of best individual + # self.fitness_individual(self.best_uncorr, plot_on=False, print_fitness=True) + residual(self.Qxexp, self.q, self.Q__Z)(self.best_uncorr) + + # make population dataframe, order of rows is first generation for all children, then second generation for all children... + # make and print best individual series + population_array = np.array( + [list(individual) for generation in population_list for individual in generation]) + fitness_array = np.array( + [individual.fitness.values[0] for generation in population_list for individual in generation]) + self.make_population_frame_best(population_array, fitness_array) + print(self.best) + filename = 'checkpoint{}.pickle'.format(checkpoint_num) + with open(filename, 'wb') as f: + pickle.dump(self, f, pickle.HIGHEST_PROTOCOL) + print('saved to ' + os.path.join(os.getcwd(), filename)) + checkpoint_num += 1 + else: + print('Iteration terminated due to ngen criterion after {} gens'.format(cur_gen)) + + # pool.close() + self.logbook = logbook + self.morestats = morestats + self.strategy = strategy + + self.minfitness_each_gen = self.logbook.select('min') + self.best_uncorr = halloffame[0] # np.abs(halloffame[0]) + self.best_fitness = halloffame[0].fitness.values[0] + self.best_corr = fittingp_to_simp(self.best_uncorr) + self.residual(self.best_corr, test='True') + print(self.best_corr, self.best_fitness) + # make population dataframe, order of rows is first generation for all children, then second generation for all children... + self.population_array = np.array( + [list(individual) for generation in population_list for individual in generation]) + self.population_array = self.fittingp_to_simp(self.population_array) + self.fitness_array = np.array( + [individual.fitness.values[0] for generation in population_list for individual in generation]) + # self.population_frame = pd.DataFrame(np.column_stack((self.population_array, self.fitness_array))) + + def mcmc(self, N, sigma, nsteps, nwalkers, use_mh=False, parallel=True, seed=None, verbose=True): + """Fit with emcee package's implementation of MCMC algorithm and place into instance of self + Calls fitness_individual many times, then calls make_population_frame_best + + Attributes: + best_uncorr: best uncorrected individual + best_fitness: scalar + minfitness_each_gen: length ngen + sampler: instance of emcee.Sampler with detailed output of algorithm + + Args: + self: instance of Run + sigma: array or scalar, initial standard deviation for each parameter + nsteps: number of steps + nwalkers: number of walkers + use_mh: True for Metropolis-Hastings proposal and ensemble sampler, False for ensemble sampler, 'MH' for Metropolis-Hastings proposal and sampler + parallel: False for no parallel, True for cpu_count() processes, or int to specify number of processes, or 'scoop' for cluster + plot_on: whether to plot fitness, best trapezoids and sim and exp scattering + seed: seed for random number generator + """ + + def do_verbose(i, sampler): + if (i % 100) == 0: + print(i) + if hasattr(sampler, 'acceptance_fraction'): + print('Acceptance fraction: ' + str(np.mean(sampler.acceptance_fraction))) + else: + print('Acceptance fraction: ' + str(np.mean([sampler.acceptance_fraction for sampler in sampler]))) + sys.stdout.flush() + if (i % 1000) == 0: + process = psutil.Process() + # print('time elapsed: {} min'.format((time.perf_counter() - last_time) / 60)) + print('rss_MB: {}'.format(process.memory_info().rss / 1048576)) + print('vms_MB: {}'.format(process.memory_info().vms / 1048576)) + + def get_sampler(a): + walker_num, N, sigma, nsteps, residual, verbose = a + cov = np.identity(N) * sigma ** 2 + sampler = emcee.MHSampler(cov.copy(), cov.shape[0], residual, args=[False, False]) + for i, _ in enumerate(sampler.sample(np.zeros(N), None, None, iterations=nsteps)): + if verbose and (walker_num == 0): + do_verbose(i, sampler) + return sampler + + ''' + # set globals + global adds + adds = self.adds + ''' + + c = 1e-1 # empirical factor to modify mcmc acceptance rate, makes printed fitness different than actual, higher c increases acceptance rate + # last_time = time.perf_counter() + print('{} CPUs in node'.format(multiprocessing.cpu_count())) + print('pid:{}'.format(os.getpid())) + print(psutil.virtual_memory()) + self.nsteps = nsteps + self.nwalkers = nwalkers + if seed is None: + seed = randrange(2 ** 32) + self.seed = seed + np.random.seed(seed) + self.fix_fitness = fix_fitness_mcmc + + if hasattr(sigma, '__len__'): + self.sigma = sigma + else: + self.sigma = [sigma] * N + + if parallel == 'scoop': + if use_mh != 'MH': + raise NotImplementedError + self.parallel = multiprocessing.cpu_count() + from scoop import futures + map_MH = futures.map + elif parallel is True: + self.parallel = multiprocessing.cpu_count() + pool = multiprocessing.Pool(self.parallel) + map_MH = pool.map + + self.parallel = 1 + map_MH = map + + if use_mh == 'MH': + samplers = list(map_MH(get_sampler, zip(range(nwalkers), repeat(N), self.sigma, repeat(nsteps), + repeat(residual), repeat(verbose)))) + chain = np.dstack(sampler.chain for sampler in samplers) + s = chain.shape + flatchain = np.transpose(chain, axes=[0, 2, 1]).reshape(s[0] * s[2], s[1]) + lnprobability = np.vstack(sampler.lnprobability for sampler in samplers) + flatlnprobability = lnprobability.transpose().flatten() + self.minfitness_each_gen = np.min(-lnprobability * c, axis=0) + else: + print('{} parameters'.format(N)) + if use_mh: + individuals = [np.zeros(N) for _ in range(nwalkers)] + mh_proposal = emcee.utils.MH_proposal_axisaligned(self.sigma) + sampler = emcee.EnsembleSampler( + nwalkers, N, residual, args=[False, False], threads=self.parallel) + for i, _ in enumerate( + sampler.sample(individuals, None, None, iterations=nsteps, mh_proposal=mh_proposal)): + if verbose: + do_verbose(i, sampler) + else: + individuals = [[np.random.normal(loc=0, scale=s) for s in self.sigma] for _ in range(nwalkers)] + sampler = emcee.EnsembleSampler( + nwalkers, N, residual, args=[False, False], threads=self.parallel) + for i, _ in enumerate(sampler.sample(individuals, None, None, iterations=nsteps)): + if verbose: + do_verbose(i, sampler) + s = sampler.chain.shape + flatchain = np.transpose(sampler.chain, axes=[1, 0, 2]).reshape(s[0] * s[1], s[2]) + flatlnprobability = sampler.lnprobability.transpose().flatten() + self.minfitness_each_gen = np.min(-sampler.lnprobability * c, axis=0) + + if 'pool' in locals(): + pool.close() + + # flatchain has shape (nwalkers * nsteps, N) + # flatlnprobability has shape (nwalkers * nsteps,) + # flatchain and flatlnprobability list first step of all walkers, then second step of all walkers... + + # sampler.flatchain and sampler.flatlnprobability (made by package) list all steps of first walker, then all steps of second walker... + # but we don't want that + + flatfitness = -flatlnprobability * c + best_index = np.argmin(flatfitness) + self.best_fitness = flatfitness[best_index] + self.best_uncorr = flatchain[best_index] + self.best_corr = self.fittingp_to_simp(self.best_uncorr) + self.residual(self.best_uncorr, test='True') + # can't make sampler attribute before run_mcmc, pickling error + self.sampler = samplers if use_mh == 'MH' else sampler + self.population_array = self.fittingp_to_simp(flatchain) + ''' + self.population_frame = pd.DataFrame(np.column_stack((self.population_array, flatfitness))) + gen_start = 0 + gen_stop = len(flatfitness) + gen_step = 1 + popsize = int(self.population_frame.shape[0] / len(flatfitness)) + index = [] + for i in range(gen_start, gen_stop, gen_step): + index.extend(list(range(i * popsize, (i + 1) * popsize))) + resampled_frame = self.population_frame.iloc[index] + self.stats = resampled_frame.describe() + self.stats.to_csv('C:/Users/cdl/Desktop/test.csv') + ''' + + +class CDRawWidget(pg.ImageView): + pass + + +class CDCartoWidget(pg.ImageView): + pass + + +class CDModelWidget(pg.PlotWidget): + def __init__(self): + super(CDModelWidget, self).__init__() + self.addLegend() + self.orders = [] + self.orders1 = [] + for i, color in enumerate('gyrbcmkgyr'): + self.orders.append(self.plot([], pen=pg.mkPen(color), name='Order ' + str(i))) + self.orders1.append(self.plot([], pen=pg.mkPen(color), name='Order ' + str(i))) + + +class CDProfileWidget(pg.ImageView): + pass diff --git a/xicam/plugins/cdsaxs/cdsaxs.py b/xicam/plugins/cdsaxs/cdsaxs.py new file mode 100644 index 00000000..fd288ac5 --- /dev/null +++ b/xicam/plugins/cdsaxs/cdsaxs.py @@ -0,0 +1,217 @@ +import numpy as np +from scipy.interpolate import LinearNDInterpolator +from astropy.modeling import models, fitting + + +def get_exp_values(qxyi, cut_val): + print(cut_val) + delta = 0.001 + dtype = [('qx', np.float32), ('qy', np.float32), ('i', np.float32)] + Sqxyi = [] + for v in qxyi: + qx, qy, i, phi = v + Sqxyi.append((qx, qy, i)) + Qi = np.array(Sqxyi, dtype) + SQi = np.sort(Qi, order='qy') + + binf, bsup = cut_val - delta, cut_val + delta + idx = np.where((SQi['qx'] > binf) * (SQi['qx'] < bsup)) # selection contraints by qy vals + + return SQi['i'][idx], SQi['qy'][idx] + + +def generate_carto(profiles, nb_pixel, Phi_min, Phi_step, pixel_size, sample_detector_distance, wavelength, center_x): + nv = np.zeros([np.shape(profiles)[1], 4], dtype=np.float32) + QxyiData = np.zeros([np.shape(profiles)[1], 4], dtype=np.float32) + for i in range(0, np.shape(profiles)[0], 1): + phi = np.radians(Phi_min + i * Phi_step) + q = [0] * np.shape(profiles)[1] + qx = [0] * np.shape(profiles)[1] + qz = [0] * np.shape(profiles)[1] + for j in range(0, np.shape(profiles)[1], 1): + q[j] = (2 * np.pi / wavelength) * np.arctan( + j * nb_pixel / np.shape(profiles)[1] * pixel_size / sample_detector_distance) + qx[j] = q[j] * np.cos(phi + 2 * np.arcsin(q[j] * wavelength / (4 * np.pi))) + qz[j] = q[j] * np.sin(phi + 2 * np.arcsin(q[j] * wavelength / (4 * np.pi))) + nv[:, 0] = qx + nv[:, 1] = qz + nv[:, 2] = profiles[i] + nv[:, 3] = phi + QxyiData = np.vstack((QxyiData, nv)) + return QxyiData + + +# Correction of the footprint and substrate attenuation // Addition of sample size/sample attenuation and polarization +def correc_Iexp(Qxyi, substratethickness, substrateattenuation): + footprintcorr = 'True' + abscorr = 'True' + samplesizecorr = 'False' + fwhm, sample_size = 1, 1 + for i in range(0, len(Qxyi[0]), 1): + footprintfactor = np.cos(Qxyi[i, 3]) if footprintcorr else 1 + absfactor = np.exp( + -substratethickness * substrateattenuation * (1 - 1 / (Qxyi[i, 3] + 0.000000001))) if abscorr else 1 + Qxyi[i, 2] *= absfactor * footprintfactor + return Qxyi + + +def inter_carto(qxyi): + # Reverse map carthography + qxyi = qxyi[1167:] + qy = qxyi[:, 0] + qz = qxyi[:, 1] + val = qxyi[:, 2] + angles = qxyi[:, 3] + + # get angles + a, i, n = np.unique(angles, return_index=True, return_counts=True) + inds = dict(zip(a, i)) + if np.sum(n - n[0]) > 0: + raise ValueError('One of the angles has too many Intensity values') + + # calculate number of columns and rows + ncol = n[0] + amax = np.abs(a).max() + nrow = np.int(ncol * np.sin(np.deg2rad(amax)) + 1) + nrow = 2 * nrow + 1 + + # setup output image + img = np.zeros((nrow, ncol)) + u, v = np.indices((nrow, ncol)) + u -= nrow // 2 + + # temp = np.round(np.rad2deg(np.arctan2(u, v)), decimals=1) + angle = np.round(2 * np.rad2deg(np.arctan2(u, v))) / 2 + radius = np.round(np.sqrt(u ** 2 + v ** 2)).astype(int) + + it = np.nditer(img, flags=['multi_index'], op_flags=['writeonly']) + while not it.finished: + y, x = it.multi_index + ang = angle[y, x] + rad = min(radius[y, x], ncol - 1) + if ang in inds: + i = inds[ang] + rad + it[0] = val[i] + it.iternext() + + return img + + +def interpolation(qxyi, sampling_size=(400, 400)): + roi_loc = (sampling_size[0] / 2., sampling_size[1] / 2.) + roi_size = 400 + img = np.zeros((roi_size, roi_size)) + + qj = np.floor( + ((qxyi[:, 0] - qxyi[:, 0].min()) / (qxyi[:, 0] - qxyi[:, 0].min()).max()) * (sampling_size[0] - 1)).astype( + np.int32) + qk = np.floor(((qxyi[:, 1].ravel() - qxyi[:, 1].min()) / (qxyi[:, 1] - qxyi[:, 1].min()).max()) * ( + sampling_size[1] - 1)).astype(np.int32) + I = qxyi[:, 2].ravel() + + # Area of the cartography to interpolate + selected_idx = [] + assert qj.size == qk.size, 'uncorrect size for q{x,y} index vector' + for i in xrange(qj.size): + if -qk[i] / 2 < qj[i] and qj[i] <= roi_loc[0] + roi_size and roi_loc[1] - roi_size < qk[i] and qk[i] <= roi_loc[ + 1] + roi_size: + selected_idx.append(i) + + qj_shifted = qj[selected_idx] - qj[selected_idx].min() + qk_shifted = qk[selected_idx] - qk[selected_idx].min() + + print(qxyi[:, 1].min()) + print(qxyi[:, 1].max()) + + Isel = I[selected_idx] + for i, isel in enumerate(Isel): + img[qj_shifted[i], qk_shifted[i]] += isel + ''' + plt.imshow((img)) + plt.show() + ''' + qk_shift = qk[selected_idx].min() + + to_fill = [] + to_fill = np.array(np.where(img == 0)).T + + interp_from = np.where(img != 0) + origin = (roi_size / 2) + + interpolator = LinearNDInterpolator(interp_from, img[interp_from]) + cpt = 0 + + # for p in to_fill: + # img[p[0], p[1]] += interpolator(p[0], p[1]) + + for p in to_fill: + if abs((p[1] - origin) / 2) >= (p[0]): + continue + try: + img[p[0], p[1]] += interpolator(p[0], p[1]) + except ValueError: + cpt += 1 + pass + + log_possible = np.where(img != 'nan') + img[log_possible] = np.log(img[log_possible] - img[log_possible].min() + 1.) + return img, qk_shift + + +def find_peaks(profile_carto, QxyiData, wavelength, nb_pixel, pixel_size, sample_detector_distance): + Int1 = np.amax(profile_carto) + ind1 = np.int(np.where(profile_carto == Int1)[0]) + pos_gauss = np.linspace(ind1 - 20, ind1 + 20, 41, dtype=np.int32) + + g_init = models.Gaussian1D(amplitude=Int1, mean=ind1, stddev=1.) + fit_g = fitting.LevMarLSQFitter() + g = fit_g(g_init, pos_gauss, profile_carto[pos_gauss]) + ind1 = g.mean.value + limit_ampli = 0.001 * Int1 + + ind = [] + ind.append(ind1) + cnt = 0 + i = 2 + finish = 'False' + + # Put this into a function + smarter way to do it + while ((i + 1) * ind[0] < np.shape(profile_carto)[0]) and (finish != 'True'): + ind_imp1 = i * ind[0] + pos_gauss1 = np.linspace(ind_imp1 - 20, ind_imp1 + 20, 41, dtype=np.int32) + g_init_1 = models.Gaussian1D(amplitude=limit_ampli, mean=ind_imp1, stddev=1.) + g_1 = fit_g(g_init_1, pos_gauss1, profile_carto[pos_gauss1]) + ind_imp1 = g_1.mean.value + if g_1.amplitude.value < limit_ampli and (i + 1) * ind[0] < np.shape(profile_carto)[0]: + ind_imp2 = (i + 1) * ind[0] + pos_gauss2 = np.linspace(ind_imp2 - 20, ind_imp2 + 20, 41, dtype=np.int32) + g_init_2 = models.Gaussian1D(amplitude=limit_ampli, mean=ind_imp2, stddev=1.) + g_2 = fit_g(g_init_2, pos_gauss2, profile_carto[pos_gauss2]) + ind_imp2 = g_2.mean.value + + if g_2.amplitude < limit_ampli: + finish = 'True' + + else: + ind.append(ind_imp1) + ind.append(ind_imp2) + i = i + 2 + + elif g_1.amplitude.value < limit_ampli and (i + 1) * ind[0] > np.shape(profile_carto)[0]: + finish = 'True' + + else: + ind.append(ind_imp1) + i = i + 1 + + # print(len(ind)) + q = [] + Qxexp = [] + Q__Z = [] + for i in range(0, len(ind), 1): + if i != 6: + q.append((2 * np.pi / wavelength) * np.sin( + np.arctan((((ind[i] * 500 / 400) * nb_pixel / 500) * (pixel_size / sample_detector_distance))))) + a, b = get_exp_values(QxyiData, q[i]) + Qxexp.append(a), Q__Z.append(b) + return q, Qxexp, Q__Z diff --git a/xicam/plugins/cdsaxs/fitting.py b/xicam/plugins/cdsaxs/fitting.py new file mode 100644 index 00000000..c6176143 --- /dev/null +++ b/xicam/plugins/cdsaxs/fitting.py @@ -0,0 +1,138 @@ +import numpy as np + +''' + + +def pso(initiale_value, lower_bnds, upper_bnds): + + + lower_bnds, upper_bnds = [], [] + for i in initiale_value: + lower_bnds.append(int(i - 10)) + upper_bnds.append(int(i + 10)) + xopt, fopt = pso(self.residual, lower_bnds, upper_bnds) + print(xopt, fopt) + self.residual(xopt) + print(opt.message) + +def py_evol(num_param, num_generation, qxs, qzs): + best_score = 0 + genome = G1DList.G1DList(num_param) + genome.setParams(rangemin=0, rangemax=1000) + genome.evaluator.set(self.residual) + + ga = GSimpleGA.GSimpleGA(genome) + ga.selector.set(Selectors.GRouletteWheel) + ga.setGenerations(num_generation) + + #ga.stepCallback.set(evolve_callback) + ga.evolve() + + #print ga.bestIndividual() + best = ga.bestIndividual() + #print(best.genomeList, best.score) + + return best + + +def evolve_callback(ga_engine): + generation = ga_engine.getCurrentGeneration() + if generation % 10 == 0: + print "Current generation: %d" % (generation,) + best = ga_engine.bestIndividual() + if best.score > best_score: + best_score = best.score + self.residual(best.genomeList, 'True') + + self.modelParameter = 5 + 0.02 * best.genomeList[2], 20 + 0.04 * best.genomeList[3], 70 + 0.04 * best.genomeList[4], best.score + H, LL, Beta = 5 + 0.02 * best.genomeList[2], 20 + 0.04 * best.genomeList[3], np.asarray([70 + 0.04 * best.genomeList[4], 70 + 0.04 * best.genomeList[5], 70 + 0.04 * best.genomeList[6], 70 + 0.04 * best.genomeList[7], 70 + 0.04 * best.genomeList[8]]) + Obj = simulation.multipyramid(H, LL, Beta, 500, 500) + Obj_plot = np.rot90(Obj, 3) + + +def residual(p, test = 'False', plot_mode=False): + DW = 0.0001 * p[0] + I0 = 0.01 * p[1] + Bk = 0.01 * p[2] + H = 5 + 0.02 * p[3] + LL = 20 + 0.04 * p[4] + Beta = [] + + for i in range(4, len(p), 1): + Beta.append(50 + 0.08 * p[i]) + + Beta = np.array(Beta) + + Qxfit = __init__.SL_model1(H, LL, Beta, DW_factor=DW, I0=I0, Bk=Bk) + + Qxfit = corrections_DWI0Bk(Qxfit, DW_factor=DW, I0=I0, Bk=Bk, qxs, qzs) + + #self.Qxfit = correc_Isim(DW, I_scale, 1) + + res = 0 + res_min = 1000 + + for i in range(0, len(self.Qxexp), 1): + res += np.sqrt(sum((self.Qxfit[i] - self.Qxexp[i])**2) / sum((self.Qxexp[i])**2)) + + maxres = min(maxres, res) + return res +''' + + +def corrections_DWI0Bk(Is, DW_factor, I0, Bk, qxs, qzs): + I_corr = [] + for I, qx, qz in zip(Is, qxs, qzs): + DW_array = np.exp(-(np.asarray(qx) ** 2 + np.asarray(qz) ** 2) * DW_factor ** 2) + I_corr.append(np.asarray(I) * DW_array * I0 + Bk) + return I_corr + + +def log_error(exp_I_array, sim_I_array): + error = np.nansum(np.abs(np.log10(exp_I_array) - np.log10(sim_I_array))) / np.count_nonzero(~np.isnan(exp_I_array)) + return error + + +def abs_error(exp_I_array, sim_I_array): + error = np.nansum(np.abs(exp_I_array - sim_I_array) / np.nanmax(exp_I_array)) / np.count_nonzero( + ~np.isnan(exp_I_array)) + return error + + +def squared_error(exp_I_array, sim_I_array): + error = np.nansum((exp_I_array - sim_I_array) ** 2 / np.nanmax(exp_I_array) ** 2) / np.count_nonzero( + ~np.isnan(exp_I_array)) + return error + + +''' +def fittingp_to_simp(self, fittingp): + # DW, I0, Bk, H, LL, *Beta[5] = simp + # values assume initial fittingp centered at 0 and std. dev. of 100 + multiples = np.array([0.0001, 0.01, 0.01, 0.02, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04]) + simp = multiples * np.asarray(fittingp) + self.adds + if np.any(simp[:5] < 0): + return None + if np.any(simp[5:] < 0) or np.any(simp[5:] > 180): + return None + return simp + +@staticmethod +def fix_fitness_cmaes(fitness): + """cmaes accepts the individuals with the lowest fitness, doesn't matter degree to which they are lower""" + return fitness, + +@staticmethod +def fix_fitness_mcmc(fitness): + """ + Metropolis-Hastings criterion: acceptance probability equal to ratio between P(new)/P(old) + where P is proportional to probability distribution we want to find + for our case we assume that probability of our parameters being the best is proportional to a Gaussian centered at fitness=0 + where fitness can be log, abs, squared error, etc. + emcee expects the fitness function to return ln(P(new)), P(old) is auto-calculated + """ + c = 1e-1 # empirical factor to modify mcmc acceptance rate, makes printed fitness different than actual, higher c increases acceptance rate + return -fitness / c + # return -0.5 * fitness ** 2 / c ** 2 + +''' diff --git a/xicam/plugins/cdsaxs/simulation.py b/xicam/plugins/cdsaxs/simulation.py new file mode 100644 index 00000000..ee8aa07e --- /dev/null +++ b/xicam/plugins/cdsaxs/simulation.py @@ -0,0 +1,72 @@ +#! /usr/bin/env python + +import numpy as np + + +def trapezoid_form_factor(qy, qz, y1, y2, langle, rangle, h): + m1 = np.tan(langle) + m2 = np.tan(np.pi - rangle) + t1 = qy + m1 * qz + t2 = qy + m2 * qz + with np.errstate(divide='ignore'): + t3 = m1 * np.exp(-1j * qy * y1) * (1 - np.exp(-1j * h / m1 * t1)) / t1 + t4 = m2 * np.exp(-1j * qy * y2) * (1 - np.exp(-1j * h / m2 * t2)) / t2 + ff = (t4 - t3) / qy + return ff + + +def stacked_trapezoids(qy, qz, y1, y2, height, langle, rangle=None): + if not isinstance(langle, np.ndarray): + raise TypeError('anlges should be array') + if rangle is not None: + if not langle.size == rangle.size: + raise ValueError('both angle array are not of same size') + else: + rangle = langle + + ff = np.zeros(qz.shape, dtype=np.complex) + # loop over all the angles + for i in range(langle.size): + shift = height * i + left, right = langle[i], rangle[i] + ff += trapezoid_form_factor(qy, qz, y1, y2, left, right, height) * np.exp(-1j * shift * qz) + m1 = np.tan(left) + m2 = np.tan(np.pi - right) + y1 += height / m1 + y2 += height / m2 + + return np.absolute(ff) ** 2 + + +def multipyramid(h, w, a, nx, ny): + if nx % 2 == 1: + nx += 1 + + n2 = nx / 2 + x0 = w / 2 + y0 = 0 + + if not type(a) is np.ndarray: + raise TypeError('Side-wall angle must be numpy array for multipyramid') + + # setup output array + img = np.zeros((ny, n2)) + y, x = np.mgrid[0:ny, 0:n2] + + a = np.deg2rad(a) + for i in range(a.size): + A = np.sin(np.pi - a[i]) + B = -np.cos(np.pi - a[i]) + C = -(A * x0 + B * y0) + d = A * x + B * y + C + + # update (x0, y0) + y0 = (i + 1) * h + x0 = -(B * y0 + C) / A + + # update image + mask = np.logical_and(y >= i * h, y < (i + 1) * h) + mask = np.logical_and(d < 0, mask) + img[mask] = 1 + + return np.hstack((np.fliplr(img), img)) diff --git a/xicam/plugins/hiprmc/obsolete/RmcView.py b/xicam/plugins/hiprmc/obsolete/RmcView.py deleted file mode 100644 index 0ab15742..00000000 --- a/xicam/plugins/hiprmc/obsolete/RmcView.py +++ /dev/null @@ -1,144 +0,0 @@ -import numpy as np # Import important packages -from pyqtgraph.Qt import QtCore, QtGui -import pyqtgraph as pg -import glob -from PIL import Image -import os -import re - - -def calcscale(imv): # Defines calcscale function - """ - - """ - image = imv.getProcessedImage() - - scale = imv.scalemax / float(image[imv.currentIndex].shape[1]) - return scale - - -class imagetimeline(list): # Sets up the image so it will fin the the viewer - - @property - def shape(self): # Defines shape function - return (len(self), self[-1].shape[0], self[-1].shape[0]) - - def __getitem__(self, item): # Defines getitem function - return list.__getitem__(self, item) - - @property - def ndim(self): # Defines ndim functionq - return 3 - - @property - def size(self): # Defines size functon - return sum(map(np.size, self)) - - @property - def max(self): - return max(map(np.max, self)) - - @property - def min(self): - return min(map(np.min, self)) - - @property - def dtype(self): - return type(self[0][0, 0]) - - -class TimelineView(pg.ImageView): # Beginnings the class Timelineview - def __init__(self, scalemax, *args, **kwargs): - super(TimelineView, self).__init__(*args, **kwargs) - self.scalemax = scalemax - - def quickMinMax(self, data): # Defines quickMinMax functon - return min(map(np.min, data)), max(map(np.max, data)) - - def updateImage(self, autoHistogramRange=True): # Defines updateImage functon - if self.image is None: - return - - scale = calcscale(self) # Scales the image - image = self.getProcessedImage() - - if autoHistogramRange: # Sets the Y axis intensity bar - self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) - if self.axes['t'] is None: - self.imageItem.updateImage(image) - else: - self.ui.roiPlot.show() - self.imageItem.updateImage(image[self.currentIndex]) - - self.imageItem.resetTransform() # Resets the scale up below - self.imageItem.scale(scale, scale) # Scales up by the factor of scale - print 'Image shape' + str(image.shape) - print 'Scale set to: ' + str(scale) - - -class rmcView(QtGui.QTabWidget): - def __init__(self, root, loadingfactors=None): - super(rmcView, self).__init__() - - - paths = glob.glob(os.path.join(root, - '[0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9]_model.tif')) - - indices = dict(zip(paths, [re.findall('\d{4}', os.path.basename(path)) for path in paths])) - - tiles = dict() - - for path, ind in indices.iteritems(): - if int(ind[1]) in tiles: - tiles[int(ind[1])].append(path) - else: - tiles[int(ind[1])] = [path] - - for tile, loadingfactor in zip(tiles, loadingfactors): - images = [] - paths = sorted(tiles[tile]) - for path in paths: - img = Image.open(path).convert('L') - img = np.array(img) - - print path # Prints the path - print img.shape # Prints the shape of the array - - images.append(img) - - data = imagetimeline(images) - - sizemax = max(map(np.shape, data))[0] - - view = TimelineView(sizemax) - view.setImage(data) - - scale = calcscale(view) # Sets up the scale - view.imageItem.resetTransform() - view.imageItem.scale(scale, scale) - view.autoRange() - view.getHistogramWidget().setHidden(True) - view.ui.roiBtn.setHidden(True) - view.ui.menuBtn.setHidden(True) - if loadingfactors is None: - self.addTab(view, u"Tile " + str(tile + 1)) - else: - self.addTab(view, str(loadingfactor)) - - -if __name__ == '__main__': # Start Qt event loop unless running in interactive mode. - import sys - - app = QtGui.QApplication([]) # Launches an app - root = '/Users/holden' - - win = QtGui.QMainWindow() # Create window with two ImageView widgets - win.resize(800, 800) - win.setWindowTitle('pyqtgraph example: Hiprmc ') - - win.setCentralWidget(rmcView(root,0.111)) - - win.show() - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtGui.QApplication.instance().exec_() diff --git a/xicam/plugins/hiprmc/obsolete/rmc.py b/xicam/plugins/hiprmc/obsolete/rmc.py deleted file mode 100644 index 8a8f19b6..00000000 --- a/xicam/plugins/hiprmc/obsolete/rmc.py +++ /dev/null @@ -1,165 +0,0 @@ -import os -import RmcView -import time -import subprocess -from PySide import QtGui, QtCore -from pipeline import hig, loader - -class gui(): - def __init__(self, ui): - self.ui = ui - - # self.ui.rmcbutton.clicked.connect(self.showrmc) - - self.ui.rmcaddfiles.clicked.connect(self.addfiles) - self.ui.rmcremovefiles.clicked.connect(self.removefiles) - self.ui.rmcExecute.clicked.connect(self.execute) - self.ui.rmcAddloadingfactor.clicked.connect(self.addloadingfactor) - self.ui.rmcSubtractloadingfactor.clicked.connect(self.subtractloadingfactor) - self.ui.rmcopen.clicked.connect(self.open) - self.ui.rmcreset.clicked.connect(self.reset) - self.ui.rmcreset_2.clicked.connect(self.reset2) - # self.ui.rmcRunRemotely.stateChanged.connect(self.runRemotely) - - def open(self): - Outputdirectory = QtGui.QFileDialog.getExistingDirectory(self.ui, "Select an Output directory") - self.ui.rmcoutput.setText(Outputdirectory) - - def reset(self): - self.ui.rmcoutput.setText("") - - def reset2(self): - self.ui.rmcoutput.setText("") - self.ui.rmcLoadingfactors.clear() - self.ui.rmcSteps.setValue(99) - self.ui.rmcScalefactor.setValue(1) - self.ui.rmcModlestartsize.setValue(1) - self.ui.rmcRunName.setText("") - self.ui.rmcinputpaths.clear() - - def showrmc(self): - """ - switch to timeline view - """ - self.ui.viewmode.setCurrentIndex(3) - self.ui.sidemode.setCurrentIndex(1) - - def addfiles(self): - paths, ok = QtGui.QFileDialog.getOpenFileNames(self.ui, 'Add files to RMC', os.curdir, - "*.tif *.edf *.fits *.tif") - self.ui.rmcinputpaths.addItems(paths) - - def removefiles(self): - for index in self.ui.rmcinputpaths.selectedIndexes(): - item = self.ui.rmcinputpaths.takeItem(index.row()) - item = None - - def addloadingfactor(self): - loadingfactor, _ = QtGui.QInputDialog.getDouble(self.ui, "Loading Factor", "Enter a loading factor:", value=0, - minValue=-0, maxValue=1000, decimals=3) - newItem = QtGui.QListWidgetItem() - newItem.setText(str(loadingfactor)) - self.ui.rmcLoadingfactors.addItem(newItem) - - def subtractloadingfactor(self): - for index in self.ui.rmcLoadingfactors.selectedIndexes(): - item = self.ui.rmcLoadingfactors.takeItem(index.row()) - item = None - - def runRemotely(self): - - ["scp test_input.hig ablair@parratt.lbl.gov:~/ " - , "ssh -t ablair@parratt.lbl.gov "" "" "] - - RemoteProcess = subprocess.Popen("scp test_input.hig ablair@parratt.lbl.gov:~/ ") - # print RemoteProcess - - - def execute(self): - steps = self.ui.rmcSteps.value() - scalefactor = self.ui.rmcScalefactor.value() - modlestartsize = self.ui.rmcModlestartsize.value() - - loadingfactors = [] - - for item in iterAllItems(self.ui.rmcLoadingfactors): - loadingfactors.append(item.text()) - - rip = self.ui.rmcinputpaths - inputpaths = [rip.item(index).text() for index in xrange(rip.count())] - - tiles = len(loadingfactors) - - for path in inputpaths: - d = {'hipRMCInput': {'instrumentation': {'inputimage': path, - 'imagesize': loader.loadimage(path).shape[0: 2], - 'numtiles': tiles, - 'loadingfactors': loadingfactors, - # 'maskimage': "data/mask.tif" - }, # optional - 'computation': { - 'runname': os.path.join(self.ui.rmcoutput.text(), self.ui.rmcRunName.text()), - 'modelstartsize': [modlestartsize, modlestartsize], - 'numstepsfactor': steps, - 'scalefactor': scalefactor}}} - h = hig.hig(**d) - h.write("test_input.hig") - self.rmcdaemon = RMCThread() - self.rmcdaemon.sig_finished.connect(self.displayoutput) - self.rmcdaemon.start() - # test_call = 1 - # if test_call == 1: - # self.execute() # Trying to get the window to delete after a new window is opened. - - # while executeNumber < 1: - # executeNumber += 1 - # if executeNumber >= 2: - # break - - def displayoutput(self, exitcode): - def checkforresults(self): - os.path.exists(self.ui.rmcoutput.text()) - - def checkforresults2(self): - if os.listdir(self.ui.rmcoutput.text()) == []: - return False - else: - return True - - if checkforresults(self) is True and checkforresults2(self) is True: - - print "Finished", exitcode - - path = os.path.join(self.ui.rmcoutput.text(), self.ui.rmcRunName.text()) - - loadingfactors = [] - - for item in iterAllItems(self.ui.rmcLoadingfactors): - loadingfactors.append(item.text()) - - layout = self.ui.rmclayout - layout.addWidget(RmcView.rmcView(path, loadingfactors)) - - -def iterAllItems(w): - for i in range(w.count()): - yield w.item(i) - - -class RMCThread(QtCore.QThread): - sig_finished = QtCore.Signal(int) - def run(self): - process = subprocess.Popen(['./hiprmc', 'test_input.hig']) - while process.poll() is None: - time.sleep(0.5) - self.sig_finished.emit(process.poll()) - - # os.system("./hiprmc test_input.hig") - - def stop(self): - self.exiting = True - print ("thread stop - %s" % self.exiting) - - def __del__(self): - self.exiting = True - self.wait() diff --git a/xicam/plugins/library.py b/xicam/plugins/library.py index bc1b68b3..9241e407 100644 --- a/xicam/plugins/library.py +++ b/xicam/plugins/library.py @@ -1,7 +1,7 @@ from PySide import QtGui import sys -import base -import viewer +from . import base +from . import viewer class LibraryPlugin(base.plugin): @@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs): self.toolbar = None - super(plugin, self).__init__(*args, **kwargs) + super(LibraryPlugin, self).__init__(*args, **kwargs) # self.centerwidget.sigOpenFile.connect(viewer.plugininstance.openfiles) @@ -205,7 +205,7 @@ def __init__(self, path, parentwindow, nameoverride=None): self.fileicon = QtGui.QImage() self.fileicon.load('xicam/gui/post-360412-0-09676400-1365986245.png') - print 'Library widget generated for ' + path + print('Library widget generated for ' + path) super(thumbwidgetitem, self).__init__() self.parentwindow = parentwindow self.setObjectName('thumb') @@ -239,7 +239,7 @@ def __init__(self, path, parentwindow, nameoverride=None): self.thumb.shape[1], QtGui.QImage.Format_Indexed8) except Exception as ex: - print ex.message + print(ex.message) self.image = self.fileicon diff --git a/xicam/plugins/log.py b/xicam/plugins/log.py index c4f1805f..d1e1cd7d 100644 --- a/xicam/plugins/log.py +++ b/xicam/plugins/log.py @@ -1,4 +1,6 @@ -import base +from __future__ import absolute_import +from __future__ import unicode_literals +from . import base from PySide import QtGui, QtCore from xicam import xglobals from pipeline import msg diff --git a/xicam/plugins/tomography/reconpkg.py b/xicam/plugins/tomography/reconpkg.py deleted file mode 100644 index 6bbfb777..00000000 --- a/xicam/plugins/tomography/reconpkg.py +++ /dev/null @@ -1,28 +0,0 @@ -import importlib -from pipeline import msg -import tomopy - -# Packages to import -PACKAGE_LIST = ['astra', 'dxchange', 'tomocam', 'pyF3D'] - -# Dictionary with package names as keys and package objects as values -packages = {} - -for name in PACKAGE_LIST: - try: - package = importlib.import_module(name) - packages[name] = package - msg.logMessage('{} module loaded'.format(name), level=20) - except ImportError as ex: - msg.logMessage('{} module not available'.format(name), level=30) # 30 -> warning' - -# Tomopy is actually necessary, so its important that its a hard import -packages['tomopy'] = tomopy - -# Add the extra functions -import pipelinefunctions -packages['pipelinefunctions'] = pipelinefunctions - -if 'tomocam' in packages: - import mbir - packages['mbir'] = mbir \ No newline at end of file diff --git a/xicam/plugins/tomography/tomocam/XT_Common.py b/xicam/plugins/tomography/tomocam/XT_Common.py deleted file mode 100644 index ccda86e2..00000000 --- a/xicam/plugins/tomography/tomocam/XT_Common.py +++ /dev/null @@ -1,40 +0,0 @@ -import numpy as np -import tomopy -import afnumpy as afnp - -def padmat(x,siz,value): -# function y=padmat(x,size,vals) -# pads x to size with constant values (vals), -# centers the matrix in the middle. - n=siz[0] - if siz.size < 2: - m=n - elif siz.size == 2: - m=siz[1] - else: - (n,m) = siz.shape - - [N,M]=x.shape - - y=np.zeros((n,m))+value - y[0:N,0:M]=x - y=np.roll(np.roll(y,np.int16(np.fix((n-N)/2)),axis=0),np.int16(np.fix((m-M)/2)),axis=1) - return y - - -def padmat_v2(x,siz,value,y): -# function y=padmat(x,size,vals) -# pads x to size with constant values (vals), -# centers the matrix in the middle. - n=siz[0] - if siz.size < 2: - m=n - elif siz.size == 2: - m=siz[1] - else: - (n,m) = siz.shape - - [N,M]=x.shape - y[0:N,0:M]=x - y=np.roll(np.roll(y,afnp.int16(np.fix((n-N)/2)),axis=0),np.int16(afnp.fix((m-M)/2)),axis=1) - return y diff --git a/xicam/plugins/tomography/tomocam/XT_ForwardModel.py b/xicam/plugins/tomography/tomocam/XT_ForwardModel.py deleted file mode 100644 index 9d5c34f3..00000000 --- a/xicam/plugins/tomography/tomocam/XT_ForwardModel.py +++ /dev/null @@ -1,156 +0,0 @@ -#Fuction to implement the tomographic forward and back-projection kernel in python - -import gnufft -import math -import numpy as np -import afnumpy as afnp -import afnumpy.fft as af_fft -import scipy.special as sc_spl #For bessel functions -import tomopy -import matplotlib.pyplot as plt -from XT_Common import padmat - -def forward_project(x,params): - #inputs : x - afnumpy array containing the complex valued image - # : params - a list containing all parameters for the NUFFT - - qxyXrxy = (params['fft2Dshift']*af_fft.fft2(x*params['deapod_filt']*params['fft2Dshift'])) #real space (rxy) to Fourier space (qxy) - - qtXqxy = gnufft.polarsample(params['gxy'],qxyXrxy,params['gkblut'],params['scale'],params['k_r'])/(params['Ns']**2) #Fourier space to polar coordinates interpolation (qxy to qt) - - rtXqt = params['fftshift1D']((af_fft.ifft(afnp.array(params['fftshift1D_center'](qtXqxy).T))).T)*params['sino_mask'] #Polar cordinates to real space qt to rt - #TODO : Remove afnp array allocation - - return rtXqt - -def back_project(y,params): - #inputs : y - afnumpy array containing the complex valued array with size of the sinogram - # : params - a list containing all parameters for the NUFFT - - qtXrt = params['giDq'].reshape((params['Ns'],1))*(params['fftshift1Dinv_center'](af_fft.fft((params['fftshift1D'](y)).T).T)) #Detector space rt to Fourier space qt - - #Polar to cartesian -# qxyXqt = gnufft.polarsample_transpose(params['gxy'],qtXrt,params['grid'],params['gkblut'],params['scale'],params['k_r']) *(afnp.pi/(2*params['Ntheta']*params['Ns']**2)) - qxyXqt = gnufft.polarsample_transpose(params['gxy'],qtXrt,params['grid'],params['gkblut'],params['scale'],params['k_r']) *(afnp.pi/(2*params['Ns'])) - - rxyXqxy =params['fft2Dshift']*(af_fft.ifft2(qxyXqt*params['fft2Dshift']))*params['deapod_filt'] #Fourier to real space : qxy to rxy - - return rxyXqxy - - -def init_nufft_params(sino,geom): - # Function to initialize parameters associated with the forward model - #inputs : sino - A list contating parameters associated with the sinogram - # Ns : Number of entries in the padded sinogram along the "detector" rows - # Ns_orig : Number of entries detector elements per slice - # center : Center of rotation in pixels computed from the left end of the detector - # angles : An array containg the angles at which the data was acquired in radians - # : geom - TBD - # - - KBLUT_LENGTH = 256 - k_r=3 #kernel size 2*kr+1 TODO : Breaks when k_r is large. Why ? - beta =4*math.pi - Ns = sino['Ns'] - Ns_orig = sino['Ns_orig'] - ang = sino['angles'] - - q_grid = np.arange(1,sino['Ns']+1) - np.floor((sino['Ns']+1)/2) - 1 - sino['tt'],sino['qq']=np.meshgrid(ang*180/math.pi,q_grid) - - # Preload the Bessel kernel (real components!) - kblut,KB,KB1D,KB2D=KBlut(k_r,beta,KBLUT_LENGTH) - - - #Normalization (density compensation factor) -# Dq=KBdensity1(sino['qq'],sino['tt'],KB1,k_r,Ns)'; - - # polar to cartesian, centered - [xi,yi]=pol2cart(sino['qq'],sino['tt']*math.pi/180) - xi = xi+np.floor((Ns+1)/2) - yi = yi+np.floor((Ns+1)/2) - - params={} - params['k_r'] = k_r - params['deapod_filt']=afnp.array(deapodization(Ns,KB1D),dtype=afnp.float32) - params['sino_mask'] = afnp.array(padmat(np.ones((Ns_orig,sino['qq'].shape[1])),np.array((Ns,sino['qq'].shape[1])),0),dtype=afnp.float32) - params['grid'] = [Ns,Ns] - params['scale']= ((KBLUT_LENGTH-1)/k_r) - params['center'] = afnp.array(sino['center']) - params['Ns'] = Ns - params['Ntheta'] = np.size(ang) - - # push parameters to gpu and initalize a few in-line functions - params['gxi'] = afnp.array(np.single(xi)) - params['gyi'] = afnp.array(np.single(yi)) - params['gxy'] = params['gxi']+1j*params['gyi'] - params['gkblut'] = afnp.array(np.single(kblut)) - params['det_grid'] = np.array(np.reshape(np.arange(0,sino['Ns']),(sino['Ns'],1))) - - #####Generate Ram-Lak/ShepLogan like filter kernel######### - - temp_mask=np.ones(Ns) - kernel=np.ones(Ns) - if 'filter' in sino: - temp_r = np.linspace(-1,1,Ns) - kernel = (Ns)*np.fabs(temp_r)*np.sinc(temp_r/2) - temp_pos = (1-sino['filter'])/2 - temp_mask[0:np.int16(temp_pos*Ns)]=0 - temp_mask[np.int16((1-temp_pos)*Ns):]=0 - params['giDq']=afnp.array(kernel*temp_mask,dtype=afnp.complex64) - - temp = afnp.array((-1)**params['det_grid'],dtype=afnp.float32) - temp2 = np.array((-1)**params['det_grid'],dtype=afnp.float32) - temp2 = afnp.array(temp2.reshape(1,sino['Ns'])) - temp3 = afnp.array(afnp.exp(-1j*2*params['center']*(afnp.pi/params['Ns'])*params['det_grid']).astype(afnp.complex64)) - temp4 = afnp.array(afnp.exp(1j*2*params['center']*afnp.pi/params['Ns']*params['det_grid']).astype(afnp.complex64)) - params['fft2Dshift'] = afnp.array(temp*temp2,dtype=afnp.complex64) - params['fftshift1D'] = lambda x : temp*x - params['fftshift1D_center'] = lambda x : temp3*x - params['fftshift1Dinv_center'] = lambda x : temp4*x - - return params - -def deapodization(Ns,KB1D): - xx=np.arange(1,Ns+1)-Ns/2-1 - dpz=np.fft.fftshift(np.fft.ifft2(np.fft.fftshift(np.reshape(KB1D(xx),(np.size(xx),1))*KB1D(xx)))) - dpz=dpz.real #astype(float) - dpz=1/dpz - return dpz - -def KBlut(k_r,beta,nlut): - kk=np.linspace(0,k_r,nlut) - kblut = KB2( kk, 2*k_r, beta) - scale = (nlut-1)/k_r - kbcrop = lambda x: (np.abs(x)<=k_r) - KBI = lambda x: np.int16(np.abs(x)*scale-np.floor(np.abs(x)*scale)) - KB1D = lambda x: (np.reshape(kblut[np.int16(np.floor(np.abs(x)*scale)*kbcrop(x))],x.shape)*KBI(x)+np.reshape(kblut[np.int16(np.ceil(np.abs(x)*scale)*kbcrop(x))],x.shape)*(1-KBI(x)))*kbcrop(x) - KB=lambda x,y: KB1D(x)*KB1D(y) - KB2D=lambda x,y: KB1D(x)*KB1D(y) - return kblut, KB, KB1D,KB2D - -def KB2(x, k_r, beta): - w = sc_spl.iv(0, beta*np.sqrt(1-(2*x/k_r)**2)) - #w=w/np.abs(sc_spl.iv(0, beta)) - w=(w*(x<=k_r)) - return w - -def cart2pol(x, y): - rho = np.sqrt(x**2 + y**2) - phi = np.arctan2(y, x) - return(rho, phi) - -def pol2cart(rho, phi): - x = rho * np.cos(phi) - y = rho * np.sin(phi) - return(x, y) - -#def densityCompensation(qq,tt,KB,nj,Ns): -# nb=100; #TODO : Why 100 ? Venkat -# [nt,nq]=size(qq); - #crop repeated angles -# ii=(tt(:,1))-min(tt(:,1))<180; -# qq1=qq(ii,:); -# tt1=tt(ii,:); -# qq1=-fliplr(qq1); -# [xi,yi]=pol2cart(tt1*pi/180,qq1); diff --git a/xicam/plugins/tomography/tomocam/__init__.py b/xicam/plugins/tomography/tomocam/__init__.py deleted file mode 100644 index 23577d03..00000000 --- a/xicam/plugins/tomography/tomocam/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env python - -from tomoCam import gpuGridrec, gpuSIRT diff --git a/xicam/plugins/tomography/tomocam/gnufft.so b/xicam/plugins/tomography/tomocam/gnufft.so deleted file mode 100755 index a4ab7d86763fb8e7a9482f76e6ed31fe3b508aa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 211280 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 0) + (image < 1)) if self.actionLog_Intensity.isChecked() else image) - # else: - # self.ui.roiPlot.show() - # self.imageItem.updateImage(np.log(image[self.currentIndex] * (image[self.currentIndex]> 0) + (image[self.currentIndex] < 1)) if self.actionLog_Intensity.isChecked() else image[self.currentIndex]) - from scipy.signal import fftconvolve @@ -1599,7 +1580,7 @@ def reload(self): del w w = self.layout().takeAt(0) - for key, plugin in self.plugins.items(): + for key, plugin in list(self.plugins.items()): if plugin.enabled: if plugin.instance.hidden: continue @@ -1612,7 +1593,7 @@ def reload(self): button.setCheckable(True) button.setAutoExclusive(True) button.clicked.connect(plugin.activate) - if plugin is self.plugins.values()[0]: + if plugin is list(self.plugins.values())[0]: button.setChecked(True) self.layout().addWidget(button) label = QtGui.QLabel('|') @@ -1651,8 +1632,8 @@ def __init__(self): # self.textitem.dataBounds = textItemBounds def loaditem(self, item): - if isinstance(item, str) or isinstance(item, unicode): - if os.path.isfile(item) or os.path.isdir(item): + if isinstance(item, str): + if os.path.isfile(item) or os.path.isdir(item) or item[:3] == 'DB:': item = loader.loadimage(item) else: self.setText(item) @@ -1684,7 +1665,7 @@ def __init__(self): super(fileTreeWidget, self).__init__() self.filetreemodel = QtGui.QFileSystemModel() self.setModel(self.filetreemodel) - self.filetreepath = pathtools.getRoot() + self.filetreepath = path.getRoot() self.treerefresh(self.filetreepath) header = self.header() self.setHeaderHidden(True) diff --git a/xicam/plugins/zipythonconsole.py b/xicam/plugins/zipythonconsole.py index 2347482d..c063faf4 100644 --- a/xicam/plugins/zipythonconsole.py +++ b/xicam/plugins/zipythonconsole.py @@ -1,6 +1,8 @@ +from __future__ import absolute_import +from __future__ import unicode_literals from PySide import QtGui import sys -import base +from . import base from xicam import plugins # Overload for Py2App diff --git a/xicam/threads.py b/xicam/threads.py index 548044e2..60c5c2f0 100644 --- a/xicam/threads.py +++ b/xicam/threads.py @@ -19,7 +19,12 @@ import types import traceback import functools -import Queue + +is_py2 = sys.version[0] == '2' +if is_py2: + import Queue as queue +else: + import queue as queue import multiprocessing as mp from PySide import QtCore from pipeline import msg @@ -131,8 +136,8 @@ def run(self): if self._callback_slot: self.emit(self._callback_slot, value) except RuntimeError: etype, ex, tb = sys.exc_info() - print 'exception1:', etype, ex, traceback.format_exc(), self._callback_slot, self._method - print 'this did not run' + print('exception1:', etype, ex, traceback.format_exc(), self._callback_slot, self._method) + print('this did not run') msg.logMessage(('Runnable method tried to return value, but signal was already disconnected.'), msg.WARNING) if self.lock is not None: self.lock.unlock() @@ -140,7 +145,7 @@ def run(self): except Exception: etype, ex, tb = sys.exc_info() - print 'exception:',etype,ex,traceback.format_exc(),self._callback_slot,self._method + print('exception:', etype, ex, traceback.format_exc(), self._callback_slot, self._method) self.emitter.sigExcept.emit(etype, ex, tb) else: self.emitter.sigFinished.emit() @@ -153,7 +158,7 @@ def emit(self,slot,value): if type(value) is not tuple: value = (value,) if str(type(slot)) == "": # allows slotting into signals; this type is not in QtCore, so must compare by name str if slot is None: return - value = map(nonesigmod.pyside_none_wrap, value) + value = list(map(nonesigmod.pyside_none_wrap, value)) tempemitter = EmitterFactory(*[object] * len(value))() tempemitter.sigTemp.connect(slot, QtCore.Qt.QueuedConnection) tempemitter.sigTemp.emit(*value) @@ -350,7 +355,7 @@ def __init__(self, queue, parent=None): super(Worker, self).__init__(parent) self.queue = queue self.pool = QtCore.QThreadPool.globalInstance() - print 'maxthreads:',self.pool.maxThreadCount() + print('maxthreads:', self.pool.maxThreadCount()) # def __del__(self): @@ -363,7 +368,7 @@ def stop(self): self.queue.put(None) self.quit() - print 'threads:',self.pool.activeThreadCount() + print('threads:', self.pool.activeThreadCount()) def run(self): """ @@ -421,7 +426,7 @@ def add_to_queue(runnable): # Application globals -queue = Queue.Queue() +queue = queue.Queue() worker = Worker(queue) mutex = QtCore.QMutex() worker.start() diff --git a/xicam/toolbar.py b/xicam/toolbar.py index 34d7a60a..9a2a6fc3 100644 --- a/xicam/toolbar.py +++ b/xicam/toolbar.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide import QtGui from PySide import QtCore diff --git a/xicam/watcher.py b/xicam/watcher.py index 9d7dc4d5..683146df 100644 --- a/xicam/watcher.py +++ b/xicam/watcher.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide import QtGui from PySide import QtCore from PySide.QtCore import Qt diff --git a/xicam/widgets/NDTimelinePlotWidget.py b/xicam/widgets/NDTimelinePlotWidget.py index 8be13ea5..0bc1137d 100644 --- a/xicam/widgets/NDTimelinePlotWidget.py +++ b/xicam/widgets/NDTimelinePlotWidget.py @@ -145,7 +145,12 @@ def addData(self, t, *args, **kwargs): plot = addData def setData(self): - self.currentPlot().setData(**self._data) + if 'x' not in self._data: + for colorhash in self._data['colors']: + color = list(map(int, colorhash.split(','))) + self.currentPlot().setData(self._data['t'], self._data[colorhash], color) + else: + self.currentPlot().setData(self._data['t'], self._data['x'], self._data['y']) def setDMode(self,is1D): index = None diff --git a/xicam/widgets/calibrationpanel.py b/xicam/widgets/calibrationpanel.py index 3e8e83b3..5f0bf93d 100644 --- a/xicam/widgets/calibrationpanel.py +++ b/xicam/widgets/calibrationpanel.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide import QtGui, QtCore import pyqtgraph.parametertree.parameterTypes as pTypes from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType diff --git a/xicam/widgets/customwidgets.py b/xicam/widgets/customwidgets.py index f425d0e6..28c901a8 100644 --- a/xicam/widgets/customwidgets.py +++ b/xicam/widgets/customwidgets.py @@ -44,7 +44,7 @@ def buildTree(self, data, parent, name='', hideRoot=False): parent.addChild(node) if isinstance(data, dict): - for k in data.keys(): + for k in list(data.keys()): self.buildTree(data[k], node, str(k)) elif isinstance(data, list) or isinstance(data, tuple): for i in range(len(data)): @@ -108,7 +108,7 @@ def mouseMoved(self, ev): try: if viewBox.sceneBoundingRect().contains(pos): mousePoint = viewBox.mapSceneToView(pos) - x, y = map(int, (mousePoint.x(), mousePoint.y())) + x, y = list(map(int, (mousePoint.x(), mousePoint.y()))) if (0 <= x < self.imageItem.image.shape[0]) & (0 <= y < self.imageItem.image.shape[1]): # within bounds self.coordsLabel.setText(u"
x={0}," u" y={1}, I={2}"\ diff --git a/xicam/widgets/explorer.py b/xicam/widgets/explorer.py index 59b1fdc1..c2eac87e 100644 --- a/xicam/widgets/explorer.py +++ b/xicam/widgets/explorer.py @@ -17,9 +17,95 @@ from collections import OrderedDict from xicam import threads from xicam import clientmanager as cmanager -from pipeline import pathtools, msg +from pipeline import path, msg from xicam import config from modpkgs import guiinvoker +from pipeline import daemon + + +class LiveFolderView(QtGui.QListWidget): + """ + Local file explorer tree view + """ + + pathChanged = QtCore.Signal(str) + sigOpen = QtCore.Signal(list) + sigOpenFolder = QtCore.Signal(list) + sigDelete = QtCore.Signal(list) + sigUpload = QtCore.Signal(list) + sigItemPreview = QtCore.Signal(str) + sigAppend = QtCore.Signal(str) + + def __init__(self, path, parent=None): + self.path = path + super(LiveFolderView, self).__init__(parent) + + # self.path = config.settings['Default Local Path'] + + self.menu = QtGui.QMenu() + standardActions = [QtGui.QAction('Open', self)] + standardActions[0].triggered.connect(self.handleOpenAction) + # standardActions[1].triggered.connect(self.handleOpenFolderAction) + # standardActions[2].triggered.connect(self.handleDeleteAction) + self.menu.addActions(standardActions) + + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.menuRequested) + self.doubleClicked.connect(self.onDoubleClick) + + self.filter = '*.*' + self.startwatcher() + + def startwatcher(self): + self.watcher = daemon.Watcher(self.path, self.filter, newcallback=lambda ev: self.autoOpen(ev.src_path), + procold=False) + + def autoOpen(self, path): + self.addItem(QtGui.QListWidgetItem(path)) + self.sigOpen.emit(path) + + def refresh(self, path=None): + """ + Refresh the file tree, or switch directories and refresh + """ + self.watcher.stop() + if not os.path.isdir(path): + self.path = os.path.dirname(path) + self.filter = os.path.basename(path) + self.startwatcher() + + def menuRequested(self, position): + self.menu.exec_(self.viewport().mapToGlobal(position)) + + def onDoubleClick(self, index): + self.sigOpen.emit(self.itemFromIndex(index).text()) + + def currentChanged(self, current, previous): + path = self.currentItem().text() + if os.path.isfile(path): + self.sigItemPreview.emit(path) + + def getSelectedFilePaths(self): + items = self.selectedIndexes() + paths = [item.text() for item in items] + self.sigOpen.emit(paths) + + def getSelectedFile(self): + pass + + def handleOpenAction(self): + paths = self.getSelectedFilePaths() + if os.path.isdir(paths[0]) and len(paths) == 1: + self.refresh(path=paths[0]) + else: + self.sigOpen.emit(paths) + + +class StreamFolderView(LiveFolderView): + + def autoOpen(self, path): + self.addItem(QtGui.QListWidgetItem(path)) + self.sigAppend.emit(path) class LocalFileView(QtGui.QTreeView): """ @@ -32,6 +118,7 @@ class LocalFileView(QtGui.QTreeView): sigDelete = QtCore.Signal(list) sigUpload = QtCore.Signal(list) sigItemPreview = QtCore.Signal(str) + sigAppend = QtCore.Signal(str) def __init__(self, parent=None): super(LocalFileView, self).__init__(parent) @@ -88,7 +175,6 @@ def refresh(self, path=None): self.file_model.setRootPath(root.absolutePath()) self.file_model.setNameFilters([filter]) self.setRootIndex(self.file_model.index(root.absolutePath())) - self.pathChanged.emit(path) config.settings['Default Local Path'] = path def menuRequested(self, position): @@ -97,7 +183,8 @@ def menuRequested(self, position): def onDoubleClick(self, index): path = self.file_model.filePath(index) if os.path.isdir(path): - self.refresh(path=path) + self.pathChanged.emit(path) + # self.refresh(path=path) else: self.sigOpen.emit([path]) @@ -154,6 +241,7 @@ class RemoteFileView(QtGui.QListWidget): sigDownload = QtCore.Signal(str, str, object, tuple, dict, object) sigTransfer = QtCore.Signal(str, str, object, tuple, dict, object) sigItemPreview = QtCore.Signal(str) + sigAppend = QtCore.Signal(str) def __init__(self, remote_client, parent=None): super(RemoteFileView, self).__init__(parent) @@ -225,64 +313,123 @@ def handleDeleteAction(self): pass -# This has been replaced with SFTP based client see SFTPFileView -# class NERSCFileView(RemoteFileView): -# """ -# File explorer for NERSC systems, must be passed a client and a worker to make REST calls -# """ -# -# def __init__(self, nersc_client, system, parent=None): -# self.system = system -# super(NERSCFileView, self).__init__(nersc_client, parent=parent) -# self.client.set_scratch_dir(system) -# self.path = self.client.scratch_dir -# self.getDirContents(self.path, self.system) -# -# def refresh(self, path=None): -# if path is None: -# path = self.path -# -# super(NERSCFileView, self).getDirContents(path, self.system) -# super(NERSCFileView, self).refresh(path=path) -# -# def onDoubleClick(self, item): -# file_name = item.text() -# if '.' in file_name: -# save_path = os.path.join(tempfile.gettempdir(), file_name) -# self.handleDownloadAction(save_path=save_path, fslot=(lambda: self.sigOpen.emit([save_path]))) -# super(NERSCFileView, self).onDoubleClick(item) -# -# def handleOpenAction(self): -# paths = self.getSelectedFilePaths() -# files = [os.path.split(path)[-1] for path in paths] -# for file_name in files: -# save_path = os.path.join(tempfile.gettempdir(), file_name) -# self.handleDownloadAction(save_path=save_path, fslot=(lambda: self.sigOpen.emit([save_path]))) -# -# def handleDownloadAction(self, save_path=None, fslot=None): -# fpath = super(NERSCFileView, self).handleDownloadAction() -# size = self.client.get_file_size(fpath, self.system) -# desc = 'File {0} from {1}'.format(fpath, self.system) -# if size < 100*2**20: -# method = self.client.download_file_generator -# args = [fpath, self.system] -# kwargs = {} -# if save_path is not None: -# kwargs['save_path'] = save_path -# else: -# # USE GLOBUS API CALL HERE, Think of how to get access to client -# pass -# -# self.sigDownload.emit(desc, method, args, kwargs, fslot) -# -# def handleTransferAction(self): -# return None -# -# def handleDeleteAction(self): -# runnable = threads.RunnableMethod(self.client.delete_file, -# method_args=(self.getSelectedFilePath(), self.system), -# finished_slot=self.refresh) -# threads.add_to_queue(runnable) +class DataBrokerView(QtGui.QListWidget): + """ + Explorer interface for DataBroker connection + """ + + pathChanged = QtCore.Signal(str) + sigDelete = QtCore.Signal(list) + sigOpen = QtCore.Signal(list) + sigOpenFolder = QtCore.Signal(list) + sigDownload = QtCore.Signal(str, str, object, tuple, dict, object) + sigTransfer = QtCore.Signal(str, str, object, tuple, dict, object) + sigItemPreview = QtCore.Signal(str) + sigAppend = QtCore.Signal(str) + + def __init__(self, db, parent=None): + self.path = '-100:' + self.db = db + self._headers = {} + super(DataBrokerView, self).__init__() + self.setSelectionMode(self.ExtendedSelection) + self.query(self.path) + self.doubleClicked.connect(self.onDoubleClick) + + self.menu = QtGui.QMenu() + standardActions = [QtGui.QAction('Open', self)] + standardActions[0].triggered.connect(self.openSelected) + self.menu.addActions(standardActions) + + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.menuRequested) + + def clear(self): + super(DataBrokerView, self).clear() + self._headers.clear() + + def openSelected(self): + items = self.selectedIndexes() + headers = [self._headers[item.data()] for item in items] + paths = ['DB:{}/{}'.format(self.db.host, header.start['uid']) + for header in headers] + self.sigOpen.emit(paths) + + def onDoubleClick(self, item): + header = self._headers[item.data()] + self.sigOpen.emit(['DB:{}/{}'.format(self.db.host, + header.start['uid'])]) + + def currentChanged(self, current, previous): + uid = 'DB:{}/{}'.format(self.db.host, + self._headers[current.data()].start['uid']) + self.sigItemPreview.emit(uid) + + def menuRequested(self, position): + self.menu.exec_(self.viewport().mapToGlobal(position)) + + def query(self, querystring): + results = [] + + # if querystring is null, limit to last 100 + if not querystring: + querystring = '-100:' + + # if querystring is int-like + if not results: + try: + query = int(querystring) + except ValueError: + pass + else: + results = [self.db[query]] + + # if querystring is slice-like, slice db + if not results: + try: + query = slice(*map(lambda x: int(x.strip()) if x.strip() else None, querystring.split(':'))) + except ValueError: + pass + else: + results = self.db[query] + + # if querystring is dict-like + if not results: + try: + query = eval("dict({})".format(querystring)) + except Exception: + pass + else: + results = self.db(**query) + + self.path = querystring + self.fillList(results) + + def fillList(self, results): + self.clear() + for h in results: + start = h.start + for n in [start.get('sample_name'), start.get('object'), '??']: + if type(n) == str: + name = n + break + + key = '{} [{}] sample: {}'.format(start.get('plan_name', '??'), + start.get('scan_id', ''), name) + + item = QtGui.QListWidgetItem(key) + + if not h.get('stop') or h.get('stop', {'exit_status': 'fail'})['exit_status'] in ['abort', 'fail']: + item.setBackground(QtGui.QBrush(QtCore.Qt.red)) + self.addItem(item) + self._headers[key] = h + + def refresh(self, path=None): + if path is None: + path = self.path + else: + self.path = path + self.query(path) class GlobusFileView(RemoteFileView): @@ -340,6 +487,7 @@ class SFTPFileView(QtGui.QTreeWidget): sigDownload = QtCore.Signal(str, str, object, tuple, dict, object) sigTransfer = QtCore.Signal(str, str, object, tuple, dict, object) sigItemPreview = QtCore.Signal(str) + sigAppend = QtCore.Signal(str) def __init__(self, sftp_client, parent=None): super(SFTPFileView, self).__init__(parent=parent) @@ -478,6 +626,7 @@ class SpotDatasetView(QtGui.QTreeWidget): sigDownload = QtCore.Signal(str, str, object, tuple, dict, object) sigTransfer = QtCore.Signal(str, str, object, tuple, dict, object) sigItemPreview = QtCore.Signal(object) + sigAppend = QtCore.Signal(str) def __init__(self, spot_client, parent=None): @@ -524,7 +673,7 @@ def createDatasetDictionary(self, data): for index in range(len(data)): derived_data = {data[index]['fs']['stage']: data[index]['name']} - if 'derivatives' in data[index]['fs'].keys(): + if 'derivatives' in list(data[index]['fs'].keys()): derivatives = data[index]['fs']['derivatives'] for d_index in range(len(derivatives)): stage = derivatives[d_index]['dstage'] @@ -543,7 +692,7 @@ def fillTree(self, data): def addTreeItems(self, item, value): item.setExpanded(False) if type(value) is dict: - for key, val in sorted(value.iteritems()): + for key, val in sorted(value.items()): icon = QtGui.QFileIconProvider().icon(QtGui.QFileIconProvider.Folder) child = QtGui.QTreeWidgetItem([key], parent=self) child.setIcon(0, icon) @@ -578,7 +727,7 @@ def getSelectedDatasets(self): return datasets def handleOpenAction(self): - save_paths = map(lambda dset: os.path.join(tempfile.gettempdir(), dset), self.getSelectedDatasets()) + save_paths = [os.path.join(tempfile.gettempdir(), dset) for dset in self.getSelectedDatasets()] for path in save_paths: self.handleDownloadAction(save_paths=[path], fslot=(lambda: self.sigOpen.emit([path]))) @@ -664,6 +813,7 @@ def onBackClicked(self): path = self.file_view.path path = os.path.dirname(str(path)) self.file_view.refresh(path=path) + self.file_view.pathChanged.emit(path) def onRefreshClicked(self): self.file_view.refresh() @@ -743,13 +893,14 @@ class MultipleFileExplorer(QtGui.QTabWidget): """ sigLoginSuccess = QtCore.Signal(bool) - sigLoginRequest = QtCore.Signal(QtCore.Signal, bool) + sigLoginRequest = QtCore.Signal(QtCore.Signal, bool, bool) sigProgJob = QtCore.Signal(str, object, list, dict, object) sigPulsJob = QtCore.Signal(str, object, list, dict, object) sigSFTPJob = QtCore.Signal(str, object, list, dict, object) sigOpen = QtCore.Signal(list) sigFolderOpen = QtCore.Signal(list) sigPreview = QtCore.Signal(object) + sigAppend = QtCore.Signal(str) def __init__(self, parent=None): super(MultipleFileExplorer, self).__init__(parent) @@ -775,16 +926,23 @@ def __init__(self, parent=None): self.tabCloseRequested.connect(self.removeTab) self.newtabmenu = QtGui.QMenu(None) + addlivefolder = QtGui.QAction('Live Folder', self.newtabmenu) + addstreamfolder = QtGui.QAction('Stream Folder', self.newtabmenu) + adddatabroker = QtGui.QAction('Data Broker', self.newtabmenu) addspot = QtGui.QAction('SPOT', self.newtabmenu) addcori = QtGui.QAction('Cori', self.newtabmenu) addedison = QtGui.QAction('Edison', self.newtabmenu) addbragg = QtGui.QAction('Bragg', self.newtabmenu) addsftp = QtGui.QAction('SFTP Connection', self.newtabmenu) showjobtab = QtGui.QAction('Jobs', self.newtabmenu) - self.standard_actions = OrderedDict({'SPOT': addspot, 'Cori': addcori, 'Edison': addedison, - 'Bragg': addbragg, 'SFTP': addsftp}) - self.newtabmenu.addActions(self.standard_actions.values()) + self.standard_actions = OrderedDict({'DataBroker': adddatabroker, 'SPOT': addspot, 'Cori': addcori, + 'Edison': addedison, 'Bragg': addbragg, 'SFTP': addsftp, + 'Live': addlivefolder, 'Stream': addstreamfolder}) + self.newtabmenu.addActions(list(self.standard_actions.values())) self.newtabmenu.addAction(showjobtab) + addlivefolder.triggered.connect(self.addLiveFolderTab) + addstreamfolder.triggered.connect(self.addStreamFolderTab) + adddatabroker.triggered.connect(self.addDataBrokerTab) addspot.triggered.connect(self.addSPOTTab) addedison.triggered.connect(lambda: self.addHPCTab('Edison')) addcori.triggered.connect(lambda: self.addHPCTab('Cori')) @@ -793,15 +951,15 @@ def __init__(self, parent=None): showjobtab.triggered.connect(lambda: self.addTab(self.jobtab, 'Jobs')) def enableActions(self): - for name, action in self.standard_actions.iteritems(): - if name in self.explorers.keys(): + for name, action in self.standard_actions.items(): + if name in list(self.explorers.keys()): action.setEnabled(False) else: action.setEnabled(True) def removeTab(self, p_int): if self.tabText(p_int) != 'Jobs': - name = self.explorers.keys()[p_int] + name = list(self.explorers.keys())[p_int] explorer = self.explorers.pop(name) cmanager.logout(explorer.file_view.client) self.widget(p_int).deleteLater() @@ -809,12 +967,38 @@ def removeTab(self, p_int): super(MultipleFileExplorer, self).removeTab(p_int) def removeTabs(self): - for i in xrange(1, self.count()): + for i in range(1, self.count()): self.removeTab(1) def onPlusClicked(self): self.newtabmenu.popup(QtGui.QCursor.pos()) + def addDataBrokerTab(self): + add_DB_tab = lambda client: self.addFileExplorer('DataBroker', + FileExplorer(DataBrokerView(client, self))) + add_DB_callback = lambda client: self.loginSuccess(client, + add_explorer=add_DB_tab) + login_callback = lambda client: cmanager.add_DB_client(client.host, + client, + add_DB_callback) + DB_client = cmanager.DB_client + self.sigLoginRequest.emit(partial(cmanager.login, login_callback, DB_client), + True, False) + + def addLiveFolderTab(self): + dialog = QtGui.QFileDialog(self, 'Choose a live folder to watch', os.curdir, + options=QtGui.QFileDialog.ShowDirsOnly) + d = dialog.getExistingDirectory() + if d: + self.addFileExplorer('Live Folder', FileExplorer(LiveFolderView(d))) + + def addStreamFolderTab(self): + dialog = QtGui.QFileDialog(self, 'Choose a stream folder to watch', os.curdir, + options=QtGui.QFileDialog.ShowDirsOnly) + d = dialog.getExistingDirectory() + if d: + self.addFileExplorer('Stream Folder', FileExplorer(StreamFolderView(d))) + def addFileExplorer(self, name, file_explorer, closable=True): self.explorers[name] = file_explorer file_explorer.file_view.sigItemPreview.connect(self.itemSelected) @@ -833,6 +1017,7 @@ def addFileExplorer(self, name, file_explorer, closable=True): def wireExplorerSignals(self, explorer): explorer.file_view.sigOpen.connect(self.handleOpenActions) explorer.file_view.sigOpenFolder.connect(self.handleOpenFolderActions) + explorer.file_view.sigAppend.connect(self.handleAppendActions) try: explorer.file_view.sigDownload.connect(self.handleDownloadActions) except AttributeError: @@ -922,6 +1107,10 @@ def handleOpenActions(self, paths): if len(paths) > 0: self.sigOpen.emit(paths) + def handleAppendActions(self, paths): + if len(paths) > 0: + self.sigAppend.emit(paths) + def handleOpenFolderActions(self, paths): if len(paths) > 0: self.sigFolderOpen.emit(paths) @@ -1214,4 +1403,4 @@ def __init__(self, name, path, icon=None, parent=None): w = SFTPFileView(client) w.setWindowTitle("Test this thing") w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/xicam/widgets/featurewidgets.py b/xicam/widgets/featurewidgets.py index 69ebb36f..524055c6 100644 --- a/xicam/widgets/featurewidgets.py +++ b/xicam/widgets/featurewidgets.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide import QtGui, QtCore diff --git a/xicam/widgets/imageviewers.py b/xicam/widgets/imageviewers.py index 2f604a9b..efc37212 100644 --- a/xicam/widgets/imageviewers.py +++ b/xicam/widgets/imageviewers.py @@ -139,8 +139,8 @@ def flipAxes(self, arr): def setData(self, data, flipAxes=False): if type(data) is dict or type(data).__bases__[0] is dict: - self.keys = data.keys() - data = np.array(data.values()) + self.keys = list(data.keys()) + data = np.array(list(data.values())) else: self.keys = None diff --git a/xicam/widgets/login.py b/xicam/widgets/login.py index aefe06ce..ef175f9b 100644 --- a/xicam/widgets/login.py +++ b/xicam/widgets/login.py @@ -2,6 +2,7 @@ """ @author: lbluque """ +from __future__ import unicode_literals import time from PySide import QtGui, QtCore from PySide.QtUiTools import QUiLoader @@ -65,7 +66,7 @@ def login_slot(self, slot): self.loginClicked.connect(self._login_slot) @QtCore.Slot(QtCore.Signal, bool) - def loginRequest(self, login_clicked_slot, show_host=False): + def loginRequest(self, login_clicked_slot, show_host=False, show_creds=True): """ Slot to receive login request signal and setup/show the loginDialog accordingly @@ -77,10 +78,9 @@ def loginRequest(self, login_clicked_slot, show_host=False): Boolean to show the host QTextEdit to input a hostname """ - if show_host: - self.ui.host_box.show() - else: - self.ui.host_box.hide() + self.ui.host_box.setVisible(show_host) + self.ui.user_box.setVisible(show_creds) + self.ui.pass_box.setVisible(show_creds) self.ui.user_box.setFocus() self.login_slot = login_clicked_slot self.setCurrentWidget(self.ui.login_page) @@ -91,9 +91,9 @@ def handleLogin(self): Handles the login button clicked signal and calls the login_slot if all input fields are satisfied """ host, usr, pwd = self.ui.host_box.text(), self.ui.user_box.text(), self.ui.pass_box.text() - if usr == '': + if usr == '' and self.ui.user_box.isVisible(): QtGui.QMessageBox.warning(self, 'Username missing', 'You forgot to mention who you are!') - elif pwd == '': + elif pwd == '' and self.ui.user_box.isVisible(): QtGui.QMessageBox.warning(self, 'Password missing', 'You need to provide proof that you really are {}!'.format(usr)) elif host == '' and not self.ui.host_box.isHidden(): diff --git a/xicam/widgets/metadatawidget.py b/xicam/widgets/metadatawidget.py new file mode 100644 index 00000000..ad97e07a --- /dev/null +++ b/xicam/widgets/metadatawidget.py @@ -0,0 +1,121 @@ +from PySide.QtGui import * +from PySide.QtCore import * +from collections import Iterable, OrderedDict + +from datetime import datetime + +from xicam import config + + +class MetaDataWidget(QTreeWidget): + """ + Widget for displaying hierarchical python data structures + (eg, nested dicts, lists, and arrays). + """ + + def __init__(self, parent=None, data=None): + super(MetaDataWidget, self).__init__(parent) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setData(data) + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + self.setSelectionMode(QAbstractItemView.SingleSelection) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setColumnCount(2) + self.setHeaderLabels(('Key', 'Value')) + self.header().setStretchLastSection(True) + self.setColumnWidth(0, 100) + self.setSelectionMode(self.SingleSelection) + + self.contextMenu = QMenu() + useAsMenu = QMenu(u'Use as...', parent=self.contextMenu) + useAsMenu.addAction('Beam Energy').triggered.connect(self.useAsEnergy) + useAsMenu.addAction('Downstream Intensity').triggered.connect(self.useAsI1) + useAsMenu.addAction('Timeline Axis').triggered.connect(self.useAsTimeline) + self.contextMenu.addMenu(useAsMenu) + + def mousePressEvent(self, ev): + if ev.button() == Qt.RightButton: + self.contextMenu.popup(self.mapToGlobal(ev.pos())) + ev.accept() + super(MetaDataWidget, self).mousePressEvent(ev) + + def setData(self, data): + self(data) + + def useAsI1(self): + config.activeExperiment.setHeaderMap('I1 AI', self.getSelectedKey()) + + def useAsEnergy(self): + config.activeExperiment.setHeaderMap('Beam Energy', self.getSelectedKey()) + + def useAsTimeline(self): + config.activeExperiment.setHeaderMap('Timeline Axis', self.getSelectedKey()) + + def getSelectedKey(self): + return self.selectedItems()[0].text(0) + + def __call__(self, header): + self.fill(header) + + def fill(self, value): + self.clear() + fill_item(self.invisibleRootItem(), value) + + +def fill_item(item, value): + """ + Display a dictionary as a QtWidgets.QtTreeWidget + + adapted from http://stackoverflow.com/a/21806048/1221924 + """ + item.setExpanded(True) + if hasattr(value, 'items'): + for key, val in sorted(value.items()): + child = QTreeWidgetItem() + # val is dict or a list -> recurse + if hasattr(val, 'items') or _listlike(val): + child.setText(0, _short_repr(key).strip("'")) + item.addChild(child) + fill_item(child, val) + if key == 'descriptors': + child.setExpanded(False) + # val is not iterable -> show key and val on one line + else: + # Show human-readable datetime alongside raw timestamp. + # 1484948553.567529 > '[2017-01-20 16:42:33] 1484948553.567529' + if (key == 'time') and isinstance(val, float): + FMT = '%Y-%m-%d %H:%M:%S' + ts = datetime.fromtimestamp(val).strftime(FMT) + text = "time", " [{}] {}".format(ts, val) + else: + text = _short_repr(key).strip("'"), _short_repr(val) + child.setText(0, text[0]) + child.setText(1, text[1]) + item.addChild(child) + + elif type(value) is list: + for val in value: + if hasattr(val, 'items'): + fill_item(item, val) + elif _listlike(val): + fill_item(item, val) + else: + child = QTreeWidgetItem() + item.addChild(child) + child.setExpanded(True) + child.setText(0, _short_repr(val)) + else: + child = QTreeWidgetItem() + child.setText(0, _short_repr(value)) + item.addChild(child) + + +def _listlike(val): + return isinstance(val, Iterable) and not isinstance(val, str) + + +def _short_repr(text): + r = repr(text) + if len(r) > 82: + r = r[:27] + '...' + return r diff --git a/xicam/xglobals.py b/xicam/xglobals.py index 7809d013..e819ab6e 100644 --- a/xicam/xglobals.py +++ b/xicam/xglobals.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict LUT = None diff --git a/xicam/xicamwindow.py b/xicam/xicamwindow.py index 1c5a8e6d..6b892590 100644 --- a/xicam/xicamwindow.py +++ b/xicam/xicamwindow.py @@ -5,19 +5,21 @@ ## TODO: Add mask clear - +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals import os from PySide.QtUiTools import QUiLoader from PySide import QtGui from PySide import QtCore -from xicam import config -import watcher +from . import config +from . import watcher #import daemon import pipeline import qdarkstyle -import plugins +from . import plugins from xicam import xglobals import numpy as np @@ -25,7 +27,7 @@ # import client.dask_local_scheduler # import client.dask_remote_scheduler # import client.dask_active_executor -import threads +from . import threads from pipeline import msg class ComboBoxAction(QtGui.QWidgetAction): @@ -75,10 +77,10 @@ def handleLogin(self): class MyMainWindow(QtCore.QObject): def __init__(self, app): QtCore.QObject.__init__(self, app) - print 'Gui:\t\t\t', QtGui.QApplication.instance().thread() + print('Gui:\t\t\t', QtGui.QApplication.instance().thread()) QtGui.QFontDatabase.addApplicationFont("xicam/gui/zerothre.ttf") - import plugins + from . import plugins config.activate() @@ -133,6 +135,7 @@ def __init__(self, app): plugins.base.fileexplorer.sigOpen.connect(self.openfiles) plugins.base.fileexplorer.sigFolderOpen.connect(self.openfolder) + plugins.base.fileexplorer.sigAppend.connect(self.appendfiles) plugins.base.booltoolbar.actionTimeline.triggered.connect(plugins.base.filetree.handleOpenAction) pluginmode = plugins.widgets.pluginModeWidget(plugins.plugins) @@ -318,6 +321,32 @@ def openfiles(self, filenames): elif response == QtGui.QMessageBox.Cancel: return None + def appendfiles(self, filenames): + """ + when a file is opened, check if there is calibration and offer to use the image as calibrant + """ + if filenames is not u'': + if config.activeExperiment.iscalibrated() or len(filenames) > 1: + self.appendimages(filenames) + else: + msgBox = QtGui.QMessageBox() + msgBox.setText("The current experiment has not yet been calibrated. ") + msgBox.setInformativeText("Use this image as a calibrant (AgBe)?") + msgBox.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | QtGui.QMessageBox.Cancel) + msgBox.setDefaultButton(QtGui.QMessageBox.Yes) + + response = msgBox.exec_() + + if response == QtGui.QMessageBox.Yes: + self.appendimages(filenames) + + self.calibrate() + elif response == QtGui.QMessageBox.No: + self.appendimages(filenames) + elif response == QtGui.QMessageBox.Cancel: + return None + + def openfolder(self, filenames): """ build a new tab from the folder path, add it to the tab view, and display it @@ -325,7 +354,7 @@ def openfolder(self, filenames): if filenames is not u'': if config.activeExperiment.iscalibrated or len(filenames) > 1: - import plugins + from . import plugins self.ui.statusbar.showMessage('Loading images from folder...') self.app.processEvents() @@ -349,7 +378,7 @@ def openimages(self, paths): build a new tab, add it to the tab view, and display it """ - import plugins + from . import plugins self.ui.statusbar.showMessage('Loading image...') self.app.processEvents() @@ -361,6 +390,20 @@ def openimages(self, paths): self.ui.statusbar.showMessage('Ready...') + def appendimages(self, paths): + """ + build a new tab, add it to the tab view, and display it + """ + + from . import plugins + + self.ui.statusbar.showMessage('Loading image...') + self.app.processEvents() + + plugins.base.activeplugin.appendfiles(paths) + + self.ui.statusbar.showMessage('Ready...') + def loadexperiment(self): """ replot the current tab (tab plotting checks if this is active) diff --git a/xicamlauncher/main.py b/xicamlauncher/main.py index 429aebac..7a2041b5 100644 --- a/xicamlauncher/main.py +++ b/xicamlauncher/main.py @@ -1,3 +1,6 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals import os import sys @@ -7,7 +10,7 @@ sys.stdout = open(os.path.join(os.path.expanduser('~'),'out.log'),'w') sys.stderr = open(os.path.join(os.path.expanduser('~'),'err.log'),'w') -from splash import SplashScreen +from .splash import SplashScreen def main(): sys.path.append(os.path.join(os.getcwd(), 'lib/python2.7/lib-dynload')) @@ -18,17 +21,17 @@ def main(): d.cdUp() d.cdUp() d.setCurrent(d.path()) - print 'QApp root:',QtCore.QDir().current() + print('QApp root:', QtCore.QDir().current()) except NameError: - print 'Could not set QApp root.' # Hopefully this is run as an executable, and this is unnecessary anyway + print('Could not set QApp root.') # Hopefully this is run as an executable, and this is unnecessary anyway for path in sys.path: - print 'path:', path + print('path:', path) import xicam # IMPORTANT! DO NOT REMOVE! Xicam must be loaded early to avoid graphical bugs on mac (?!) app=QtGui.QApplication(sys.argv) pixmap = QtGui.QPixmap("xicam/gui/splash.gif") - print 'CWD:', os.getcwd() + print('CWD:', os.getcwd()) if True: # Disable to bypass splashscreen for testing on windows splash = SplashScreen(pixmap, f=QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.SplashScreen) splash.setAttribute(QtCore.Qt.WA_DeleteOnClose) diff --git a/xicamlauncher/splash.py b/xicamlauncher/splash.py index 7838ef0e..5bd4944d 100644 --- a/xicamlauncher/splash.py +++ b/xicamlauncher/splash.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from PySide import QtCore, QtGui From 7b09a283c798284be4adccca8335e50ffed2d59a Mon Sep 17 00:00:00 2001 From: Ronald Pandolfi Date: Tue, 30 Apr 2019 11:22:05 -0700 Subject: [PATCH 2/3] Add databroker --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5288d2a0..78d4f61e 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,7 @@ install_requires=['scipy', 'Cython', 'pyFAI==0.16.0', 'h5py', 'PySide==1.2.2', 'pyqtgraph', 'QDarkStyle', 'Pillow', 'pyfits', 'PyOpenGL', 'PyYAML', 'qtconsole','tifffile','pysftp', 'requests','dask','distributed','appdirs','futures','scikit-image','imageio','vispy', - 'pypaws>=0.8.4', 'matplotlib', 'astropy', 'watchdog'], + 'pypaws>=0.8.4', 'matplotlib', 'astropy', 'watchdog', 'databroker'], # setup_requires=['numpy', 'cython'], From c164656734cce1789844b32ebb29e72e71ff8303 Mon Sep 17 00:00:00 2001 From: Maksim Rakitin Date: Wed, 1 May 2019 17:09:24 -0400 Subject: [PATCH 3/3] Fix databroker configuration and globus indent problem --- .gitignore | 1 + client/databrokerclient.py | 52 +++++++++++++++++--------------------- client/globus.py | 18 ++++++------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 6f10c7e3..e9c3e507 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ dist/ xicam.egg_info/ +xicam.egg-info/ diff --git a/client/databrokerclient.py b/client/databrokerclient.py index ebad1590..ea700f5a 100644 --- a/client/databrokerclient.py +++ b/client/databrokerclient.py @@ -11,38 +11,32 @@ def __init__(self, host, **kwargs): # from suitcase.als733 import ALSEDFHandler super(DataBrokerClient, self).__init__() self.host = host + self.port = 27017 - # set up filestore - - # from filestore.utils import install_sentinels - # from filestore.fs import FileStore - from databroker.assets.mongo import Registry - from databroker.headersource.mongo import MDS from databroker import Broker - # from databroker.core import register_builtin_handlers - - fs_config = {'host': host, 'port': 27017, - 'database': config.settings['Databroker FileStore Name']} - - # try: - # # this only needs to be done once - # install_sentinels(fs_config, 1) - # except RuntimeError: - # pass - - fs = Registry(fs_config, version=1) - - # fs.register_handler('ALS_HDF', ALSHDF5Handler) - # fs.register_handler('ALS_HDF_SINO', ALSHDF5SinoHandler) - # fs.register_handler('ALS_EDF', ALSEDFHandler) - # register_builtin_handlers(fs) - mds_conf = dict(database=config.settings['Databroker MetaDataStore Name'], host=host, - port=27017, timezone='US/Eastern') - - mds = MDS(mds_conf, 1, auth=False) - db = Broker(mds, fs) - print(db) + # TODO: implement this case when the name of the .yml file is passed + # db = Broker.named(str(host).lower()) + + db_config = {'description': 'Configuration to access the data from mongodb', + 'metadatastore': { + 'module': 'databroker.headersource.mongo', + 'class': 'MDS', + 'config': { + 'host': self.host, + 'port': self.port, + 'database': config.settings['Databroker MetaDataStore Name'], + 'timezone': 'US/Eastern'}}, + 'assets': { + 'module': 'databroker.assets.mongo', + 'class': 'Registry', + 'config': { + 'host': self.host, + 'port': self.port, + 'database': config.settings['Databroker FileStore Name']}} + } + + db = Broker.from_config(db_config) self.db = db def __getitem__(self, item): diff --git a/client/globus.py b/client/globus.py index bf07e709..90fcff99 100644 --- a/client/globus.py +++ b/client/globus.py @@ -178,15 +178,15 @@ def determine_local_endpoint(self): for endpoint in user_endpoints: params = {'path': str(test)} - r = self.get(self.TRANSFER_URL + '/endpoint/' + quote(endpoint) + '/ls', - headers=self.authentication, params=params) - try: - file_response = r.json() - if 'is a file' in file_response['message']: - os.remove(test) - return endpoint - except KeyError: - pass + r = self.get(self.TRANSFER_URL + '/endpoint/' + quote(endpoint) + '/ls', + headers=self.authentication, params=params) + try: + file_response = r.json() + if 'is a file' in file_response['message']: + os.remove(test) + return endpoint + except KeyError: + pass os.remove(test) return None