[SCM] Packaging for sardana branch, master, updated. debian/1.0.0_rc6-2-24-g452aa9a
Picca Frédéric-Emmanuel
picca at debian.org
Thu Jul 25 20:21:31 UTC 2013
The following commit has been merged in the master branch:
commit 7111139f6da3051a96fc3bbe117059a7b7f5a4f2
Author: Picca Frédéric-Emmanuel <picca at debian.org>
Date: Thu Jul 25 21:56:03 2013 +0200
Imported Upstream version 1.2.0
diff --git a/PKG-INFO b/PKG-INFO
index 716a724..2eaff69 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -3,8 +3,8 @@ Name: sardana
Version: 1.2.0
Summary: Sardana is a generic program for control applications in large and small installations
Home-page: http://packages.python.org/sardana
-Author: Tiago Coutinho
-Author-email: tcoutinho at cells.es
+Author: Carlos Pascual-Izarra
+Author-email: cpascual at cells.es
License: LGPL
Download-URL: http://pypi.python.org/packages/source/s/sardana
Description: Produce a modular, high performance, robust and generic user environment
@@ -31,9 +31,9 @@ Classifier: Programming Language :: Python
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries
Requires: PyTango (>=7.2.3)
-Requires: taurus (>= 2.1)
+Requires: taurus (>= 3.1)
Requires: lxml (>=2.1)
-Requires: ipython (>=0.10)
+Requires: ipython (>=0.10, !=0.11)
Provides: sardana
Provides: sardana.pool
Provides: sardana.macroserver
diff --git a/doc/source/_static/sardana_sketch.png b/doc/source/_static/sardana_sketch.png
new file mode 100644
index 0000000..3cd2d13
Binary files /dev/null and b/doc/source/_static/sardana_sketch.png differ
diff --git a/doc/source/_static/snapshot11.png b/doc/source/_static/snapshot11.png
deleted file mode 100644
index faac55a..0000000
Binary files a/doc/source/_static/snapshot11.png and /dev/null differ
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 6b77b24..a6d3d79 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -290,7 +290,7 @@ intersphinx_mapping = {
'http://ipython.org/ipython-doc/stable/' : None,
'http://www.tango-controls.org/static/PyTango/latest/doc/html' : None,
'http://www.tango-controls.org/static/taurus/latest/doc/html' : None,
- 'http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html' : None,
+ 'http://pyqt.sourceforge.net/Docs/PyQt4/' : None,
'http://matplotlib.sourceforge.net/' : None,
'http://packages.python.org/guiqwt/' : None,
}
diff --git a/doc/source/devel/api/sardana/macroserver/macros.rst b/doc/source/devel/api/sardana/macroserver/macros.rst
index 3be2752..f28f96c 100644
--- a/doc/source/devel/api/sardana/macroserver/macros.rst
+++ b/doc/source/devel/api/sardana/macroserver/macros.rst
@@ -67,7 +67,7 @@
The step size for each motor is (start_pos-final_pos)/nr_interv.
The number of data points collected will be nr_interv+1.
Count time is given by time which if positive, specifies seconds and
- if negative, specifies monitor counts."""
+ if negative, specifies monitor counts.
.. class:: scan.ascan
@@ -95,8 +95,9 @@
will be nr_interv+1. Count time is given by time which if positive,
specifies seconds and if negative, specifies monitor counts.
-.. autoclass:: demo.clear_sar_demo
+.. class:: demo.clear_sar_demo
+ Undoes changes done with sar_demo
.. class:: expert.commit_ctrllib
@@ -211,7 +212,7 @@
The step size for each motor is (start_pos-final_pos)/nr_interv.
The number of data points collected will be nr_interv+1.
Count time is given by time which if positive, specifies seconds and
- if negative, specifies monitor counts."""
+ if negative, specifies monitor counts.
.. class:: scan.dscan
@@ -375,8 +376,9 @@
Lists all existing motors
-.. autoclass:: env.lsvo
+.. class:: env.lsvo
+ Lists the view options
.. class:: mca.mca_start
@@ -404,7 +406,9 @@
2d grid scan. scans continuous
-.. autoclass:: standard.mstate
+.. class:: standard.mstate
+
+ Prints the state of a motor
.. class:: standard.mv
@@ -469,8 +473,9 @@
Logs a new record into the message report system (if active)
-.. autoclass:: demo.sar_demo
+.. class:: demo.sar_demo
+ Sets up a demo environment. It creates many elements for testing
.. class:: expert.sar_info
diff --git a/doc/source/devel/guide_coding.rst b/doc/source/devel/guide_coding.rst
index dddc570..7b56ef6 100644
--- a/doc/source/devel/guide_coding.rst
+++ b/doc/source/devel/guide_coding.rst
@@ -14,21 +14,25 @@ development of sardana. So if you want to help out, read on!
How to contribute to sardana
----------------------------
-Sardana development is done using SVN. Because sardana is part of Tango_, it uses
-its `tango-cs sourceforge project <https://sourceforge.net/projects/tango-cs/>`_
-to host the source code. This makes it easy for people to contribute to the
-development of sardana.
+Sardana development is managed with the `Sardana sourceforge project
+<https://sourceforge.net/projects/sardana/>`_. Until release 1.2, a svn
+repository has been used to host the source code. From then on, the version
+control will be moved to a git repository.
How to checkout sardana from SVN
--------------------------------
+.. warning:: These instructions will become obsolete as soon as Sardana 1.2
+ is released because the code development will be moved from SVN to
+ Git. Updated instructions for using Git will be posted ASAP.
+
**For read-only**::
- svn co https://tango-cs.svn.sourceforge.net/svnroot/tango-cs/share/Sardana/trunk Sardana
+ svn co https://svn.code.sf.net/p/sardana/code/trunk Sardana
**To being able to commit**::
- svn co https://<user name>@tango-cs.svn.sourceforge.net/svnroot/tango-cs/share/Sardana/trunk Sardana
+ svn co https://<user name>@svn.code.sf.net/p/sardana/code/trunk Sardana
.. note::
diff --git a/doc/source/users/faq.rst b/doc/source/users/faq.rst
index 7079da1..ac8c1ee 100644
--- a/doc/source/users/faq.rst
+++ b/doc/source/users/faq.rst
@@ -1,18 +1,28 @@
.. _sardana-faq:
+
+.. todo:: The FAQ is work-in-progress. Many answers need polishing and mostly
+ links need to be added
+
+
===
FAQ
===
What is the Sardana SCADA_ and how do I get an overview over the different components?
---------------------------------------------------------------------------------------
-An overview over the different Sardana components can be found here **<LINK>**.
+An overview over the different Sardana components is shown in the following figure:
+
+.. image:: /_static/sardana_sketch.png
+ :align: center
+ :width: 500
+
The basic Sardana SCADA_ philosophy can be found :ref:`here <sardana-overview>`.
How do I install Sardana?
-------------------------
-The Sardan SCADA_ system consists of different components which have to be
+The Sardana SCADA_ system consists of different components which have to be
installed:
* Tango_: The control system middleware and tools
@@ -26,7 +36,7 @@ The complete sardana installation instructions can be found
How to work with Taurus_ :term:`GUI`?
-------------------------------------
A user documentation for the Taurus_ :term:`GUI` application can be found
-`here <http://packages.python.org/taurus/>`_.
+`here <http://packages.python.org/taurus/>`__.
How to produce your own Taurus_ :term:`GUI` panel?
--------------------------------------------------
@@ -34,10 +44,10 @@ How to produce your own Taurus_ :term:`GUI` panel?
The basic philosophy of Taurus_ :term:`GUI` is to provide automatic
:term:`GUI` s which are automatically replaced by more and more specific
:term:`GUI` s if these are found.
-The documentation how to create a generic panel which can be filled via
-selection and cut and paste can be found here **<LINK>**.
-A more advanced usage is to create a Taurus_ widget and ingrate it into the
-application. Documentation for this approach can be found here **<LINK>**.
+
+Refer to the `user documentation on TaurusGUI <http://www.tango-
+controls.org/static/taurus/latest/doc/html/users/ui/taurusgui.html>`_ for more
+details on how to work with panels
How to call procedures?
-----------------------
@@ -46,12 +56,11 @@ The execution can be started from either:
* *spock* offers a command line interface with commands very similar to SPEC_.
It is documented :ref:`here <sardana-spock>`.
- * Procedures can also be executed with a macro executor :term:`GUI`.
- This :term:`GUI` interface offering input from the keyboard and the generic
- widgets is documented here **<LINK>**. A macro can be associated with a
- specific :term:`GUI` interface. This mechanism is documented here **<LINK>**.
+ * Procedures can also be executed with from a :term:`GUI`. Taurus provides
+ `generic widgets for macro execution <http://www.tango-
+ controls.org/static/taurus/latest/doc/html/users/ui/macros/>`__.
* Procedures can also be executed in specific :term:`GUI` s and specific Taurus_
- widgets. The :term:`API` to execute macros from this python code is documented
+ widgets. The :term:`API` to execute macros from python code is documented
here **<LINK>**.
How to write procedures?
@@ -64,16 +73,15 @@ Macro writers might also find the following documentation interesting:
* In addition of the strength of the python language macro writers can
interface with common elements (motors, counters) , call other macros
and use many utilities provided. The macro :term:`API` can be found
- :class:`here <MacroServer.macro.Macro>`.
+ :ref:`here <sardana-macro-api>`.
* Documentation how to document your macros can be found
:ref:`here <sardana-macros-howto>`
How to write scan procedures?
-----------------------------
-A very common type of procedure is the *ascan* where some quantity is
-varied while recording some other quantities. Many common types of
-general-purpose scans procedures are already available in Sardana **<LINK>**,
-and a simple :term:`API` is provided for writing more specific ones.
+A very common type of procedure is the *scan* where some quantity is
+varied while recording some other quantities. See the documentation on the
+:ref:`Sardana Scan API <sardana-macros-scanframework>`
How to adapt SARDANA to your own hardware?
------------------------------------------
diff --git a/doc/source/users/getting_started/installing.rst b/doc/source/users/getting_started/installing.rst
index 203cb7f..282a361 100644
--- a/doc/source/users/getting_started/installing.rst
+++ b/doc/source/users/getting_started/installing.rst
@@ -14,8 +14,8 @@ Installing
#. Download the sardana source code:
#. from latest stable version of `sardana <http://pypi.python.org/pypi/sardana>`_ (|version|)
- #. from `SVN snapshot <http://tango-cs.svn.sourceforge.net/viewvc/tango-cs/share/Sardana/trunk/?view=tar>`_
-
+ #. from `SVN snapshot <https://sourceforge.net/p/sardana/code/HEAD/tarball?path=/trunk>`_
+
#. Extract the downloaded tar.gz into a temporary directory
#. type [2]_ ::
@@ -45,7 +45,7 @@ necessary to run sardana on your windows machine
#. from scratch:
#. Download and install `PyQwt`_ < 6.0 from `PyQwt downdoad page <http://pyqwt.sourceforge.net/download.html>`_
- #. Download and install compatible python with from link in the same `PyQwt`_ page
+ #. Download and install compatible python from link in the same `PyQwt`_ page
#. Download and install compatible `numpy`_ from link in the same `PyQwt`_ page.
#. Download and install compatible `PyQt`_ from link in the same `PyQwt`_ page.
@@ -58,6 +58,10 @@ necessary to run sardana on your windows machine
Working from SVN
----------------
+.. warning:: These instructions will become obsolete as soon as Sardana 1.2
+ is released because the code development will be moved from SVN to
+ Git. Updated instructions for using Git will be posted ASAP.
+
You can checkout sardana from SVN from the following location::
svn co http://svn.code.sf.net/p/sardana/code/trunk Sardana
diff --git a/doc/source/users/getting_started/running_cli.rst b/doc/source/users/getting_started/running_cli.rst
index 1a5ff09..d21e2f7 100644
--- a/doc/source/users/getting_started/running_cli.rst
+++ b/doc/source/users/getting_started/running_cli.rst
@@ -40,7 +40,7 @@ you should get an output like this::
Door_lab-01_1 [1]:
-That't it! You now have a running sardana client. Still not impressed, I see!
+That's it! You now have a running sardana client. Still not impressed, I see!
The next chapter describes how to start adding new elements to your sardana
environment.
diff --git a/doc/source/users/getting_started/running_server.rst b/doc/source/users/getting_started/running_server.rst
index 50382d8..e1b4ffc 100644
--- a/doc/source/users/getting_started/running_server.rst
+++ b/doc/source/users/getting_started/running_server.rst
@@ -47,7 +47,7 @@ Running Pool and MacroServer tango servers separately
.. note::
- You should only read this chapter if you are if you have Tango <= 7.2.6
+ You should only read this chapter if you have Tango <= 7.2.6
without all patches applied. If you do, please follow in instructions from
:ref:`sardana-getting-started-running-server` instead.
diff --git a/doc/source/users/overview.rst b/doc/source/users/overview.rst
index 53f983f..7f31ed7 100644
--- a/doc/source/users/overview.rst
+++ b/doc/source/users/overview.rst
@@ -209,7 +209,7 @@ Symbolic Sketch
I would like to end this summary with a symbolic sketch of the different
subsystems in Sardana.
-.. image:: /_static/snapshot11.png
+.. image:: /_static/sardana_sketch.png
:align: center
:width: 500
diff --git a/doc/source/users/scan.rst b/doc/source/users/scan.rst
index 79a6c32..3996ac0 100644
--- a/doc/source/users/scan.rst
+++ b/doc/source/users/scan.rst
@@ -69,8 +69,8 @@ wait for motors to accelerate and decelerate between acquisitions.
.. note:: The synchronization of movement and acquisition can be done via
hardware or via software. Currently Sardana only provides an interface for
software-synchronized continuous scans. An API abstracting the specificities
- of hardware-synchronized systems has not been yet implemented, and therefore
- it cannot be supported by Sardana yet.
+ of hardware-synchronized systems is being implemented too but it is not yet
+ available for production.
The (software-synchronized) continuous scans introduce some constraints and
issues that should be considered.
diff --git a/scripts/h5toascii b/scripts/h5toascii
index ddb3ef8..4a42be1 100755
--- a/scripts/h5toascii
+++ b/scripts/h5toascii
@@ -1,5 +1,28 @@
#!/usr/bin/env python
+##############################################################################
+##
+## This file is part of Sardana
+##
+## http://www.tango-controls.org/static/sardana/latest/doc/html/index.html
+##
+## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
+##
+## Sardana is free software: you can redistribute it and/or modify
+## it under the terms of the GNU Lesser General Public License as published by
+## the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## Sardana is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public License
+## along with Sardana. If not, see <http://www.gnu.org/licenses/>.
+##
+##############################################################################
+
'''
Script for extracting "scan tables" from Sardana NeXus files into an tab-separated ASCII tables.
When data from multiple entries are extracted from a file, each table is stored in a different file.
diff --git a/scripts/h5tospec b/scripts/h5tospec
index 0dcb85a..724ada0 100755
--- a/scripts/h5tospec
+++ b/scripts/h5tospec
@@ -1,5 +1,28 @@
#!/usr/bin/env python
+##############################################################################
+##
+## This file is part of Sardana
+##
+## http://www.tango-controls.org/static/sardana/latest/doc/html/index.html
+##
+## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
+##
+## Sardana is free software: you can redistribute it and/or modify
+## it under the terms of the GNU Lesser General Public License as published by
+## the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## Sardana is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public License
+## along with Sardana. If not, see <http://www.gnu.org/licenses/>.
+##
+##############################################################################
+
'''
Script for extracting "scans" from Sardana NeXus files into Sardana Spec Files.
@@ -33,7 +56,7 @@ def _getMovables(mystr):
_a.pop(0)
for i in _a:
if re.match('[^-_0-9\.\+\\n]',i):
- _m.append(i)
+ _m.append(i)
return _m
def getMetadata(fd, entry):
diff --git a/scripts/spectoascii b/scripts/spectoascii
new file mode 100755
index 0000000..875a493
--- /dev/null
+++ b/scripts/spectoascii
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+
+##############################################################################
+##
+## This file is part of Sardana
+##
+## http://www.tango-controls.org/static/sardana/latest/doc/html/index.html
+##
+## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
+##
+## Sardana is free software: you can redistribute it and/or modify
+## it under the terms of the GNU Lesser General Public License as published by
+## the Free Software Foundation, either version 3 of the License, or
+## (at your option) any later version.
+##
+## Sardana is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public License
+## along with Sardana. If not, see <http://www.gnu.org/licenses/>.
+##
+##############################################################################
+
+'''
+Script for extracting "scan tables" from Sardana Spec files into single-scan tab-separated ASCII tables.
+When data from multiple scans are extracted from a file, each table is stored in a different file.
+The output files for a given input file are stored in a directory whose name matches the input scan ID
+
+Usage:
+python spectoascii.py <specfile> [<scan_ID_1> [<scan_ID_2>] ...]
+
+If no scan IDs are provided, all scans from the given Spec file will be extracted.
+
+Note that this is not a generic Spec conversor. Spec files created by means other than the Sardana
+SPEC_FileRecorder may not be converted properly.
+
+'''
+
+
+import sys,os
+
+def main():
+ if len(sys.argv) > 1:
+ fname = sys.argv[1]
+ else:
+ print "Usage:\nspectoascii <specfile> [<scan_ID_1> [<scan_ID_2>] ...] "
+ sys.exit(1)
+
+ scanids = sys.argv[2:] or None
+
+ f = open(fname,'r')
+ s = f.read()
+ f.close()
+
+ dirname,ext = os.path.splitext(fname)
+ try:
+ os.makedirs(dirname)
+ except:
+ print 'Cannot create dir "%s". Skipping.'%dirname
+
+ for scan in s.split('\n\n'):
+ lines = scan.split('\n')
+ if not lines[0].startswith('#S '):
+ continue
+ scan_name = lines[0].split()[1]
+ if scanids is None or scan_name in scanids: #extract only selected ids
+ ofname = os.path.join(dirname,"%s.dat"%scan_name)
+ print "Extracting %s:%s to %s"%(fname,scan_name,ofname)
+ try:
+ ofile = open(ofname,'w')
+ except:
+ print 'Cannot create file "%s". Skipping.'%ofname
+ continue
+ for line in lines[1:]:
+ if line.startswith('#L '):
+ ofile.write('\t'.join(line[3:].split(' '))+'\n')
+ elif line.startswith('#'):
+ continue
+ else:
+ ofile.write('\t'.join(line.split())+'\n')
+ ofile.close()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 232a4d4..915461c 100644
--- a/setup.py
+++ b/setup.py
@@ -36,15 +36,14 @@ from distutils.core import setup, Extension, Command
from distutils.dist import Distribution
from distutils.command.build import build as dftbuild
from distutils.command.install import install as dftinstall
-
+from distutils.version import StrictVersion as V
import distutils.sysconfig
try:
import sphinx
import sphinx.util.console
sphinx.util.console.color_terminal = lambda : False
- sphinx_v = tuple(map(int, sphinx.__version__.split(".",3)))
- if sphinx_v < (1,0,0):
+ if V(sphinx.__version__) < V("1.0.0"):
sphinx = None
except:
sphinx = None
@@ -245,6 +244,8 @@ def main():
Release = get_release_info()
author = Release.authors['Tiago']
+ maintainer = Release.authors['Pascual-Izarra']
+
package_name = Release.name
package_dir = { 'sardana' : abspath('src', 'sardana') }
@@ -290,9 +291,9 @@ def main():
requires = [
'PyTango (>=7.2.3)',
- 'taurus (>= 2.1)',
+ 'taurus (>= 3.1)',
'lxml (>=2.1)',
- 'ipython (>=0.10)'
+ 'ipython (>=0.10, !=0.11)'
]
package_data = {
@@ -328,6 +329,8 @@ def main():
long_description = Release.long_description,
author = author[0],
author_email = author[1],
+ maintainer = maintainer[0],
+ maintainer_email = maintainer[1],
url = Release.url,
download_url = Release.download_url,
platforms = Release.platforms,
diff --git a/src/sardana/macroserver/macro.py b/src/sardana/macroserver/macro.py
index b112a4c..c730d3d 100644
--- a/src/sardana/macroserver/macro.py
+++ b/src/sardana/macroserver/macro.py
@@ -1618,7 +1618,7 @@ class Macro(Logger):
#-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-
@mAPI
- def getEnv(self, key, macro_name=None, door_name=None):
+ def getEnv(self, key=None, macro_name=None, door_name=None):
"""**Macro API**. Gets the local environment matching the given
parameters:
diff --git a/src/sardana/macroserver/macros/demo.py b/src/sardana/macroserver/macros/demo.py
index 5a4130f..5275b1f 100644
--- a/src/sardana/macroserver/macros/demo.py
+++ b/src/sardana/macroserver/macros/demo.py
@@ -52,6 +52,7 @@ def get_free_names(db, prefix, nb, start_at=1):
@macro()
def clear_sar_demo(self):
+ """Undoes changes done with sar_demo"""
try:
SAR_DEMO = self.getEnv(_ENV)
except:
@@ -76,6 +77,7 @@ def clear_sar_demo(self):
@macro()
def sar_demo(self):
+ """Sets up a demo environment. It creates many elements for testing"""
try:
SAR_DEMO = self.getEnv(_ENV)
diff --git a/src/sardana/macroserver/macros/env.py b/src/sardana/macroserver/macros/env.py
index a6edd7a..35244db 100644
--- a/src/sardana/macroserver/macros/env.py
+++ b/src/sardana/macroserver/macros/env.py
@@ -62,7 +62,7 @@ class dumpenv(Macro):
class lsvo(Macro):
-
+ """Lists the view options"""
def run(self):
vo = self.getViewOptions()
out = List(['View option', 'Value'])
@@ -121,12 +121,11 @@ class lsenv(Macro):
else:
# list the environment for the current door for the given macros
out = List(['Macro', 'Name', 'Value', 'Type'])
- for macro_name in macro_list:
- macro_env = m.getMacroClass(macro_name).env
- env = m.getDoorMacroEnv(door_name, macro_name, macro_env)
+ for macro in macro_list:
+ env = self.getEnv(key=None, macro_name=macro.name)
for k, v in env.iteritems():
type_name = type(v).__name__
- out.appendRow([ macro_name, k, self.reprValue(v), type_name ])
+ out.appendRow([ macro.name, k, self.reprValue(v), type_name ])
for line in out.genOutput():
self.output(line)
diff --git a/src/sardana/macroserver/macros/examples/hooks.py b/src/sardana/macroserver/macros/examples/hooks.py
index cb2992e..9789fdd 100644
--- a/src/sardana/macroserver/macros/examples/hooks.py
+++ b/src/sardana/macroserver/macros/examples/hooks.py
@@ -99,7 +99,7 @@ class hooked_scan(Macro):
demonstration purposes"""
param_def = [
- ['motor', Type.Motor, None, 'Motor to move'],
+ ['motor', Type.Moveable,None, 'Motor to move'],
['start_pos', Type.Float, None, 'Scan start position'],
['final_pos', Type.Float, None, 'Scan final position'],
['nr_interv', Type.Integer, None, 'Number of scan intervals'],
@@ -114,9 +114,23 @@ class hooked_scan(Macro):
def hook3(self):
self.info("\thook3 execution")
+ def hook4(self):
+ self.info("\thook4 execution")
+
+ def hook5(self):
+ self.info("\thook5 execution")
+
+ def hook6(self):
+ self.info("\thook6 execution")
+
def run(self, motor, start_pos, final_pos, nr_interv, integ_time):
ascan, pars = self.createMacro("ascan",motor, start_pos, final_pos, nr_interv, integ_time)
- ascan.hooks = [ (self.hook1, ["pre-acq"]), (self.hook2, ["pre-acq","post-acq","pre-move", "post-move","aaaa"]), self.hook3 ]
+ ascan.hooks = [ (self.hook1, ["pre-acq"]),
+ (self.hook2, ["pre-acq","post-acq","pre-move", "post-move","aaaa"]),
+ self.hook3,
+ (self.hook4, ["pre-scan"]),
+ (self.hook5, ["pre-scan", "post-scan"]),
+ (self.hook6, ["post-step"])]
self.runMacro(ascan)
diff --git a/src/sardana/macroserver/macros/examples/scans.py b/src/sardana/macroserver/macros/examples/scans.py
index 2f2a103..57fbcc0 100644
--- a/src/sardana/macroserver/macros/examples/scans.py
+++ b/src/sardana/macroserver/macros/examples/scans.py
@@ -564,4 +564,42 @@ class ascanc_demo(Macro):
def run(self,*args):
for step in self._gScan.step_scan():
yield step
+
+
+
+class ascan_with_addcustomdata(ascan_demo):
+ '''
+ example of an ascan-like macro where we demonstrate how to pass custom data to the data handler.
+ This is an extension of the ascan_demo macro. Wemake several calls to `:meth:DataHandler.addCustomData`
+ exemplifying different features.
+ At least the following recorders will act on custom data:
+ - OutputRecorder (this will ignore array data)
+ - NXscan_FileRecorder
+ - SPEC_FileRecorder (this will ignore array data)
+ '''
+ def run(self, motor, start_pos, final_pos, nr_interv, integ_time, **opts):
+ #we get the datahandler
+ dh = self._gScan._data_handler
+ #at this point the entry name is not yet set, so we give it explicitly (otherwise it would default to "entry")
+ dh.addCustomData('Hello world1', 'dummyChar1', nxpath='/custom_entry:NXentry/customdata:NXcollection')
+ #this is the normal scan loop
+ for step in self._gScan.step_scan():
+ yield step
+ #the entry number is known and the default nxpath is used "/<currententry>/custom_data") if none given
+ dh.addCustomData('Hello world1', 'dummyChar1')
+ #you can pass arrays (but not all recorders will handle them)
+ dh.addCustomData(range(10), 'dummyArray1')
+ #you can pass a custom nxpath *relative* to the current entry
+ dh.addCustomData('Hello world2', 'dummyChar2', nxpath='sample:NXsample')
+
+ #calculate a linear fit to the timestamps VS motor positions and store it
+ x = [r.data [motor.getName()] for r in self.data.records]
+ y = [r.data['timestamp'] for r in self.data.records]
+ fitted_y = numpy.polyval(numpy.polyfit(x,y,1), x)
+ dh.addCustomData(fitted_y, 'fittedtime', nxpath='measurement:NXcollection')
+
+ #as a bonus, plot the fit
+ self.pyplot.plot(x, y, 'ro')
+ self.pyplot.plot(x, fitted_y, 'b-')
+
\ No newline at end of file
diff --git a/src/sardana/macroserver/macros/scan.py b/src/sardana/macroserver/macros/scan.py
index 77cb04a..f38e1be 100644
--- a/src/sardana/macroserver/macros/scan.py
+++ b/src/sardana/macroserver/macros/scan.py
@@ -39,6 +39,7 @@ __all__ = ["a2scan", "a3scan", "a4scan", "amultiscan", "aNscan", "ascan",
"a2scanc", "a3scanc", "a4scanc", "ascanc",
"d2scanc", "d3scanc", "d4scanc", "dNScanc", "dscanc",
"meshc",
+ "a2scanct", "a3scanct", "a4scanct", "ascanct",
"scanhist", "getCallable", "UNCONSTRAINED"]
__docformat__ = 'restructuredtext'
@@ -57,11 +58,13 @@ from sardana.macroserver.msexception import UnknownEnv
from sardana.macroserver.macro import *
from sardana.macroserver.scan import *
from sardana.util.motion import Motor, MotionPath
+from sardana.util.tree import BranchNode, LeafNode, Tree
UNCONSTRAINED="unconstrained"
StepMode = 's'
-ContinuousMode = 'c'
+ContinuousMode = 'c' #TODO: change it to be more verbose e.g. ContinuousSwMode
+ContinuousHwTimeMode = 'ct'
HybridMode = 'h'
def getCallable(repr):
@@ -106,8 +109,9 @@ class aNscan(Hookable):
constrains = [getCallable(cns) for cns in opts.get('constrains',[UNCONSTRAINED])]
extrainfodesc = opts.get('extrainfodesc',[])
- self.pre_scan_hooks = self.getHooks('pre-scan')
- self.post_scan_hooks = self.getHooks('post-scan')
+ #Hooks are not always set at this point. We will call getHooks later on in the scan_loop
+ #self.pre_scan_hooks = self.getHooks('pre-scan')
+ #self.post_scan_hooks = self.getHooks('post-scan'
if mode == StepMode:
self.nr_interv = scan_length
@@ -115,12 +119,28 @@ class aNscan(Hookable):
self.interv_sizes = ( self.finals - self.starts) / self.nr_interv
self.name = opts.get('name','a%iscan'%self.N)
self._gScan = SScan(self, self._stepGenerator, moveables, env, constrains, extrainfodesc)
- elif mode == ContinuousMode:
- self.slow_down = scan_length
- self.nr_waypoints = 2 #aNscans will only have two waypoints (the start and the final positions)
- self.way_lengths = ( self.finals - self.starts) / (self.nr_waypoints -1)
- self.name = opts.get('name','a%iscanc'%self.N)
- self._gScan = CScan(self, self._waypoint_generator, self._period_generator, moveables, env, constrains, extrainfodesc)
+ elif mode in [ContinuousMode, ContinuousHwTimeMode]:
+ #TODO: probably not 100% correct,
+ # the idea is to allow passing a list of waypoints
+ if isinstance(endlist[0],list):
+ self.waypoints = self.finals
+ else:
+ self.waypoints = [self.finals]
+ self.nr_waypoints = len(self.waypoints)
+ if mode == ContinuousMode:
+ self.slow_down = scan_length
+ self.nr_waypoints = 2 #aNscans will only have two waypoints (the start and the final positions)
+ self.way_lengths = ( self.finals - self.starts) / (self.nr_waypoints -1)
+ self.name = opts.get('name','a%iscanc'%self.N)
+ self._gScan = CSScan(self, self._waypoint_generator, self._period_generator, moveables, env, constrains, extrainfodesc)
+ elif mode == ContinuousHwTimeMode:
+ self.nr_of_points = scan_length
+ self.name = opts.get('name','a%iscanct'%self.N)
+ self._gScan = CTScan(self, self._waypoint_generator_hwtime,
+ moveables,
+ env,
+ constrains,
+ extrainfodesc)
elif mode == HybridMode:
self.nr_interv = scan_length
self.nr_points = self.nr_interv+1
@@ -155,7 +175,63 @@ class aNscan(Hookable):
step["positions"] = self.starts + point_no * self.way_lengths
step["waypoint_id"] = point_no
yield step
-
+
+ def _waypoint_generator_hwtime(self):
+ #TODO: remove starts
+ def calculate_positions(moveable_node, start, end):
+ '''Function to calculate starting and ending positions on the physical
+ motors level.
+ :param moveable_node: (BaseNode) node representing a moveable.
+ Can be a BranchNode representing a PseudoMotor,
+ or a LeafNode representing a PhysicalMotor).
+ :param start: (float) starting position of the moveable
+ :param end: (float) ending position of the moveable
+
+ :return: (list<(float,float)>) a list of tuples comprising starting
+ and ending positions. List order is important and preserved.'''
+ start_positions = []
+ end_positions = []
+ if isinstance(moveable_node, BranchNode):
+ pseudo_node = moveable_node
+ moveable = pseudo_node.data
+ moveable_nodes = moveable_node.children
+ starts = moveable.calcPhysical(start)
+ ends = moveable.calcPhysical(end)
+ for moveable_node, start, end in zip(moveable_nodes, starts,
+ ends):
+ _start_positions, _end_positions = calculate_positions(
+ moveable_node,
+ start, end)
+ start_positions += _start_positions
+ end_positions += _end_positions
+ else:
+ start_positions = [start]
+ end_positions = [end]
+
+ return start_positions, end_positions
+
+ #CScan in its constructor populates a list of data structures - trees.
+ #Each tree represent one Moveables with its hierarchy of inferior moveables.
+ moveables_trees = self._gScan.get_moveables_trees()
+ step = {}
+ step["pre-move-hooks"] = self.getHooks('pre-move')
+ step["post-move-hooks"] = self.getHooks('post-move')
+ step["check_func"] = []
+ step["active_time"] = self.nr_of_points * self.integ_time
+ step["positions"] = []
+ step["start_positions"] = []
+ starts = self.starts
+ for point_no, waypoint in enumerate(self.waypoints):
+ for start, end, moveable_tree in zip(starts, waypoint,
+ moveables_trees):
+ moveable_root = moveable_tree.root()
+ start_positions, end_positions = calculate_positions(moveable_root, start, end)
+ step["start_positions"] += start_positions
+ step["positions"] += end_positions
+ step["waypoint_id"] = point_no
+ starts = waypoint
+ yield step
+
def _period_generator(self):
step = {}
step["integ_time"] = self.integ_time
@@ -202,6 +278,7 @@ class aNscan(Hookable):
elif mode == ContinuousMode:
total_time = gScan.waypoint_estimation()
+ #TODO: add time estimation for ContinuousHwTimeMode
return total_time
def getIntervalEstimation(self):
@@ -553,8 +630,9 @@ class mesh(Macro,Hookable):
env=opts.get('env',{})
constrains=[getCallable(cns) for cns in opts.get('constrains',[UNCONSTRAINED])]
- self.pre_scan_hooks = self.getHooks('pre-scan')
- self.post_scan_hooks = self.getHooks('post-scan')
+ #Hooks are not always set at this point. We will call getHooks later on in the scan_loop
+ #self.pre_scan_hooks = self.getHooks('pre-scan')
+ #self.post_scan_hooks = self.getHooks('post-scan')
self._gScan=SScan(self, generator, moveables, env, constrains)
@@ -580,7 +658,7 @@ class mesh(Macro,Hookable):
space = m1_space_inv
for m1pos in space:
step["positions"] = numpy.array([m1pos,m2pos])
- step["point_id"]= point_no #@TODO: maybe another ID would be better? (e.g. "(A,B)")
+ step["point_id"]= point_no #TODO: maybe another ID would be better? (e.g. "(A,B)")
point_no+=1
yield step
@@ -670,8 +748,10 @@ class fscan(Macro,Hookable):
env=opts.get('env',{})
constrains=[getCallable(cns) for cns in opts.get('constrains',[UNCONSTRAINED])]
- self.pre_scan_hooks = self.getHooks('pre-scan')
- self.post_scan_hooks = self.getHooks('post-scan')
+ #Hooks are not always set at this point. We will call getHooks later on in the scan_loop
+ #self.pre_scan_hooks = self.getHooks('pre-scan')
+ #self.post_scan_hooks = self.getHooks('post-scan'
+
self._gScan=SScan(self, generator, moveables, env, constrains)
def _generator(self):
@@ -1020,8 +1100,9 @@ class meshc(Macro,Hookable):
constrains=[getCallable(cns) for cns in opts.get('constrains',[UNCONSTRAINED])]
extrainfodesc = opts.get('extrainfodesc',[])
- self.pre_scan_hooks = self.getHooks('pre-scan')
- self.post_scan_hooks = self.getHooks('post-scan')
+ #Hooks are not always set at this point. We will call getHooks later on in the scan_loop
+ #self.pre_scan_hooks = self.getHooks('pre-scan')
+ #self.post_scan_hooks = self.getHooks('post-scan'
self._gScan = CScan(self, self._waypoint_generator, self._period_generator, moveables, env, constrains, extrainfodesc)
self._gScan.frozen_motors = [m2]
@@ -1073,3 +1154,210 @@ class meshc(Macro,Hookable):
@property
def data(self):
return self._gScan.data
+
+class ascanct(aNscan, Macro):
+ '''Continuous scan controlled by hardware trigger signals. A sequence of
+ trigger pulses is programmed by time. The scan active time is calculated from
+ nr_of_points * point time. It corresponds to the time while all the involved
+ in the scan moveables are at the constant velocity. Experimental channels are
+ configured to acquire during acquistion time calculated from acq_time [%] of
+ the point_time.
+ Temporary solution used to configure trigger device (pulse train generator):
+ "TriggerDevices" evironment variable must be set to Ni660X device name
+ or any other Tango device name implementing:
+ +) following attributes:
+ - InitialDelayTime [s] - delay time from calling Start to generating first pulse
+ - HighTime [s] - time interval while signal will maintain its high state
+ - LowTime [s] - time interval while signal will maintain its low state
+ - SampPerChan - nr of pulses to be generated
+ - IdleState - state (high or low) which signal will take after the Start command
+ and which will maintain during the InitialDelayTime.
+ +) following commands:
+ - Start
+ - Stop)'''
+
+ hints = {'scan' : 'ascanct', 'allowsHooks': ('pre-configuration',
+ 'post-configuration',
+ 'pre-start',
+ 'pre-cleanup',
+ 'post-cleanup') }
+
+ param_def = [['motor', Type.Moveable, None, 'Moveable name'],
+ ['start_pos', Type.Float, None, 'Starting position'],
+ ['end_pos', Type.Float, None, 'Ending pos value'],
+ ['nr_of_points', Type.Integer, None, 'Nr of scan points'],
+ ['point_time', Type.Float, None, 'Time interval reserved for ' +
+ 'each scan point [s].'],
+ ['acq_time', Type.Float, 99, 'Acquisition time per scan point. ' +
+ 'Expressed in percentage of point_time. Default: 99 [%]'],
+ ['samp_freq', Type.Float, -1, 'Sampling frequency. ' +
+ 'Default: -1 (means maximum possible)']]
+
+
+ def prepare(self, motor, start_pos, end_pos, nr_of_points,
+ point_time, acq_time, samp_freq, **opts):
+ self._prepare([motor], [start_pos], [end_pos], nr_of_points,
+ point_time, mode=ContinuousHwTimeMode, **opts)
+ self.acq_time = acq_time
+ self.samp_freq = samp_freq
+
+
+class a2scanct(aNscan, Macro):
+ '''Continuous scan controlled by hardware trigger signals. A sequence of
+ trigger pulses is programmed by time. The scan active time is calculated from
+ nr_of_points * point time. It corresponds to the time while all the involved
+ in the scan moveables are at the constant velocity. Experimental channels are
+ configured to acquire during acquistion time calculated from acq_time [%] of
+ the point_time.
+ Temporary solution used to configure trigger device (pulse train generator):
+ "TriggerDevices" evironment variable must be set to Ni660X device name
+ or any other Tango device name implementing:
+ +) following attributes:
+ - InitialDelayTime [s] - delay time from calling Start to generating first pulse
+ - HighTime [s] - time interval while signal will maintain its high state
+ - LowTime [s] - time interval while signal will maintain its low state
+ - SampPerChan - nr of pulses to be generated
+ - IdleState - state (high or low) which signal will take after the Start command
+ and which will maintain during the InitialDelayTime.
+ +) following commands:
+ - Start
+ - Stop)'''
+
+ hints = {'scan' : 'a2scanct', 'allowsHooks': ('pre-configuration',
+ 'post-configuration',
+ 'pre-start',
+ 'pre-cleanup',
+ 'post-cleanup') }
+
+ param_def = [
+ ['motor1', Type.Moveable, None, 'Moveable 1 to move'],
+ ['start_pos1', Type.Float, None, 'Scan start position 1'],
+ ['final_pos1', Type.Float, None, 'Scan final position 1'],
+ ['motor2', Type.Moveable, None, 'Moveable 2 to move'],
+ ['start_pos2', Type.Float, None, 'Scan start position 2'],
+ ['final_pos2', Type.Float, None, 'Scan final position 2'],
+ ["nr_of_points", Type.Integer, None, "Nr of scan points"],
+ ['point_time', Type.Float, None, 'Time interval reserved for ' +
+ 'each scan point [s].'],
+ ["acq_time", Type.Float, 99, 'Acquisition time per scan point. ' +
+ 'Expressed in percentage of point_time. Default: 99 [%]'],
+ ["samp_freq", Type.Float, -1, 'Sampling frequency. ' +
+ 'Default: -1 (means maximum possible)']]
+
+ def prepare(self, m1, s1, f1, m2, s2, f2, nr_of_points,
+ point_time, acq_time, samp_freq, **opts):
+ self._prepare([m1, m2], [s1, s2], [f1, f2], nr_of_points,
+ point_time, mode=ContinuousHwTimeMode, **opts)
+ self.acq_time = acq_time
+ self.samp_freq = samp_freq
+
+
+
+class a3scanct(aNscan, Macro):
+ '''Continuous scan controlled by hardware trigger signals. A sequence of
+ trigger pulses is programmed by time. The scan active time is calculated from
+ nr_of_points * point time. It corresponds to the time while all the involved
+ in the scan moveables are at the constant velocity. Experimental channels are
+ configured to acquire during acquistion time calculated from acq_time [%] of
+ the point_time.
+ Temporary solution used to configure trigger device (pulse train generator):
+ "TriggerDevices" evironment variable must be set to Ni660X device name
+ or any other Tango device name implementing:
+ +) following attributes:
+ - InitialDelayTime [s] - delay time from calling Start to generating first pulse
+ - HighTime [s] - time interval while signal will maintain its high state
+ - LowTime [s] - time interval while signal will maintain its low state
+ - SampPerChan - nr of pulses to be generated
+ - IdleState - state (high or low) which signal will take after the Start command
+ and which will maintain during the InitialDelayTime.
+ +) following commands:
+ - Start
+ - Stop)'''
+ hints = {'scan' : 'a2scanct', 'allowsHooks': ('pre-configuration',
+ 'post-configuration',
+ 'pre-start',
+ 'pre-cleanup',
+ 'post-cleanup') }
+
+ param_def = [
+ ['motor1', Type.Moveable, None, 'Moveable 1 to move'],
+ ['start_pos1', Type.Float, None, 'Scan start position 1'],
+ ['final_pos1', Type.Float, None, 'Scan final position 1'],
+ ['motor2', Type.Moveable, None, 'Moveable 2 to move'],
+ ['start_pos2', Type.Float, None, 'Scan start position 2'],
+ ['final_pos2', Type.Float, None, 'Scan final position 2'],
+ ['motor3', Type.Moveable, None, 'Moveable 3 to move'],
+ ['start_pos3', Type.Float, None, 'Scan start position 3'],
+ ['final_pos3', Type.Float, None, 'Scan final position 3'],
+ ["nr_of_points", Type.Integer, None, "Nr of scan points"],
+ ['point_time', Type.Float, None, 'Time interval reserved for ' +
+ 'each scan point [s].'],
+ ["acq_time", Type.Float, 99, 'Acquisition time per scan point. ' +
+ 'Expressed in percentage of point_time. Default: 99 [%]'],
+ ["samp_freq", Type.Float, -1, 'Sampling frequency. ' +
+ 'Default: -1 (means maximum possible)']]
+
+ def prepare(self, m1, s1, f1, m2, s2, f2, m3, s3, f3, nr_of_points,
+ point_time, acq_time, samp_freq, **opts):
+ self._prepare([m1, m2, m3], [s1, s2, s3], [f1, f2, f3], nr_of_points,
+ point_time, mode=ContinuousHwTimeMode, **opts)
+ self.acq_time = acq_time
+ self.samp_freq = samp_freq
+
+class a4scanct(aNscan, Macro):
+ '''Continuous scan controlled by hardware trigger signals. A sequence of
+ trigger pulses is programmed by time. The scan active time is calculated from
+ nr_of_points * point time. It corresponds to the time while all the involved
+ in the scan moveables are at the constant velocity. Experimental channels are
+ configured to acquire during acquistion time calculated from acq_time [%] of
+ the point_time.
+ Temporary solution used to configure trigger device (pulse train generator):
+ "TriggerDevices" evironment variable must be set to Ni660X device name
+ or any other Tango device name implementing:
+ +) following attributes:
+ - InitialDelayTime [s] - delay time from calling Start to generating first pulse
+ - HighTime [s] - time interval while signal will maintain its high state
+ - LowTime [s] - time interval while signal will maintain its low state
+ - SampPerChan - nr of pulses to be generated
+ - IdleState - state (high or low) which signal will take after the Start command
+ and which will maintain during the InitialDelayTime.
+ +) following commands:
+ - Start
+ - Stop)'''
+
+ hints = {'scan' : 'a2scanct', 'allowsHooks': ('pre-configuration',
+ 'post-configuration',
+ 'pre-start',
+ 'pre-cleanup',
+ 'post-cleanup') }
+
+ param_def = [
+ ['motor1', Type.Moveable, None, 'Moveable 1 to move'],
+ ['start_pos1', Type.Float, None, 'Scan start position 1'],
+ ['final_pos1', Type.Float, None, 'Scan final position 1'],
+ ['motor2', Type.Moveable, None, 'Moveable 2 to move'],
+ ['start_pos2', Type.Float, None, 'Scan start position 2'],
+ ['final_pos2', Type.Float, None, 'Scan final position 2'],
+ ['motor3', Type.Moveable, None, 'Moveable 3 to move'],
+ ['start_pos3', Type.Float, None, 'Scan start position 3'],
+ ['final_pos3', Type.Float, None, 'Scan final position 3'],
+ ['motor4', Type.Moveable, None, 'Moveable 4 to move'],
+ ['start_pos4', Type.Float, None, 'Scan start position 4'],
+ ['final_pos4', Type.Float, None, 'Scan final position 4'],
+ ["nr_of_points", Type.Integer, None, "Nr of scan points"],
+ ['point_time', Type.Float, None, 'Time interval reserved for ' +
+ 'each scan point [s].'],
+ ["acq_time", Type.Float, 99, 'Acquisition time per scan point. ' +
+ 'Expressed in percentage of point_time. Default: 99 [%]'],
+ ["samp_freq", Type.Float, -1, 'Sampling frequency. ' +
+ 'Default: -1 (means maximum possible)']]
+
+
+ def prepare(self, m1, s1, f1, m2, s2, f2, m3, s3, f3, m4, s4, f4,
+ nr_of_points, point_time, acq_time, samp_freq, **opts):
+ self._prepare([m1, m2, m3, m4], [s1, s2, s3, s4], [f1, f2, f3, f4],
+ nr_of_points, point_time, mode=ContinuousHwTimeMode,
+ **opts)
+ self.acq_time = acq_time
+ self.samp_freq = samp_freq
+
diff --git a/src/sardana/macroserver/macros/standard.py b/src/sardana/macroserver/macros/standard.py
index d65eccf..2fbc578 100644
--- a/src/sardana/macroserver/macros/standard.py
+++ b/src/sardana/macroserver/macros/standard.py
@@ -340,11 +340,12 @@ class mv(Macro):
self.info("\n".join(msg))
class mstate(Macro):
+ """Prints the state of a motor"""
+
+ param_def = [['motor', Type.Moveable, None, 'Motor to check state']]
- param_def = [['motor', Type.Moveable, None, 'Motor to check state']]
-
- def run(self, motor):
- self.info("Motor %s" % str(motor.getState()))
+ def run(self, motor):
+ self.info("Motor %s" % str(motor.getState()))
class umv(Macro):
"""Move motor(s) to the specified position(s) and update"""
diff --git a/src/sardana/macroserver/scan/gscan.py b/src/sardana/macroserver/scan/gscan.py
index 5c5e83e..30ef9ed 100644
--- a/src/sardana/macroserver/scan/gscan.py
+++ b/src/sardana/macroserver/scan/gscan.py
@@ -27,7 +27,7 @@
scan"""
__all__ = ["ScanSetupError", "ScanException", "ExtraData", "TangoExtraData",
- "GScan", "SScan", "CScan", "HScan"]
+ "GScan", "SScan", "CScan", "CSScan", "CTScan", "HScan"]
__docformat__ = 'restructuredtext'
@@ -36,12 +36,15 @@ import datetime
import operator
import time
import threading
+import numpy as np
+import PyTango
import taurus
from taurus.core.util import USER_NAME, Logger
from taurus.core.tango import FROM_TANGO_TO_STR_TYPE
from taurus.core.tango.sardana.pool import Ready
+from sardana.util.tree import BranchNode, LeafNode, Tree
from sardana.util.motion import Motor as VMotor
from sardana.util.motion import MotionPath
from sardana.macroserver.msexception import MacroServerException, UnknownEnv, \
@@ -51,8 +54,6 @@ from scandata import ColumnDesc, MoveableDesc, ScanFactory, ScanDataEnvironment
from recorder import OutputRecorder, JsonRecorder, SharedMemoryRecorder, \
FileRecorder
-
-
class ScanSetupError(Exception): pass
class ScanException(MacroServerException): pass
@@ -136,19 +137,21 @@ class GScan(Logger):
a generator function pointer, a list of moveable items, an extra
environment and a sequence of constrains.
+ If the referenced macro is hookable, 'pre-scan' and 'post-scan' hook hints
+ will be used to execute callables before the start and after the end of the
+ scan, respectively
+
The generator must be a function yielding a dictionary with the following
content (minimum) at each step of the scan:
- 'positions' : In a step scan, the position where the moveables should go
- 'integ_time' : In a step scan, a number representing the integration time for the step
(in seconds)
- 'integ_time' : In a continuous scan, the time between acquisitions
- - 'pre-scan-hooks' : (optional) a sequence of callables to be called in strict order before starting the scan.
- 'pre-move-hooks' : (optional) a sequence of callables to be called in strict order before starting to move.
- 'post-move-hooks': (optional) a sequence of callables to be called in strict order after finishing the move.
- 'pre-acq-hooks' : (optional) a sequence of callables to be called in strict order before starting to acquire.
- 'post-acq-hooks' : (optional) a sequence of callables to be called in strict order after finishing acquisition but before recording the step.
- 'post-step-hooks' : (optional) a sequence of callables to be called in strict order after finishing recording the step.
- - 'post-scan-hooks' : (optional) a sequence of callables to be called in strict order after finishing the scan
- 'hooks' : (deprecated, use post-acq-hooks instead)
- 'point_id' : a hashable identifing the scan point.
- 'check_func' : (optional) a list of callable objects. callable(moveables, counters)
@@ -156,6 +159,7 @@ class GScan(Logger):
field. The extra information fields must be described in
extradesc (passed in the constructor of the Gscan)
+
The moveables must be a sequence Motion or MoveableDesc objects.
The environment is a dictionary of extra environment to be added specific
@@ -213,8 +217,8 @@ class GScan(Logger):
self._generator = generator
self._extrainfodesc = extrainfodesc
- #nasty hack to make sure macro has access to gScan as soon as possible
- self._macro._gScan = self
+ #nasty hack to make sure macro has access to gScan as soon as possible
+ self._macro._gScan = self #TODO: CAUTION! this may be causing a circular reference!
self._moveables, moveable_names = [], []
for moveable in moveables:
@@ -490,13 +494,12 @@ class GScan(Logger):
env = ScanDataEnvironment(
{ 'serialno' : serialno,
- 'user' : USER_NAME, #todo: this should be got from self.measurement_group.getChannelsInfo()
+ 'user' : USER_NAME, #TODO: this should be got from self.measurement_group.getChannelsInfo()
'title' : self.macro.getCommand() } )
# Initialize the data_desc list (and add the point number column)
data_desc = [
- ColumnDesc(name='point_nb', label='#Pt No', dtype='int64'),
- ColumnDesc(name='timestamp', label='dt', dtype='float64')
+ ColumnDesc(name='point_nb', label='#Pt No', dtype='int64')
]
# add motor columns
@@ -556,6 +559,8 @@ class GScan(Logger):
data_desc.append(extra_column.getColumnDesc())
# add extra columns
data_desc += self._extrainfodesc
+ data_desc.append(ColumnDesc(name='timestamp', label='dt', dtype='float64'))
+
env['datadesc'] = data_desc
#set the data compression default
@@ -617,7 +622,6 @@ class GScan(Logger):
:return: (list<ColumnDesc>) a list of :class:`ColumnDesc`, each including a
"pre_scan_value" attribute with the read value for that attr
'''
- import PyTango,numpy
manager = self.macro.getManager()
all_elements_info = manager.get_elements_with_interface('Element')
ret = []
@@ -636,8 +640,8 @@ class GScan(Logger):
v = PyTango.AttributeProxy(column.source).read().value #@Fixme: Tango-centric. It should work for any Taurus Attribute
column.pre_scan_value = v
- column.shape = numpy.shape(v)
- column.dtype = getattr(v, 'dtype', numpy.dtype(type(v))).name
+ column.shape = np.shape(v)
+ column.dtype = getattr(v, 'dtype', np.dtype(type(v))).name
ret.append(column)
except:
self.macro.warning('Error taking pre-scan snapshot of %s (%s)',label,src)
@@ -831,8 +835,8 @@ class SScan(GScan):
else:
yield 0.0
- if hasattr(macro, "pre_scan_hooks"):
- for hook in macro.pre_scan_hooks:
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('pre-scan'):
hook()
self._sum_motion_time = 0
@@ -846,8 +850,8 @@ class SScan(GScan):
if scream:
yield ((i+1) / nr_points) * 100.0
- if hasattr(macro, "post_scan_hooks"):
- for hook in macro.post_scan_hooks:
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('post-scan'):
hook()
if not scream:
@@ -977,66 +981,87 @@ class SScan(GScan):
class CScan(GScan):
- """Continuous scan"""
+ """Continuous scan abstract class. Implements helper methods."""
- def __init__(self, macro, waypointGenerator=None, periodGenerator=None,
- moveables=[], env={}, constraints=[], extrainfodesc=[]):
- GScan.__init__(self, macro, generator=waypointGenerator,
+ def __init__(self, macro, generator=None, moveables=[],
+ env={}, constraints=[], extrainfodesc=[]):
+ GScan.__init__(self, macro, generator=generator,
moveables=moveables, env=env, constraints=constraints,
extrainfodesc=extrainfodesc)
- self._periodGenerator = periodGenerator
self._current_waypoint_finished = False
self._all_waypoints_finished = False
self.motion_event = threading.Event()
self.motion_end_event = threading.Event()
-
- def do_backup(self):
- super(CScan, self).do_backup()
- self._backup = backup = []
- for moveable in self.moveables:
- # first backup all motor parameters
- motor = moveable.moveable
- try:
- velocity = motor.getVelocity()
- accel_time = motor.getAcceleration()
- decel_time = motor.getDeceleration()
- motor_backup = dict(moveable=moveable, velocity=velocity,
- acceleration=accel_time,
- deceleration=decel_time)
- self.debug("Backup of %s", motor)
- except AttributeError:
- motor_backup = None
- backup.append(motor_backup)
-
- def do_restore(self):
- super(CScan, self).do_restore()
- # restore changed motors to initial state
- for motor_backup in self._backup:
- if motor_backup is None:
- continue
- try:
- motor = motor_backup['moveable'].moveable
- motor.setVelocity(motor_backup['velocity'])
- motor.setAcceleration(motor_backup['acceleration'])
- motor.setDeceleration(motor_backup['deceleration'])
- self.debug("Restored %s", motor)
- except:
- self.macro.warning("Failed to restore %s", motor)
- self.debug("Details:", exc_info=1)
-
- def _calculateTotalAcquisitionTime(self):
- return None
-
- @property
- def period_generator(self):
- return self._periodGenerator
-
- @property
- def period_steps(self):
- if not hasattr(self, '_period_steps'):
- self._period_steps = enumerate(self.period_generator())
- return self._period_steps
+ data_structures = self.populate_moveables_data_structures(moveables)
+ self._moveables_trees, \
+ physical_moveables_names, \
+ self._physical_moveables = data_structures
+ self._physical_motion = self.macro.getMotion(physical_moveables_names)
+
+ def populate_moveables_data_structures(self, moveables):
+ '''Populates moveables data structures.
+ :param moveables: (list<Moveable>) data structures will be generated
+ for these moveables
+ :return (moveable_trees, physical_moveables_names, physical_moveables)
+ - moveable_trees (list<Tree>) - each tree represent one Moveables
+ with its hierarchy of inferior moveables.
+ - physical_moveables_names (list<str> - list of the names of the
+ physical moveables. List order is important and preserved.
+ - physical_moveables (list<Moveable> - list of the moveable objects.
+ List order is important and preserved.'''
+
+ def generate_moveable_node(macro, moveable):
+ '''Function to generate a moveable data structures based on moveable object.
+ Internally can be recursively called if moveable is a PseudoMotor.
+ :param moveable: moveable object
+ :return (moveable_node, physical_moveables_names, physical_moveables)
+ - moveable_node (BaseNode) - can be a BranchNode if moveable is a PseudoMotor
+ or a LeafNode if moveable is a PhysicalMotor.
+ - physical_moveables_names (list<str> - list of the names of the
+ physical moveables. List order is important and preserved.
+ - physical_moveables (list<Moveable> - list of the moveable objects.
+ List order is important and preserved.'''
+ moveable_node = None
+ physical_moveables_names = []
+ physical_moveables = []
+ moveable_type = moveable.getType()
+ if moveable_type == "PseudoMotor":
+ moveable_node = BranchNode(moveable)
+ moveables_names = moveable.elements
+ sub_moveables = [macro.getMoveable(name) \
+ for name in moveables_names]
+ for sub_moveable in sub_moveables:
+ sub_moveable_node, \
+ _physical_moveables_names, \
+ _physical_moveables = generate_moveable_node(macro,
+ sub_moveable)
+ physical_moveables_names += _physical_moveables_names
+ physical_moveables += _physical_moveables
+ moveable_node.addChild(sub_moveable_node)
+ elif moveable_type == "Motor":
+ moveable_node = LeafNode(moveable)
+ moveable_name = moveable.getName()
+ physical_moveables_names.append(moveable_name)
+ physical_moveables.append(moveable)
+ return moveable_node, physical_moveables_names, physical_moveables
+
+ moveable_trees = []
+ physical_moveables_names = []
+ physical_moveables = []
+
+ for moveable in moveables:
+ moveable_root_node, _physical_moveables_names, _physical_moveables = \
+ generate_moveable_node(self.macro, moveable.moveable)
+ moveable_tree = Tree(moveable_root_node)
+ moveable_trees.append(moveable_tree)
+ physical_moveables_names += _physical_moveables_names
+ physical_moveables += _physical_moveables
+ return moveable_trees, physical_moveables_names, physical_moveables
+ def get_moveables_trees(self):
+ '''Returns reference to the list of the moveables trees'''
+ return self._moveables_trees
+
def on_waypoints_end(self, restore_positions=None):
"""To be called by the waypoint thread to handle the end of waypoints
(either because no more waypoints or because a macro abort was
@@ -1044,89 +1069,25 @@ class CScan(GScan):
self.set_all_waypoints_finished(True)
if restore_positions is not None:
self.macro.info("Correcting overshoot...")
- self.motion.move(restore_positions)
+ self._physical_motion.move(restore_positions)
+ #self.motion.move(restore_positions)
self.motion_end_event.set()
self.motion_event.set()
def go_through_waypoints(self, iterate_only=False):
- """go through the different waypoints."""
+ """Go through the different waypoints."""
try:
self._go_through_waypoints()
except:
self.macro.error("An error occured moving to waypoints. Aborting...")
- self.debug("Details:", exc_info=1)
+ self.macro.debug("Details:", exc_info=1)
self.on_waypoints_end()
def _go_through_waypoints(self):
"""Internal, unprotected method to go through the different waypoints."""
- macro, motion, waypoints = self.macro, self.motion, self.steps
-
- last_positions = None
- for _, waypoint in waypoints:
- start_positions = waypoint.get('start_positions')
- positions = waypoint['positions']
- if start_positions is None:
- start_positions = last_positions
- if start_positions is None:
- last_positions = positions
- continue
-
- waypoint_info = self.prepare_waypoint(waypoint, start_positions)
- motion_paths, delta_start, acq_duration = waypoint_info
-
- self.acq_duration = acq_duration
-
- #execute pre-move hooks
- for hook in waypoint.get('pre-move-hooks',[]):
- hook()
-
- start_pos, final_pos = [] , []
- for path in motion_paths:
- start_pos.append(path.initial_user_pos)
- final_pos.append(path.final_user_pos)
-
- if macro.isStopped():
- self.on_waypoints_end()
- return
-
- # move to start position
-
- motion.move(start_pos)
-
- if macro.isStopped():
- return self.on_waypoints_end()
-
- # prepare motor(s) to move with their maximum velocity
- for path in motion_paths:
- if not path.apply_correction:
- continue
- vmotor = path.motor
- motor = path.moveable.moveable
- motor.setVelocity(vmotor.getMaxVelocity())
-
- if macro.isStopped():
- return self.on_waypoints_end()
-
- self.timestamp_to_start = time.time() + delta_start
- self.motion_event.set()
-
- # move to waypoint end position
- motion.move(final_pos)
-
- self.motion_event.clear()
-
- if macro.isStopped():
- return self.on_waypoints_end()
-
- #execute post-move hooks
- for hook in waypoint.get('post-move-hooks',[]):
- hook()
-
- if start_positions is None:
- last_positions = positions
-
- self.on_waypoints_end(positions)
-
+ raise NotImplementedError("_go_through_waypoints must be implemented " +
+ "in CScan derived classes")
+
def waypoint_estimation(self):
"""Internal, unprotected method to go through the different waypoints."""
motion, waypoints = self.motion, self.generator()
@@ -1183,6 +1144,47 @@ class CScan(GScan):
total_duration += overshoot_duration
return total_duration
+ def prepare_waypoint(self, waypoint, start_positions, iterate_only=False):
+ raise NotImplementedError("prepare_waypoint must be implemented in " +
+ "CScan derived classes")
+
+ def set_all_waypoints_finished(self, v):
+ self._all_waypoints_finished = v
+
+ def do_backup(self):
+ super(CScan, self).do_backup()
+ self._backup = backup = []
+ for moveable in self._physical_moveables:
+ # first backup all motor parameters
+ motor = moveable
+ try:
+ velocity = motor.getVelocity()
+ accel_time = motor.getAcceleration()
+ decel_time = motor.getDeceleration()
+ motor_backup = dict(moveable=moveable, velocity=velocity,
+ acceleration=accel_time,
+ deceleration=decel_time)
+ self.debug("Backup of %s", motor)
+ except AttributeError:
+ motor_backup = None
+ backup.append(motor_backup)
+
+ def do_restore(self):
+ super(CScan, self).do_restore()
+ # restore changed motors to initial state
+ for motor_backup in self._backup:
+ if motor_backup is None:
+ continue
+ try:
+ motor = motor_backup['moveable']
+ motor.setVelocity(motor_backup['velocity'])
+ motor.setAcceleration(motor_backup['acceleration'])
+ motor.setDeceleration(motor_backup['deceleration'])
+ self.debug("Restored %s", motor)
+ except:
+ self.macro.warning("Failed to restore %s", motor)
+ self.debug("Details:", exc_info=1)
+
def get_max_top_velocity(self, motor):
"""Helper method to find the maximum top velocity for the motor.
If the motor doesn't have a defined range for top velocity,
@@ -1201,7 +1203,33 @@ class CScan(GScan):
max_top_vel = self._maxVelDict[motor]
except AttributeError:
pass
- return max_top_vel
+ return max_top_vel
+
+ def get_min_acc_time(self, motor):
+ """Helper method to find the minimum acceleration time for the motor.
+ If the motor doesn't have a defined range for the acceleration time,
+ then use the current acceleration time"""
+
+ acc_time_obj = motor.getAccelerationObj()
+ min_acc_time, max_acc_time = acc_time_obj.getRange()
+ try:
+ min_acc_time = float(min_acc_time)
+ except ValueError:
+ min_acc_time = motor.getAcceleration()
+ return min_acc_time
+
+ def get_min_dec_time(self, motor):
+ """Helper method to find the minimum deceleration time for the motor.
+ If the motor doesn't have a defined range for the acceleration time,
+ then use the current acceleration time"""
+
+ dec_time_obj = motor.getDecelerationObj()
+ min_dec_time, max_dec_time = dec_time_obj.getRange()
+ try:
+ min_dec_time = float(min_dec_time)
+ except ValueError:
+ min_dec_time = motor.getDeceleration()
+ return min_dec_time
# def set_max_top_velocity(self, motor):
# """Helper method to set the maximum top velocity for the motor to
@@ -1213,8 +1241,32 @@ class CScan(GScan):
# except:
# pass
- def prepare_waypoint(self, waypoint, start_positions, iterate_only=False):
+
+class CSScan(CScan):
+ """Continuous scan controlled by software"""
+
+ def __init__(self, macro, waypointGenerator=None, periodGenerator=None,
+ moveables=[], env={}, constraints=[], extrainfodesc=[]):
+ CScan.__init__(self, macro, generator=waypointGenerator,
+ moveables=moveables, env=env, constraints=constraints,
+ extrainfodesc=extrainfodesc)
+ self._periodGenerator = periodGenerator
+
+ def _calculateTotalAcquisitionTime(self):
+ return None
+
+ @property
+ def period_generator(self):
+ return self._periodGenerator
+
+ @property
+ def period_steps(self):
+ if not hasattr(self, '_period_steps'):
+ self._period_steps = enumerate(self.period_generator())
+ return self._period_steps
+
+ def prepare_waypoint(self, waypoint, start_positions, iterate_only=False):
slow_down = waypoint.get('slow_down', 1)
positions = waypoint['positions']
@@ -1309,9 +1361,90 @@ class CScan(GScan):
path.setFinalUserPos(new_final_pos)
return ideal_paths, delta_start, cruise_duration
+
- def set_all_waypoints_finished(self, v):
- self._all_waypoints_finished = v
+ def go_through_waypoints(self, iterate_only=False):
+ """go through the different waypoints."""
+ try:
+ self._go_through_waypoints()
+ except Exception, e:
+ self.macro.error("An error occured moving to waypoints. Aborting...")
+ self.macro.debug("Details:", exc_info=1)
+ self.on_waypoints_end()
+ raise e
+
+ def _go_through_waypoints(self):
+ """Internal, unprotected method to go through the different waypoints."""
+ macro, motion, waypoints = self.macro, self.motion, self.steps
+ self.macro.debug("_go_through_waypoints() entering...")
+
+ last_positions = None
+ for _, waypoint in waypoints:
+ self.macro.debug("Waypoint iteration...")
+ start_positions = waypoint.get('start_positions')
+ positions = waypoint['positions']
+ if start_positions is None:
+ start_positions = last_positions
+ if start_positions is None:
+ last_positions = positions
+ continue
+
+ waypoint_info = self.prepare_waypoint(waypoint, start_positions)
+ motion_paths, delta_start, acq_duration = waypoint_info
+
+ self.acq_duration = acq_duration
+
+ #execute pre-move hooks
+ for hook in waypoint.get('pre-move-hooks',[]):
+ hook()
+
+ start_pos, final_pos = [] , []
+ for path in motion_paths:
+ start_pos.append(path.initial_user_pos)
+ final_pos.append(path.final_user_pos)
+
+ if macro.isStopped():
+ self.on_waypoints_end()
+ return
+
+ # move to start position
+ self.macro.debug("Moving to start position: %s" % repr(start_pos))
+ motion.move(start_pos)
+
+ if macro.isStopped():
+ return self.on_waypoints_end()
+
+ # prepare motor(s) to move with their maximum velocity
+ for path in motion_paths:
+ if not path.apply_correction:
+ continue
+ vmotor = path.motor
+ motor = path.moveable.moveable
+ motor.setVelocity(vmotor.getMaxVelocity())
+
+ if macro.isStopped():
+ return self.on_waypoints_end()
+
+ self.timestamp_to_start = time.time() + delta_start
+ self.motion_event.set()
+
+ # move to waypoint end position
+ motion.move(final_pos)
+
+ self.motion_event.clear()
+
+ if macro.isStopped():
+ return self.on_waypoints_end()
+
+ #execute post-move hooks
+ for hook in waypoint.get('post-move-hooks',[]):
+ hook()
+
+ if start_positions is None:
+ last_positions = positions
+
+ self.on_waypoints_end(positions)
+
def scan_loop(self):
motion, mg, waypoints = self.motion, self.measurement_group, self.steps
@@ -1335,8 +1468,8 @@ class CScan(GScan):
point_nb, step = -1, None
data = self.data
- if hasattr(macro, "pre_scan_hooks"):
- for hook in macro.pre_scan_hooks:
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('pre-scan'):
hook()
# start move & acquisition as close as possible
@@ -1449,10 +1582,478 @@ class CScan(GScan):
self.motion_end_event.wait()
- if hasattr(macro, "post_scan_hooks"):
- for hook in macro.post_scan_hooks:
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('post-scan'):
+ hook()
+
+
+ env = self._env
+ env['acqtime'] = sum_integ_time
+ env['delaytime'] = sum_delay
+
+ if not scream:
+ yield 100.0
+
+
+class CTScan(CScan):
+ '''Continuous scan controlled by hardware trigger signals.
+ Sequence of trigger signals is programmed in time. '''
+
+ class ExtraTrigger:
+ '''Helper class and temporary solution for configuring trigger device.
+ It is used to configure any Tango device name implementing:
+ +) following attributes:
+ - InitialDelayTime [s] - delay time from calling Start to generating first pulse
+ - HighTime [s] - time interval while signal will maintain its high state
+ - LowTime [s] - time interval while signal will maintain its low state
+ - SampPerChan - nr of pulses to be generated
+ - IdleState - state (high or low) which signal will take after the Start command
+ and which will maintain during the InitialDelayTime.
+ +) following commands:
+ - Start
+ - Stop)'''
+
+ MIN_HIGH_TIME = 0.0000002
+ MIN_TIME_PER_TRIGGER = 0.000001
+
+ def __init__(self, macro):
+ self.macro = macro
+
+ triggerDeviceName = self.macro.getEnv("TriggerDevice")
+ self.master = None
+ self.slaves = []
+ masterName = None
+ slaveNames = []
+
+ if isinstance(triggerDeviceName, str):
+ masterName = triggerDeviceName
+ elif isinstance(triggerDeviceName, list):
+ masterName = triggerDeviceName[0]
+ slaveNames = triggerDeviceName[1:]
+
+ for name in slaveNames:
+ slave = PyTango.DeviceProxy(name)
+ self.slaves.append(slave)
+ if masterName != None:
+ self.master = PyTango.DeviceProxy(masterName)
+
+ def configure(self, scanTime=None, nrOfTriggers=None, idleState="Low", lowTime=None, highTime=None, delayTime=0):
+ if not None in (scanTime, nrOfTriggers, delayTime, idleState):
+ timePerTrigger = scanTime / nrOfTriggers
+ if timePerTrigger < self.MIN_TIME_PER_TRIGGER:
+ raise Exception("scanTime is not long enough to manage this amount of triggers")
+ highTime = self.MIN_HIGH_TIME
+ lowTime = timePerTrigger - highTime
+ elif not None in (lowTime, highTime, delayTime, nrOfTriggers, idleState):
+ pass
+ else:
+ raise Exception("Missing parameters.")
+
+ self.master.write_attribute("InitialDelayTime", delayTime)
+ self.master.write_attribute("HighTime", highTime) # 162.5 ns
+ self.master.write_attribute("LowTime", lowTime) # 2.75 ms
+ self.master.write_attribute("SampPerChan", long(nrOfTriggers))
+ self.master.write_attribute("IdleState", idleState)
+ self.master.write_attribute("SampleTimingType", "Implicit")
+
+ for slave in self.slaves:
+ slave.write_attribute("HighTime", highTime) # 162.5 ns
+ slave.write_attribute("LowTime", lowTime) # 2.75 ms
+ slave.write_attribute("SampPerChan", long(nrOfTriggers))
+ slave.write_attribute("IdleState", idleState)
+ slave.write_attribute("SampleTimingType", "Implicit")
+
+ return timePerTrigger
+
+ def getConfiguration(self):
+ return None, None, None, None
+
+ def start(self):
+ for slave in self.slaves:
+ self.macro.debug("Staring %s" % slave.name())
+ slave.Start()
+ if self.master != None:
+ self.master.Start()
+
+ def stop(self):
+ for slave in self.slaves:
+ self.macro.debug("Stopping %s" % slave.name())
+ slave.Stop()
+ if self.master != None:
+ self.master.Stop()
+
+
+ class ExtraMntGrp:
+ '''Helper class and temporary solution for configuring experimental channels.
+ It assumes that experimental channels are implementing:
+ +) following attributes:
+ - Data - an array of acquired data
+ - TriggerMode - Soft or Gate - to configure/unconfigure channel for hardware trigger
+ - NrOfTriggers - to specify how many hardware triggers and acquistions will be done
+ - SamplingFrequency - have sence only for sampling experimental channels
+ +) following SendToCtrl strings:
+ - "pre-start"
+ - "start"
+ - "pre-stop"
+ - "stop"'''
+
+ def __init__(self, macro):
+ self.macro = macro
+ activeMntGrpName = self.macro.getEnv("ActiveMntGrp")
+ self.mntGrp = self.macro.getMeasurementGroup(activeMntGrpName)
+ self.activeChannels = []
+ self.nrOfTriggers = 0
+ channels = self.mntGrp.getChannels()
+ for channel in channels:
+ channelName = channel["name"]
+ expChannel = self.macro.getExpChannel(channelName)
+ expChannel.getHWObj().set_timeout_millis(120000) #in case of readout of position channels, it can take really long...
+ self.activeChannels.append(expChannel)
+
+ def isMoving(self):
+ for channel in self.activeChannels:
+ if channel.State() == PyTango.DevState.MOVING:
+ return True
+ return False
+
+ def start(self):
+ for channel in self.activeChannels:
+ pool = channel.getPoolObj()
+ ctrlName = channel.getControllerName()
+ axis = channel.getAxis()
+ self.macro.debug("Pre-starting controller: %s, axis: %d", ctrlName, axis)
+ pool.SendToController([ctrlName, 'pre-start %d' % axis])
+
+ for channel in self.activeChannels:
+ pool = channel.getPoolObj()
+ ctrlName = channel.getControllerName()
+ axis = channel.getAxis()
+ self.macro.debug("Starting controller: %s, axis: %d", ctrlName, axis)
+ pool.SendToController([ctrlName, 'start %d' % axis])
+
+ def stop(self):
+ for channel in self.activeChannels:
+ pool = channel.getPoolObj()
+ ctrlName = channel.getControllerName()
+ axis = channel.getAxis()
+ self.macro.debug("Pre-stopping controller: %s, axis: %d", ctrlName, axis)
+ pool.SendToController([ctrlName, 'pre-stop %d' % axis])
+
+ for channel in self.activeChannels:
+ pool = channel.getPoolObj()
+ ctrlName = channel.getControllerName()
+ axis = channel.getAxis()
+ self.macro.debug("Stopping controller: %s, axis: %d", ctrlName, axis)
+ pool.SendToController([ctrlName, 'stop %d' % axis])
+
+ def getDataList(self):
+ dataList = [ {"point_nb" : i, "timestamp" : 0} for i in xrange(self.nrOfTriggers) ]
+ for channel in self.activeChannels:
+ dataDesc = channel.getFullName()
+ channelData = channel.getAttribute("Data").read().value
+ for i, data in enumerate(channelData):
+ dataList[i][dataDesc] = data
+ return dataList
+
+ def setSamplingFrequency(self, freq):
+ for channel in self.activeChannels:
+ channel.getAttribute('SamplingFrequency').write(freq)
+
+ def setAcquisitionTime(self, acqTime):
+ for channel in self.activeChannels:
+ channel.getAttribute('AcquisitionTime').write(acqTime)
+
+ def setTriggerMode(self, mode):
+ if mode not in ["soft", "gate"]:
+ raise Exception("Trigger mode must be either soft or gate.")
+ for channel in self.activeChannels:
+ channel.getAttribute('TriggerMode').write(mode)
+
+ def setNrOfTriggers(self, nrOfTriggers):
+ self.nrOfTriggers = nrOfTriggers
+ for channel in self.activeChannels:
+ channel.getAttribute('NrOfTriggers').write(nrOfTriggers)
+
+ def configure(self, nrOfTriggers, acqTime, timePerTrigger, sampFreq=-1, triggerMode="gate"):
+ self.macro.debug("acqTime: %s" % acqTime)
+ if timePerTrigger == None:
+ raise Exception("TimePerTrigger attribute must be set")
+ acqTime = timePerTrigger * acqTime / 100.0
+ self.setTriggerMode(triggerMode)
+ self.setNrOfTriggers(nrOfTriggers)
+ self.setSamplingFrequency(sampFreq)
+ self.setAcquisitionTime(acqTime)
+ self.macro.debug("MG: nrOfTriggers: %s, timePerTrigger: %s, acqTime: %s, sampFreq: %s" % (nrOfTriggers,timePerTrigger,acqTime,sampFreq))
+
+ def getConfiguration(self):
+ return None
+
+ def setConfiguration(self, configuration):
+ pass
+
+
+ def __init__(self, macro, generator=None,
+ moveables=[], env={}, constraints=[], extrainfodesc=[]):
+ CScan.__init__(self, macro, generator=generator,
+ moveables=moveables, env=env, constraints=constraints,
+ extrainfodesc=extrainfodesc)
+ self._measurement_group = self.ExtraMntGrp(macro)
+ self.extraTrigger = self.ExtraTrigger(macro)
+
+
+ def prepare_waypoint(self, waypoint, start_positions, iterate_only=False):
+ '''Prepare list of MotionPath objects per each physical motor.
+ :param waypoint: (dict) waypoint dictionary with necessary information
+ :param start_positions: (list<float>) list of starting position per each
+ physical motor
+ :return (ideal_paths, acc_time, active_time)
+ - ideal_paths: (list<MotionPath> representing motion attributes
+ of each physical motor)
+ - acc_time: acceleration time which will be used during the scan
+ it corresponds to the longest acceleration time of
+ all the motors
+ - active_time: time interval while all the physical motors will
+ maintain constant velocity'''
+
+ positions = waypoint['positions']
+ active_time = waypoint["active_time"]
+
+ ideal_paths = []
+
+ max_acc_time, max_dec_time = 0, 0
+ for moveable, end_position in zip(self._physical_moveables, positions):
+ motor = moveable
+ self.macro.debug("Motor: %s" % motor.getName())
+ self.macro.debug("AccTime: %f" % self.get_min_acc_time(motor))
+ self.macro.debug("DecTime: %f" % self.get_min_dec_time(motor))
+ max_acc_time = max(self.get_min_acc_time(motor), max_acc_time)
+ max_dec_time = max(self.get_min_dec_time(motor), max_dec_time)
+
+ acc_time = max_acc_time
+ dec_time = max_dec_time
+
+ for moveable, start_position, end_position in \
+ zip(self._physical_moveables, start_positions, positions):
+ base_vel = moveable.getBaseRate()
+ ideal_vmotor = VMotor(accel_time=acc_time,
+ decel_time=dec_time,
+ min_vel=base_vel)
+ ideal_path = MotionPath(ideal_vmotor,
+ start_position,
+ end_position,
+ active_time)
+ ideal_path.moveable = moveable
+ ideal_path.apply_correction = True
+ ideal_paths.append(ideal_path)
+
+ return ideal_paths, acc_time, active_time
+
+ def _go_through_waypoints(self):
+ """Internal, unprotected method to go through the different waypoints.
+ It controls all the three objects: motion, trigger and measurement
+ group."""
+ macro, motion, waypoints = self.macro, self._physical_motion, self.steps
+ self.macro.debug("_go_through_waypoints() entering...")
+
+ last_positions = None
+ for _, waypoint in waypoints:
+ self.macro.debug("Waypoint iteration...")
+ start_positions = waypoint.get('start_positions')
+ positions = waypoint['positions']
+ if start_positions is None:
+ start_positions = last_positions
+ if start_positions is None:
+ last_positions = positions
+ continue
+
+ waypoint_info = self.prepare_waypoint(waypoint, start_positions)
+ motion_paths, delta_start, acq_duration = waypoint_info
+
+ self.acq_duration = acq_duration
+
+ #execute pre-move hooks
+ for hook in waypoint.get('pre-move-hooks',[]):
+ hook()
+
+ start_pos, final_pos = [] , []
+ for path in motion_paths:
+ start_pos.append(path.initial_user_pos)
+ final_pos.append(path.final_user_pos)
+
+ if macro.isStopped():
+ self.on_waypoints_end()
+ return
+
+ self.__mntGrpConfigured = False
+ self.__triggerConfigured = False
+ self.__mntGrpStarted = False
+ self.__triggerStarted = False
+
+ #validation of parameters
+ for start, end in zip(self.macro.starts, self.macro.finals):
+ if start == end:
+ raise Exception("Start and End can not be equal.")
+
+ startTimestamp = time.time()
+
+ #extra pre configuration
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('pre-configuration'):
+ hook()
+ self.macro.checkPoint()
+
+ #configuring trigger lines
+ oldHighTime, oldLowTime, oldDelay, oldNrOfTriggers = \
+ self.extraTrigger.getConfiguration()
+ self.__triggerConfigured = True
+ timePerTrigger = self.extraTrigger.configure(delayTime=delta_start,
+ scanTime=acq_duration,
+ nrOfTriggers=self.macro.nr_of_points)
+ self.macro.checkPoint()
+
+ #configuring measurementGroup
+ self.mntGrpConfiguration = self._measurement_group.getConfiguration()
+ self.__mntGrpConfigured = True
+ self._measurement_group.configure(self.macro.nr_of_points,
+ self.macro.acq_time,
+ timePerTrigger)
+ self.macro.checkPoint()
+
+ #extra post configuration
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('post-configuration'):
+ hook()
+ self.macro.checkPoint()
+
+ endTimestamp = time.time()
+ self.macro.info("Configuration took %s time." % repr(endTimestamp - startTimestamp))
+
+ # move to start position
+ self.macro.debug("Moving to start position: %s" % repr(start_pos))
+ motion.move(start_pos)
+
+ if macro.isStopped():
+ return self.on_waypoints_end()
+
+ # prepare motor(s) to move with their maximum velocity
+ for path in motion_paths:
+ motor = path.moveable
+ self.macro.debug("Motor: %s" % motor.getName())
+ self.macro.debug("Velocity: %f" % path.max_vel)
+ self.macro.debug("AccTime: %f" % path.max_vel_time)
+ self.macro.debug("DecTime: %f" % path.min_vel_time)
+ #TODO: check why we have 0 here
+ #if 0 in [path.max_vel, path.max_vel_time, path.min_vel_time]:
+ # continue
+ motor.setVelocity(path.max_vel)
+ motor.setAcceleration(path.max_vel_time)
+ motor.setDeceleration(path.min_vel_time)
+
+ if macro.isStopped():
+ return self.on_waypoints_end()
+
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('pre-start'):
+ hook()
+ self.macro.checkPoint()
+
+ self.macro.debug("Starting measurement group")
+ self.__mntGrpStarted = True
+ self._measurement_group.start()
+
+ self.timestamp_to_start = time.time() + delta_start
+
+ self.motion_event.set()
+
+ # move to waypoint end position
+ self.macro.debug("Moving to waypoint position: %s" % repr(final_pos))
+ self.macro.debug("Starting triggers")
+ self.__triggerStarted = True
+ self.extraTrigger.start()
+ motion.move(final_pos)
+
+ self.motion_event.clear()
+
+ if macro.isStopped():
+ return self.on_waypoints_end()
+
+ #execute post-move hooks
+ for hook in waypoint.get('post-move-hooks',[]):
+ hook()
+
+ self.macro.debug("Waiting for measurement group to finish")
+ while self._measurement_group.isMoving():
+ self.macro.checkPoint()
+ time.sleep(0.1)
+
+ self.macro.debug("Getting data")
+ data_list = self._measurement_group.getDataList()
+
+ def populate_ideal_positions():
+ moveables = self.moveables
+ nr_of_points = self.macro.nr_of_points
+ starts = self.macro.starts
+ finals = self.macro.finals
+ positions_records = [{} for i in xrange(nr_of_points)]
+
+ for moveable, start, final in zip(moveables, starts, finals):
+ name = moveable.moveable.getName()
+ step_size = abs((end-start)/nr_of_points)
+ for point_nr, position in enumerate(np.arange(start, \
+ final, step_size)):
+ positions_records[point_nr][name] = position
+
+ return positions_records
+
+ #TODO: decide what to do with moveables
+ position_list = populate_ideal_positions()
+
+ self.macro.debug("Storing data")
+ for data_dict, position_dict in zip(data_list,position_list):
+ data_dict.update(position_dict)
+ self.data.addRecord(data_dict)
+
+ if start_positions is None:
+ last_positions = positions
+
+ self.on_waypoints_end(positions)
+
+ def on_waypoints_end(self, restore_positions=None):
+ self.macro.debug("on_waypoints_end() entering...")
+ CScan.on_waypoints_end(self, restore_positions=restore_positions)
+ self.cleanup()
+
+ def scan_loop(self):
+ macro = self.macro
+ manager = macro.getManager()
+ scream = False
+ motion_event = self.motion_event
+ startts = self._env['startts']
+
+ sum_delay = 0
+ sum_integ_time = 0
+
+ if hasattr(macro, "nr_points"):
+ nr_points = float(macro.nr_points)
+ scream = True
+ else:
+ yield 0.0
+
+ moveables = [ m.moveable for m in self.moveables ]
+
+ point_nb, step = -1, None
+ data = self.data
+
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('pre-scan'):
hook()
+
+ self.go_through_waypoints()
+
+ if hasattr(macro, 'getHooks'):
+ for hook in macro.getHooks('post-scan'):
+ hook()
env = self._env
env['acqtime'] = sum_integ_time
@@ -1461,6 +2062,57 @@ class CScan(GScan):
if not scream:
yield 100.0
+ def cleanup(self):
+ '''This method is responsible for restoring state of measurement group
+ and trigger to its state before the scan.'''
+ startTimestamp = time.time()
+
+ if self.__mntGrpStarted:
+ self.debug("Stopping measurement group")
+ try:
+ self._measurement_group.stop()
+ except Exception, e:
+ self.warning("Exception while trying to stop measurement group.")
+ self.debug(e)
+
+ if self.__triggerStarted:
+ self.debug("Stopping triggers")
+ try:
+ self.extraTrigger.stop()
+ except Exception, e:
+ self.warning("Exception while trying to stop trigger.")
+ self.debug(e)
+
+ if hasattr(self.macro, 'getHooks'):
+ for hook in self.macro.getHooks('pre-cleanup'):
+ self.debug("Executing pre-cleanup hook")
+ try:
+ hook()
+ except Exception, e:
+ self.warning("Exception while trying to execute a pre-cleanup hook")
+ self.debug(e)
+
+ if self.__mntGrpConfigured:
+ self.debug("Restoring configuration of measurement group")
+ try:
+ self._measurement_group.setConfiguration(self.mntGrpConfiguration)
+ #TODO: mntGrp configuration should contain also: nrOfTriggers, acqTime, sampling frequency
+ except:
+ self.warning("Exception while trying to restore measurement group parameters")
+ self.debug(e)
+
+ if hasattr(self.macro, 'getHooks'):
+ for hook in self.macro.getHooks('post-cleanup'):
+ self.debug("Executing post-cleanup hook")
+ try:
+ hook()
+ except Exception, e:
+ self.warning("Exception while trying to execute a post-cleanup hook")
+ self.debug(e)
+
+ endTimestamp = time.time()
+ self.debug("Cleanup took %s time." % repr(endTimestamp - startTimestamp))
+
class HScan(SScan):
"""Hybrid scan"""
diff --git a/src/sardana/macroserver/scan/recorder/datarecorder.py b/src/sardana/macroserver/scan/recorder/datarecorder.py
index 2ddaed0..7f0a18a 100644
--- a/src/sardana/macroserver/scan/recorder/datarecorder.py
+++ b/src/sardana/macroserver/scan/recorder/datarecorder.py
@@ -69,6 +69,20 @@ class DataHandler:
else: # blockSave
pass
+ def addCustomData(self, value, name, **kwargs):
+ '''Write data other than a record.
+
+ :param value: The value to be written
+ :param name: An identification for this value
+
+ Optional keyword arguments can be passed with information that some
+ recorders may need in order to record this value. For example: the NeXus
+ recorder will make use of "nxpath" info if available to place the value
+ where it belongs in the nexus hierarchy. Check the `addCustomData`
+ method of each recorder to see what they use/require.
+ '''
+ for recorder in self.recorders:
+ recorder.addCustomData(value, name, **kwargs )
#
# Recorders
#
@@ -130,7 +144,13 @@ class DataRecorder(Logger):
def setSaveMode( self, mode ):
self.savemode = mode
-
+
+ def addCustomData(self, value, name, **kwargs):
+ self._addCustomData(value, name, **kwargs)
+
+ def _addCustomData(self, value, name, **kwargs):
+ pass
+
class DumbRecorder(DataRecorder):
def _startRecordList(self, recordlist):
diff --git a/src/sardana/macroserver/scan/recorder/output.py b/src/sardana/macroserver/scan/recorder/output.py
index 37d3e6d..61a2560 100644
--- a/src/sardana/macroserver/scan/recorder/output.py
+++ b/src/sardana/macroserver/scan/recorder/output.py
@@ -99,6 +99,18 @@ class JsonRecorder(DataRecorder):
#data = self._codec.encode(('', kwargs))
#self._stream.sendRecordData(*data)
self._stream.sendRecordData(kwargs, codec='json')
+
+ def _addCustomData(self, value, name, **kwargs):
+ '''
+ The custom data will be sent as a packet with type='custom_data' and its
+ data will be the dictionary of keyword arguments passed to this method
+ plus 'name' and 'value'
+ '''
+ macro_id = self.recordlist.getEnvironValue('macro_id')
+ data = dict(kwargs) #shallow copy
+ data['name'] = name
+ data['value'] = value
+ self._sendPacket(type="custom_data", data=data, macro_id=macro_id)
class OutputRecorder(DataRecorder):
@@ -230,3 +242,15 @@ class OutputRecorder(DataRecorder):
self._stream.output(scan_line)
self._stream.flushOutput()
+
+ def _addCustomData(self, value, name, **kwargs):
+ '''
+ The custom data will be added as an info line in the form::
+ Custom data: name : value
+ '''
+ if numpy.rank(value)>0:
+ v = 'Array(%s)'%str(numpy.shape(value))
+ else:
+ v = str(value)
+ self._stream.output('Custom data: %s : %s'%(name,v) )
+ self._stream.flushOutput()
diff --git a/src/sardana/macroserver/scan/recorder/storage.py b/src/sardana/macroserver/scan/recorder/storage.py
index f576741..fbd2434 100644
--- a/src/sardana/macroserver/scan/recorder/storage.py
+++ b/src/sardana/macroserver/scan/recorder/storage.py
@@ -32,6 +32,7 @@ __docformat__ = 'restructuredtext'
import os
import time
import itertools
+import re
import numpy
@@ -363,6 +364,39 @@ class SPEC_FileRecorder(BaseFileRecorder):
self.fd.flush()
self.fd.close()
+
+ def _addCustomData(self, value, name, **kwargs):
+ '''
+ The custom data will be added as a comment line in the form::
+
+ #C name : value
+
+ ..note:: non-scalar values (or name/values containing end-of-line) will not be written
+ '''
+ if self.filename is None:
+ self.info('Custom data "%s" will not be stored in SPEC file. Reason: uninitialized file',name)
+ return
+ if numpy.rank(value)>0: #ignore non-scalars
+ self.info('Custom data "%s" will not be stored in SPEC file. Reason: value is non-scalar',name)
+ return
+ v = str(value)
+ if '\n' in v or '\n' in name: #ignore if name or the string representation of the value contains end-of-line
+ self.info('Custom data "%s" will not be stored in SPEC file. Reason: unsupported format',name)
+ return
+
+ fileWasClosed = self.fd is None or self.fd.closed
+ if fileWasClosed:
+ try:
+ self.fd = open(self.filename,'a')
+ except:
+ self.info('Custom data "%s" will not be stored in SPEC file. Reason: cannot open file',name)
+ return
+ self.fd.write( '#C %s : %s\n'%(name,v) )
+ self.fd.flush()
+ if fileWasClosed:
+ self.fd.close() #leave the file descriptor as found
+
+
class BaseNEXUS_FileRecorder(BaseFileRecorder):
"""Base class for NeXus file recorders"""
@@ -383,7 +417,8 @@ class BaseNEXUS_FileRecorder(BaseFileRecorder):
self.nxs = nxs
except ImportError:
raise Exception("NeXus is not available")
-
+
+ self.macro = macro
self.overwrite = overwrite
if filename:
self.setFileName(filename)
@@ -467,6 +502,8 @@ class BaseNAPI_FileRecorder(BaseNEXUS_FileRecorder):
# Convenience methods to make NAPI less tedious
#===========================================================================
+ _nxentryInPath = re.compile(r'/[^/:]+:NXentry')
+
def _makedata(self, name, dtype=None, shape=None, mode='lzw', chunks=None, comprank=None):
'''
combines :meth:`nxs.NeXus.makedata` and :meth:`nxs.NeXus.compmakedata` by selecting between
@@ -544,26 +581,43 @@ class BaseNAPI_FileRecorder(BaseNEXUS_FileRecorder):
self.fd.makelink(nid)
def _createBranch(self, path):
- """navigates the nexus tree starting in the current <entry> and finishing in <entry>/path.
- It creates the groups if they do not exist, using the class info in self.instrDict
- If successful, path is left open"""
- groups=path.split('/')
- self.fd.openpath("/%s:NXentry" % self.entryname)
- relpath="" #the current path relative to <entry>, to use as a key for instrDict
- for g in groups:
+ """
+ Navigates the nexus tree starting in / and finishing in path.
+
+ If path does not start with `/<something>:NXentry`, the current entry is
+ prepended to it.
+
+ This method creates the groups if they do not exist. If the
+ path is given using `name:nxclass` notation, the given nxclass is used.
+ Otherwise, the class name is obtained from self.instrDict values (and if
+ not found, it defaults to NXcollection). If successful, path is left
+ open
+ """
+ m = self._nxentryInPath.match(path)
+ if m is None:
+ self._createBranch("/%s:NXentry"%self.entryname) #if at all, it will recurse just once
+# self.fd.openpath("/%s:NXentry" % self.entryname)
+ else:
+ self.fd.openpath("/")
+
+ relpath=""
+ for g in path.split('/'):
if len(g) == 0:
continue
relpath = relpath + "/"+ g
- try:
- group_type = self.instrDict[relpath].klass
- except:
- group_type = 'NXcollection'
+ if ':' in g:
+ g,group_type = g.split(':')
+ else:
+ try:
+ group_type = self.instrDict[relpath].klass
+ except:
+ group_type = 'NXcollection'
try:
self.fd.opengroup(g, group_type)
except:
self.fd.makegroup(g, group_type)
- self.fd.opengroup(g, group_type)
-
+ self.fd.opengroup(g, group_type)
+
class NXscan_FileRecorder(BaseNAPI_FileRecorder):
"""saves data to a nexus file that follows the NXscan application definition
@@ -588,7 +642,39 @@ class NXscan_FileRecorder(BaseNAPI_FileRecorder):
if not self.overwrite and os.path.exists(self.filename): nxfilemode='rw'
self.fd = nxs.open(self.filename, nxfilemode)
self.entryname = "entry%d" % serialno
- self.fd.makegroup(self.entryname,"NXentry")
+ try:
+ self.fd.makegroup(self.entryname,"NXentry")
+ except NeXusError:
+ entrynames = self.fd.getentries().keys()
+
+ #===================================================================
+ ##Warn and abort
+ if self.entryname in entrynames:
+ raise RuntimeError(('"%s" already exists in %s. To prevent data corruption the macro will be aborted.\n'%(self.entryname, self.filename)+
+ 'This is likely caused by a wrong ScanID\n'+
+ 'Possible workarounds:\n'+
+ ' * first, try re-running this macro (the ScanID may be automatically corrected)\n'
+ ' * if not, try changing ScanID with senv, or...\n'+
+ ' * change the file name (%s will be in both files containing different data)\n'%self.entryname+
+ '\nPlease report this problem.'))
+ else:
+ raise
+ #===================================================================
+
+ #===================================================================
+ ## Warn and continue writing to another entry
+ #if self.entryname in entrynames:
+ # i = 2
+ # newname = "%s_%i"%(self.entryname,i)
+ # while(newname in entrynames):
+ # i +=1
+ # newname = "%s_%i"%(self.entryname,i)
+ # self.warning('"%s" already exists. Using "%s" instead. This may indicate a bug in %s',self.entryname, newname, self.macro.name)
+ # self.macro.warning('"%s" already exists. Using "%s" instead. \nThis may indicate a bug in %s. Please report it.',self.entryname, newname, self.macro.name)
+ # self.entryname = newname
+ # self.fd.makegroup(self.entryname,"NXentry")
+ #===================================================================
+
self.fd.opengroup(self.entryname,"NXentry")
@@ -629,8 +715,7 @@ class NXscan_FileRecorder(BaseNAPI_FileRecorder):
self.fd.closegroup()
#prepare the "measurement" group
- self.fd.makegroup("measurement","NXcollection")
- self.fd.opengroup("measurement","NXcollection")
+ self._createBranch("measurement:NXcollection")
if self.savemode==SaveModes.Record:
#create extensible datasets
for dd in self.datadesc:
@@ -649,12 +734,9 @@ class NXscan_FileRecorder(BaseNAPI_FileRecorder):
self.fd.flush()
def _createPreScanSnapshot(self, env):
- measurementpath = "/%s:NXentry/measurement:NXcollection"%self.entryname
- self.fd.openpath(measurementpath)
#write the pre-scan snapshot in the "measurement:NXcollection/pre_scan_snapshot:NXcollection" group
self.preScanSnapShot = env.get('preScanSnapShot',[])
- self.fd.makegroup("pre_scan_snapshot","NXcollection")
- self.fd.opengroup("pre_scan_snapshot","NXcollection")
+ self._createBranch('measurement:NXcollection/pre_scan_snapshot:NXcollection')
links = {}
for dd in self.preScanSnapShot: #desc is a ColumnDesc object
label = self.sanitizeName(dd.label)
@@ -825,8 +907,56 @@ class NXscan_FileRecorder(BaseNAPI_FileRecorder):
self._nxln(src, dst)
except:
self.warning("cannot create link for '%s'. Skipping",axis)
-
-
+
+ def _addCustomData(self, value, name, nxpath=None, dtype=None, **kwargs):
+ '''
+ apart from value and name, this recorder can use the following optional parameters:
+
+ :param nxpath: (str) a nexus path (optionally using name:nxclass notation for
+ the group names). See the rules for automatic nxclass
+ resolution used by
+ :meth:`NXscan_FileRecorder._createBranch`.
+ If None given, it defaults to
+ nxpath='custom_data:NXcollection'
+
+ :param dtype: name of data type (it is inferred from value if not given)
+
+ '''
+ if nxpath is None:
+ nxpath='custom_data:NXcollection'
+ if dtype is None:
+ if numpy.isscalar(value):
+ dtype = numpy.dtype(type(value)).name
+ if numpy.issubdtype(dtype,str):
+ dtype='char'
+ if dtype == 'bool':
+ value, dtype = int(value), 'int8'
+ else:
+ value = numpy.array(value)
+ dtype = value.dtype.name
+
+ if dtype not in self.supported_dtypes and dtype != 'char':
+ self.warning("cannot write '%s'. Reason: unsupported data type",name)
+ return
+ #open the file if necessary
+ fileWasClosed = self.fd is None or not self.fd.isopen
+ if fileWasClosed:
+ if not self.overwrite and os.path.exists(self.filename): nxfilemode='rw'
+ import nxs
+ self.fd = nxs.open(self.filename, nxfilemode)
+ #write the data
+ self._createBranch(nxpath)
+ try:
+ self._writeData(name, value, dtype)
+ except ValueError, e:
+ msg = "Error writing %s. Reason: %s"%(name, str(e))
+ self.warning(msg)
+ self.macro.warning(msg)
+ #leave the file as it was
+ if fileWasClosed:
+ self.fd.close()
+
+
class NXxas_FileRecorder(BaseNEXUS_FileRecorder):
"""saves data to a nexus file that follows the NXsas application definition
diff --git a/src/sardana/pool/pool.py b/src/sardana/pool/pool.py
index f477f6a..9e7ca66 100644
--- a/src/sardana/pool/pool.py
+++ b/src/sardana/pool/pool.py
@@ -35,8 +35,8 @@ __docformat__ = 'restructuredtext'
import os.path
import logging.handlers
-from taurus.core import AttributeNameValidator
-from taurus.core.util import CaselessDict
+from taurus.core.taurusvalidator import AttributeNameValidator
+from taurus.core.util.containers import CaselessDict
from sardana import InvalidId, ElementType, TYPE_ACQUIRABLE_ELEMENTS, \
TYPE_PSEUDO_ELEMENTS, TYPE_PHYSICAL_ELEMENTS, TYPE_MOVEABLE_ELEMENTS
@@ -705,4 +705,4 @@ class Pool(PoolContainer, PoolObject, SardanaElementManager, SardanaIDManager):
graph = Graph()
for moveable in moveable_elems_map.values():
self._build_element_dependencies(moveable, graph)
- return graph
\ No newline at end of file
+ return graph
diff --git a/src/sardana/pool/poolbasegroup.py b/src/sardana/pool/poolbasegroup.py
index bac0d4b..69326f4 100644
--- a/src/sardana/pool/poolbasegroup.py
+++ b/src/sardana/pool/poolbasegroup.py
@@ -30,7 +30,7 @@ __all__ = [ "PoolBaseGroup"]
__docformat__ = 'restructuredtext'
-from taurus.core import AttributeNameValidator
+from taurus.core.taurusvalidator import AttributeNameValidator
from sardana import State, ElementType, TYPE_PHYSICAL_ELEMENTS
from poolexternal import PoolExternalObject
diff --git a/src/sardana/pool/poolmeasurementgroup.py b/src/sardana/pool/poolmeasurementgroup.py
index 670ae32..87bdf54 100644
--- a/src/sardana/pool/poolmeasurementgroup.py
+++ b/src/sardana/pool/poolmeasurementgroup.py
@@ -30,7 +30,7 @@ __all__ = [ "PoolMeasurementGroup" ]
__docformat__ = 'restructuredtext'
-from taurus.core import AttributeNameValidator
+from taurus.core.taurusvalidator import AttributeNameValidator
from taurus.core.tango.sardana import PlotType, Normalization
from sardana import State,ElementType, \
diff --git a/src/sardana/pool/poolpseudomotor.py b/src/sardana/pool/poolpseudomotor.py
index 481e4ec..f12b318 100644
--- a/src/sardana/pool/poolpseudomotor.py
+++ b/src/sardana/pool/poolpseudomotor.py
@@ -51,8 +51,22 @@ class Position(SardanaAttribute):
def __init__(self, *args, **kwargs):
self._exc_info = None
super(Position, self).__init__(*args, **kwargs)
- for position_attr in self.obj.get_physical_position_attribute_iterator():
- position_attr.add_listener(self.on_change)
+
+ # 130226: We found a bug https://sourceforge.net/p/sardana/tickets/2/ that makes the Pool segfault with some pseudomotor configuration:
+ # It can be reproduced by:
+ # 4 physical motors: m1 m2 m3 m4
+ # 2 slits: s1g,s1o = f(m1,m2) and s2g,s2o = f(m3,m4)
+ # The pool will not be able to start if we create a third slit with s3g,s3o = f(s1g, s2g)
+ # self.obj.get_physical_position_attribute_iterator() raises a KeyError exception
+ # so we will flag the Position object as no_listeners for later configuration
+ # We should still investigate the root of the problem when ordering the creation of elements
+ self._listeners_configured = False
+ try:
+ for position_attr in self.obj.get_physical_position_attribute_iterator():
+ position_attr.add_listener(self.on_change)
+ self._listeners_configured = True
+ except KeyError:
+ pass
def _in_error(self):
for position_attr in self.obj.get_physical_position_attribute_iterator():
@@ -210,9 +224,9 @@ class PoolPseudoMotor(PoolBaseGroup, PoolElement):
self._drift_correction = kwargs.pop('drift_correction', None)
user_elements = kwargs.pop('user_elements')
kwargs['elem_type'] = ElementType.PseudoMotor
+ PoolElement.__init__(self, **kwargs)
PoolBaseGroup.__init__(self, user_elements=user_elements,
pool=kwargs['pool'])
- PoolElement.__init__(self, **kwargs)
self._position = Position(self, listeners=self.on_change)
# --------------------------------------------------------------------------
@@ -430,6 +444,13 @@ class PoolPseudoMotor(PoolBaseGroup, PoolElement):
_STD_STATUS = "{name} is {state}\n{ctrl_status}"
def calculate_state_info(self, status_info=None):
+
+ # Refer to Position.__init__ method for an explanation on this 'hack'
+ if not self._position._listeners_configured:
+ for position_attr in self.get_physical_position_attribute_iterator():
+ position_attr.add_listener(self._position.on_change)
+ self._position._listeners_configured = True
+
if status_info is None:
status_info = self._state, self._status
state, status = status_info
diff --git a/src/sardana/release.py b/src/sardana/release.py
index c86c6dd..7c94185 100644
--- a/src/sardana/release.py
+++ b/src/sardana/release.py
@@ -26,11 +26,6 @@
__docformat__ = "restructuredtext"
-__svn_revision = '$Rev:: $'
-try:
- __svn_revision = int(__svn_revision[7:14])
-except:
- __svn_revision = 0
"""
Release data for the taurus project. It contains the following members:
@@ -54,10 +49,8 @@ name = 'sardana'
#: the new substring. We have to avoid using either dashes or underscores,
#: because bdist_rpm does not accept dashes (an RPM) convention, and
#: bdist_deb does not accept underscores (a Debian convention).
-version_info = (1, 2, 0, 'dev', __svn_revision)
+version_info = (1, 2, 0, 'rc', 0)
version = '.'.join(map(str, version_info[:3]))
-
-#: svn revision number
revision = str(version_info[4])
description = "Sardana is a generic program for control applications in large and small installations"
@@ -69,7 +62,8 @@ and the standard basis of collaborations in control."""
license = 'LGPL'
-authors = {'Tiago' : ('Tiago Coutinho','tcoutinho at cells.es') }
+authors = {'Tiago' : ('Tiago Coutinho','tiago.coutinho at esrf.fr'),
+ 'Pascual-Izarra' : ('Carlos Pascual-Izarra','cpascual at cells.es') }
url = 'http://packages.python.org/sardana'
diff --git a/src/sardana/requirements.py b/src/sardana/requirements.py
index de182d2..527255a 100644
--- a/src/sardana/requirements.py
+++ b/src/sardana/requirements.py
@@ -35,7 +35,7 @@ __requires__ = {
# module minimum
"Python" : (2,6,0),
"PyTango" : (7,2,3),
- "taurus.core" : (3,0,0),
+ "taurus.core" : (3,1,0),
}
def check_requirements(exec_name=None):
@@ -93,7 +93,7 @@ def check_requirements(exec_name=None):
sys.exit(-1)
try:
- from taurus.core.util import etree
+ from lxml import etree
except:
print "Could not find any suitable XML library"
sys.exit(-1)
diff --git a/src/sardana/spock/magic.py b/src/sardana/spock/magic.py
index f67b21f..5953ee2 100644
--- a/src/sardana/spock/magic.py
+++ b/src/sardana/spock/magic.py
@@ -45,9 +45,24 @@ def expconf(self, parameter_s=''):
except:
print "Error importing ExpDescriptionEditor "\
"(hint: is taurus extra_sardana installed?)"
+ return
doorname = get_door().name()
- w = ExpDescriptionEditor(door=doorname)
- w.show()
+
+ #===========================================================================
+ ## ugly hack to avoid ipython/qt thread problems #e.g. see
+ ## https://sourceforge.net/p/sardana/tickets/10/
+ ## this hack does not allow inter-process communication and leaves the
+ ## widget open after closing spock
+ ## @todo: investigate cause of segfaults when using launching qt widgets from ipython
+ #
+ #w = ExpDescriptionEditor(door=doorname)
+ #w.show() #launching it like this, produces the problem of https://sourceforge.net/p/sardana/tickets/10/
+ import subprocess
+ import sys
+ fname = sys.modules[ExpDescriptionEditor.__module__].__file__
+ args = ['python', fname, doorname]
+ subprocess.Popen(args)
+ #===========================================================================
def showscan(self, parameter_s=''):
diff --git a/src/sardana/spock/release.py b/src/sardana/spock/release.py
index 5ffc26c..3a765da 100644
--- a/src/sardana/spock/release.py
+++ b/src/sardana/spock/release.py
@@ -52,7 +52,8 @@ replacement for the interactive Python interpreter with extra functionality.
license = 'GNU'
-authors = {'Tiago' : ('Tiago Coutinho','tcoutinho at cells.es') }
+authors = {'Tiago' : ('Tiago Coutinho','tiago.coutinho at esrf.fr'),
+ 'Pascual-Izarra' : ('Carlos Pascual-Izarra','cpascual at cells.es') }
url = ''
diff --git a/src/sardana/spock/spockms.py b/src/sardana/spock/spockms.py
index b0cd8ad..755ef12 100644
--- a/src/sardana/spock/spockms.py
+++ b/src/sardana/spock/spockms.py
@@ -63,10 +63,17 @@ class GUIViewer(BaseGUIViewer):
def show_scan(self, scan_nb=None, scan_history_info=None, directory_map=None):
if scan_nb is None and scan_history_info is None:
- import taurus.qt.qtgui.plot
- w = taurus.qt.qtgui.plot.TaurusTrend()
- w.model = "scan://" + self._door.getNormalName()
- w.show()
+ #===================================================================
+ ##Hack to avoid ipython-qt issues. See similar workaround in expconf magic command
+ ## @todo: do this in a better way
+ #import taurus.qt.qtgui.plot
+ #w = taurus.qt.qtgui.plot.TaurusTrend()
+ #w.model = "scan://" + self._door.getNormalName()
+ #w.show()
+ import subprocess
+ args = ['taurustrend','scan://%s'%self._door.getNormalName()]
+ subprocess.Popen(args)
+ #===================================================================
return
scan_dir, scan_file = None, None
diff --git a/src/sardana/tango/core/util.py b/src/sardana/tango/core/util.py
index 6bda8b8..ea630f7 100644
--- a/src/sardana/tango/core/util.py
+++ b/src/sardana/tango/core/util.py
@@ -785,7 +785,12 @@ def prepare_logging(options, args, tango_args, start_time=None, log_messages=Non
_, ds_name = os.path.split(args[0])
ds_name, _ = os.path.splitext(ds_name)
ds_instance = args[-1].lower()
- path = os.path.join(os.sep, "tmp", "tango", ds_name, ds_instance)
+ import getpass
+ try:
+ tangodir = 'tango-%s'%getpass.getuser() #include the user name to avoid permission errors
+ except:
+ tangodir = 'tango'%getpass.getuser()
+ path = os.path.join(os.sep, "tmp", tangodir, ds_name, ds_instance)
log_file_name = os.path.join(path, 'log.txt')
else:
log_file_name = options.log_file_name
diff --git a/src/sardana/tango/macroserver/Door.py b/src/sardana/tango/macroserver/Door.py
index 8885c0e..218eb1a 100644
--- a/src/sardana/tango/macroserver/Door.py
+++ b/src/sardana/tango/macroserver/Door.py
@@ -37,7 +37,8 @@ from PyTango import Util, DevFailed, Except, DevVoid, DevShort, DevLong, \
import taurus
import taurus.core.util
-from taurus.core.util import etree, CodecFactory, DebugIt
+from lxml import etree
+from taurus.core.util import CodecFactory, DebugIt
from sardana import State, InvalidId, SardanaServer
from sardana.sardanaattribute import SardanaAttribute
diff --git a/src/sardana/tango/macroserver/MacroServer.py b/src/sardana/tango/macroserver/MacroServer.py
index 4d8b974..d0db368 100644
--- a/src/sardana/tango/macroserver/MacroServer.py
+++ b/src/sardana/tango/macroserver/MacroServer.py
@@ -98,6 +98,7 @@ class MacroServer(SardanaDevice):
import tempfile
env_db = os.path.join(tempfile.mkdtemp(),
MacroServerClass.DefaultEnvRelDir)
+ env_db = self._calculate_name(env_db)
db = Util.instance().get_database()
db.put_device_property(self.get_name(), dict(EnvironmentDb=env_db))
self.EnvironmentDb = env_db
diff --git a/src/sardana/tango/pool/Motor.py b/src/sardana/tango/pool/Motor.py
index e051421..f769621 100644
--- a/src/sardana/tango/pool/Motor.py
+++ b/src/sardana/tango/pool/Motor.py
@@ -547,7 +547,6 @@ with this value is sent to clients using events.
def read_Limit_switches(self, attr):
motor = self.motor
use_cache = motor.is_in_operation() and not self.Force_HW_Read
- self.info("read_Limit_switches(%s)", use_cache)
limit_switches = motor.get_limit_switches(cache=use_cache)
self.set_attribute(attr, value=limit_switches.value, priority=0,
timestamp=limit_switches.timestamp)
diff --git a/src/sardana/util/motion/motion.py b/src/sardana/util/motion/motion.py
index f2f4047..489c8ee 100644
--- a/src/sardana/util/motion/motion.py
+++ b/src/sardana/util/motion/motion.py
@@ -70,10 +70,24 @@ class MotionPath(object):
#: time the motion will take
duration = -1
- def __init__(self, motor, initial_user_pos, final_user_pos):
+ def __init__(self, motor,
+ initial_user_pos,
+ final_user_pos,
+ active_time=None):
+ """MotionPath constructor - creates and calculates
+ motion path parameters.
+ :param initial_user_pos: position at which constant vel
+ should be reached
+ :param final_user_pos: position at which deceleration should
+ start
+ :param active_time: if passed, will fix the constant velocity
+ (abs(final_user_pos - initial_user_pos)/active_time)
+ otherwise motor constant velocity
+ will be selected as high as possible"""
self.motor = motor
self.initial_user_pos = initial_user_pos
self.final_user_pos = final_user_pos
+ self.active_time = active_time
self._calculateMotionPath()
def setInitialUserPos(self, initial_user_pos):
@@ -93,6 +107,24 @@ class MotionPath(object):
final_pos = final_user_pos * motor.step_per_unit
displacement = abs(final_pos - initial_pos)
+
+ # in this case active_time forces that the user range
+ # correspond to the constant velocity
+ # and
+ if self.active_time != None:
+ velocity = displacement / self.active_time
+ self.motor.setMaxVelocity(velocity)
+ sign = final_pos > initial_pos and 1 or -1
+ accel_time = motor.getAccelerationTime()
+ decel_time = motor.getDecelerationTime()
+ base_vel = motor.getMinVelocity()
+ accel_displacement = accel_time * 0.5 * (velocity + base_vel)
+ decel_displacement = decel_time * 0.5 * (velocity + base_vel)
+ initial_pos -= sign * accel_displacement
+ final_pos += sign * decel_displacement
+ displacement = abs(final_pos - initial_pos)
+ self.initial_user_pos = initial_pos
+ self.final_user_pos = final_pos
if displacement == 0:
positive_displacement = False
@@ -201,12 +233,12 @@ class MotionPath(object):
self.small_motion = small_motion
self.accel = accel
- self.decel = decel
-
+ self.decel = decel
+
self.displacement_reach_max_vel = displacement_reach_max_vel
self.displacement_reach_min_vel = displacement_reach_min_vel
- self.max_vel = max_vel
- self.min_vel = min_vel
+ self.max_vel = abs(max_vel) #velocity must be a positive value
+ self.min_vel = abs(min_vel)
self.max_vel_pos = max_vel_pos
self.at_max_vel_displacement = at_max_vel_displacement
self.max_vel_time = max_vel_time
@@ -511,8 +543,10 @@ class Motor(BaseMotor):
self.min_vel = vi
+ #TODO: consult this solution with others
if self.max_vel < self.min_vel:
- self.max_vel = self.min_vel
+ pass
+ #self.max_vel = self.min_vel (original version)
# force recalculation of accelerations
if self.accel_time >= 0:
@@ -530,9 +564,12 @@ class Motor(BaseMotor):
raise Exception("Maximum velocity must be > 0")
self.max_vel = vf
-
+
+ #TODO: consult this solution with others
if self.min_vel > self.max_vel:
- self.min_vel = self.max_vel
+ pass
+ #self.min_vel = self.max_vel #accel set to zero (original version)
+ #self.setMinVelocity(0) another solution could be to set it to 0
# force recalculation of accelerations
if self.accel_time >= 0:
@@ -556,6 +593,7 @@ class Motor(BaseMotor):
self.accel = float('inf')
self.__recalculate_acc_constants()
+
def getAccelerationTime(self):
return self.accel_time
diff --git a/src/sardana/util/tree.py b/src/sardana/util/tree.py
new file mode 100644
index 0000000..3fdb33f
--- /dev/null
+++ b/src/sardana/util/tree.py
@@ -0,0 +1,28 @@
+class BaseNode:
+ """BaseNode, stores reference to data."""
+
+ def __init__(self, data):
+ self.data = data
+
+class BranchNode(BaseNode):
+ """BranchNode, apart of reference to data, stores a list of
+ children Nodes."""
+ def __init__(self, data):
+ BaseNode.__init__(self, data)
+ self.children = []
+
+ def addChild(self, child):
+ self.children.append(child)
+
+class LeafNode(BaseNode):
+ """LeafMode, just stores reference to data."""
+ def __init__(self, data):
+ BaseNode.__init__(self, data)
+
+class Tree:
+ """Base tree class, stores reference to root Node object"""
+ def __init__(self, root):
+ self._root = root
+
+ def root(self):
+ return self._root
\ No newline at end of file
--
Packaging for sardana
More information about the debian-science-commits
mailing list