From 74c54dff38627ccdee8d841b2daccf7fec6d3b16 Mon Sep 17 00:00:00 2001 From: gac-x04sa Date: Fri, 16 Aug 2019 14:54:56 +0200 Subject: [PATCH] Startup --- config/diffcalc/test1.json | 12 +- config/settings.properties | 2 +- script/cpython/image_functions.py | 8 +- script/daq/daq4-setup.py | 10 +- script/device/Image.py | 2 +- script/geometry/fourcv.py | 78 - script/local.py | 107 +- script/test/Retrieving context.py | 5 + .../diffcalc (copy)/.cache/v/cache/lastfailed | 1 + script/test/diffcalc (copy)/.gitignore | 9 + .../com.wdev91.eclipse.copyright.xml | 29 + .../org.eclipse.core.resources.prefs | 3 + .../.settings/org.eclipse.core.runtime.prefs | 2 + script/test/diffcalc (copy)/.travis.yml | 21 + script/test/diffcalc (copy)/COPYING | 674 ++++ script/test/diffcalc (copy)/Makefile | 52 + script/test/diffcalc (copy)/README.rst | 513 +++ .../test/diffcalc (copy)/README_template.rst | 267 ++ script/test/diffcalc (copy)/buckminster.cspec | 5 + script/test/diffcalc (copy)/diffcalc.py | 9 + .../test/diffcalc (copy)/diffcalc/__init__.py | 0 .../diffcalc (copy)/diffcalc/dc/__init__.py | 0 .../diffcalc (copy)/diffcalc/dc/common.py | 24 + .../diffcalc (copy)/diffcalc/dc/dcvlieg.py | 76 + .../diffcalc (copy)/diffcalc/dc/dcwillmot.py | 47 + .../test/diffcalc (copy)/diffcalc/dc/dcyou.py | 56 + .../test/diffcalc (copy)/diffcalc/dc/help.py | 161 + .../diffcalc/gdasupport/__init__.py | 0 .../diffcalc/gdasupport/minigda/__init__.py | 0 .../diffcalc/gdasupport/minigda/command.py | 322 ++ .../diffcalc/gdasupport/minigda/scannable.py | 511 +++ .../diffcalc/gdasupport/scannable/__init__.py | 0 .../diffcalc/gdasupport/scannable/base.py | 62 + .../gdasupport/scannable/diffractometer.py | 126 + .../diffcalc/gdasupport/scannable/hkl.py | 135 + .../diffcalc/gdasupport/scannable/mock.py | 47 + .../gdasupport/scannable/parameter.py | 45 + .../diffcalc/gdasupport/scannable/sim.py | 21 + .../gdasupport/scannable/simulation.py | 139 + .../gdasupport/scannable/slave_driver.py | 109 + .../gdasupport/scannable/vrmlanimator.py | 184 ++ .../gdasupport/scannable/wavelength.py | 50 + .../diffcalc/gdasupport/you.py | 113 + .../test/diffcalc (copy)/diffcalc/hardware.py | 382 +++ .../diffcalc (copy)/diffcalc/hkl/__init__.py | 0 .../diffcalc (copy)/diffcalc/hkl/calcbase.py | 155 + .../diffcalc (copy)/diffcalc/hkl/common.py | 55 + .../diffcalc/hkl/vlieg/__init__.py | 0 .../diffcalc/hkl/vlieg/calc.py | 846 +++++ .../diffcalc/hkl/vlieg/constraints.py | 336 ++ .../diffcalc/hkl/vlieg/geometry.py | 523 ++++ .../diffcalc (copy)/diffcalc/hkl/vlieg/hkl.py | 139 + .../diffcalc/hkl/vlieg/transform.py | 480 +++ .../diffcalc/hkl/willmott/__init__.py | 0 .../diffcalc/hkl/willmott/calc.py | 292 ++ .../diffcalc/hkl/willmott/commands.py | 58 + .../diffcalc/hkl/willmott/constraints.py | 156 + .../diffcalc/hkl/you/__init__.py | 0 .../diffcalc (copy)/diffcalc/hkl/you/calc.py | 1167 +++++++ .../diffcalc/hkl/you/constraints.py | 377 +++ .../diffcalc/hkl/you/geometry.py | 219 ++ .../diffcalc (copy)/diffcalc/hkl/you/hkl.py | 187 ++ script/test/diffcalc (copy)/diffcalc/log.py | 27 + .../test/diffcalc (copy)/diffcalc/settings.py | 24 + .../diffcalc (copy)/diffcalc/ub/__init__.py | 0 .../test/diffcalc (copy)/diffcalc/ub/calc.py | 854 +++++ .../diffcalc (copy)/diffcalc/ub/calcstate.py | 220 ++ .../diffcalc (copy)/diffcalc/ub/crystal.py | 147 + .../diffcalc/ub/orientations.py | 118 + .../diffcalc/ub/persistence.py | 151 + .../diffcalc (copy)/diffcalc/ub/reference.py | 99 + .../diffcalc/ub/reflections.py | 126 + script/test/diffcalc (copy)/diffcalc/ub/ub.py | 768 +++++ script/test/diffcalc (copy)/diffcalc/util.py | 347 +++ .../test/diffcalc (copy)/diffcmd/__init__.py | 0 .../diffcmd/diffcalc_launcher.py | 86 + .../diffcalc (copy)/diffcmd/diffcmd_utils.py | 43 + .../test/diffcalc (copy)/diffcmd/ipython.py | 302 ++ .../diffcalc (copy)/diffcmd/ipythonmagic.py | 79 + .../diffcalc (copy)/diffcmd/make_manual.py | 146 + script/test/diffcalc (copy)/diffcmd/start.py | 87 + script/test/diffcalc (copy)/doc/Makefile | 193 ++ .../doc/docs-build-diffcalc_doc-linux.launch | 8 + script/test/diffcalc (copy)/doc/references | 22 + .../test/diffcalc (copy)/doc/source/ACKS.rst | 34 + .../test/diffcalc (copy)/doc/source/conf.py | 243 ++ .../doc/source/developer/contents.rst | 25 + .../doc/source/developer/development.rst | 24 + .../doc/source/developer/intro.rst | 55 + .../doc/source/developer/package.rst | 30 + .../doc/source/developer/quickstart_api.rst | 266 ++ .../developer/quickstart_setup_environment | 25 + .../doc/source/diffcalc_pdf.png | Bin 0 -> 20720 bytes .../doc/source/diffcalc_web.png | Bin 0 -> 3980 bytes .../test/diffcalc (copy)/doc/source/index.rst | 26 + .../doc/source/vliegmanual/contents.rst | 22 + .../doc/source/vliegmanual/images/fix.png | Bin 0 -> 283 bytes .../images/sixcircle_gamma_on_arm.pdf | Bin 0 -> 69851 bytes .../images/sixcircle_gamma_on_arm.png | Bin 0 -> 73974 bytes .../images/sixcircle_gamma_on_arm.ppt | Bin 0 -> 79360 bytes .../source/vliegmanual/images/unit_cell.pdf | Bin 0 -> 5812 bytes .../source/vliegmanual/images/unit_cell.png | Bin 0 -> 1598 bytes .../doc/source/vliegmanual/vliegmanual.rst | 827 +++++ .../diffcalc (copy)/doc/source/youmanual.rst | 888 ++++++ .../youmanual_images/4s_2d_diffractometer.png | Bin 0 -> 336954 bytes .../doc/source/youmanual_template.rst | 565 ++++ .../diffcalc (copy)/doc/tmp/constraints.txt | 47 + .../doc/tmp/extensions_to_yous_paper.wxm | 525 ++++ .../doc/tmp/i16_non_diffcalc_manual.txt | 71 + .../install-jython-environment.sh | 19 + .../diffcalc (copy)/integration_checks.py | 72 + script/test/diffcalc (copy)/mock.py | 2288 ++++++++++++++ script/test/diffcalc (copy)/model/README.txt | 13 + script/test/diffcalc (copy)/model/fivec.fxw | Bin 0 -> 120483 bytes script/test/diffcalc (copy)/model/fivec.wcfg | 5 + script/test/diffcalc (copy)/model/fivec.wrl | 1011 ++++++ script/test/diffcalc (copy)/model/sixc.fxw | Bin 0 -> 127136 bytes script/test/diffcalc (copy)/model/sixc.wcfg | 6 + script/test/diffcalc (copy)/model/sixc.wrl | 1101 +++++++ .../diffcalc (copy)/model/sixc_horizontal.fxw | Bin 0 -> 128929 bytes .../diffcalc (copy)/model/sixc_horizontal.wrl | 1106 +++++++ .../diffcalc (copy)/model/vrml_animator.py | 134 + script/test/diffcalc (copy)/numjy/__init__.py | 36 + .../numjy/jama_matrix_wrapper.py | 120 + script/test/diffcalc (copy)/numjy/linalg.py | 20 + script/test/diffcalc (copy)/setup.py | 31 + .../diffcalc (copy)/simplejson/__init__.py | 510 +++ .../diffcalc (copy)/simplejson/_speedups.c | 2745 +++++++++++++++++ .../diffcalc (copy)/simplejson/decoder.py | 427 +++ .../diffcalc (copy)/simplejson/encoder.py | 567 ++++ .../simplejson/ordered_dict.py | 119 + .../diffcalc (copy)/simplejson/scanner.py | 77 + .../test/diffcalc (copy)/simplejson/tool.py | 39 + .../test/diffcalc (copy)/startup/__init__.py | 0 .../startup/_common_imports.py | 66 + script/test/diffcalc (copy)/startup/_demo.py | 151 + .../startup/_make_sixcircle_manual.py | 44 + .../diffcalc (copy)/startup/api/__init__.py | 0 .../diffcalc (copy)/startup/api/sixcircle.py | 73 + .../startup/beamlinespecific/__init__.py | 0 .../startup/beamlinespecific/i21.py | 340 ++ .../diffcalc (copy)/startup/fivecircle.py | 36 + .../diffcalc (copy)/startup/fourcircle.py | 31 + script/test/diffcalc (copy)/startup/i06.py | 56 + script/test/diffcalc (copy)/startup/i10.py | 138 + script/test/diffcalc (copy)/startup/i13.py | 186 ++ script/test/diffcalc (copy)/startup/i16.py | 71 + script/test/diffcalc (copy)/startup/i21.py | 465 +++ .../test/diffcalc (copy)/startup/sixcircle.py | 37 + script/test/diffcalc (copy)/test/__init__.py | 0 .../diffcalc (copy)/test/diffcalc/__init__.py | 0 .../test/diffcalc/dc/__init__.py | 0 .../test/diffcalc/dc/test_dcvlieg.py | 707 +++++ .../test/diffcalc/dc/you/__init__.py | 0 .../test/diffcalc/dc/you/test_fourcircle.py | 72 + .../test/diffcalc/dc/you/test_sixcircle.py | 95 + .../test/diffcalc/gdasupport/__init__.py | 0 .../diffcalc/gdasupport/minigda/__init__.py | 0 .../gdasupport/minigda/test_command.py | 138 + .../gdasupport/minigda/test_scannable.py | 83 + .../diffcalc/gdasupport/scannable/__init__.py | 0 .../gdasupport/scannable/mockdiffcalc.py | 55 + .../scannable/test_diffractometer.py | 169 + .../diffcalc/gdasupport/scannable/test_hkl.py | 222 ++ .../gdasupport/scannable/test_parameter.py | 38 + .../gdasupport/scannable/test_simulation.py | 102 + .../gdasupport/scannable/test_wavelength.py | 40 + .../test/diffcalc/gdasupport/test_you.py | 180 ++ .../test/diffcalc/hkl/__init__.py | 0 .../test/diffcalc/hkl/vlieg/__init__.py | 0 .../test/diffcalc/hkl/vlieg/test_calc.py | 253 ++ .../diffcalc/hkl/vlieg/test_constraints.py | 92 + .../test/diffcalc/hkl/vlieg/test_geometry.py | 248 ++ .../test/diffcalc/hkl/vlieg/test_hkl.py | 84 + .../test/diffcalc/hkl/vlieg/test_transform.py | 463 +++ .../test/diffcalc/hkl/willmot/__init__.py | 0 .../diffcalc/hkl/willmot/test_calcwill.py | 323 ++ .../test/diffcalc/hkl/you/__init__.py | 0 .../test/diffcalc/hkl/you/test_calc.py | 1265 ++++++++ .../diffcalc/hkl/you/test_calc_methods.py | 481 +++ .../diffcalc/hkl/you/test_calc_surface.py | 557 ++++ .../test/diffcalc/hkl/you/test_constraints.py | 575 ++++ .../test/diffcalc/hkl/you/test_hkl.py | 96 + .../test/diffcalc/scenarios.py | 202 ++ .../test/diffcalc/test_hardware.py | 246 ++ .../test/diffcalc/test_utils.py | 127 + .../test/diffcalc/ub/__init__.py | 0 .../test/diffcalc/ub/test_calculation.py | 174 ++ .../diffcalc/ub/test_calculation_vlieg.py | 380 +++ .../test/diffcalc/ub/test_calculation_you.py | 83 + .../test/diffcalc/ub/test_crystal.py | 62 + .../test/diffcalc/ub/test_persistence.py | 100 + .../test/diffcalc/ub/test_reference.py | 61 + .../test/diffcalc/ub/test_reflections.py | 91 + .../test/diffcalc/ub/test_ub.py | 734 +++++ .../diffcalc (copy)/test/diffcmd/__init__.py | 0 .../test/diffcmd/test_ipython.py | 172 ++ ..._for_SixCircleGammaOnArmTestAgainstDif.txt | 690 +++++ .../test/ref/i16_reference_results.txt | 198 ++ .../test/ref/output_from_spec.txt | 1343 ++++++++ .../diffcalc (copy)/test/startup/__init__.py | 0 .../test/startup/beamlinespecific/__init__.py | 0 .../test/startup/beamlinespecific/i21.py | 247 ++ .../test/diffcalc (copy)/test/test_numjy.py | 245 ++ .../test/diffcalc (copy)/test/test_tools.py | 177 ++ script/test/diffcalc (copy)/test/tools.py | 164 + script/test/diffcalc (copy)/todo.txt | 70 + script/test/diffcalc (copy)/tox.ini | 23 + 208 files changed, 40866 insertions(+), 130 deletions(-) create mode 100644 script/test/Retrieving context.py create mode 100644 script/test/diffcalc (copy)/.cache/v/cache/lastfailed create mode 100644 script/test/diffcalc (copy)/.gitignore create mode 100644 script/test/diffcalc (copy)/.settings/com.wdev91.eclipse.copyright.xml create mode 100644 script/test/diffcalc (copy)/.settings/org.eclipse.core.resources.prefs create mode 100644 script/test/diffcalc (copy)/.settings/org.eclipse.core.runtime.prefs create mode 100644 script/test/diffcalc (copy)/.travis.yml create mode 100644 script/test/diffcalc (copy)/COPYING create mode 100644 script/test/diffcalc (copy)/Makefile create mode 100644 script/test/diffcalc (copy)/README.rst create mode 100644 script/test/diffcalc (copy)/README_template.rst create mode 100644 script/test/diffcalc (copy)/buckminster.cspec create mode 100755 script/test/diffcalc (copy)/diffcalc.py create mode 100644 script/test/diffcalc (copy)/diffcalc/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/dc/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/dc/common.py create mode 100644 script/test/diffcalc (copy)/diffcalc/dc/dcvlieg.py create mode 100644 script/test/diffcalc (copy)/diffcalc/dc/dcwillmot.py create mode 100644 script/test/diffcalc (copy)/diffcalc/dc/dcyou.py create mode 100644 script/test/diffcalc (copy)/diffcalc/dc/help.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/command.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/scannable.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/base.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/diffractometer.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/hkl.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/mock.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/parameter.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/sim.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/simulation.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/slave_driver.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/vrmlanimator.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/wavelength.py create mode 100644 script/test/diffcalc (copy)/diffcalc/gdasupport/you.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hardware.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/calcbase.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/common.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/vlieg/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/vlieg/calc.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/vlieg/constraints.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/vlieg/geometry.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/vlieg/hkl.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/vlieg/transform.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/willmott/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/willmott/calc.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/willmott/commands.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/willmott/constraints.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/you/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/you/calc.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/you/constraints.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/you/geometry.py create mode 100644 script/test/diffcalc (copy)/diffcalc/hkl/you/hkl.py create mode 100644 script/test/diffcalc (copy)/diffcalc/log.py create mode 100644 script/test/diffcalc (copy)/diffcalc/settings.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/__init__.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/calc.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/calcstate.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/crystal.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/orientations.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/persistence.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/reference.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/reflections.py create mode 100644 script/test/diffcalc (copy)/diffcalc/ub/ub.py create mode 100644 script/test/diffcalc (copy)/diffcalc/util.py create mode 100644 script/test/diffcalc (copy)/diffcmd/__init__.py create mode 100755 script/test/diffcalc (copy)/diffcmd/diffcalc_launcher.py create mode 100644 script/test/diffcalc (copy)/diffcmd/diffcmd_utils.py create mode 100644 script/test/diffcalc (copy)/diffcmd/ipython.py create mode 100644 script/test/diffcalc (copy)/diffcmd/ipythonmagic.py create mode 100644 script/test/diffcalc (copy)/diffcmd/make_manual.py create mode 100644 script/test/diffcalc (copy)/diffcmd/start.py create mode 100644 script/test/diffcalc (copy)/doc/Makefile create mode 100644 script/test/diffcalc (copy)/doc/docs-build-diffcalc_doc-linux.launch create mode 100644 script/test/diffcalc (copy)/doc/references create mode 100644 script/test/diffcalc (copy)/doc/source/ACKS.rst create mode 100644 script/test/diffcalc (copy)/doc/source/conf.py create mode 100644 script/test/diffcalc (copy)/doc/source/developer/contents.rst create mode 100644 script/test/diffcalc (copy)/doc/source/developer/development.rst create mode 100644 script/test/diffcalc (copy)/doc/source/developer/intro.rst create mode 100644 script/test/diffcalc (copy)/doc/source/developer/package.rst create mode 100644 script/test/diffcalc (copy)/doc/source/developer/quickstart_api.rst create mode 100644 script/test/diffcalc (copy)/doc/source/developer/quickstart_setup_environment create mode 100644 script/test/diffcalc (copy)/doc/source/diffcalc_pdf.png create mode 100644 script/test/diffcalc (copy)/doc/source/diffcalc_web.png create mode 100644 script/test/diffcalc (copy)/doc/source/index.rst create mode 100644 script/test/diffcalc (copy)/doc/source/vliegmanual/contents.rst create mode 100644 script/test/diffcalc (copy)/doc/source/vliegmanual/images/fix.png create mode 100644 script/test/diffcalc (copy)/doc/source/vliegmanual/images/sixcircle_gamma_on_arm.pdf create mode 100755 script/test/diffcalc (copy)/doc/source/vliegmanual/images/sixcircle_gamma_on_arm.png create mode 100755 script/test/diffcalc (copy)/doc/source/vliegmanual/images/sixcircle_gamma_on_arm.ppt create mode 100644 script/test/diffcalc (copy)/doc/source/vliegmanual/images/unit_cell.pdf create mode 100644 script/test/diffcalc (copy)/doc/source/vliegmanual/images/unit_cell.png create mode 100644 script/test/diffcalc (copy)/doc/source/vliegmanual/vliegmanual.rst create mode 100644 script/test/diffcalc (copy)/doc/source/youmanual.rst create mode 100644 script/test/diffcalc (copy)/doc/source/youmanual_images/4s_2d_diffractometer.png create mode 100644 script/test/diffcalc (copy)/doc/source/youmanual_template.rst create mode 100644 script/test/diffcalc (copy)/doc/tmp/constraints.txt create mode 100644 script/test/diffcalc (copy)/doc/tmp/extensions_to_yous_paper.wxm create mode 100644 script/test/diffcalc (copy)/doc/tmp/i16_non_diffcalc_manual.txt create mode 100755 script/test/diffcalc (copy)/install-jython-environment.sh create mode 100644 script/test/diffcalc (copy)/integration_checks.py create mode 100644 script/test/diffcalc (copy)/mock.py create mode 100644 script/test/diffcalc (copy)/model/README.txt create mode 100644 script/test/diffcalc (copy)/model/fivec.fxw create mode 100644 script/test/diffcalc (copy)/model/fivec.wcfg create mode 100644 script/test/diffcalc (copy)/model/fivec.wrl create mode 100644 script/test/diffcalc (copy)/model/sixc.fxw create mode 100644 script/test/diffcalc (copy)/model/sixc.wcfg create mode 100644 script/test/diffcalc (copy)/model/sixc.wrl create mode 100644 script/test/diffcalc (copy)/model/sixc_horizontal.fxw create mode 100644 script/test/diffcalc (copy)/model/sixc_horizontal.wrl create mode 100644 script/test/diffcalc (copy)/model/vrml_animator.py create mode 100644 script/test/diffcalc (copy)/numjy/__init__.py create mode 100644 script/test/diffcalc (copy)/numjy/jama_matrix_wrapper.py create mode 100644 script/test/diffcalc (copy)/numjy/linalg.py create mode 100644 script/test/diffcalc (copy)/setup.py create mode 100644 script/test/diffcalc (copy)/simplejson/__init__.py create mode 100644 script/test/diffcalc (copy)/simplejson/_speedups.c create mode 100644 script/test/diffcalc (copy)/simplejson/decoder.py create mode 100644 script/test/diffcalc (copy)/simplejson/encoder.py create mode 100644 script/test/diffcalc (copy)/simplejson/ordered_dict.py create mode 100644 script/test/diffcalc (copy)/simplejson/scanner.py create mode 100644 script/test/diffcalc (copy)/simplejson/tool.py create mode 100644 script/test/diffcalc (copy)/startup/__init__.py create mode 100644 script/test/diffcalc (copy)/startup/_common_imports.py create mode 100644 script/test/diffcalc (copy)/startup/_demo.py create mode 100644 script/test/diffcalc (copy)/startup/_make_sixcircle_manual.py create mode 100644 script/test/diffcalc (copy)/startup/api/__init__.py create mode 100644 script/test/diffcalc (copy)/startup/api/sixcircle.py create mode 100644 script/test/diffcalc (copy)/startup/beamlinespecific/__init__.py create mode 100644 script/test/diffcalc (copy)/startup/beamlinespecific/i21.py create mode 100644 script/test/diffcalc (copy)/startup/fivecircle.py create mode 100644 script/test/diffcalc (copy)/startup/fourcircle.py create mode 100644 script/test/diffcalc (copy)/startup/i06.py create mode 100644 script/test/diffcalc (copy)/startup/i10.py create mode 100644 script/test/diffcalc (copy)/startup/i13.py create mode 100644 script/test/diffcalc (copy)/startup/i16.py create mode 100644 script/test/diffcalc (copy)/startup/i21.py create mode 100644 script/test/diffcalc (copy)/startup/sixcircle.py create mode 100644 script/test/diffcalc (copy)/test/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/dc/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/dc/test_dcvlieg.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/dc/you/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/dc/you/test_fourcircle.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/dc/you/test_sixcircle.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/minigda/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/minigda/test_command.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/minigda/test_scannable.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/scannable/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/scannable/mockdiffcalc.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/scannable/test_diffractometer.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/scannable/test_hkl.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/scannable/test_parameter.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/scannable/test_simulation.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/scannable/test_wavelength.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/gdasupport/test_you.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/vlieg/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/vlieg/test_calc.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/vlieg/test_constraints.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/vlieg/test_geometry.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/vlieg/test_hkl.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/vlieg/test_transform.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/willmot/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/willmot/test_calcwill.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/you/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/you/test_calc.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/you/test_calc_methods.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/you/test_calc_surface.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/you/test_constraints.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/hkl/you/test_hkl.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/scenarios.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/test_hardware.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/test_utils.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_calculation.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_calculation_vlieg.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_calculation_you.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_crystal.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_persistence.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_reference.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_reflections.py create mode 100644 script/test/diffcalc (copy)/test/diffcalc/ub/test_ub.py create mode 100644 script/test/diffcalc (copy)/test/diffcmd/__init__.py create mode 100644 script/test/diffcalc (copy)/test/diffcmd/test_ipython.py create mode 100644 script/test/diffcalc (copy)/test/ref/b16_dif_270608_printout_for_SixCircleGammaOnArmTestAgainstDif.txt create mode 100644 script/test/diffcalc (copy)/test/ref/i16_reference_results.txt create mode 100644 script/test/diffcalc (copy)/test/ref/output_from_spec.txt create mode 100644 script/test/diffcalc (copy)/test/startup/__init__.py create mode 100644 script/test/diffcalc (copy)/test/startup/beamlinespecific/__init__.py create mode 100644 script/test/diffcalc (copy)/test/startup/beamlinespecific/i21.py create mode 100644 script/test/diffcalc (copy)/test/test_numjy.py create mode 100644 script/test/diffcalc (copy)/test/test_tools.py create mode 100644 script/test/diffcalc (copy)/test/tools.py create mode 100644 script/test/diffcalc (copy)/todo.txt create mode 100644 script/test/diffcalc (copy)/tox.ini diff --git a/config/diffcalc/test1.json b/config/diffcalc/test1.json index ab7f00c..0b0df76 100644 --- a/config/diffcalc/test1.json +++ b/config/diffcalc/test1.json @@ -3,25 +3,25 @@ "crystal": "['cubic', 7.723, 7.707, 7.723, 90.0, 89.265, 90.0]", "reflist": { "1": { - "tag": null, + "tag": "None", "hkl": "[0.0, 1.0, 2.0]", "pos": "[5.0, 9.379, 19.7895, 102.6162, 0, 0]", "energy": 9.5007, - "time": "2019-07-31T14:14:59.939000" + "time": "2019-08-15T11:11:28.003000" }, "2": { - "tag": null, + "tag": "None", "hkl": "[2.0, 0.0, 2.0]", "pos": "[5.0, 19.0754, 20.1865, 11.9693, 0, 0]", "energy": 9.5007, - "time": "2019-07-31T14:14:59.973000" + "time": "2019-08-15T11:11:28.025000" }, "3": { - "tag": null, + "tag": "None", "hkl": "[2.0, 2.0, 2.0]", "pos": "[5.0, 27.1234, 21.2368, 60.0354, 0, 0]", "energy": 9.5007, - "time": "2019-07-31T14:15:00.093000" + "time": "2019-08-15T11:11:28.082000" } }, "orientlist": {}, diff --git a/config/settings.properties b/config/settings.properties index 64f5b7a..4048598 100644 --- a/config/settings.properties +++ b/config/settings.properties @@ -1,4 +1,4 @@ -#Wed Jul 31 17:19:31 CEST 2019 +#Fri Aug 16 14:54:41 CEST 2019 count_time=1.0 geometry=fourcv roi=173 88 285 136 diff --git a/script/cpython/image_functions.py b/script/cpython/image_functions.py index dab5206..eb6f4e9 100644 --- a/script/cpython/image_functions.py +++ b/script/cpython/image_functions.py @@ -1,9 +1,15 @@ import numpy +from scipy import signal, ndimage def img_get_int(fname, thres1, thres2, thres3, thres4, header, width, height, depth, x1,y1,x2,y2, bx1,by1,bx2,by2 , filter_median = False, filter_nsigma = 0): # read actual image file img = numpy.fromfile(fname, dtype=numpy.uint32) img.shape = height, width + if filter_nsigma>0: + img = ndimage.gaussian_filter(img, filter_nsigma) + elif filter_median: + #img = signal.medfilt2d(img.astype('d'), kernel_size=3) + img = ndimage.median_filter(img, size=3) # signal roi area_I = ( x2 - x1 + 1) * ( y2 - y1 + 1) I_sum = img[y1:y2, x1:x2].sum() @@ -11,10 +17,10 @@ def img_get_int(fname, thres1, thres2, thres3, thres4, header, width, height, d thresh2_count = len(numpy.where(img>thres2)[0]) thresh3_count = len(numpy.where(img>thres3)[0]) thresh4_count = len(numpy.where(img>thres4)[0]) - # background roi I_sum_bgr = img[by1:by2, bx1:bx2].sum() area_bgr= (bx2 - bx1 + 1) * (by2 - by1 + 1) + return (I_sum, area_I, thresh1_count, thresh2_count, thresh3_count, thresh4_count, I_sum_bgr, area_bgr) diff --git a/script/daq/daq4-setup.py b/script/daq/daq4-setup.py index 89ca13c..f48b93d 100644 --- a/script/daq/daq4-setup.py +++ b/script/daq/daq4-setup.py @@ -6,19 +6,19 @@ set_geometry("fourcv", True) ###################################################################################################\ #Constraints ################################################################################################### -#hkl.con('a_eq_b') +#con('a_eq_b') #equivalent to 'setmode 0' + 'freeze 5' -hkl.con( 'alpha', 5) #alpha constant +con( 'alpha', 5) #alpha constant #equivalent to 'setmode 1' + 'freeze 5' -#hkl.con( 'beta', 5) #alpha constant +#con( 'beta', 5) #alpha constant #equivalent to 'setmode 2'' -#hkl.con( 'a_eq_b', 0) #alpha constant +#con( 'a_eq_b', 0) #alpha constant -#hkl.con( 'eta', 0) +#con( 'eta', 0) setup_axis(alpha, alpha.getMinValue(), alpha.getMaxValue()) diff --git a/script/device/Image.py b/script/device/Image.py index 6ae1a52..f0ef8bc 100644 --- a/script/device/Image.py +++ b/script/device/Image.py @@ -92,7 +92,7 @@ class Image(DeviceBase, Readable): try: ret = img_get_int(tmp_file, threshold1, threshold2, threshold3, threshold4, \ self.pixel.image_header_length, self.pixel.PIX_XDIM, self.pixel.PIX_YDIM,self.pixel.PIX_COLOR_DEPTH, \ - roi[0], roi[1], roi[2], roi[3], bkroi[0], bkroi[1], bkroi[2], bkroi[3]) + roi[0], roi[1], roi[2], roi[3], bkroi[0], bkroi[1], bkroi[2], bkroi[3], filter_median = True, filter_nsigma = 30) self.I_sum, self.area_I, self.thresh1_count, self.thresh2_count, self.thresh3_count, self.thresh4_count, self.I_sum_bgr, self.area_bgr = ret except: log("Error calculating intensity: " + str(filename) + " - " + str(sys.exc_info()[1]), False) diff --git a/script/geometry/fourcv.py b/script/geometry/fourcv.py index 2ac2ed2..ec2a7b2 100644 --- a/script/geometry/fourcv.py +++ b/script/geometry/fourcv.py @@ -1,86 +1,8 @@ run("diffutils") -###################################################################################################\ -#Setup -################################################################################################### - #alpha, delta, gamma, omegaV setup_diff(fourcv, energy, ("mu", "delta", "gam", "eta"), simultaneous_move=True) -#setup_axis(alpha, alpha.getMinValue(), alpha.getMaxValue()) -#setup_axis(delta, delta.getMinValue(), 90) #delta.getMaxValue()) -#setup_axis(gamma, 0, gamma.getMaxValue()) -#setup_axis(omegaV, omegaV.getMinValue(), omegaV.getMaxValue()) - - if energy.isSimulated(): wavelength.write(1.305) - - -""" -###################################################################################################\ -#Orientation -################################################################################################### -help(ub.ub) -ub.listub() - -#alpha delta gamma omegaV - -# Create a new ub calculation and set lattice parameters -ub.newub('test') - -#ub.setlat('cubic', 5.114, 5.8361, 11.058, 90, 90, 90) -#en = 8 -##ub.c2th([0, 0, 4], en) -##ub.addref([0, 0, 4]) #From current position and ekergy -#ub.addref([0, 0, 4], [16.2785, 0.0, 32.5568, 0.0], en) -##ub.c2th([2, 0, 12], en) -#ub.addref([2, 0, 12], [71.8285, 37.3082, 138.7440, 0.0], en) -##ub.c2th([1, -4, 10], en) -#ub.addref([1, -4, 10], [27.7185, 17.6409 , 128.4220, 0.0], en) - -#ub.setlat('cubic', 1.0, 1.0, 1.0, 90, 90, 90) -ub.setlat('cubic', 1.305, 1.305, 1.305, 90, 90, 90) - -#en = 12.4 -en = 9.5 -#ub.c2th([0, 0, 4], en) -#ub.addref([0, 0, 4]) #From current position and ekergy -ub.addref([0, 0, 1], [30.0, 0.0, 60.0, 0.0], en) -#ub.c2th([2, 0, 12], en) -ub.addref([1, 0, 1], [20.0, 45.5564,90.000, 44.4437], en) -#ub.c2th([1, -4, 10], en) -ub.addref([0, 1, 1], [20.0, 45.5564,90.000, 134.4437], en) - -ub.ub() - -#ub.setub([[1.22862,0.00000,0.00000], [-0.00000,1.07663,0.00000], [-0.00000,-0.00000,0.56820]]) - - -# check the state - -ub.checkub() - - - -###################################################################################################\ -#Constraints -################################################################################################### -help(hkl.con) -#hkl.con('a_eq_b') -#hkl.con('eta:0') - - -hkl.con( 'eta', 0) #OmegaV constant -#hkl.con( 'mu', 20) #Alpha constant - - -###################################################################################################\ -#Motion -################################################################################################### - -#print angles_to_hkl((16.278, 0.0000, 32.5568, 0.0)) -#print angles_to_hkl((44.3400, 0.0000, 123.7322 , 0.0)) -#print hkl_to_angles(2, -2, 10) -""" diff --git a/script/local.py b/script/local.py index 05fb0ed..82a06b0 100644 --- a/script/local.py +++ b/script/local.py @@ -3,22 +3,26 @@ ################################################################################################### import os import os.path +from shutil import copyfile +import json ################################################################################################### # Interlocks ################################################################################################### -class MyInterlock1 (Interlock): - def __init__(self): - Interlock.__init__(self, (alpha, gamma)) +class InterlockFourcv (Interlock): + def __init__(self): + Interlock.__init__(self, (fourcv, alpha, delta, gamma, omegaV)) - def check(self, (a, g)): - if a>=g: + def check(self, (p, a, d, g, o)): + if fourcv.isStartingSimultaneousMove(): + a, d, g, o = p + if a>g: return False return True - -#interlock1 = MyInterlock1() + +#interlock1 = InterlockFourcv() ################################################################################################### # Hardware @@ -99,37 +103,6 @@ def set_count_time(value): """ set_setting(COUNT_TIME_PREFERENCE, value ) - - -def get_geometry(): - """ - """ - setting = get_setting(GEOMETRY_PREFERENCE) - if setting is None or (len(setting.strip()) == 0): - return None - return setting - -def set_geometry(value, apply = None): - """ - """ - if value is None or (len(value.strip()) == 0): - set_setting(GEOMETRY_PREFERENCE, "" ) - for name in "wavelength", "hkl_group", "h", "k", "l": - dev = get_device(name) - if dev is not None: - remove_device(dev) - return - filename = get_context().setup.expandPath("{script}/geometry/"+ str(value)+".py") - if not os.path.isfile(filename): - raise Exception("Invalid geometry file: " + value) - former = get_geometry() - if ((apply is None) and former != value) or (apply==True) : - set_setting(GEOMETRY_PREFERENCE, value ) - run(filename) - -def is_geometry_set(): - return get_device("wavelength") is not None - def get_roi(): """ """ @@ -162,6 +135,41 @@ def set_bg_roi(x1, y1, x2, y2): set_setting(BG_ROI_PREFERENCE, str(int(x1)) + " " + str(int(y1)) + " " + str(int(x2)) + " " + str(int(y2)) ) +################################################################################################### +# Context +################################################################################################### + + +def get_geometry(): + """ + """ + setting = get_setting(GEOMETRY_PREFERENCE) + if setting is None or (len(setting.strip()) == 0): + return None + return setting + +def set_geometry(value, apply = None): + """ + """ + if value is None or (len(value.strip()) == 0): + set_setting(GEOMETRY_PREFERENCE, "" ) + for name in "wavelength", "hkl_group", "h", "k", "l": + dev = get_device(name) + if dev is not None: + remove_device(dev) + return + filename = get_context().setup.expandPath("{script}/geometry/"+ str(value)+".py") + if not os.path.isfile(filename): + raise Exception("Invalid geometry file: " + value) + former = get_geometry() + if ((apply is None) and former != value) or (apply==True) : + set_setting(GEOMETRY_PREFERENCE, value ) + run(filename) + +def is_geometry_set(): + return get_device("wavelength") is not None + + ################################################################################################### # Scan callbacks ################################################################################################### @@ -377,12 +385,34 @@ def wait_for_file_size(filepath, size, timeout = None): def set_data_path(path): + """Changes data root path. + """ get_context().setDataPath(path) def set_script_path(path): + """Changes script root path. + """ get_context().setScriptPath(path) +def backup_ub(name=None, destination = "{data}"): + """Copies ub matrix (default= current) to user space. + """ + if not name: + name = ub.ubcalc._state.name + name = name + ".json" + f = settings.persistence_path + "/" + name + if not os.path.isfile(f): + raise Exception("Invalid UB name: " + str(name)) + copyfile(f, get_context().setup.expandPath(destination + "/" +name)) + +def restore_ub(name, origin = "{data}"): + """Restores ub matrix from user space and loads it. + """ + f = settings.persistence_path + "/" + name + ".json" + copyfile(get_context().setup.expandPath(origin + "/" +name + ".json"), f) + loadub(name) + ################################################################################################### # HKL commands ################################################################################################### @@ -496,6 +526,7 @@ def hkllinscan(hstart, hfinish, kstart, kfinish, lstart, lfinish, number_of_step ################################################################################################### set_geometry(get_geometry(),True) +load_exp_context() diff --git a/script/test/Retrieving context.py b/script/test/Retrieving context.py new file mode 100644 index 0000000..651d894 --- /dev/null +++ b/script/test/Retrieving context.py @@ -0,0 +1,5 @@ +print getub() + + +print hklci((44.3400, 0.0000, 123.7322 , 0.0))[0] +print hklca((0, 1, 2))[0] diff --git a/script/test/diffcalc (copy)/.cache/v/cache/lastfailed b/script/test/diffcalc (copy)/.cache/v/cache/lastfailed new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/script/test/diffcalc (copy)/.cache/v/cache/lastfailed @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/script/test/diffcalc (copy)/.gitignore b/script/test/diffcalc (copy)/.gitignore new file mode 100644 index 0000000..edda307 --- /dev/null +++ b/script/test/diffcalc (copy)/.gitignore @@ -0,0 +1,9 @@ +*.class +*~ +*.pyc +doc/build/ +.pydevproject +.project +.tox +.DS_Store +.idea diff --git a/script/test/diffcalc (copy)/.settings/com.wdev91.eclipse.copyright.xml b/script/test/diffcalc (copy)/.settings/com.wdev91.eclipse.copyright.xml new file mode 100644 index 0000000..eb2e1ed --- /dev/null +++ b/script/test/diffcalc (copy)/.settings/com.wdev91.eclipse.copyright.xml @@ -0,0 +1,29 @@ + + + Diamond Light Source Ltd. + .]]> +
+ + + +
+
+ + + +
+
diff --git a/script/test/diffcalc (copy)/.settings/org.eclipse.core.resources.prefs b/script/test/diffcalc (copy)/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..d0a9e4e --- /dev/null +++ b/script/test/diffcalc (copy)/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//doc/source/conf.py=utf-8 +encoding/=UTF-8 diff --git a/script/test/diffcalc (copy)/.settings/org.eclipse.core.runtime.prefs b/script/test/diffcalc (copy)/.settings/org.eclipse.core.runtime.prefs new file mode 100644 index 0000000..5a0ad22 --- /dev/null +++ b/script/test/diffcalc (copy)/.settings/org.eclipse.core.runtime.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +line.separator=\n diff --git a/script/test/diffcalc (copy)/.travis.yml b/script/test/diffcalc (copy)/.travis.yml new file mode 100644 index 0000000..2ee17ea --- /dev/null +++ b/script/test/diffcalc (copy)/.travis.yml @@ -0,0 +1,21 @@ +# based on https://www.topbug.net/blog/2012/05/27/use-travis-ci-with-jython/ + +language: python + +python: + - "2.7" + +env: + - JYTHON=false + - JYTHON=true + +install: + - if [ "$JYTHON" == "true" ]; then . install-jython-environment.sh; fi + - if [ "$JYTHON" == "false" ]; then pip install --upgrade pytest; pip install pytest-xdist; fi + +before_script: + - if [ "$JYTHON" == "true" ]; then export PYTEST=$HOME/jython/bin/pytest; else export PYTEST=pytest; fi + - echo PYTEST:- $PYTEST + +script: $PYTEST + diff --git a/script/test/diffcalc (copy)/COPYING b/script/test/diffcalc (copy)/COPYING new file mode 100644 index 0000000..20d40b6 --- /dev/null +++ b/script/test/diffcalc (copy)/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/script/test/diffcalc (copy)/Makefile b/script/test/diffcalc (copy)/Makefile new file mode 100644 index 0000000..221a775 --- /dev/null +++ b/script/test/diffcalc (copy)/Makefile @@ -0,0 +1,52 @@ + +test-all: test-python test-jython test-integration test-launcher + +test-python: + py.test + +test-jython: + export CLASSPATH=$(HOME)/lib/Jama-1.0.3.jar:$(CLASSPATH); echo $$CLASSPATH; $(HOME)/jython/bin/py.test + +test-integration: + py.test --boxed integration_checks.py + +test-launcher: + ./diffcalc.py --help + ./diffcalc.py --modules + ./diffcalc.py --non-interactive --python sixcircle + +install-jython: + ./install-jython-environment.sh + +doc-source: + ./diffcalc.py --non-interactive --make-manuals-source + +doc-html: + cd doc; make html + +doc-pdf: + cd doc; make pdf + +doc-all: + cd doc; make all + +doc-clean: + cd doc; make clean + +help: + @echo + @echo "Please use \`make ' where is one of" + @echo + @echo " test-all" + @echo " test-python" + @echo " test-jython" + @echo " test-integration" + @echo " test-launcher" + @echo " install-jython" + @echo + @echo " doc-source : to expand *_template.rst to *.rst" + @echo " doc-html" + @echo " doc-pdf" + @echo " doc-all" + @echo " doc-clean" + @echo diff --git a/script/test/diffcalc (copy)/README.rst b/script/test/diffcalc (copy)/README.rst new file mode 100644 index 0000000..95f0ad3 --- /dev/null +++ b/script/test/diffcalc (copy)/README.rst @@ -0,0 +1,513 @@ +Diffcalc - A Diffraction Condition Calculator for Diffractometer Control +======================================================================== + +Diffcalc is a python/jython based diffraction condition calculator used for +controlling diffractometers within reciprocal lattice space. It performs the +same task as the fourc, sixc, twoc, kappa, psic and surf macros from SPEC. + +There is a `user guide `_ and `developer guide `_, both at `diffcalc.readthedocs.io `_ + +|Travis| |Read the docs| + +.. |Travis| image:: https://travis-ci.org/DiamondLightSource/diffcalc.svg?branch=master + :target: https://travis-ci.org/DiamondLightSource/diffcalc + :alt: Build Status + +.. |Read the docs| image:: https://readthedocs.org/projects/diffcalc/badge/?version=latest + :target: http://diffcalc.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. contents:: + +.. section-numbering:: + +Software compatibility +---------------------- + +- Written in Python using numpy +- Works in Jython using Jama +- Runs directly in `OpenGDA` +- Runs in in Python or IPython using minimal OpenGda emulation (included) +- Contact us for help running in your environment + +Diffractometer compatibility +---------------------------- + +Diffcalc’s standard calculation engine is an implementation of [You1999]_ and +[Busing1967]_. Diffcalc works with any diffractometer which is a subset of: + + .. image:: https://raw.githubusercontent.com/DiamondLightSource/diffcalc/master/doc/source/youmanual_images/4s_2d_diffractometer.png + :alt: 4s + 2d six-circle diffractometer, from H.You (1999) + :width: 50% + :align: center + +Diffcalc can be configured to work with any diffractometer geometry which is a +subset of this. For example, a five-circle diffractometer might be missing the +nu circle above. + +Note that the first versions of Diffcalc were based on [Vlieg1993]_ and +[Vlieg1998]_ and a ‘Vlieg’ engine is still available. There is also an engine +based on [Willmott2011]_. The ‘You’ engine is more generic and the plan is to +remove the old ‘Vlieg’ engine once beamlines have been migrated. + +Installation +------------ + +Check it out:: + + $ git clone https://github.com/DiamondLightSource/diffcalc.git + Cloning into 'diffcalc'... + +At Diamond Diffcalc may be installed within an OpenGDA deployment and is +available via the 'module' system from bash. + +Starting +-------- + +Start diffcalc in ipython using a sixcircle dummy diffractometer:: + + $ cd diffcalc + $ ./diffcalc.py --help + ... + + $ ./diffcalc.py sixcircle + + Running: "ipython --no-banner --HistoryManager.hist_file=/tmp/ipython_hist_zrb13439.sqlite -i -m diffcmd.start sixcircle False" + + ---------------------------------- DIFFCALC ----------------------------------- + Startup script: '/Users/zrb13439/git/diffcalc/startup/sixcircle.py' + Loading ub calculation: 'test' + ------------------------------------ Help ------------------------------------- + Quick: https://github.com/DiamondLightSource/diffcalc/blob/master/README.rst + Manual: https://diffcalc.readthedocs.io + Type: > help ub + > help hkl + ------------------------------------------------------------------------------- + In [1]: + +Within Diamond use:: + + $ module load diffcalc + $ diffcalc --help + ... + $ diffcalc sixcircle + +Trying it out +------------- + +Type ``demo.all()`` to see it working and then move try the following quick +start guide:: + + >>> demo.all() + ... + +Getting help +------------ + +To view help with orientation and then moving in hkl space:: + + >>> help ub + ... + >>> help hkl + ... + +Configuring a UB calculation +---------------------------- +See the full `user manual for many more +options and an explanation of what this all means. + +To load the last used UB-calculation:: + + >>> lastub + Loading ub calculation: 'mono-Si' + +To load a previous UB-calculation:: + + >>> listub + UB calculations in: /Users/walton/.diffcalc/i16 + + 0) mono-Si 15 Feb 2017 (22:32) + 1) i16-32 13 Feb 2017 (18:32) + + >>> loadub 0 + +To create a new UB-calculation:: + + >>> newub 'example' + >>> setlat '1Acube' 1 1 1 90 90 90 + +Find U matrix from two reflections:: + + >>> pos wl 1 + wl: 1.0000 + >>> c2th [0 0 1] + 59.99999999999999 + + >>> pos sixc [0 60 0 30 90 0] + sixc: mu: 0.0000 delta: 60.0000 gam: 0.0000 eta: 30.0000 chi: 90.0000 phi: 0.0000 + >>> addref [0 0 1] + + >>> pos sixc [0 90 0 45 45 90] + sixc: mu: 0.0000 delta: 90.0000 gam: 0.0000 eta: 45.0000 chi: 45.0000 phi: 90.0000 + >>> addref [0 1 1] + Calculating UB matrix. + + +Check that it looks good:: + + >>> checkub + + ENERGY H K L H_COMP K_COMP L_COMP TAG + 1 12.3984 0.00 0.00 1.00 0.0000 0.0000 1.0000 + 2 12.3984 0.00 1.00 1.00 0.0000 1.0000 1.0000 + +To see the resulting UB-calculation:: + + >>> ub + UBCALC + + name: example + + n_phi: 0.00000 0.00000 1.00000 <- set + n_hkl: -0.00000 0.00000 1.00000 + miscut: None + + CRYSTAL + + name: 1Acube + + a, b, c: 1.00000 1.00000 1.00000 + 90.00000 90.00000 90.00000 + + B matrix: 6.28319 0.00000 0.00000 + 0.00000 6.28319 0.00000 + 0.00000 0.00000 6.28319 + + UB MATRIX + + U matrix: 1.00000 0.00000 0.00000 + 0.00000 1.00000 0.00000 + 0.00000 0.00000 1.00000 + + U angle: 0 + + UB matrix: 6.28319 0.00000 0.00000 + 0.00000 6.28319 0.00000 + 0.00000 0.00000 6.28319 + + REFLECTIONS + + ENERGY H K L MU DELTA GAM ETA CHI PHI TAG + 1 12.398 0.00 0.00 1.00 0.0000 60.0000 0.0000 30.0000 90.0000 0.0000 + 2 12.398 0.00 1.00 1.00 0.0000 90.0000 0.0000 45.0000 45.0000 90.0000 + +Setting the reference vector +---------------------------- +See the full `user manual for many more +options and an explanation of what this all means. + +By default the reference vector is set parallel to the phi axis. That is, +along the z-axis of the phi coordinate frame. + +The `ub` command shows the current reference vector, along with any inferred +miscut, at the top its report (or it can be shown by calling ``setnphi`` or +``setnhkl'`` with no args):: + + >>> ub + ... + n_phi: 0.00000 0.00000 1.00000 <- set + n_hkl: -0.00000 0.00000 1.00000 + miscut: None + ... + +Constraining solutions for moving in hkl space +---------------------------------------------- +See the full `user manual for many more +options and an explanation of what this all means. + +To get help and see current constraints:: + + >>> help con + ... + + >>> con + DET REF SAMP + ------ ------ ------ + delta --> a_eq_b --> mu + --> gam alpha eta + qaz beta chi + naz psi phi + mu_is_gam + + gam : 0.0000 + a_eq_b + mu : 0.0000 + + Type 'help con' for instructions + +Three constraints can be given: zero or one from the DET and REF columns and the +remainder from the SAMP column. Not all combinations are currently available. +Use ``help con`` to see a summary if you run into troubles. + +To configure four-circle vertical scattering:: + + >>> con gam 0 mu 0 a_eq_b + gam : 0.0000 + a_eq_b + mu : 0.0000 + +Moving in hkl space +------------------- + +Simulate moving to a reflection:: + + >>> sim hkl [0 1 1] + sixc would move to: + mu : 0.0000 + delta : 90.0000 + gam : 0.0000 + eta : 45.0000 + chi : 45.0000 + phi : 90.0000 + + alpha : 30.0000 + beta : 30.0000 + naz : 35.2644 + psi : 90.0000 + qaz : 90.0000 + tau : 45.0000 + theta : 45.0000 + +Move to reflection:: + + >>> pos hkl [0 1 1] + hkl: h: 0.00000 k: 1.00000 l: 1.00000 + + >>> pos sixc + sixc: mu: 0.0000 delta: 90.0000 gam: 0.0000 eta: 45.0000 chi: 45.0000 phi: 90.0000 + + +Scanning in hkl space +--------------------- + +Scan an hkl axis (and read back settings):: + + >>> scan l 0 1 .2 sixc + l mu delta gam eta chi phi + ------- ------- -------- ------- -------- ------- -------- + 0.00000 0.0000 60.0000 0.0000 30.0000 0.0000 90.0000 + 0.20000 0.0000 61.3146 0.0000 30.6573 11.3099 90.0000 + 0.40000 0.0000 65.1654 0.0000 32.5827 21.8014 90.0000 + 0.60000 0.0000 71.3371 0.0000 35.6685 30.9638 90.0000 + 0.80000 0.0000 79.6302 0.0000 39.8151 38.6598 90.0000 + 1.00000 0.0000 90.0000 0.0000 45.0000 45.0000 90.0000 + +Scan a constraint (and read back virtual angles and eta):: + + >>> con psi + gam : 0.0000 + ! psi : --- + mu : 0.0000 + >>> scan psi 70 110 10 hklverbose [0 1 1] eta + psi eta h k l theta qaz alpha naz tau psi beta + -------- -------- ------- ------- ------- -------- -------- -------- -------- -------- -------- -------- + 70.00000 26.1183 0.00000 1.00000 1.00000 45.00000 90.00000 19.20748 45.28089 45.00000 70.00000 42.14507 + 80.00000 35.1489 -0.00000 1.00000 1.00000 45.00000 90.00000 24.40450 40.12074 45.00000 80.00000 35.93196 + 90.00000 45.0000 0.00000 1.00000 1.00000 45.00000 90.00000 30.00000 35.26439 45.00000 90.00000 30.00000 + 100.00000 54.8511 -0.00000 1.00000 1.00000 45.00000 90.00000 35.93196 30.68206 45.00000 100.00000 24.40450 + 110.00000 63.8817 -0.00000 1.00000 1.00000 45.00000 90.00000 42.14507 26.34100 45.00000 110.00000 19.20748 + + +Orientation Commands +-------------------- + ++-----------------------------+---------------------------------------------------+ +| **STATE** | ++-----------------------------+---------------------------------------------------+ +| **-- newub** {'name'} | start a new ub calculation name | ++-----------------------------+---------------------------------------------------+ +| **-- loadub** 'name' | num | load an existing ub calculation | ++-----------------------------+---------------------------------------------------+ +| **-- lastub** | load the last used ub calculation | ++-----------------------------+---------------------------------------------------+ +| **-- listub** | list the ub calculations available to load | ++-----------------------------+---------------------------------------------------+ +| **-- rmub** 'name'|num | remove existing ub calculation | ++-----------------------------+---------------------------------------------------+ +| **-- saveubas** 'name' | save the ub calculation with a new name | ++-----------------------------+---------------------------------------------------+ +| **LATTICE** | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** | interactively enter lattice parameters (Angstroms | +| | and Deg) | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a | assumes cubic | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b | assumes tetragonal | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b c | assumes ortho | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b c | assumes mon/hex with gam not equal to 90 | +| gamma | | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b c | arbitrary | +| alpha beta gamma | | ++-----------------------------+---------------------------------------------------+ +| **-- c2th** [h k l] | calculate two-theta angle for reflection | ++-----------------------------+---------------------------------------------------+ +| **-- hklangle** [h1 k1 l1] | calculate angle between [h1 k1 l1] and [h2 k2 l2] | +| [h2 k2 l2] | crystal planes | ++-----------------------------+---------------------------------------------------+ +| **REFERENCE (SURFACE)** | ++-----------------------------+---------------------------------------------------+ +| **-- setnphi** {[x y z]} | sets or displays n_phi reference | ++-----------------------------+---------------------------------------------------+ +| **-- setnhkl** {[h k l]} | sets or displays n_hkl reference | ++-----------------------------+---------------------------------------------------+ +| **REFLECTIONS** | ++-----------------------------+---------------------------------------------------+ +| **-- showref** | shows full reflection list | ++-----------------------------+---------------------------------------------------+ +| **-- addref** | add reflection interactively | ++-----------------------------+---------------------------------------------------+ +| **-- addref** [h k l] | add reflection with current position and energy | +| {'tag'} | | ++-----------------------------+---------------------------------------------------+ +| **-- addref** [h k l] (p1, | add arbitrary reflection | +| .., pN) energy {'tag'} | | ++-----------------------------+---------------------------------------------------+ +| **-- editref** num | interactively edit a reflection | ++-----------------------------+---------------------------------------------------+ +| **-- delref** num | deletes a reflection (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **-- clearref** | deletes all the reflections | ++-----------------------------+---------------------------------------------------+ +| **-- swapref** | swaps first two reflections used for calculating | +| | U matrix | ++-----------------------------+---------------------------------------------------+ +| **-- swapref** num1 num2 | swaps two reflections (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **CRYSTAL ORIENTATIONS** | ++-----------------------------+---------------------------------------------------+ +| **-- showorient** | shows full list of crystal orientations | ++-----------------------------+---------------------------------------------------+ +| **-- addorient** | add crystal orientation interactively | ++-----------------------------+---------------------------------------------------+ +| **-- addorient** [h k l] | add crystal orientation in laboratory frame | +| [x y z] {'tag'} | | ++-----------------------------+---------------------------------------------------+ +| **-- editorient** num | interactively edit a crystal orientation | ++-----------------------------+---------------------------------------------------+ +| **-- delorient** num | deletes a crystal orientation (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **-- clearorient** | deletes all the crystal orientations | ++-----------------------------+---------------------------------------------------+ +| **-- swaporient** | swaps first two crystal orientations used for | +| | calculating U matrix | ++-----------------------------+---------------------------------------------------+ +| **-- swaporient** num1 num2 | swaps two crystal orientations (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **UB MATRIX** | ++-----------------------------+---------------------------------------------------+ +| **-- checkub** | show calculated and entered hkl values for | +| | reflections | ++-----------------------------+---------------------------------------------------+ +| **-- setu** | manually set u matrix | +| {[[..][..][..]]} | | ++-----------------------------+---------------------------------------------------+ +| **-- setub** | manually set ub matrix | +| {[[..][..][..]]} | | ++-----------------------------+---------------------------------------------------+ +| **-- calcub** | (re)calculate u matrix from ref1 and ref2 | ++-----------------------------+---------------------------------------------------+ +| **-- trialub** | (re)calculate u matrix from ref1 only (check | +| | carefully) | ++-----------------------------+---------------------------------------------------+ +| **-- refineub** {[h k l]} | refine unit cell dimensions and U matrix to match | +| {pos} | diffractometer angles for a given hkl value | ++-----------------------------+---------------------------------------------------+ +| **-- addmiscut** angle | apply miscut to U matrix using a specified miscut | +| {[x y z]} | angle in degrees and a rotation axis | +| | (default: [0 1 0]) | ++-----------------------------+---------------------------------------------------+ +| **-- setmiscut** angle | manually set U matrix using a specified miscut | +| {[x y z]} | angle in degrees and a rotation axis | +| | (default: [0 1 0]) | ++-----------------------------+---------------------------------------------------+ + +Motion Commands +--------------- + ++-----------------------------+---------------------------------------------------+ +| **CONSTRAINTS** | ++-----------------------------+---------------------------------------------------+ +| **-- con** | list available constraints and values | ++-----------------------------+---------------------------------------------------+ +| **-- con** {val} | constrains and optionally sets one constraint | ++-----------------------------+---------------------------------------------------+ +| **-- con** {val} | clears and then fully constrains | +| {val} {val} | | ++-----------------------------+---------------------------------------------------+ +| **-- uncon** | remove constraint | ++-----------------------------+---------------------------------------------------+ +| **HKL** | ++-----------------------------+---------------------------------------------------+ +| **-- allhkl** [h k l] | print all hkl solutions ignoring limits | ++-----------------------------+---------------------------------------------------+ +| **HARDWARE** | ++-----------------------------+---------------------------------------------------+ +| **-- hardware** | show diffcalc limits and cuts | ++-----------------------------+---------------------------------------------------+ +| **-- setcut** {name {val}} | sets cut angle | ++-----------------------------+---------------------------------------------------+ +| **-- setmin** {axis {val}} | set lower limits used by auto sector code (None | +| | to clear) | ++-----------------------------+---------------------------------------------------+ +| **-- setmax** {name {val}} | sets upper limits used by auto sector code (None | +| | to clear) | ++-----------------------------+---------------------------------------------------+ +| **MOTION** | ++-----------------------------+---------------------------------------------------+ +| **-- sim** hkl scn | simulates moving scannable (not all) | ++-----------------------------+---------------------------------------------------+ +| **-- sixc** | show Eularian position | ++-----------------------------+---------------------------------------------------+ +| **-- pos** sixc [mu, delta, | move to Eularian position(None holds an axis | +| gam, eta, chi, phi] | still) | ++-----------------------------+---------------------------------------------------+ +| **-- sim** sixc [mu, delta, | simulate move to Eulerian positionsixc | +| gam, eta, chi, phi] | | ++-----------------------------+---------------------------------------------------+ +| **-- hkl** | show hkl position | ++-----------------------------+---------------------------------------------------+ +| **-- pos** hkl [h k l] | move to hkl position | ++-----------------------------+---------------------------------------------------+ +| **-- pos** {h | k | l} val | move h, k or l to val | ++-----------------------------+---------------------------------------------------+ +| **-- sim** hkl [h k l] | simulate move to hkl position | ++-----------------------------+---------------------------------------------------+ + + +References +---------- + +.. [You1999] H. You. *Angle calculations for a '4S+2D' six-circle diffractometer.* + J. Appl. Cryst. (1999). **32**, 614-623. `(pdf link) + `__. + +.. [Busing1967] W. R. Busing and H. A. Levy. *Angle calculations for 3- and 4-circle X-ray + and neutron diffractometers.* Acta Cryst. (1967). **22**, 457-464. `(pdf link) + `__. + +.. [Vlieg1993] Martin Lohmeier and Elias Vlieg. *Angle calculations for a six-circle + surface x-ray diffractometer.* J. Appl. Cryst. (1993). **26**, 706-716. `(pdf link) + `__. + +.. [Vlieg1998] Elias Vlieg. *A (2+3)-type surface diffractometer: mergence of the z-axis and + (2+2)-type geometries.* J. Appl. Cryst. (1998). **31**, 198-203. `(pdf link) + `__. + +.. [Willmott2011] C. M. Schlepütz, S. O. Mariager, S. A. Pauli, R. Feidenhans'l and + P. R. Willmott. *Angle calculations for a (2+3)-type diffractometer: focus + on area detectors.* J. Appl. Cryst. (2011). **44**, 73-83. `(pdf link) + `__. diff --git a/script/test/diffcalc (copy)/README_template.rst b/script/test/diffcalc (copy)/README_template.rst new file mode 100644 index 0000000..f62cc5f --- /dev/null +++ b/script/test/diffcalc (copy)/README_template.rst @@ -0,0 +1,267 @@ +Diffcalc - A Diffraction Condition Calculator for Diffractometer Control +======================================================================== + +Diffcalc is a python/jython based diffraction condition calculator used for +controlling diffractometers within reciprocal lattice space. It performs the +same task as the fourc, sixc, twoc, kappa, psic and surf macros from SPEC. + +There is a `user guide `_ and `developer guide `_, both at `diffcalc.readthedocs.io `_ + +|Travis| |Read the docs| + +.. |Travis| image:: https://travis-ci.org/DiamondLightSource/diffcalc.svg?branch=master + :target: https://travis-ci.org/DiamondLightSource/diffcalc + :alt: Build Status + +.. |Read the docs| image:: https://readthedocs.org/projects/diffcalc/badge/?version=latest + :target: http://diffcalc.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. contents:: + +.. section-numbering:: + +Software compatibility +---------------------- + +- Written in Python using numpy +- Works in Jython using Jama +- Runs directly in `OpenGDA` +- Runs in in Python or IPython using minimal OpenGda emulation (included) +- Contact us for help running in your environment + +Diffractometer compatibility +---------------------------- + +Diffcalc’s standard calculation engine is an implementation of [You1999]_ and +[Busing1967]_. Diffcalc works with any diffractometer which is a subset of: + + .. image:: https://raw.githubusercontent.com/DiamondLightSource/diffcalc/master/doc/source/youmanual_images/4s_2d_diffractometer.png + :alt: 4s + 2d six-circle diffractometer, from H.You (1999) + :width: 50% + :align: center + +Diffcalc can be configured to work with any diffractometer geometry which is a +subset of this. For example, a five-circle diffractometer might be missing the +nu circle above. + +Note that the first versions of Diffcalc were based on [Vlieg1993]_ and +[Vlieg1998]_ and a ‘Vlieg’ engine is still available. There is also an engine +based on [Willmott2011]_. The ‘You’ engine is more generic and the plan is to +remove the old ‘Vlieg’ engine once beamlines have been migrated. + +If we choose the x axis parallel to b, the yaxis intheplaneofblandb2,andthezaxis perpendicular to that plane, + +Installation +------------ + +Check it out:: + + $ git clone https://github.com/DiamondLightSource/diffcalc.git + Cloning into 'diffcalc'... + +At Diamond Diffcalc may be installed within an OpenGDA deployment and is +available via the 'module' system from bash. + +Starting +-------- + +Start diffcalc in ipython using a sixcircle dummy diffractometer:: + + $ cd diffcalc + $ ./diffcalc.py --help + ... + + $ ./diffcalc.py sixcircle + + Running: "ipython --no-banner --HistoryManager.hist_file=/tmp/ipython_hist_zrb13439.sqlite -i -m diffcmd.start sixcircle False" + + ---------------------------------- DIFFCALC ----------------------------------- + Startup script: '/Users/zrb13439/git/diffcalc/startup/sixcircle.py' + Loading ub calculation: 'test' + ------------------------------------ Help ------------------------------------- + Quick: https://github.com/DiamondLightSource/diffcalc/blob/master/README.rst + Manual: https://diffcalc.readthedocs.io + Type: > help ub + > help hkl + ------------------------------------------------------------------------------- + In [1]: + +Within Diamond use:: + + $ module load diffcalc + $ diffcalc --help + ... + $ diffcalc sixcircle + +Trying it out +------------- + +Type ``demo.all()`` to see it working and then move try the following quick +start guide:: + + >>> demo.all() + ... + +Getting help +------------ + +To view help with orientation and then moving in hkl space:: + + >>> help ub + ... + >>> help hkl + ... + +Configuring a UB calculation +---------------------------- +See the full `user manual for many more +options and an explanation of what this all means. + +To load the last used UB-calculation:: + + >>> lastub + Loading ub calculation: 'mono-Si' + +To load a previous UB-calculation:: + + >>> listub + UB calculations in: /Users/walton/.diffcalc/i16 + + 0) mono-Si 15 Feb 2017 (22:32) + 1) i16-32 13 Feb 2017 (18:32) + + >>> loadub 0 + +To create a new UB-calculation:: + + ==> newub 'example' + ==> setlat '1Acube' 1 1 1 90 90 90 + +where the basis is defined by Busing & Levy: + + "...we choose the x axis parallel to b, the y axis in the plane of bl + and b2, and the zaxis perpendicular to that plane." + + +Find U matrix from two reflections:: + + ==> pos wl 1 + ==> c2th [0 0 1] + 59.99999999999999 + + ==> pos sixc [0 60 0 30 90 0] + ==> addref [0 0 1] + + ==> pos sixc [0 90 0 45 45 90] + ==> addref [0 1 1] + + +Check that it looks good:: + + ==> checkub + +To see the resulting UB-calculation:: + + ==> ub + +Setting the reference vector +---------------------------- +See the full `user manual for many more +options and an explanation of what this all means. + +By default the reference vector is set parallel to the phi axis. That is, +along the z-axis of the phi coordinate frame. + +The `ub` command shows the current reference vector, along with any inferred +miscut, at the top its report (or it can be shown by calling ``setnphi`` or +``setnhkl'`` with no args):: + + >>> ub + ... + n_phi: 0.00000 0.00000 1.00000 <- set + n_hkl: -0.00000 0.00000 1.00000 + miscut: None + ... + +Constraining solutions for moving in hkl space +---------------------------------------------- +See the full `user manual for many more +options and an explanation of what this all means. + +To get help and see current constraints:: + + >>> help con + ... + + ==> con + +Three constraints can be given: zero or one from the DET and REF columns and the +remainder from the SAMP column. Not all combinations are currently available. +Use ``help con`` to see a summary if you run into troubles. + +To configure four-circle vertical scattering:: + + ==> con gam 0 mu 0 a_eq_b + +Moving in hkl space +------------------- + +Simulate moving to a reflection:: + + ==> sim hkl [0 1 1] + +Move to reflection:: + + ==> pos hkl [0 1 1] + + ==> pos sixc + + +Scanning in hkl space +--------------------- + +Scan an hkl axis (and read back settings):: + + ==> scan l 0 1 .2 sixc + +Scan a constraint (and read back virtual angles and eta):: + + ==> con psi + ==> scan psi 70 110 10 hklverbose [0 1 1] eta + + +Orientation Commands +-------------------- + +==> UB_HELP_TABLE + +Motion Commands +--------------- + +==> HKL_HELP_TABLE + + +References +---------- + +.. [You1999] H. You. *Angle calculations for a '4S+2D' six-circle diffractometer.* + J. Appl. Cryst. (1999). **32**, 614-623. `(pdf link) + `__. + +.. [Busing1967] W. R. Busing and H. A. Levy. *Angle calculations for 3- and 4-circle X-ray + and neutron diffractometers.* Acta Cryst. (1967). **22**, 457-464. `(pdf link) + `__. + +.. [Vlieg1993] Martin Lohmeier and Elias Vlieg. *Angle calculations for a six-circle + surface x-ray diffractometer.* J. Appl. Cryst. (1993). **26**, 706-716. `(pdf link) + `__. + +.. [Vlieg1998] Elias Vlieg. *A (2+3)-type surface diffractometer: mergence of the z-axis and + (2+2)-type geometries.* J. Appl. Cryst. (1998). **31**, 198-203. `(pdf link) + `__. + +.. [Willmott2011] C. M. Schlepütz, S. O. Mariager, S. A. Pauli, R. Feidenhans'l and + P. R. Willmott. *Angle calculations for a (2+3)-type diffractometer: focus + on area detectors.* J. Appl. Cryst. (2011). **44**, 73-83. `(pdf link) + `__. diff --git a/script/test/diffcalc (copy)/buckminster.cspec b/script/test/diffcalc (copy)/buckminster.cspec new file mode 100644 index 0000000..745b4c3 --- /dev/null +++ b/script/test/diffcalc (copy)/buckminster.cspec @@ -0,0 +1,5 @@ + + + + + diff --git a/script/test/diffcalc (copy)/diffcalc.py b/script/test/diffcalc (copy)/diffcalc.py new file mode 100755 index 0000000..66be493 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc.py @@ -0,0 +1,9 @@ +#!/usr/bin/python + +import sys + +from diffcmd.diffcalc_launcher import main + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/__init__.py b/script/test/diffcalc (copy)/diffcalc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/dc/__init__.py b/script/test/diffcalc (copy)/diffcalc/dc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/dc/common.py b/script/test/diffcalc (copy)/diffcalc/dc/common.py new file mode 100644 index 0000000..9793f71 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/dc/common.py @@ -0,0 +1,24 @@ +from diffcalc.util import allnum, command, DiffcalcException + + +def sim(scn, hkl): + """sim hkl scn -- simulates moving scannable (not all) + """ + if not isinstance(hkl, (tuple, list)): + raise TypeError() + + if not allnum(hkl): + raise TypeError() + + try: + print scn.simulateMoveTo(hkl) + except AttributeError: + raise TypeError( + "The first argument does not support simulated moves") + +def energy_to_wavelength(energy): + try: + return 12.39842 / energy + except ZeroDivisionError: + raise DiffcalcException( + "Cannot calculate hkl position as Energy is set to 0") \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/dc/dcvlieg.py b/script/test/diffcalc (copy)/diffcalc/dc/dcvlieg.py new file mode 100644 index 0000000..b1fb5da --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/dc/dcvlieg.py @@ -0,0 +1,76 @@ +from diffcalc.dc.common import energy_to_wavelength + +from diffcalc import settings +from diffcalc.hkl.vlieg.transform import VliegTransformSelector,\ + TransformCommands, VliegPositionTransformer +from diffcalc.dc.help import compile_extra_motion_commands_for_help +import diffcalc.hkl.vlieg.calc + + +# reload to aid testing only +import diffcalc.ub.ub as _ub +reload(_ub) +from diffcalc import hardware as _hardware +#reload(_hardware) +import diffcalc.hkl.vlieg.hkl as _hkl +reload(_hkl) + +from diffcalc.ub.ub import * # @UnusedWildImport +from diffcalc.hardware import * # @UnusedWildImport +from diffcalc.hkl.vlieg.hkl import * # @UnusedWildImport +from diffcalc.gdasupport.scannable.sim import sim + +_transform_selector = VliegTransformSelector() +_transform_commands = TransformCommands(_transform_selector) +_transformer = VliegPositionTransformer(settings.geometry, settings.hardware, + _transform_selector) + +transform = _transform_commands.transform +transforma = _transform_commands.transforma +transformb = _transform_commands.transformb +transformc = _transform_commands.transformc + + +on = 'on' +off = 'off' +auto = 'auto' +manual = 'manual' + +def hkl_to_angles(h, k, l, energy=None): + """Convert a given hkl vector to a set of diffractometer angles""" + if energy is None: + energy = settings.hardware.get_energy() # @UndefinedVariable + + position, params = hklcalc.hklToAngles(h, k, l, energy_to_wavelength(energy)) + position = _transformer.transform(position) + angle_tuple = settings.geometry.internal_position_to_physical_angles(position) # @UndefinedVariable + angle_tuple = settings.hardware.cut_angles(angle_tuple) # @UndefinedVariable + + return angle_tuple, params + + +def angles_to_hkl(angleTuple, energy=None): + """Converts a set of diffractometer angles to an hkl position + ((h, k, l), paramDict)=angles_to_hkl(self, (a1, a2,aN), energy=None)""" + if energy is None: + energy = settings.hardware.get_energy() # @UndefinedVariable + + i_pos = settings.geometry.physical_angles_to_internal_position(angleTuple) # @UndefinedVariable + return hklcalc.anglesToHkl(i_pos, energy_to_wavelength(energy)) + + +settings.ubcalc_strategy = diffcalc.hkl.vlieg.calc.VliegUbCalcStrategy() +settings.angles_to_hkl_function = diffcalc.hkl.vlieg.calc.vliegAnglesToHkl +settings.include_sigtau = True + +ub_commands_for_help = _ub.commands_for_help + +hkl_commands_for_help = (_hkl.commands_for_help + + _hardware.commands_for_help + + ['Transform', + transform, + transforma, + transformb, + transformc] + + compile_extra_motion_commands_for_help()) + diff --git a/script/test/diffcalc (copy)/diffcalc/dc/dcwillmot.py b/script/test/diffcalc (copy)/diffcalc/dc/dcwillmot.py new file mode 100644 index 0000000..7bd8a87 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/dc/dcwillmot.py @@ -0,0 +1,47 @@ +# This file differs from dcyou in only two places + +from diffcalc import settings +from diffcalc.dc.common import energy_to_wavelength +from diffcalc.dc.help import compile_extra_motion_commands_for_help +import diffcalc.hkl.willmott.calc + + +# reload to aid testing only +from diffcalc.ub import ub as _ub +reload(_ub) +from diffcalc import hardware as _hardware +#reload(_hardware) +from diffcalc.hkl.you import hkl as _hkl +reload(_hkl) + +from diffcalc.ub.ub import * # @UnusedWildImport +from diffcalc.hardware import * # @UnusedWildImport +from diffcalc.hkl.willmot.hkl import * # @UnusedWildImport +from diffcalc.gdasupport.scannable.sim import sim + +def hkl_to_angles(h, k, l, energy=None): + """Convert a given hkl vector to a set of diffractometer angles""" + if energy is None: + energy = settings.hardware.get_energy() # @UndefinedVariable + + (pos, params) = hklcalc.hklToAngles(h, k, l, energy_to_wavelength(energy)) + angle_tuple = settings.geometry.internal_position_to_physical_angles(pos) # @UndefinedVariable + angle_tuple = settings.hardware.cut_angles(angle_tuple) # @UndefinedVariable + + return angle_tuple, params + +def angles_to_hkl(angleTuple, energy=None): + """Converts a set of diffractometer angles to an hkl position + ((h, k, l), paramDict)=angles_to_hkl(self, (a1, a2,aN), energy=None)""" + if energy is None: + energy = settings.hardware.get_energy() # @UndefinedVariable + i_pos = settings.geometry.physical_angles_to_internal_position(angleTuple) # @UndefinedVariable + return hklcalc.anglesToHkl(i_pos, energy_to_wavelength(energy)) + +settings.ubcalc_strategy = diffcalc.hkl.willmott.calc.WillmottHorizontalUbCalcStrategy() +settings.angles_to_hkl_function = diffcalc.hkl.willmott.calc.angles_to_hkl + + +ub_commands_for_help = _ub.commands_for_help + +hkl_commands_for_help = _hkl.commands_for_help + _hardware.commands_for_help + compile_extra_motion_commands_for_help() diff --git a/script/test/diffcalc (copy)/diffcalc/dc/dcyou.py b/script/test/diffcalc (copy)/diffcalc/dc/dcyou.py new file mode 100644 index 0000000..651c2fe --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/dc/dcyou.py @@ -0,0 +1,56 @@ +from diffcalc import settings +from diffcalc.dc.common import energy_to_wavelength +from diffcalc.dc.help import compile_extra_motion_commands_for_help + +import diffcalc.hkl.you.calc +settings.ubcalc_strategy = diffcalc.hkl.you.calc.YouUbCalcStrategy() +settings.angles_to_hkl_function = diffcalc.hkl.you.calc.youAnglesToHkl +settings.include_reference = True + +# reload to aid testing only +from diffcalc.ub import ub as _ub + +reload(_ub) +from diffcalc import hardware as _hardware +#reload(_hardware) +from diffcalc.hkl.you import hkl as _hkl +reload(_hkl) + +from diffcalc.ub.ub import * # @UnusedWildImport +from diffcalc.hardware import * # @UnusedWildImport +from diffcalc.hkl.you.hkl import * # @UnusedWildImport + + +def hkl_to_angles(h, k, l, energy=None): + """Convert a given hkl vector to a set of diffractometer angles + + return angle tuple and params dictionary + + """ + if energy is None: + energy = settings.hardware.get_energy() # @UndefinedVariable + + (pos, params) = hklcalc.hklToAngles(h, k, l, energy_to_wavelength(energy)) + angle_tuple = settings.geometry.internal_position_to_physical_angles(pos) # @UndefinedVariable + angle_tuple = settings.hardware.cut_angles(angle_tuple) # @UndefinedVariable + + return angle_tuple, params + + +def angles_to_hkl(angleTuple, energy=None): + """Converts a set of diffractometer angles to an hkl position + + Return hkl tuple and params dictionary + + """ + if energy is None: + energy = settings.hardware.get_energy() # @UndefinedVariable + i_pos = settings.geometry.physical_angles_to_internal_position(angleTuple) # @UndefinedVariable + return hklcalc.anglesToHkl(i_pos, energy_to_wavelength(energy)) + + + + + +ub_commands_for_help = _ub.commands_for_help +hkl_commands_for_help = _hkl.commands_for_help + _hardware.commands_for_help + compile_extra_motion_commands_for_help() diff --git a/script/test/diffcalc (copy)/diffcalc/dc/help.py b/script/test/diffcalc (copy)/diffcalc/dc/help.py new file mode 100644 index 0000000..5d4afe6 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/dc/help.py @@ -0,0 +1,161 @@ +''' +Created on 6 May 2016 + +@author: walton +''' +from diffcalc import settings +from diffcalc.gdasupport.scannable.sim import sim +import textwrap +from diffcalc.util import bold + + +class ExternalCommand(object): + """Instances found in a command_list by format_command_help will + result in documentation for a command without there actually being one. + """ + def __init__(self, docstring): + """Set the docstring that will be pulled off by format_command_help. + """ + self.__doc__ = docstring + self.__name__ = '' + + +WIDTH = 27 +INDENT = 3 + + +def format_command_help(command_list): + + row_list = _command_list_to_table_cells(command_list) + lines = [] + for row_cells in row_list: + if len(row_cells) == 1: + heading = row_cells[0] + lines.append('') + lines.append(bold(heading)) + lines.append('') + elif len(row_cells) == 2: + cell1, cell2 = row_cells + + cell1_lines = textwrap.wrap(cell1, WIDTH, subsequent_indent=' ') + cell2_lines = textwrap.wrap(cell2, 79 - INDENT - 3 - WIDTH) + + first_line = True + while cell1_lines or cell2_lines: + line = ' ' * INDENT + if cell1_lines: + line += cell1_lines.pop(0).ljust(WIDTH) + else: + line += ' ' * (WIDTH) + line += ' : ' if first_line else ' ' + if cell2_lines: + line += cell2_lines.pop(0) + lines.append(line) + first_line = False + + return '\n'.join(lines) + + +def format_commands_for_rst_table(title, command_list): + W1 = WIDTH # internal width + W2 = 79 - W1 - 3 # internal width + HORIZ_LINE = '+-' + '-' * W1 + '-+-' + '-' * W2 + '-+' + + row_list = _command_list_to_table_cells(command_list) + + lines = [] + + lines.append(HORIZ_LINE) # Top line + for row_cells in row_list: + if len(row_cells) == 1: + lines.append('| ' + ('**' + row_cells[0] + '**').ljust(W1 + W2 + 3) + ' |') + + elif len(row_cells) == 2: + cmd_and_args = row_cells[0].split(' ', 1) + cmd = cmd_and_args[0] + args = cmd_and_args[1] if len(cmd_and_args) == 2 else '' + cell1 = '**-- %s** %s' % (cmd, args) + cell1_lines = textwrap.wrap(cell1, W1) #, subsequent_indent=' ') + cell2_lines = textwrap.wrap(row_cells[1], W2) + + while cell1_lines or cell2_lines: + line = '| ' + line += (cell1_lines.pop(0) if cell1_lines else '').ljust(W1) + line += ' | ' + line += (cell2_lines.pop(0) if cell2_lines else '').ljust(W2) + line += ' |' + lines.append(line) + + else: + assert False + + lines.append(HORIZ_LINE) + return lines + + + + + +def _command_list_to_table_cells(command_list): + row_list = [] + for obj in command_list: + + if isinstance(obj, basestring): # group heading + row_list.append([obj.upper()]) + + else: # individual command + doc_before_empty_line = obj.__doc__.split('\n\n')[0] + doc_lines = [s.strip() for s in doc_before_empty_line.split('\n')] + for doc_line in doc_lines: + if doc_line == '': + continue + if obj.__name__ in ('ub', 'hkl'): + continue + name, args, desc = _split_doc_line(doc_line) + desc = desc.strip() + args = args.strip() + if desc and desc[-1] == '.': + desc = desc[:-1] + + row_list.append([name + (' ' if args else '') + args, desc]) + + return row_list + + +def _split_doc_line(docLine): + name, _, right = docLine.partition(' ') + args, _, desc = right.partition('-- ') + return name, args, desc + + +def compile_extra_motion_commands_for_help(): + + _hwname = settings.hardware.name # @UndefinedVariable + _angles = ', '.join(settings.hardware.get_axes_names()) # @UndefinedVariable + + commands = [] + + commands.append('Motion') + commands.append(sim) + commands.append(ExternalCommand( + '%(_hwname)s -- show Eularian position' % vars())) + commands.append(ExternalCommand( + 'pos %(_hwname)s [%(_angles)s] -- move to Eularian position' + '(None holds an axis still)' % vars())) + commands.append(ExternalCommand( + 'sim %(_hwname)s [%(_angles)s] -- simulate move to Eulerian position' + '%(_hwname)s' % vars())) + + commands.append(ExternalCommand( + 'hkl -- show hkl position')) + commands.append(ExternalCommand( + 'pos hkl [h k l] -- move to hkl position')) + commands.append(ExternalCommand( + 'pos {h | k | l} val -- move h, k or l to val')) + commands.append(ExternalCommand( + 'sim hkl [h k l] -- simulate move to hkl position')) + +# if engine_name != 'vlieg': +# pass +# # TODO: remove sigtau command and 'Surface' string + return commands \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/__init__.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/__init__.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/command.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/command.py new file mode 100644 index 0000000..d6e7fc5 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/command.py @@ -0,0 +1,322 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +#try: +# from gda.device import Scannable +#except ImportError: +# from diffcalc.gdasupport.minigda.scannable import Scannable +from diffcalc.gdasupport.minigda.scannable import Scannable +from diffcalc.util import getMessageFromException, allnum, bold +import math + + +ROOT_NAMESPACE_DICT = {} + +class Pos(object): + + def __init__(self): + self.__name__ = 'pos' + + def __call__(self, *posargs): + if len(posargs) == 0: + + keys = dict(ROOT_NAMESPACE_DICT).keys() + keys.sort() + for key in keys: + val = ROOT_NAMESPACE_DICT[key] + if isinstance(val, Scannable): + print self.posReturningReport(val) + else: + print self.posReturningReport(*posargs) + + def posReturningReport(self, *posargs): + # report position of this scannable + if len(posargs) == 1: + scannable = posargs[0] + self._assert_scannable(scannable) + return self._generatePositionReport(scannable) + + # Move the scannable and report + elif len(posargs) == 2: + scannable = posargs[0] + self._assert_scannable(scannable) + # Move it + scannable.asynchronousMoveTo(posargs[1]) + # TODO: minigda assumes all moves complete instantly, so no need + # yet to check the move is complete + return self._generatePositionReport(scannable) + + else: + raise ValueError( + "Invlaid arguements: 'pos [ scannable [ value ] ]'") + + def _assert_scannable(self, obj): + if not isinstance(obj, Scannable): + raise TypeError( + "The first argument to the pos command must be scannable. " + "Not: " + str(type(obj))) + + def _generatePositionReport(self, scannable): + fieldNames = (tuple(scannable.getInputNames()) + + tuple(scannable.getExtraNames())) + # All scannables + result = "%s:" % scannable.getName() + result = result.ljust(10) + try: + pos = scannable.getPosition() + except Exception, e: + return result + "Error: %s" % getMessageFromException(e) + if pos is None: + return result + "---" + # Single field scannable: + if len(fieldNames) == 1: + try: + result += "%s" % scannable.formatPositionFields(pos)[0] + except AttributeError: + result += str(scannable()) + # Multi field scannable: + else: + try: + formatted = scannable.formatPositionFields(pos) + for name, formattedValue in zip(fieldNames, formatted): + result += "%s: %s " % (name, formattedValue) + except AttributeError: + result += str(scannable()) + + return result + + +class ScanDataHandler: + def __init__(self): + self.scannables = None + + def callAtScanStart(self, scannables): + pass + + def callWithScanPoint(self, PositionDictIndexedByScannable): + pass + + def callAtScanEnd(self): + pass + + +class ScanDataPrinter(ScanDataHandler): + + def __init__(self): + self.first_point_printed = False + self.widths = [] + self.scannables = [] + + def callAtScanStart(self, scannables): + self.first_point_printed = False + self.scannables = scannables + + def print_first_point(self, position_dict): + # also sets self.widths + header_strings = [] + for scn in self.scannables: + field_names = list(scn.getInputNames()) + list(scn.getExtraNames()) + if len(field_names) == 1: + header_strings.append(scn.getName()) + else: + for field_name in field_names: + header_strings.append(field_name) + + first_row_strings = [] + for scn in self.scannables: + pos = position_dict[scn] + first_row_strings.extend(scn.formatPositionFields(pos)) + + self.widths = [] + for header, pos_string in zip(header_strings, first_row_strings): + self.widths.append(max(len(header), len(pos_string))) + + header_cells = [] + for heading, width in zip(header_strings, self.widths): + header_cells.append(heading.rjust(width)) + + underline_cells = ['-' * w for w in self.widths] + + first_row_cells = [] + for pos, width in zip(first_row_strings, self.widths): + first_row_cells.append(pos.rjust(width)) + + #table_width = sum(self.widths) + len(self.widths * 2) - 2 + lines = [] + #lines.append('=' * table_width) + lines.append(bold(' '.join(header_cells))) + lines.append(' '.join(underline_cells)) + lines.append(' '.join(first_row_cells)) + print '\n'.join(lines) + + def callWithScanPoint(self, position_dict): + if not self.first_point_printed: + self.print_first_point(position_dict) + self.first_point_printed = True + else: + row_strings = [] + for scn in self.scannables: + pos = position_dict[scn] + row_strings.extend(scn.formatPositionFields(pos)) + + row_cells = [] + for pos, width in zip(row_strings, self.widths): + row_cells.append(pos.rjust(width)) + + print ' '.join(row_cells) + + def callAtScanEnd(self): + #table_width = sum(self.widths) + len(self.widths * 2) - 2 + #print '=' * table_width + pass + + +class Scan(object): + class Group: + def __init__(self, scannable): + self.scannable = scannable + self.args = [] + + def __cmp__(self, other): + return(self.scannable.getLevel() - other.scannable.getLevel()) + + def __repr__(self): + return "Group(%s, %s)" % (self.scannable.getName(), str(self.args)) + + def shouldTriggerLoop(self): + return len(self.args) == 3 + + def __init__(self, scanDataHandlers): + # scanDataHandlers should be list + if type(scanDataHandlers) not in (tuple, list): + scanDataHandlers = (scanDataHandlers,) + self.dataHandlers = scanDataHandlers + + def __call__(self, *scanargs): + groups = self._parseScanArgsIntoScannableArgGroups(scanargs) + groups = self._reorderInnerGroupsAccordingToLevel(groups) + # Configure data handlers for a new scan + for handler in self.dataHandlers: handler.callAtScanStart( + [grp.scannable for grp in groups]) + # Perform the scan + self._performScan(groups, currentRecursionLevel=0) + # Inform data handlers of scan completion + for handler in self.dataHandlers: handler.callAtScanEnd() + + def _parseScanArgsIntoScannableArgGroups(self, scanargs): + """ + -> [ Group(scnA, (a1, a2, a2)), Group((scnB), (b1)), ... + ... Group((scnC),()), Group((scnD),(d1))] + """ + result = [] + if not isinstance(scanargs[0], Scannable): + raise TypeError("First scan argument must be a scannable") + + # Parse out scannables followed by non-scannable args + for arg in scanargs: + if isinstance(arg, Scannable): + result.append(Scan.Group(arg)) + else: + result[-1].args.append(arg) + return result + + def _reorderInnerGroupsAccordingToLevel(self, groups): + # Find the first group not to trigger a loop + for idx, group in enumerate(groups): + if not group.shouldTriggerLoop(): + break + latter = groups[idx:]; latter.sort() # Horrible hack not needed in python 3! + return groups[:idx] + latter + + def _performScan(self, groups, currentRecursionLevel): + # groups[currentRecursionLevel:] will start with either: + # a) A loop triggering group + # b) A number (possibly 0) of non-loop triggering groups + unprocessedGroups = groups[currentRecursionLevel:] + + # 1) If first remaining group should trigger a loop, perform this loop, + # recursively calling this method on the remaining groups + if len(unprocessedGroups) > 0: + first = unprocessedGroups[0] + # If groups starts with a request to loop: + if first.shouldTriggerLoop(): + posList = self._frange(first.args[0], first.args[1], first.args[2]) + for pos in posList: + first.scannable.asynchronousMoveTo(pos) + # TODO: Should wait. minigda assumes all moves complete immediately + self._performScan(groups, currentRecursionLevel + 1) + return + + # 2) Move all non-loop triggering groups (may be zero) + self._moveNonLoopTriggeringGroups(unprocessedGroups) + + # 3) Sample position of all scannables + posDict = self._samplePositionsOfAllScannables(groups) + + # 4) Inform the data handlers that this point has been recorded + for handler in self.dataHandlers: handler.callWithScanPoint(posDict) + + def _moveNonLoopTriggeringGroups(self, groups): + # TODO: Should wait. minigda assumes all moves complete immediately. groups could be zero lengthed. + for grp in groups: + if len(grp.args) == 0: + pass + elif len(grp.args) == 1: + grp.scannable.asynchronousMoveTo(grp.args[0]) + elif len(grp.args) == 2: + raise Exception("Scannables followed by two args not supported by minigda's scan command ") + else: + raise Exception("Scannable: %s args%s" % (grp.scannable, str(grp.args))) + + def _samplePositionsOfAllScannables(self, groups): + posDict = {} + for grp in groups: + posDict[grp.scannable] = grp.scannable.getPosition() + return posDict + + def _frange(self, limit1, limit2, increment): + """Range function that accepts floats (and integers). + """ +# limit1 = float(limit1) +# limit2 = float(limit2) + try: + increment = float(increment) + except TypeError: + raise TypeError( + "Only scaler values are supported, not GDA format vectors.") + count = int(math.ceil(((limit2 - limit1) + increment / 100.) / increment)) + result = [] + for n in range(count): + result.append(limit1 + n * increment) + return result + + +def sim(scn, hkl): + """sim hkl scn -- simulates moving scannable (not all) + """ + if not isinstance(hkl, (tuple, list)): + raise TypeError() + + if not allnum(hkl): + raise TypeError() + + try: + print scn.simulateMoveTo(hkl) + except AttributeError: + raise TypeError( + "The first argument does not support simulated moves") \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/scannable.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/scannable.py new file mode 100644 index 0000000..f6f9926 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/minigda/scannable.py @@ -0,0 +1,511 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +import time + +try: + from gda.device.scannable import ScannableBase +except ImportError: + class Scannable(object): + pass + + class ScannableBase(Scannable): + """Implemtation of a subset of OpenGDA's Scannable interface + """ + + level = 5 + inputNames = [] + extraNames = [] + outputFormat = [] + + def isBusy(self): + raise NotImplementedError() + + def rawGetPosition(self): + raise NotImplementedError() + + def rawAsynchronousMoveTo(self, newpos): + raise NotImplementedError() + + def waitWhileBusy(self): + while self.isBusy(): + time.sleep(.1) + + def getPosition(self): + return self.rawGetPosition() + + def asynchronousMoveTo(self, newpos): + self.rawAsynchronousMoveTo(newpos) + + def atScanStart(self): + pass + + def atScanEnd(self): + pass + + def atCommandFailure(self): + pass + + ### + + def __repr__(self): + pos = self.getPosition() + formattedValues = self.formatPositionFields(pos) + if len(tuple(self.getInputNames()) + tuple(self.getExtraNames())) > 1: + result = self.getName() + ': ' + else: + result = '' + + names = tuple(self.getInputNames()) + tuple(self.getExtraNames()) + for name, val in zip(names, formattedValues): + result += ' ' + name + ': ' + val + return result + ### + + def formatPositionFields(self, pos): + """Returns position as array of formatted strings""" + # Make sure pos is a tuple or list + if type(pos) not in (tuple, list): + pos = tuple([pos]) + + # Sanity check + if len(pos) != len(self.getOutputFormat()): + raise Exception( + "In scannable '%s':number of position fields differs from " + "number format strings specified" % self.getName()) + + result = [] + for field, format in zip(pos, self.getOutputFormat()): + if field is None: + result.append('???') + else: + s = (format % field) + ## if width!=None: + ## s = s.ljust(width) + result.append(s) + + return result + + def getName(self): + return self.name + + def setName(self, value): + self.name = value + + def getLevel(self): + return self.level + + def setLevel(self, value): + self.level = value + + def getInputNames(self): + return self.inputNames + + def setInputNames(self, value): + self.inputNames = value + + def getExtraNames(self): + return self.extraNames + + def setExtraNames(self, value): + self.extraNames = value + + def getOutputFormat(self): + return self.outputFormat + + def setOutputFormat(self, value): + if type(value) not in (tuple, list): + raise TypeError( + "%s.setOutputFormat() expects tuple or list; not %s" % + (self.getName(), str(type(value)))) + self.outputFormat = value + + def __call__(self, newpos=None): + if newpos is None: + return self.getPosition() + self.asynchronousMoveTo(newpos) + + class ScannableAdapter(Scannable): + '''Wrap up a Scannable and give it a new name and optionally an offset + (added to the delegate when reading up and subtracting when setting down + ''' + + def __init__(self, delegate_scn, name, offset=0): + assert len(delegate_scn.getInputNames()) == 1 + assert len(delegate_scn.getExtraNames()) == 0 + self.delegate_scn = delegate_scn + self.name = name + self.offset = offset + + def __getattr__(self, name): + return getattr(self.delegate_scn, name) + + def getName(self): + return self.name + + def getInputNames(self): + return [self.name] + + def getPosition(self): + return self.delegate_scn.getPosition() + self.offset + + def asynchronousMoveTo(self, newpos): + self.delegate_scn.asynchronousMoveTo(newpos - self.offset) + + def __repr__(self): + pos = self.getPosition() + formatted_values = self.delegate_scn.formatPositionFields(pos) + return self.name + ': ' + formatted_values[0] + ' ' + self.get_hint() + + def get_hint(self): + if self.offset: + offset_hint = ' + ' if self.offset >= 0 else ' - ' + offset_hint += str(self.offset) + else: + offset_hint = '' + return '(%s%s)' % (self.delegate_scn.name, offset_hint) + + def __call__(self, newpos=None): + if newpos is None: + return self.getPosition() + self.asynchronousMoveTo(newpos) + +class SingleFieldDummyScannable(ScannableBase): + + def __init__(self, name, initial_position=0.): + self.name = name + self.inputNames = [name] + self.outputFormat = ['% 6.4f'] + self.level = 3 + self._current_position = float(initial_position) + + def isBusy(self): + return False + + def waitWhileBusy(self): + return + + def asynchronousMoveTo(self, new_position): + self._current_position = float(new_position) + + def getPosition(self): + return self._current_position + + +class DummyPD(SingleFieldDummyScannable): + """For compatability with the gda's dummy_pd module""" + pass + + +class MultiInputExtraFieldsDummyScannable(ScannableBase): + '''Multi input Dummy PD Class supporting input and extra fields''' + def __init__(self, name, inputNames, extraNames): + self.setName(name) + self.setInputNames(inputNames) + self.setExtraNames(extraNames) + self.setOutputFormat(['%6.4f'] * (len(inputNames) + len(extraNames))) + self.setLevel(3) + self.currentposition = [0.0] * len(inputNames) + + def isBusy(self): + return 0 + + def asynchronousMoveTo(self, new_position): + if type(new_position) == type(1) or type(new_position) == type(1.0): + new_position = [new_position] + msg = "Wrong new_position size" + assert len(new_position) == len(self.currentposition), msg + for i in range(len(new_position)): + if new_position[i] != None: + self.currentposition[i] = float(new_position[i]) + + def getPosition(self): + extraValues = range(100, 100 + (len(self.getExtraNames()))) + return self.currentposition + map(float, extraValues) + + +class ZeroInputExtraFieldsDummyScannable(ScannableBase): + '''Zero input/extra field dummy pd + ''' + def __init__(self, name): + self.setName(name) + self.setInputNames([]) + self.setOutputFormat([]) + + def isBusy(self): + return 0 + + def asynchronousMoveTo(self, new_position): + pass + + def getPosition(self): + pass + + +class ScannableGroup(ScannableBase): + """wraps up motors. Simulates motors if non given.""" + + def __init__(self, name, motorList): + + self.setName(name) + # Set input format + motorNames = [] + for scn in motorList: + motorNames.append(scn.getName()) + self.setInputNames(motorNames) + # Set output format + format = [] + for motor in motorList: + format.append(motor.getOutputFormat()[0]) + self.setOutputFormat(format) + self.__motors = motorList + + def asynchronousMoveTo(self, position): + # if input has any Nones, then replace these with the current positions + if None in position: + position = list(position) + current = self.getPosition() + for idx, val in enumerate(position): + if val is None: + position[idx] = current[idx] + + for scn, pos in zip(self.__motors, position): + scn.asynchronousMoveTo(pos) + + def getPosition(self): + return [scn.getPosition() for scn in self.__motors] + + def isBusy(self): + for scn in self.__motors: + if scn.isBusy(): + return True + return False + + def configure(self): + pass + + +class ScannableMotionWithScannableFieldsBase(ScannableBase): + ''' + This extended version of ScannableMotionBase contains a + completeInstantiation() method which adds a dictionary of + MotionScannableParts to an instance. Each part allows one of the + instances fields to be interacted with like it itself is a scannable. + Fields are dynamically added to the instance linking to these parts + allowing dotted access from Jython. They may also be accessed using + Jython container access methods (via the __getitem__() method). To acess + them from Jave use the getComponent(name) method. + + When moving a part (via either a pos or scan command), the part calls + the parent to perform the actual task. The parts asynchronousMoveto + command will call the parent with a list of None values except for the + field it represents which will be passed the desired position value. + + The asynchronousMoveTo method in class that inherats from this base + class then must handle these Nones. In some cases the method may + actually be able to move the underlying system assoiciated with one + field individually from others. If this is not possible the best + behaviour may be to simply not support this beahviour and exception or + alternatively to substitute the None values with actual current position + of parent's scannables associated fields. + + ScannableMotionBaseWithMemory() inherats from this calss and provides a + solution useful for some scenarious: it keeps track of the last position + moved to, and replaces the Nones in an asynchronousMoveTo request with + these values. There are a number of dangers associated with this which + are addressed in that class's documentation, but it provides a way to + move one axis within a group of non-orthogonal axis while keeping the + others still. + ''' + childrenDict = {} + numInputFields = None + numExtraFields = None + + def completeInstantiation(self): + '''This method should be called at the end of all user defined + consructors''' + # self.validate() + self.numInputFields = len(self.getInputNames()) + self.numExtraFields = len(self.getExtraNames()) + self.addScannableParts() + self.autoCompletePartialMoveToTargets = False + self.positionAtScanStart = None + + def setAutoCompletePartialMoveToTargets(self, b): + self.autoCompletePartialMoveToTargets = b + + def atScanStart(self): + self.positionAtScanStart = self.getPosition() + + def atCommandFailure(self): + self.positionAtScanStart = None + + def atScanEnd(self): + self.positionAtScanStart = None + +### + + def __repr__(self): + pos = self.getPosition() + formattedValues = self.formatPositionFields(pos) + if len(tuple(self.getInputNames()) + tuple(self.getExtraNames())) > 1: + result = self.getName() + ': ' + else: + result = '' + + names = tuple(self.getInputNames()) + tuple(self.getExtraNames()) + for name, val in zip(names, formattedValues): + result += ' ' + name + ': ' + val + return result +### + + def formatPositionFields(self, pos): + """Returns position as array of formatted strings""" + # Make sure pos is a tuple or list + if type(pos) not in (tuple, list): + pos = tuple([pos]) + + # Sanity check + if len(pos) != len(self.getOutputFormat()): + raise Exception( + "In scannable '%s':number of position fields differs from " + "number format strings specified" % self.getName()) + + result = [] + for field, format in zip(pos, self.getOutputFormat()): + if field is None: + result.append('???') + else: + s = (format % field) +## if width!=None: +## s = s.ljust(width) + result.append(s) + + return result + +### + + def addScannableParts(self): + ''' + Creates an array of MotionScannableParts each of which allows access to + the scannable's fields. See this class's documentation for more info. + ''' + self.childrenDict = {} + # Add parts to access the input fields + for index in range(len(self.getInputNames())): + scannableName = self.getInputNames()[index] + self.childrenDict[scannableName] = self.MotionScannablePart( + scannableName, index, self, isInputField=1) + + # Add parts to access the extra fields + for index in range(len(self.getExtraNames())): + scannableName = self.getExtraNames()[index] + self.childrenDict[scannableName] = self.MotionScannablePart( + scannableName, index + len(self.getInputNames()), + self, isInputField=0) + + def asynchronousMoveTo(self, newpos): + if self.autoCompletePartialMoveToTargets: + newpos = self.completePosition(newpos) + ScannableBase.asynchronousMoveTo(self, newpos) + + def completePosition(self, position): + ''' + If position contains any null or None values, these are replaced with + the corresponding fields from the scannables current position and then + returned.''' + # Just return position if it does not need padding + if None not in position: + return position + if self.positionAtScanStart is not None: + basePosition = self.positionAtScanStart + else: + basePosition = self.getPosition()[:self.numInputFields] + for i in range(self.numInputFields): + if position[i] is None: + position[i] = basePosition[i] + return position + + def __getattr__(self, name): + try: + return self.childrenDict[name] + except: + raise AttributeError("No child named:" + name) + + def __getitem__(self, key): + '''Provides container like access from Jython''' + return self.childrenDict[key] + + def getPart(self, name): + '''Returns the a compnent scannable''' + return self.childrenDict[name] + + class MotionScannablePart(ScannableBase): + ''' + A scannable to be placed in the parent's childrenDict that allows + access to the parent's individual fields.''' + + def __init__(self, scannableName, index, parentScannable, + isInputField): + self.setName(scannableName) + if isInputField: + self.setInputNames([scannableName]) + else: + self.setExtraNames([scannableName]) + self.index = index + self.parentScannable = parentScannable + self.setOutputFormat( + [self.parentScannable.getOutputFormat()[index]]) + + def isBusy(self): + return self.parentScannable.isBusy() + + def asynchronousMoveTo(self, new_position): + if self.parentScannable.isBusy(): + raise Exception( + self.parentScannable.getName() + "." + self.getName() + + " cannot be moved because " + + self.parentScannable.getName() + " is already moving") + + toMoveTo = [None] * len(self.parentScannable.getInputNames()) + toMoveTo[self.index] = new_position + self.parentScannable.asynchronousMoveTo(toMoveTo) + + def moveTo(self, new_position): + self.asynchronousMoveTo(new_position) + self.waitWhileBusy() + + def getPosition(self): + return self.parentScannable.getPosition()[self.index] + + def __str__(self): + return self.__repr__() + + def __repr__(self): + # Get the name of this field + # (assume its an input field first and correct if wrong) + name = self.getInputNames()[0] + + if name == 'value': + name = self.getExtraNames()[0] + parentName = self.parentScannable.getName() + return parentName + "." + name + " : " + str(self.getPosition()) + + + + \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/__init__.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/base.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/base.py new file mode 100644 index 0000000..dfa3182 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/base.py @@ -0,0 +1,62 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +try: + from gda.device.scannable import PseudoDevice +except ImportError: + from diffcalc.gdasupport.minigda.scannable import \ + ScannableBase as PseudoDevice + + +class ScannableGroup(PseudoDevice): + + def __init__(self, name, motorList): + + self.setName(name) + # Set input format + motorNames = [] + for scn in motorList: + motorNames.append(scn.getName()) + self.setInputNames(motorNames) + # Set output format + format = [] + for motor in motorList: + format.append(motor.getOutputFormat()[0]) + self.setOutputFormat(format) + self.__motors = motorList + + def asynchronousMoveTo(self, position): + # if input has any Nones, then replace these with the current positions + if None in position: + position = list(position) + current = self.getPosition() + for idx, val in enumerate(position): + if val is None: + position[idx] = current[idx] + + for scn, pos in zip(self.__motors, position): + scn.asynchronousMoveTo(pos) + + def getPosition(self): + return [scn.getPosition() for scn in self.__motors] + + def isBusy(self): + for scn in self.__motors: + if scn.isBusy(): + return True + return False diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/diffractometer.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/diffractometer.py new file mode 100644 index 0000000..1d1b993 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/diffractometer.py @@ -0,0 +1,126 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +try: + from gda.device.scannable import ScannableMotionBase +except ImportError: + from diffcalc.gdasupport.minigda.scannable import \ + ScannableBase as ScannableMotionBase + +from diffcalc.util import getMessageFromException + +# TODO: Split into a base class when making other scannables + + +class DiffractometerScannableGroup(ScannableMotionBase): + """" + Wraps up a scannableGroup of axis to tweak the way the resulting + object is displayed and to add a simulate move to method. + + The scannable group should have the same geometry as that expected + by the diffractometer hardware geometry used in the diffraction + calculator. + + The optional parameter slaveDriver can be used to provide a + slave_driver. This is useful for triggering a move of an incidental + axis whose position depends on that of the diffractometer, but whose + position need not be included in the DiffractometerScannableGroup + itself. This parameter is exposed as a field and can be set or + cleared to null at will without effecting the core calculation code. + """ + + def __init__(self, name, diffcalc_module, scannableGroup, + slave_driver=None, hint_generator=None): + # if motorList is None, will create a dummy __group + self.diffcalc_module = diffcalc_module + self.__group = scannableGroup + self.slave_driver = slave_driver + self.setName(name) + self.hint_generator = hint_generator + + def getInputNames(self): + return self.__group.getInputNames() + + def getExtraNames(self): + if self.slave_driver is None: + return [] + else: + return self.slave_driver.getScannableNames() + + def getOutputFormat(self): + if self.slave_driver is None: + slave_formats = [] + else: + slave_formats = self.slave_driver.getScannableNames() + return list(self.__group.getOutputFormat()) + slave_formats + + def asynchronousMoveTo(self, position): + self.__group.asynchronousMoveTo(position) + if self.slave_driver is not None: + self.slave_driver.triggerAsynchronousMove(position) + + def getPosition(self): + if self.slave_driver is None: + slave_positions = [] + else: + slave_positions = self.slave_driver.getPositions() + return list(self.__group.getPosition()) + list(slave_positions) + + def isBusy(self): + if self.slave_driver is None: + return self.__group.isBusy() + else: + return self.__group.isBusy() or self.slave_driver.isBusy() + + def waitWhileBusy(self): + self.__group.waitWhileBusy() + if self.slave_driver is not None: + self.slave_driver.waitWhileBusy() + + def simulateMoveTo(self, pos): + if len(pos) != len(self.getInputNames()): + raise ValueError('Wrong number of inputs') + try: + (hkl, params) = self.diffcalc_module.angles_to_hkl(pos) + except Exception, e: + return "Error: %s" % getMessageFromException(e) + width = max(len(k) for k in params) + + lines = ([' ' + 'hkl'.rjust(width) + ' : % 9.4f %.4f %.4f' % + (hkl[0], hkl[1], hkl[2])]) + lines[-1] = lines[-1] + '\n' + fmt = ' %' + str(width) + 's : % 9.4f' + for k in sorted(params): + lines.append(fmt % (k, params[k])) + return '\n'.join(lines) + + def __repr__(self): + position = self.getPosition() + names = list(self.getInputNames()) + list(self.getExtraNames()) + if self.hint_generator is None: + hint_list = [''] * len(self.getInputNames()) + else: + hint_list = self.hint_generator() + + lines = [self.name + ':'] + width = max(len(k) for k in names) + fmt = ' %' + str(width) + 's : % 9.4f %s' + for name, pos, hint in zip(names, position, hint_list): + lines.append(fmt % (name, pos, hint)) + lines[len(self.getInputNames())] += '\n' + return '\n'.join(lines) diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/hkl.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/hkl.py new file mode 100644 index 0000000..12376d2 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/hkl.py @@ -0,0 +1,135 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### +import platform + +DEBUG = False + +try: + from gda.device.scannable.scannablegroup import \ + ScannableMotionWithScannableFieldsBase +except ImportError: + from diffcalc.gdasupport.minigda.scannable import \ + ScannableMotionWithScannableFieldsBase + +from diffcalc.util import getMessageFromException, DiffcalcException + + +class _DynamicDocstringMetaclass(type): + + def _get_doc(self): + return Hkl.dynamic_docstring + + __doc__ = property(_get_doc) # @ReservedAssignment + + +class Hkl(ScannableMotionWithScannableFieldsBase): + + if platform.system() != 'Java': + __metaclass__ = _DynamicDocstringMetaclass # TODO: Removed to fix Jython + + dynamic_docstring = 'Hkl Scannable' + + def _get_doc(self): + return Hkl.dynamic_docstring + + __doc__ = property(_get_doc) # @ReservedAssignment + + def __init__(self, name, diffractometerObject, diffcalcObject, + virtualAnglesToReport=None): + self.diffhw = diffractometerObject + self._diffcalc = diffcalcObject + if type(virtualAnglesToReport) is str: + virtualAnglesToReport = (virtualAnglesToReport,) + self.vAngleNames = virtualAnglesToReport + + self.setName(name) + self.setInputNames(['h', 'k', 'l']) + self.setOutputFormat(['%7.5f'] * 3) + if self.vAngleNames: + self.setExtraNames(self.vAngleNames) + self.setOutputFormat(['%7.5f'] * (3 + len(self.vAngleNames))) + + self.completeInstantiation() + self.setAutoCompletePartialMoveToTargets(True) + self.dynamic_class_doc = 'Hkl Scannable xyz' + + def rawAsynchronousMoveTo(self, hkl): + if len(hkl) != 3: raise ValueError('Hkl device expects three inputs') + try: + (pos, _) = self._diffcalc.hkl_to_angles(hkl[0], hkl[1], hkl[2]) + except DiffcalcException, e: + if DEBUG: + raise + else: + raise DiffcalcException(e.message) + self.diffhw.asynchronousMoveTo(pos) + + def rawGetPosition(self): + pos = self.diffhw.getPosition() # a tuple + (hkl , params) = self._diffcalc.angles_to_hkl(pos) + result = list(hkl) + if self.vAngleNames: + for vAngleName in self.vAngleNames: + result.append(params[vAngleName]) + return result + + def getFieldPosition(self, i): + return self.getPosition()[i] + + def isBusy(self): + return self.diffhw.isBusy() + + def waitWhileBusy(self): + return self.diffhw.waitWhileBusy() + + def simulateMoveTo(self, hkl): + if type(hkl) not in (list, tuple): + raise ValueError('Hkl device expects three inputs') + if len(hkl) != 3: + raise ValueError('Hkl device expects three inputs') + (pos, params) = self._diffcalc.hkl_to_angles(hkl[0], hkl[1], hkl[2]) + + width = max(len(k) for k in (params.keys() + list(self.diffhw.getInputNames()))) + fmt = ' %' + str(width) + 's : % 9.4f' + + lines = [self.diffhw.getName() + ' would move to:'] + for idx, name in enumerate(self.diffhw.getInputNames()): + lines.append(fmt % (name, pos[idx])) + lines[-1] = lines[-1] + '\n' + for k in sorted(params): + lines.append(fmt % (k, params[k])) + return '\n'.join(lines) + + def __str__(self): + return self.__repr__() + + def __repr__(self): + lines = ['hkl:'] + pos = self.diffhw.getPosition() + try: + (hkl, params) = self._diffcalc.angles_to_hkl(pos) + except Exception, e: + return "" % getMessageFromException(e) + + width = max(len(k) for k in params) + lines.append(' ' + 'hkl'.rjust(width) + ' : %9.4f %.4f %.4f' % (hkl[0], hkl[1], hkl[2])) + lines[-1] = lines[-1] + '\n' + fmt = ' %' + str(width) + 's : % 9.4f' + for k in sorted(params): + lines.append(fmt % (k, params[k])) + return '\n'.join(lines) diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/mock.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/mock.py new file mode 100644 index 0000000..13871da --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/mock.py @@ -0,0 +1,47 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +try: + from gda.device.scannable import ScannableMotionBase +except ImportError: + from diffcalc.gdasupport.minigda.scannable import \ + ScannableBase as ScannableMotionBase + + +class MockMotor(ScannableMotionBase): + + def __init__(self, name='mock'): + self.pos = 0.0 + self._busy = False + self.name = name + + def asynchronousMoveTo(self, pos): + self._busy = True + self.pos = float(pos) + + def getPosition(self): + return self.pos + + def isBusy(self): + return self._busy + + def makeNotBusy(self): + self._busy = False + + def getOutputFormat(self): + return ['%f'] diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/parameter.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/parameter.py new file mode 100644 index 0000000..a08e95c --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/parameter.py @@ -0,0 +1,45 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +try: + from gda.device.scannable import ScannableMotionBase +except ImportError: + from diffcalc.gdasupport.minigda.scannable import \ + ScannableBase as ScannableMotionBase + + +class DiffractionCalculatorParameter(ScannableMotionBase): + + def __init__(self, name, parameterName, parameter_manager): + + self.parameter_manager = parameter_manager + self.parameterName = parameterName + + self.setName(name) + self.setInputNames([parameterName]) + self.setOutputFormat(['%5.5f']) + self.setLevel(3) + + def asynchronousMoveTo(self, value): + self.parameter_manager.set_constraint(self.parameterName, value) + + def getPosition(self): + return self.parameter_manager.get_constraint(self.parameterName) + + def isBusy(self): + return False diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/sim.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/sim.py new file mode 100644 index 0000000..44b2108 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/sim.py @@ -0,0 +1,21 @@ +''' +Created on 7 May 2016 + +@author: walton +''' +from diffcalc.util import allnum + +def sim(scn, hkl): + """sim hkl scn -- simulates moving scannable (not all) + """ + if not isinstance(hkl, (tuple, list)): + raise TypeError + + if not allnum(hkl): + raise TypeError() + + try: + print scn.simulateMoveTo(hkl) + except AttributeError: + raise TypeError( + "The first argument does not support simulated moves") \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/simulation.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/simulation.py new file mode 100644 index 0000000..0255a5f --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/simulation.py @@ -0,0 +1,139 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +import time +from math import sqrt, pi, exp + +try: + from gda.device.scannable import PseudoDevice +except ImportError: + from diffcalc.gdasupport.minigda.scannable import \ + ScannableBase as PseudoDevice + +from diffcalc.ub.crystal import CrystalUnderTest +from diffcalc.hkl.you.calc import youAnglesToHkl +from diffcalc.hkl.vlieg.calc import vliegAnglesToHkl +from diffcalc.hkl.you.geometry import calcCHI, calcPHI + +TORAD = pi / 180 +TODEG = 180 / pi + + +class Equation(object): + + def __call__(self, dh, dk, dl): + raise Exception('Abstract') + + def __str__(self): + "Abstract equation" + + +class Gaussian(Equation): + + def __init__(self, variance): + self.variance = float(variance) + + def __call__(self, dh, dk, dl): + dr_squared = dh * dh + dk * dk + dl * dl + return (1 / sqrt(2 * pi * self.variance) * + exp(-dr_squared / (2 * self.variance))) + + +class SimulatedCrystalCounter(PseudoDevice): + + def __init__(self, name, diffractometerScannable, geometryPlugin, + wavelengthScannable, equation=Gaussian(.01), engine='you'): + self.setName(name) + self.setInputNames([name + '_count']) + self.setOutputFormat(['%7.5f']) + self.exposureTime = 1 + self.pause = True + self.diffractometerScannable = diffractometerScannable + self.geometry = geometryPlugin + self.wavelengthScannable = wavelengthScannable + self.equation = equation + self.engine = engine + + self.cut = None + self.UB = None + self.chiMissmount = 0. + self.phiMissmount = 0. + self.setCrystal('cubic', 1, 1, 1, 90, 90, 90) + + def setCrystal(self, name, a, b, c, alpha, beta, gamma): + self.cut = CrystalUnderTest(name, a, b, c, alpha, beta, gamma) + self.calcUB() + + def setChiMissmount(self, chi): + self.chiMissmount = chi + self.calcUB() + + def setPhiMissmount(self, phi): + self.phiMissmount = phi + self.calcUB() + + def calcUB(self): + CHI = calcCHI(self.chiMissmount * TORAD) + PHI = calcPHI(self.phiMissmount * TORAD) + self.UB = CHI * PHI * self.cut.B + + def asynchronousMoveTo(self, exposureTime): + self.exposureTime = exposureTime + if self.pause: + time.sleep(exposureTime) # Should not technically block! + + def getPosition(self): + h, k, l = self.getHkl() + dh, dk, dl = h - round(h), k - round(k), l - round(l) + count = self.equation(dh, dk, dl) + #return self.exposureTime, count*self.exposureTime + return count * self.exposureTime + + def getHkl(self): + pos = self.geometry.physical_angles_to_internal_position( + self.diffractometerScannable.getPosition()) + pos.changeToRadians() + wavelength = self.wavelengthScannable.getPosition() + if self.engine.lower() == 'vlieg': + return vliegAnglesToHkl(pos, wavelength, self.UB) + elif self.engine.lower() == 'you': + return youAnglesToHkl(pos, wavelength, self.UB) + else: + raise ValueError(self.engine) + + def isBusy(self): + return False + + def __str__(self): + return self.__repr__() + + def __repr__(self): + s = 'simulated crystal detector: %s\n' % self.getName() + h, k, l = self.getHkl() + s += ' h : %f\n' % h + s += ' k : %f\n' % k + s += ' l : %f\n' % l + s += self.cut.__str__() + '\n' + s += "chi orientation: %s\n" % self.chiMissmount + s += "phi orientation: %s\n" % self.phiMissmount + ub = self.UB.tolist() + s += "UB:\n" + s += " % 18.13f% 18.13f% 18.12f\n" % (ub[0][0], ub[0][1], ub[0][2]) + s += " % 18.13f% 18.13f% 18.12f\n" % (ub[1][0], ub[1][1], ub[1][2]) + s += " % 18.13f% 18.13f% 18.12f\n" % (ub[2][0], ub[2][1], ub[2][2]) + return s diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/slave_driver.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/slave_driver.py new file mode 100644 index 0000000..cd5ce24 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/slave_driver.py @@ -0,0 +1,109 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi, tan, sin, atan, cos, atan2 + +TORAD = pi / 180 +TODEG = 180 / pi + + +class SlaveScannableDriver(object): + + def __init__(self, scannables): + self.scannables = scannables + + def isBusy(self): + for scn in self.scannables: + if scn.isBusy(): + return True + return False + + def waitWhileBusy(self): + for scn in self.scannables: + scn.waitWhileBusy() + + def triggerAsynchronousMove(self, triggerPos): + nu = self.slaveFromTriggerPos(triggerPos) + for scn in self.scannables: + scn.asynchronousMoveTo(nu) + + def getPosition(self): + return self.scannables[0].getPosition() + + def slaveFromTriggerPos(self, triggerPos): + raise Exception("Abstract") + + def getScannableNames(self): + return [scn.name for scn in self.scannables] + + def getOutputFormat(self): + return [list(scn.outputFormat)[0] for scn in self.scannables] + + def getPositions(self): + return [float(scn.getPosition()) for scn in self.scannables] + + +""" +Based on: Elias Vlieg, "A (2+3)-Type Surface Diffractometer: Mergence of the +z-axis and (2+2)-Type Geometries", J. Appl. Cryst. (1998). 31. 198-203 +""" + + +class NuDriverForSixCirclePlugin(SlaveScannableDriver): + + def slaveFromTriggerPos(self, triggerPos): + + alpha, delta, gamma, _, _, _ = triggerPos + alpha = alpha * TORAD + delta = delta * TORAD + gamma = gamma * TORAD + + ### Equation16 RHS ### + rhs = -1 * tan(gamma - alpha) * sin(delta) + nu = atan(rhs) # -pi/2 <= nu <= pi/2 + return nu * TODEG + + +class NuDriverForWillmottHorizontalGeometry(SlaveScannableDriver): + + """ + Based on: Phillip Willmott, "Angle calculations for a (2+3)-type + diffractometer: focus on area detectors", J. Appl. Cryst. (2011). 44. + 73-83 + """ + + def __init__(self, scannables, area_detector=False): + SlaveScannableDriver.__init__(self, scannables) + self.area_detector = area_detector + + def slaveFromTriggerPos(self, triggerPos): + + delta, gamma, omegah, _ = triggerPos + delta *= TORAD + gamma *= TORAD + omegah *= TORAD + if self.area_detector: + nu = atan2(sin(delta - omegah), tan(gamma)) # (66) + else: + top = -sin(gamma) * sin(omegah) + bot = (sin(omegah) * cos(gamma) * sin(delta) + + cos(omegah) * cos(delta)) + nu = atan2(top, bot) # (61) + + print 'nu:', nu * TODEG + return nu * TODEG diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/vrmlanimator.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/vrmlanimator.py new file mode 100644 index 0000000..9f40441 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/vrmlanimator.py @@ -0,0 +1,184 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +import time +import threading +import socket +PORT = 4567 + +from gda.device.scannable import ScannableMotionWithScannableFieldsBaseTest + +#import scannable.vrmlModelDriver +#reload(scannable.vrmlModelDriver);from scannable.vrmlModelDriver import \ +# VrmlModelDriver, LinearProfile, MoveThread +#fc=VrmlModelDriver( +# 'fc',['alpha','delta','omega', 'chi','phi'], speed=30, host='diamrl5104') +#alpha = fc.alpha +#delta = fc.delta +#omega = fc.omega +#chi = fc.chi +#phi = fc.phi + + +def connect_to_socket(host, port): + print "Connecting to %s on port %d" % (host, port) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.connect((host, port)) + print "Connected" + socketfile = sock.makefile('rw', 0) + return socketfile + + +class LinearProfile(object): + + def __init__(self, v, t_accel, startList, endList): + assert len(startList) == len(endList) + self.v = float(v) + self.start = startList + self.end = endList + self.t_accel = t_accel + + distances = [e - s for e, s in zip(self.end, self.start)] + max_distance = max([abs(d) for d in distances]) + if max_distance == 0: + self.delta_time = 0 + else: + self.delta_time = abs(max_distance / self.v) + self.speeds = [d / self.delta_time for d in distances] + self.start_time = time.time() + + def getPosition(self): + if self.start_time is None: + return self.start + if not self.isMoving(): + return self.end + t = abs(float(time.time() - self.start_time)) + if t > self.delta_time: + # we are in the deceleration phase (i.e paused for now) + return self.end + return [s + v * t for s, v in zip(self.start, self.speeds)] + + def isMoving(self): + return time.time() < self.start_time + self.delta_time + self.t_accel + + +class MoveThread(threading.Thread): + + def __init__(self, profile, socketfile, axisNames): + threading.Thread.__init__(self) + self.profile = profile + self.socketfile = socketfile + self.axisNames = axisNames + + def run(self): + while self.profile.isMoving(): + self.update() + time.sleep(.1) + self.update() + + def update(self): + pos = self.profile.getPosition() + d = dict(zip(map(str, self.axisNames), pos)) + if self.socketfile: + self.socketfile.write(repr(d) + '\n') + + +class VrmlModelDriver(ScannableMotionWithScannableFieldsBaseTest): + + def __init__(self, name, axes_names, host=None, speed=60, t_accel=.1, + format='%.3f'): + self.name = name + self.inputNames = list(axes_names) + self.extraNames = [] + self.outputFormat = [format] * len(self.inputNames) + self.completeInstantiation() + self.__last_target = [0.] * len(self.inputNames) + self.verbose = False + self.move_thread = None + self.speed = speed + self.host = host + self.t_accel = t_accel + self.socketfile = None + if self.host: + try: + self.connect() + except socket.error: + print "Failed to connect to %s:%r" % (self.host, PORT) + print "Connect with: %s.connect()" % self.name + + def connect(self): + self.socketfile = connect_to_socket(self.host, PORT) + self.rawAsynchronousMoveTo(self.__last_target) + + def isBusy(self): + if self.move_thread is None: + return False + return self.move_thread.profile.isMoving() + + def rawGetPosition(self): + if self.move_thread is None: + return self.__last_target + else: + return self.move_thread.profile.getPosition() + + def rawAsynchronousMoveTo(self, targetList): + if self.isBusy(): + raise Exception(self.name + ' is already moving') + if self.verbose: + print self.name + ".rawAsynchronousMoveTo(%r)" % targetList + + for i, target in enumerate(targetList): + if target is None: + targetList[i] = self.__last_target[i] + profile = LinearProfile( + self.speed, self.t_accel, self.__last_target, targetList) + self.move_thread = MoveThread( + profile, self.socketfile, self.inputNames) + self.move_thread.start() + self.__last_target = targetList + + def getFieldPosition(self, index): + return self.getPosition()[index] + + def __del__(self): + self.socketfile.close() + +#class TrapezoidProfile(object): +# +# def __init__(self, t_accel, v_max, delta_x): +# self.t_a = t_accel +# self.v_m = v_max +# self.delta_x = delta_x +# +# self.t_c = (self.X - self.v_m*self.t_a) / self.v_m +# +# def x(self, t): +# if self.t_c <=0: +# return self.__xshort(t) +# else: +# return self.__xlong(t) +# +# def __xshort(self, t): +# delta_t = 2 * sqrt(self.delta_x*self.t_a/self.v_m) +# if t <= .5*delta_t: +# return (.5*self.v_m/self.t_a) * t**2 +# else: +# v_peak = (self.v_m/self.t_a) * .5*delta_t +# return (t-.5*delta_t)*v_peak - (t-.5*delta_t)**2 ####HERE, bugged +# self.delta_x/2 diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/wavelength.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/wavelength.py new file mode 100644 index 0000000..52fd925 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/scannable/wavelength.py @@ -0,0 +1,50 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +try: + from gdascripts.pd.dummy_pds import DummyPD +except ImportError: + from diffcalc.gdasupport.minigda.scannable import DummyPD + + +class Wavelength(DummyPD): + + def __init__(self, name, energyScannable, + energyScannableMultiplierToGetKeV=1): + self.energyScannable = energyScannable + self.energyScannableMultiplierToGetKeV = \ + energyScannableMultiplierToGetKeV + + DummyPD.__init__(self, name) + + def asynchronousMoveTo(self, pos): + self.energyScannable.asynchronousMoveTo( + (12.39842 / pos) / self.energyScannableMultiplierToGetKeV) + + def getPosition(self): + energy = self.energyScannable.getPosition() + if energy == 0: + raise Exception( + "The energy is 0, so no wavelength could be calculated.run_All()") + return 12.39842 / (energy * self.energyScannableMultiplierToGetKeV) + + def isBusy(self): + return self.energyScannable.isBusy() + + def waitWhileBusy(self): + return self.energyScannable.waitWhileBusy() diff --git a/script/test/diffcalc (copy)/diffcalc/gdasupport/you.py b/script/test/diffcalc (copy)/diffcalc/gdasupport/you.py new file mode 100644 index 0000000..609a344 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/gdasupport/you.py @@ -0,0 +1,113 @@ +from diffcalc.gdasupport.scannable.diffractometer import DiffractometerScannableGroup +from diffcalc.gdasupport.scannable.hkl import Hkl +from diffcalc.gdasupport.scannable.simulation import SimulatedCrystalCounter +from diffcalc.gdasupport.scannable.wavelength import Wavelength +from diffcalc.gdasupport.scannable.parameter import DiffractionCalculatorParameter + + +from diffcalc.dc import dcyou as _dc +from diffcalc.dc.help import format_command_help +reload(_dc) +from diffcalc.dc.dcyou import * # @UnusedWildImport +from diffcalc import settings + +try: + import gda # @UnusedImport @UnresolvedImport + GDA = True +except: + GDA = False + +if not GDA: + from diffcalc.gdasupport.minigda import command + _pos = command.Pos() + _scan = command.Scan(command.ScanDataPrinter()) + + def pos(*args): + """ + pos show position of all Scannables + pos scn show position of scn + pos scn targetmove scn to target (a number) + """ + return _pos(*args) + + def scan(*args): + """ + scan scn start stop step {scn {target}} {det t} + """ + return _scan(*args) + + +from diffcalc.gdasupport.scannable.sim import sim # @UnusedImport + +_scn_group = settings.axes_scannable_group +_diff_scn_name = settings.geometry.name # @UndefinedVariable +_energy_scannable = settings.energy_scannable + + +# Create diffractometer scannable +_diff_scn = DiffractometerScannableGroup(_diff_scn_name, _dc, _scn_group) +globals()[_diff_scn_name] = _diff_scn + +# Create hkl scannables +hkl = Hkl('hkl', _scn_group, _dc) +h = hkl.h +k = hkl.k +l = hkl.l + +Hkl.dynamic_docstring = format_command_help(hkl_commands_for_help) # must be on the class +ub.__doc__ = format_command_help(ub_commands_for_help) + +_virtual_angles = ('theta', 'qaz', 'alpha', 'naz', 'tau', 'psi', 'beta') +hklverbose = Hkl('hklverbose', _scn_group, _dc, _virtual_angles) + + +# Create wavelength scannable +wl = Wavelength( + 'wl', _energy_scannable, settings.energy_scannable_multiplier_to_get_KeV) +if not GDA: + wl.asynchronousMoveTo(1) # Angstrom +_energy_scannable.level = 3 +wl.level = 3 + + +# Create simulated counter timer +ct = SimulatedCrystalCounter('ct', _scn_group, settings.geometry, wl) +ct.level = 10 + + +# Create constraint scannables +def _create_constraint_scannable(con_name, scn_name=None): + if not scn_name: + scn_name = con_name + return DiffractionCalculatorParameter( + scn_name, con_name, _dc.constraint_manager) + +# Detector constraints +def isconstrainable(name): + return not constraint_manager.is_constraint_fixed(name) + +if isconstrainable('delta'): delta_con = _create_constraint_scannable('delta', 'delta_con') +if isconstrainable('gam'): gam_con = _create_constraint_scannable('gam', 'gam_con') +if isconstrainable('qaz'): qaz = _create_constraint_scannable('qaz') +if isconstrainable('naz'): naz = _create_constraint_scannable('naz') + +# Reference constraints +alpha = _create_constraint_scannable('alpha') +beta = _create_constraint_scannable('beta') +psi = _create_constraint_scannable('psi') +a_eq_b = 'a_eq_b' + +# Sample constraints +if isconstrainable('mu'): mu_con = _create_constraint_scannable('mu', 'mu_con') +if isconstrainable('eta'): eta_con = _create_constraint_scannable('eta', 'eta_con') +if isconstrainable('chi'): chi_con = _create_constraint_scannable('chi', 'chi_con') +if isconstrainable('phi'): phi_con = _create_constraint_scannable('phi', 'phi_con') +if isconstrainable('mu') and isconstrainable('gam'): mu_is_gam = 'mu_is_gam' + + +# Cleanup to allow "from gdasupport.you import *" +del DiffractometerScannableGroup, Hkl, SimulatedCrystalCounter +del Wavelength, DiffractionCalculatorParameter + +# Cleanup other cruft +del format_command_help \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/hardware.py b/script/test/diffcalc (copy)/diffcalc/hardware.py new file mode 100644 index 0000000..274feaf --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hardware.py @@ -0,0 +1,382 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from __future__ import absolute_import + +from diffcalc.util import DiffcalcException +from diffcalc import settings + +SMALL = 1e-8 + +from diffcalc.util import command + +__all__ = ['hardware', 'setcut', 'setmin', 'setmax'] + + +def getNameFromScannableOrString(o): + try: # it may be a scannable + return o.getName() + except AttributeError: + return str(o) + + + +@command +def hardware(): + """hardware -- show diffcalc limits and cuts""" + print settings.hardware.repr_sector_limits_and_cuts() # @UndefinedVariable + +@command +def setcut(scannable_or_string=None, val=None): + """setcut {name {val}} -- sets cut angle + """ + if scannable_or_string is None and val is None: + print settings.hardware.repr_sector_limits_and_cuts() # @UndefinedVariable + else: + name = getNameFromScannableOrString(scannable_or_string) + if val is None: + print '%s: %f' % (name, settings.hardware.get_cuts()[name]) # @UndefinedVariable + else: + oldcut = settings.hardware.get_cuts()[name] # @UndefinedVariable + settings.hardware.set_cut(name, float(val)) # @UndefinedVariable + newcut = settings.hardware.get_cuts()[name] # @UndefinedVariable + +@command +def setmin(name=None, val=None): + """setmin {axis {val}} -- set lower limits used by auto sector code (None to clear)""" #@IgnorePep8 + _setMinOrMax(name, val, settings.hardware.set_lower_limit) # @UndefinedVariable + +@command +def setmax(name=None, val=None): + """setmax {name {val}} -- sets upper limits used by auto sector code (None to clear)""" #@IgnorePep8 + _setMinOrMax(name, val, settings.hardware.set_upper_limit) # @UndefinedVariable + +@command +def setrange(name=None, lower=None, upper=None): + """setrange {axis {min} {max}} -- set lower and upper limits used by auto sector code (None to clear)""" #@IgnorePep8 + _setMinOrMax(name, lower, settings.hardware.set_lower_limit) # @UndefinedVariable + _setMinOrMax(name, upper, settings.hardware.set_upper_limit) # @UndefinedVariable + +def _setMinOrMax(name, val, setMethod): + if name is None: + print settings.hardware.repr_sector_limits_and_cuts() # @UndefinedVariable + else: + name = getNameFromScannableOrString(name) + if val is None: + print settings.hardware.repr_sector_limits_and_cuts(name) # @UndefinedVariable + else: + setMethod(name, float(val)) + + +commands_for_help = ['Hardware', + hardware, + setcut, + setmin, + setmax] + + +class HardwareAdapter(object): + + def __init__(self, diffractometerAngleNames, defaultCuts={}, + energyScannableMultiplierToGetKeV=1): + + self._diffractometerAngleNames = diffractometerAngleNames + self._upperLimitDict = {} + self._lowerLimitDict = {} + self._cut_angles = {} + self._configure_cuts(defaultCuts) + self.energyScannableMultiplierToGetKeV = \ + energyScannableMultiplierToGetKeV + self._name = 'base' + + @property + def name(self): + return self._name + + def get_axes_names(self): + return tuple(self._diffractometerAngleNames) + + def get_position(self): + """pos = get_position() -- returns the current physical diffractometer + position as a diffcalc.util object in degrees + """ + raise NotImplementedError() + + def get_wavelength(self): + """wavelength = get_wavelength() -- returns wavelength in Angstroms + """ + return 12.39842 / self.get_energy() + + def get_energy(self): + """energy = get_energy() -- returns energy in kEv """ + raise NotImplementedError() + + def __str__(self): + s = self.name + ":\n" + s += " energy : " + str(self.get_energy()) + " keV\n" + s += " wavelength : " + str(self.get_wavelength()) + " Angstrom\n" + names = self._diffractometerAngleNames + for name, pos in zip(names, self.get_position()): + s += " %s : %r deg\n" % (name, pos) + return s + + def __repr__(self): + return self.__str__() + + def get_position_by_name(self, angleName): + names = list(self._diffractometerAngleNames) + return self.get_position()[names.index(angleName)] + +### Limits ### + + def get_lower_limit(self, name): + '''returns lower limits by axis name. Limit may be None if not set + ''' + if name not in self._diffractometerAngleNames: + raise ValueError("No angle called %s. Try one of: %s" % + (name, self._diffractometerAngleNames)) + return self._lowerLimitDict.get(name) + + def get_upper_limit(self, name): + '''returns upper limit by axis name. Limit may be None if not set + ''' + if name not in self._diffractometerAngleNames: + raise ValueError("No angle called %s. Try one of: %s" % + name, self._diffractometerAngleNames) + return self._upperLimitDict.get(name) + + def set_lower_limit(self, name, value): + """value may be None to remove limit""" + if name not in self._diffractometerAngleNames: + raise ValueError( + "Cannot set lower Diffcalc limit: No angle called %s. Try one " + "of: %s" % (name, self._diffractometerAngleNames)) + if value is None: + try: + del self._lowerLimitDict[name] + except KeyError: + print ("WARNING: There was no lower Diffcalc limit %s set to " + "clear" % name) + else: + self._lowerLimitDict[name] = value + + def set_upper_limit(self, name, value): + """value may be None to remove limit""" + if name not in self._diffractometerAngleNames: + raise ValueError( + "Cannot set upper Diffcalc limit: No angle called %s. Try one " + "of: %s" % (name, self._diffractometerAngleNames)) + if value is None: + try: + del self._upperLimitDict[name] + except KeyError: + print ("WARNING: There was no upper Diffcalc limit %s set to " + "clear" % name) + else: + self._upperLimitDict[name] = value + + def is_position_within_limits(self, positionArray): + """ + where position array is in degrees and cut to be between -180 and 180 + """ + names = self._diffractometerAngleNames + for axis_name, value in zip(names, positionArray): + if not self.is_axis_value_within_limits(axis_name, value): + return False + return True + + def is_axis_value_within_limits(self, axis_name, value): + if axis_name in self._upperLimitDict: + if value > self._upperLimitDict[axis_name]: + return False + if axis_name in self._lowerLimitDict: + if value < self._lowerLimitDict[axis_name]: + return False + return True + + def repr_sector_limits_and_cuts(self, name=None): + if name is None: + s = '' + for name in self.get_axes_names(): + s += self.repr_sector_limits_and_cuts(name) + '\n' + s += "Note: When auto sector/transforms are used,\n " + s += " cuts are applied before checking limits." + return s + # limits: + low = self.get_lower_limit(name) + high = self.get_upper_limit(name) + s = ' ' + if low is not None: + s += "% 6.1f <= " % low + else: + s += ' ' * 10 + s += '%5s' % name + if high is not None: + s += " <= % 6.1f" % high + else: + s += ' ' * 10 + # cuts: + try: + if self.get_cuts()[name] is not None: + s += " (cut: % 6.1f)" % self.get_cuts()[name] + except KeyError: + pass + + return s + +### Cutting Stuff ### + + def _configure_cuts(self, defaultCutsDict): + # 1. Set default cut angles + self._cut_angles = dict.fromkeys(self._diffractometerAngleNames, -180.) + if 'phi' in self._cut_angles: + self._cut_angles['phi'] = 0. + # 2. Overide with user-specified cuts + for name, val in defaultCutsDict.iteritems(): + self.set_cut(name, val) + + def set_cut(self, name, value): + if name in self._cut_angles: + self._cut_angles[name] = value + else: + raise KeyError("Diffractometer has no angle %s. Try: %s." % + (name, self._diffractometerAngleNames)) + + def get_cuts(self): + return self._cut_angles + + def cut_angles(self, positionArray): + '''Assumes each angle in positionArray is between -360 and 360 + ''' + cutArray = [] + names = self._diffractometerAngleNames + for axis_name, value in zip(names, positionArray): + cutArray.append(self.cut_angle(axis_name, value)) + return tuple(cutArray) + + def cut_angle(self, axis_name, value): + cut_angle = self._cut_angles[axis_name] + if cut_angle is None: + return value + return cut_angle_at(cut_angle, value) + + +def cut_angle_at(cut_angle, value): + if (cut_angle == 0 and (abs(value - 360) < SMALL) or + (abs(value + 360) < SMALL) or + (abs(value) < SMALL)): + value = 0. + if value < (cut_angle - SMALL): + return value + 360. + elif value >= cut_angle + 360. + SMALL: + return value - 360. + else: + return value + + +class DummyHardwareAdapter(HardwareAdapter): + + def __init__(self, diffractometerAngleNames): + super(self.__class__, self).__init__(diffractometerAngleNames) +# HardwareAdapter.__init__(self, diffractometerAngleNames) + + self._position = [0.] * len(diffractometerAngleNames) + self._wavelength = 1. + self.energyScannableMultiplierToGetKeV = 1 + self._name = "Dummy" + +# Required methods + + def get_position(self): + """ + pos = getDiffractometerPosition() -- returns the current physical + diffractometer position as a list in degrees + """ + return self._position + + def _set_position(self, pos): + assert len(pos) == len(self.get_axes_names()), \ + "Wrong length of input list" + self._position = pos + + position = property(get_position, _set_position) + + def get_energy(self): + """energy = get_energy() -- returns energy in kEv """ + if self._wavelength is None: + raise DiffcalcException( + "Energy or wavelength have not been set") + return (12.39842 / + (self._wavelength * self.energyScannableMultiplierToGetKeV)) + + def _set_energy(self, energy): + self._wavelength = 12.39842 / energy + + energy = property(get_energy, _set_energy) + + def get_wavelength(self): + """wavelength = get_wavelength() -- returns wavelength in Angstroms""" + if self._wavelength is None: + raise DiffcalcException( + "Energy or wavelength have not been set") + return self._wavelength + + def _set_wavelength(self, wavelength): + self._wavelength = wavelength + + wavelength = property(get_wavelength, _set_wavelength) + + +class ScannableHardwareAdapter(HardwareAdapter): + + def __init__(self, diffractometerScannable, energyScannable, + energyScannableMultiplierToGetKeV=1): + input_names = diffractometerScannable.getInputNames() + super(self.__class__, self).__init__(input_names) +# HardwareAdapter.__init__(self, input_names) + self.diffhw = diffractometerScannable + self.energyhw = energyScannable + self.energyScannableMultiplierToGetKeV = \ + energyScannableMultiplierToGetKeV + self._name = "ScannableHarwdareMonitor" + +# Required methods + + def get_position(self): + """ + pos = getDiffractometerPosition() -- returns the current physical + diffractometer position as a list in degrees + """ + return self.diffhw.getPosition() + + def get_energy(self): + """energy = get_energy() -- returns energy in kEv (NOT eV!) """ + multiplier = self.energyScannableMultiplierToGetKeV + energy = self.energyhw.getPosition() * multiplier + if energy is None: + raise DiffcalcException("Energy has not been set") + return energy + + def get_wavelength(self): + """wavelength = get_wavelength() -- returns wavelength in Angstroms""" + energy = self.get_energy() + return 12.39842 / energy + + @property + def name(self): + return self.diffhw.getName() diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/__init__.py b/script/test/diffcalc (copy)/diffcalc/hkl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/calcbase.py b/script/test/diffcalc (copy)/diffcalc/hkl/calcbase.py new file mode 100644 index 0000000..c4db5f3 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/calcbase.py @@ -0,0 +1,155 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi + +from diffcalc.util import DiffcalcException, differ + +TORAD = pi / 180 +TODEG = 180 / pi + + +class HklCalculatorBase(object): + + def __init__(self, ubcalc, geometry, hardware, + raiseExceptionsIfAnglesDoNotMapBackToHkl=False): + + self._ubcalc = ubcalc # to get the UBMatrix, tau and sigma + self._geometry = geometry # to access information about the + # diffractometer geometry and mode_selector + self._hardware = hardware # Used for tracking parameters only + self.raiseExceptionsIfAnglesDoNotMapBackToHkl = \ + raiseExceptionsIfAnglesDoNotMapBackToHkl + + def anglesToHkl(self, pos, wavelength): + """ + Return hkl tuple and dictionary of all virtual angles in degrees from + Position in degrees and wavelength in Angstroms. + """ + + h, k, l = self._anglesToHkl(pos.inRadians(), wavelength) + paramDict = self.anglesToVirtualAngles(pos, wavelength) + return ((h, k, l), paramDict) + + def anglesToVirtualAngles(self, pos, wavelength): + """ + Return dictionary of all virtual angles in degrees from Position object + in degrees and wavelength in Angstroms. + """ + anglesDict = self._anglesToVirtualAngles(pos.inRadians(), wavelength) + for name in anglesDict: + anglesDict[name] = anglesDict[name] * TODEG + return anglesDict + + def hklToAngles(self, h, k, l, wavelength): + """ + Return verified Position and all virtual angles in degrees from + h, k & l and wavelength in Angstroms. + + The calculated Position is verified by checking that it maps back using + anglesToHkl() to the requested hkl value. + + Those virtual angles fixed or generated while calculating the position + are verified by by checking that they map back using + anglesToVirtualAngles to the virtual angles for the given position. + + Throws a DiffcalcException if either check fails and + raiseExceptionsIfAnglesDoNotMapBackToHkl is True, otherwise displays a + warning. + """ + + # Update tracked parameters. During this calculation parameter values + # will be read directly from self._parameters instead of via + # self.getParameter which would trigger another potentially time-costly + # position update. + self.parameter_manager.update_tracked() + + pos, virtualAngles = self._hklToAngles(h, k, l, wavelength) # in rad + + # to degrees: + pos.changeToDegrees() + + for key, val in virtualAngles.items(): + if val is not None: + virtualAngles[key] = val * TODEG + + self._verify_pos_map_to_hkl(h, k, l, wavelength, pos) + + virtualAnglesReadback = self._verify_virtual_angles(h, k, l, wavelength, pos, virtualAngles) + + return pos, virtualAnglesReadback + + def _verify_pos_map_to_hkl(self, h, k, l, wavelength, pos): + hkl, _ = self.anglesToHkl(pos, wavelength) + e = 0.001 + if ((abs(hkl[0] - h) > e) or (abs(hkl[1] - k) > e) or + (abs(hkl[2] - l) > e)): + s = "ERROR: The angles calculated for hkl=(%f,%f,%f) were %s.\n" % (h, k, l, str(pos)) + s += "Converting these angles back to hkl resulted in hkl="\ + "(%f,%f,%f)" % (hkl[0], hkl[1], hkl[2]) + if self.raiseExceptionsIfAnglesDoNotMapBackToHkl: + raise DiffcalcException(s) + else: + print s + + def _verify_virtual_angles(self, h, k, l, wavelength, pos, virtualAngles): + # Check that the virtual angles calculated/fixed during the hklToAngles + # those read back from pos using anglesToVirtualAngles + virtualAnglesReadback = self.anglesToVirtualAngles(pos, wavelength) + for key, val in virtualAngles.items(): + if val != None: # Some values calculated in some mode_selector + r = virtualAnglesReadback[key] + if ((differ(val, r, .00001) and differ(val, r + 360, .00001) and differ(val, r - 360, .00001))): + s = "ERROR: The angles calculated for hkl=(%f,%f,%f) with"\ + " mode=%s were %s.\n" % (h, k, l, self.repr_mode(), str(pos)) + s += "During verification the virtual angle %s resulting "\ + "from (or set for) this calculation of %f" % (key, val) + s += "did not match that calculated by "\ + "anglesToVirtualAngles of %f" % virtualAnglesReadback[key] + if self.raiseExceptionsIfAnglesDoNotMapBackToHkl: + raise DiffcalcException(s) + else: + print s + + return virtualAnglesReadback + + def repr_mode(self): + pass + +### Collect all math access to context here + + def _getUBMatrix(self): + return self._ubcalc.UB + + def _getMode(self): + return self.mode_selector.getMode() + + def _getSigma(self): + return self._ubcalc.sigma + + def _getTau(self): + return self._ubcalc.tau + + def _getParameter(self, name): + # Does not use context.getParameter as this will trigger a costly + # parameter collection + pm = self.parameter_manager + return pm.getParameterWithoutUpdatingTrackedParemeters(name) + + def _getGammaParameterName(self): + return self._gammaParameterName diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/common.py b/script/test/diffcalc (copy)/diffcalc/hkl/common.py new file mode 100644 index 0000000..0b3c13a --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/common.py @@ -0,0 +1,55 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### +from diffcalc.util import allnum + +def getNameFromScannableOrString(o): + try: # it may be a scannable + return o.getName() + except AttributeError: + return str(o) + raise TypeError() + + +class DummyParameterManager(object): + + def getParameterDict(self): + return {} + + def _setParameter(self, name, value): + raise KeyError(name) + + def _getParameter(self, name): + raise KeyError(name) + + def update_tracked(self): + pass + + +def sim(self, scn, hkl): + """sim hkl scn -- simulates moving scannable (not all) + """ + if not isinstance(hkl, (tuple, list)): + raise TypeError + + if not allnum(hkl): + raise TypeError() + + try: + print scn.simulateMoveTo(hkl) + except AttributeError: + raise TypeError("The first argument does not support simulated moves") diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/__init__.py b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/calc.py b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/calc.py new file mode 100644 index 0000000..215e31a --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/calc.py @@ -0,0 +1,846 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi, asin, acos, sin, cos, sqrt, atan2, fabs, atan + +try: + from numpy import matrix + from numpy.linalg import norm +except ImportError: + from numjy import matrix + from numjy.linalg import norm + +from diffcalc.hkl.calcbase import HklCalculatorBase +from diffcalc.hkl.vlieg.transform import TransformCInRadians +from diffcalc.util import dot3, cross3, bound, differ +from diffcalc.hkl.vlieg.geometry import createVliegMatrices, \ + createVliegsPsiTransformationMatrix, \ + createVliegsSurfaceTransformationMatrices, calcPHI +from diffcalc.hkl.vlieg.geometry import VliegPosition +from diffcalc.hkl.vlieg.constraints import VliegParameterManager +from diffcalc.hkl.vlieg.constraints import ModeSelector +from diffcalc.ub.calc import PaperSpecificUbCalcStrategy + + +TORAD = pi / 180 +TODEG = 180 / pi +transformC = TransformCInRadians() + + +PREFER_POSITIVE_CHI_SOLUTIONS = True + +I = matrix('1 0 0; 0 1 0; 0 0 1') +y = matrix('0; 1; 0') + + +def check(condition, ErrorOrStringOrCallable, *args): + """ + fail = check(condition, ErrorOrString) -- if condition is false raises the + Exception passed in, or creates one from a string. If a callable function + is passed in this is called with any args specified and the thing returns + false. + """ + # TODO: Remove (really nasty) check function + if condition == False: + if callable(ErrorOrStringOrCallable): + ErrorOrStringOrCallable(*args) + return False + elif isinstance(ErrorOrStringOrCallable, str): + raise Exception(ErrorOrStringOrCallable) + else: # assume input is an exception + raise ErrorOrStringOrCallable + return True + + +def sign(x): + if x < 0: + return -1 + else: + return 1 + + +def vliegAnglesToHkl(pos, wavelength, UBMatrix): + """ + Returns hkl indices from pos object in radians. + """ + wavevector = 2 * pi / wavelength + + # Create transformation matrices + [ALPHA, DELTA, GAMMA, OMEGA, CHI, PHI] = createVliegMatrices( + pos.alpha, pos.delta, pos.gamma, pos.omega, pos.chi, pos.phi) + + # Create the plane normal vector in the alpha axis coordinate frame + qa = ((DELTA * GAMMA) - ALPHA.I) * matrix([[0], [wavevector], [0]]) + + # Transform the plane normal vector from the alpha frame to reciprical + # lattice frame. + hkl = UBMatrix.I * PHI.I * CHI.I * OMEGA.I * qa + + return hkl[0, 0], hkl[1, 0], hkl[2, 0] + + +class VliegUbCalcStrategy(PaperSpecificUbCalcStrategy): + + def calculate_q_phi(self, pos): + + [ALPHA, DELTA, GAMMA, OMEGA, CHI, PHI] = createVliegMatrices( + pos.alpha, pos.delta, pos.gamma, pos.omega, pos.chi, pos.phi) + + u1a = (DELTA * GAMMA - ALPHA.I) * y + u1p = PHI.I * CHI.I * OMEGA.I * u1a + return u1p + + +class VliegHklCalculator(HklCalculatorBase): + + def __init__(self, ubcalc, geometry, hardware, + raiseExceptionsIfAnglesDoNotMapBackToHkl=True): + r = raiseExceptionsIfAnglesDoNotMapBackToHkl + HklCalculatorBase.__init__(self, ubcalc, geometry, hardware, + raiseExceptionsIfAnglesDoNotMapBackToHkl=r) + self._gammaParameterName = ({'arm': 'gamma', 'base': 'oopgamma'} + [self._geometry.gamma_location]) + self.mode_selector = ModeSelector(self._geometry, None, + self._gammaParameterName) + self.parameter_manager = VliegParameterManager( + self._geometry, self._hardware, self.mode_selector, + self._gammaParameterName) + self.mode_selector.setParameterManager(self.parameter_manager) + + def __str__(self): + # should list paramemeters and indicate which are used in selected mode + result = "Available mode_selector:\n" + result += self.mode_selector.reportAvailableModes() + result += '\nCurrent mode:\n' + result += self.mode_selector.reportCurrentMode() + result += '\n\nParameters:\n' + result += self.parameter_manager.reportAllParameters() + return result + + def _anglesToHkl(self, pos, wavelength): + """ + Return hkl tuple from VliegPosition in radians and wavelength in + Angstroms. + """ + return vliegAnglesToHkl(pos, wavelength, self._getUBMatrix()) + + def _anglesToVirtualAngles(self, pos, wavelength): + """ + Return dictionary of all virtual angles in radians from VliegPosition + object win radians and wavelength in Angstroms. The virtual angles are: + Bin, Bout, azimuth and 2theta. + """ + + # Create transformation matrices + [ALPHA, DELTA, GAMMA, OMEGA, CHI, PHI] = createVliegMatrices( + pos.alpha, pos.delta, pos.gamma, pos.omega, pos.chi, pos.phi) + [SIGMA, TAU] = createVliegsSurfaceTransformationMatrices( + self._getSigma() * TORAD, self._getTau() * TORAD) + + S = TAU * SIGMA + y_vector = matrix([[0], [1], [0]]) + + # Calculate Bin from equation 15: + surfacenormal_alpha = OMEGA * CHI * PHI * S * matrix([[0], [0], [1]]) + incoming_alpha = ALPHA.I * y_vector + minusSinBetaIn = dot3(surfacenormal_alpha, incoming_alpha) + Bin = asin(bound(-minusSinBetaIn)) + + # Calculate Bout from equation 16: + # surfacenormal_alpha has just ben calculated + outgoing_alpha = DELTA * GAMMA * y_vector + sinBetaOut = dot3(surfacenormal_alpha, outgoing_alpha) + Bout = asin(bound(sinBetaOut)) + + # Calculate 2theta from equation 25: + + cosTwoTheta = dot3(ALPHA * DELTA * GAMMA * y_vector, y_vector) + twotheta = acos(bound(cosTwoTheta)) + psi = self._anglesToPsi(pos, wavelength) + + return {'Bin': Bin, 'Bout': Bout, 'azimuth': psi, '2theta': twotheta} + + def _hklToAngles(self, h, k, l, wavelength): + """ + Return VliegPosition and virtual angles in radians from h, k & l and + wavelength in Angstroms. The virtual angles are those fixed or + generated while calculating the position: Bin, Bout and 2theta; and + azimuth in four and five circle modes. + """ + + if self._getMode().group in ("fourc", "fivecFixedGamma", + "fivecFixedAlpha"): + return self._hklToAnglesFourAndFiveCirclesModes(h, k, l, + wavelength) + elif self._getMode().group == "zaxis": + return self._hklToAnglesZaxisModes(h, k, l, wavelength) + else: + raise RuntimeError( + 'The current mode (%s) has an unrecognised group: %s.' + % (self._getMode().name, self._getMode().group)) + + def _hklToAnglesFourAndFiveCirclesModes(self, h, k, l, wavelength): + """ + Return VliegPosition and virtual angles in radians from h, k & l and + wavelength in Angstrom for four and five circle modes. The virtual + angles are those fixed or generated while calculating the position: + Bin, Bout, 2theta and azimuth. + """ + + # Results in radians during calculations, returned in degreess + pos = VliegPosition(None, None, None, None, None, None) + + # Normalise hkl + wavevector = 2 * pi / wavelength + hklNorm = matrix([[h], [k], [l]]) / wavevector + + # Compute hkl in phi axis coordinate frame + hklPhiNorm = self._getUBMatrix() * hklNorm + + # Determine Bin and Bout + if self._getMode().name == '4cPhi': + Bin = Bout = None + else: + Bin, Bout = self._determineBinAndBoutInFourAndFiveCirclesModes( + hklNorm) + + # Determine alpha and gamma + if self._getMode().group == 'fourc': + pos.alpha, pos.gamma = \ + self._determineAlphaAndGammaForFourCircleModes(hklPhiNorm) + else: + pos.alpha, pos.gamma = \ + self._determineAlphaAndGammaForFiveCircleModes(Bin, hklPhiNorm) + if pos.alpha < -pi: + pos.alpha += 2 * pi + if pos.alpha > pi: + pos.alpha -= 2 * pi + + # Determine delta + (pos.delta, twotheta) = self._determineDelta(hklPhiNorm, pos.alpha, + pos.gamma) + + # Determine omega, chi & phi + pos.omega, pos.chi, pos.phi, psi = \ + self._determineSampleAnglesInFourAndFiveCircleModes( + hklPhiNorm, pos.alpha, pos.delta, pos.gamma, Bin) + # (psi will be None in fixed phi mode) + + # Ensure that by default omega is between -90 and 90, by possibly + # transforming the sample angles + if self._getMode().name != '4cPhi': # not in fixed-phi mode + if pos.omega < -pi / 2 or pos.omega > pi / 2: + pos = transformC.transform(pos) + + # Gather up the virtual angles calculated along the way... + # -pi pi: + psi -= 2 * pi + if psi < (-1 * pi): + psi += 2 * pi + + v = {'2theta': twotheta, 'Bin': Bin, 'Bout': Bout, 'azimuth': psi} + return pos, v + + def _hklToAnglesZaxisModes(self, h, k, l, wavelength): + """ + Return VliegPosition and virtual angles in radians from h, k & l and + wavelength in Angstroms for z-axis modes. The virtual angles are those + fixed or generated while calculating the position: Bin, Bout, and + 2theta. + """ + # Section 6: + + # Results in radians during calculations, returned in degreess + pos = VliegPosition(None, None, None, None, None, None) + + # Normalise hkl + wavevector = 2 * pi / wavelength + hkl = matrix([[h], [k], [l]]) + hklNorm = hkl * (1.0 / wavevector) + + # Compute hkl in phi axis coordinate frame + hklPhi = self._getUBMatrix() * hkl + hklPhiNorm = self._getUBMatrix() * hklNorm + + # Determine Chi and Phi (Equation 29): + pos.phi = -self._getTau() * TORAD + pos.chi = -self._getSigma() * TORAD + + # Equation 30: + [ALPHA, DELTA, GAMMA, OMEGA, CHI, PHI] = createVliegMatrices( + None, None, None, None, pos.chi, pos.phi) + del ALPHA, DELTA, GAMMA, OMEGA + Hw = CHI * PHI * hklPhi + + # Determine Bin and Bout: + (Bin, Bout) = self._determineBinAndBoutInZaxisModes( + Hw[2, 0] / wavevector) + + # Determine Alpha and Gamma (Equation 32): + pos.alpha = Bin + pos.gamma = Bout + + # Determine Delta: + (pos.delta, twotheta) = self._determineDelta(hklPhiNorm, pos.alpha, + pos.gamma) + + # Determine Omega: + delta = pos.delta + gamma = pos.gamma + d1 = (Hw[1, 0] * sin(delta) * cos(gamma) - Hw[0, 0] * + (cos(delta) * cos(gamma) - cos(pos.alpha))) + d2 = (Hw[0, 0] * sin(delta) * cos(gamma) + Hw[1, 0] * + (cos(delta) * cos(gamma) - cos(pos.alpha))) + + if fabs(d2) < 1e-30: + pos.omega = sign(d1) * sign(d2) * pi / 2.0 + else: + pos.omega = atan2(d1, d2) + + # Gather up the virtual angles calculated along the way + return pos, {'2theta': twotheta, 'Bin': Bin, 'Bout': Bout} + +### + + def _determineBinAndBoutInFourAndFiveCirclesModes(self, hklNorm): + """(Bin, Bout) = _determineBinAndBoutInFourAndFiveCirclesModes()""" + BinModes = ('4cBin', '5cgBin', '5caBin') + BoutModes = ('4cBout', '5cgBout', '5caBout') + BeqModes = ('4cBeq', '5cgBeq', '5caBeq') + azimuthModes = ('4cAzimuth') + fixedBusingAndLeviWmodes = ('4cFixedw') + + # Calculate RHS of equation 20 + # RHS (1/K)(S^-1*U*B*H)_3 where H/K = hklNorm + UB = self._getUBMatrix() + [SIGMA, TAU] = createVliegsSurfaceTransformationMatrices( + self._getSigma() * TORAD, self._getTau() * TORAD) + #S = SIGMA * TAU + S = TAU * SIGMA + RHS = (S.I * UB * hklNorm)[2, 0] + + if self._getMode().name in BinModes: + Bin = self._getParameter('betain') + check(Bin != None, "The parameter betain must be set for mode %s" % + self._getMode().name) + Bin = Bin * TORAD + sinBout = RHS - sin(Bin) + check(fabs(sinBout) <= 1, "Could not compute Bout") + Bout = asin(sinBout) + + elif self._getMode().name in BoutModes: + Bout = self._getParameter('betaout') + check(Bout != None, "The parameter Bout must be set for mode %s" % + self._getMode().name) + Bout = Bout * TORAD + sinBin = RHS - sin(Bout) + check(fabs(sinBin) <= 1, "Could not compute Bin") + Bin = asin(sinBin) + + elif self._getMode().name in BeqModes: + sinBeq = RHS / 2 + check(fabs(sinBeq) <= 1, "Could not compute Bin=Bout") + Bin = Bout = asin(sinBeq) + + elif self._getMode().name in azimuthModes: + azimuth = self._getParameter('azimuth') + check(azimuth != None, "The parameter azimuth must be set for " + "mode %s" % self._getMode().name) + del azimuth + # TODO: codeit + raise NotImplementedError() + + elif self._getMode().name in fixedBusingAndLeviWmodes: + bandlomega = self._getParameter('blw') + check(bandlomega != None, "The parameter abandlomega must be set " + "for mode %s" % self._getMode().name) + del bandlomega + # TODO: codeit + raise NotImplementedError() + else: + raise RuntimeError("AngleCalculator does not know how to handle " + "mode %s" % self._getMode().name) + + return (Bin, Bout) + + def _determineBinAndBoutInZaxisModes(self, Hw3OverK): + """(Bin, Bout) = _determineBinAndBoutInZaxisModes(HwOverK)""" + BinModes = ('6czBin') + BoutModes = ('6czBout') + BeqModes = ('6czBeq') + + if self._getMode().name in BinModes: + Bin = self._getParameter('betain') + check(Bin != None, "The parameter betain must be set for mode %s" % + self._getMode().name) + Bin = Bin * TORAD + # Equation 32a: + Bout = asin(Hw3OverK - sin(Bin)) + + elif self._getMode().name in BoutModes: + Bout = self._getParameter('betaout') + check(Bout != None, "The parameter Bout must be set for mode %s" % + self._getMode().name) + Bout = Bout * TORAD + # Equation 32b: + Bin = asin(Hw3OverK - sin(Bout)) + + elif self._getMode().name in BeqModes: + # Equation 32c: + Bin = Bout = asin(Hw3OverK / 2) + + return (Bin, Bout) + +### + + def _determineAlphaAndGammaForFourCircleModes(self, hklPhiNorm): + + if self._getMode().group == 'fourc': + alpha = self._getParameter('alpha') * TORAD + gamma = self._getParameter(self._getGammaParameterName()) * TORAD + check(alpha != None, "alpha parameter must be set in fourc modes") + check(gamma != None, "gamma parameter must be set in fourc modes") + return alpha, gamma + else: + raise RuntimeError( + "determineAlphaAndGammaForFourCirclesModes() " + "is not appropriate for %s modes" % self._getMode().group) + + def _determineAlphaAndGammaForFiveCircleModes(self, Bin, hklPhiNorm): + + ## Solve equation 34 for one possible Y, Yo + # Calculate surface normal in phi frame + [SIGMA, TAU] = createVliegsSurfaceTransformationMatrices( + self._getSigma() * TORAD, self._getTau() * TORAD) + S = TAU * SIGMA + surfaceNormalPhi = S * matrix([[0], [0], [1]]) + # Compute beta in vector + BetaVector = matrix([[0], [-sin(Bin)], [cos(Bin)]]) + # Find Yo + Yo = self._findMatrixToTransformAIntoB(surfaceNormalPhi, BetaVector) + + ## Calculate Hv from equation 39 + Z = matrix([[1, 0, 0], + [0, cos(Bin), sin(Bin)], + [0, -sin(Bin), cos(Bin)]]) + Hv = Z * Yo * hklPhiNorm + # Fixed gamma: + if self._getMode().group == 'fivecFixedGamma': + gamma = self._getParameter(self._getGammaParameterName()) + check(gamma != None, + "gamma parameter must be set in fivecFixedGamma modes") + gamma = gamma * TORAD + H2 = (hklPhiNorm[0, 0] ** 2 + hklPhiNorm[1, 0] ** 2 + + hklPhiNorm[2, 0] ** 2) + a = -(0.5 * H2 * sin(Bin) - Hv[2, 0]) + b = -(1.0 - 0.5 * H2) * cos(Bin) + c = cos(Bin) * sin(gamma) + check((b * b + a * a - c * c) >= 0, 'Could not solve for alpha') + alpha = 2 * atan2(-(b + sqrt(b * b + a * a - c * c)), -(a + c)) + + # Fixed Alpha: + elif self._getMode().group == 'fivecFixedAlpha': + alpha = self._getParameter('alpha') + check(alpha != None, + "alpha parameter must be set in fivecFixedAlpha modes") + alpha = alpha * TORAD + H2 = (hklPhiNorm[0, 0] ** 2 + hklPhiNorm[1, 0] ** 2 + + hklPhiNorm[2, 0] ** 2) + t0 = ((2 * cos(alpha) * Hv[2, 0] - sin(Bin) * cos(alpha) * H2 + + cos(Bin) * sin(alpha) * H2 - 2 * cos(Bin) * sin(alpha)) / + (cos(Bin) * 2.0)) + check(abs(t0) <= 1, "Cannot compute gamma: sin(gamma)>1") + gamma = asin(t0) + else: + raise RuntimeError( + "determineAlphaAndGammaInFiveCirclesModes() is not " + "appropriate for %s modes" % self._getMode().group) + + return (alpha, gamma) + +### + + def _determineDelta(self, hklPhiNorm, alpha, gamma): + """ + (delta, twotheta) = _determineDelta(hklPhiNorm, alpha, gamma) -- + computes delta for all modes. Also returns twotheta for sanity + checking. hklPhiNorm is a 3X1 matrix. + + alpha, gamma & delta - in radians. + h k & l normalised to wavevector and in phi axis coordinates + """ + h = hklPhiNorm[0, 0] + k = hklPhiNorm[1, 0] + l = hklPhiNorm[2, 0] + # See Vlieg section 5 (with K=1) + cosdelta = ((1 + sin(gamma) * sin(alpha) - (h * h + k * k + l * l) / 2) + / (cos(gamma) * cos(alpha))) + costwotheta = (cos(alpha) * cos(gamma) * bound(cosdelta) - + sin(alpha) * sin(gamma)) + return (acos(bound(cosdelta)), acos(bound(costwotheta))) + + def _determineSampleAnglesInFourAndFiveCircleModes(self, hklPhiNorm, alpha, + delta, gamma, Bin): + """ + (omega, chi, phi, psi)=determineNonZAxisSampleAngles(hklPhiNorm, alpha, + delta, gamma, sigma, tau) where hkl has been normalised by the + wavevector and is in the phi Axis coordinate frame. All angles in + radians. hklPhiNorm is a 3X1 matrix + """ + + def equation49through59(psi): + # equation 49 R = (D^-1)*PI*D*Ro + PSI = createVliegsPsiTransformationMatrix(psi) + R = D.I * PSI * D * Ro + + # eq 57: extract omega from R + if abs(R[0, 2]) < 1e-20: + omega = -sign(R[1, 2]) * sign(R[0, 2]) * pi / 2 + else: + omega = -atan2(R[1, 2], R[0, 2]) + + # eq 58: extract chi from R + sinchi = sqrt(pow(R[0, 2], 2) + pow(R[1, 2], 2)) + sinchi = bound(sinchi) + check(abs(sinchi) <= 1, 'could not compute chi') + # (there are two roots to this equation, but only the first is also + # a solution to R33=cos(chi)) + chi = asin(sinchi) + + # eq 59: extract phi from R + if abs(R[2, 0]) < 1e-20: + phi = sign(R[2, 1]) * sign(R[2, 1]) * pi / 2 + else: + phi = atan2(-R[2, 1], -R[2, 0]) + return omega, chi, phi + + def checkSolution(omega, chi, phi): + _, _, _, OMEGA, CHI, PHI = createVliegMatrices( + None, None, None, omega, chi, phi) + R = OMEGA * CHI * PHI + RtimesH_phi = R * H_phi + print ("R*H_phi=%s, Q_alpha=%s" % + (R * H_phi.tolist(), Q_alpha.tolist())) + return not differ(RtimesH_phi, Q_alpha, .0001) + + # Using Vlieg section 7.2 + + # Needed througout: + [ALPHA, DELTA, GAMMA, _, _, _] = createVliegMatrices( + alpha, delta, gamma, None, None, None) + + ## Find Ro, one possible solution to equation 46: R*H_phi=Q_alpha + + # Normalise hklPhiNorm (As it is currently normalised only to the + # wavevector) + normh = norm(hklPhiNorm) + check(normh >= 1e-10, "reciprical lattice vector too close to zero") + H_phi = hklPhiNorm * (1 / normh) + + # Create Q_alpha from equation 47, (it comes normalised) + Q_alpha = ((DELTA * GAMMA) - ALPHA.I) * matrix([[0], [1], [0]]) + Q_alpha = Q_alpha * (1 / norm(Q_alpha)) + + if self._getMode().name == '4cPhi': + ### Use the fixed value of phi as the final constraint ### + phi = self._getParameter('phi') * TORAD + PHI = calcPHI(phi) + H_chi = PHI * H_phi + omega, chi = _findOmegaAndChiToRotateHchiIntoQalpha(H_chi, Q_alpha) + return (omega, chi, phi, None) # psi = None as not calculated + else: + ### Use Bin as the final constraint ### + + # Find a solution Ro to Ro*H_phi=Q_alpha + Ro = self._findMatrixToTransformAIntoB(H_phi, Q_alpha) + + ## equation 50: Find a solution D to D*Q=norm(Q)*[[1],[0],[0]]) + D = self._findMatrixToTransformAIntoB( + Q_alpha, matrix([[1], [0], [0]])) + + ## Find psi and create PSI + + # eq 54: compute u=D*Ro*S*[[0],[0],[1]], the surface normal in + # psi frame + [SIGMA, TAU] = createVliegsSurfaceTransformationMatrices( + self._getSigma() * TORAD, self._getTau() * TORAD) + S = TAU * SIGMA + [u1], [u2], [u3] = (D * Ro * S * matrix([[0], [0], [1]])).tolist() + # TODO: If u points along 100, then any psi is a solution. Choose 0 + if not differ([u1, u2, u3], [1, 0, 0], 1e-9): + psi = 0 + omega, chi, phi = equation49through59(psi) + else: + # equation 53: V=A*(D^-1) + V = ALPHA * D.I + v21 = V[1, 0] + v22 = V[1, 1] + v23 = V[1, 2] + # equation 55 + a = v22 * u2 + v23 * u3 + b = v22 * u3 - v23 * u2 + c = -sin(Bin) - v21 * u1 # TODO: changed sign from paper + + # equation 44 + # Try first root: + def myatan2(y, x): + if abs(x) < 1e-20 and abs(y) < 1e-20: + return pi / 2 + else: + return atan2(y, x) + psi = 2 * myatan2(-(b - sqrt(b * b + a * a - c * c)), -(a + c)) + #psi = -acos(c/sqrt(a*a+b*b))+atan2(b,a)# -2*pi + omega, chi, phi = equation49through59(psi) + + # if u points along z axis, the psi could have been either 0 or 180 + if (not differ([u1, u2, u3], [0, 0, 1], 1e-9) and + abs(psi - pi) < 1e-10): + # Choose 0 to match that read up by angles-to-virtual-angles + psi = 0. + # if u points a long + return (omega, chi, phi, psi) + + def _anglesToPsi(self, pos, wavelength): + """ + pos assumed in radians. -180<= psi <= 180 + """ + # Using Vlieg section 7.2 + + # Needed througout: + [ALPHA, DELTA, GAMMA, OMEGA, CHI, PHI] = createVliegMatrices( + pos.alpha, pos.delta, pos.gamma, pos.omega, pos.chi, pos.phi) + + # Solve equation 49 for psi, the rotation of the a reference solution + # about Qalpha or H_phi## + + # Find Ro, the reference solution to equation 46: R*H_phi=Q_alpha + + # Create Q_alpha from equation 47, (it comes normalised) + Q_alpha = ((DELTA * GAMMA) - ALPHA.I) * matrix([[0], [1], [0]]) + Q_alpha = Q_alpha * (1 / norm(Q_alpha)) + + # Finh H_phi + h, k, l = self._anglesToHkl(pos, wavelength) + H_phi = self._getUBMatrix() * matrix([[h], [k], [l]]) + normh = norm(H_phi) + check(normh >= 1e-10, "reciprical lattice vector too close to zero") + H_phi = H_phi * (1 / normh) + + # Find a solution Ro to Ro*H_phi=Q_alpha + # This the reference solution with zero azimuth (psi) + Ro = self._findMatrixToTransformAIntoB(H_phi, Q_alpha) + + # equation 48: + R = OMEGA * CHI * PHI + + ## equation 50: Find a solution D to D*Q=norm(Q)*[[1],[0],[0]]) + D = self._findMatrixToTransformAIntoB(Q_alpha, matrix([[1], [0], [0]])) + + # solve equation 49 for psi + # D*R = PSI*D*Ro + # D*R*(D*Ro)^-1 = PSI + PSI = D * R * ((D * Ro).I) + + # Find psi within PSI as defined in equation 51 + PSI_23 = PSI[1, 2] + PSI_33 = PSI[2, 2] + psi = atan2(PSI_23, PSI_33) + + #print "PSI: ", PSI.tolist() + return psi + + def _findMatrixToTransformAIntoB(self, a, b): + """ + Finds a particular matrix Mo that transforms the unit vector a into the + unit vector b. Thats is it finds Mo Mo*a=b. a and b 3x1 matrixes and Mo + is a 3x3 matrix. + + Throws an exception if this is not possible. + """ + # Maths from the appendix of "Angle caluculations + # for a 5-circle diffractometer used for surface X-ray diffraction", + # E. Vlieg, J.F. van der Veen, J.E. Macdonald and M. Miller, J. of + # Applied Cryst. 20 (1987) 330. + # - courtesy of Elias Vlieg again + + # equation A2: compute angle xi between vectors a and b + cosxi = dot3(a, b) + try: + cosxi = bound(cosxi) + except ValueError: + raise Exception("Could not compute cos(xi), vectors a=%f and b=%f " + "must be of unit length" % (norm(a), norm(b))) + xi = acos(cosxi) + + # Mo is identity matrix if xi zero (math below would blow up) + if abs(xi) < 1e-10: + return I + + # equation A3: c=cross(a,b)/sin(xi) + c = cross3(a, b) * (1 / sin(xi)) + + # equation A4: find D matrix that transforms a into the frame + # x = a; y = c x a; z = c. */ + a1 = a[0, 0] + a2 = a[1, 0] + a3 = a[2, 0] + c1 = c[0, 0] + c2 = c[1, 0] + c3 = c[2, 0] + D = matrix([[a1, a2, a3], + [c2 * a3 - c3 * a2, c3 * a1 - c1 * a3, c1 * a2 - c2 * a1], + [c1, c2, c3]]) + + # equation A5: create Xi to rotate by xi about z-axis + XI = matrix([[cos(xi), -sin(xi), 0], + [sin(xi), cos(xi), 0], + [0, 0, 1]]) + + # eq A6: compute Mo + return D.I * XI * D + + +def _findOmegaAndChiToRotateHchiIntoQalpha(h_chi, q_alpha): + """ + (omega, chi) = _findOmegaAndChiToRotateHchiIntoQalpha(H_chi, Q_alpha) + + Solves for omega and chi in OMEGA*CHI*h_chi = q_alpha where h_chi and + q_alpha are 3x1 matrices with unit length. Omega and chi are returned in + radians. + + Throws an exception if this is not possible. + """ + + def solve(a, b, c): + """ + x1,x2 = solve(a , b, c) + solves for the two solutions to x in equations of the form + a*sin(x) + b*cos(x) = c + by using the trigonometric identity + a*sin(x) + b*cos(x) = a*sin(x)+b*cos(x)=sqrt(a**2+b**2)-sin(x+p) + where + p = atan(b/a) + {0 if a>=0 + {pi if a<0 + """ + if a == 0: + p = pi / 2 if b >= 0 else - pi / 2 + else: + p = atan(b / a) + if a < 0: + p = p + pi + guts = c / sqrt(a ** 2 + b ** 2) + if guts < -1: + guts = -1 + elif guts > 1: + guts = 1 + left1 = asin(guts) + left2 = pi - left1 + return (left1 - p, left2 - p) + + def ne(a, b): + """ + shifts a and b in between -pi and pi and tests for near equality + """ + def shift(a): + if a > pi: + return a - 2 * pi + elif a <= -pi: + return a + 2 * pi + else: + return a + return abs(shift(a) - shift(b)) < .0000001 + + # 1. Compute some solutions + h_chi1 = h_chi[0, 0] + h_chi2 = h_chi[1, 0] + h_chi3 = h_chi[2, 0] + q_alpha1 = q_alpha[0, 0] + q_alpha2 = q_alpha[1, 0] + q_alpha3 = q_alpha[2, 0] + + try: + # a) Solve for chi using Equation 3 + chi1, chi2 = solve(-h_chi1, h_chi3, q_alpha3) + + # b) Solve for omega Equation 1 and each chi + B = h_chi1 * cos(chi1) + h_chi3 * sin(chi1) + eq1omega11, eq1omega12 = solve(h_chi2, B, q_alpha1) + B = h_chi1 * cos(chi2) + h_chi3 * sin(chi2) + eq1omega21, eq1omega22 = solve(h_chi2, B, q_alpha1) + + # c) Solve for omega Equation 2 and each chi + A = -h_chi1 * cos(chi1) - h_chi3 * sin(chi1) + eq2omega11, eq2omega12 = solve(A, h_chi2, q_alpha2) + A = -h_chi1 * cos(chi2) - h_chi3 * sin(chi2) + eq2omega21, eq2omega22 = solve(A, h_chi2, q_alpha2) + + except ValueError, e: + raise ValueError( + str(e) + ":\nProblem in fixed-phi calculation for:\nh_chi: " + + str(h_chi.tolist()) + " q_alpha: " + str(q_alpha.tolist())) + + # 2. Choose values of chi and omega that are solutions to equations 1 and 2 + solutions = [] + # a) Check the chi1 solutions + print "_findOmegaAndChiToRotateHchiIntoQalpha:" + if ne(eq1omega11, eq2omega11) or ne(eq1omega11, eq2omega12): +# print "1: eq1omega11, chi1 = ", eq1omega11, chi1 + solutions.append((eq1omega11, chi1)) + if ne(eq1omega12, eq2omega11) or ne(eq1omega12, eq2omega12): +# print "2: eq1omega12, chi1 = ", eq1omega12, chi1 + solutions.append((eq1omega12, chi1)) + # b) Check the chi2 solutions + if ne(eq1omega21, eq2omega21) or ne(eq1omega21, eq2omega22): +# print "3: eq1omega21, chi2 = ", eq1omega21, chi2 + solutions.append((eq1omega21, chi2)) + if ne(eq1omega22, eq2omega21) or ne(eq1omega22, eq2omega22): +# print "4: eq1omega22, chi2 = ", eq1omega22, chi2 + solutions.append((eq1omega22, chi2)) +# print solutions +# print "*" + + if len(solutions) == 0: + e = "h_chi: " + str(h_chi.tolist()) + e += " q_alpha: " + str(q_alpha.tolist()) + e += ("\nchi1:%4f eq1omega11:%4f eq1omega12:%4f eq2omega11:%4f " + "eq2omega12:%4f" % (chi1 * TODEG, eq1omega11 * TODEG, + eq1omega12 * TODEG, eq2omega11 * TODEG, eq2omega12 * TODEG)) + e += ("\nchi2:%4f eq1omega21:%4f eq1omega22:%4f eq2omega21:%4f " + "eq2omega22:%4f" % (chi2 * TODEG, eq1omega21 * TODEG, + eq1omega22 * TODEG, eq2omega21 * TODEG, eq2omega22 * TODEG)) + raise Exception("Could not find simultaneous solution for this fixed " + "phi mode problem\n" + e) + + if not PREFER_POSITIVE_CHI_SOLUTIONS: + return solutions[0] + + positive_chi_solutions = [sol for sol in solutions if sol[1] > 0] + + if len(positive_chi_solutions) == 0: + print "WARNING: A +ve chi solution was requested, but none were found." + print " Returning a -ve one. Try the mapper" + return solutions[0] + + if len(positive_chi_solutions) > 1: + print ("INFO: Multiple +ve chi solutions were found [(omega, chi) ...]" + " = " + str(positive_chi_solutions)) + print " Returning the first" + + return positive_chi_solutions[0] diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/constraints.py b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/constraints.py new file mode 100644 index 0000000..4751bef --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/constraints.py @@ -0,0 +1,336 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from copy import copy + +from diffcalc.util import DiffcalcException + + +class Mode(object): + + def __init__(self, index, name, group, description, parameterNames, + implemented=True): + self.index = index + self.group = group + self.name = name + self.description = description + self.parameterNames = parameterNames + self.implemented = implemented + + def __repr__(self): + return "%i) %s" % (self.index, self.description) + + def __str__(self): + return self.__repr__() + + def usesParameter(self, name): + return name in self.parameterNames + + +class ModeSelector(object): + + def __init__(self, geometry, parameterManager=None, + gammaParameterName='gamma'): + self.parameter_manager = parameterManager + self._geometry = geometry + self._gammaParameterName = gammaParameterName + self._modelist = {} # indexed by non-contiguous mode number + self._configureAvailableModes() + self._selectedIndex = 1 + + def setParameterManager(self, manager): + """ + Required as a ParameterManager and ModelSelector are mutually tied + together in practice + """ + self.parameter_manager = manager + + def _configureAvailableModes(self): + + gammaName = self._gammaParameterName + + ml = self._modelist + + ml[0] = Mode(0, '4cFixedw', 'fourc', 'fourc fixed-bandlw', + ['alpha', gammaName, 'blw'], False) + + ml[1] = Mode(1, '4cBeq', 'fourc', 'fourc bisecting', + ['alpha', gammaName]) + + ml[2] = Mode(2, '4cBin', 'fourc', 'fourc incoming', + ['alpha', gammaName, 'betain']) + + ml[3] = Mode(3, '4cBout', 'fourc', 'fourc outgoing', + ['alpha', gammaName, 'betaout']) + + ml[4] = Mode(4, '4cAzimuth', 'fourc', 'fourc azimuth', + ['alpha', gammaName, 'azimuth'], False) + + ml[5] = Mode(5, '4cPhi', 'fourc', 'fourc fixed-phi', + ['alpha', gammaName, 'phi']) + + ml[10] = Mode(10, '5cgBeq', 'fivecFixedGamma', 'fivec bisecting', + [gammaName]) + + ml[11] = Mode(11, '5cgBin', 'fivecFixedGamma', 'fivec incoming', + [gammaName, 'betain']) + + ml[12] = Mode(12, '5cgBout', 'fivecFixedGamma', 'fivec outgoing', + [gammaName, 'betaout']) + + ml[13] = Mode(13, '5caBeq', 'fivecFixedAlpha', 'fivec bisecting', + ['alpha']) + + ml[14] = Mode(14, '5caBin', 'fivecFixedAlpha', 'fivec incoming', + ['alpha', 'betain']) + + ml[15] = Mode(15, '5caBout', 'fivecFixedAlpha', 'fivec outgoing', + ['alpha', 'betaout']) + + ml[20] = Mode(20, '6czBeq', 'zaxis', 'zaxis bisecting', + []) + + ml[21] = Mode(21, '6czBin', 'zaxis', 'zaxis incoming', + ['betain']) + + ml[22] = Mode(22, '6czBout', 'zaxis', 'zaxiz outgoing', + ['betaout']) + + def setModeByIndex(self, index): + if index in self._modelist: + self._selectedIndex = index + else: + raise DiffcalcException("mode %r is not defined" % index) + + def setModeByName(self, name): + def findModeWithName(name): + for index, mode in self._modelist.items(): + if mode.name == name: + return index, mode + raise ValueError + + try: + index, mode = findModeWithName(name) + except ValueError: + raise DiffcalcException( + 'Unknown mode. The diffraction calculator supports these ' + 'modeSelector: %s' % self._supportedModes.keys()) + if self._geometry.supports_mode_group(mode.group): + self._selectedIndex = index + else: + raise DiffcalcException( + "Mode %s not supported for this diffractometer (%s)." % + (name, self._geometry.name)) + + def getMode(self): + return self._modelist[self._selectedIndex] + + def reportCurrentMode(self): + return self.getMode().__str__() + + def reportAvailableModes(self): + result = '' + indecis = self._modelist.keys() + indecis.sort() + for index in indecis: + mode = self._modelist[index] + if self._geometry.supports_mode_group(mode.group): + paramString = '' + flags = '' + pm = self.parameter_manager + for paramName in pm.getUserChangableParametersForMode(mode): + paramString += paramName + ", " + if paramString: + paramString = paramString[:-2] # remove trailing commas + if not mode.implemented: + flags += "(Not impl.)" + result += ('%2i) %-15s (%s) %s\n' % (mode.index, + mode.description, paramString, flags)) + return result + + +class VliegParameterManager(object): + + def __init__(self, geometry, hardware, modeSelector, + gammaParameterName='gamma'): + self._geometry = geometry + self._hardware = hardware + self._modeSelector = modeSelector + self._gammaParameterName = gammaParameterName + self._parameters = {} + self._defineParameters() + + def _defineParameters(self): + # Set default fixed values (In degrees if angles) + self._parameters = {} + self._parameters['alpha'] = 0 + self._parameters[self._gammaParameterName] = 0 + self._parameters['blw'] = None # Busing and Levi omega! + self._parameters['betain'] = None + self._parameters['betaout'] = None + self._parameters['azimuth'] = None + self._parameters['phi'] = None + + self._parameterDisplayOrder = ( + 'alpha', self._gammaParameterName, 'betain', 'betaout', 'azimuth', + 'phi', 'blw') + self._trackableParameters = ('alpha', self._gammaParameterName, 'phi') + self._trackedParameters = [] + + # Overide parameters that are unchangable for this diffractometer + for (name, value) in self._geometry.fixed_parameters.items(): + if name not in self._parameters: + raise RuntimeError( + "The %s diffractometer geometry specifies a fixed " + "parameter %s that is not used by the diffractometer " + "calculator" % (self._geometry.getName, name)) + self._parameters[name] = value + + def reportAllParameters(self): + self.update_tracked() + result = '' + for name in self._parameterDisplayOrder: + flags = "" + if not self._modeSelector.getMode().usesParameter(name): + flags += '(not relevant in this mode)' + if self._geometry.parameter_fixed(name): + flags += ' (fixed by this diffractometer)' + if self.isParameterTracked(name): + flags += ' (tracking hardware)' + value = self._parameters[name] + if value is None: + value = '---' + else: + value = float(value) + result += '%s: %s %s\n' % (name.rjust(8), value, flags) + return result + + def reportParametersUsedInCurrentMode(self): + self.update_tracked() + result = '' + for name in self.getUserChangableParametersForMode( + self._modeSelector.getMode()): + flags = "" + value = self._parameters[name] + if value is None: + value = '---' + else: + value = float(value) + if self.isParameterTracked(name): + flags += ' (tracking hardware)' + result += '%s: %s %s\n' % (name.rjust(8), value, flags) + return result + + def getUserChangableParametersForMode(self, mode=None): + """ + (p1,p2...p3) = getUserChangableParametersForMode(mode) returns a list + of parameters names used in this mode for this diffractometer geometry. + Checks current mode if no mode specified. + """ + if mode is None: + mode = self._mode + result = [] + for name in self._parameterDisplayOrder: + if self._isParameterChangeable(name, mode): + result += [name] + return result + +### Fixed parameters stuff ### + + def set_constraint(self, name, value): + if not name in self._parameters: + raise DiffcalcException("No fixed parameter %s is used by the " + "diffraction calculator" % name) + if self._geometry.parameter_fixed(name): + raise DiffcalcException( + "The parameter %s cannot be changed: It has been fixed by the " + "%s diffractometer geometry" + % (name, self._geometry.name)) + if self.isParameterTracked(name): + # for safety and to avoid confusion: + raise DiffcalcException( + "Cannot change parameter %s as it is set to track an axis.\n" + "To turn this off use a command like 'trackalpha 0'." % name) + + if not self.isParameterUsedInSelectedMode(name): + print ("WARNING: The parameter %s is not used in mode %i" % + (name, self._modeSelector.getMode().index)) + self._parameters[name] = value + + def isParameterUsedInSelectedMode(self, name): + return self._modeSelector.getMode().usesParameter(name) + + def getParameterWithoutUpdatingTrackedParemeters(self, name): + try: + return self._parameters[name] + except KeyError: + raise DiffcalcException("No fixed parameter %s is used by the " + "diffraction calculator" % name) + + def get_constraint(self, name): + self.update_tracked() + return self.getParameterWithoutUpdatingTrackedParemeters(name) + + def getParameterDict(self): + self.update_tracked() + return copy(self._parameters) + + @property + def settable_constraint_names(self): + """list of all available constraints that have settable values""" + return sorted(self.getParameterDict().keys()) + + def setTrackParameter(self, name, switch): + if not name in self._parameters.keys(): + raise DiffcalcException("No fixed parameter %s is used by the " + "diffraction calculator" % name) + if not name in self._trackableParameters: + raise DiffcalcException("Parameter %s is not trackable" % name) + if not self._isParameterChangeable(name): + print ("WARNING: Parameter %s is not used in mode %i" % + (name, self._mode.index)) + if switch: + if name not in self._trackedParameters: + self._trackedParameters.append(name) + else: + if name in self._trackedParameters: + self._trackedParameters.remove(name) + + def isParameterTracked(self, name): + return (name in self._trackedParameters) + + def update_tracked(self): + """Note that the name of a tracked parameter MUST map into the name of + an external diffractometer angle + """ + if self._trackedParameters: + externalAnglePositionArray = self._hardware.get_position() + externalAngleNames = list(self._hardware.get_axes_names()) + for name in self._trackedParameters: + self._parameters[name] = \ + externalAnglePositionArray[externalAngleNames.index(name)] + + def _isParameterChangeable(self, name, mode=None): + """ + Returns true if parameter is used in a mode (current mode if none + specified), AND if it is not locked by the diffractometer geometry + """ + if mode is None: + mode = self._modeSelector.getMode() + return (mode.usesParameter(name) and + not self._geometry.parameter_fixed(name)) diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/geometry.py b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/geometry.py new file mode 100644 index 0000000..fc26a83 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/geometry.py @@ -0,0 +1,523 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import tan, cos, sin, asin, atan, pi, fabs + +try: + from numpy import matrix +except ImportError: + from numjy import matrix + +from diffcalc.util import x_rotation, z_rotation, y_rotation +from diffcalc.util import AbstractPosition +from diffcalc.util import bound, nearlyEqual + + +TORAD = pi / 180 +TODEG = 180 / pi + + +def calcALPHA(alpha): + return x_rotation(alpha) + + +def calcDELTA(delta): + return z_rotation(-delta) + + +def calcGAMMA(gamma): + return x_rotation(gamma) + + +def calcOMEGA(omega): + return z_rotation(-omega) + + +def calcCHI(chi): + return y_rotation(chi) + + +def calcPHI(phi): + return z_rotation(-phi) + + +def createVliegMatrices(alpha=None, delta=None, gamma=None, omega=None, + chi=None, phi=None): + + ALPHA = None if alpha is None else calcALPHA(alpha) + DELTA = None if delta is None else calcDELTA(delta) + GAMMA = None if gamma is None else calcGAMMA(gamma) + OMEGA = None if omega is None else calcOMEGA(omega) + CHI = None if chi is None else calcCHI(chi) + PHI = None if phi is None else calcPHI(phi) + return ALPHA, DELTA, GAMMA, OMEGA, CHI, PHI + + +def createVliegsSurfaceTransformationMatrices(sigma, tau): + """[SIGMA, TAU] = createVliegsSurfaceTransformationMatrices(sigma, tau) + angles in radians + """ + SIGMA = matrix([[cos(sigma), 0, sin(sigma)], + [0, 1, 0], \ + [-sin(sigma), 0, cos(sigma)]]) + + TAU = matrix([[cos(tau), sin(tau), 0], + [-sin(tau), cos(tau), 0], + [0, 0, 1]]) + return(SIGMA, TAU) + + +def createVliegsPsiTransformationMatrix(psi): + """PSI = createPsiTransformationMatrices(psi) + angles in radians + """ + return matrix([[1, 0, 0], + [0, cos(psi), sin(psi)], + [0, -sin(psi), cos(psi)]]) + + +class VliegPosition(AbstractPosition): + """The position of all six diffractometer axis""" + def __init__(self, alpha=None, delta=None, gamma=None, omega=None, + chi=None, phi=None): + self.alpha = alpha + self.delta = delta + self.gamma = gamma + self.omega = omega + self.chi = chi + self.phi = phi + + def clone(self): + return VliegPosition(self.alpha, self.delta, self.gamma, self.omega, + self.chi, self.phi) + + def changeToRadians(self): + self.alpha *= TORAD + self.delta *= TORAD + self.gamma *= TORAD + self.omega *= TORAD + self.chi *= TORAD + self.phi *= TORAD + + def changeToDegrees(self): + self.alpha *= TODEG + self.delta *= TODEG + self.gamma *= TODEG + self.omega *= TODEG + self.chi *= TODEG + self.phi *= TODEG + + def inRadians(self): + pos = self.clone() + pos.changeToRadians() + return pos + + def inDegrees(self): + pos = self.clone() + pos.changeToDegrees() + return pos + + def nearlyEquals(self, pos2, maxnorm): + for a, b in zip(self.totuple(), pos2.totuple()): + if abs(a - b) > maxnorm: + return False + return True + + def totuple(self): + return (self.alpha, self.delta, self.gamma, self.omega, + self.chi, self.phi) + + def __str__(self): + return ("VliegPosition(alpha %r delta: %r gamma: %r omega: %r chi: %r" + " phi: %r)" % self.totuple()) + + def __repr__(self): + return self.__str__() + + def __eq__(self, b): + return self.nearlyEquals(b, .001) + + +class VliegGeometry(object): + +# Required methods + + def __init__(self, name, supported_mode_groups, fixed_parameters, + gamma_location): + """ + Set geometry name (String), list of supported mode groups (list of + strings), list of axis names (list of strings). Define the parameters + e.g. alpha and gamma for a four circle (dictionary). Define wether the + gamma angle is on the 'arm' or the 'base'; used only by AngleCalculator + to interpret the gamma parameter in fixed gamma mode: for instruments + with gamma on the base, rather than on the arm as the code assume + internally, the two methods physical_angles_to_internal_position and + internal_position_to_physical_angles must still be used. + """ + if gamma_location not in ('arm', 'base', None): + raise RuntimeError( + "Gamma must be on either 'arm' or 'base' or None") + + self.name = name + self.supported_mode_groups = supported_mode_groups + self.fixed_parameters = fixed_parameters + self.gamma_location = gamma_location + + def physical_angles_to_internal_position(self, physicalAngles): + raise NotImplementedError() + + def internal_position_to_physical_angles(self, physicalAngles): + raise NotImplementedError() + +### Do not overide these these ### + + def supports_mode_group(self, name): + return name in self.supported_mode_groups + + def parameter_fixed(self, name): # parameter_fixed + return name in self.fixed_parameters.keys() + + +class SixCircleGammaOnArmGeometry(VliegGeometry): + """ + This six-circle diffractometer geometry simply passes through the + angles from a six circle diffractometer with the same geometry and + angle names as those defined in Vliegs's paper defined internally. + """ + + def __init__(self): + VliegGeometry.__init__( + self, + name='sixc_gamma_on_arm', + supported_mode_groups=('fourc', 'fivecFixedGamma', + 'fivecFixedAlpha', 'zaxis'), + fixed_parameters={}, + gamma_location='arm') + + def physical_angles_to_internal_position(self, physicalAngles): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + assert (len(physicalAngles) == 6), "Wrong length of input list" + return VliegPosition(*physicalAngles) + + def internal_position_to_physical_angles(self, internalPosition): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + return internalPosition.totuple() + + +class SixCircleGeometry(VliegGeometry): + """ + This six-circle diffractometer geometry simply passes through the + angles from a six circle diffractometer with the same geometry and + angle names as those defined in Vliegs's paper defined internally. + """ + + def __init__(self): + VliegGeometry.__init__( + self, + name='sixc', + supported_mode_groups=('fourc', 'fivecFixedGamma', + 'fivecFixedAlpha', 'zaxis'), + fixed_parameters={}, + gamma_location='base') + self.hardwareMonitor = None +#(deltaA, gammaA) = gammaOnBaseToArm(deltaB, gammaB, alpha) (all in radians) +#(deltaB, gammaB) = gammaOnArmToBase(deltaA, gammaA, alpha) (all in radians) + + def physical_angles_to_internal_position(self, physicalAngles): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + assert (len(physicalAngles) == 6), "Wrong length of input list" + alpha, deltaB, gammaB, omega, chi, phi = physicalAngles + (deltaA, gammaA) = gammaOnBaseToArm( + deltaB * TORAD, gammaB * TORAD, alpha * TORAD) + return VliegPosition( + alpha, deltaA * TODEG, gammaA * TODEG, omega, chi, phi) + + def internal_position_to_physical_angles(self, internalPosition): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + alpha, deltaA, gammaA, omega, chi, phi = internalPosition.totuple() + deltaB, gammaB = gammaOnArmToBase( + deltaA * TORAD, gammaA * TORAD, alpha * TORAD) + deltaB, gammaB = deltaB * TODEG, gammaB * TODEG + + if self.hardwareMonitor is not None: + gammaName = self.hardwareMonitor.get_axes_names()[2] + minGamma = self.hardwareMonitor.get_lower_limit(gammaName) + maxGamma = self.hardwareMonitor.get_upper_limit(gammaName) + + if maxGamma is not None: + if gammaB > maxGamma: + gammaB = gammaB - 180 + deltaB = 180 - deltaB + if minGamma is not None: + if gammaB < minGamma: + gammaB = gammaB + 180 + deltaB = 180 - deltaB + + return alpha, deltaB, gammaB, omega, chi, phi + + +class FivecWithGammaOnBase(SixCircleGeometry): + + def __init__(self): + VliegGeometry.__init__( + self, + name='fivec_with_gamma', + supported_mode_groups=('fourc', 'fivecFixedGamma'), + fixed_parameters={'alpha': 0.0}, + gamma_location='base') + self.hardwareMonitor = None + + def physical_angles_to_internal_position(self, physicalAngles): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(d,g,o,c,p) + """ + assert (len(physicalAngles) == 5), "Wrong length of input list" + return SixCircleGeometry.physical_angles_to_internal_position( + self, (0,) + tuple(physicalAngles)) + + def internal_position_to_physical_angles(self, internalPosition): + """ (d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + return SixCircleGeometry.internal_position_to_physical_angles( + self, internalPosition)[1:] + + +class Fivec(VliegGeometry): + """ + This five-circle diffractometer geometry is for diffractometers with the + same geometry and angle names as those defined in Vliegs's paper defined + internally, but with no out plane detector arm gamma.""" + + def __init__(self): + VliegGeometry.__init__(self, + name='fivec', + supported_mode_groups=('fourc', 'fivecFixedGamma'), + fixed_parameters={'gamma': 0.0}, + gamma_location='arm' + ) + + def physical_angles_to_internal_position(self, physicalAngles): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + assert (len(physicalAngles) == 5), "Wrong length of input list" + physicalAngles = tuple(physicalAngles) + angles = physicalAngles[0:2] + (0.0,) + physicalAngles[2:] + return VliegPosition(*angles) + + def internal_position_to_physical_angles(self, internalPosition): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + sixAngles = internalPosition.totuple() + return sixAngles[0:2] + sixAngles[3:] + + +class Fourc(VliegGeometry): + """ + This five-circle diffractometer geometry is for diffractometers with the + same geometry and angle names as those defined in Vliegs's paper defined + internally, but with no out plane detector arm gamma.""" + + def __init__(self): + VliegGeometry.__init__(self, + name='fourc', + supported_mode_groups=('fourc'), + fixed_parameters={'gamma': 0.0, 'alpha': 0.0}, + gamma_location='arm' + ) + + def physical_angles_to_internal_position(self, physicalAngles): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + assert (len(physicalAngles) == 4), "Wrong length of input list" + physicalAngles = tuple(physicalAngles) + angles = (0.0, physicalAngles[0], 0.0) + physicalAngles[1:] + return VliegPosition(*angles) + + def internal_position_to_physical_angles(self, internalPosition): + """ (a,d,g,o,c,p) = physicalAnglesToInternal(a,d,g,o,c,p) + """ + sixAngles = internalPosition.totuple() + return sixAngles[1:2] + sixAngles[3:] + + +def sign(x): + if x < 0: + return -1 + else: + return 1 + +""" +Based on: Elias Vlieg, "A (2+3)-Type Surface Diffractometer: Mergence of +the z-axis and (2+2)-Type Geometries", J. Appl. Cryst. (1998). 31. +198-203 +""" + + +def solvesEq8(alpha, deltaA, gammaA, deltaB, gammaB): + tol = 1e-6 + return (nearlyEqual(sin(deltaA) * cos(gammaA), sin(deltaB), tol) and + nearlyEqual(cos(deltaA) * cos(gammaA), + cos(gammaB - alpha) * cos(deltaB), tol) and + nearlyEqual(sin(gammaA), sin(gammaB - alpha) * cos(deltaB), tol)) + + +GAMMAONBASETOARM_WARNING = ''' +WARNING: This diffractometer has the gamma circle attached to the + base rather than the end of + the delta arm as Vlieg's paper defines. A conversion has + been made from the physical angles to their internal + representation (gamma-on-base-to-arm). This conversion has + forced gamma to be positive by applying the mapping: + + delta --> 180+delta + gamma --> 180+gamma. + + This should have no adverse effect. +''' + + +def gammaOnBaseToArm(deltaB, gammaB, alpha): + """ + (deltaA, gammaA) = gammaOnBaseToArm(deltaB, gammaB, alpha) (all in + radians) + + Maps delta and gamma for an instrument where the gamma circle rests on + the base to the case where it is on the delta arm. + + There are always two possible solutions. To get the second apply the + transform: + + delta --> 180+delta (flip to opposite side of circle) + gamma --> 180+gamma (flip to opposite side of circle) + + This code will return the solution where gamma is between 0 and 180. + """ + + ### Equation11 ### + if fabs(cos(gammaB - alpha)) < 1e-20: + deltaA1 = sign(tan(deltaB)) * sign(cos(gammaB - alpha)) * pi / 2 + else: + deltaA1 = atan(tan(deltaB) / cos(gammaB - alpha)) + # ...second root + if deltaA1 <= 0: + deltaA2 = deltaA1 + pi + else: + deltaA2 = deltaA1 - pi + + ### Equation 12 ### + gammaA1 = asin(bound(cos(deltaB) * sin(gammaB - alpha))) + # ...second root + if gammaA1 >= 0: + gammaA2 = pi - gammaA1 + else: + gammaA2 = -pi - gammaA1 + + # Choose the delta solution that fits equations 8 + if solvesEq8(alpha, deltaA1, gammaA1, deltaB, gammaB): + deltaA, gammaA = deltaA1, gammaA1 + elif solvesEq8(alpha, deltaA2, gammaA1, deltaB, gammaB): + deltaA, gammaA = deltaA2, gammaA1 + print "gammaOnBaseToArm choosing 2nd delta root (to internal)" + elif solvesEq8(alpha, deltaA1, gammaA2, deltaB, gammaB): + print "gammaOnBaseToArm choosing 2nd gamma root (to internal)" + deltaA, gammaA = deltaA1, gammaA2 + elif solvesEq8(alpha, deltaA2, gammaA2, deltaB, gammaB): + print "gammaOnBaseToArm choosing 2nd delta root and 2nd gamma root" + deltaA, gammaA = deltaA2, gammaA2 + else: + raise RuntimeError( + "No valid solutions found mapping from gamma-on-base to gamma-on-arm") + + return deltaA, gammaA + +GAMMAONARMTOBASE_WARNING = ''' + WARNING: This diffractometer has the gamma circle attached to the base + rather than the end of the delta arm as Vlieg's paper defines. + A conversion has been made from the internal representation of + angles to physical angles (gamma-on-arm-to-base). This + conversion has forced gamma to be positive by applying the + mapping: + + delta --> 180-delta + gamma --> 180+gamma. + + This should have no adverse effect. +''' + + +def gammaOnArmToBase(deltaA, gammaA, alpha): + """ + (deltaB, gammaB) = gammaOnArmToBase(deltaA, gammaA, alpha) (all in + radians) + + Maps delta and gamma for an instrument where the gamma circle is on + the delta arm to the case where it rests on the base. + + There are always two possible solutions. To get the second apply the + transform: + + delta --> 180-delta (reflect and flip to opposite side) + gamma --> 180+gamma (flip to opposite side) + + This code will return the solution where gamma is positive, but will + warn if a sign change was made. + """ + + ### Equation 9 ### + deltaB1 = asin(bound(sin(deltaA) * cos(gammaA))) + # ...second root: + if deltaB1 >= 0: + deltaB2 = pi - deltaB1 + else: + deltaB2 = -pi - deltaB1 + + ### Equation 10 ###: + if fabs(cos(deltaA)) < 1e-20: + gammaB1 = sign(tan(gammaA)) * sign(cos(deltaA)) * pi / 2 + alpha + else: + gammaB1 = atan(tan(gammaA) / cos(deltaA)) + alpha + #... second root: + if gammaB1 <= 0: + gammaB2 = gammaB1 + pi + else: + gammaB2 = gammaB1 - pi + + ### Choose the solution that fits equation 8 ### + if (solvesEq8(alpha, deltaA, gammaA, deltaB1, gammaB1) and + 0 <= gammaB1 <= pi): + deltaB, gammaB = deltaB1, gammaB1 + elif (solvesEq8(alpha, deltaA, gammaA, deltaB2, gammaB1) and + 0 <= gammaB1 <= pi): + deltaB, gammaB = deltaB2, gammaB1 + print "gammaOnArmToBase choosing 2nd delta root (to physical)" + elif (solvesEq8(alpha, deltaA, gammaA, deltaB1, gammaB2) and + 0 <= gammaB2 <= pi): + print "gammaOnArmToBase choosing 2nd gamma root (to physical)" + deltaB, gammaB = deltaB1, gammaB2 + elif (solvesEq8(alpha, deltaA, gammaA, deltaB2, gammaB2) + and 0 <= gammaB2 <= pi): + print "gammaOnArmToBase choosing 2nd delta root and 2nd gamma root" + deltaB, gammaB = deltaB2, gammaB2 + else: + raise RuntimeError( + "No valid solutions found mapping gamma-on-arm to gamma-on-base") + + return deltaB, gammaB diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/hkl.py b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/hkl.py new file mode 100644 index 0000000..ae05018 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/hkl.py @@ -0,0 +1,139 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from diffcalc.hkl.common import getNameFromScannableOrString +from diffcalc.util import command +from diffcalc import settings + + +from diffcalc.ub import ub +from diffcalc.hkl.vlieg.calc import VliegHklCalculator + + +__all__ = ['hklmode', 'setpar', 'trackalpha', 'trackgamma', 'trackphi', + 'parameter_manager', 'hklcalc'] + + +hklcalc = VliegHklCalculator(ub.ubcalc, settings.geometry, settings.hardware) + +parameter_manager = hklcalc.parameter_manager + +def __str__(self): + return hklcalc.__str__() + +@command +def hklmode(num=None): + """hklmode {num} -- changes mode or shows current and available modes and all settings""" #@IgnorePep8 + + if num is None: + print hklcalc.__str__() + else: + hklcalc.mode_selector.setModeByIndex(int(num)) + pm = hklcalc.parameter_manager + print (hklcalc.mode_selector.reportCurrentMode() + "\n" + + pm.reportParametersUsedInCurrentMode()) + +def _setParameter(name, value): + hklcalc.parameter_manager.set_constraint(name, value) + +def _getParameter(name): + return hklcalc.parameter_manager.get_constraint(name) + +@command +def setpar(scannable_or_string=None, val=None): + """setpar {parameter_scannable {{val}} -- sets or shows a parameter' + setpar {parameter_name {val}} -- sets or shows a parameter' + """ + + if scannable_or_string is None: + #show all + parameterDict = hklcalc.parameter_manager.getParameterDict() + names = parameterDict.keys() + names.sort() + for name in names: + print _representParameter(name) + else: + name = getNameFromScannableOrString(scannable_or_string) + if val is None: + _representParameter(name) + else: + oldval = _getParameter(name) + _setParameter(name, float(val)) + print _representParameter(name, oldval, float(val)) + +def _representParameter(name, oldval=None, newval=None): + flags = '' + if hklcalc.parameter_manager.isParameterTracked(name): + flags += '(tracking hardware) ' + if settings.geometry.parameter_fixed(name): # @UndefinedVariable + flags += '(fixed by geometry) ' + pm = hklcalc.parameter_manager + if not pm.isParameterUsedInSelectedMode(name): + flags += '(not relevant in this mode) ' + if oldval is None: + val = _getParameter(name) + if val is None: + val = "---" + else: + val = str(val) + return "%s: %s %s" % (name, val, flags) + else: + return "%s: %s --> %f %s" % (name, oldval, newval, flags) + +def _checkInputAndSetOrShowParameterTracking(name, b=None): + """ + for track-parameter commands: If no args displays parameter settings, + otherwise sets the tracking switch for the given parameter and displays + settings. + """ + # set if arg given + if b is not None: + hklcalc.parameter_manager.setTrackParameter(name, b) + # Display: + lastValue = _getParameter(name) + if lastValue is None: + lastValue = "---" + else: + lastValue = str(lastValue) + flags = '' + if hklcalc.parameter_manager.isParameterTracked(name): + flags += '(tracking hardware)' + print "%s: %s %s" % (name, lastValue, flags) + +@command +def trackalpha(b=None): + """trackalpha {boolean} -- determines wether alpha parameter will track alpha axis""" #@IgnorePep8 + _checkInputAndSetOrShowParameterTracking('alpha', b) + +@command +def trackgamma(b=None): + """trackgamma {boolean} -- determines wether gamma parameter will track alpha axis""" #@IgnorePep8 + _checkInputAndSetOrShowParameterTracking('gamma', b) + +@command +def trackphi(b=None): + """trackphi {boolean} -- determines wether phi parameter will track phi axis""" #@IgnorePep8 + _checkInputAndSetOrShowParameterTracking('phi', b) + + +commands_for_help = ['Mode', + hklmode, + setpar, + trackalpha, + trackgamma, + trackphi] diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/transform.py b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/transform.py new file mode 100644 index 0000000..a41e8b9 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/vlieg/transform.py @@ -0,0 +1,480 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from diffcalc.util import command + +from copy import copy +from math import pi + +from diffcalc.hkl.vlieg.geometry import VliegPosition as P + +SMALL = 1e-10 + + +class Transform(object): + + def transform(self, pos): + raise RuntimeError('Not implemented') + + +### Transforms, currently for definition and testing the theory only + +class TransformC(Transform): + '''Flip omega, invert chi and flip phi + ''' + def transform(self, pos): + pos = pos.clone() + pos.omega -= 180 + pos.chi *= -1 + pos.phi -= 180 + return pos + + +class TransformB(Transform): + '''Flip chi, and invert and flip omega + ''' + def transform(self, pos): + pos = pos.clone() + pos.chi -= 180 + pos.omega = 180 - pos.omega + return pos + + +class TransformA(Transform): + '''Invert scattering plane: invert delta and omega and flip chi''' + def transform(self, pos): + pos = pos.clone() + pos.delta *= -1 + pos.omega *= -1 + pos.chi -= 180 + return pos + + +class TransformCInRadians(Transform): + ''' + Flip omega, invert chi and flip phi. Using radians and keeping + -pi 0: + pos.omega -= pi + else: + pos.omega += pi + pos.chi *= -1 + pos.phi += pi + return pos + + +### + +transformsFromSector = { + 0: (), + 1: ('c',), + 2: ('a',), + 3: ('a', 'c'), + 4: ('b', 'c'), + 5: ('b',), + 6: ('a', 'b', 'c'), + 7: ('a', 'b') +} + +sectorFromTransforms = {} +for k, v in transformsFromSector.iteritems(): + sectorFromTransforms[v] = k + + +class VliegPositionTransformer(object): + + def __init__(self, geometry, hardware, solution_transformer): + self._geometry = geometry + self._hardware = hardware + self._solution_transformer = solution_transformer + solution_transformer.limitCheckerFunction = self.is_position_within_limits + + def transform(self, pos): + # 1. Choose the correct sector/transforms + return self._solution_transformer.transformPosition(pos) + + def is_position_within_limits(self, position): + '''where position is Position object in degrees''' + angleTuple = self._geometry.internal_position_to_physical_angles(position) + angleTuple = self._hardware.cut_angles(angleTuple) + return self._hardware.is_position_within_limits(angleTuple) + + +class VliegTransformSelector(object): + '''All returned angles are between -180. and 180. -180.<=angle<180. + ''' +### basic sector selection + + def __init__(self): + self.transforms = [] + self.autotransforms = [] + self.autosectors = [] + self.limitCheckerFunction = None # inject + self.sector = None + self.setSector(0) + + def setSector(self, sector): + if not 0 <= sector <= 7: + raise ValueError('%i must between 0 and 7.' % sector) + self.sector = sector + self.transforms = list(transformsFromSector[sector]) + + def setTransforms(self, transformList): + transformList = list(transformList) + transformList.sort() + self.sector = sectorFromTransforms[tuple(transformList)] + self.transforms = transformList + + def addTransorm(self, transformName): + if transformName not in ('a', 'b', 'c'): + raise ValueError('%s is not a recognised transform. Try a, b or c' + % transformName) + if transformName in self.transforms: + print "WARNING, transform %s is already selected" + else: + self.setTransforms(self.transforms + [transformName]) + + def removeTransorm(self, transformName): + if transformName not in ('a', 'b', 'c'): + raise ValueError('%s is not a recognised transform. Try a, b or c' + % transformName) + if transformName in self.transforms: + new = copy(self.transforms) + new.remove(transformName) + self.setTransforms(new) + else: + print "WARNING, transform %s was not selected" % transformName + + def addAutoTransorm(self, transformOrSector): + ''' + If input is a string (letter), tags one of the transofrms as being a + candidate for auto application. If a number, tags a sector as being a + candidate for auto application, and removes similar tags for any + transforms (as the two are incompatable). + ''' + if type(transformOrSector) == str: + transform = transformOrSector + if transform not in ('a', 'b', 'c'): + raise ValueError( + '%s is not a recognised transform. Try a, b or c' % + transform) + if transform not in self.autotransforms: + self.autosectors = [] + self.autotransforms.append(transform) + else: + print "WARNING: %s is already set to auto apply" % transform + elif type(transformOrSector) == int: + sector = transformOrSector + if not 0 <= sector <= 7: + raise ValueError('%i must between 0 and 7.' % sector) + if sector not in self.autosectors: + self.autotransforms = [] + self.autosectors.append(sector) + else: + print "WARNING: %i is already set to auto apply" % sector + else: + raise ValueError("Input must be 'a', 'b' or 'c', " + "or 1,2,3,4,5,6 or 7.") + + def removeAutoTransform(self, transformOrSector): + if type(transformOrSector) == str: + transform = transformOrSector + if transform not in ('a', 'b', 'c'): + raise ValueError("%s is not a recognised transform. " + "Try a, b or c" % transform) + if transform in self.autotransforms: + self.autotransforms.remove(transform) + else: + print "WARNING: %s is not set to auto apply" % transform + elif type(transformOrSector) == int: + sector = transformOrSector + if not 0 <= sector <= 7: + raise ValueError('%i must between 0 and 7.' % sector) + if sector in self.autosectors: + self.autosectors.remove(sector) + else: + print "WARNING: %s is not set to auto apply" % sector + else: + raise ValueError("Input must be 'a', 'b' or 'c', " + "or 1,2,3,4,5,6 or 7.") + + def setAutoSectors(self, sectorList): + for sector in sectorList: + if not 0 <= sector <= 7: + raise ValueError('%i must between 0 and 7.' % sector) + self.autosectors = list(sectorList) + + def transformPosition(self, pos): + pos = self.transformNWithoutCut(self.sector, pos) + cutpos = self.cutPosition(pos) + # -180 <= cutpos < 180, NOT the externally applied cuts + if len(self.autosectors) > 0: + if self.is_position_within_limits(cutpos): + return cutpos + else: + return self.autoTransformPositionBySector(cutpos) + if len(self.autotransforms) > 0: + if self.is_position_within_limits(cutpos): + return cutpos + else: + return self.autoTransformPositionByTransforms(pos) + #else + return cutpos + + def transformNWithoutCut(self, n, pos): + + if n == 0: + return P(pos.alpha, pos.delta, pos.gamma, + pos.omega, pos.chi, pos.phi) + if n == 1: + return P(pos.alpha, pos.delta, pos.gamma, + pos.omega - 180., -pos.chi, pos.phi - 180.) + if n == 2: + return P(pos.alpha, -pos.delta, pos.gamma, + -pos.omega, pos.chi - 180., pos.phi) + if n == 3: + return P(pos.alpha, -pos.delta, pos.gamma, + 180. - pos.omega, 180. - pos.chi, pos.phi - 180.) + if n == 4: + return P(pos.alpha, pos.delta, pos.gamma, + -pos.omega, 180. - pos.chi, pos.phi - 180.) + if n == 5: + return P(pos.alpha, pos.delta, pos.gamma, + 180. - pos.omega, pos.chi - 180., pos.phi) + if n == 6: + return P(pos.alpha, -pos.delta, pos.gamma, + pos.omega, -pos.chi, pos.phi - 180.) + if n == 7: + return P(pos.alpha, -pos.delta, pos.gamma, + pos.omega - 180., pos.chi, pos.phi) + else: + raise Exception("sector must be between 0 and 7") + +### autosector + + def hasAutoSectorsOrTransformsToApply(self): + return len(self.autosectors) > 0 or len(self.autotransforms) > 0 + + def autoTransformPositionBySector(self, pos): + okaysectors = [] + okaypositions = [] + for sector in self.autosectors: + newpos = self.transformNWithoutCut(sector, pos) + if self.is_position_within_limits(newpos): + okaysectors.append(sector) + okaypositions.append(newpos) + if len(okaysectors) == 0: + raise Exception( + "Autosector could not find a sector (from %s) to move %s into " + "limits." % (self.autosectors, str(pos))) + if len(okaysectors) > 1: + print ("WARNING: Autosector found multiple sectors that would " + "move %s to move into limits: %s" % (str(pos), okaysectors)) + + print ("INFO: Autosector changed sector from %i to %i" % + (self.sector, okaysectors[0])) + self.sector = okaysectors[0] + return okaypositions[0] + + def autoTransformPositionByTransforms(self, pos): + possibleTransforms = self.createListOfPossibleTransforms() + okaytransforms = [] + okaypositions = [] + for transforms in possibleTransforms: + sector = sectorFromTransforms[tuple(transforms)] + newpos = self.cutPosition(self.transformNWithoutCut(sector, pos)) + if self.is_position_within_limits(newpos): + okaytransforms.append(transforms) + okaypositions.append(newpos) + if len(okaytransforms) == 0: + raise Exception( + "Autosector could not find a sector (from %r) to move %r into " + "limits." % (self.autosectors, pos)) + if len(okaytransforms) > 1: + print ("WARNING: Autosector found multiple sectors that would " + "move %s to move into limits: %s" % + (repr(pos), repr(okaytransforms))) + + print ("INFO: Autosector changed selected transforms from %r to %r" % + (self.transforms, okaytransforms[0])) + self.setTransforms(okaytransforms[0]) + return okaypositions[0] + + def createListOfPossibleTransforms(self): + def vary(possibleTransforms, name): + result = [] + for transforms in possibleTransforms: + # add the original. + result.append(transforms) + # add a modified one + toadd = list(copy(transforms)) + if name in transforms: + toadd.remove(name) + else: + toadd.append(name) + toadd.sort() + result.append(toadd) + return result + # start with the currently selected list of transforms + if len(self.transforms) == 0: + possibleTransforms = [()] + else: + possibleTransforms = copy(self.transforms) + + for name in self.autotransforms: + possibleTransforms = vary(possibleTransforms, name) + + return possibleTransforms + + def is_position_within_limits(self, pos): + '''where pos os a poistion object in degrees''' + return self.limitCheckerFunction(pos) + + def __repr__(self): + def createPrefix(transform): + if transform in self.transforms: + s = '*on* ' + else: + s = 'off ' + if len(self.autotransforms) > 0: + if transform in self.autotransforms: + s += '*auto*' + else: + s += ' ' + return s + s = 'Transforms/sector:\n' + s += (' %s (a transform) Invert scattering plane: invert delta and ' + 'omega and flip chi\n' % createPrefix('a')) + s += (' %s (b transform) Flip chi, and invert and flip omega\n' % + createPrefix('b')) + s += (' %s (c transform) Flip omega, invert chi and flip phi\n' % + createPrefix('c')) + s += ' Current sector: %i (Spec fourc equivalent)\n' % self.sector + if len(self.autosectors) > 0: + s += ' Auto sectors: %s\n' % self.autosectors + return s + + def cutPosition(self, position): + '''Cuts angles at -180.; moves each argument between -180. and 180. + ''' + def cut(a): + if a is None: + return None + else: + if a < (-180. - SMALL): + return a + 360. + if a > (180. + SMALL): + return a - 360. + return a + return P(cut(position.alpha), cut(position.delta), cut(position.gamma), + cut(position.omega), cut(position.chi), cut(position.phi)) + + +def getNameFromScannableOrString(o): + try: # it may be a scannable + return o.getName() + except AttributeError: + return str(o) + + +class TransformCommands(object): + + def __init__(self, sector_selector): + self._sectorSelector = sector_selector + + @command + def transform(self): + """transform -- show transform configuration""" + print self._sectorSelector.__repr__() + + @command + def transforma(self, *args): + """transforma {on|off|auto|manual} -- configure transform A application + """ + self._transform('transforma', 'a', args) + + @command + def transformb(self, *args): + """transformb {on|off|auto|manual} -- configure transform B application + """ + self._transform('transformb', 'b', args) + + @command + def transformc(self, *args): + """transformc {on|off|auto|manual} -- configure transform C application + """ + + self._transform('transformc', 'c', args) + + def _transform(self, commandName, transformName, args): + if len(args) == 0: + print self._sectorSelector.__repr__() + return + # get name + if len(args) != 1: + raise TypeError() + if type(args[0]) is not str: + raise TypeError() + + ss = self._sectorSelector + if args[0] == 'on': + ss.addTransorm(transformName) + elif args[0] == 'off': + ss.removeTransorm(transformName) + elif args[0] == 'auto': + ss.addAutoTransorm(transformName) + elif args[0] == 'manual': + ss.removeAutoTransform(transformName) + else: + raise TypeError() + print self._sectorSelector.__repr__() + + @command + def sector(self, sector=None): + """sector {0-7} -- Select or display sector (a la Spec) + """ + if sector is None: + print self._sectorSelector.__repr__() + else: + if type(sector) is not int and not (0 <= sector <= 7): + raise TypeError() + self._sectorSelector.setSector(sector) + print self._sectorSelector.__repr__() + + @command + def autosector(self, *args): + """autosector [None] [0-7] [0-7]... -- Set sectors that might be automatically applied""" #@IgnorePep8 + if len(args) == 0: + print self._sectorSelector.__repr__() + elif len(args) == 1 and args[0] is None: + self._sectorSelector.setAutoSectors([]) + print self._sectorSelector.__repr__() + else: + sectorList = [] + for arg in args: + if type(arg) is not int: + raise TypeError() + sectorList.append(arg) + self._sectorSelector.setAutoSectors(sectorList) + print self._sectorSelector.__repr__() + + + diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/willmott/__init__.py b/script/test/diffcalc (copy)/diffcalc/hkl/willmott/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/willmott/calc.py b/script/test/diffcalc (copy)/diffcalc/hkl/willmott/calc.py new file mode 100644 index 0000000..a8e94ba --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/willmott/calc.py @@ -0,0 +1,292 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi, asin, acos, atan2, sin, cos, sqrt + +try: + from numpy import matrix +except ImportError: + from numjy import matrix + +from diffcalc.log import logging +from diffcalc.util import bound, AbstractPosition, DiffcalcException,\ + x_rotation, z_rotation +from diffcalc.hkl.vlieg.geometry import VliegGeometry +from diffcalc.ub.calc import PaperSpecificUbCalcStrategy +from diffcalc.hkl.calcbase import HklCalculatorBase +from diffcalc.hkl.common import DummyParameterManager + +logger = logging.getLogger("diffcalc.hkl.willmot.calcwill") + +CHOOSE_POSITIVE_GAMMA = True + +TORAD = pi / 180 +TODEG = 180 / pi +I = matrix('1 0 0; 0 1 0; 0 0 1') +SMALL = 1e-10 + +TEMPORARY_CONSTRAINTS_DICT_RAD = {'betain': 2 * TORAD} + + +def create_matrices(delta, gamma, omegah, phi): + return (calc_DELTA(delta), calc_GAMMA(gamma), calc_OMEGAH(omegah), + calc_PHI(phi)) + + +def calc_DELTA(delta): + return x_rotation(delta) # (39) + + +def calc_GAMMA(gamma): + return z_rotation(gamma) # (40) + + +def calc_OMEGAH(omegah): + return x_rotation(omegah) # (41) + + +def calc_PHI(phi): + return z_rotation(phi) # (42) + + +def angles_to_hkl_phi(delta, gamma, omegah, phi): + """Calculate hkl matrix in phi frame in units of 2*pi/lambda + """ + DELTA, GAMMA, OMEGAH, PHI = create_matrices(delta, gamma, omegah, phi) + H_lab = (GAMMA * DELTA - I) * matrix([[0], [1], [0]]) # (43) + H_phi = PHI.I * OMEGAH.I * H_lab # (44) + return H_phi + + +def angles_to_hkl(delta, gamma, omegah, phi, wavelength, UB): + """Calculate hkl matrix in reprical lattice space in units of 1/Angstrom + """ + H_phi = angles_to_hkl_phi(delta, gamma, omegah, phi) * 2 * pi / wavelength + hkl = UB.I * H_phi # (5) + return hkl + + +class WillmottHorizontalPosition(AbstractPosition): + + def __init__(self, delta=None, gamma=None, omegah=None, phi=None): + self.delta = delta + self.gamma = gamma + self.omegah = omegah + self.phi = phi + + def clone(self): + return WillmottHorizontalPosition(self.delta, self.gamma, self.omegah, + self.phi) + + def changeToRadians(self): + self.delta *= TORAD + self.gamma *= TORAD + self.omegah *= TORAD + self.phi *= TORAD + + def changeToDegrees(self): + self.delta *= TODEG + self.gamma *= TODEG + self.omegah *= TODEG + self.phi *= TODEG + + def totuple(self): + return (self.delta, self.gamma, self.omegah, self.phi) + + def __str__(self): + return ('WillmottHorizontalPosition(' + 'delta: %.4f gamma: %.4f omegah: %.4f phi: %.4f)' % + (self.delta, self.gamma, self.omegah, self.phi)) + + +class WillmottHorizontalGeometry(object): + + def __init__(self): + self.name = 'willmott_horizontal' + + def physical_angles_to_internal_position(self, physicalAngles): + return WillmottHorizontalPosition(*physicalAngles) + + def internal_position_to_physical_angles(self, internalPosition): + return internalPosition.totuple() + + def create_position(self, delta, gamma, omegah, phi): + return WillmottHorizontalPosition(delta, gamma, omegah, phi) + + +class WillmottHorizontalUbCalcStrategy(PaperSpecificUbCalcStrategy): + + def calculate_q_phi(self, pos): + H_phi = angles_to_hkl_phi(*pos.totuple()) + return matrix(H_phi.tolist()) + + +class DummyConstraints(object): + + @property + def reference(self): + """dictionary of constrained reference circles""" + return TEMPORARY_CONSTRAINTS_DICT_RAD + + +class ConstraintAdapter(object): + + def __init__(self, constraints): + self._constraints = constraints + + def getParameterDict(self): + names = self._constraints.available + return dict(zip(names, [None] * len(names))) + + def setParameter(self, name, value): + self._constraints.set_constraint(name, value) + + def get(self, name): + if name in self._constraints.all: + val = self._constraints.get_value(name) + return 999 if val is None else val + else: + return 999 + + def update_tracked(self): + pass + + +class WillmottHorizontalCalculator(HklCalculatorBase): + + def __init__(self, ubcalc, geometry, hardware, constraints, + raiseExceptionsIfAnglesDoNotMapBackToHkl=True): + """" + Where constraints.reference is a one element dict with the key either + ('betain', 'betaout' or 'equal') and the value a number or None for + 'betain_eq_betaout' + """ + + HklCalculatorBase.__init__(self, ubcalc, geometry, hardware, + raiseExceptionsIfAnglesDoNotMapBackToHkl) + + if constraints is not None: + self.constraints = constraints + self.parameter_manager = ConstraintAdapter(constraints) + else: + self.constraints = DummyConstraints() + self.parameter_manager = DummyParameterManager() + + @property + def _UB(self): + return self._ubcalc.UB + + def _anglesToHkl(self, pos, wavelength): + """ + Calculate miller indices from position in radians. + """ + hkl_matrix = angles_to_hkl(pos.delta, pos.gamma, pos.omegah, pos.phi, + wavelength, self._UB) + return hkl_matrix[0, 0], hkl_matrix[1, 0], hkl_matrix[2, 0], + + def _anglesToVirtualAngles(self, pos, wavelength): + """ + Calculate virtual-angles in radians from position in radians. + + Return theta, alpha, and beta in a dictionary. + """ + + betain = pos.omegah # (52) + + hkl = angles_to_hkl(pos.delta, pos.gamma, pos.omegah, pos.phi, + wavelength, self._UB) + H_phi = self._UB * hkl + H_phi = H_phi / (2 * pi / wavelength) + l_phi = H_phi[2, 0] + sin_betaout = l_phi - sin(betain) + betaout = asin(bound(sin_betaout)) # (54) + + cos_2theta = cos(pos.delta) * cos(pos.gamma) + theta = acos(bound(cos_2theta)) / 2. + + return {'theta': theta, 'betain': betain, 'betaout': betaout} + + def _hklToAngles(self, h, k, l, wavelength): + """ + Calculate position and virtual angles in radians for a given hkl. + """ + + H_phi = self._UB * matrix([[h], [k], [l]]) # units: 1/Angstrom + H_phi = H_phi / (2 * pi / wavelength) # units: 2*pi/wavelength + h_phi = H_phi[0, 0] + k_phi = H_phi[1, 0] + l_phi = H_phi[2, 0] # (5) + + ### determine betain (omegah) and betaout ### + + if not self.constraints.reference: + raise ValueError("No reference constraint has been constrained.") + + ref_name, ref_value = self.constraints.reference.items()[0] + if ref_value is not None: + ref_value *= TORAD + if ref_name == 'betain': + betain = ref_value + betaout = asin(bound(l_phi - sin(betain))) # (53) + elif ref_name == 'betaout': + betaout = ref_value + betain = asin(bound(l_phi - sin(betaout))) # (54) + elif ref_name == 'bin_eq_bout': + betain = betaout = asin(bound(l_phi / 2)) # (55) + else: + raise ValueError("Unexpected constraint name'%s'." % ref_name) + + if abs(betain) < SMALL: + raise DiffcalcException('required betain was 0 degrees (requested ' + 'q is perpendicular to surface normal)') + if betain < -SMALL: + raise DiffcalcException("betain was -ve (%.4f)" % betain) +# logger.info('betain = %.4f, betaout = %.4f', +# betain * TODEG, betaout * TODEG) + omegah = betain # (52) + + ### determine H_lab (X, Y and Z) ### + + Y = -(h_phi ** 2 + k_phi ** 2 + l_phi ** 2) / 2 # (45) + + Z = (sin(betaout) + sin(betain) * (Y + 1)) / cos(omegah) # (47) + + X_squared = (h_phi ** 2 + k_phi ** 2 - + ((cos(betain) * Y + sin(betain) * Z) ** 2)) # (48) + if (X_squared < 0) and (abs(X_squared) < SMALL): + X_squared = 0 + Xpositive = sqrt(X_squared) + if CHOOSE_POSITIVE_GAMMA: + X = -Xpositive + else: + X = Xpositive +# logger.info('H_lab (X,Y,Z) = [%.4f, %.4f, %.4f]', X, Y, Z) + ### determine diffractometer angles ### + + gamma = atan2(-X, Y + 1) # (49) + if (abs(gamma) < SMALL): + # degenerate case, only occurs when q || z + delta = 2 * omegah + else: + delta = atan2(Z * sin(gamma), -X) # (50) + M = cos(betain) * Y + sin(betain) * Z + phi = atan2(h_phi * M - k_phi * X, h_phi * X + k_phi * M) # (51) + + pos = WillmottHorizontalPosition(delta, gamma, omegah, phi) + virtual_angles = {'betain': betain, 'betaout': betaout} + return pos, virtual_angles diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/willmott/commands.py b/script/test/diffcalc (copy)/diffcalc/hkl/willmott/commands.py new file mode 100644 index 0000000..1d6cadd --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/willmott/commands.py @@ -0,0 +1,58 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from diffcalc.hkl.common import getNameFromScannableOrString +from diffcalc.util import command + + +class WillmottHklCommands(object): + + def __init__(self, hklcalc): + self._hklcalc = hklcalc + self.commands = [self.con, + self.uncon, + self.cons] + + def __str__(self): + return self._hklcalc.__str__() + + @command + def con(self, scn_or_string): + """con -- constrains constraint + """ + name = getNameFromScannableOrString(scn_or_string) + self._hklcalc.constraints.constrain(name) + print self._report_constraints() + + @command + def uncon(self, scn_or_string): + """uncon -- unconstrains constraint + """ + name = getNameFromScannableOrString(scn_or_string) + self._hklcalc.constraints.unconstrain(name) + print self._report_constraints() + + @command + def cons(self): + """cons -- list available constraints and values + """ + print self._report_constraints() + + def _report_constraints(self): + return (self._hklcalc.constraints.build_display_table_lines() + '\n\n' + + self._hklcalc.constraints._report_constraints()) diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/willmott/constraints.py b/script/test/diffcalc (copy)/diffcalc/hkl/willmott/constraints.py new file mode 100644 index 0000000..9ab5abd --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/willmott/constraints.py @@ -0,0 +1,156 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from diffcalc.util import DiffcalcException + + +def filter_dict(d, keys): + """Return a copy of d containing only keys that are in keys""" + ##return {k: d[k] for k in keys} # requires Python 2.6 + return dict((k, d[k]) for k in keys if k in d.keys()) + + +ref_constraints = ('betain', 'betaout', 'bin_eq_bout') +valueless_constraints = ('bin_eq_bout') +all_constraints = ref_constraints + + +class WillmottConstraintManager(object): + """Constraints in degrees. + """ + + def __init__(self): + self._constrained = {'bin_eq_bout': None} + + @property + def available_constraint_names(self): + """list of all available constraints""" + return all_constraints + + @property + def all(self): # @ReservedAssignment + """dictionary of all constrained values""" + return self._constrained.copy() + + @property + def reference(self): + """dictionary of constrained reference circles""" + return filter_dict(self.all, ref_constraints) + + @property + def constrained_names(self): + """ordered tuple of constained circles""" + names = self.all.keys() + names.sort(key=lambda name: list(all_constraints).index(name)) + return tuple(names) + + def is_constrained(self, name): + return name in self._constrained + + def get_value(self, name): + return self._constrained[name] + + def _build_display_table(self): + constraint_types = (ref_constraints,) + num_rows = max([len(col) for col in constraint_types]) + max_name_width = max( + [len(name) for name in sum(constraint_types[:2], ())]) + # headings + lines = [' ' + 'REF'.ljust(max_name_width)] + lines.append(' ' + '=' * max_name_width + ' ') + + # constraint rows + for n_row in range(num_rows): + cells = [] + for col in constraint_types: + name = col[n_row] if n_row < len(col) else '' + cells.append(self._label_constraint(name)) + cells.append(('%-' + str(max_name_width) + 's ') % name) + lines.append(''.join(cells)) + lines.append + return '\n'.join(lines) + + def _report_constraints(self): + if not self.reference: + return "!!! No reference constraint set" + name, val = self.reference.items()[0] + if name in valueless_constraints: + return " %s" % name + else: + if val is None: + return "!!! %s: ---" % name + else: + return " %s: %.4f" % (name, val) + + def _label_constraint(self, name): + if name == '': + label = ' ' + elif (self.is_constrained(name) and (self.get_value(name) is None) and + name not in valueless_constraints): + label = 'o-> ' + elif self.is_constrained(name): + label = '--> ' + else: + label = ' ' + return label + + def constrain(self, name): + if name in self.all: + return "%s is already constrained." % name.capitalize() + elif name in ref_constraints: + return self._constrain_reference(name) + else: + raise DiffcalcException('%s is not a valid constraint name') + + def _constrain_reference(self, name): + if self.reference: + constrained_name = self.reference.keys()[0] + del self._constrained[constrained_name] + self._constrained[name] = None + return '%s constraint replaced.' % constrained_name.capitalize() + else: + self._constrained[name] = None + + def unconstrain(self, name): + if name in self._constrained: + del self._constrained[name] + else: + return "%s was not already constrained." % name.capitalize() + +### + def _check_constraint_settable(self, name, verb): + if name not in all_constraints: + raise DiffcalcException( + 'Could not %(verb)s %(name)s as this is not an available ' + 'constraint.' % locals()) + elif name not in self.all.keys(): + raise DiffcalcException( + 'Could not %(verb)s %(name)s as this is not currently ' + 'constrained.' % locals()) + elif name in valueless_constraints: + raise DiffcalcException( + 'Could not %(verb)s %(name)s as this constraint takes no ' + 'value.' % locals()) + + def set_constraint(self, name, value): # @ReservedAssignment + self._check_constraint_settable(name, 'set') + old_value = self.all[name] + old = str(old_value) if old_value is not None else '---' + self._constrained[name] = float(value) + new = str(value) + return "%(name)s : %(old)s --> %(new)s" % locals() diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/you/__init__.py b/script/test/diffcalc (copy)/diffcalc/hkl/you/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/you/calc.py b/script/test/diffcalc (copy)/diffcalc/hkl/you/calc.py new file mode 100644 index 0000000..1e9ac96 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/you/calc.py @@ -0,0 +1,1167 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi, sin, cos, acos, asin, atan2, sqrt +from itertools import product + +try: + from numpy import matrix + from numpy.linalg import norm +except ImportError: + from numjy import matrix + from numjy.linalg import norm + +from diffcalc.log import logging +from diffcalc.hkl.calcbase import HklCalculatorBase +from diffcalc.hkl.you.geometry import create_you_matrices, calcMU, calcPHI, \ + calcCHI, calcETA +from diffcalc.hkl.you.geometry import YouPosition +from diffcalc.util import DiffcalcException, bound, angle_between_vectors,\ + y_rotation +from diffcalc.util import cross3, z_rotation, x_rotation +from diffcalc.ub.calc import PaperSpecificUbCalcStrategy + +from diffcalc.hkl.you.constraints import NUNAME +logger = logging.getLogger("diffcalc.hkl.you.calc") +I = matrix('1 0 0; 0 1 0; 0 0 1') + +SMALL = 1e-6 +TORAD = pi / 180 +TODEG = 180 / pi + +PRINT_DEGENERATE = False +PRINT_WARNINGS = False + + +def is_small(x): + return abs(x) < SMALL + + +def sign(x): + if is_small(x): + return 0 + if x > 0: + return 1 + # x < 0 + return -1 + + +def normalised(vector): + return vector * (1 / norm(vector)) + + +def cut_at_minus_pi(value): + if value < (-pi - SMALL): + return value + 2 * pi + if value >= pi + SMALL: + return value - 2 * pi + return value + + +def _calc_N(Q, n): + """Return N as described by Equation 31""" + Q = normalised(Q) + n = normalised(n) + if is_small(angle_between_vectors(Q, n)): + # Replace the reference vector with an alternative vector from Eq.(78) + idx_min, _ = min(enumerate([abs(Q[0, 0]), abs(Q[1, 0]), abs(Q[2, 0])]), key=lambda v: v[1]) + idx_1, idx_2 = [idx for idx in range(3) if idx != idx_min] + qval = sqrt(Q[idx_1, 0] * Q[idx_1, 0] + Q[idx_2, 0] * Q[idx_2, 0]) + n[idx_min, 0] = qval + n[idx_1, 0] = -Q[idx_min, 0] * Q[idx_1, 0] / qval + n[idx_2, 0] = -Q[idx_min, 0] * Q[idx_2, 0] / qval + if is_small(norm(n)): + n[idx_min, 0] = 0 + n[idx_1, 0] = Q[idx_2, 0] / qval + n[idx_2, 0] = -Q[idx_1, 0] / qval + Qxn = cross3(Q, n) + QxnxQ = cross3(Qxn, Q) + QxnxQ = normalised(QxnxQ) + Qxn = normalised(Qxn) + return matrix([[Q[0, 0], QxnxQ[0, 0], Qxn[0, 0]], + [Q[1, 0], QxnxQ[1, 0], Qxn[1, 0]], + [Q[2, 0], QxnxQ[2, 0], Qxn[2, 0]]]) + + +def _calc_angle_between_naz_and_qaz(theta, alpha, tau): + # Equation 30: + top = cos(tau) - sin(alpha) * sin(theta) + bottom = cos(alpha) * cos(theta) + if is_small(bottom): + if is_small(cos(alpha)): + raise ValueError('cos(alpha) is too small') + if is_small(cos(theta)): + raise ValueError('cos(theta) is too small') + if is_small(sin(tau)): + return 0. + return acos(bound(top / bottom)) + + +def youAnglesToHkl(pos, wavelength, UBmatrix): + """Calculate miller indices from position in radians. + """ + + [MU, DELTA, NU, ETA, CHI, PHI] = create_you_matrices(*pos.totuple()) + + q_lab = (NU * DELTA - I) * matrix([[0], [2 * pi / wavelength], [0]]) # 12 + + hkl = UBmatrix.I * PHI.I * CHI.I * ETA.I * MU.I * q_lab + + return hkl[0, 0], hkl[1, 0], hkl[2, 0] + + +def _tidy_degenerate_solutions(pos, constraints): + + original = pos.inDegrees() + detector_like_constraint = constraints.detector or constraints.naz + nu_constrained_to_0 = is_small(pos.nu) and detector_like_constraint + mu_constrained_to_0 = is_small(pos.mu) and 'mu' in constraints.sample + delta_constrained_to_0 = is_small(pos.delta) and detector_like_constraint + eta_constrained_to_0 = is_small(pos.eta) and 'eta' in constraints.sample + phi_not_constrained = not 'phi' in constraints.sample + + if nu_constrained_to_0 and mu_constrained_to_0 and phi_not_constrained: + # constrained to vertical 4-circle like mode + if is_small(pos.chi): # phi || eta + desired_eta = pos.delta / 2. + eta_diff = desired_eta - pos.eta + pos.eta = desired_eta + pos.phi -= eta_diff + if PRINT_DEGENERATE: + print ('DEGENERATE: with chi=0, phi and eta are colinear:' + 'choosing eta = delta/2 by adding % 7.3f to eta and ' + 'removing it from phi. (mu=%s=0 only)' % (eta_diff * TODEG, NUNAME)) + print ' original:', original + + elif delta_constrained_to_0 and eta_constrained_to_0 and phi_not_constrained: + # constrained to horizontal 4-circle like mode + if is_small(pos.chi - pi / 2): # phi || mu + desired_mu = pos.nu / 2. + mu_diff = desired_mu - pos.mu + pos.mu = desired_mu + pos.phi += mu_diff + if PRINT_DEGENERATE: + print ('DEGENERATE: with chi=90, phi and mu are colinear: choosing' + ' mu = %s/2 by adding % 7.3f to mu and to phi. ' + '(delta=eta=0 only)' % (NUNAME, mu_diff * TODEG)) + print ' original:', original + + return pos + + +def _theta_and_qaz_from_detector_angles(delta, nu): + # Equation 19: + cos_2theta = cos(delta) * cos(nu) + theta = acos(cos_2theta) / 2. + sgn = sign(sin(2. * theta)) + qaz = atan2(sgn * sin(delta), sgn * cos(delta) * sin(nu)) + return theta, qaz + + +class YouUbCalcStrategy(PaperSpecificUbCalcStrategy): + + def calculate_q_phi(self, pos): + + [MU, DELTA, NU, ETA, CHI, PHI] = create_you_matrices(*pos.totuple()) + # Equation 12: Compute the momentum transfer vector in the lab frame + y = matrix('0; 1; 0') + q_lab = (NU * DELTA - I) * y + # Transform this into the phi frame. + return PHI.I * CHI.I * ETA.I * MU.I * q_lab + + +UNREACHABLE_MSG = ( + 'The current combination of constraints with %s = %.4f\n' + 'prohibits a solution for the specified reflection.') + + +class YouHklCalculator(HklCalculatorBase): + + def __init__(self, ubcalc, geometry, hardware, constraints, + raiseExceptionsIfAnglesDoNotMapBackToHkl=True): + HklCalculatorBase.__init__(self, ubcalc, geometry, hardware, + raiseExceptionsIfAnglesDoNotMapBackToHkl) + self._hardware = hardware # for checking limits only + self.constraints = constraints + self.parameter_manager = constraints # TODO: remove need for this attr + + def __str__(self): + return self.constraints.__str__() + + def _get_n_phi(self): + return self._ubcalc.n_phi + + def _get_ubmatrix(self): + return self._getUBMatrix() # for consistency + + def repr_mode(self): + return repr(self.constraints.all) + + def _anglesToHkl(self, pos, wavelength): + """Calculate miller indices from position in radians. + """ + return youAnglesToHkl(pos, wavelength, self._get_ubmatrix()) + + def _anglesToVirtualAngles(self, pos, _wavelength): + """Calculate pseudo-angles in radians from position in radians. + + Return theta, qaz, alpha, naz, tau, psi and beta in a dictionary. + + """ + + # depends on surface normal n_lab. + mu, delta, nu, eta, chi, phi = pos.totuple() + + theta, qaz = _theta_and_qaz_from_detector_angles(delta, nu) # (19) + + [MU, _, _, ETA, CHI, PHI] = create_you_matrices(mu, + delta, nu, eta, chi, phi) + Z = MU * ETA * CHI * PHI + n_lab = Z * self._get_n_phi() + alpha = asin(bound((-n_lab[1, 0]))) + naz = atan2(n_lab[0, 0], n_lab[2, 0]) # (20) + + cos_tau = cos(alpha) * cos(theta) * cos(naz - qaz) + \ + sin(alpha) * sin(theta) + tau = acos(bound(cos_tau)) # (23) + + # Compute Tau using the dot product directly (THIS ALSO WORKS) +# q_lab = ( (NU * DELTA - I ) * matrix([[0],[1],[0]]) +# norm = norm(q_lab) +# q_lab = matrix([[1],[0],[0]]) if norm == 0 else q_lab * (1/norm) +# tau_from_dot_product = acos(bound(dot3(q_lab, n_lab))) + + sin_beta = 2 * sin(theta) * cos(tau) - sin(alpha) + beta = asin(bound(sin_beta)) # (24) + + psi = next(self._calc_psi(alpha, theta, tau, qaz, naz)) + + return {'theta': theta, 'qaz': qaz, 'alpha': alpha, + 'naz': naz, 'tau': tau, 'psi': psi, 'beta': beta} + + + def _choose_single_solution(self, pos_virtual_angles_pairs_in_degrees): + + if len(pos_virtual_angles_pairs_in_degrees) == 1: + return pos_virtual_angles_pairs_in_degrees[0] + + absolute_distances = [] + for pos_, _ in pos_virtual_angles_pairs_in_degrees: + absolute_distances.append(sum([abs(pos_.totuple()[i]) for i in (0, 3, 4, 5)])) + + shortest_solution_index = absolute_distances.index( + min(absolute_distances)) + pos, virtual_angles = pos_virtual_angles_pairs_in_degrees[shortest_solution_index] + + if logger.isEnabledFor(logging.INFO): + msg = ('Multiple sample solutions found (choosing solution with ' + 'shortest distance to all-zeros position):\n') + i = 0 + for (pos_, _), distance in zip(pos_virtual_angles_pairs_in_degrees, + absolute_distances): + msg += '*' if i == shortest_solution_index else '.' + + msg += ('mu=% 7.3f, delta=% 7.3f, nu=% 7.3f, eta=% 7.3f, chi=% 7.3f, phi=% 7.3f' % + pos_.totuple()) + msg += ' (distance=% 4.3f)\n' % (distance * TODEG) + i += 1 + msg += ':\n' + logger.info(msg) + + return pos, virtual_angles + + def hklToAngles(self, h, k, l, wavelength, return_all_solutions=False): + """ + Return verified Position and all virtual angles in degrees from + h, k & l and wavelength in Angstroms. + + The calculated Position is verified by checking that it maps back using + anglesToHkl() to the requested hkl value. + + Those virtual angles fixed or generated while calculating the position + are verified by by checking that they map back using + anglesToVirtualAngles to the virtual angles for the given position. + + Throws a DiffcalcException if either check fails and + raiseExceptionsIfAnglesDoNotMapBackToHkl is True, otherwise displays a + warning. + """ + + pos_virtual_angles_pairs = self._hklToAngles(h, k, l, wavelength, return_all_solutions) # in rad + assert pos_virtual_angles_pairs + pos_virtual_angles_pairs_in_degrees = [] + for pos, virtual_angles in pos_virtual_angles_pairs: + + # to degrees: + pos.changeToDegrees() + for key, val in virtual_angles.items(): + if val is not None: + virtual_angles[key] = val * TODEG + + self._verify_pos_map_to_hkl(h, k, l, wavelength, pos) + + pos_virtual_angles_pairs_in_degrees.append((pos, virtual_angles)) + + if return_all_solutions: + return pos_virtual_angles_pairs_in_degrees + else: + pos, virtual_angles = self._choose_single_solution(pos_virtual_angles_pairs_in_degrees) + return pos, virtual_angles + + + def hkl_to_all_angles(self, h, k, l, wavelength): + return self.hklToAngles(h, k, l, wavelength, True) + + + def _hklToAngles(self, h, k, l, wavelength, return_all_solutions=False): + """(pos, virtualAngles) = hklToAngles(h, k, l, wavelength) --- with + Position object pos and the virtual angles returned in degrees. Some + modes may not calculate all virtual angles. + """ + + if not self.constraints.is_fully_constrained(): + raise DiffcalcException( + "Diffcalc is not fully constrained.\n" + "Type 'help con' for instructions") + + if not self.constraints.is_current_mode_implemented(): + raise DiffcalcException( + "Sorry, the selected constraint combination is valid but " + "is not implemented. Type 'help con' for implemented combinations") + + # constraints are dictionaries + ref_constraint = self.constraints.reference + if ref_constraint: + ref_constraint_name, ref_constraint_value = ref_constraint.items()[0] + det_constraint = self.constraints.detector + naz_constraint = self.constraints.naz + samp_constraints = self.constraints.sample + + assert not (det_constraint and naz_constraint), ( + "Two 'detector' constraints given") + + + h_phi = self._get_ubmatrix() * matrix([[h], [k], [l]]) + theta = self._calc_theta(h_phi, wavelength) + tau = angle_between_vectors(h_phi, self._get_n_phi()) + + if is_small(sin(tau)) and ref_constraint: + if ref_constraint_name == 'psi': + raise DiffcalcException("Azimuthal angle 'psi' is undefined as reference and scattering vectors parallel.\n" + "Please constrain one of the sample angles or choose different reference vector orientation.") + elif ref_constraint_name == 'a_eq_b': + raise DiffcalcException("Reference constraint 'a_eq_b' is redundant as reference and scattering vectors are parallel.\n" + "Please constrain one of the sample angles or choose different reference vector orientation.") + + ### Reference constraint column ### + + if ref_constraint: + # An angle for the reference vector (n) is given (Section 5.2) + alpha, _ = self._calc_remaining_reference_angles( + ref_constraint_name, ref_constraint_value, theta, tau) + + solution_tuples = [] + if det_constraint or naz_constraint: + + if len(samp_constraints) == 1: + for qaz, naz, delta, nu in self._calc_det_angles_given_det_or_naz_constraint( + det_constraint, naz_constraint, theta, tau, alpha): + for mu, eta, chi, phi in self._calc_sample_angles_from_one_sample_constraint( + samp_constraints, h_phi, theta, alpha, qaz, naz): + solution_tuples.append((mu, delta, nu, eta, chi, phi)) + + elif len(samp_constraints) == 2: + if det_constraint: + det_constraint_name, det_constraint_val = det_constraint.items()[0] + for delta, nu, qaz in self._calc_remaining_detector_angles(det_constraint_name, det_constraint_val, theta): + for mu, eta, chi, phi in self._calc_sample_angles_given_two_sample_and_detector( + samp_constraints, qaz, theta, h_phi, self._get_n_phi()): + solution_tuples.append((mu, delta, nu, eta, chi, phi)) + + else: + raise DiffcalcException( + 'No code yet to handle this combination of detector and sample constraints!') + + elif len(samp_constraints) == 2: + if ref_constraint_name == 'psi': + psi_vals = [ref_constraint_value,] + else: + psi_vals = self._calc_psi(alpha, theta, tau) + for psi in psi_vals: + angles = list(self._calc_sample_given_two_sample_and_reference( + samp_constraints, h_phi, theta, psi)) + solution_tuples.extend(angles) + + elif len(samp_constraints) == 3: + for angles in self._calc_angles_given_three_sample_constraints( + h, k, l, wavelength, return_all_solutions, samp_constraints, + h_phi, theta): + solution_tuples.append(angles) + + if not solution_tuples: + raise DiffcalcException('No solutions were found. ' + 'Please consider using an alternative set of constraints.') + + tidy_solutions = [_tidy_degenerate_solutions(YouPosition(*pos, unit='RAD'), + self.constraints).totuple() for pos in solution_tuples] + merged_solution_tuples = set(self._filter_angle_limits(tidy_solutions, + not return_all_solutions)) + if not merged_solution_tuples: + raise DiffcalcException('No solutions were found matching existing hardware limits. ' + 'Please consider using an alternative set of constraints.') + + #def _find_duplicate_angles(el): + # idx, tpl = el + # for tmp_tpl in filtered_solutions[idx:]: + # if False not in [abs(x-y) < SMALL for x,y in zip(tmp_tpl, tpl)]: + # return False + # return True + #merged_solution_tuples = filter(_find_duplicate_angles, enumerate(filtered_solutions, 1)) + position_pseudo_angles_pairs = self._create_position_pseudo_angles_pairs(wavelength, merged_solution_tuples) + if not position_pseudo_angles_pairs: + raise DiffcalcException('No solutions were found. Please check hardware limits and ' + 'consider using an alternative pseudo-angle constraints.') + + return position_pseudo_angles_pairs + + + def _create_position_pseudo_angles_pairs(self, wavelength, merged_solution_tuples): + + position_pseudo_angles_pairs = [] + for pos in merged_solution_tuples: + # Create position + position = YouPosition(*pos, unit='RAD') + #position = _tidy_degenerate_solutions(position, self.constraints) + #if position.phi <= -pi + SMALL: + # position.phi += 2 * pi + # pseudo angles calculated along the way were for the initial solution + # and may be invalid for the chosen solution TODO: anglesToHkl need no + # longer check the pseudo_angles as they will be generated with the + # same function and it will prove nothing + pseudo_angles = self._anglesToVirtualAngles(position, wavelength) + is_sol = True + for constraint in [self.constraints.reference, + self.constraints.detector, + self.constraints.naz]: + try: + constraint_name, constraint_value = constraint.items()[0] + if constraint_name == 'a_eq_b': + if not is_small(pseudo_angles['alpha'] - pseudo_angles['beta']): + is_sol = False + break + else: + if not is_small(constraint_value - pseudo_angles[constraint_name]): + is_sol = False + break + except: + continue + if is_sol: + position_pseudo_angles_pairs.append((position, pseudo_angles)) + return position_pseudo_angles_pairs + + + def _calc_theta(self, h_phi, wavelength): + """Calculate theta using Equation1 + """ + q_length = norm(h_phi) + if is_small(q_length): + raise DiffcalcException('Reflection is unreachable as |Q| is too small') + wavevector = 2 * pi / wavelength + try: + theta = asin(bound(q_length / (2 * wavevector))) + except AssertionError: + raise DiffcalcException( + 'Reflection is unreachable as |Q| is too long') + if is_small(cos(theta)): + raise DiffcalcException( + 'Reflection is unreachable as theta angle is too close to 90 deg') + return theta + + def _calc_psi(self, alpha, theta, tau, qaz=None, naz=None): + """Calculate psi from Eq. (18), (25) and (28) + """ + sin_tau = sin(tau) + cos_theta = cos(theta) + if is_small(sin_tau): + # The reference vector is parallel to the scattering vector + yield float('nan') + elif is_small(cos_theta): + # Reflection is unreachable as theta angle is too close to 90 deg + yield float('nan') + elif is_small(sin(theta)): + # Reflection is unreachable as |Q| is too small + yield float('nan') + else: + cos_psi = ((cos(tau) * sin(theta) - sin(alpha)) / cos_theta) # (28) + if qaz is None or naz is None : + try: + acos_psi = acos(bound(cos_psi / sin_tau)) + if is_small(acos_psi): + yield 0. + else: + for psi in [acos_psi, -acos_psi]: + yield psi + except AssertionError: + if PRINT_WARNINGS: + print ('WARNING: Diffcalc could not calculate an azimuth (psi)') + yield float('nan') + else: + sin_psi = cos(alpha) * sin(qaz - naz) + sgn = sign(sin_tau) + eps = sin_psi**2 + cos_psi**2 + sigma_ = eps/sin_tau**2 - 1 + if not is_small(sigma_): + if PRINT_WARNINGS: + print ('WARNING: Diffcalc could not calculate a unique azimuth ' + '(psi) because of loss of accuracy in numerical calculation') + yield float('nan') + else: + psi = atan2(sgn * sin_psi, sgn * cos_psi) + yield psi + + + def _calc_remaining_reference_angles(self, name, value, theta, tau): + """Return alpha and beta given one of a_eq_b, alpha, beta or psi + """ + if name == 'psi': + psi = value + # Equation 26 for alpha + sin_alpha = (cos(tau) * sin(theta) - + cos(theta) * sin(tau) * cos(psi)) + if abs(sin_alpha) > 1 + SMALL: + raise DiffcalcException(UNREACHABLE_MSG % (name, value * TODEG)) + alpha = asin(bound(sin_alpha)) + # Equation 27 for beta + sin_beta = cos(tau) * sin(theta) + cos(theta) * sin(tau) * cos(psi) + if abs(sin_beta) > 1 + SMALL: + raise DiffcalcException(UNREACHABLE_MSG % (name, value * TODEG)) + + beta = asin(bound(sin_beta)) + + elif name == 'a_eq_b': + alpha = beta = asin(cos(tau) * sin(theta)) # (24) + + elif name == 'alpha': + alpha = value # (24) + sin_beta = 2 * sin(theta) * cos(tau) - sin(alpha) + if abs(sin_beta) > 1 + SMALL: + raise DiffcalcException(UNREACHABLE_MSG % (name, value * TODEG)) + beta = asin(sin_beta) + + elif name == 'beta': + beta = value + sin_alpha = 2 * sin(theta) * cos(tau) - sin(beta) # (24) + if abs(sin_alpha) > 1 + SMALL: + raise DiffcalcException(UNREACHABLE_MSG % (name, value * TODEG)) + + alpha = asin(sin_alpha) + + return alpha, beta + + def _calc_det_angles_given_det_or_naz_constraint( + self, det_constraint, naz_constraint, theta, tau, alpha): + + assert det_constraint or naz_constraint + try: + naz_qaz_angle = _calc_angle_between_naz_and_qaz(theta, alpha, tau) + except AssertionError: + return + if det_constraint: + # One of the detector angles is given (Section 5.1) + det_constraint_name, det_constraint = det_constraint.items()[0] + for delta, nu, qaz in self._calc_remaining_detector_angles( + det_constraint_name, det_constraint, theta): + if is_small(naz_qaz_angle): + naz_angles = [qaz,] + else: + naz_angles = [qaz - naz_qaz_angle, qaz + naz_qaz_angle] + for naz in naz_angles: + yield qaz, naz, delta, nu + elif naz_constraint: # The 'detector' angle naz is given: + det_constraint_name, det_constraint = naz_constraint.items()[0] + naz_name, naz = det_constraint_name, det_constraint + assert naz_name == 'naz' + if is_small(naz_qaz_angle): + qaz_angles = [naz,] + else: + qaz_angles = [naz - naz_qaz_angle, naz + naz_qaz_angle] + for qaz in qaz_angles: + for delta, nu, _ in self._calc_remaining_detector_angles( + 'qaz', qaz, theta): + yield qaz, naz, delta, nu + + def _calc_remaining_detector_angles(self, constraint_name, + constraint_value, theta): + """Return delta, nu and qaz given one detector angle + """ + # (section 5.1) + # Find qaz using various derivations of 17 and 18 + sin_2theta = sin(2 * theta) + cos_2theta = cos(2 * theta) + if is_small(sin_2theta): + raise DiffcalcException( + 'No meaningful scattering vector (Q) can be found when ' + 'theta is so small (%.4f).' % theta * TODEG) + + if constraint_name == 'delta': + delta = constraint_value + try: + asin_qaz = asin(bound(sin(delta) / sin_2theta)) # (17 & 18) + except AssertionError: + return + cos_delta = cos(delta) + if is_small(cos_delta): + #raise DiffcalcException( + # 'The %s and %s circles are redundant when delta is constrained to %.0f degrees.' + # 'Please change delta constraint or use 4-circle mode.' % (NUNAME, 'mu', delta * TODEG)) + if PRINT_DEGENERATE: + print (('DEGENERATE: with delta=90, %s is degenerate: choosing ' + '%s = 0 (allowed because %s is unconstrained)') % + (NUNAME, NUNAME, NUNAME)) + acos_nu = 1. + else: + try: + acos_nu = acos(bound(cos_2theta / cos_delta)) + except AssertionError: + return + if is_small(cos(asin_qaz)): + qaz_angles = [sign(asin_qaz) * pi / 2.,] + else: + qaz_angles = [asin_qaz, pi - asin_qaz] + if is_small(acos_nu): + nu_angles = [0.,] + else: + nu_angles = [acos_nu, -acos_nu] + for qaz, nu in product(qaz_angles, nu_angles): + sgn_ref = sign(sin_2theta) * sign(cos(qaz)) + sgn_ratio = sign(sin(nu)) * sign(cos_delta) + if sgn_ref == sgn_ratio: + yield delta, nu, qaz + + elif constraint_name == NUNAME: + nu = constraint_value + cos_nu = cos(nu) + if is_small(cos_nu): + raise DiffcalcException( + 'The %s circle constraint to %.0f degrees is redundant.' + 'Please change this constraint or use 4-circle mode.' % (NUNAME, nu * TODEG)) + cos_delta = cos_2theta / cos(nu) + cos_qaz = cos_delta * sin(nu) / sin_2theta + try: + acos_delta = acos(bound(cos_delta)) + acos_qaz = acos(bound(cos_qaz)) + except AssertionError: + return + if is_small(acos_qaz): + qaz_angles = [0.,] + else: + qaz_angles = [acos_qaz, -acos_qaz] + if is_small(acos_delta): + delta_angles = [0.,] + else: + delta_angles = [acos_delta, -acos_delta] + for qaz, delta in product(qaz_angles, delta_angles): + sgn_ref = sign(sin(delta)) + sgn_ratio = sign(sin(qaz)) * sign(sin_2theta) + if sgn_ref == sgn_ratio: + yield delta, nu, qaz + + elif constraint_name == 'qaz': + qaz = constraint_value + asin_delta = asin(sin(qaz) * sin_2theta) + if is_small(cos(asin_delta)): + delta_angles = [sign(asin_delta) * pi / 2.,] + else: + delta_angles = [asin_delta, pi - asin_delta] + for delta in delta_angles: + cos_delta = cos(delta) + if is_small(cos_delta): + if PRINT_DEGENERATE: + print (('DEGENERATE: with delta=90, %s is degenerate: choosing ' + '%s = 0 (allowed because %s is unconstrained)') % + (NUNAME, NUNAME, NUNAME)) + #raise DiffcalcException( + # 'The %s circle is redundant when delta is at %.0f degrees.' + # 'Please change detector constraint or use 4-circle mode.' % (NUNAME, delta * TODEG)) + nu = 0. + else: + sgn_delta = sign(cos_delta) + nu = atan2(sgn_delta * sin_2theta * cos(qaz), sgn_delta * cos_2theta) + yield delta, nu, qaz + else: + raise DiffcalcException( + constraint_name + ' is not an explicit detector angle ' + '(naz cannot be handled here)') + + + def _calc_sample_angles_from_one_sample_constraint( + self, samp_constraints, h_phi, theta, alpha, qaz, naz): + + sample_constraint_name, sample_value = samp_constraints.items()[0] + q_lab = matrix([[cos(theta) * sin(qaz)], + [-sin(theta)], + [cos(theta) * cos(qaz)]]) # (18) + n_lab = matrix([[cos(alpha) * sin(naz)], + [-sin(alpha)], + [cos(alpha) * cos(naz)]]) # (20) + mu_eta_chi_phi_tuples = list(self._calc_remaining_sample_angles( + sample_constraint_name, sample_value, q_lab, n_lab, h_phi, + self._get_n_phi())) + return mu_eta_chi_phi_tuples + + def _calc_sample_given_two_sample_and_reference( + self, samp_constraints, h_phi, theta, psi): + + for angles in self._calc_sample_angles_given_two_sample_and_reference( + samp_constraints, psi, theta, h_phi, self._get_n_phi()): + qaz, psi, mu, eta, chi, phi = angles + values_in_deg = tuple(v * TODEG for v in angles) + logger.info('Initial angles: xi=%.3f, psi=%.3f, mu=%.3f, ' + 'eta=%.3f, chi=%.3f, phi=%.3f' % + values_in_deg) # Try to find a solution for each possible transformed xi + + logger.info("") + msg = "---Trying psi=%.3f, qaz=%.3f" % (psi * TODEG, qaz * TODEG) + logger.info(msg) + + for delta, nu, _ in self._calc_remaining_detector_angles('qaz', qaz, theta): + logger.info("delta=%.3f, %s=%.3f", delta * TODEG, NUNAME, nu * TODEG) + #for mu, eta, chi, phi in self._generate_sample_solutions( + # mu, eta, chi, phi, samp_constraints.keys(), delta, + # nu, wavelength, (h, k, l), ref_constraint_name, + # ref_constraint_value): + yield mu, delta, nu, eta, chi, phi + + def _calc_remaining_sample_angles(self, constraint_name, constraint_value, + q_lab, n_lab, q_phi, n_phi): + """Return phi, chi, eta and mu, given one of these""" + # (section 5.3) + + N_lab = _calc_N(q_lab, n_lab) + N_phi = _calc_N(q_phi, n_phi) + Z = N_lab * N_phi.T + + if constraint_name == 'mu': # (35) + mu = constraint_value + V = calcMU(mu).I * N_lab * N_phi.T + try: + acos_chi = acos(bound(V[2, 2])) + except AssertionError: + return + if is_small(sin(acos_chi)): + # chi ~= 0 or 180 and therefor phi || eta The solutions for phi + # and eta here will be valid but will be chosen unpredictably. + # Choose eta=0: + # + # tan(phi+eta)=v12/v11 from docs/extensions_to_yous_paper.wxm + chi = acos_chi + eta = 0. + phi = atan2(-V[1, 0], V[1, 1]) + logger.debug( + 'Eta and phi cannot be chosen uniquely with chi so close ' + 'to 0 or 180. Returning phi=%.3f and eta=%.3f', + phi * TODEG, eta * TODEG) + yield mu, eta, chi, phi + else: + for chi in [acos_chi, -acos_chi]: + sgn = sign(sin(chi)) + phi = atan2(-sgn * V[2, 1], -sgn * V[2, 0]) + eta = atan2(-sgn * V[1, 2], sgn * V[0, 2]) + yield mu, eta, chi, phi + + elif constraint_name == 'phi': # (37) + phi = constraint_value + V = N_lab * N_phi.I * calcPHI(phi).T + try: + asin_eta = asin(bound(V[0, 1])) + except AssertionError: + return + if is_small(cos(asin_eta)): + raise DiffcalcException('Chi and mu cannot be chosen uniquely ' + 'with eta so close to +/-90.') + for eta in [asin_eta, pi - asin_eta]: + sgn = sign(cos(eta)) + mu = atan2(sgn * V[2, 1], sgn * V[1, 1]) + chi = atan2(sgn * V[0, 2], sgn * V[0, 0]) + yield mu, eta, chi, phi + + elif constraint_name in ('eta', 'chi'): + if constraint_name == 'eta': # (39) + eta = constraint_value + cos_eta = cos(eta) + if is_small(cos_eta): + #TODO: Not likely to happen in real world!? + raise DiffcalcException( + 'Chi and mu cannot be chosen uniquely with eta ' + 'constrained so close to +-90.') + try: + asin_chi = asin(bound(Z[0, 2] / cos_eta)) + except AssertionError: + return + all_eta = [eta,] + all_chi = [asin_chi, pi - asin_chi] + + else: # constraint_name == 'chi' # (40) + chi = constraint_value + sin_chi = sin(chi) + if is_small(sin_chi): + raise DiffcalcException( + 'Eta and phi cannot be chosen uniquely with chi ' + 'constrained so close to 0. (Please contact developer ' + 'if this case is useful for you)') + try: + acos_eta = acos(bound(Z[0, 2] / sin_chi)) + except AssertionError: + return + all_eta = [acos_eta, -acos_eta] + all_chi = [chi,] + + for chi, eta in product(all_chi, all_eta): + top_for_mu = Z[2, 2] * sin(eta) * sin(chi) + Z[1, 2] * cos(chi) + bot_for_mu = -Z[2, 2] * cos(chi) + Z[1, 2] * sin(eta) * sin(chi) + if is_small(top_for_mu) and is_small(bot_for_mu): + # chi == +-90, eta == 0/180 and therefore phi || mu cos(chi) == + # 0 and sin(eta) == 0 Experience shows that even though e.g. + # the z[2, 2] and z[1, 2] values used to calculate mu may be + # basically 0 (1e-34) their ratio in every case tested so far + # still remains valid and using them will result in a phi + # solution that is continuous with neighbouring positions. + # + # We cannot test phi minus mu here unfortunately as the final + # phi and mu solutions have not yet been chosen (they may be + # +-x or 180+-x). Otherwise we could choose a sensible solution + # here if the one found was incorrect. + + # tan(phi+eta)=v12/v11 from extensions_to_yous_paper.wxm + phi_minus_mu = -atan2(Z[2, 0], Z[1, 1]) + logger.debug( + 'Mu and phi cannot be chosen uniquely with chi so close ' + 'to +/-90 and eta so close 0 or 180.\n After the final ' + 'solution has been chose phi-mu should equal: %.3f', + phi_minus_mu * TODEG) + mu = atan2(-top_for_mu, -bot_for_mu) # (41) + + top_for_phi = Z[0, 1] * cos(eta) * cos(chi) - Z[0, 0] * sin(eta) + bot_for_phi = Z[0, 1] * sin(eta) + Z[0, 0] * cos(eta) * cos(chi) + phi = atan2(top_for_phi, bot_for_phi) # (42) + # if is_small(bot_for_phi) and is_small(top_for_phi): + # raise DiffcalcException( + # 'phi=%.3f cannot be known with confidence as top and ' + # 'bottom are both close to zero. chi=%.3f, eta=%.3f' + # % (mu * TODEG, chi * TODEG, eta * TODEG)) + yield mu, eta, chi, phi + + else: + raise DiffcalcException('Given angle must be one of phi, chi, eta or mu') + + def _calc_angles_given_three_sample_constraints( + self, h, k, l, wavelength, return_all_solutions, samp_constraints, + h_phi, theta): + + if not 'mu' in samp_constraints: + eta_ = self.constraints.sample['eta'] + chi_ = self.constraints.sample['chi'] + phi_ = self.constraints.sample['phi'] + try: + two_mu_qaz_pairs = _mu_and_qaz_from_eta_chi_phi(eta_, chi_, phi_, theta, h_phi) + except AssertionError: + return + else: + raise DiffcalcException( + 'No code yet to handle this combination of 3 sample constraints!') + # TODO: Code duplicated above + for mu_, qaz in two_mu_qaz_pairs: + logger.debug("--- Trying mu_:%.f qaz_%.f", mu_ * TODEG, qaz * TODEG) + for delta, nu, _ in self._calc_remaining_detector_angles('qaz', qaz, theta): + logger.info("delta=%.3f, %s=%.3f", delta * TODEG, NUNAME, nu * TODEG) + yield mu_, delta, nu, eta_, chi_, phi_ + + def _calc_sample_angles_given_two_sample_and_reference( + self, samp_constraints, psi, theta, q_phi, n_phi): + """Available combinations: + chi, phi, reference + mu, eta, reference, + chi, eta, reference + chi, mu, reference + """ + + def __get_phi_and_qaz(chi, eta, mu): + a = sin(chi) * cos(eta) + b = sin(chi) * sin(eta) * sin(mu) - cos(chi) * cos(mu) + #atan2_xi = atan2(V[2, 2] * a + V[2, 0] * b, + # V[2, 0] * a - V[2, 2] * b) # (54) + qaz = atan2(V[2, 0] * a - V[2, 2] * b, + -V[2, 2] * a - V[2, 0] * b) # (54) + + a = sin(chi) * sin(mu) - cos(mu) * cos(chi) * sin(eta) + b = cos(mu) * cos(eta) + phi = atan2(V[1, 1] * a - V[0, 1] * b, + V[0, 1] * a + V[1, 1] * b) # (55) + # if is_small(mu+pi/2) and is_small(eta) and False: + # phi_general = phi + # # solved in extensions_to_yous_paper.wxm + # phi = atan2(V[1, 1], V[0, 1]) + # logger.info("phi = %.3f or %.3f (std)", + # phi*TODEG, phi_general*TODEG ) + + return qaz, phi + + N_phi = _calc_N(q_phi, n_phi) + THETA = z_rotation(-theta) + PSI = x_rotation(psi) + + if 'chi' in samp_constraints and 'phi' in samp_constraints: + + chi = samp_constraints['chi'] + phi = samp_constraints['phi'] + + CHI = calcCHI(chi) + PHI = calcPHI(phi) + V = CHI * PHI * N_phi * PSI.T * THETA.T # (46) + + #atan2_xi = atan2(-V[2, 0], V[2, 2]) + #atan2_eta = atan2(-V[0, 1], V[1, 1]) + #atan2_mu = atan2(-V[2, 1], sqrt(V[2, 2] ** 2 + V[2, 0] ** 2)) + try: + asin_mu = asin(bound(-V[2, 1])) + except AssertionError: + return + for mu in [asin_mu, pi - asin_mu]: + sgn_cosmu = sign(cos(mu)) + #xi = atan2(-sgn_cosmu * V[2, 0], sgn_cosmu * V[2, 2]) + qaz = atan2(sgn_cosmu * V[2, 2], sgn_cosmu * V[2, 0], ) + eta = atan2(-sgn_cosmu * V[0, 1], sgn_cosmu * V[1, 1]) + yield qaz, psi, mu, eta, chi, phi + + elif 'mu' in samp_constraints and 'eta' in samp_constraints: + + mu = samp_constraints['mu'] + eta = samp_constraints['eta'] + + V = N_phi * PSI.T * THETA.T # (49) + try: + bot = bound(-V[2, 1] / sqrt(sin(eta) ** 2 * cos(mu) ** 2 + sin(mu) ** 2)) + except AssertionError: + return + if is_small(cos(mu) * sin(eta)): + eps = atan2(sin(eta) * cos(mu), sin(mu)) + chi_vals = [eps + acos(bot), eps - acos(bot)] + else: + eps = atan2(sin(mu), sin(eta) * cos(mu)) + chi_vals = [asin(bot) - eps, pi - asin(bot) - eps] # (52) + + ## Choose final chi solution here to obtain compatable xi and mu + ## TODO: This temporary solution works only for one case used on i07 + ## Return a list of possible solutions? + #if is_small(eta) and is_small(mu + pi / 2): + # for chi in _generate_transformed_values(chi_orig): + # if pi / 2 <= chi < pi: + # break + #else: + # chi = chi_orig + + for chi in chi_vals: + qaz, phi = __get_phi_and_qaz(chi, eta, mu) + yield qaz, psi, mu, eta, chi, phi + + elif 'chi' in samp_constraints and 'eta' in samp_constraints: + + chi = samp_constraints['chi'] + eta = samp_constraints['eta'] + + V = N_phi * PSI.T * THETA.T # (49) + try: + bot = bound(-V[2, 1] / sqrt(sin(eta) ** 2 * sin(chi) ** 2 + cos(chi) ** 2)) + except AssertionError: + return + if is_small(cos(chi)): + eps = atan2(cos(chi), sin(chi) * sin(eta)) + mu_vals = [eps + acos(bot), eps - acos(bot)] + else: + eps = atan2(sin(chi) * sin(eta), cos(chi)) + mu_vals = [asin(bot) - eps, pi - asin(bot) - eps] # (52) + + for mu in mu_vals: + qaz, phi = __get_phi_and_qaz(chi, eta, mu) + yield qaz, psi, mu, eta, chi, phi + + elif 'chi' in samp_constraints and 'mu' in samp_constraints: + + chi = samp_constraints['chi'] + mu = samp_constraints['mu'] + + V = N_phi * PSI.T * THETA.T # (49) + + try: + asin_eta = asin(bound((-V[2, 1] - cos(chi) * sin(mu)) / (sin(chi) * cos(mu)))) + except AssertionError: + return + + for eta in [asin_eta, pi - asin_eta]: + qaz, phi = __get_phi_and_qaz(chi, eta, mu) + yield qaz, psi, mu, eta, chi, phi + + else: + raise DiffcalcException( + 'No code yet to handle this combination of 2 sample ' + 'constraints and one reference!:' + str(samp_constraints)) + + def _calc_sample_angles_given_two_sample_and_detector( + self, samp_constraints, qaz, theta, q_phi, n_phi): + """Available combinations: + chi, phi, detector + mu, eta, detector + mu, phi, detector + """ + + N_phi = _calc_N(q_phi, n_phi) + + if 'mu' in samp_constraints and 'eta' in samp_constraints: + + mu = samp_constraints['mu'] + eta = samp_constraints['eta'] + + F = y_rotation(qaz - pi/2.) + THETA = z_rotation(-theta) + V = calcETA(eta).T * calcMU(mu).T * F * THETA # (56) + + try: + bot = bound(-V[1, 0] / sqrt(N_phi[0, 0]**2 + N_phi[1, 0]**2)) + eps = atan2(N_phi[1, 0], N_phi[0, 0]) + phi_vals = [asin(bot) + eps, pi - asin(bot) + eps] # (59) + except (AssertionError, ZeroDivisionError): + # For the case of (00l) reflection, where N_phi[0,0] = N_phi[1,0] = 0 + chi = atan2(V[0, 0] * N_phi[2, 0], V[2, 0] * N_phi[2, 0]) # (57) + sgn_denom = sign(N_phi[1, 1] * N_phi[0, 2] - N_phi[1, 2] * N_phi[0, 1]) + sin_phi = V[1, 1] * N_phi[1, 2] - V[1, 2] * N_phi[1, 1] + cos_phi = V[1, 1] * N_phi[0, 2] - V[1, 2] * N_phi[0, 1] + phi = atan2(sin_phi * sgn_denom, cos_phi * sgn_denom) + yield mu, eta, chi, phi + return + for phi in phi_vals: + a = N_phi[0, 0] * cos(phi) + N_phi[1, 0] * sin(phi) + chi = atan2(N_phi[2, 0] * V[0, 0] - a * V[2, 0], + N_phi[2, 0] * V[2, 0] + a * V[0, 0]) # (60) + yield mu, eta, chi, phi + + elif 'chi' in samp_constraints and 'phi' in samp_constraints: + + chi = samp_constraints['chi'] + phi = samp_constraints['phi'] + + CHI = calcCHI(chi) + PHI = calcPHI(phi) + V = CHI * PHI * N_phi # (62) + + try: + bot = bound(V[2, 0] / sqrt(cos(qaz) ** 2 * cos(theta) ** 2 + sin(theta) ** 2)) + except AssertionError: + return + eps = atan2(-cos(qaz) * cos(theta), sin(theta)) + for mu in [asin(bot) + eps, pi - asin(bot) + eps]: + a = cos(theta) * sin(qaz) + b = -cos(theta) * sin(mu) * cos(qaz) + cos(mu) * sin(theta) + eta = atan2(V[1, 0] * a + V[0, 0] * b, V[0, 0] * a - V[1, 0]* b) + + #a = -cos(mu) * cos(qaz) * sin(theta) + sin(mu) * cos(theta) + #b = cos(mu) * sin(qaz) + #psi = atan2(-V[2, 2] * a - V[2, 1] * b, V[2, 1] * a - V[2, 2] * b) + yield mu, eta, chi, phi + + elif 'mu' in samp_constraints and 'phi' in samp_constraints: + + mu = samp_constraints['mu'] + phi = samp_constraints['phi'] + + F = y_rotation(qaz - pi/2.) + THETA = z_rotation(-theta) + V = calcMU(mu).T * F * THETA + E = calcPHI(phi) * N_phi + + try: + bot = bound(-V[2, 0] / sqrt(E[0, 0]**2 + E[2, 0]**2)) + except AssertionError: + return + eps = atan2(E[2, 0], E[0, 0]) + for chi in [asin(bot) + eps, pi - asin(bot) + eps]: + a = E[0, 0] * cos(chi) + E[2, 0] * sin(chi) + eta = atan2(V[0, 0] * E[1, 0] - V[1, 0] * a, V[0, 0] * a + V[1, 0] * E[1, 0]) + yield mu, eta, chi, phi + else: + raise DiffcalcException( + 'No code yet to handle this combination of 2 sample ' + 'constraints and one detector!:' + str(samp_constraints)) + + def _filter_angle_limits(self, possible_solutions, filter_out_of_limits=True): + res = [] + angle_names = self._hardware.get_axes_names() + for possible_solution in possible_solutions: + hw_sol = [] + hw_possible_solution = self._geometry.internal_position_to_physical_angles(YouPosition(*possible_solution, unit='RAD')) + for name, value in zip(angle_names, hw_possible_solution): + hw_sol.append(self._hardware.cut_angle(name, value)) + if filter_out_of_limits: + is_in_limits = all([self._hardware.is_axis_value_within_limits(name, value) for name, value in zip(angle_names, hw_sol)]) + else: + is_in_limits = True + if is_in_limits: + sol = self._geometry.physical_angles_to_internal_position(tuple(hw_sol)) + sol.changeToRadians() + res.append(sol.totuple()) + return res + +def _mu_and_qaz_from_eta_chi_phi(eta, chi, phi, theta, h_phi): + + h_phi_norm = normalised(h_phi) # (68,69) + h1, h2, h3 = h_phi_norm[0, 0], h_phi_norm[1, 0], h_phi_norm[2, 0] + a = sin(chi) * h2 * sin(phi) + sin(chi) * h1 * cos(phi) - cos(chi) * h3 + b = (- cos(chi) * sin(eta) * h2 * sin(phi) + - cos(eta) * h1 * sin(phi) + cos(eta) * h2 * cos(phi) + - cos(chi) * sin(eta) * h1 * cos(phi) + - sin(chi) * sin(eta) * h3) + c = -sin(theta) + sin_bit = bound(c / sqrt(a * a + b * b)) + mu1 = asin(sin_bit) - atan2(b, a) + mu2 = pi - asin(sin_bit) - atan2(b, a) + + mu1 = cut_at_minus_pi(mu1) + mu2 = cut_at_minus_pi(mu2) + + # TODO: This special case should be *removed* when the general case has shown + # to encompass it. It exists as fallback for a particular i16 experiment in + # May 2013 --RobW. +# if eta == chi == 0: +# logger.debug("Testing against simplified equations for eta == chi == 0") +# a = - h3 +# b = - h1 * sin(phi) + h2 * cos(phi) +# sin_bit = bound(c / sqrt(a * a + b * b)) +# mu_simplified = pi - asin(sin_bit) - atan2(b, a) +# mu_simplified = cut_at_minus_pi(mu_simplified) +# if not ne(mu_simplified, mu): +# raise AssertionError("mu_simplified != mu , %f!=%f" % (mu_simplified, mu)) + + + [MU, _, _, ETA, CHI, PHI] = create_you_matrices(mu1, None, None, eta, chi, phi) + h_lab = MU * ETA * CHI * PHI * h_phi # (11) + qaz1 = atan2(h_lab[0, 0] , h_lab[2, 0]) + + [MU, _, _, ETA, CHI, PHI] = create_you_matrices(mu2, None, None, eta, chi, phi) + h_lab = MU * ETA * CHI * PHI * h_phi # (11) + qaz2 = atan2(h_lab[0, 0] , h_lab[2, 0]) + + return (mu1, qaz1) , (mu2, qaz2) diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/you/constraints.py b/script/test/diffcalc (copy)/diffcalc/hkl/you/constraints.py new file mode 100644 index 0000000..1fe08d7 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/you/constraints.py @@ -0,0 +1,377 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi + +try: + from numpy import matrix +except ImportError: + from numjy import matrix + +from diffcalc.util import DiffcalcException, bold + +TODEG = 180 / pi +TORAD = pi / 180 + +NUNAME = 'gam' + +def filter_dict(d, keys): + """Return a copy of d containing only keys that are in keys""" + ##return {k: d[k] for k in keys} # requires Python 2.6 + return dict((k, d[k]) for k in keys if k in d.keys()) + + +det_constraints = ('delta', NUNAME, 'qaz', 'naz') +ref_constraints = ('a_eq_b', 'alpha', 'beta', 'psi') +samp_constraints = ('mu', 'eta', 'chi', 'phi', 'mu_is_' + NUNAME) + +valueless_constraints = ('a_eq_b', 'mu_is_' + NUNAME) +all_constraints = det_constraints + ref_constraints + samp_constraints + + +number_single_sample = (len(det_constraints) * len(ref_constraints) * + len(samp_constraints)) + + +class YouConstraintManager(object): + + def __init__(self, hardware, fixed_constraints = {}): + self._hardware = hardware + self._constrained = {} +# self._tracking = [] + self.n_phi = matrix([[0], [0], [1]]) + self._hide_detector_constraint = False # default + self._fixed_samp_constraints = () + self._fix_constraints(fixed_constraints) + + def __str__(self): + lines = [] +# TODO: Put somewhere with access to UB matrix! +# WIDTH = 13 +# n_phi = self.n_phi +# fmt = "% 9.5f % 9.5f % 9.5f" +# lines.append(" n_phi:".ljust(WIDTH) + +# fmt % (n_phi[0, 0], n_phi[1, 0], n_phi[2, 0])) +# if self._getUBMatrix(): +# n_cryst = self._getUMatrix().I * self.n_phi +# lines.append(" n_cryst:".ljust(WIDTH) + +# fmt % (n_cryst[0, 0], n_cryst[1, 0], n_cryst[2, 0])) +# n_recip = self._getUBMatrix().I * self.n_phi +# lines.append(" n_recip:".ljust(WIDTH) + +# fmt % (n_recip[0, 0], n_recip[1, 0], n_recip[2, 0])) +# else: +# lines.append( +# " n_cryst:".ljust(WIDTH) + ' "<<< No U matrix >>>"') +# lines.append( +# " n_recip:".ljust(WIDTH) + ' "<<< No UB matrix >>>"') + + lines.extend(self.build_display_table_lines()) + lines.append("") + lines.extend(self.report_constraints_lines()) + lines.append("") + if (self.is_fully_constrained() and + not self.is_current_mode_implemented()): + lines.append( + " Sorry, this constraint combination is not implemented") + lines.append(" Type 'help con' for available combinations") + else: + lines.append(" Type 'help con' for instructions") # okay + return '\n'.join(lines) + + @property + def available_constraint_names(self): + """list of all available constraints""" + return all_constraints + + @property + def settable_constraint_names(self): + """list of all available constraints that have settable values""" + all_copy = list(all_constraints) + for valueless in valueless_constraints: + all_copy.remove(valueless) + return all_copy + + @property + def all(self): # @ReservedAssignment + """dictionary of all constrained values""" + return self._constrained.copy() + + @property + def detector(self): + """dictionary of constrained detector circles""" + return filter_dict(self.all, det_constraints[:-1]) + + @property + def reference(self): + """dictionary of constrained reference circles""" + return filter_dict(self.all, ref_constraints) + + @property + def sample(self): + """dictionary of constrained sample circles""" + return filter_dict(self.all, samp_constraints) + + @property + def naz(self): + """dictionary with naz and value if constrained""" + return filter_dict(self.all, ('naz',)) + + @property + def constrained_names(self): + """ordered tuple of constained circles""" + names = self.all.keys() + names.sort(key=lambda name: list(all_constraints).index(name)) + return tuple(names) + + def _fix_constraints(self, fixed_constraints): + for name in fixed_constraints: + self.constrain(name) + self.set_constraint(name, fixed_constraints[name]) + + if self.detector or self.naz: + self._hide_detector_constraint = True + + fixed_samp_constraints = list(self.sample.keys()) + if 'mu' in self.sample or NUNAME in self.detector: + fixed_samp_constraints.append('mu_is_' + NUNAME) + self._fixed_samp_constraints = tuple(fixed_samp_constraints) + + + def is_constrained(self, name): + return name in self._constrained + + def get_value(self, name): + return self._constrained[name] + + def build_display_table_lines(self): + unfixed_samp_constraints = list(samp_constraints) + for name in self._fixed_samp_constraints: + unfixed_samp_constraints.remove(name) + if self._hide_detector_constraint: + constraint_types = (ref_constraints, unfixed_samp_constraints) + else: + constraint_types = (det_constraints, ref_constraints, + unfixed_samp_constraints) + num_rows = max([len(col) for col in constraint_types]) + max_name_width = max( + [len(name) for name in sum(constraint_types[:-1], ())]) + + cells = [] + + header_cells = [] + if not self._hide_detector_constraint: + header_cells.append(bold(' ' + 'DET'.ljust(max_name_width))) + header_cells.append(bold(' ' + 'REF'.ljust(max_name_width))) + header_cells.append(bold(' ' + 'SAMP')) + cells.append(header_cells) + + underline_cells = [' ' + '-' * max_name_width] * len(constraint_types) + cells.append(underline_cells) + + for n_row in range(num_rows): + row_cells = [] + for col in constraint_types: + name = col[n_row] if n_row < len(col) else '' + row_cells.append(self._label_constraint(name)) + row_cells.append(('%-' + str(max_name_width) + 's') % name) + cells.append(row_cells) + + lines = [' '.join(row_cells).rstrip() for row_cells in cells] + return lines + + def _report_constraint(self, name): + val = self.get_constraint(name) + if name in valueless_constraints: + return " %s" % name + else: + if val is None: + return "! %-5s: ---" % name + else: + return " %-5s: %.4f" % (name, val) + + def report_constraints_lines(self): + lines = [] + required = 3 - len(self.all) + if required == 0: + pass + elif required == 1: + lines.append('! 1 more constraint required') + else: + lines.append('! %d more constraints required' % required) + constraints = [] + constraints.extend(self.detector.keys()) + constraints.extend(self.naz.keys()) + constraints.extend(self.reference.keys()) + constraints.extend(sorted(self.sample.keys())) + for name in constraints: + lines.append(self._report_constraint(name)) + return lines + + def is_fully_constrained(self): + return len(self.all) == 3 + + def is_current_mode_implemented(self): + if not self.is_fully_constrained(): + raise ValueError("Three constraints required") + + if len(self.sample) == 3: + if set(self.sample.keys()) == set(['chi', 'phi', 'eta']): + return True + return False + + if len(self.sample) == 1: + return True + + if len(self.reference) == 1: + return (set(self.sample.keys()) == set(['chi', 'phi']) or + set(self.sample.keys()) == set(['chi', 'eta']) or + set(self.sample.keys()) == set(['chi', 'mu']) or + set(self.sample.keys()) == set(['mu', 'eta'])) + + if len(self.detector) == 1: + return (set(self.sample.keys()) == set(['chi', 'phi']) or + set(self.sample.keys()) == set(['mu', 'eta']) or + set(self.sample.keys()) == set(['mu', 'phi']) + ) + + return False + + + def _label_constraint(self, name): + if name == '': + label = ' ' +# elif self.is_tracking(name): # implies constrained +# label = '~~> ' + elif (self.is_constrained(name) and (self.get_value(name) is None) and + name not in valueless_constraints): + label = 'o->' + elif self.is_constrained(name): + label = '-->' + else: + label = ' ' + return label + + def constrain(self, name): + if self.is_constraint_fixed(name): + raise DiffcalcException('%s is not a valid constraint name' % name) + if name in self.all: + return "%s is already constrained." % name.capitalize() + elif name in det_constraints: + return self._constrain_detector(name) + elif name in ref_constraints: + return self._constrain_reference(name) + elif name in samp_constraints: + return self._constrain_sample(name) + else: + raise DiffcalcException("%s is not a valid constraint name. Type 'con' for a table of constraint name" % name) + + def is_constraint_fixed(self, name): + return ((name in det_constraints and self._hide_detector_constraint) or + (name in samp_constraints and name in self._fixed_samp_constraints)) + + def _constrain_detector(self, name): + if self.naz: + del self._constrained['naz'] + self._constrained[name] = None + return 'Naz constraint replaced.' + elif self.detector: + constrained_name = self.detector.keys()[0] + del self._constrained[constrained_name] + self._constrained[name] = None + return'%s constraint replaced.' % constrained_name.capitalize() + elif len(self.all) == 3: # and no detector + raise self._could_not_constrain_exception(name) + else: + self._constrained[name] = None + + def _could_not_constrain_exception(self, name): + return DiffcalcException( + "%s could not be constrained. First un-constrain one of the\n" + "angles %s, %s or %s (with 'uncon')" % + ((name.capitalize(),) + self.constrained_names)) + + def _constrain_reference(self, name): + if self.reference: + constrained_name = self.reference.keys()[0] + del self._constrained[constrained_name] + self._constrained[name] = None + return '%s constraint replaced.' % constrained_name.capitalize() + elif len(self.all) == 3: # and no reference + raise self._could_not_constrain_exception(name) + else: + self._constrained[name] = None + + def _constrain_sample(self, name): + if len(self._constrained) < 3: + # okay, more to add + self._constrained[name] = None + # else: three constraints are set + elif len(self.sample) == 1: + # (detector and reference constraints set) + # it is clear which sample constraint to remove + constrained_name = self.sample.keys()[0] + del self._constrained[constrained_name] + self._constrained[name] = None + return '%s constraint replaced.' % constrained_name.capitalize() + else: + raise self._could_not_constrain_exception(name) + + def unconstrain(self, name): + if self.is_constraint_fixed(name): + raise DiffcalcException('%s is not a valid constraint name') + if name in self._constrained: + del self._constrained[name] + else: + return "%s was not already constrained." % name.capitalize() + + def _check_constraint_settable(self, name): + if name not in all_constraints: + raise DiffcalcException( + 'Could not set %(name)s. This is not an available ' + 'constraint.' % locals()) + elif name not in self.all.keys(): + raise DiffcalcException( + 'Could not set %(name)s. This is not currently ' + 'constrained.' % locals()) + elif name in valueless_constraints: + raise DiffcalcException( + 'Could not set %(name)s. This constraint takes no ' + 'value.' % locals()) + + def clear_constraints(self): + self._constrained = {} + + def set_constraint(self, name, value): # @ReservedAssignment + if self.is_constraint_fixed(name): + raise DiffcalcException('%s is not a valid constraint name') + self._check_constraint_settable(name) +# if name in self._tracking: +# raise DiffcalcException( +# "Could not set %s as this constraint is configured to track " +# "its associated\nphysical angle. First remove this tracking " +# "(use 'untrack %s').""" % (name, name)) + old_value = self.get_constraint(name) + old = str(old_value) if old_value is not None else '---' + self._constrained[name] = float(value) * TORAD + new = str(value) + return "%(name)s : %(old)s --> %(new)s" % locals() + + def get_constraint(self, name): + value = self.all[name] + return None if value is None else value * TODEG + \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/you/geometry.py b/script/test/diffcalc (copy)/diffcalc/hkl/you/geometry.py new file mode 100644 index 0000000..d8d034a --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/you/geometry.py @@ -0,0 +1,219 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi + +from diffcalc.util import AbstractPosition, DiffcalcException + +TORAD = pi / 180 +TODEG = 180 / pi +from diffcalc.util import x_rotation, z_rotation, y_rotation + +from diffcalc.hkl.you.constraints import NUNAME + +class YouGeometry(object): + + def __init__(self, name, fixed_constraints, beamline_axes_transform=None): + self.name = name + self.fixed_constraints = fixed_constraints + # beamline_axes_transform matrix is composed of the diffcalc basis vector coordinates + # in the beamline coordinate system, i.e. it transforms the beamline coordinate system + # into the reference diffcalc one. + self.beamline_axes_transform = beamline_axes_transform + + def physical_angles_to_internal_position(self, physical_angle_tuple): + raise NotImplementedError() + + def internal_position_to_physical_angles(self, internal_position): + raise NotImplementedError() + + def create_position(self, *args): + return YouPosition(*args, unit='DEG') + + +#============================================================================== +#============================================================================== +# Geometry plugins for use with 'You' hkl calculation engine +#============================================================================== +#============================================================================== + + +class SixCircle(YouGeometry): + def __init__(self, beamline_axes_transform=None): + YouGeometry.__init__(self, 'sixc', {}, beamline_axes_transform) + + def physical_angles_to_internal_position(self, physical_angle_tuple): + # mu, delta, nu, eta, chi, phi + return YouPosition(*physical_angle_tuple, unit='DEG') + + def internal_position_to_physical_angles(self, internal_position): + clone_position = internal_position.clone() + clone_position.changeToDegrees() + return clone_position.totuple() + + +class FourCircle(YouGeometry): + """For a diffractometer with angles: + delta, eta, chi, phi + """ + def __init__(self, beamline_axes_transform=None): + YouGeometry.__init__(self, 'fourc', {'mu': 0, NUNAME: 0}, beamline_axes_transform) + + def physical_angles_to_internal_position(self, physical_angle_tuple): + # mu, delta, nu, eta, chi, phi + delta, eta, chi, phi = physical_angle_tuple + return YouPosition(0, delta, 0, eta, chi, phi, 'DEG') + + def internal_position_to_physical_angles(self, internal_position): + clone_position = internal_position.clone() + clone_position.changeToDegrees() + _, delta, _, eta, chi, phi = clone_position.totuple() + return delta, eta, chi, phi + + +class FiveCircle(YouGeometry): + """For a diffractometer with angles: + delta, nu, eta, chi, phi + """ + def __init__(self, beamline_axes_transform=None): + YouGeometry.__init__(self, 'fivec', {'mu': 0}, beamline_axes_transform) + + def physical_angles_to_internal_position(self, physical_angle_tuple): + # mu, delta, nu, eta, chi, phi + delta, nu, eta, chi, phi = physical_angle_tuple + return YouPosition(0, delta, nu, eta, chi, phi, 'DEG') + + def internal_position_to_physical_angles(self, internal_position): + clone_position = internal_position.clone() + clone_position.changeToDegrees() + _, delta, nu, eta, chi, phi = clone_position.totuple() + return delta, nu, eta, chi, phi + + +#============================================================================== + + +def create_you_matrices(mu=None, delta=None, nu=None, eta=None, chi=None, + phi=None): + """ + Create transformation matrices from H. You's paper. + """ + MU = None if mu is None else calcMU(mu) + DELTA = None if delta is None else calcDELTA(delta) + NU = None if nu is None else calcNU(nu) + ETA = None if eta is None else calcETA(eta) + CHI = None if chi is None else calcCHI(chi) + PHI = None if phi is None else calcPHI(phi) + return MU, DELTA, NU, ETA, CHI, PHI + + +def calcNU(nu): + return x_rotation(nu) + + +def calcDELTA(delta): + return z_rotation(-delta) + + +def calcMU(mu_or_alpha): + return x_rotation(mu_or_alpha) + + +def calcETA(eta): + return z_rotation(-eta) + + +def calcCHI(chi): + return y_rotation(chi) + + +def calcPHI(phi): + return z_rotation(-phi) + + +def you_position_names(): + return ('mu', 'delta', NUNAME, 'eta', 'chi', 'phi') + +class YouPosition(AbstractPosition): + + def __init__(self, mu, delta, nu, eta, chi, phi, unit): + self.mu = mu + self.delta = delta + self.nu = nu + self.eta = eta + self.chi = chi + self.phi = phi + if unit not in ['DEG', 'RAD']: + raise DiffcalcException("Invalid angle unit value %s." % str(unit)) + else: + self.unit = unit + + def clone(self): + return YouPosition(self.mu, self.delta, self.nu, self.eta, self.chi, + self.phi, self.unit) + + def changeToRadians(self): + if self.unit == 'DEG': + self.mu *= TORAD + self.delta *= TORAD + self.nu *= TORAD + self.eta *= TORAD + self.chi *= TORAD + self.phi *= TORAD + self.unit = 'RAD' + elif self.unit == 'RAD': + return + else: + raise DiffcalcException("Invalid angle unit value %s." % str(self.unit)) + + def changeToDegrees(self): + if self.unit == 'RAD': + self.mu *= TODEG + self.delta *= TODEG + self.nu *= TODEG + self.eta *= TODEG + self.chi *= TODEG + self.phi *= TODEG + self.unit = 'DEG' + elif self.unit == 'DEG': + return + else: + raise DiffcalcException("Invalid angle unit value %s." % str(self.unit)) + + def totuple(self): + return (self.mu, self.delta, self.nu, self.eta, self.chi, self.phi) + + def __str__(self): + mu, delta, nu, eta, chi, phi = self.totuple() + return ("YouPosition(mu %r delta: %r nu: %r eta: %r chi: %r phi: %r) in %s" + % (mu, delta, nu, eta, chi, phi, self.unit)) + + def __eq__(self, other): + return self.totuple() == other.totuple() + + +class WillmottHorizontalPosition(YouPosition): + + def __init__(self, delta=None, gamma=None, omegah=None, phi=None): + self.mu = 0 + self.delta = delta + self.nu = gamma + self.eta = omegah + self.chi = -90 + self.phi = phi + self.unit= 'DEG' diff --git a/script/test/diffcalc (copy)/diffcalc/hkl/you/hkl.py b/script/test/diffcalc (copy)/diffcalc/hkl/you/hkl.py new file mode 100644 index 0000000..fe3e806 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/hkl/you/hkl.py @@ -0,0 +1,187 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from __future__ import absolute_import + +from diffcalc.hkl.common import getNameFromScannableOrString +from diffcalc.util import command +from diffcalc.hkl.you.calc import YouHklCalculator +from diffcalc import settings + + + +import diffcalc.ub.ub +from diffcalc.hkl.you.constraints import YouConstraintManager + +__all__ = ['allhkl', 'con', 'uncon', 'hklcalc', 'constraint_manager'] + + +_fixed_constraints = settings.geometry.fixed_constraints # @UndefinedVariable + +constraint_manager = YouConstraintManager(settings.hardware, _fixed_constraints) + +hklcalc = YouHklCalculator( + diffcalc.ub.ub.ubcalc, settings.geometry, settings.hardware, constraint_manager) + + +def __str__(self): + return hklcalc.__str__() + +@command +def con(*args): + """ + con -- list available constraints and values + con {val} -- constrains and optionally sets one constraint + con {val} {val} {val} -- clears and then fully constrains + + Select three constraints using 'con' and 'uncon'. Choose up to one + from each of the sample and detector columns and up to three from + the sample column. + + Not all constraint combinations are currently available: + + 1 x samp: all 80 of 80 + + 2 x samp and 1 x ref: chi & phi + chi & eta + chi & mu + mu & eta (4 of 6) + + 2 x samp and 1 x det: chi & phi + mu & eta + mu & phi (3 of 6) + + 3 x samp: eta, chi & phi (1 of 4) + + See also 'uncon' + """ + args = list(args) + msg = _handle_con(args) + if (hklcalc.constraints.is_fully_constrained() and + not hklcalc.constraints.is_current_mode_implemented()): + msg += ("\n\nWARNING:. The selected constraint combination is valid but " + "is not implemented.\n\nType 'help con' to see implemented combinations") + + if msg: + print msg + +def _handle_con(args): + if not args: + return hklcalc.constraints.__str__() + + if len(args) > 6: + raise TypeError("Unexpected args: " + str(args)) + + cons_value_pairs = [] + while args: + scn_or_str = args.pop(0) + name = getNameFromScannableOrString(scn_or_str) + if args and isinstance(args[0], (int, long, float)): + value = args.pop(0) + else: + try: + value = settings.hardware.get_position_by_name(name) + except ValueError: + value = None + cons_value_pairs.append((name, value)) + + if len(cons_value_pairs) == 1: + pass + elif len(cons_value_pairs) == 3: + hklcalc.constraints.clear_constraints() + else: + raise TypeError("Either one or three constraints must be specified") + for name, value in cons_value_pairs: + hklcalc.constraints.constrain(name) + if value is not None: + hklcalc.constraints.set_constraint(name, value) + return '\n'.join(hklcalc.constraints.report_constraints_lines()) + + +@command +def uncon(scn_or_string): + """uncon -- remove constraint + + See also 'con' + """ + name = getNameFromScannableOrString(scn_or_string) + hklcalc.constraints.unconstrain(name) + print '\n'.join(hklcalc.constraints.report_constraints_lines()) + +@command +def allhkl(hkl, wavelength=None): + """allhkl [h k l] -- print all hkl solutions ignoring limits + + """ + hardware = hklcalc._hardware + geometry = hklcalc._geometry + if wavelength is None: + wavelength = hardware.get_wavelength() + h, k, l = hkl + pos_virtual_angles_pairs = hklcalc.hkl_to_all_angles( + h, k, l, wavelength) + cells = [] + # virtual_angle_names = list(pos_virtual_angles_pairs[0][1].keys()) + # virtual_angle_names.sort() + virtual_angle_names = ['qaz', 'psi', 'naz', 'tau', 'theta', 'alpha', 'beta'] + header_cells = list(hardware.get_axes_names()) + [' '] + virtual_angle_names + cells.append(['%9s' % s for s in header_cells]) + cells.append([''] * len(header_cells)) + + + for pos, virtual_angles in pos_virtual_angles_pairs: + row_cells = [] + + + angle_tuple = geometry.internal_position_to_physical_angles(pos) + angle_tuple = hardware.cut_angles(angle_tuple) + for val in angle_tuple: + row_cells.append('%9.4f' % val) + + row_cells.append('|') + + for name in virtual_angle_names: + row_cells.append('%9.4f' % virtual_angles[name]) + cells.append(row_cells) + + + column_widths = [] + for col in range(len(cells[0])): + widths = [] + for row in range(len(cells)): + cell = cells[row][col] + width = len(cell.strip()) + widths.append(width) + column_widths.append(max(widths)) + + lines = [] + for row_cells in cells: + trimmed_row_cells = [] + for cell, width in zip(row_cells, column_widths): + trimmed_cell = cell.strip().rjust(width) + trimmed_row_cells.append(trimmed_cell) + lines.append(' '.join(trimmed_row_cells)) + print '\n'.join(lines) + + +commands_for_help = ['Constraints', + con, + uncon, + 'Hkl', + allhkl + ] diff --git a/script/test/diffcalc (copy)/diffcalc/log.py b/script/test/diffcalc (copy)/diffcalc/log.py new file mode 100644 index 0000000..ac4f1af --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/log.py @@ -0,0 +1,27 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from __future__ import absolute_import + +import logging +import getpass + +logging.basicConfig(format="%(asctime)s %(levelname)s:%(name)s:%(message)s", + datefmt='%m/%d/%Y %I:%M:%S', + filename='/tmp/diffcalc_%s.log' % getpass.getuser(), + level=logging.DEBUG) diff --git a/script/test/diffcalc (copy)/diffcalc/settings.py b/script/test/diffcalc (copy)/diffcalc/settings.py new file mode 100644 index 0000000..5221212 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/settings.py @@ -0,0 +1,24 @@ +''' +Created on Aug 5, 2013 + +@author: walton +''' +import os + +from diffcalc.ub.persistence import UbCalculationNonPersister + +# These should be by the user *before* importing other modules +geometry = None +hardware = None +ubcalc_persister = UbCalculationNonPersister() + +axes_scannable_group = None +energy_scannable = None +energy_scannable_multiplier_to_get_KeV=1 + + +# These will be set by dcyou, dcvlieg or dcwillmot +ubcalc_strategy = None +angles_to_hkl_function = None # Used by checkub to avoid coupling it to an hkl module +include_sigtau=False +include_reference=False \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/ub/__init__.py b/script/test/diffcalc (copy)/diffcalc/ub/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcalc/ub/calc.py b/script/test/diffcalc (copy)/diffcalc/ub/calc.py new file mode 100644 index 0000000..d71bc5e --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/calc.py @@ -0,0 +1,854 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from diffcalc.ub.calcstate import decode_ubcalcstate +from diffcalc.ub.calcstate import UBCalcState +from diffcalc.ub.crystal import CrystalUnderTest +from diffcalc.ub.reflections import ReflectionList +from diffcalc.ub.persistence import UBCalculationJSONPersister, UBCalculationPersister +from diffcalc.util import DiffcalcException, cross3, dot3, bold, xyz_rotation,\ + bound +from math import acos, cos, sin, pi +from diffcalc.ub.reference import YouReference +from diffcalc.ub.orientations import OrientationList + +try: + from numpy import matrix, hstack + from numpy.linalg import norm +except ImportError: + from numjy import matrix, hstack + from numjy.linalg import norm + +SMALL = 1e-7 +TODEG = 180 / pi + +WIDTH = 13 + +def z(num): + """Round to zero if small. This is useful to get rid of erroneous + minus signs resulting from float representation close to zero. + """ + if abs(num) < SMALL: + num = 0 + return num + +#The UB matrix is used to find or set the orientation of a set of +#planes described by an hkl vector. The U matrix can be used to find +#or set the orientation of the crystal lattices' y axis. If there is +#crystal miscut the crystal lattices y axis is not parallel to the +#crystals optical surface normal. For surface diffraction experiments, +#where not only the crystal lattice must be oriented appropriately but +#so must the crystal's optical surface, two angles tau and sigma are +#used to describe the difference between the two. Sigma is (minus) the +#ammount of chi axis rotation and tau (minus) the ammount of phi axis +#rotation needed to move the surface normal into the direction of the + + +class PaperSpecificUbCalcStrategy(object): + + def calculate_q_phi(self, pos): + """Calculate hkl in the phi frame in units of 2 * pi / lambda from + pos object in radians""" + raise NotImplementedError() + + +class UBCalculation: + """A UB matrix calculation for an experiment. + + Contains the parameters for the _crystal under test, a list of measured + reflections and, if its been calculated, a UB matrix to be used by the rest + of the code. + """ + + def __init__(self, hardware, diffractometerPluginObject, + persister, strategy, include_sigtau=True, include_reference=True): + + # The diffractometer geometry is required to map the internal angles + # into those used by this diffractometer (for display only) + + self._hardware = hardware + self._geometry = diffractometerPluginObject + self._persister = persister + self._strategy = strategy + self.include_sigtau = include_sigtau + self.include_reference = include_reference + try: + self._ROT = diffractometerPluginObject.beamline_axes_transform + except AttributeError: + self._ROT = None + self._clear() + + def _get_diffractometer_axes_names(self): + return self._hardware.get_axes_names() + + def _clear(self, name=None): + # NOTE the Diffraction calculator is expecting this object to exist in + # the long run. We can't remove this entire object, and recreate it. + # It also contains a required link to the angle calculator. + reflist = ReflectionList(self._geometry, self._get_diffractometer_axes_names()) + orientlist = OrientationList() + reference = YouReference(self._get_UB) + self._state = UBCalcState(name=name, reflist=reflist, orientlist=orientlist, reference=reference) + self._U = None + self._UB = None + self._state.configure_calc_type() + +### State ### + def start_new(self, name): + """start_new(name) --- creates a new blank ub calculation""" + # Create storage object if name does not exist (TODO) + if name in self._persister.list(): + print ("No UBCalculation started: There is already a calculation " + "called: " + name) + print "Saved calculations: " + repr(self._persister.list()) + return + self._clear(name) + self.save() + + def load(self, name): + state = self._persister.load(name) + if isinstance(self._persister, UBCalculationJSONPersister): + self._state = decode_ubcalcstate(state, self._geometry, self._get_diffractometer_axes_names()) + self._state.reference.get_UB = self._get_UB + elif isinstance(self._persister, UBCalculationPersister): + self._state = state + else: + raise Exception('Unexpected persister type: ' + str(self._persister)) + if self._state.manual_U is not None: + self._U = self._state.manual_U + self._UB = self._U * self._state.crystal.B + self.save() + elif self._state.manual_UB is not None: + self._UB = self._state.manual_UB + self.save() + elif self._state.or0 is not None: + if self._state.or1 is None: + self.calculate_UB_from_primary_only() + else: + if self._state.reflist: + self.calculate_UB() + elif self._state.orientlist: + self.calculate_UB_from_orientation() + else: + pass + else: + pass + + def save(self): + self.saveas(self._state.name) + + def saveas(self, name): + self._state.name = name + self._persister.save(self._state, name) + + def listub(self): + return self._persister.list() + + def listub_metadata(self): + return self._persister.list_metadata() + + def remove(self, name): + self._persister.remove(name) + if self._state == name: + self._clear(name) + + def getState(self): + return self._state.getState() + + def __str__(self): + + if self._state.name is None: + return "<<< No UB calculation started >>>" + lines = [] + lines.append(bold("UBCALC")) + lines.append("") + lines.append( + " name:".ljust(WIDTH) + self._state.name.rjust(9)) + + if self.include_sigtau: + lines.append("") + lines.append( + " sigma:".ljust(WIDTH) + ("% 9.5f" % self._state.sigma).rjust(9)) + lines.append( + " tau:".ljust(WIDTH) + ("% 9.5f" % self._state.tau).rjust(9)) + + if self.include_reference: + lines.append("") + ub_calculated = self._UB is not None + lines.extend(self._state.reference.repr_lines(ub_calculated, WIDTH, self._ROT)) + + lines.append("") + lines.append(bold("CRYSTAL")) + lines.append("") + + if self._state.crystal is None: + lines.append(" <<< none specified >>>") + else: + lines.extend(self._state.crystal.str_lines()) + + lines.append("") + lines.append(bold("UB MATRIX")) + lines.append("") + + if self._UB is None: + lines.append(" <<< none calculated >>>") + else: + lines.extend(self.str_lines_u()) + lines.append("") + lines.extend(self.str_lines_u_angle_and_axis()) + lines.append("") + lines.extend(self.str_lines_ub()) + + lines.append("") + lines.append(bold("REFLECTIONS")) + lines.append("") + + lines.extend(self._state.reflist.str_lines()) + + lines.append("") + lines.append(bold("CRYSTAL ORIENTATIONS")) + lines.append("") + + lines.extend(self._state.orientlist.str_lines(R=self._ROT)) + + return '\n'.join(lines) + + def str_lines_u(self): + lines = [] + fmt = "% 9.5f % 9.5f % 9.5f" + try: + U = self._ROT.I * self.U * self._ROT + except AttributeError: + U = self.U + lines.append(" U matrix:".ljust(WIDTH) + + fmt % (z(U[0, 0]), z(U[0, 1]), z(U[0, 2]))) + lines.append(' ' * WIDTH + fmt % (z(U[1, 0]), z(U[1, 1]), z(U[1, 2]))) + lines.append(' ' * WIDTH + fmt % (z(U[2, 0]), z(U[2, 1]), z(U[2, 2]))) + return lines + + def str_lines_u_angle_and_axis(self): + lines = [] + fmt = "% 9.5f % 9.5f % 9.5f" + y = matrix('0; 0; 1') + try: + rotation_axis = cross3(self._ROT * y, self._ROT * self.U * y) + except TypeError: + rotation_axis = cross3(y, self.U * y) + if abs(norm(rotation_axis)) < SMALL: + lines.append(" miscut angle:".ljust(WIDTH) + " 0") + else: + rotation_axis = rotation_axis * (1 / norm(rotation_axis)) + cos_rotation_angle = dot3(y, self.U * y) + rotation_angle = acos(cos_rotation_angle) + + lines.append(" miscut:") + lines.append(" angle:".ljust(WIDTH) + "% 9.5f" % (rotation_angle * TODEG)) + lines.append(" axis:".ljust(WIDTH) + fmt % tuple((rotation_axis.T).tolist()[0])) + + return lines + + def str_lines_ub(self): + lines = [] + fmt = "% 9.5f % 9.5f % 9.5f" + try: + RI = self._ROT.I + B = self._state.crystal.B + UB = RI * self.UB * B.I * self._ROT * B + except AttributeError: + UB = self.UB + lines.append(" UB matrix:".ljust(WIDTH) + + fmt % (z(UB[0, 0]), z(UB[0, 1]), z(UB[0, 2]))) + lines.append(' ' * WIDTH + fmt % (z(UB[1, 0]), z(UB[1, 1]), z(UB[1, 2]))) + lines.append(' ' * WIDTH + fmt % (z(UB[2, 0]), z(UB[2, 1]), z(UB[2, 2]))) + return lines + + @property + def name(self): + return self._state.name +### Lattice ### + + def set_lattice(self, name, *shortform): + """ + Converts a list shortform crystal parameter specification to a six-long + tuple returned as . Returns None if wrong number of input args. See + set_lattice() for a description of the shortforms supported. + + shortformLattice -- a tuple as follows: + [a] - assumes cubic + [a,b]) - assumes tetragonal + [a,b,c]) - assumes ortho + [a,b,c,gam]) - assumes mon/hex gam different from 90. + [a,b,c,alp,bet,gam]) - for arbitrary + where all measurements in angstroms and angles in degrees + """ + self._set_lattice_without_saving(name, *shortform) + self.save() + + def _set_lattice_without_saving(self, name, *shortform): + sf = shortform + if len(sf) == 1: + fullform = (sf[0], sf[0], sf[0], 90., 90., 90.) # cubic + elif len(sf) == 2: + fullform = (sf[0], sf[0], sf[1], 90., 90., 90.) # tetragonal + elif len(sf) == 3: + fullform = (sf[0], sf[1], sf[2], 90., 90., 90.) # ortho + elif len(sf) == 4: + fullform = (sf[0], sf[1], sf[2], 90., 90., sf[3]) # mon/hex gam + # not 90 + elif len(sf) == 5: + raise ValueError("wrong length input to set_lattice") + elif len(sf) == 6: + fullform = sf # triclinic/arbitrary + else: + raise ValueError("wrong length input to set_lattice") + self._set_lattice(name, *fullform) + + def _set_lattice(self, name, a, b, c, alpha, beta, gamma): + """set lattice parameters in degrees""" + if self._state.name is None: + raise DiffcalcException( + "Cannot set lattice until a UBCalcaluation has been started " + "with newubcalc") + self._state.crystal = CrystalUnderTest(name, a, b, c, alpha, beta, gamma) + # Clear U and UB if these exist + if self._U is not None: # (UB will also exist) + print "Warning: the old UB calculation has been cleared." + print " Use 'calcub' to recalculate with old reflections or" + print " 'orientub' to recalculate with old orientations." + +### Surface normal stuff ### + + def _gettau(self): + """ + Returns tau (in degrees): the (minus) ammount of phi axis rotation , + that together with some chi axis rotation (minus sigma) brings the + optical surface normal parallel to the omega axis. + """ + return self._state.tau + + def _settau(self, tau): + self._state.tau = tau + self.save() + + tau = property(_gettau, _settau) + + def _getsigma(self): + """ + Returns sigma (in degrees): the (minus) ammount of phi axis rotation , + that together with some phi axis rotation (minus tau) brings the + optical surface normal parallel to the omega axis. + """ + return self._state.sigma + + def _setsigma(self, sigma): + self.state._sigma = sigma + self.save() + + sigma = property(_getsigma, _setsigma) + + +### Reference vector ### + + def _get_n_phi(self): + return self._state.reference.n_phi + + n_phi = property(_get_n_phi) + + def set_n_phi_configured(self, n_phi): + try: + self._state.reference.n_phi_configured = self._ROT.I * n_phi + except AttributeError: + self._state.reference.n_phi_configured = n_phi + self.save() + + def set_n_hkl_configured(self, n_hkl): + self._state.reference.n_hkl_configured = n_hkl + self.save() + + def print_reference(self): + print '\n'.join(self._state.reference.repr_lines(self.is_ub_calculated(), R=self._ROT)) + +### Reflections ### + + def add_reflection(self, h, k, l, position, energy, tag, time): + """add_reflection(h, k, l, position, tag=None) -- adds a reflection + + position is in degrees and in the systems internal representation. + """ + if self._state.reflist is None: + raise DiffcalcException("No UBCalculation loaded") + self._state.reflist.add_reflection(h, k, l, position, energy, tag, time) + self.save() # incase autocalculateUbAndReport fails + + # If second reflection has just been added then calculateUB + if len(self._state.reflist) == 2: + self._autocalculateUbAndReport() + self.save() + + def edit_reflection(self, num, h, k, l, position, energy, tag, time): + """ + edit_reflection(num, h, k, l, position, tag=None) -- adds a reflection + + position is in degrees and in the systems internal representation. + """ + if self._state.reflist is None: + raise DiffcalcException("No UBCalculation loaded") + self._state.reflist.edit_reflection(num, h, k, l, position, energy, tag, time) + + # If first or second reflection has been changed and there are at least + # two reflections then recalculate UB + if (num == 1 or num == 2) and len(self._state.reflist) >= 2: + self._autocalculateUbAndReport() + self.save() + + def get_reflection(self, num): + """--> ( [h, k, l], position, energy, tag, time + num starts at 1, position in degrees""" + return self._state.reflist.getReflection(num) + + def get_reflection_in_external_angles(self, num): + """--> ( [h, k, l], position, energy, tag, time + num starts at 1, position in degrees""" + return self._state.reflist.get_reflection_in_external_angles(num) + + def get_number_reflections(self): + return 0 if self._state.reflist is None else len(self._state.reflist) + + def del_reflection(self, reflectionNumber): + self._state.reflist.removeReflection(reflectionNumber) + if ((reflectionNumber == 1 or reflectionNumber == 2) and + (self._U is not None)): + self._autocalculateUbAndReport() + self.save() + + def swap_reflections(self, num1, num2): + self._state.reflist.swap_reflections(num1, num2) + if ((num1 == 1 or num1 == 2 or num2 == 1 or num2 == 2) and + (self._U is not None)): + self._autocalculateUbAndReport() + self.save() + + def _autocalculateUbAndReport(self): + if len(self._state.reflist) < 2: + pass + elif self._state.crystal is None: + print ("Not calculating UB matrix as no lattice parameters have " + "been specified.") + elif not self._state.is_okay_to_autocalculate_ub: + print ("Not calculating UB matrix as it has been manually set. " + "Use 'calcub' to explicitly recalculate it.") + else: # okay to autocalculate + if self._UB is None: + print "Calculating UB matrix." + else: + print "Recalculating UB matrix." + self.calculate_UB() + +### Orientations ### + + def add_orientation(self, h, k, l, x, y, z, tag, time): + """add_reflection(h, k, l, x, y, z, tag=None) -- adds a crystal orientation + """ + if self._state.orientlist is None: + raise DiffcalcException("No UBCalculation loaded") + try: + xyz_rot = self._ROT * matrix([[x],[y],[z]]) + xr, yr, zr = xyz_rot.T.tolist()[0] + self._state.orientlist.add_orientation(h, k, l, xr, yr, zr, tag, time) + except TypeError: + self._state.orientlist.add_orientation(h, k, l, x, y, z, tag, time) + self.save() # incase autocalculateUbAndReport fails + + # If second reflection has just been added then calculateUB + if len(self._state.orientlist) == 2: + self._autocalculateOrientationUbAndReport() + self.save() + + def edit_orientation(self, num, h, k, l, x, y, z, tag, time): + """ + edit_orientation(num, h, k, l, x, y, z, tag=None) -- edit a crystal reflection """ + if self._state.orientlist is None: + raise DiffcalcException("No UBCalculation loaded") + try: + xyz_rot = self._ROT * matrix([[x],[y],[z]]) + xr, yr, zr = xyz_rot.T.tolist()[0] + self._state.orientlist.edit_orientation(num, h, k, l, xr, yr, zr, tag, time) + except TypeError: + self._state.orientlist.edit_orientation(num, h, k, l, x, y, z, tag, time) + + # If first or second orientation has been changed and there are + # two orientations then recalculate UB + if (num == 1 or num == 2) and len(self._state.orientlist) == 2: + self._autocalculateOrientationUbAndReport() + self.save() + + def get_orientation(self, num): + """--> ( [h, k, l], [x, y, z], tag, time ) + num starts at 1""" + try: + hkl, xyz, tg, tm = self._state.orientlist.getOrientation(num) + xyz_rot = self._ROT.I * matrix([[xyz[0]],[xyz[1]],[xyz[2]]]) + xyz_lst = xyz_rot.T.tolist()[0] + return hkl, xyz_lst, tg, tm + except AttributeError: + return self._state.orientlist.getOrientation(num) + + + def get_number_orientations(self): + return 0 if self._state.orientlist is None else len(self._state.reflist) + + def del_orientation(self, orientationNumber): + self._state.orientlist.removeOrientation(orientationNumber) + if ((orientationNumber == 2) and (self._U is not None)): + self._autocalculateOrientationUbAndReport() + self.save() + + def swap_orientations(self, num1, num2): + self._state.orientlist.swap_orientations(num1, num2) + if ((num1 == 2 or num2 == 2) and + (self._U is not None)): + self._autocalculateOrientationUbAndReport() + self.save() + + def _autocalculateOrientationUbAndReport(self): + if len(self._state.orientlist) < 2: + pass + elif self._state.crystal is None: + print ("Not calculating UB matrix as no lattice parameters have " + "been specified.") + elif not self._state.is_okay_to_autocalculate_ub: + print ("Not calculating UB matrix as it has been manually set. " + "Use 'orientub' to explicitly recalculate it.") + else: # okay to autocalculate + if self._UB is None: + print "Calculating UB matrix." + else: + print "Recalculating UB matrix." + self.calculate_UB_from_orientation() + +# @property +# def reflist(self): +# return self._state.reflist +### Calculations ### + + def set_U_manually(self, m): + """Manually sets U. matrix must be 3*3 Jama or python matrix. + Turns off aution UB calcualtion.""" + + # Check matrix is a 3*3 Jama matrix + if m.__class__ != matrix: + m = matrix(m) # assume its a python matrix + if m.shape[0] != 3 or m.shape[1] != 3: + raise ValueError("Expects 3*3 matrix") + + if self._UB is None: + print "Calculating UB matrix." + else: + print "Recalculating UB matrix." + + if self._ROT is not None: + self._U = self._ROT * m * self._ROT.I + else: + self._U = m + self._state.configure_calc_type(manual_U=self._U) + if self._state.crystal is None: + raise DiffcalcException( + "A crystal must be specified before manually setting U") + self._UB = self._U * self._state.crystal.B + print ("NOTE: A new UB matrix will not be automatically calculated " + "when the orientation reflections are modified.") + self.save() + + def set_UB_manually(self, m): + """Manually sets UB. matrix must be 3*3 Jama or python matrix. + Turns off aution UB calcualtion.""" + + # Check matrix is a 3*3 Jama matrix + if m.__class__ != matrix: + m = matrix(m) # assume its a python matrix + if m.shape[0] != 3 or m.shape[1] != 3: + raise ValueError("Expects 3*3 matrix") + + if self._ROT is not None: + self._UB = self._ROT * m + else: + self._UB = m + self._state.configure_calc_type(manual_UB=self._UB) + self.save() + + @property + def U(self): + if self._U is None: + raise DiffcalcException( + "No U matrix has been calculated during this ub calculation") + return self._U + + @property + def UB(self): + return self._get_UB() + + def is_ub_calculated(self): + return self._UB is not None + + def _get_UB(self): + if not self.is_ub_calculated(): + raise DiffcalcException( + "No UB matrix has been calculated during this ub calculation") + else: + return self._UB + + def _calc_UB(self, h1, h2, u1p, u2p): + B = self._state.crystal.B + h1c = B * h1 + h2c = B * h2 + + # Create modified unit vectors t1, t2 and t3 in crystal and phi systems + t1c = h1c + t3c = cross3(h1c, h2c) + t2c = cross3(t3c, t1c) + + t1p = u1p # FIXED from h1c 9July08 + t3p = cross3(u1p, u2p) + t2p = cross3(t3p, t1p) + + # ...and nornmalise and check that the reflections used are appropriate + SMALL = 1e-4 # Taken from Vlieg's code + e = DiffcalcException("Invalid orientation reflection(s)") + + def normalise(m): + d = norm(m) + if d < SMALL: + raise e + return m / d + + t1c = normalise(t1c) + t2c = normalise(t2c) + t3c = normalise(t3c) + + t1p = normalise(t1p) + t2p = normalise(t2p) + t3p = normalise(t3p) + + Tc = hstack([t1c, t2c, t3c]) + Tp = hstack([t1p, t2p, t3p]) + self._state.configure_calc_type(or0=1, or1=2) + self._U = Tp * Tc.I + self._UB = self._U * B + self.save() + + def calculate_UB(self): + """ + Calculate orientation matrix. Uses first two orientation reflections + as in Busang and Levy, but for the diffractometer in Lohmeier and + Vlieg. + """ + + # Major variables: + # h1, h2: user input reciprical lattice vectors of the two reflections + # h1c, h2c: user input vectors in cartesian crystal plane + # pos1, pos2: measured diffractometer positions of the two reflections + # u1a, u2a: measured reflection vectors in alpha frame + # u1p, u2p: measured reflection vectors in phi frame + + + # Get hkl and angle values for the first two refelctions + if self._state.reflist is None: + raise DiffcalcException("Cannot calculate a U matrix until a " + "UBCalculation has been started with " + "'newub'") + try: + (h1, pos1, _, _, _) = self._state.reflist.getReflection(1) + (h2, pos2, _, _, _) = self._state.reflist.getReflection(2) + except IndexError: + raise DiffcalcException( + "Two reflections are required to calculate a U matrix") + h1 = matrix([h1]).T # row->column + h2 = matrix([h2]).T + pos1.changeToRadians() + pos2.changeToRadians() + + # Compute the two reflections' reciprical lattice vectors in the + # cartesian crystal frame + u1p = self._strategy.calculate_q_phi(pos1) + u2p = self._strategy.calculate_q_phi(pos2) + + self._calc_UB(h1, h2, u1p, u2p) + + def calculate_UB_from_orientation(self): + """ + Calculate orientation matrix. Uses first two crystal orientations. + """ + + # Major variables: + # h1, h2: user input reciprical lattice vectors of the two reflections + # h1c, h2c: user input vectors in cartesian crystal plane + # pos1, pos2: measured diffractometer positions of the two reflections + # u1a, u2a: measured reflection vectors in alpha frame + # u1p, u2p: measured reflection vectors in phi frame + + + # Get hkl and angle values for the first two crystal orientations + if self._state.orientlist is None: + raise DiffcalcException("Cannot calculate a U matrix until a " + "UBCalculation has been started with " + "'newub'") + try: + (h1, x1, _, _) = self._state.orientlist.getOrientation(1) + (h2, x2, _, _) = self._state.orientlist.getOrientation(2) + except IndexError: + raise DiffcalcException( + "Two crystal orientations are required to calculate a U matrix") + h1 = matrix([h1]).T # row->column + h2 = matrix([h2]).T + u1p = matrix([x1]).T + u2p = matrix([x2]).T + + self._calc_UB(h1, h2, u1p, u2p) + + def calculate_UB_from_primary_only(self): + """ + Calculate orientation matrix with the shortest absolute angle change. + Uses first orientation reflection + """ + + # Algorithm from http://www.j3d.org/matrix_faq/matrfaq_latest.html + + # Get hkl and angle values for the first two refelctions + if self._state.reflist is None: + raise DiffcalcException( + "Cannot calculate a u matrix until a UBCalcaluation has been " + "started with newub") + try: + (h, pos, _, _, _) = self._state.reflist.getReflection(1) + except IndexError: + raise DiffcalcException( + "One reflection is required to calculate a u matrix") + + h = matrix([h]).T # row->column + pos.changeToRadians() + B = self._state.crystal.B + h_crystal = B * h + h_crystal = h_crystal * (1 / norm(h_crystal)) + + q_measured_phi = self._strategy.calculate_q_phi(pos) + q_measured_phi = q_measured_phi * (1 / norm(q_measured_phi)) + + rotation_axis = cross3(h_crystal, q_measured_phi) + rotation_axis = rotation_axis * (1 / norm(rotation_axis)) + + cos_rotation_angle = dot3(h_crystal, q_measured_phi) + rotation_angle = acos(cos_rotation_angle) + + uvw = rotation_axis.T.tolist()[0] # TODO: cleanup + print "resulting U angle: %.5f deg" % (rotation_angle * TODEG) + u_repr = (', '.join(['% .5f' % el for el in uvw])) + print "resulting U axis direction: [%s]" % u_repr + + u, v, w = uvw + rcos = cos(rotation_angle) + rsin = sin(rotation_angle) + m = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] # TODO: tidy + m[0][0] = rcos + u * u * (1 - rcos) + m[1][0] = w * rsin + v * u * (1 - rcos) + m[2][0] = -v * rsin + w * u * (1 - rcos) + m[0][1] = -w * rsin + u * v * (1 - rcos) + m[1][1] = rcos + v * v * (1 - rcos) + m[2][1] = u * rsin + w * v * (1 - rcos) + m[0][2] = v * rsin + u * w * (1 - rcos) + m[1][2] = -u * rsin + v * w * (1 - rcos) + m[2][2] = rcos + w * w * (1 - rcos) + + if self._UB is None: + print "Calculating UB matrix from the first reflection only." + else: + print "Recalculating UB matrix from the first reflection only." + print ("NOTE: A new UB matrix will not be automatically calculated " + "when the orientation reflections are modified.") + + self._state.configure_calc_type(or0=1) + + self._U = matrix(m) + self._UB = self._U * B + + self.save() + + def set_miscut(self, xyz, angle, add_miscut=False): + """Calculate U matrix using a miscut axis and an angle""" + if xyz is None: + rot_matrix = xyz_rotation([0, 1, 0], angle) + if self.is_ub_calculated() and add_miscut: + self._U = rot_matrix * self._U + else: + self._U = rot_matrix + else: + rot_matrix = xyz_rotation(xyz, angle) + try: + rot_matrix = self._ROT * rot_matrix * self._ROT.I + except TypeError: + pass + if self.is_ub_calculated() and add_miscut: + self._U = rot_matrix * self._U + else: + self._U = rot_matrix + self._state.configure_calc_type(manual_U=self._U) + self._UB = self._U * self._state.crystal.B + self.print_reference() + self.save() + + def get_hkl_plane_distance(self, hkl): + """Calculates and returns the distance between planes""" + return self._state.crystal.get_hkl_plane_distance(hkl) + + def get_hkl_plane_angle(self, hkl1, hkl2): + """Calculates and returns the angle between planes""" + return self._state.crystal.get_hkl_plane_angle(hkl1, hkl2) + + def rescale_unit_cell(self, h, k, l, pos): + """ + Calculate unit cell scaling parameter that matches + given hkl position and diffractometer angles + """ + q_vec = self._strategy.calculate_q_phi(pos) + q_hkl = norm(q_vec) / self._hardware.get_wavelength() + d_hkl = self._state.crystal.get_hkl_plane_distance([h, k, l]) + sc = 1/ (q_hkl * d_hkl) + name, a1, a2, a3, alpha1, alpha2, alpha3 = self._state.crystal.getLattice() + if abs(sc - 1.) < SMALL: + return None, None + return sc, (name, sc * a1, sc* a2, sc * a3, alpha1, alpha2, alpha3) + + def calc_miscut(self, h, k, l, pos): + """ + Calculate miscut angle and axis that matches + given hkl position and diffractometer angles + """ + q_vec = self._strategy.calculate_q_phi(pos) + hkl_nphi = self._UB * matrix([[h], [k], [l]]) + try: + axis = cross3(self._ROT.I * q_vec, self._ROT.I * hkl_nphi) + except AttributeError: + axis = cross3(q_vec, hkl_nphi) + norm_axis = norm(axis) + if norm_axis < SMALL: + return None, None + axis = axis / norm(axis) + try: + miscut = acos(bound(dot3(q_vec, hkl_nphi) / (norm(q_vec) * norm(hkl_nphi)))) * TODEG + except AssertionError: + return None, None + return miscut, axis.T.tolist()[0] diff --git a/script/test/diffcalc (copy)/diffcalc/ub/calcstate.py b/script/test/diffcalc (copy)/diffcalc/ub/calcstate.py new file mode 100644 index 0000000..1e8f438 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/calcstate.py @@ -0,0 +1,220 @@ +from diffcalc.hkl.vlieg.geometry import VliegPosition +from diffcalc.ub.crystal import CrystalUnderTest +from diffcalc.ub.reflections import ReflectionList, _Reflection +from math import pi +import datetime # @UnusedImport For crazy time eval code! +from diffcalc.ub.reference import YouReference +from diffcalc.ub.orientations import _Orientation, OrientationList + +try: + from collection import OrderedDict +except ImportError: + from simplejson import OrderedDict + +try: + import json +except ImportError: + import simplejson as json + +try: + from numpy import matrix +except ImportError: + from numjy import matrix + + +TODEG = 180 / pi + + +class UBCalcState(): + + def __init__(self, name=None, crystal=None, reflist=None, orientlist=None, tau=0, sigma=0, + manual_U=None, manual_UB=None, or0=None, or1=None, reference=None): + + assert reflist is not None + self.name = name + self.crystal = crystal + self.reflist = reflist + self.orientlist = orientlist + self.tau = tau # degrees + self.sigma = sigma # degrees + self.manual_U = manual_U + self.manual_UB = manual_UB + self.or0 = or0 + self.or1 = or1 + self.reference = reference + + @property + def is_okay_to_autocalculate_ub(self): + nothing_set = ((self.manual_U is None) and + (self.manual_UB is None) and + (self.or0 is None) and + (self.or1 is None)) + or0_and_or1_used = (self.or0 is not None) and (self.or1 is not None) + return nothing_set or or0_and_or1_used + + + def configure_calc_type(self, + manual_U=None, + manual_UB=None, + or0=None, + or1=None): + self.manual_U = manual_U + self.manual_UB = manual_UB + self.or0 = or0 + self.or1 = or1 + + +class UBCalcStateEncoder(json.JSONEncoder): + + def default(self, obj): + + if isinstance(obj, UBCalcState): + d = OrderedDict() + d['name'] = obj.name + d['crystal'] = obj.crystal + d['reflist'] = obj.reflist + d['orientlist'] = obj.orientlist + d['tau'] = obj.tau + d['sigma'] = obj.sigma + d['reference'] = obj.reference + d['u'] = obj.manual_U + d['ub'] = obj.manual_UB + d['or0'] = obj.or0 + d['or1'] = obj.or1 + + return d + + if isinstance(obj, CrystalUnderTest): + return repr([obj._name, obj._a1, obj._a2, obj._a3, obj._alpha1 * TODEG, + obj._alpha2 * TODEG, obj._alpha3 * TODEG]) + + if isinstance(obj, matrix): + l = [', '.join((repr(e) for e in row)) for row in obj.tolist()] + return l + + if isinstance(obj, ReflectionList): + d = OrderedDict() + for n, ref in enumerate(obj._reflist): + d[str(n+1)] = ref + return d + + if isinstance(obj, _Reflection): + d = OrderedDict() + d['tag'] = obj.tag + d['hkl'] = repr([obj.h, obj.k, obj.l]) + d['pos'] = repr(list(obj.pos.totuple())) + d['energy'] = obj.energy + dt = eval(obj.time) # e.g. --> datetime.datetime(2013, 8, 5, 15, 47, 7, 962432) + d['time'] = None if dt is None else dt.isoformat() + return d + + if isinstance(obj, OrientationList): + d = OrderedDict() + for n, orient in enumerate(obj._orientlist): + d[str(n+1)] = orient + return d + + if isinstance(obj, _Orientation): + d = OrderedDict() + d['tag'] = obj.tag + d['hkl'] = repr([obj.h, obj.k, obj.l]) + d['xyz'] = repr([obj.x, obj.y, obj.z]) + dt = eval(obj.time) # e.g. --> datetime.datetime(2013, 8, 5, 15, 47, 7, 962432) + d['time'] = None if dt is None else dt.isoformat() + return d + + if isinstance(obj, YouReference): + d = OrderedDict() + if obj.n_hkl_configured is not None: + d['n_hkl_configured'] = repr(obj.n_hkl_configured.T.tolist()[0]) + else: + d['n_hkl_configured'] = None + if obj.n_phi_configured is not None: + d['n_phi_configured'] = repr(obj.n_phi_configured.T.tolist()[0]) + else: + d['n_phi_configured'] = None + return d + + + return json.JSONEncoder.default(self, obj) + + +def decode_ubcalcstate(state, geometry, diffractometer_axes_names): + + # Backwards compatibility code + orientlist_=OrientationList([]) + try: + orientlist_=decode_orientlist(state['orientlist']) + except KeyError: + pass + return UBCalcState( + name=state['name'], + crystal=state['crystal'] and CrystalUnderTest(*eval(state['crystal'])), + reflist=decode_reflist(state['reflist'], geometry, diffractometer_axes_names), + orientlist=orientlist_, + tau=state['tau'], + sigma=state['sigma'], + manual_U=state['u'] and decode_matrix(state['u']), + manual_UB=state['ub'] and decode_matrix(state['ub']), + or0=state['or0'], + or1=state['or1'], + reference=decode_reference(state.get('reference', None)) + ) + + +def decode_matrix(rows): + return matrix([[eval(e) for e in row.split(', ')] for row in rows]) + + +def decode_reflist(reflist_dict, geometry, diffractometer_axes_names): + reflections = [] + for key in sorted(reflist_dict.keys()): + reflections.append(decode_reflection(reflist_dict[key], geometry)) + + return ReflectionList(geometry, diffractometer_axes_names, reflections) + + +def decode_orientlist(orientlist_dict): + orientations = [] + for key in sorted(orientlist_dict.keys()): + orientations.append(decode_orientation(orientlist_dict[key])) + + return OrientationList(orientations) + + +def decode_reflection(ref_dict, geometry): + h, k, l = eval(ref_dict['hkl']) + time = ref_dict['time'] and gt(ref_dict['time']) + pos_tuple = eval(ref_dict['pos']) + try: + position = geometry.create_position(*pos_tuple) + except AttributeError: + position = VliegPosition(*pos_tuple) + return _Reflection(h, k, l, position, ref_dict['energy'], str(ref_dict['tag']), repr(time)) + + +def decode_reference(ref_dict): + reference = YouReference(None) # TODO: We can't set get_ub method yet (tangles!) + if ref_dict: + nhkl = ref_dict.get('n_hkl_configured', None) + nphi = ref_dict.get('n_phi_configured', None) + if nhkl: + reference.n_hkl_configured = matrix([eval(nhkl)]).T + if nphi: + reference.n_phi_configured = matrix([eval(nphi)]).T + return reference + + +def decode_orientation(orient_dict): + h, k, l = eval(orient_dict['hkl']) + x, y, z = eval(orient_dict['xyz']) + time = orient_dict['time'] and gt(orient_dict['time']) + return _Orientation(h, k, l, x, y, z, str(orient_dict['tag']), repr(time)) + + +# From: http://stackoverflow.com/questions/127803/how-to-parse-iso-formatted-date-in-python +def gt(dt_str): + dt, _, us= dt_str.partition(".") + dt= datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S") + us= int(us.rstrip("Z"), 10) + return dt + datetime.timedelta(microseconds=us) \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/ub/crystal.py b/script/test/diffcalc (copy)/diffcalc/ub/crystal.py new file mode 100644 index 0000000..84072d9 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/crystal.py @@ -0,0 +1,147 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi, cos, sin, acos, sqrt +from diffcalc.util import angle_between_vectors + + +try: + from numpy import matrix +except ImportError: + from numjy import matrix + +TORAD = pi / 180 +TODEG = 180 / pi +SMALL = 1e-7 + +def z(num): + """Round to zero if small. This is useful to get rid of erroneous + minus signs resulting from float representation close to zero. + """ + if abs(num) < SMALL: + num = 0 + return num + + +class CrystalUnderTest(object): + """ + Contains the lattice parameters and calculated B matrix for the crytsal + under test. Also Calculates the distance between planes at a given hkl + value. + + The lattice paraemters can be specified and then if desired saved to a + __library to be loaded later. The parameters are persisted across restarts. + Lattices stored in config/var/crystals.xml . + """ + + def __init__(self, name, a, b, c, alpha, beta, gamma): + '''Creates a new lattice and calculates related values. + + Keyword arguments: + name -- a string + a,b,c,alpha,beta,gamma -- lengths and angles (in degrees) + ''' + + self._name = name + + # Set the direct lattice parameters + self._a1 = a1 = a + self._a2 = a2 = b + self._a3 = a3 = c + self._alpha1 = alpha1 = alpha * TORAD + self._alpha2 = alpha2 = beta * TORAD + self._alpha3 = alpha3 = gamma * TORAD + + # Calculate the reciprocal lattice parameters + self._beta1 = acos((cos(alpha2) * cos(alpha3) - cos(alpha1)) / + (sin(alpha2) * sin(alpha3))) + + self._beta2 = beta2 = acos((cos(alpha1) * cos(alpha3) - cos(alpha2)) / + (sin(alpha1) * sin(alpha3))) + + self._beta3 = beta3 = acos((cos(alpha1) * cos(alpha2) - cos(alpha3)) / + (sin(alpha1) * sin(alpha2))) + + volume = (a1 * a2 * a3 * + sqrt(1 + 2 * cos(alpha1) * cos(alpha2) * cos(alpha3) - + cos(alpha1) ** 2 - cos(alpha2) ** 2 - cos(alpha3) ** 2)) + + self._b1 = b1 = 2 * pi * a2 * a3 * sin(alpha1) / volume + self._b2 = b2 = 2 * pi * a1 * a3 * sin(alpha2) / volume + self._b3 = b3 = 2 * pi * a1 * a2 * sin(alpha3) / volume + + # Calculate the BMatrix from the direct and reciprical parameters. + # Reference: Busang and Levy (1967) + self._bMatrix = matrix([ + [b1, b2 * cos(beta3), b3 * cos(beta2)], + [0.0, b2 * sin(beta3), -b3 * sin(beta2) * cos(alpha1)], + [0.0, 0.0, 2 * pi / a3]]) + + @property + def B(self): + ''' + Returns the B matrix, may be null if crystal is not set, or if there + was a problem calculating this''' + return self._bMatrix + + def get_hkl_plane_distance(self, hkl): + '''Calculates and returns the distance between planes''' + hkl = matrix([hkl]) + bReduced = self._bMatrix / (2 * pi) + bMT = bReduced.I * bReduced.T.I + return 1.0 / sqrt((hkl * bMT.I * hkl.T)[0,0]) + + def get_hkl_plane_angle(self, hkl1, hkl2): + '''Calculates and returns the angle between [hkl1] and [hkl2] planes''' + hkl1 = matrix([hkl1]).T + hkl2 = matrix([hkl2]).T + nphi1 = self._bMatrix * hkl1 + nphi2 = self._bMatrix * hkl2 + angle = angle_between_vectors(nphi1, nphi2) + return angle + + def __str__(self): + ''' Returns lattice name and all set and calculated parameters''' + return '\n'.join(self.str_lines()) + + def str_lines(self): + WIDTH = 13 + if self._name is None: + return [" none specified"] + + b = self._bMatrix + lines = [] + lines.append(" name:".ljust(WIDTH) + self._name.rjust(9)) + lines.append("") + lines.append(" a, b, c:".ljust(WIDTH) + + "% 9.5f % 9.5f % 9.5f" % (self.getLattice()[1:4])) + lines.append(" " * WIDTH + + "% 9.5f % 9.5f % 9.5f" % (self.getLattice()[4:])) + lines.append("") + + fmt = "% 9.5f % 9.5f % 9.5f" + lines.append(" B matrix:".ljust(WIDTH) + + fmt % (z(b[0, 0]), z(b[0, 1]), z(b[0, 2]))) + lines.append(' ' * WIDTH + fmt % (z(b[1, 0]), z(b[1, 1]), z(b[1, 2]))) + lines.append(' ' * WIDTH + fmt % (z(b[2, 0]), z(b[2, 1]), z(b[2, 2]))) + return lines + + def getLattice(self): + return(self._name, self._a1, self._a2, self._a3, self._alpha1 * TODEG, + self._alpha2 * TODEG, self._alpha3 * TODEG) + diff --git a/script/test/diffcalc (copy)/diffcalc/ub/orientations.py b/script/test/diffcalc (copy)/diffcalc/ub/orientations.py new file mode 100644 index 0000000..1726136 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/orientations.py @@ -0,0 +1,118 @@ +### +# Copyright 2008-2017 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from copy import deepcopy +import datetime # @UnusedImport for the eval below + +try: + from numpy import matrix +except ImportError: + from numjy import matrix + +from diffcalc.util import DiffcalcException, bold + + +class _Orientation: + """A orientation""" + def __init__(self, h, k, l, x, y, z, tag, time): + + self.h = float(h) + self.k = float(k) + self.l = float(l) + self.x = float(x) + self.y = float(y) + self.z = float(z) + self.tag = tag + self.time = time # Saved as e.g. repr(datetime.now()) + + def __str__(self): + return ("h=%-4.2f k=%-4.2f l=%-4.2f x=%-4.2f " + "y=%-4.2f z=%-4.2 " + " %-s %s" % (self.h, self.k, self.l, + self.x, self.y, self.z, + self.tag, self.time)) + + +class OrientationList: + + def __init__(self, orientations=None): + self._orientlist = orientations if orientations else [] + + + def add_orientation(self, h, k, l, x, y, z, tag, time): + """adds a crystal orientation + """ + self._orientlist += [_Orientation(h, k, l, x, y, z, tag, + time.__repr__())] + + def edit_orientation(self, num, h, k, l, x, y, z, tag, time): + """num starts at 1""" + try: + self._orientlist[num - 1] = _Orientation(h, k, l, x, y, z, tag, + time.__repr__()) + except IndexError: + raise DiffcalcException("There is no orientation " + repr(num) + + " to edit.") + + def getOrientation(self, num): + """ + getOrientation(num) --> ( [h, k, l], [x, y, z], tag, time ) -- + num starts at 1 + """ + r = deepcopy(self._orientlist[num - 1]) # for convenience + return [r.h, r.k, r.l], [r.x, r.y, r.z], r.tag, eval(r.time) + + def removeOrientation(self, num): + del self._orientlist[num - 1] + + def swap_orientations(self, num1, num2): + orig1 = self._orientlist[num1 - 1] + self._orientlist[num1 - 1] = self._orientlist[num2 - 1] + self._orientlist[num2 - 1] = orig1 + + def __len__(self): + return len(self._orientlist) + + def __str__(self): + return '\n'.join(self.str_lines()) + + def str_lines(self, R=None): + if not self._orientlist: + return [" <<< none specified >>>"] + + lines = [] + + str_format = (" %5s %5s %5s %5s %5s %5s TAG") + values = ('H', 'K', 'L', 'X', 'Y', 'Z') + lines.append(bold(str_format % values)) + + for n in range(len(self._orientlist)): + orient_tuple = self.getOrientation(n + 1) + [h, k, l], [x, y, z], tag, _ = orient_tuple + try: + xyz_rot = R.I * matrix([[x],[y],[z]]) + xr, yr, zr = xyz_rot.T.tolist()[0] + except AttributeError: + xr, yr, zr = x ,y ,z + if tag is None: + tag = "" + str_format = (" %2d % 4.2f % 4.2f % 4.2f " + + "% 4.2f % 4.2f % 4.2f %s") + values = (n + 1, h, k, l, xr, yr, zr, tag) + lines.append(str_format % values) + return lines diff --git a/script/test/diffcalc (copy)/diffcalc/ub/persistence.py b/script/test/diffcalc (copy)/diffcalc/ub/persistence.py new file mode 100644 index 0000000..526dbcd --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/persistence.py @@ -0,0 +1,151 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from __future__ import with_statement + +import os, glob +from diffcalc.ub.calcstate import UBCalcStateEncoder +import datetime + +try: + import json +except ImportError: + import simplejson as json + + +def is_writable(directory): + """Return true if the file is writable from the current user + """ + probe = os.path.join(directory, "probe") + try: + open(probe, 'w') + except IOError: + return False + else: + os.remove(probe) + return True + +def check_directory_appropriate(directory): + + if not os.path.exists(directory): + raise IOError("'%s' does not exist") + + if not os.path.isdir(directory): + raise IOError("'%s' is not a directory") + + if not is_writable(directory): + raise IOError("'%s' is not writable") + + +class UBCalculationJSONPersister(object): + + def __init__(self, directory): + check_directory_appropriate(directory) + self.directory = directory + self.description = directory + + def filepath(self, name): + return os.path.join(self.directory, name + '.json') + + def save(self, state, name): + # FORMAT = '%Y-%m-%d %H:%M:%S' + # time_string = datetime.datetime.strftime(datetime.datetime.now(), FORMAT) + with open(self.filepath(name), 'w') as f: + json.dump(state, f, indent=4, cls=UBCalcStateEncoder) + + def load(self, name): + with open(self.filepath(name), 'r') as f: + return json.load(f) + + def list(self): # @ReservedAssignment + files = self._get_save_files() + return [os.path.basename(f + '.json').split('.json')[0] for f in files] + + def list_metadata(self): + metadata = [] + for f in self._get_save_files(): + dt = datetime.datetime.fromtimestamp(os.path.getmtime(f)) + metadata.append(dt.strftime('%d %b %Y (%H:%M)')) + return metadata + + def _get_save_files(self): + files = filter(os.path.isfile, glob.glob(os.path.join(self.directory, '*.json'))) + files.sort(key=lambda x: os.path.getmtime(x)) + files.reverse() + return files + + def remove(self, name): + os.remove(self.filepath(name)) + + + +class UBCalculationPersister(object): + """Attempts to the use the gda's database to store ub calculation state + """ + def __init__(self): + try: + from uk.ac.diamond.daq.persistence.jythonshelf import LocalJythonShelfManager + from uk.ac.diamond.daq.persistence.jythonshelf.LocalDatabase import \ + LocalDatabaseException + self.shelf = LocalJythonShelfManager.getLocalObjectShelf( + 'diffcalc.ub') + except ImportError, e: + print ("!!! UBCalculationPersister could not import the gda database " + "code: " + repr(e)) + self.shelf = None + except LocalDatabaseException, e: + print ("UBCalculationPersister could not connect to the gda " + "database: " + repr(e)) + self.shelf = None + self.description = 'GDA sql database' + + def save(self, state, key): + if self.shelf is not None: + self.shelf[key] = state + else: + print "<<>>" + + def load(self, name): + if self.shelf is not None: + return self.shelf[name] + else: + raise IOError("Could not load UB calculation: no database available") + + def list(self): # @ReservedAssignment + if self.shelf is not None: + names = list(self.shelf.keys()) + names.sort() + return names + else: + return [] + + def remove(self, name): + if self.shelf is not None: + del self.shelf[name] + else: + raise IOError("Could not remove UB calculation: no database available") + + +class UbCalculationNonPersister(UBCalculationPersister): + """ + A version of UBCalculationPersister that simply stores to a local dict + rather than a database. Useful for testing. + """ + def __init__(self): + self.shelf = dict() + self.description = 'memory only' diff --git a/script/test/diffcalc (copy)/diffcalc/ub/reference.py b/script/test/diffcalc (copy)/diffcalc/ub/reference.py new file mode 100644 index 0000000..53a8e4b --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/reference.py @@ -0,0 +1,99 @@ +from math import pi, acos + +try: + from numpy import matrix + from numpy.linalg import norm +except ImportError: + from numjy import matrix + from numjy.linalg import norm + +from diffcalc.util import cross3, dot3 + + +SMALL = 1e-7 +TODEG = 180 / pi + +class YouReference(object): + + def __init__(self, get_UB): + self.get_UB = get_UB # callable + self._n_phi_configured = None + self._n_hkl_configured = None + self._set_n_phi_configured(matrix('0; 0; 1')) + + def _set_n_phi_configured(self, n_phi): + self._n_phi_configured = n_phi + self._n_hkl_configured = None + + def _get_n_phi_configured(self): + return self._n_phi_configured + + n_phi_configured = property(_get_n_phi_configured, _set_n_phi_configured) + + def _set_n_hkl_configured(self, n_hkl): + self._n_phi_configured = None + self._n_hkl_configured = n_hkl + + def _get_n_hkl_configured(self): + return self._n_hkl_configured + + n_hkl_configured = property(_get_n_hkl_configured, _set_n_hkl_configured) + + @property + def n_phi(self): + n_phi = (self.get_UB() * self._n_hkl_configured if self._n_phi_configured is None + else self._n_phi_configured) + return n_phi / norm(n_phi) + + @property + def n_hkl(self): + n_hkl = (self.get_UB().I * self._n_phi_configured if self._n_hkl_configured is None + else self._n_hkl_configured) + return n_hkl / norm(n_hkl) + + def _pretty_vector(self, m): + return ' '.join([('% 9.5f' % e).rjust(9) for e in m.T.tolist()[0]]) + + def repr_lines(self, ub_calculated, WIDTH=9, R=None): + SET_LABEL = ' <- set' + lines = [] + if self._n_phi_configured is not None: + nphi_label = SET_LABEL + nhkl_label = '' + elif self._n_hkl_configured is not None: + nphi_label = '' + nhkl_label = SET_LABEL + else: + raise AssertionError("Neither a manual n_phi nor n_hkl is configured") + + if ub_calculated: + try: + lines.append(" n_phi:".ljust(WIDTH) + self._pretty_vector(R.I * self.n_phi) + nphi_label) + except AttributeError: + lines.append(" n_phi:".ljust(WIDTH) + self._pretty_vector(self.n_phi) + nphi_label) + lines.append(" n_hkl:".ljust(WIDTH) + self._pretty_vector(self.n_hkl) + nhkl_label) + try: + rotation_axis = R.I * cross3(matrix('0; 0; 1'), self.n_phi) + except AttributeError: + rotation_axis = cross3(matrix('0; 0; 1'), self.n_phi) + if abs(norm(rotation_axis)) < SMALL: + lines.append(" normal:".ljust(WIDTH) + " None") + else: + rotation_axis = rotation_axis * (1 / norm(rotation_axis)) + cos_rotation_angle = dot3(matrix('0; 0; 1'), self.n_phi) + rotation_angle = acos(cos_rotation_angle) + lines.append(" normal:") + lines.append(" angle:".ljust(WIDTH) + "% 9.5f" % (rotation_angle * TODEG)) + lines.append(" axis:".ljust(WIDTH) + self._pretty_vector(rotation_axis)) + + else: # no ub calculated + if self._n_phi_configured is not None: + try: + lines.append(" n_phi:".ljust(WIDTH) + self._pretty_vector(R.I * self._n_phi_configured) + SET_LABEL) + except AttributeError: + lines.append(" n_phi:".ljust(WIDTH) + self._pretty_vector(self._n_phi_configured) + SET_LABEL) + elif self._n_hkl_configured is not None: + lines.append(" n_hkl:".ljust(WIDTH) + self._pretty_vector(self._n_hkl_configured) + SET_LABEL) + + return lines + diff --git a/script/test/diffcalc (copy)/diffcalc/ub/reflections.py b/script/test/diffcalc (copy)/diffcalc/ub/reflections.py new file mode 100644 index 0000000..c447468 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/reflections.py @@ -0,0 +1,126 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from copy import deepcopy +import datetime # @UnusedImport for the eval below +from diffcalc.util import DiffcalcException, bold +from diffcalc.hkl.vlieg.geometry import VliegPosition + + +class _Reflection: + """A reflection""" + def __init__(self, h, k, l, position, energy, tag, time): + + self.h = float(h) + self.k = float(k) + self.l = float(l) + self.pos = position + self.tag = tag + self.energy = float(energy) # energy=12.39842/lambda + self.wavelength = 12.3984 / self.energy + self.time = time # Saved as e.g. repr(datetime.now()) + + def __str__(self): + return ("energy=%-6.3f h=%-4.2f k=%-4.2f l=%-4.2f alpha=%-8.4f " + "delta=%-8.4f gamma=%-8.4f omega=%-8.4f chi=%-8.4f " + "phi=%-8.4f %-s %s" % (self.energy, self.h, self.k, self.l, + self.pos.alpha, self.pos.delta, self.pos.gamma, self.pos.omega, + self.pos.chi, self.pos.phi, self.tag, self.time)) + + +class ReflectionList: + + def __init__(self, diffractometerPluginObject, externalAngleNames, reflections=None): + self._geometry = diffractometerPluginObject + self._externalAngleNames = externalAngleNames + self._reflist = reflections if reflections else [] + + + def add_reflection(self, h, k, l, position, energy, tag, time): + """adds a reflection, position in degrees + """ + if type(position) in (list, tuple): + try: + position = self._geometry.create_position(*position) + except AttributeError: + position = VliegPosition(*position) + self._reflist += [_Reflection(h, k, l, position, energy, tag, + time.__repr__())] + + def edit_reflection(self, num, h, k, l, position, energy, tag, time): + """num starts at 1""" + if type(position) in (list, tuple): + position = VliegPosition(*position) + try: + self._reflist[num - 1] = _Reflection(h, k, l, position, energy, tag, + time.__repr__()) + except IndexError: + raise DiffcalcException("There is no reflection " + repr(num) + + " to edit.") + + def getReflection(self, num): + """ + getReflection(num) --> ( [h, k, l], position, energy, tag, time ) -- + num starts at 1 position in degrees + """ + r = deepcopy(self._reflist[num - 1]) # for convenience + return [r.h, r.k, r.l], deepcopy(r.pos), r.energy, r.tag, eval(r.time) + + def get_reflection_in_external_angles(self, num): + """getReflection(num) --> ( [h, k, l], (angle1...angleN), energy, tag ) + -- num starts at 1 position in degrees""" + r = deepcopy(self._reflist[num - 1]) # for convenience + externalAngles = self._geometry.internal_position_to_physical_angles(r.pos) + result = [r.h, r.k, r.l], externalAngles, r.energy, r.tag, eval(r.time) + return result + + def removeReflection(self, num): + del self._reflist[num - 1] + + def swap_reflections(self, num1, num2): + orig1 = self._reflist[num1 - 1] + self._reflist[num1 - 1] = self._reflist[num2 - 1] + self._reflist[num2 - 1] = orig1 + + def __len__(self): + return len(self._reflist) + + def __str__(self): + return '\n'.join(self.str_lines()) + + def str_lines(self): + axes = tuple(s.upper() for s in self._externalAngleNames) + if not self._reflist: + return [" <<< none specified >>>"] + + lines = [] + + format = (" %6s %5s %5s %5s " + "%8s " * len(axes) + " TAG") + values = ('ENERGY', 'H', 'K', 'L') + axes + lines.append(bold(format % values)) + + for n in range(len(self._reflist)): + ref_tuple = self.get_reflection_in_external_angles(n + 1) + [h, k, l], externalAngles, energy, tag, _ = ref_tuple + if tag is None: + tag = "" + format = (" %2d %6.3f % 4.2f % 4.2f % 4.2f " + + "% 8.4f " * len(axes) + " %s") + values = (n + 1, energy, h, k, l) + externalAngles + (tag,) + lines.append(format % values) + return lines diff --git a/script/test/diffcalc (copy)/diffcalc/ub/ub.py b/script/test/diffcalc (copy)/diffcalc/ub/ub.py new file mode 100644 index 0000000..afd2f47 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/ub/ub.py @@ -0,0 +1,768 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from diffcalc import settings +from diffcalc.ub.calc import UBCalculation + +from math import asin, pi +from datetime import datetime + +try: + from numpy import matrix +except ImportError: + from numjy import matrix + + +from diffcalc.util import getInputWithDefault as promptForInput, \ + promptForNumber, promptForList, allnum, isnum, bold, xyz_rotation +from diffcalc.util import command + +TORAD = pi / 180 +TODEG = 180 / pi + + +# When using ipython magic, these functions must not be imported to the top +# level namespace. Doing so will stop them from being called with magic. + +__all__ = ['addorient', 'addref', 'c2th', 'hklangle', 'calcub', 'delorient', 'delref', 'editorient', + 'editref', 'listub', 'loadub', 'newub', 'orientub', 'saveubas', 'setlat', + 'addmiscut', 'setmiscut', 'setu', 'setub', 'showorient', 'showref', 'swaporient', + 'swapref', 'trialub', 'checkub', 'ub', 'ubcalc', 'rmub', 'clearorient', + 'clearref', 'lastub', 'refineub'] + +if settings.include_sigtau: + __all__.append('sigtau') + +if settings.include_reference: + __all__.append('setnphi') + __all__.append('setnhkl') + + +ubcalc = UBCalculation(settings.hardware, + settings.geometry, + settings.ubcalc_persister, + settings.ubcalc_strategy, + settings.include_sigtau, + settings.include_reference) + + + +### UB state ### + +@command +def newub(name=None): + """newub {'name'} -- start a new ub calculation name + """ + if name is None: + # interactive + name = promptForInput('calculation name') + ubcalc.start_new(name) + setlat() + elif isinstance(name, str): + # just trying might cause confusion here + ubcalc.start_new(name) + else: + raise TypeError() + +@command +def loadub(name_or_num): + """loadub 'name' | num -- load an existing ub calculation + """ + if isinstance(name_or_num, str): + ubcalc.load(name_or_num) + else: + ubcalc.load(ubcalc.listub()[int(name_or_num)]) + +@command +def lastub(): + """lastub -- load the last used ub calculation + """ + try: + lastub_name = ubcalc.listub()[0] + print "Loading ub calculation: '%s'" % lastub_name + loadub(0) + except IndexError: + print "WARNING: There is no record of the last ub calculation used" + +@command +def rmub(name_or_num): + """rmub 'name'|num -- remove existing ub calculation + """ + if isinstance(name_or_num, str): + ubcalc.remove(name_or_num) + else: + ubcalc.remove(ubcalc.listub()[int(name_or_num)]) + +@command +def listub(): + """listub -- list the ub calculations available to load. + """ + if hasattr(ubcalc._persister, 'description'): + print "UB calculations in: " + ubcalc._persister.description + else: + print "UB calculations:" + print + ubnames = ubcalc.listub() + # TODO: whole mechanism of making two calls is messy + try: + ub_metadata = ubcalc.listub_metadata() + except AttributeError: + ub_metadata = [''] * len(ubnames) + + for n, name, data in zip(range(len(ubnames)), ubnames, ub_metadata): + print "%2i) %-15s %s" % (n, name, data) + +@command +def saveubas(name): + """saveubas 'name' -- save the ub calculation with a new name + """ + if isinstance(name, str): + # just trying might cause confusion here + ubcalc.saveas(name) + else: + raise TypeError() + +@command +def ub(): + """ub -- show the complete state of the ub calculation + """ + #wavelength = float(hardware.get_wavelength()) + #energy = float(hardware.get_energy()) + print ubcalc.__str__() + +@command +def refineub(*args): + """ + refineub {[h k l]} {pos} -- refine unit cell dimensions and U matrix to match diffractometer angles for a given hkl value + """ + if len(args) > 0: + args = list(args) + h, k, l = args.pop(0) + if not (isnum(h) and isnum(k) and isnum(l)): + raise TypeError() + else: + h = promptForNumber('h', 0.) + k = promptForNumber('k', 0.) + l = promptForNumber('l', 0.) + if None in (h, k, l): + _handleInputError("h,k and l must all be numbers") + if len(args) == 1: + pos = settings.geometry.physical_angles_to_internal_position( # @UndefinedVariable + args.pop(0)) + elif len(args) == 0: + reply = promptForInput('current pos', 'y') + if reply in ('y', 'Y', 'yes'): + positionList = settings.hardware.get_position() # @UndefinedVariable + else: + currentPos = settings.hardware.get_position() # @UndefinedVariable + positionList = [] + names = settings.hardware.get_axes_names() # @UndefinedVariable + for i, angleName in enumerate(names): + val = promptForNumber(angleName.rjust(7), currentPos[i]) + if val is None: + _handleInputError("Please enter a number, or press" + " Return to accept default!") + return + positionList.append(val) + pos = settings.geometry.physical_angles_to_internal_position(positionList) # @UndefinedVariable + else: + raise TypeError() + + pos.changeToRadians() + scale, lat = ubcalc.rescale_unit_cell(h, k, l, pos) + if scale: + lines = ["Unit cell scaling factor:".ljust(9) + + "% 9.5f" % scale] + lines.append("Refined crystal lattice:") + lines.append(" a, b, c:".ljust(9) + + "% 9.5f % 9.5f % 9.5f" % (lat[1:4])) + lines.append(" " * 12 + + "% 9.5f % 9.5f % 9.5f" % (lat[4:])) + lines.append("") + print '\n'.join(lines) + reply = promptForInput('Update crystal settings?', 'y') + if reply in ('y', 'Y', 'yes'): + ubcalc.set_lattice(*lat) + else: + print "No unit cell mismatch detected" + mc_angle, mc_axis = ubcalc.calc_miscut(h, k, l, pos) + if mc_angle: + lines = ["Miscut parameters:",] + lines.append(" angle:".ljust(9) + "% 9.5f" % mc_angle) + lines.append(" axis:".ljust(9) + "% 9.5f % 9.5f % 9.5f" % tuple(mc_axis)) + print '\n'.join(lines) + reply = promptForInput('Apply miscut parameters?', 'y') + if reply in ('y', 'Y', 'yes'): + ubcalc.set_miscut(mc_axis, -mc_angle * TORAD, True) + else: + print "No miscut detected for the given settings" + +### UB lattice ### + +@command +def setlat(name=None, *args): + """ + setlat -- interactively enter lattice parameters (Angstroms and Deg) + setlat name a -- assumes cubic + setlat name a b -- assumes tetragonal + setlat name a b c -- assumes ortho + setlat name a b c gamma -- assumes mon/hex with gam not equal to 90 + setlat name a b c alpha beta gamma -- arbitrary + """ + + if name is None: # Interactive + name = promptForInput("crystal name") + a = promptForNumber(' a', 1) + b = promptForNumber(' b', a) + c = promptForNumber(' c', a) + alpha = promptForNumber('alpha', 90) + beta = promptForNumber('beta', 90) + gamma = promptForNumber('gamma', 90) + ubcalc.set_lattice(name, a, b, c, alpha, beta, gamma) + + elif (isinstance(name, str) and + len(args) in (1, 2, 3, 4, 6) and + allnum(args)): + # first arg is string and rest are numbers + ubcalc.set_lattice(name, *args) + else: + raise TypeError() + +@command +def c2th(hkl, en=None): + """ + c2th [h k l] -- calculate two-theta angle for reflection + """ + if en is None: + wl = settings.hardware.get_wavelength() # @UndefinedVariable + else: + wl = 12.39842 / en + d = ubcalc.get_hkl_plane_distance(hkl) + if wl > (2 * d): + raise ValueError( + 'Reflection un-reachable as wavelength (%f) is more than twice\n' + 'the plane distance (%f)' % (wl, d)) + try: + return 2.0 * asin(wl / (d * 2)) * TODEG + except ValueError as e: + raise ValueError('asin(wl / (d * 2) with wl=%f and d=%f: ' %(wl, d) + e.args[0]) + + +@command +def hklangle(hkl1, hkl2): + """ + hklangle [h1 k1 l1] [h2 k2 l2] -- calculate angle between [h1 k1 l1] and [h2 k2 l2] planes + """ + return ubcalc.get_hkl_plane_angle(hkl1, hkl2) * TODEG + +### Surface and reference vector stuff ### + +@command +def sigtau(sigma=None, tau=None): + """sigtau {sigma tau} -- sets or displays sigma and tau""" + if sigma is None and tau is None: + chi = settings.hardware.get_position_by_name('chi') # @UndefinedVariable + phi = settings.hardware.get_position_by_name('phi') # @UndefinedVariable + _sigma, _tau = ubcalc.sigma, ubcalc.tau + print "sigma, tau = %f, %f" % (_sigma, _tau) + print " chi, phi = %f, %f" % (chi, phi) + sigma = promptForInput("sigma", -chi) + tau = promptForInput(" tau", -phi) + ubcalc.sigma = sigma + ubcalc.tau = tau + else: + ubcalc.sigma = float(sigma) + ubcalc.tau = float(tau) + + +@command +def setnphi(xyz=None): + """setnphi {[x y z]} -- sets or displays n_phi reference""" + if xyz is None: + ubcalc.print_reference() + else: + ubcalc.set_n_phi_configured(_to_column_vector_triple(xyz)) + ubcalc.print_reference() + +@command +def setnhkl(hkl=None): + """setnhkl {[h k l]} -- sets or displays n_hkl reference""" + if hkl is None: + ubcalc.print_reference() + else: + ubcalc.set_n_hkl_configured(_to_column_vector_triple(hkl)) + ubcalc.print_reference() + + +def _to_column_vector_triple(o): + m = matrix(o) + if m.shape == (1, 3): + return m.T + elif m.shape == (3, 1): + return m + else: + raise ValueError("Unexpected shape matrix: " + m) + +### UB refelections ### + +@command +def showref(): + """showref -- shows full reflection list""" + if ubcalc._state.reflist: + print '\n'.join(ubcalc._state.reflist.str_lines()) + else: + print "<<< No reflections stored >>>" + +@command +def addref(*args): + """ + addref -- add reflection interactively + addref [h k l] {'tag'} -- add reflection with current position and energy + addref [h k l] (p1, .., pN) energy {'tag'} -- add arbitrary reflection + """ + + if len(args) == 0: + h = promptForNumber('h', 0.) + k = promptForNumber('k', 0.) + l = promptForNumber('l', 0.) + if None in (h, k, l): + _handleInputError("h,k and l must all be numbers") + reply = promptForInput('current pos', 'y') + if reply in ('y', 'Y', 'yes'): + positionList = settings.hardware.get_position() # @UndefinedVariable + energy = settings.hardware.get_energy() # @UndefinedVariable + else: + currentPos = settings.hardware.get_position() # @UndefinedVariable + positionList = [] + names = settings.hardware.get_axes_names() # @UndefinedVariable + for i, angleName in enumerate(names): + val = promptForNumber(angleName.rjust(7), currentPos[i]) + if val is None: + _handleInputError("Please enter a number, or press" + " Return to accept default!") + return + positionList.append(val) + muliplier = settings.hardware.energyScannableMultiplierToGetKeV # @UndefinedVariable + energy = promptForNumber('energy', settings.hardware.get_energy() / muliplier) # @UndefinedVariable + if val is None: + _handleInputError("Please enter a number, or press " + "Return to accept default!") + return + energy = energy * muliplier + tag = promptForInput("tag") + if tag == '': + tag = None + pos = settings.geometry.physical_angles_to_internal_position(positionList) # @UndefinedVariable + ubcalc.add_reflection(h, k, l, pos, energy, tag, + datetime.now()) + elif len(args) in (1, 2, 3, 4): + args = list(args) + h, k, l = args.pop(0) + if not (isnum(h) and isnum(k) and isnum(l)): + raise TypeError() + if len(args) >= 2: + pos = settings.geometry.physical_angles_to_internal_position( # @UndefinedVariable + args.pop(0)) + energy = args.pop(0) + if not isnum(energy): + raise TypeError() + else: + pos = settings.geometry.physical_angles_to_internal_position( # @UndefinedVariable + settings.hardware.get_position()) # @UndefinedVariable + energy = settings.hardware.get_energy() # @UndefinedVariable + if len(args) == 1: + tag = args.pop(0) + if not isinstance(tag, str): + raise TypeError() + else: + tag = None + ubcalc.add_reflection(h, k, l, pos, energy, tag, + datetime.now()) + else: + raise TypeError() + +@command +def editref(num): + """editref num -- interactively edit a reflection. + """ + num = int(num) + + # Get old reflection values + [oldh, oldk, oldl], oldExternalAngles, oldEnergy, oldTag, oldT = \ + ubcalc.get_reflection_in_external_angles(num) + del oldT # current time will be used. + + h = promptForNumber('h', oldh) + k = promptForNumber('k', oldk) + l = promptForNumber('l', oldl) + if None in (h, k, l): + _handleInputError("h,k and l must all be numbers") + reply = promptForInput('update position with current hardware setting', + 'n') + if reply in ('y', 'Y', 'yes'): + positionList = settings.hardware.get_position() # @UndefinedVariable + energy = settings.hardware.get_energy() # @UndefinedVariable + else: + positionList = [] + names = settings.hardware.get_axes_names() # @UndefinedVariable + for i, angleName in enumerate(names): + val = promptForNumber(angleName.rjust(7), oldExternalAngles[i]) + if val is None: + _handleInputError("Please enter a number, or press " + "Return to accept default!") + return + positionList.append(val) + muliplier = settings.hardware.energyScannableMultiplierToGetKeV # @UndefinedVariable + energy = promptForNumber('energy', oldEnergy / muliplier) + if val is None: + _handleInputError("Please enter a number, or press Return " + "to accept default!") + return + energy = energy * muliplier + tag = promptForInput("tag", oldTag) + if tag == '': + tag = None + pos = settings.geometry.physical_angles_to_internal_position(positionList) # @UndefinedVariable + ubcalc.edit_reflection(num, h, k, l, pos, energy, tag, + datetime.now()) + +@command +def delref(num): + """delref num -- deletes a reflection (numbered from 1) + """ + ubcalc.del_reflection(int(num)) + +@command +def clearref(): + """clearref -- deletes all the reflections + """ + while ubcalc.get_number_reflections(): + ubcalc.del_reflection(1) + +@command +def swapref(num1=None, num2=None): + """ + swapref -- swaps first two reflections used for calulating U matrix + swapref num1 num2 -- swaps two reflections (numbered from 1) + """ + if num1 is None and num2 is None: + ubcalc.swap_reflections(1, 2) + elif isinstance(num1, int) and isinstance(num2, int): + ubcalc.swap_reflections(num1, num2) + else: + raise TypeError() + +### U calculation from crystal orientation +@command +def showorient(): + """showorient -- shows full list of crystal orientations""" + if ubcalc._state.orientlist: + print '\n'.join(ubcalc._state.orientlist.str_lines()) + else: + print "<<< No crystal orientations stored >>>" + +@command +def addorient(*args): + """ + addorient -- add crystal orientation interactively + addorient [h k l] [x y z] {'tag'} -- add crystal orientation in laboratory frame + """ + + if len(args) == 0: + h = promptForNumber('h', 0.) + k = promptForNumber('k', 0.) + l = promptForNumber('l', 0.) + if None in (h, k, l): + _handleInputError("h,k and l must all be numbers") + + x = promptForNumber('x', 0.) + y = promptForNumber('y', 0.) + z = promptForNumber('z', 0.) + if None in (x, y, z): + _handleInputError("x,y and z must all be numbers") + + tag = promptForInput("tag") + if tag == '': + tag = None + ubcalc.add_orientation(h, k, l, x , y, z, tag, + datetime.now()) + elif len(args) in (1, 2, 3): + args = list(args) + h, k, l = args.pop(0) + if not (isnum(h) and isnum(k) and isnum(l)): + raise TypeError() + x, y, z = args.pop(0) + if not (isnum(x) and isnum(y) and isnum(z)): + raise TypeError() + if len(args) == 1: + tag = args.pop(0) + if not isinstance(tag, str): + raise TypeError() + else: + tag = None + ubcalc.add_orientation(h, k, l, x, y ,z, tag, + datetime.now()) + else: + raise TypeError() + +@command +def editorient(num): + """editorient num -- interactively edit a crystal orientation. + """ + num = int(num) + + # Get old reflection values + [oldh, oldk, oldl], [oldx, oldy, oldz], oldTag, oldT = \ + ubcalc.get_orientation(num) + del oldT # current time will be used. + + h = promptForNumber('h', oldh) + k = promptForNumber('k', oldk) + l = promptForNumber('l', oldl) + if None in (h, k, l): + _handleInputError("h,k and l must all be numbers") + x = promptForNumber('x', oldx) + y = promptForNumber('y', oldy) + z = promptForNumber('z', oldz) + if None in (x, y, z): + _handleInputError("x,y and z must all be numbers") + tag = promptForInput("tag", oldTag) + if tag == '': + tag = None + ubcalc.edit_orientation(num, h, k, l, x, y, z, tag, + datetime.now()) + +@command +def delorient(num): + """delorient num -- deletes a crystal orientation (numbered from 1) + """ + ubcalc.del_orientation(int(num)) + +@command +def clearorient(): + """clearorient -- deletes all the crystal orientations + """ + while ubcalc.get_number_orientations(): + ubcalc.del_orientation(1) + +@command +def swaporient(num1=None, num2=None): + """ + swaporient -- swaps first two crystal orientations used for calulating U matrix + swaporient num1 num2 -- swaps two crystal orientations (numbered from 1) + """ + if num1 is None and num2 is None: + ubcalc.swap_orientations(1, 2) + elif isinstance(num1, int) and isinstance(num2, int): + ubcalc.swap_orientations(num1, num2) + else: + raise TypeError() + + +### UB calculations ### + +@command +def setu(U=None): + """setu {[[..][..][..]]} -- manually set U matrix + """ + if U is None: + U = _promptFor3x3MatrixDefaultingToIdentity() + if U is None: + return # an error will have been printed or thrown + if _is3x3TupleOrList(U) or _is3x3Matrix(U): + ubcalc.set_U_manually(U) + else: + raise TypeError("U must be given as 3x3 list or tuple") + +@command +def setub(UB=None): + """setub {[[..][..][..]]} -- manually set UB matrix""" + if UB is None: + UB = _promptFor3x3MatrixDefaultingToIdentity() + if UB is None: + return # an error will have been printed or thrown + if _is3x3TupleOrList(UB): + ubcalc.set_UB_manually(UB) + else: + raise TypeError("UB must be given as 3x3 list or tuple") + +def _promptFor3x3MatrixDefaultingToIdentity(): + estring = "Please enter a number, or press Return to accept default!" + row1 = promptForList("row1", (1, 0, 0)) + if row1 is None: + _handleInputError(estring) + return None + row2 = promptForList("row2", (0, 1, 0)) + if row2 is None: + _handleInputError(estring) + return None + row3 = promptForList("row3", (0, 0, 1)) + if row3 is None: + _handleInputError(estring) + return None + return [row1, row2, row3] + +@command +def calcub(): + """calcub -- (re)calculate U matrix from ref1 and ref2. + """ + ubcalc.calculate_UB() + +@command +def trialub(): + """trialub -- (re)calculate U matrix from ref1 only (check carefully). + """ + ubcalc.calculate_UB_from_primary_only() + +@command +def orientub(): + """orientub -- (re)calculate U matrix from orient1 and orient2. + """ + ubcalc.calculate_UB_from_orientation() + + + # This command requires the ubcalc + +def checkub(): + """checkub -- show calculated and entered hkl values for reflections. + """ + + s = "\n %7s %4s %4s %4s %6s %6s %6s TAG\n" % \ + ('ENERGY', 'H', 'K', 'L', 'H_COMP', 'K_COMP', 'L_COMP') + s = bold(s) + nref = ubcalc.get_number_reflections() + if not nref: + s += "<>" + for n in range(nref): + hklguess, pos, energy, tag, _ = ubcalc.get_reflection(n + 1) + wavelength = 12.39842 / energy + hkl = settings.angles_to_hkl_function(pos.inRadians(), wavelength, ubcalc.UB) + h, k, l = hkl + if tag is None: + tag = "" + s += ("% 2d % 6.4f % 4.2f % 4.2f % 4.2f % 6.4f % 6.4f " + "% 6.4f %6s\n" % (n + 1, energy, hklguess[0], + hklguess[1], hklguess[2], h, k, l, tag)) + print s + + +@command +def addmiscut(*args): + """addmiscut angle {[x y z]} -- apply miscut to U matrix using a specified miscut angle in degrees and a rotation axis""" + + if len(args) == 0: + _handleInputError("Please specify a miscut angle in degrees " + "and, optionally, a rotation axis (default: [0 1 0])") + else: + args=list(args) + angle = args.pop(0) + rad_angle = float(angle) * TORAD + if len(args) == 0: + xyz = None + else: + xyz = args.pop(0) + ubcalc.set_miscut(xyz, rad_angle, True) + +@command +def setmiscut(*args): + """setmiscut angle {[x y z]} -- manually set U matrix using a specified miscut angle in degrees and a rotation axis (default: [0 1 0])""" + + if len(args) == 0: + _handleInputError("Please specify a miscut angle in degrees " + "and, optionally, a rotation axis (default: [0 1 0])") + else: + args=list(args) + angle = args.pop(0) + rad_angle = float(angle) * TORAD + if len(args) == 0: + xyz = None + else: + xyz = args.pop(0) + ubcalc.set_miscut(xyz, rad_angle, False) + +commands_for_help = ['State', + newub, + loadub, + lastub, + listub, + rmub, + saveubas, + ub, + 'Lattice', + setlat, + c2th, + hklangle] + +if ubcalc.include_reference: + commands_for_help.extend([ + 'Reference (surface)', + setnphi, + setnhkl]) + +if ubcalc.include_sigtau: + commands_for_help.extend([ + 'Surface', + sigtau]) + +commands_for_help.extend([ + 'Reflections', + showref, + addref, + editref, + delref, + clearref, + swapref, + 'Orientations', + showorient, + addorient, + editorient, + delorient, + clearorient, + swaporient, + 'UB matrix', + checkub, + setu, + setub, + calcub, + orientub, + trialub, + refineub, + addmiscut, + setmiscut]) + + + +def _is3x3TupleOrList(m): + if type(m) not in (list, tuple): + return False + if len(m) != 3: + return False + for mrow in m: + if type(mrow) not in (list, tuple): + return False + if len(mrow) != 3: + return False + return True + + +def _is3x3Matrix(m): + return isinstance(m, matrix) and tuple(m.shape) == (3, 3) + + +def _handleInputError(msg): + raise TypeError(msg) \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcalc/util.py b/script/test/diffcalc (copy)/diffcalc/util.py new file mode 100644 index 0000000..873040c --- /dev/null +++ b/script/test/diffcalc (copy)/diffcalc/util.py @@ -0,0 +1,347 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +from math import pi, acos, cos, sin +from functools import wraps +import textwrap + +try: + from gda.jython.commands.InputCommands import requestInput as raw_input + GDA = True +except ImportError: + GDA = False + pass # raw_input unavailable in gda +try: + from numpy import matrix + from numpy.linalg import norm +except ImportError: + from numjy import matrix + from numjy.linalg import norm + + +# from http://physics.nist.gov/ +h_in_eV_per_s = 4.135667516E-15 +c = 299792458 +TWELVEISH = c * h_in_eV_per_s # 12.39842 + + +SMALL = 1e-10 +TORAD = pi / 180 +TODEG = 180 / pi + + +COLOURISE_TERMINAL_OUTPUT = not GDA + +def bold(s): + if not COLOURISE_TERMINAL_OUTPUT: + return s + else: + BOLD = '\033[1m' + END = '\033[0m' + return BOLD + s + END + + +def x_rotation(th): + return matrix(((1, 0, 0), (0, cos(th), -sin(th)), (0, sin(th), cos(th)))) + + +def y_rotation(th): + return matrix(((cos(th), 0, sin(th)), (0, 1, 0), (-sin(th), 0, cos(th)))) + + +def z_rotation(th): + return matrix(((cos(th), -sin(th), 0), (sin(th), cos(th), 0), (0, 0, 1))) + + +def xyz_rotation(u, angle): + u = [list(u), [0, 0, 0], [0, 0, 0]] + u = matrix(u) / norm(matrix(u)) + e11=u[0,0]**2+(1-u[0,0]**2)*cos(angle) + e12=u[0,0]*u[0,1]*(1-cos(angle))-u[0,2]*sin(angle) + e13=u[0,0]*u[0,2]*(1-cos(angle))+u[0,1]*sin(angle) + e21=u[0,0]*u[0,1]*(1-cos(angle))+u[0,2]*sin(angle) + e22=u[0,1]**2+(1-u[0,1]**2)*cos(angle) + e23=u[0,1]*u[0,2]*(1-cos(angle))-u[0,0]*sin(angle) + e31=u[0,0]*u[0,2]*(1-cos(angle))-u[0,1]*sin(angle) + e32=u[0,1]*u[0,2]*(1-cos(angle))+u[0,0]*sin(angle) + e33=u[0,2]**2+(1-u[0,2]**2)*cos(angle) + return matrix([[e11,e12,e13],[e21,e22,e23],[e31,e32,e33]]) + + +class DiffcalcException(Exception): + """Error caused by user misuse of diffraction calculator. + """ + def __str__(self): + lines = [] + for msg_line in self.message.split('\n'): + lines.append('* ' + msg_line) + width = max(len(l) for l in lines) + lines.insert(0, '\n\n' + '*' * width) + lines.append('*' * width) + return '\n'.join(lines) + + +class AbstractPosition(object): + + def inRadians(self): + pos = self.clone() + pos.changeToRadians() + return pos + + def inDegrees(self): + pos = self.clone() + pos.changeToDegrees() + return pos + + def changeToRadians(self): + raise NotImplementedError() + + def changeToDegrees(self): + raise NotImplementedError() + + def totuple(self): + raise NotImplementedError() + + +### Matrices + +def cross3(x, y): + """z = cross3(x ,y) -- where x, y & z are 3*1 Jama matrices""" + [[x1], [x2], [x3]] = x.tolist() + [[y1], [y2], [y3]] = y.tolist() + return matrix([[x2 * y3 - x3 * y2], + [x3 * y1 - x1 * y3], + [x1 * y2 - x2 * y1]]) + + +def dot3(x, y): + """z = dot3(x ,y) -- where x, y are 3*1 Jama matrices""" + return x[0, 0] * y[0, 0] + x[1, 0] * y[1, 0] + x[2, 0] * y[2, 0] + + +def angle_between_vectors(a, b): + costheta = dot3(a * (1 / norm(a)), b * (1 / norm(b))) + return acos(bound(costheta)) + + +## Math + +def bound(x): + """ + moves x between -1 and 1. Used to correct for rounding errors which may + have moved the sin or cosine of a value outside this range. + """ + if abs(x) > (1 + SMALL): + raise AssertionError( + "The value (%f) was unexpectedly too far outside -1 or 1 to " + "safely bound. Please report this." % x) + if x > 1: + return 1 + if x < -1: + return -1 + return x + + +def matrixToString(m): + ''' str = matrixToString(m) --- displays a Jama matrix m as a string + ''' + toReturn = '' + for row in m.array: + for el in row: + toReturn += str(el) + '\t' + toReturn += '\n' + return toReturn + + +def nearlyEqual(first, second, tolerance): + if type(first) in (int, float): + return abs(first - second) <= tolerance + + if type(first) != type(matrix([[1]])): + # lists + first = matrix([list(first)]) + second = matrix([list(second)]) + diff = first - (second) + return norm(diff) <= tolerance + + +def radiansEquivilant(first, second, tolerance): + if abs(first - second) <= tolerance: + return True + if abs((first - 2 * pi) - second) <= tolerance: + return True + if abs((first + 2 * pi) - second) <= tolerance: + return True + if abs(first - (second - 2 * pi)) <= tolerance: + return True + if abs(first - (second + 2 * pi)) <= tolerance: + return True + + return False + + +def degreesEquivilant(first, second, tolerance): + return radiansEquivilant(first * TORAD, second * TORAD, tolerance) + + +def differ(first, second, tolerance): + """Returns error message if the norm of the difference between two arrays + or numbers is greater than the given tolerance. Else returns False. + """ + # TODO: Fix spaghetti + nonArray = False + if type(first) in (int, float): + if type(second) not in (int, float): + raise TypeError( + "If first is an int or float, so must second. " + "first=%s, second=%s" & (repr(first), repr(second))) + first = [first] + second = [second] + nonArray = True + if not isinstance(first, matrix): + first = matrix([list(first)]) + if not isinstance(second, matrix): + second = matrix([list(second)]) + diff = first - second + if norm(diff) >= tolerance: + if nonArray: + return ('%s!=%s' % + (repr(first.tolist()[0][0]), repr(second.tolist()[0][0]))) + return ('%s!=%s' % + (repr(tuple(first.tolist()[0])), + repr(tuple(second.tolist()[0])))) + return False + + +### user input + +def getInputWithDefault(prompt, default=""): + """ + Prompts user for input and returns if possible a float or a list of floats, + or if failing this a string. default may be a number, array of numbers, + or string. + """ + if default is not "": + # Generate default string + if type(default) in (list, tuple): + defaultString = "" + for val in default: + defaultString += str(val) + ' ' + defaultString = defaultString.strip() + else: + defaultString = str(default) + prompt = str(prompt) + '[' + defaultString + ']: ' + else: + prompt = str(prompt) + ': ' + + rawresult = raw_input(prompt) + + # Return default if no input provided + if rawresult == "": + return default + + # Try to process result into list of numbers + try: + result = [] + for val in rawresult.split(): + result.append(float(val)) + except ValueError: + # return a string + return rawresult + if len(result) == 1: + result = result[0] + return result + + +class MockRawInput(object): + def __init__(self, toReturnList): + if type(toReturnList) != list: + toReturnList = [toReturnList] + self.toReturnList = toReturnList + + def __call__(self, prompt): + toReturn = self.toReturnList.pop(0) + if type(toReturn) != str: + raise TypeError + print prompt + toReturn + return toReturn + + +def getMessageFromException(e): + try: # Jython + return e.args[0] + except: + try: # Python + return e.message + except: + # Java + return e.args[0] + + +def promptForNumber(prompt, default=""): + val = getInputWithDefault(prompt, default) + if type(val) not in (float, int): + return None + return val + + +def promptForList(prompt, default=""): + val = getInputWithDefault(prompt, default) + if type(val) not in (list, tuple): + return None + return val + + +def isnum(o): + return isinstance(o, (int, float)) + + +def allnum(l): + return not [o for o in l if not isnum(o)] + + +DEBUG = False + + + +def command(f): + """A decorator to wrap a command method or function. + + Calls to the decorated method or function are wrapped by call_command. + """ + # TODO: remove one level of stack trace by not using wraps + @wraps(f) + def wrapper(*args, **kwds): + return call_command(f, args) + + return wrapper + + +def call_command(f, args): + + if DEBUG: + return f(*args) + try: + return f(*args) + except TypeError, e: + # NOTE: TypeErrors resulting from bugs in the core code will be + # erroneously caught here! TODO: check depth of TypeError stack + raise TypeError(e.message + '\n\nUSAGE:\n' + f.__doc__) + except DiffcalcException, e: + # TODO: log and create a new one to shorten stack trace for user + raise DiffcalcException(e.message) diff --git a/script/test/diffcalc (copy)/diffcmd/__init__.py b/script/test/diffcalc (copy)/diffcmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test/diffcalc (copy)/diffcmd/diffcalc_launcher.py b/script/test/diffcalc (copy)/diffcmd/diffcalc_launcher.py new file mode 100755 index 0000000..0653460 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcmd/diffcalc_launcher.py @@ -0,0 +1,86 @@ +#!/usr/bin/python + +import argparse +import subprocess +import os +import getpass + +DIFFCALC_BIN = os.path.split(os.path.realpath(__file__))[0] +DIFFCALC_ROOT = os.path.abspath(os.path.join(DIFFCALC_BIN, os.pardir)) + +MODULE_FOR_MANUALS = '_make_sixcircle_manual' + +def main(): + parser = argparse.ArgumentParser(description='Diffcalc: A diffraction condition calculator of x-ray and neutron crystalography') + parser.add_argument('--modules', dest='show_modules', action='store_true', + help='list available modules') + parser.add_argument('--python', dest='use_python', action='store_true', + help='run within python rather than ipython') + parser.add_argument('--debug', dest='debug', action='store_true', + help='run in debug mode') + parser.add_argument('--make-manuals-source', dest='make_manuals', action='store_true', + help='make .rst manual files by running template through sixcircle') + parser.add_argument('--non-interactive', dest='non_interactive', action='store_true', + help='do not enter interactive mode after startup') + parser.add_argument('module', type=str, nargs='?', + help='the module to startup with') + args = parser.parse_args() + + # Create list of available modules + module_names = [] + for module_path in os.listdir(os.path.join(DIFFCALC_ROOT, 'startup')): + if not module_path.startswith('_') and module_path.endswith('.py'): + module_names.append(module_path.split('.')[0]) + module_names.sort() + + if args.show_modules: + print_available_modules(module_names) + exit(0) + + if not args.make_manuals and not args.module: + print "A module name should be provided. Choose one of:" + print_available_modules(module_names) + exit(0) + + if args.make_manuals: + if args.module: + print "When building the manuals no module should be given" + exit(1) + args.module = MODULE_FOR_MANUALS + + if not args.make_manuals and args.module not in module_names: + print "The provided argument '%s' is not one of:" % args.module + print_available_modules(module_names) + exit(1) + + env = os.environ.copy() + + if 'PYTHONPATH' not in env: + env['PYTHONPATH'] = '' + env['PYTHONPATH'] = DIFFCALC_ROOT + ':' + env['PYTHONPATH'] + + diffcmd_start_path = os.path.join(DIFFCALC_ROOT, 'diffcmd', 'start.py') + + if args.use_python: + cmd = 'python' + else: # ipython + cmd = 'ipython --no-banner --HistoryManager.hist_file=/tmp/ipython_hist_%s.sqlite' % getpass.getuser() + + iflag = '' if args.non_interactive else '-i' + cmd = cmd + ' ' + ' '.join([iflag, diffcmd_start_path, args.module, str(args.debug)]) + + print 'Running: ' + cmd + rc = subprocess.call(cmd, env=env, shell=True) + exit(rc) + + +def print_available_modules(module_names): + lines = [] + for m in sorted(module_names): + lines.append(' ' + m) + print '\n'.join(lines) + + +if __name__ == '__main__': + main() +# diff --git a/script/test/diffcalc (copy)/diffcmd/diffcmd_utils.py b/script/test/diffcalc (copy)/diffcmd/diffcmd_utils.py new file mode 100644 index 0000000..57ed4aa --- /dev/null +++ b/script/test/diffcalc (copy)/diffcmd/diffcmd_utils.py @@ -0,0 +1,43 @@ +# +# General utility functions to handle Diffcalc commands +# + +from gda.jython.commands.GeneralCommands import alias + +try: + import gda + GDA = True +except ImportError: + GDA = False + + + +def alias_commands(global_namespace_dict): + """Alias commands left in global_namespace_dict by previous import from + diffcalc. + + This is the equivalent of diffcmd/ipython/magic_commands() for use + when IPython is not available + """ + gnd = global_namespace_dict + global GLOBAL_NAMESPACE_DICT + GLOBAL_NAMESPACE_DICT = gnd + print "Aliasing commands" + + ### Alias commands in namespace ### + commands = gnd['hkl_commands_for_help'] + commands += gnd['ub_commands_for_help'] + if not GDA: # TODO: encapsulation issue: this should be done outside this function! + commands.append(gnd['pos']) + commands.append(gnd['scan']) + aliased_names = [] + + for f in commands: + # Skip section headers like 'Motion' + if not hasattr(f, '__call__'): + continue + + alias(f.__name__) + aliased_names.append(f.__name__) + + print "Aliased commands: " + ' '.join(aliased_names) diff --git a/script/test/diffcalc (copy)/diffcmd/ipython.py b/script/test/diffcalc (copy)/diffcmd/ipython.py new file mode 100644 index 0000000..81b4c33 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcmd/ipython.py @@ -0,0 +1,302 @@ +import re +from functools import wraps +from IPython.core.magic import register_line_magic +from IPython import get_ipython # @UnusedImport (used by register_line_magic) +from diffcalc.gdasupport.scannable.hkl import Hkl + +""" +For wrapping functions: + + In [1]: import diffcmd.ipython + + In [2]: diffcmd.ipython.GLOBAL_NAMESPACE_DICT = globals() + + In [3]: from IPython.core.magic import register_line_magic + + In [4]: from diffcmd.ipython import parse_line + + In [5]: @register_line_magic + ...: @parse_line + ...: def check_parser(*args): + ...: return args + ...: + + In [6]: check_parser + Out[6]: + + In [7]: del check_parser + + In [8]: check_parser + Out[8]: () + + In [9]: check_parser 1 + Out[9]: (1,) + + In [10]: check_parser 1 2 + Out[10]: (1, 2) + + In [11]: check_parser 1 2 [3] + Out[11]: (1, 2, [3]) + + In [12]: b='bbb' + + In [13]: check_parser 1 2 [3] b + Out[13]: (1, 2, [3], 'bbb') + + +And to create something dynamically from a function: + + In [28]: def f(a, b, c): + ....: ....: return a, b, c + ....: + + In [29]: register_line_magic(parse_line(f)) + Out[29]: + + In [30]: del f + + In [31]: f 'a' -2 [1 3 -4] + Out[31]: ('a', -2, [1, 3, -4]) + +And from a list of functions: + + In [32]: def one(a): + ....: return a + ....: + + In [33]: def two(a, b): + ....: return a, b + ....: + + In [34]: functions = one, two + + In [35]: del one, two + + In [36]: for f in functions: + ....: register_line_magic(parse_line(f)) + ....: + + In [37]: one 1 + Out[37]: 1 + + In [39]: two 1 2 + Out[39]: (1, 2) + +And to check if we are running in iPython: + + In [47]: 'get_ipython' in globals() + Out[47]: True + +def in_ipython(): + try: + get_ipython() + return True + except NameError: + return False + + +""" + + +GLOBAL_NAMESPACE_DICT = {} + +MATH_OPERATORS = set(['-', '+', '/', '*']) + +# Keep a copy of python's original help as we may remove it later +if 'help' in __builtins__: + ORIGINAL_PYTHON_HELP = __builtins__['help'] + + +COMMA_USAGE_HELP = \ +''' +| When calling a function without brackets, whitespace must be used in +| place of commas. For example: +| +| >>> function a b [1 2 3] 'c' +| +| is equivalent to: +| +| >>> function(a, b, [1, 2, 3], 'c') +| +''' + + +MATH_OPERATOR_USAGE_HELP = \ +''' +| When calling a function without brackets, whitespace is used in place of +| commas. Therefore terms which require evaluation must contain no space. +| These will fail for example: +| +| >>> function - 1 +| >>> function a() * 2 + +| But this + +| >>> function -1 1-1 +1 a()+1 [-1 0+1 b()] c+1 +| +| is okay and equivalent to: +| +| >>> function(-1, 0, 1, a() + 1, [-1, 1, b()], c + 1) +| + +''' + +comma_finder = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''') +space_finder = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''') +hash_finder = re.compile(r'''((?:[^#"']|"[^"]*"|'[^']*')+)''') +open_square_finder = re.compile(r'''((?:[^["']|"[^"]*"|'[^']*')+)''') +close_square_finder = re.compile(r'''((?:[^]"']|"[^"]*"|'[^']*')+)''') + +def tokenify(s): + + # Don't accept commas outside strings. + # Users are frustrated by not knowing when commas _are_ required. + # Making it clear when they are not helps them understand the + # difference. + + if ',' in comma_finder.split(s): + print COMMA_USAGE_HELP + print "(string was: %s)" % s + raise SyntaxError('unexpected comma') + + # ignore comment + hash_split = hash_finder.split(s) + if '#' in hash_split: + s = '' if hash_split[0] == '#' else hash_split[1] + + # surround square brackets with spaces to simplify token extraction + s = ''.join(' [ ' if e == '[' else e for e in open_square_finder.split(s)) + s = ''.join(' ] ' if e == ']' else e for e in close_square_finder.split(s)) + + + # tokens are now separated by spaces + + tokens = space_finder.split(s)[1::2] + tokens = [tok for tok in tokens if tok != ''] + return tokens + + +def parse(s, d): + s = str(s) + tokens = tokenify(s) + for tok in tokens: + if tok in MATH_OPERATORS: + print MATH_OPERATOR_USAGE_HELP + raise SyntaxError('could not evaluate: "%s"' % tok) + + + s = ', '.join(tokens) + + s = s.replace('[, ', '[') + s = s.replace(',]', ']') + s = s.replace(', ]', ']') + + try: + args = eval('[' + s + ']', d) + except SyntaxError: + raise SyntaxError('could not evaluate: "%s"' % s) + return args + +def parse_line(f, global_namespace_dict=None): + '''A decorator that parses a single string argument into a list of arguments + and calls the wrapped function with these. + ''' + if not global_namespace_dict: + global_namespace_dict = GLOBAL_NAMESPACE_DICT + @wraps(f) + def wrapper(line): + args = parse(line, global_namespace_dict) + return f(*args) + return wrapper + + + +_DEFAULT_HELP = \ +""" +For help with diffcalc's orientation phase try: + + >>> help ub + +For help with moving in reciprocal lattice space try: + + >>> help hkl + +For more detailed help try for example: + + >>> help newub + +For help with driving axes or scanning: + + >>> help pos + >>> help scan + +For help with regular python try for example: + + >>> help list + +For more detailed help with diffcalc go to: + + https://diffcalc.readthedocs.io + +""" + +def magic_commands(global_namespace_dict): + """Magic commands left in global_namespace_dict by previous import from + diffcalc. + + Also creates a help command. NOTE that calling this will + remove the original commands from the global namespace as otherwise these + would shadow the ipython magiced versions. + + Depends on hkl_commands_for_help & ub_commands_for_help list having been + left in the global namespace and assumes there is pos and scan command. + """ + gnd = global_namespace_dict + global GLOBAL_NAMESPACE_DICT + GLOBAL_NAMESPACE_DICT = gnd + + ### Magic commands in namespace ### + commands = list(gnd['hkl_commands_for_help']) + commands += gnd['ub_commands_for_help'] + commands.append(gnd['pos']) + commands.append(gnd['scan']) + command_map = {} + for f in commands: + # Skip section headers like 'Motion' + if not hasattr(f, '__call__'): + continue + # magic the function and remove from namespace (otherwise it would + # shadow the magiced command) + register_line_magic(parse_line(f, gnd)) + del gnd[f.__name__] + command_map[f.__name__] = f + + ### Create help function ### + #Expects python's original help to be named pythons_help and to be + #available in the top-level global namespace (where non-diffcalc + #objects may have help called from). + def help(s): # @ReservedAssignment + """Diffcalc help for iPython + """ + if s == '': + print _DEFAULT_HELP + elif s == 'hkl': + # Use help injected into hkl object + print Hkl.dynamic_docstring + elif s == 'ub': + # Use help injected into ub command + print command_map['ub'].__doc__ + elif s in command_map: + print "%s (diffcalc command):" %s + print command_map[s].__doc__ + else: + exec('pythons_help(%s)' %s, gnd) + + + ### Setup help command ### + gnd['pythons_help'] = ORIGINAL_PYTHON_HELP + register_line_magic(help) + # Remove builtin help + # (otherwise it would shadow magiced command + if 'help' in __builtins__: + del __builtins__['help'] diff --git a/script/test/diffcalc (copy)/diffcmd/ipythonmagic.py b/script/test/diffcalc (copy)/diffcmd/ipythonmagic.py new file mode 100644 index 0000000..6679864 --- /dev/null +++ b/script/test/diffcalc (copy)/diffcmd/ipythonmagic.py @@ -0,0 +1,79 @@ + +import diffcmd.ipython +from IPython.core.magic import register_line_magic +from diffcmd.ipython import parse_line + +command_map = {} + +_DEFAULT_HELP = """ +For help with diffcalc's orientation phase try: + + >>> help ub + +For help with moving in reciprocal lattice space try: + + >>> help hkl + +For more detailed help try for example: + + >>> help newub + +For help with driving axes or scanning: + + >>> help pos + >>> help scan + +For help with regular python try for example: + + >>> help list + +For more detailed help with diffcalc go to: + + https://diffcalc.readthedocs.io + +""" + +# This function should be called with parameter globals() +def define_commands(dictionary): + print "Ipython detected - magicing commands" + magiced_names = [] + commands = hkl_commands_for_help + ub_commands_for_help # @UndefinedVariable + commands += [pos, scan] # @UndefinedVariable + ipython.GLOBAL_NAMESPACE_DICT = dictionary + for f in commands: + # Skip section headers like 'Motion' + if not hasattr(f, '__call__'): + continue + + # magic the function and remove from namespace (otherwise it would + # shadow the magiced command) + register_line_magic(parse_line(f)) + del dictionary[f.__name__] + command_map[f.__name__] = f + magiced_names.append(f.__name__) + + print "Magiced commands: " + ' '.join(magiced_names) + + # because the functions have gone from namespace we need to override + pythons_help = __builtins__.help + del __builtins__.help + + register_line_magic(help) + del help + +def help(s): + """Diffcalc help for iPython + """ + if s == '': + print _DEFAULT_HELP + elif s == 'hkl': + # Use help injected into hkl object + print hkl.__doc__ + elif s == 'ub': + # Use help injected into ub command + print command_map['ub'].__doc__ + elif s in command_map: + print "%s (diffcalc command):" %s + print command_map[s].__doc__ + else: + exec('pythons_help(%s)' %s) diff --git a/script/test/diffcalc (copy)/diffcmd/make_manual.py b/script/test/diffcalc (copy)/diffcmd/make_manual.py new file mode 100644 index 0000000..a3b77bf --- /dev/null +++ b/script/test/diffcalc (copy)/diffcmd/make_manual.py @@ -0,0 +1,146 @@ + + +from StringIO import StringIO +from IPython import get_ipython +import sys +from diffcalc.dc.help import format_commands_for_rst_table + + +TEST_INPUT=""" +Diffcalc's Scannables +===================== + +Please see :ref:`moving-in-hkl-space` and :ref:`scanning-in-hkl-space` for some relevant examples. + +To list and show the current positions of your beamline's scannables +use ``pos`` with no arguments:: + + >>> pos wl + +should do nought, but this should be replaced:: + + ==> pos wl 2 + +should do the thing + + ==> abcd +""" + + + +def echorun(magic_cmd): + print "\n>>> " + str(magic_cmd) + + + +def make_manual(input_file_path, + output_file_path, + ub_commands_for_help, + hkl_commands_for_help): + + # Read input file (should be .rst file) + with open(input_file_path, 'r') as f: + input_string = f.read() + + # Parse input string + output_lines = [] + for lineno, line in enumerate(input_string.split('\n')): + process = '==>' in line + + if process and 'STOP' in line: + print "'==> STOP' found on line. STOPPING", lineno + 1 + return + + elif process and 'UB_HELP_TABLE' in line: + print 'Creating UB help table' + output_lines_from_line = format_commands_for_rst_table( + '', ub_commands_for_help) + + elif process and 'HKL_HELP_TABLE' in line: + print 'Creating HKL help table' + output_lines_from_line = format_commands_for_rst_table( + '', hkl_commands_for_help) + + else: + output_lines_from_line = parse_line( + line, lineno + 1, input_file_path) + +# print '\n'.join(output_lines_from_line) + output_lines.extend(output_lines_from_line) + + # Write output file + if output_file_path: + with open(output_file_path, 'w') as f: + f.write('\n'.join(output_lines)) + print "Wrote file:", output_file_path +# try: +# if output_file_path: +# orig_stdout = sys.stdout +# f = file(output_file_path, 'w') +# sys.stdout = f +# +# +# +# finally: +# if output_file_path: +# sys.stdout = orig_stdout +# f.close() + + +def parse_line(linein, lineno, filepath): + output_lines = [] + if '==>' in linein: + pre, cmd = linein.split('==>') + _check_spaces_only(pre, lineno, filepath) + cmd = cmd.strip() # strip whitespace + output_lines.append(pre + ">>> " + cmd) + result_lines = _capture_magic_command_output(cmd, lineno, filepath) + + + # append to output + for line in result_lines: + output_lines.append(pre + line) + else: + output_lines.append(linein) + return output_lines + + +def _check_spaces_only(s, lineno, filepath): + for c in s: + if c != ' ': + raise Exception('Error on line %i of %s :\n text proceeding --> must be ' + 'spaces only' % (lineno, filepath)) + +def _capture_magic_command_output(magic_cmd, lineno, filepath): + orig_stdout = sys.stdout + result = StringIO() + sys.stdout = result + + def log_error(): + msg = "Error on line %i of %s evaluating '%s'" % (lineno, filepath, magic_cmd) + sys.stderr.write('\n' + '=' * 79 + '\n' + msg + '\n' +'v' * 79 + '\n') + return msg + + try: + line_magics = get_ipython().magics_manager.magics['line'] + magic = magic_cmd.split(' ')[0] + if magic not in line_magics: + msg = log_error() + raise Exception(msg + " ('%s' is not a magic command)" % magic) + get_ipython().magic(magic_cmd) + except: + log_error() + raise + finally: + sys.stdout = orig_stdout + + result_lines = result.getvalue().split('\n') + + # trim trailing lines which are whitespace only + while result_lines and (result_lines[-1].isspace() or not result_lines[-1]): + result_lines.pop() + + return result_lines + + + \ No newline at end of file diff --git a/script/test/diffcalc (copy)/diffcmd/start.py b/script/test/diffcalc (copy)/diffcmd/start.py new file mode 100644 index 0000000..faa7b6d --- /dev/null +++ b/script/test/diffcalc (copy)/diffcmd/start.py @@ -0,0 +1,87 @@ +""" +start the diffcmd environemt using a script from startup. +This should normally be run by the main diffcalc.py program. + +with diffcalc on PYTHONPATH +$ ipython -i -m diffcm.diffcmd module_name_string debug_bool +""" +from __future__ import absolute_import + +import diffcalc +import diffcalc.settings +import os +import sys +from diffcalc.ub.persistence import UBCalculationJSONPersister +from diffcalc.util import bold +import diffcalc.util +import diffcalc.gdasupport.minigda.command +DIFFCALC_ROOT = os.path.realpath(diffcalc.__file__).split('diffcalc/__init__.py')[0] + +try: + __IPYTHON__ # @UndefinedVariable + IPYTHON = True +except NameError: + IPYTHON = False + + +module_name = sys.argv[1] #3 if IPYTHON else 1] +debug = sys.argv[2] == 'True' #4 if IPYTHON else 2]) + +print +print bold('-' * 34 + ' DIFFCALC ' + '-' * 35) + +# configure persisentence +DIFFCALC_VAR = os.path.join(os.path.expanduser('~'), '.diffcalc', module_name) +if not os.path.exists(DIFFCALC_VAR): + print "Making diffcalc var folder:'%s'" % DIFFCALC_VAR + os.makedirs(DIFFCALC_VAR) +diffcalc.settings.ubcalc_persister = UBCalculationJSONPersister(DIFFCALC_VAR) + +# configure debug +diffcalc.util.DEBUG = debug +if debug: + print "WARNING: debug mode on; help for command syntax errors disabled." + +# import script +script = os.path.join(DIFFCALC_ROOT, 'startup', module_name) + '.py' + +print "Startup script: '%s'" % script +namespace = {} +execfile(script, namespace) +globals().update(namespace) +diffcalc.gdasupport.minigda.command.ROOT_NAMESPACE_DICT = dict(namespace) +print bold('-' * 36 + ' Help ' + '-' * 37) +print HELP_STRING # @UndefinedVariable +if 'LOCAL_MANUAL' in locals(): + print "Local: " + LOCAL_MANUAL # @UndefinedVariable +print bold('-' * 79) +print + + +# magic commands if IPython +if IPYTHON: + from diffcmd.ipython import magic_commands + magic_commands(globals()) + + +if 'MANUALS_TO_MAKE' in locals(): + summary_lines = ['Made manuals:'] + from diffcmd.make_manual import make_manual + for source_path in MANUALS_TO_MAKE: # @UndefinedVariable + import diffcalc.ub.ub + try: + diffcalc.ub.ub.rmub('example') + except KeyError: + pass + target_path = source_path.replace('_template', '') + print '@' * 79 + print "Making manual" + print " Source:", source_path + print " Target:", target_path + + make_manual(source_path, target_path, + ub_commands_for_help, # @UndefinedVariable + hkl_commands_for_help) # @UndefinedVariable + summary_lines.append(' - ' + source_path + ' -- > ' + target_path) + print '\n'.join(summary_lines) + \ No newline at end of file diff --git a/script/test/diffcalc (copy)/doc/Makefile b/script/test/diffcalc (copy)/doc/Makefile new file mode 100644 index 0000000..032954a --- /dev/null +++ b/script/test/diffcalc (copy)/doc/Makefile @@ -0,0 +1,193 @@ +# Makefile for documentation +# This makefile is a modified version of the makefile generated by the sphinx-quickstart command + +# set environment for Diamond Light Source +ifeq ($(CONTEXT),diamond) + @echo "Environment variable CONTEXT=diamond, so setting build environment suitable for Diamond Light Source" + # get the location of a Python that has Sphinx installed + SPHINXBUILD?=$(shell module load python/2.7.2;which sphinx-build) + # set http proxy + export http_proxy?=http://wwwcache.rl.ac.uk:8080 + export https_proxy?=https://wwwcache.rl.ac.uk:8080 +endif + +# optionally use Sphinx's "-W" options, which converts warnings into errors +ifeq ($(HALTONWARNING),y) + SPHINXEXTRAOPT=-W +else ifeq ($(HALTONWARNING),Y) + SPHINXEXTRAOPT=-W +else + SPHINXEXTRAOPT= +endif + +### <-- start of Makefile contents generated by sphinx-quickstart --> ### + +# You can set these variables from the command line. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +PAPER ?= +BUILDDIR ?= build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXEXTRAOPT) $(SPHINXOPTS) source + +.PHONY: help helpfull pwd clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest pdfa4 pdfletter pdf all + +help: + @echo + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " pdf to create pdf files for both A4- and Letter-sized paper" + @echo " pdfa4 to create pdf files for A4-sized paper" + @echo " pdfletter to create pdf files for Letter-sized paper" + @echo " all to build html and pdf" + @echo " clean to wipe the build directory" + @echo + @echo "You can use \`make helpfull' to get a full list of targets (not all have been tested)" + +helpfull: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +pwd: + @echo + @echo "*******************************************************************************************************" + @echo "Current directory = "`pwd` + @echo "*******************************************************************************************************" + +clean: pwd + -rm -rf $(BUILDDIR)/* + +html: pwd + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GDA.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GDA.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/GDA" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GDA" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: pwd + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: pwd + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +### <-- end of Makefile contents generated by sphinx-quickstart --> ### + +pdfa4: pwd + rm -rf $(BUILDDIR)/latex + rm -rf $(BUILDDIR)/pdf-a4 + make latexpdf PAPER=a4 + mkdir $(BUILDDIR)/pdf-a4/ + cp $(BUILDDIR)/latex/*.pdf $(BUILDDIR)/pdf-a4/. + @echo + @echo "Build finished for A4-sized PDFs. The PDF files are in $(BUILDDIR)/pdf-a4." + +pdfletter: pwd + rm -rf $(BUILDDIR)/latex + rm -rf $(BUILDDIR)/pdf-letter + make latexpdf PAPER=letter + mkdir $(BUILDDIR)/pdf-letter/ + cp $(BUILDDIR)/latex/*.pdf $(BUILDDIR)/pdf-letter/. + @echo + @echo "Build finished for Letter-sized PDFs. The PDF files are in $(BUILDDIR)/pdf-letter." + +pdf: pwd pdfa4 pdfletter + +all: pwd html pdf diff --git a/script/test/diffcalc (copy)/doc/docs-build-diffcalc_doc-linux.launch b/script/test/diffcalc (copy)/doc/docs-build-diffcalc_doc-linux.launch new file mode 100644 index 0000000..0bba525 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/docs-build-diffcalc_doc-linux.launch @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/script/test/diffcalc (copy)/doc/references b/script/test/diffcalc (copy)/doc/references new file mode 100644 index 0000000..a094e55 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/references @@ -0,0 +1,22 @@ + +.. [You1999] H. You. *Angle calculations for a '4S+2D' six-circle diffractometer.* + J. Appl. Cryst. (1999). **32**, 614-623. `(pdf link) + `__. + +.. [Busing1967] W. R. Busing and H. A. Levy. *Angle calculations for 3- and 4-circle X-ray + and neutron diffractometers.* Acta Cryst. (1967). **22**, 457-464. `(pdf link) + `__. + +.. [Vlieg1993] Martin Lohmeier and Elias Vlieg. *Angle calculations for a six-circle + surface x-ray diffractometer.* J. Appl. Cryst. (1993). **26**, 706-716. `(pdf link) + `__. + +.. [Vlieg1998] Elias Vlieg. *A (2+3)-type surface diffractometer: mergence of the z-axis and + (2+2)-type geometries.* J. Appl. Cryst. (1998). **31**, 198-203. `(pdf link) + `__. + +.. [Willmott2011] C. M. Schlepütz, S. O. Mariager, S. A. Pauli, R. Feidenhans'l and + P. R. Willmott. *Angle calculations for a (2+3)-type diffractometer: focus + on area detectors.* J. Appl. Cryst. (2011). **44**, 73-83. `(pdf link) + `__. + diff --git a/script/test/diffcalc (copy)/doc/source/ACKS.rst b/script/test/diffcalc (copy)/doc/source/ACKS.rst new file mode 100644 index 0000000..81fb355 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/ACKS.rst @@ -0,0 +1,34 @@ +################ +Acknowledgements +################ + +.. toctree:: + :maxdepth: 2 + :numbered: + +We would like to acknowledge the people who have made a direct impact on the +Diffcalc project, knowingly or not, in terms of encouragement, suggestions, +criticism, bug reports, code contributions, and related projects. + +Names are ordered alphabetically by surname. + +.. If you add new entries, keep the list sorted by surname! + +.. acks:: + + * Allesandro Bombardi + * Mark Booth + * W. R. Busing + * Steve Collins + * Mirian Garcia-Fernandez + * H. A. Levy + * Martin Lohmier + * Chris Nicklin + * Elias Vlieg --- writer of DIF software used as a model for Diffcalc + * Robert Walton + * H. You + * Fajin Yuan + +Thank you! + +Rob Walton & Irakli Sikharulidze \ No newline at end of file diff --git a/script/test/diffcalc (copy)/doc/source/conf.py b/script/test/diffcalc (copy)/doc/source/conf.py new file mode 100644 index 0000000..fe9ba1d --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/conf.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- + +# +# documentation build configuration file, created by +# sphinx-quickstart on Fri Apr 15 10:03:07 2011. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os, time + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.imgmath', 'sphinx.ext.ifconfig'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Diffcalc' +copyright = u'2017-%s, Diamond Light Source' % time.strftime('%Y') + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '2.1' +# The full version, including alpha/beta/rc tags. +release = '2.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +#html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} +# html_theme_options = { +# 'sidebarbgcolor' : '#f2f2f2', +# 'sidebartextcolor': '#444a95', +# 'sidebarlinkcolor': '#0b0f40', +# } + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + + +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" + +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = 'diffcalc_web.png' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +#html_static_path = ['_static'] +html_static_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True +html_show_sourcelink = False + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' +latex_paper_size = 'a4' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('youmanual', 'diffcalc_user_guide.tex', u'Diffcalc User Guide', + u'Diamond Light Source', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page +latex_logo = 'diffcalc_pdf.png' + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +#man_pages = [] + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {} + +""" + Additional options for Diffcalc +""" + +todo_include_todos = True + +extlinks = { + 'opengda_url' :('http://www.opengda.org/%s', None), + } + +rst_prolog = """ +.. |DLS| replace:: :abbr:`DLS (Diamond Light Source)` +""" diff --git a/script/test/diffcalc (copy)/doc/source/developer/contents.rst b/script/test/diffcalc (copy)/doc/source/developer/contents.rst new file mode 100644 index 0000000..d52fdd9 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/developer/contents.rst @@ -0,0 +1,25 @@ +######################## +Diffcalc Developer Guide +######################## + +:Author: Rob Walton +:Contact: rob.walton (at) diamond (dot) ac (dot) uk +:Website: http://www.opengda.org/ + +.. rubric:: Diffcalc: A diffraction condition calculator for diffractometer control + +.. toctree:: + :maxdepth: 2 + :numbered: + + intro + package + quickstart_api + development + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/script/test/diffcalc (copy)/doc/source/developer/development.rst b/script/test/diffcalc (copy)/doc/source/developer/development.rst new file mode 100644 index 0000000..12b90c6 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/developer/development.rst @@ -0,0 +1,24 @@ +Development +=========== + +The files are kept here_ on github_. See bootcamp for an introduction to +using github. To contribute please fork the project. Otherwise you can make +a read-only clone or export. + +Code format should follow pep8 guidelines. PyDev has a good pep8 checker. + +To run the tests install nose_, change directory into the test folder and run:: + + $ nosetests + .......... ... + ---------------------------------------------------------------------- + Ran 3914 tests in 9.584s + + OK (SKIP=15) + + + +.. _here: https://github.com/DiamondLightSource/diffcalc +.. _github: https://github.com +.. _nose: http://nose.readthedocs.org/en/latest/ +.. _pep8: http://www.python.org/dev/peps/pep-0008/ diff --git a/script/test/diffcalc (copy)/doc/source/developer/intro.rst b/script/test/diffcalc (copy)/doc/source/developer/intro.rst new file mode 100644 index 0000000..a45eae4 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/developer/intro.rst @@ -0,0 +1,55 @@ +Introduction +============ + +Diffcalc is a diffraction condition calculator used for controlling +diffractometers within reciprocal lattice space. It performs the same +task as the ``fourc``, ``sixc``, ``twoc``, ``kappa``, ``psic`` and +``surf`` macros from SPEC_. + +Diffcalc's standard calculation engine is an implementation of +[You1999]_ . The first versions of Diffcalc were based on +[Vlieg1993]_ and [Vlieg1998]_ and a 'Vlieg' engine is still +available. The 'You' engine is more generic and the plan is to remove +the old 'Vlieg' engine once beamlines have been migrated. New users +should use the 'You' engine. + +The foundations for this type of calculation were laid by by Busing & +Levi in their classic paper [Busing1967]_. Diffcalc's orientation +algorithm is taken from this paper. Busing & Levi also provided the +original definition of the coordinate frames and of the U and B +matrices used to describe a crystal's orientation and to convert between +Cartesian and reciprical lattice space. + +Geometry plugins are used to adapt the six circle model used +internally by Diffcalc to apply to other diffractometers. These +contain a dictionary of the 'missing' angles which Diffcalc uses to +constrain these angles internally, and a methods to map from external +angles to Diffcalc angles and visa versa. + +Options to use Diffcalc: + +- **The User manual next to this developer manual or README file on github.** +- The :ref:`quickstart-api` section describes how to run up only + the core in Python_. This provides a base option for system integration. + +Diffcalc will work with Python 2.7 or higher with numpy_, or with +Jython 2.7 of higher with Jama_. + + +.. [*] The very small 'Willmott' engine currently handles the case for + surface diffraction where the surface normal is held vertical + [Willmott2011]_. The 'You' engine handles this case fine, but + currently spins nu into an unhelpful quadrant. We hope to + remove the need for this engine soon. + + +.. _SPEC: http://www.certif.com/ +.. _Python: http://python.org +.. _IPython: http://http://ipython.org/ +.. _Jython: http://jython.org +.. _OpenGDA: http://opengda.org +.. _numpy: http://numpy.scipy.org/ +.. _Jama: http://math.nist.gov/javanumerics/jama/ + + +.. include:: ../../references diff --git a/script/test/diffcalc (copy)/doc/source/developer/package.rst b/script/test/diffcalc (copy)/doc/source/developer/package.rst new file mode 100644 index 0000000..e4d3210 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/developer/package.rst @@ -0,0 +1,30 @@ +Project Files & Directories +=========================== + +diffcalc + The main source package. + +test + Diffcalcs unit-test package (use Nose_ to run them). + +diffcmd + A spec-like openGDA emulator. + +numjy + A *very* minimal implentation of numpy for jython. It supports only what + Diffcalc needs. + +doc + The documentation is written in reStructuredText and can be compiled into + html and pdf using Python's `Sphinx `_. With Sphinx + installed use ``make clean all`` from within the user and developer guide + folders to build the documentation. + +startup + Starup scripts called by diffcmd or openGDA to startup diffcalc + +model + Vrml models of diffractometers and a hokey script for animating then and + controlling them from diffcalc. + +.. _Nose: http://readthedocs.org/docs/nose/en/latest/ \ No newline at end of file diff --git a/script/test/diffcalc (copy)/doc/source/developer/quickstart_api.rst b/script/test/diffcalc (copy)/doc/source/developer/quickstart_api.rst new file mode 100644 index 0000000..96bb9a2 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/developer/quickstart_api.rst @@ -0,0 +1,266 @@ +.. _quickstart-api: + +.. warning:: This documentation is out of date. The README and the user doc has been updated recently. For now if you need help with API, please contact me at Diamond. -- Rob Walton + +Quick-Start: Python API +======================= + +This section describes how to run up only the core in Python or +IPython. This provides an API which could be used to integrate Diffcalc into an +existing data acquisition system; although the interface described in +the README would normally provide a better starting point. + +For a full description of what Diffcalc does and how to use it please +see the 'Diffcalc user manual'. + +Setup environment +----------------- + +.. include:: quickstart_setup_environment + + +Start +----- + +With Python start the sixcircle_api.py example startup script (notice +the -i and -m) and call `demo_all()`:: + + $ python -i -m startup.api.sixcircle + >>> demo_all() + +IPython requires:: + + $ ipython -i startup/api/sixcircle.py + >>> demo_all() + +Alternatively start Python or IPython and cut and paste lines from the rest of +this tutorial. + +Configure a diffraction calculator +---------------------------------- + +By default some exceptions are handled in a way to make user interaction +friendlier. Switch this off with:: + + >>> import diffcalc.util + >>> diffcalc.util.DEBUG = True + +To setup a Diffcalc calculator, first configure `diffcalc.settings` module:: + + >>> from diffcalc import settings + >>> from diffcalc.hkl.you.geometry import SixCircle + >>> from diffcalc.hardware import DummyHardwareAdapter + >>> settings.hardware = DummyHardwareAdapter(('mu', 'delta', 'gam', 'eta', 'chi', 'phi')) + >>> settings.geometry = SixCircle() # @UndefinedVariable + +The hardware adapter is used by Diffcalc to read up the current angle +settings, wavelength and axes limits. It is primarily used to simplify +commands for end users. It could be dropped for this API use, but it +is also used for the important job of checking axes limits while +choosing solutions. + +Geometry plugins are used to adapt the six circle model used +internally by Diffcalc to apply to other diffractometers. These +contain a dictionary of the 'missing' angles which Diffcalc internally +uses to constrain these angles, and a methods to map from +external angles to Diffcalc angles and visa versa. + + +Calling the API +--------------- + +The ``diffcalc.dc.dcyou`` module (and others) read the ``diffcalc.settings`` module when first +imported. Note that this means that changes to the settings will most likely +have no effect unless ``diffcalc.dc.dcyou`` is reloaded:: + + >>> import diffcalc.dc.dcyou as dc + +This includes the two critical functions:: + + def hkl_to_angles(h, k, l, energy=None): + """Convert a given hkl vector to a set of diffractometer angles + + return angle tuple and virtual angles dictionary + """ + + def angles_to_hkl(angle_tuple, energy=None): + """Converts a set of diffractometer angles to an hkl position + + Return hkl tuple and virtual angles dictionary + """ + +``diffcalc.dc.dcyou`` also brings in all the commands from ``diffcalc.ub.ub``, +``diffcalc.hardware`` and ``diffcalc.hkl.you.hkl``. That is it includes all the +commands exposed in the top level namespace when diffcalc is used interactively:: + + >>> dir(dc) + + ['__builtins__', '__doc__', '__file__', '__name__', '__package__', + '_hardware','_hkl', '_ub', 'addref', 'allhkl', 'angles_to_hkl', 'c2th', + 'calcub', 'checkub', 'clearref', 'con', 'constraint_manager', 'delref', + 'diffcalc', 'editref', 'energy_to_wavelength', 'hardware', 'hkl_to_angles', + 'hklcalc', 'lastub', 'listub', 'loadub', 'newub', 'rmub', 'saveubas', 'setcut', + 'setlat', 'setmax', 'setmin', 'settings', 'setu', 'setub', 'showref', + 'swapref', 'trialub', 'ub', 'ub_commands_for_help', 'ubcalc', 'uncon'] + +This doesn't form the best API to program against though, so it is best to +use the four modules more directly. The example below assumes you have +also imported:: + + >>> from diffcalc.ub import ub + >>> from diffcalc import hardware + >>> from diffcalc.hkl.you import hkl + +Getting help +------------ + +To get help for the diffcalc angle calculations, the orientation phase, the +angle calculation phase, and the dummy hardware adapter commands:: + + >>> help(dc) + >>> help(ub) + >>> help(hkl) + >>> help(hardware) + + +Orientation +----------- + +To orient the crystal for example (see the user manual for a fuller +tutorial) first find some reflections:: + + # Create a new ub calculation and set lattice parameters + ub.newub('test') + ub.setlat('cubic', 1, 1, 1, 90, 90, 90) + + # Add 1st reflection (demonstrating the hardware adapter) + hardware.settings.hardware.wavelength = 1 + ub.c2th([1, 0, 0]) # energy from hardware + settings.hardware.position = 0, 60, 0, 30, 0, 0 # mu del nu eta chi ph + ub.addref([1, 0, 0]) # energy & pos from hardware + + # Add 2nd reflection (this time without the hardware adapter) + ub.c2th([0, 1, 0], 12.39842) + ub.addref([0, 1, 0], [0, 60, 0, 30, 0, 90], 12.39842) + + +To check the state of the current UB calculation:: + + >>> ub.ub() + + UBCALC + + name: test + + n_phi: 0.00000 0.00000 1.00000 <- set + n_hkl: -0.00000 0.00000 1.00000 + miscut: None + + CRYSTAL + + name: cubic + + a, b, c: 1.00000 1.00000 1.00000 + 90.00000 90.00000 90.00000 + + B matrix: 6.28319 0.00000 0.00000 + 0.00000 6.28319 0.00000 + 0.00000 0.00000 6.28319 + + UB MATRIX + + U matrix: 1.00000 0.00000 0.00000 + 0.00000 1.00000 0.00000 + 0.00000 0.00000 1.00000 + + U angle: 0 + + UB matrix: 6.28319 0.00000 0.00000 + 0.00000 6.28319 0.00000 + 0.00000 0.00000 6.28319 + + REFLECTIONS + + ENERGY H K L MU DELTA GAM ETA CHI PHI TAG + 1 12.398 1.00 0.00 0.00 0.0000 60.0000 0.0000 30.0000 0.0000 0.0000 + 2 12.398 0.00 1.00 0.00 0.0000 60.0000 0.0000 30.0000 0.0000 90.0000 + +And finally to check the reflections were specified acurately:: + + >>> dc.checkub() + + ENERGY H K L H_COMP K_COMP L_COMP TAG + 1 12.3984 1.00 0.00 0.00 1.0000 0.0000 0.0000 + 2 12.3984 0.00 1.00 0.00 -0.0000 1.0000 0.0000 + +Motion +------ + +Hkl positions and virtual angles can now be read up from angle +settings (the easy direction!):: + + + >>> dc.angles_to_hkl((0., 60., 0., 30., 0., 0.)) # energy from hardware + + ((1.0, 5.5511151231257827e-17, 0.0), + {'alpha': -0.0, + 'beta': 3.5083546492674376e-15, + 'naz': 0.0, + 'psi': 90.0, + 'qaz': 90.0, + 'tau': 90.0, + 'theta': 29.999999999999996}) + +Before calculating the settings to reach an hkl position (the trickier +direction) hardware limits must be set and combination of constraints +chosen. The constraints here result in a four circle like mode with a +vertical scattering plane and incident angle 'alpha' equal to the exit +angle 'beta':: + + >>> hkl.con('qaz', 90) + ! 2 more constraints required + qaz: 90.0000 + + >>> hkl.con('a_eq_b') + ! 1 more constraint required + qaz: 90.0000 + a_eq_b + + >>> hkl.con('mu', 0) + qaz: 90.0000 + a_eq_b + mu: 0.0000 + +To check the constraints:: + + >>> hkl.con() + DET REF SAMP + ====== ====== ====== + delta --> a_eq_b --> mu + alpha eta + --> qaz beta chi + naz psi phi + mu_is_nu + + qaz: 90.0000 + a_eq_b + mu: 0.0000 + + Type 'help con' for instructions + +Limits can be set to help Diffcalc choose a solution:: + + >>> hardware.setmin('delta', 0) # used when choosing solution + +Angles and virtual angles are then easily determined for a given hkl reflection:: + + >>> dc.hkl_to_angles(1, 0, 0) # energy from hardware + ((0.0, 60.0, 0.0, 30.0, 0.0, 0.0), + {'alpha': -0.0, + 'beta': 0.0, + 'naz': 0.0, + 'psi': 90.0, + 'qaz': 90.0, + 'tau': 90.0, + 'theta': 30.0} + ) diff --git a/script/test/diffcalc (copy)/doc/source/developer/quickstart_setup_environment b/script/test/diffcalc (copy)/doc/source/developer/quickstart_setup_environment new file mode 100644 index 0000000..fe36a74 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/developer/quickstart_setup_environment @@ -0,0 +1,25 @@ + +Change directory to the diffcalc project (python adds the current +working directory to the path):: + + $ cd diffcalc + $ ls + COPYING diffcalc doc example mock.py mock.pyc model numjy test + +If using Python make sure numpy and diffcalc can be imported:: + + $ python + Python 2.7.2+ (default, Oct 4 2011, 20:06:09) + [GCC 4.6.1] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> import numpy + >>> import diffcalc + +If using Jython make sure Jama and diffcalc can be imported:: + + $ jython -Dpython.path=:/Jama-1.0.1.jar + + Jython 2.2.1 on java1.5.0_11 + Type "copyright", "credits" or "license" for more information. + >>> import Jama + >>> import diffcalc diff --git a/script/test/diffcalc (copy)/doc/source/diffcalc_pdf.png b/script/test/diffcalc (copy)/doc/source/diffcalc_pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..7e08c66384d719f12e573f52e1fa871d1db2acdb GIT binary patch literal 20720 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

$~-0*kyrhZrZa2kiJzWi20 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%0Ajw+g3^M6w``2$!X0vA9J<~xCO!0WdJ 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. + +.. toctree:: + :maxdepth: 2 + + youmanual + vliegmanual/contents + developer/contents + ACKS + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/script/test/diffcalc (copy)/doc/source/vliegmanual/contents.rst b/script/test/diffcalc (copy)/doc/source/vliegmanual/contents.rst new file mode 100644 index 0000000..7d60a69 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/vliegmanual/contents.rst @@ -0,0 +1,22 @@ +############################################# +Diffcalc User Guide (Deprecated Vlieg Engine) +############################################# + +:Author: Rob Walton +:Contact: rob.walton (at) diamond (dot) ac (dot) uk +:Website: http://www.opengda.org/ + +.. rubric:: Diffcalc: A diffraction condition calculator for diffractometer control + +.. toctree:: + :maxdepth: 2 + :numbered: + + vliegmanual + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/script/test/diffcalc (copy)/doc/source/vliegmanual/images/fix.png b/script/test/diffcalc (copy)/doc/source/vliegmanual/images/fix.png new file mode 100644 index 0000000000000000000000000000000000000000..b4307d4abbf01b89ae8a87a91a54349cf5955640 GIT binary patch literal 283 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 literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/doc/source/vliegmanual/images/sixcircle_gamma_on_arm.png b/script/test/diffcalc (copy)/doc/source/vliegmanual/images/sixcircle_gamma_on_arm.png new file mode 100755 index 0000000000000000000000000000000000000000..a1f2d93c9d2cd7d279661f56e677f03041baa8fa GIT binary patch literal 73974 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 literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/doc/source/vliegmanual/images/sixcircle_gamma_on_arm.ppt b/script/test/diffcalc (copy)/doc/source/vliegmanual/images/sixcircle_gamma_on_arm.ppt new file mode 100755 index 0000000000000000000000000000000000000000..0af759a3ad4e89d8934e445a57be1f2fe86beb18 GIT binary patch literal 79360 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 literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/doc/source/vliegmanual/images/unit_cell.pdf b/script/test/diffcalc (copy)/doc/source/vliegmanual/images/unit_cell.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3114c3510fe799b1b3de8c4c91a0580e4e2f002f GIT binary patch literal 5812 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 E5BKnt!~g&Q literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/doc/source/vliegmanual/images/unit_cell.png b/script/test/diffcalc (copy)/doc/source/vliegmanual/images/unit_cell.png new file mode 100644 index 0000000000000000000000000000000000000000..b4314586dc7f68cede020a0228d8fc58ca2f6224 GIT binary patch literal 1598 zcmV-E2EqA>P)P-WS10Q`PU-FM~OiS=TKO{$w&HUk<_D~WziWVZY(>?vpmZ;llwAN z-XzPH=`y*W?^dRdeNR$lxSeIV@np$Js@z_u%2zB;ls~|hq{*)Uu9GBNi@zd8(xup~ zQ{+ZUm?g;iQaxEr%TiabCnX0{J)e>XZmRdiglv!1>j}AnrTuq4E~{Q1(<3T5fyc+f z(=pkBBl&zp?p$fxM`hzl(>^K}0J+=kJSvGV^`yq6d|!GJt|IavaN)l;$7LHxxWVNr zlp+?GtU`G}rFcEC(yzNkTkjy0zoT-8$X`*}K=9GQAEI&*%C18t3FQuz3_{tUlFHq7 z&>@P+4I&9D>rgU4Wf@9}O8uenV$h)$kq1Q9sB95gqH?A7&juaBWEIMTnhQfIC+ASM zsALez6)x53^iqiIP#wzdX&_~XFsW0UD?7lYPHnkUMR7@WYIEgAcNLK`wYaj5N}1YR zS>jTK{mM-tYI2c?&GpJ6LF)ENOji2LtG#Srzgy^pl-CP1O^Qjq8IxCi3R#+d zV^R#@7bn@wOZCv=a_2~s#Upa#Ca|cKVOu#Vg!h3$hYzDH?y#ZXl{Eua=WPc4*t9=)dPv4oW#NVZJ4OFQ; zJ*ga~5KPKvS~S_5KyqD=rI0%%J1wzfa{@`V%)PRS{#}wPz%80=P9|At%Z=LWnEJAL zr?OTOO(AzmR!XAD<^)o8utihIU6MM$Et*1NQlI=3*_Pu{hpiPUWK;HK6Vtbo^Hllz z0^n~lE_Hx+?-6rMH-!x4BXO+fc>$Znl>9{Of6Q0Yl7B1q-{kG2Y^7wk1vS!5Hi)FR z1xu6dZNZ6NY>CT8%FXK%$X}!nxQV_m;nNCgA*+ok->hQtdjgd!4^b($Qpox!Yh0=T z6Y-_o$Qrup82vzU`SrcAWi}!^UlLqaTH4bi%2LvZq*AuF%popYPqJj$MI=$@@P!lb zLmz!yQd@pGB5O}VnlxcVB-f*O*2`zd*?`KcUM|U~%<^95xKsfqEOWLCuqgzQ`}ea8 zYRc-E5y@fwFw16Ek}T>{c&(LOIC(pdo$WihGm%`qtg ztYX=h#rQ9%xg;CNC3sSdKl_fM@@gPO`yZgv8<3iA+iZqN0{)UJQP0oVeMEW#(sI_a zIU?1)Oty0*z>0l2l-vx6eSIve-l9^)vhyS-nAFQ@@W7QFVA90WlRHFqb}{)UTb5e` z>_H%@iRI6(TmX9#$Qu*OC$1#+X;-dIELZwLf4>Jr(xr*z!jTl0WhghOydADgT2`psgwm`L4du;1*2fE= zp)~W@l{-vshwn@&o1x?%1N>I@%i>2_2gm$SOTYcfjir_Odd^A!|F;&E|05AC7XW{% z*I#?`5XwTg4qjp+ZY-_5zn*L9wolwx+OdBja;)k7wIi!QG7-p|!c-JT>BbUU3AGc2 zd$B}UfoE3krle_~d$GjV-`7SiCZ+zZMb{^qxK!D(V=ygM`yEY@YVBr|B=;rII!TI2 wP0KXN=l?*Og#AaU5}qeZILos<%m17F7aGz-opE?&EC2ui07*qoM6N<$f>ICp>Hq)$ literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/doc/source/vliegmanual/vliegmanual.rst b/script/test/diffcalc (copy)/doc/source/vliegmanual/vliegmanual.rst new file mode 100644 index 0000000..4638b3e --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/vliegmanual/vliegmanual.rst @@ -0,0 +1,827 @@ +Introduction +============ + +.. warning:: + + This manual refers to the 'Vlieg' calculation available in Diffcalc I. By + default Diffcalc II now uses its 'You' engine. This manual will be updated + soon. For now the developer guide shows how the new constraint system works. + +This manual assumes that you are running Diffcalc within the external +framework of the GDA or Minigda and that Diffcalc has been configured +for the six circle diffractometer pictured here: + +.. figure:: images/sixcircle_gamma_on_arm.* + :scale: 50 + :align: center + + Gamma-on-delta six-circle diffractometer, modified from Elias Vlieg + & Martin Lohmeier (1993) + +Your Diffcalc configuration will have been customised for the geometry +of your diffractometer and possibly the types of experiment you +perform. For example: a five-circle diffractometer might be missing +the Gamma circle above, some six-circle modes and the option to fix +gamma that would otherwise exist in some modes. + +The laboratory, crystal and reciprocal-lattice coordinate frames are +defined with respect to the beam and to gravity to be (for a cubic crystal): + +.. figure:: images/fix.png + :align: center + + Laboratory and illustratrive crystal coordinate frames for a cubic crystal + +The crystal lattice basis vectors are defined within the Cartesian +crystal coordinate frame to be: + +.. figure:: images/unit_cell.* + :align: center + :scale: 100 + + Unit cell defined in crystal coordinate frame + +.. _overview: + +Overview +======== + +The following assumes that the diffractometer has been properly levelled, aligned with +the beam and zeroed. See the `SPEC fourc manual `__. + +Before moving in hkl space you must calculate a UB matrix by +specifying the crystal's lattice parameters (which define the B +matrix) and finding two reflections (from which the +U matrix can be inferred); and, optionally for surface-diffraction +experiments, determine how the surface of the crystal is oriented with +respect to the phi axis. + + +Once a UB matrix has been calculated, the diffractometer may be driven +in hkl coordinates. A valid diffractometer setting maps easily into a +single hkl value. However for a diffractometer with more than three circles +there are excess degrees of freedom when calculating a diffractometer +setting from an hkl value. Diffcalc provides modes for using up +the excess degrees of freedom. + +Diffcalc does not perform scans directly. Instead, scannables that use +diffcalc to map between reciprocal lattice space and real +diffractometer settings are scanned using the Gda's (or minigda's) +generic scan mechanism. + + +Theory +------ + +Thanks to Elias Vlieg for sharing his dos based ``DIF`` software that +Diffcalc has borrowed heavily from. (See also the THANKS.txt file). + +See the papers (included in ``docs/ref``): + +* Busing & Levi (1966), "Angle Calculations for 3- and 4- Circle X-ray + and Neutron Diffractometers", Acta Cryst. 22, 457 + +* Elias Vlieg & Martin Lohmeier (1993), "Angle Calculations for a Six-Circle + Surface X-ray Diffractometer", J. Appl. Cryst. 26, 706-716 + +Getting Help +============ + +There are few commands to remember. If a command is called without +arguments, Diffcalc will prompt for arguments and provide sensible +defaults which can be chosen by pressing enter. + +The ``helpub`` and ``helphkl`` commands provide help with the crystal +orientation and hkl movement phases of an experiment respectively:: + + >>> helpub + + Diffcalc + -------- + helpub ['command'] - lists all ub commands, or one if command is given + helphkl ['command'] - lists all hkl commands, or one if command is given + + UB State + -------- + newub 'name' - starts a new ub calculation with no lattice or + reflection list + loadub 'name' - loads an existing ub calculation: lattice and + reflection list + saveubas 'name' - saves the ubcalculation with a new name (other + changes autosaved) + ub - shows the complete state of the ub calculation + + UB lattice + ---------- + setlat - prompts user to enter lattice parameters (in + Angstroms and Deg.) + setlat 'name' a - assumes cubic + setlat 'name' a b - assumes tetragonal + setlat 'name' a b c - assumes ortho + setlat 'name' a b c gam - assumes mon/hex with gam not equal to 90 + setlat 'name' a b c alpha beta gamma - arbitrary + + UB surface + ---------- + sigtau [sigma tau] - sets sigma and tau + + UB reflections + -------------- + showref - shows full reflection list + addref - add reflection + addref h k l ['tag'] - add reflection with hardware position and energy + addref h k l (p1,p2...pN) energy ['tag']- add reflection with specified position + and energy + delref num - deletes a reflection (numbered from 1) + swapref - swaps first two reflections used for calculating U + swapref num1 num2 - swaps two reflections (numbered from 1) + + UB calculation + -------------- + setu [((,,),(,,),(,,))] - manually set u matrix + setub ((,,),(,,),(,,)) - manually set ub matrix + calcub - (re)calculate u matrix from ref1 and ref2 + checkub - show calculated and entered hkl values for reflections + + >>> helphkl + + Diffcalc + -------- + helphkl [command] - lists all hkl commands, or one if command is given + helpub [command] - lists all ub commands, or one if command is given + + Settings + -------- + hklmode [num] - changes mode or shows current and available modes + and all settings + setalpha [num] - fixes alpha, or shows all settings if no num given + setgamma [num] - fixes gamma, or shows all settings if no num given + setbetain [num] - fixes betain, or shows all settings if no num given + setbetaout [num] - fixes betaout, or shows all settings if no num given + trackalpha [boolean] - determines wether alpha parameter will track alpha axis + trackgamma [boolean] - determines wether gamma parameter will track gamma axis + trackphi [boolean] - determines wether phi parameter will track phi axis + setsectorlim [omega_high omega_low phi_high phi_low]- sets sector limits + + Motion + ------ + pos hkl [h k l] - move diffractometer to hkl, or read hkl position. + Use None to hold a value still + sim hkl [h k l] - simulates moving hkl + hkl - shows loads of info about current hkl position + pos sixc [alpha, delta, gamma, omega, chi, phi,]- move diffractometer to Eularian + position. Use None to hold a + value still + sim sixc [alpha, delta, gamma, omega, chi, phi,]- simulates moving sixc + sixc - shows loads of info about current sixc position + + +Diffcalc's Scannables +===================== + +Please see :ref:`moving-in-hkl-space` and :ref:`scanning-in-hkl-space` for some relevant examples. + +To list and show the current positions of your beamline's scannables +use ``pos`` with no arguments:: + + >>> pos + +Results in: + +**Energy and wavelength scannables**:: + + energy 12.3984 + wl: 1.0000 + +**Diffractometer scannables**, as a group and in component axes (in +the real GDA these have limits):: + + sixc: alpha: 0.0000 delta: 0.0000 gamma: 0.0000 omega: 0.0000 chi: 0.0000 phi: 0.0000 + alpha: 0.0000 + chi: 0.0000 + delta: 0.0000 + gamma: 0.0000 + omega: 0.0000 + phi: 0.0000 + +**Dummy counter**, which in this example simply counts at 1hit/s:: + + cnt: 0.0000 + +**Hkl scannable**, as a group and in component:: + + hkl: Error: No UB matrix + h: Error: No UB matrix + k: Error: No UB matrix + l: Error: No UB matrix + +**Parameter scannables**, used in some modes, these provide a +scannable alternative to the series of ``fix`` commands described in +:ref:`moving-in-hkl-space`.:: + + alpha_par:0.00000 + azimuth: --- + betain: --- + betaout: --- + gamma_par:0.00000 + phi_par: --- + + Note that where a parameter corresponds with a physical + diffractometer axis, it can also be set to track that axis + directly. See `Tracking axis`_ below. + +Crystal orientation +=================== + +Before moving in hkl space you must calculate a UB matrix by +specifying the crystal's lattice parameters (which define the B +matrix) and finding two reflections (from which the +U matrix can be inferred); and, optionally for surface-diffraction +experiments, determine how the surface of the crystal is oriented with +respect to the phi axis (see :ref:`overview`). + +Starting a UB calculation +------------------------- + +A *UB-calculation* contains the description of the crystal-under-test, +any saved reflections, sigma & tau (both default to 0), and a B & UB +matrix pair if they have been calculated or manually specified. +Starting a new UB calculation will clear all of these. + +Before starting a UB-calculation, the ``ub`` command used to summarise +the state of the current UB-calculation, will reflect that no +UB-calculation has been started:: + + >>> ub + No UB calculation started. + Wavelength: 1.239842 + Energy: 10.000000 + +A new UB-calculation calculation may be started and lattice specified +explicitly:: + + >>> newub 'b16_270608' + >>> setlat 'xtal' 3.8401 3.8401 5.43072 90 90 90 + +or interactively:: + + >>> newub + calculation name: b16_270608 + crystal name: xtal + a [1]: 3.8401 + b [3.8401]: 3.8401 + c [3.8401]: 5.43072 + alpha [90]: 90 + beta [90]: 90 + gamma [90]: 90 + +where a,b and c are the lengths of the three unit cell basis vectors +in Angstroms, and alpha, beta and gamma the typically used angles +(defined in the figure above) in Degrees. + +The ``ub`` command will show the state of the current UB-calculation +(and the current energy for reference):: + + UBCalc: b16_270608 + ====== + + Crystal + ------- + name: xtal + + lattice: a ,b ,c = 3.84010, 3.84010, 5.43072 + alpha, beta , gamma = 90.00000, 90.00000, 90.00000 + + reciprocal: b1, b2, b3 = 1.63620, 1.63620, 1.15697 + beta1, beta2, beta3 = 1.57080, 1.57080, 1.57080 + + B matrix: 1.6362035642769 -0.0000000000000 -0.000000000000 + 0.0000000000000 1.6362035642769 -0.000000000000 + 0.0000000000000 0.0000000000000 1.156970955450 + + Reflections + ----------- + energy h k l alpha delta gamma omega chi phi tag + + UB matrix + --------- + none calculated + + Sigma: 0.000000 + Tau: 0.000000 + Wavelength: 1.000000 + Energy: 12.398420 + + +Specifying Sigma and Tau for surface diffraction experiments +------------------------------------------------------------ +Sigma and Tau are used in modes that fix either the beam exit or entry angle with +respect to the crystal surface, or that keep the surface normal in the horizontal +laboratory plane. For non surface-diffraction experiments these can +safely be left at zero. + +For surface diffraction experiments, where not only the crystal's +lattice planes must be oriented appropriately but so must the crystal's +optical surface, two angles _Tau_ and _Sigma_ define the orientation of +the surface with respect to the phi axis. Sigma is (minus) the amount of chi axis +rotation and Tau (minus) the amount of phi axis rotation needed to +move the surface normal parallel to the omega circle +axis. These angles are often determined by reflecting a laser from the +surface of the Crystal onto some thing and moving chi and tau until +the reflected spot remains stationary with movements of omega. + +Use ``sigtau`` with no args to set interactively:: + + >>> pos chi -3.1 + chi: -3.1000 + >>> pos phi 10.0 + phi: 10.0000 + >>> sigtau + sigma, tau = 0.000000, 0.000000 + chi, phi = -3.100000, 10.000000 + sigma[ 3.1]: 3.1 + tau[-10.0]: 10.0 + +Sigma and Tau can also be set explicitly:: + + >>>sigtau 0 0 + + +Managing reflections +-------------------- +The normal way to calculate a UB matrix is to find the position of **two** +reflections with known hkl values. Diffcalc allows many +reflections to be recorded but currently only uses the first two when +calculating a UB matrix. + +Add reflection at current location +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is normal to first move to a reflection:: + + >>> pos en 10 + en: 10.0000 + >>> pos sixc [5.000, 22.790, 0.000, 1.552, 22.400, 14.255] + sixc: alpha: 5.0000 delta: 22.7900 gamma: 0.0000 omega: 1.5520 chi: 22.4000 phi: 14.2550 + + +and then use the ``addref`` command either explicitly:: + + addref 1 0 1.0628 'optional_tag' + +or interactively:: + + >>> addref + h: 1 + k: 0 + l: 1.0628 + current pos[y]: y + tag: 'tag_string' + +to add a reflection. + +Add a reflection manually +~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a reflection cannot be reached but its position is known (or if its +position has been previously determined), a reflection may be added +without first moving to it either explicitly:: + + >>> addref 0 1 1.0628 [5.000, 22.790, 0.000,4.575, 24.275, 101.320] 'optional_tag' + +or interactively:: + + >>> addref + h: 0 + k: 1 + l: 1.0628 + current pos[y]: n + alpha[5.000]: + delta[22.79]: + gamma[0.000]: + omega[1.552]: 4.575 + chi[22.40]: 24.275 + phi[14.25]: 101.320 + en[9.998]: + tag: optional_tag2 + +Edit reflection list +~~~~~~~~~~~~~~~~~~~~ + +Use ``showref`` to show the reflection list:: + + >>> showref + energy h k l alpha delta gamma omega chi phi tag + 1 9.999 1.00 0.00 1.06 5.0000 22.7900 0.0000 1.5520 22.4000 14.2550 1st + 2 9.999 0.00 1.00 1.06 5.0000 22.7900 0.0000 4.5750 24.2750 101.32000 2nd + +Use ``swapref`` to swap reflections:: + + >>> swapref 1 2 + Recalculating UB matrix. + >>> showref + energy h k l alpha delta gamma omega chi phi tag + 1 9.999 0.00 1.00 1.06 5.0000 22.7900 0.0000 4.5750 24.2750 101.3200 2nd + 2 9.999 1.00 0.00 1.06 5.0000 22.7900 0.0000 1.5520 22.4000 14.2550 1st + +Use ``delref`` to delete a reflection:: + + >>> delref 1 + >>> showref + energy h k l alpha delta gamma omega chi phi tag + 1 9.999 1.00 0.00 1.06 5.0000 22.7900 0.0000 1.5520 22.4000 14.2550 1st + +Calculating a UB matrix +----------------------- + +Unless a U or UB matrix has been manually specified, a new UB matrix +will be calculated after the second reflection has been found, or +whenever one of the first two reflections is changed. + +Use the command ``calcub`` to force the UB matrix to be calculated +from the first two reflections. + +If you have misidentified a reflection used for the orientation the +resulting UB matrix will be incorrect. Always use the ``checkub`` +command to check that the computed values agree with the estimated values:: + + >>>checkub + energy h k l h_comp k_comp l_comp tag + 1 9.9987 1.00 0.00 1.06 1.0000 0.0000 1.0628 1st + 2 9.9987 0.00 1.00 1.06 -0.0329 1.0114 1.0400 2nd + +Notice that the first reflection will always match, but that the +second will not match exactly. (The system of equations used to +calculate the U matrix is overdetermined and some information from the +second reflection is thrown away.) + +Manually setting U and UB +------------------------- + +*To help find the initial reflections* it may be useful to set the U +matrix manually---to the identity matrix for example. Use the ``setu`` +command to do this. Once set the diffractometer may be driven to the +ideal location of a reflection and then the actual reflection +sought. Normally this would be done in the default mode, four-circle-bisecting, (see +:ref:`moving-in-hkl-space`). In the following example this has been done +by setting the alpha to 5 and leaving gamma at 0 (it would be normal +to leave alpha at 0):: + + >>> hklmode 1 + 1) fourc bisecting + alpha: 0.0 + gamma: 0.0 + + >>> setalpha 5 + alpha: 0 --> 5.000000 + >>> setu + row1[1 0 0]: + row2[0 1 0]: + row3[0 0 1]: + >>> sim hkl [1,0,1.0628] # Check it all makes sense + sixc would move to: + alpha : 5.00000 deg + delta : 22.79026 deg + gamma : 0.00000 deg + omega : 5.82845 deg + chi : 24.57658 deg + phi : 6.14137 deg + + theta : 70702.991919 + 2theta : 23.303705 + Bin : 6.969151 + Bout : 6.969151 + azimuth : 7.262472 + + >>> pos hkl [1,0,1.0628] + hkl: h: 1.00000 k: 0.00000 l: 1.06280 + + >>> # scan about to find actual reflection + + >>> addref + h[0.0]: 1 + k[0.0]: 0 + l[0.0]: 1.0628 + current pos[y]: y + tag: 'ref1' + >>> + + +There is currently no way to refine a manually specified U matrix by +inferring as much as possible from just one found reflection. + +.. _moving-in-hkl-space: + +Moving in hkl space +=================== + +Once a UB matrix has been calculated, the diffractometer may be driven +in hkl coordinates. A given diffractometer setting maps easily into a +single hkl value. However for a diffractometer with more than three circles +there are excess degrees of freedom when calculating a diffractometer +setting from an hkl value. Diffcalc provides many for using up +the excess degrees of freedom. + +By default Diffcalc selects four-circle bisecting mode (see below). + +Note that to play along with the following ``run`` the file in +``example/session/sixc_example.py`` to configure the UB-calculation. + + +Modes +----- + +Use the command ``hklmode`` to summarise the state of Diffcalc's angle +calculator. It shows a list the available modes for your +diffractometer and the parameters that must be fixed for each, the +current mode and the current parameter settings:: + + >>> hklmode + Available modes: + 0) fourc fixed-bandlw (alpha, gamma, blw) (Not impl.) + 1) fourc bisecting (alpha, gamma) + 2) fourc incoming (alpha, gamma, betain) + 3) fourc outgoing (alpha, gamma, betaout) + 4) fourc azimuth (alpha, gamma, azimuth) (Not impl.) + 5) fourc fixed-phi (alpha, gamma, phi) (Not impl.) + 10) fivec bisecting (gamma) + 11) fivec incoming (gamma, betain) + 12) fivec outgoing (gamma, betaout) + 13) fivec bisecting (alpha) + 14) fivec incoming (alpha, betain) + 15) fivec outgoing (alpha, betaout) + 20) zaxis bisecting () + 21) zaxis incoming (betain) + 22) zaxiz outgoing (betaout) + + Current mode: + + 1) fourc bisecting + Parameters: + + alpha: 0.0 + gamma: 0.0 + betain: --- (not relevant in this mode) + betaout: --- (not relevant in this mode) + azimuth: --- (not relevant in this mode) + phi: --- (not relevant in this mode) + blw: --- (not relevant in this mode) + +Note that 'Not impl.' is short for 'not implemented'. Standby. + +Your output may differ. For example: + + - When listed with a typical five-circle diffractometer with no gamma + circle: the fourc modes will have no gamma parameter to fix + (actually it will have been fixed under the covers to 0), there + will be no gamma or alpha parameters to fix in the five circle + modes (again, under the covers gamma will have been fixed) and + there will be no zaxis modes (as these require six circles, or an + actual z-axis diffractometer). + + - When listed with a typical four-circle diffractometer with no alpha + or gamma circle, the four-circle modes will appear with no alpha or + gamma parameters (again, they are fixed under the covers), and + there will be no five circle or zaxis modes. + +To change the current mode, call ``hklmode`` with an argument:: + + >>> hklmode 2 + 2) fourc incoming + alpha: 0.0 + gamma: 0.0 + betain: --- + +(The dashes next to the betain parameter indicate that a parameter +has not yet been set.) + +Mode parameters +--------------- + +A parameter can be set using either one of the series of {{{set}}} +commands, by moving one of the scannables associated with each +parameter or, where appropriate, by asking that a parameter track an +axis. + +Set commands +~~~~~~~~~~~~ +Use the series of commands ``set`` to set a parameter:: + + >>> setalpha 3 + alpha: 0 --> 3.000000 + >>> setbetain 5 + WARNING: The parameter betain is not used in mode 1 + betain: --- --> 5.000000 + >>> setalpha # With no args, the current value is displayed + alpha: 3 + >>> setbetain + betain: --- + + +Parameter Scannables +~~~~~~~~~~~~~~~~~~~~ + +In most installations there will be a scannable for each parameter. In +this example installation, the parameters which correspond to physical +axes have had '_par' appended to their names to prevent clashes. These +may be used to change a parameter either with the ``pos`` command or +by using them within a scan (see :ref:`scanning-in-hkl-space`).:: + + >>> pos betain + betain: 0.00000 + >>> pos betain 5 + betain: 5.00000 + >>> setbetain + betain: 5 + + >>> pos alpha_par + alpha_par:3.00000 + >>> setalpha + alpha: 3 + + +Tracking Axis +~~~~~~~~~~~~~ +Where a parameter matches an axis name, that parameter may be set to +track that axis:: + + >>> pos alpha + alpha: 5.0000 + + >>> hklmode 1 + 1) fourc bisecting + alpha: 0.0 + gamma: 0.0 + + >>> trackalpha + alpha: 5 + + >>> pos alpha + alpha: 6.0000 + + >>> hklmode 1 + 1) fourc bisecting + alpha: 6.0 (tracking physical axis) + gamma: 0.0 + + +Although convenient, there is a danger with this method that in +geometries where the axes are built from other axes (such as in a +kappa geometry), the position of an axis may drift slightly during a +scan. + +Sectors +------- + +When mapping from reciprocal lattice space to a set of diffractometer +settings, there is normally a choice of solutions for the sample +orientation. The selected sector mode will determine which solution is +used. There is currently only one sector mode: + +Sector mode: Find first solution within sector limits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this sector mode, taken from 'DIF', the first solution found within +the 'sector limits' is chosen. These are different from the physical +or software limits on the axes and can be checked/modified using +``setsectorlim``:: + + >>> setsectorlim + omega_high[270]: + omega_low[-90]: + phi_high[180]: + phi_low[-180]: + + +The hkl scannable +----------------- +Once a UB matrix has been calculated, a mode chosen and parmeters set, +use the hkl scannable to move to a point in reciprocal lattice space:: + + >>> pos hkl [1,0,0] + hkl: h: 1.00000 k: -0.00000 l: -0.00000 + >>> pos sixc + sixc: alpha: 3.0000 delta: 17.2252 gamma: 4.0000 omega: 7.5046 chi: -24.6257 phi: 4.8026 + >>> pos hkl + hkl: h: 1.00000 k: -0.00000 l: -0.00000 + >>> hkl + hkl: + h : 1.000000 + k : -0.000000 + l : -0.000000 + 2theta : 18.582618 + Bin : -0.387976 + Bout : -0.387976 + azimuth : 1.646099 + +Notice that typing ``hkl`` will also display some virtual angles (such +as twotheta and Bin), that checking the position with ``pos hkl`` will +not. + +To get this extra information into a scan use the scannable hklverbose +instead of hkl:: + + >>> pos hklverbose [1,0,0] + hklverbose: h: 1.00000 k: -0.00000 l: -0.00000 2theta : 18.582618 Bin : -0.387976 + Bout :-0.387976 azimuth : 1.646099 + +The ``sim`` command will report, without moving the diffractometer, +where an hkl position would be found:: + + >>> sim hkl [1,0,0] + sixc would move to: + alpha : 3.00000 deg + delta : 17.22516 deg + gamma : 4.00000 deg + omega : 7.50461 deg + chi : -24.62568 deg + phi : 4.80260 deg + + theta : 70702.991919 + 2theta : 18.582618 + Bin : -0.387976 + Bout : -0.387976 + azimuth : 1.646099 + + + +Moving out of range +~~~~~~~~~~~~~~~~~~~ +Not every hkl position can be reached:: + + >>> pos hkl [10,10,10] + Exception: Could not compute delta for this hkl position + +The diffractometer scannable (sixc) +----------------------------------- +We've seen this before, but it also works with sim:: + + gda>>>sim sixc [3, 17.22516, 4, 7.50461, -24.62568, 4.80260] + hkl would move to: + h : 1.000000 + k : 0.000000 + l : -0.000000 + +.. _scanning-in-hkl-space: + +Scanning in hkl space +===================== + +All scans described below use the same generic scanning mechanism +provided by the GDA system or by minigda. Here are some examples. + +Fixed hkl scans +--------------- + +In a 'fixed hkl scan' something (such as energy or Bin) is scanned, +and at each step hkl is 'moved' to keep the sample and detector +aligned. Also plonk the diffractometer scannable (sixc) on there with no +destination to monitor what is actually happening and then +throw on a detector (cnt) with an exposure time if appropriate:: + + >>> #scan scannable_name start stop step [scannable_name [pos or time]].. + + >>> scan en 9 11 .5 hkl [1,0,0] sixc cnt 1 + + >>> scan en 9 11 .5 hklverbose [1,0,0] sixc cnt 1 + + >>> scan betain 4 5 .2 hkl [1,0,0] sixc cnt 1 + + >>> scan alpha_par 0 10 2 hkl [1,0,0] sixc cnt 1 + + >>> trackalpha + >>> scan alpha 0 10 2 hkl [1,0,0] sixc cnt 1 # Equivalent to last scan + +Scanning hkl +------------ + +Hkl, or one component, may also be scanned directly:: + + >>> scan h .8 1.2 .1 hklverbose sixc cnt 1 + +At each step, this will read the current hkl position, modify the h +component and then move to the resulting vector. There is a danger +that with this method k and l may drift. To get around this the start, +stop and step values may also be specified as vectors. So for example:: + + >>> scan hkl [1,0,0] [1,.3,0] [1,0.1,0] cnt1 + +is equivilant to:: + + >>> pos hkl [1,0,0] + >>> scan k 0 .3 .1 cnt1 + +but will not suffer from drifting. This method also allows scans along +any direction in hkl space to be performed. + +Multidimension scans +-------------------- + +Two and three dimensional scans:: + + >>> scan en 9 11 .5 h .9 1.1 .2 hklverbose sixc cnt 1 + >>> scan h 1 3 1 k 1 3 1 l 1 3 1 hkl cnt 1 + + + +Good luck --- RobW diff --git a/script/test/diffcalc (copy)/doc/source/youmanual.rst b/script/test/diffcalc (copy)/doc/source/youmanual.rst new file mode 100644 index 0000000..a6f1b30 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/youmanual.rst @@ -0,0 +1,888 @@ +################################ +Diffcalc User Guide (You Engine) +################################ + +.. rubric:: Diffcalc: A diffraction condition calculator for diffractometer control + +:Author: Rob Walton +:Contact: rob.walton (at) diamond (dot) ac (dot) uk +:Website: https://github.com/DiamondLightSource/diffcalc + +.. toctree:: + :maxdepth: 2 + :numbered: + +See also the `quickstart guide at github `_ + +Introduction +============ + +This manual assumes that you are running Diffcalc within OpenGDA or have started +it using IPython. It assumes that Diffcalc has been configured for the six +circle diffractometer pictured here: + +.. figure:: youmanual_images/4s_2d_diffractometer.png + :scale: 100 + :align: center + + 4s + 2d six-circle diffractometer, from H.You (1999) + +Your Diffcalc configuration may have been customised for the geometry of your +diffractometer and possibly the types of experiment you perform. For example, a +five-circle diffractometer might be missing the nu circle above. + +The laboratory frame is shown above. With all settings at zero as shown the +crystal cartesian frame aligns with the laboratory frame. Therefor a cubic +crystal mounted squarely in a way that the U matrix (defined below) is unitary +will have h||a||x, k||b||y & l||c||z, crystal and reciprocal-lattice coordinate +frames are defined with respect to the beam and to gravity to be (for a cubic +crystal): + +Overview +======== + +The following assumes that the diffractometer has been properly leveled, aligned +with the beam and zeroed. See the `SPEC fourc manual +`__. + +Before moving in hkl space you must calculate a UB matrix by specifying the +crystal's lattice parameters (which define the B matrix) and finding two +reflections (from which the U matrix defining any mismount can be inferred); +and, optionally for surface-diffraction experiments, determine how the surface +of the crystal is oriented with respect to the phi axis. + +Once a UB matrix has been calculated, the diffractometer may be driven in hkl +coordinates. A valid diffractometer setting maps easily into a single hkl value. +However for a diffractometer with more than three circles there are excess +degrees of freedom when calculating a diffractometer setting from an hkl value. +Diffcalc provides modes for using up the excess degrees of freedom. + +Diffcalc does not perform scans directly. Instead, Scannables that use diffcalc +to map between reciprocal lattice space and real diffractometer settings are +scanned using the Gda's (or minigda's) generic scan mechanism. + + +Theory +------ + +Thanks to Elias Vlieg for sharing his dos based ``DIF`` software that Diffcalc +has borrowed heavily from. The version of Diffcalc described here is based on papers by +pHH. You. [You1999]_ and Busing & Levy [Busing1967]_. (See also the THANKS.txt file.) + +Getting Help +============ + +There are few commands to remember. If a command is called without +arguments in some cases Diffcalc will prompt for arguments and provide sensible +defaults which can be chosen by pressing enter. + + +**Orientation**. The ``helpub`` command lists all commands related with crystal +orientation and the reference vector (often used with surfaces). See the +`Orientation Commands`_ section at the end of this manual:: + + >>> help ub + ... + + +**HKL movement**. The ``help hkl`` list all commands related to moving in reciprocal-lattice +space. See the `Motion Commands`_ section at the end of this manual:: + + >>> help hkl + ... + + +Call help on any command. e.g.:: + + >>> help loadub + loadub (diffcalc command): + loadub 'name' | num -- load an existing ub calculation + +Diffcalc's Scannables +===================== + +To list and show the current positions of your beamline's scannables +use ``pos`` with no arguments:: + + >>> pos + +Results in: + +**Energy and wavelength scannables**:: + + energy 12.3984 + wl: 1.0000 + +**Diffractometer scannables**, as a group and in component axes (in +the real GDA these have limits):: + + sixc: mu: 0.0000 delta: 0.0000 gamma: 0.0000 omega: 0.0000 chi: 0.0000 phi: 0.0000 + mu: 0.0000 + chi: 0.0000 + delta: 0.0000 + gamma: 0.0000 + omega: 0.0000 + phi: 0.0000 + +**Dummy counter**, which in this example simply counts at 1hit/s:: + + ct: 0.0000 + +**Hkl scannable**, as a group and in component:: + + hkl: Error: No UB matrix + h: Error: No UB matrix + k: Error: No UB matrix + l: Error: No UB matrix + +**Parameter scannables**, used in some modes, these provide a +scannable alternative to the `Motion`_ section. Some constrain of +these constrain virtual angles:: + + alpha: --- + beta: --- + naz: --- + psi: --- + qaz: --- + +and some constrain physical angles:: + + phi_con: --- + chi_con: --- + delta_con:--- + eta_con: --- + gam_con: --- + mu_con: --- + + +Crystal orientation +=================== + +Before moving in hkl space you must calculate a UB matrix by specifying the +crystal's lattice parameters (which define the B matrix) and finding two +reflections (from which the U matrix can be inferred); and, optionally for +surface-diffraction experiments, determine how the surface of the crystal is +oriented with respect to the phi axis. + +Start a new UB calculation +-------------------------- + +A *UB calculation* contains the description of the crystal-under-test, +any saved reflections, reference angle direction, and a B & UB +matrix pair if they have been calculated or manually specified. +Starting a new UB calculation will clear all of these. + +Before starting a UB-calculation, the ``ub`` command used to summarise +the state of the current UB-calculation, will reflect that no +UB-calculation has been started:: + + >>> ub + <<< No UB calculation started >>> + +A new UB-calculation calculation may be started and lattice specified +explicitly:: + + >>> newub 'example' + >>> setlat '1Acube' 1 1 1 90 90 90 + +or interactively:: + + >>> newub + calculation name: example + crystal name: 1Acube + a [1]: 1 + b [1]: 1 + c [1]: 1 + alpha [90]: 90 + beta [90]: 90 + gamma [90]: 90 + +where a,b and c are the lengths of the three unit cell basis vectors +in Angstroms, and alpha, beta and gamma are angles in Degrees. + +The ``ub`` command will show the state of the current UB-calculation +(and the current energy for reference):: + + >>> ub + UBCALC + + name: example + + n_phi: 0.00000 0.00000 1.00000 <- set + + CRYSTAL + + name: 1Acube + + a, b, c: 1.00000 1.00000 1.00000 + 90.00000 90.00000 90.00000 + + B matrix: 6.28319 0.00000 0.00000 + 0.00000 6.28319 0.00000 + 0.00000 0.00000 6.28319 + + UB MATRIX + + <<< none calculated >>> + + REFLECTIONS + + <<< none specified >>> + + CRYSTAL ORIENTATIONS + + <<< none specified >>> + +Load a UB calculation +--------------------- + +To load the last used UB-calculation:: + + >>> lastub + Loading ub calculation: 'mono-Si' + +To load a previous UB-calculation:: + + >>> listub + UB calculations in: /Users/walton/.diffcalc/i16 + + 0) mono-Si 15 Feb 2017 (22:32) + 1) i16-32 13 Feb 2017 (18:32) + + >>> loadub 0 + +Generate a U matrix from two reflections +---------------------------------------- + +The normal way to calculate a U matrix is to find the position of **two** +reflections with known hkl values. Diffcalc allows many reflections to be +recorded but currently only uses the first two when calculating a UB matrix. + +Find U matrix from two reflections:: + + >>> pos wl 1 + wl: 1.0000 + >>> c2th [0 0 1] + 59.99999999999999 + + >>> pos sixc [0 60 0 30 90 0] + sixc: mu: 0.0000 delta: 60.0000 gam: 0.0000 eta: 30.0000 chi: 90.0000 phi: 0.0000 + >>> addref [0 0 1] + + >>> pos sixc [0 90 0 45 45 90] + sixc: mu: 0.0000 delta: 90.0000 gam: 0.0000 eta: 45.0000 chi: 45.0000 phi: 90.0000 + >>> addref [0 1 1] + Calculating UB matrix. + +Check that it looks good:: + + >>> checkub + + ENERGY H K L H_COMP K_COMP L_COMP TAG + 1 12.3984 0.00 0.00 1.00 0.0000 0.0000 1.0000 + 2 12.3984 0.00 1.00 1.00 0.0000 1.0000 1.0000 + +Generate a U matrix from one reflection +--------------------------------------- + +To estimate based on first reflection only:: + + >>> trialub + resulting U angle: 0.00000 deg + resulting U axis direction: [-1.00000, 0.00000, 0.00000] + Recalculating UB matrix from the first reflection only. + NOTE: A new UB matrix will not be automatically calculated when the orientation reflections are modified. + +Edit reflection list +-------------------- + +Use ``showref`` to show the reflection list:: + + >>> showref + ENERGY H K L MU DELTA GAM ETA CHI PHI TAG + 1 12.398 0.00 0.00 1.00 0.0000 60.0000 0.0000 30.0000 90.0000 0.0000 + 2 12.398 0.00 1.00 1.00 0.0000 90.0000 0.0000 45.0000 45.0000 90.0000 + +Use ``swapref`` to swap reflections:: + + >>> swapref 1 2 + Not calculating UB matrix as it has been manually set. Use 'calcub' to explicitly recalculate it. + Recalculating UB matrix. + +Use ``delref`` to delete a reflection:: + + >>> delref 1 + +Generate a U matrix from two lattice directions +----------------------------------------------- + +Another approach to calculate a U matrix is to provide orientation of **two** crystal lattice +directions in laboratory frame of reference using ``addorient`` command. The first lattice +direction will be aligned along the specified in the laboratory frame. The second lattice +direction will be used to set azimuthal orientation of the crystal in the plane perpendicular +to the first lattice orientation. Diffcalc allows many lattice directions to be recorded but +currently uses only the first two when calculating a UB matrix. + +Find U matrix from two lattice directions:: + + >>> addorient [0 0 1] [0 0 1] + + >>> addorient [1 0 0] [1 1 0] + Calculating UB matrix. + +Calculate a UB matrix +--------------------- + +Unless a U or UB matrix has been manually specified, a new UB matrix will be +calculated after the second reflection has been found, or whenever one of the +first two reflections is changed. + +Use the command ``calcub`` to force the UB matrix to be calculated from the +first two reflections. In case of using lattice orientations instead of reflections, +use command ``orientub`` to force the UB matrix to be calculated from the first two orientations. + +If you have misidentified a reflection used for the orientation the +resulting UB matrix will be incorrect. Always use the ``checkub``command +to check that the computed reflection indices agree with the estimated values:: + + >>> checkub + + ENERGY H K L H_COMP K_COMP L_COMP TAG + 1 12.3984 0.00 1.00 1.00 0.0000 1.0000 1.0000 + 2 12.3984 0.00 0.00 1.00 0.0000 0.0000 1.0000 + +Calculate a U matrix from crystal mismount +------------------------------------------- + +U matrix can be defined from crystal mismount by using a rotation matrix calculated from a provided +mismount angle and axis. ``setmiscut`` command defines new U matrix by setting it to a rotation matrix +calculated from the specified angle and axis parameters. ``addmiscut`` command applies the calculated +rotation matrix to the existing U matrix, i.e. adds extra mismount to the already existing one:: + + >>> setmiscut 5 [1 0 0] + n_phi: -0.00000 -0.08716 0.99619 + n_hkl: 0.00000 0.00000 1.00000 <- set + normal: + angle: 5.00000 + axis: 1.00000 -0.00000 0.00000 + + +Manually specify U matrix +------------------------- + +Set U matrix manually (pretending sample is squarely mounted):: + + >>> setu [[1 0 0] [0 1 0] [0 0 1]] + Recalculating UB matrix. + NOTE: A new UB matrix will not be automatically calculated when the orientation reflections are modified. + +Refining UB matrix from reflection +---------------------------------- + +UB matrix elements can be refined to match diffractometer settings and crystal orientation experimentally +found for a given reflection with the corresponding reflection indices. ``refineub`` command rescales +crystal unit cell dimensions to match with the found scattering angle value and recalculates mismount +parameters to update U matrix:: + + >>> refineub [1 0 0] + current pos[y]: y + Unit cell scaling factor: 0.99699 + Refined crystal lattice: + a, b, c: 0.99699 0.99699 0.99699 + 90.00000 90.00000 90.00000 + + Update crystal settings?[y]: y + Warning: the old UB calculation has been cleared. + Use 'calcub' to recalculate with old reflections or + 'orientub' to recalculate with old orientations. + Miscut parameters: + angle: 2.90000 + axis: -0.00000 1.00000 -0.00000 + Apply miscut parameters?[y]: y + n_phi: 0.67043 -0.00000 0.74198 + n_hkl: 0.00000 0.00000 1.00000 <- set + normal: + angle: 42.10000 + axis: 0.00000 1.00000 0.00000 + +Set the reference vector +------------------------- + +When performing surface experiments the reference vector should be set normal +to the surface. It can also be used to define other directions within the crystal +with which we want to orient the incident or diffracted beam. + +By default the reference vector is set parallel to the phi axis. That is, +along the z-axis of the phi coordinate frame. + +The `ub` command shows the current reference vector along with the orientation relative to +the z-axis, at the top its report (or it can be shown by calling ``setnphi`` or +``setnhkl'`` with no args):: + + >>> ub + ... + n_phi: 0.00000 0.00000 1.00000 <- set + n_hkl: -0.00000 0.00000 1.00000 + normal: None + ... + +The ``<- set`` label here indicates that the reference vector is set in the phi +coordinate frame. In this case, therefore, its direction in the crystal's +reciprocal lattice space is inferred from the UB matrix. + +To set the reference vector in the phi coordinate frame use:: + + >>> setnphi [0 0 1] + ... + +This is useful if the surface normal has be found with a laser or by x-ray +occlusion. This vector must currently be manually calculated from the sample +angle settings required to level the surface (sigma and tau commands on the +way). + +To set the reference vector in the crystal's reciprocal lattice space use (this +is a quick way to determine the surface orientation if the surface is known to +be cleaved cleanly along a known axis):: + + >>> setnhkl [0 0 1] + ... + +Motion +====== + +Once a UB matrix has been calculated, the diffractometer may be driven +in hkl coordinates. A given diffractometer setting maps easily into a +single hkl value. However for a diffractometer with more than three circles +there are excess degrees of freedom when calculating a diffractometer +setting from an hkl value. Diffcalc provides many for using up +the excess degrees of freedom. + +By default Diffcalc selects no mode. + +Constraining solutions for moving in hkl space +---------------------------------------------- + +To get help and see current constraints:: + + >>> help con + ... + + >>> con + DET REF SAMP + ------ ------ ------ + delta a_eq_b mu + gam alpha eta + qaz beta chi + naz psi phi + mu_is_gam + + ! 3 more constraints required + + Type 'help con' for instructions + +Three constraints can be given: zero or one from the DET and REF columns and the +remainder from the SAMP column. Not all combinations are currently available. +Use ``help con`` to see a summary if you run into troubles. + +To configure four-circle vertical scattering:: + + >>> con gam 0 mu 0 a_eq_b + gam : 0.0000 + a_eq_b + mu : 0.0000 + +In the following the *scattering plane* is defined as the plane including the +scattering vector, or momentum transfer vector, and the incident beam. + +**DETECTOR COLUMN:** + +- **delta** - physical delta setting (vertical detector motion) *del=0 is equivalent to qaz=0* +- **gam** - physical gamma setting (horizontal detector motion) *gam=0 is equivalent to qaz=90* +- **qaz** - azimuthal rotation of scattering vector (about the beam, from horizontal) +- **naz** - azimuthal rotation of reference vector (about the beam, from horizontal) + +**REFERENCE COLUMN:** + +- **alpha** - incident angle to surface (if reference is normal to surface) +- **beta** - exit angle from surface (if reference is normal to surface) +- **psi** - azimuthal rotation about scattering vector of reference vector (from scattering plane) +- **a_eq_b** - bisecting mode with alpha=beta. *Equivalent to psi=90* + +**SAMPLE COLUMN:** + +- **mu, eta, chi & phi** - physical settings +- **mu_is_gam** - force mu to follow gamma (results in a 5-circle geometry) + +Diffcalc will report two other (un-constrainable) virtual angles: + +- **theta** - half of 2theta, the angle through the diffracted beam bends +- **tau** - longitude of reference vector from scattering vector (in scattering plane) + +Example constraint modes +------------------------ + +There is sometimes more than one way to get the same effect. + +**Vertical four-circle mode**:: + + >>> con gam 0 mu 0 a_eq_b # or equivalently: + >>> con qaz 90 mu 0 a_eq_b + + >>> con alpha 1 # replaces a_eq_b + +**Horizontal four-circle mode**:: + + >>> con del 0 eta 0 alpha 1 # or equivalently: + >>> con qaz 0 mu 0 alpha 1 + +**Surface vertical mode**:: + + >>> con naz 90 mu 0 alpha 1 + +**Surface horizontal mode**:: + + >>> con naz 0 eta 0 alpha 1 + +**Z-axis mode (surface horizontal)**:: + + >>> con chi (-sigma) phi (-tau) alpha 1 + +where sigma and tau are the offsets required in chi and phi to bring the surface +normal parallel to eta. Alpha will determine mu directly leaving eta to orient +the planes. Or:: + + >>> con naz 0 phi 0 alpha 1 # or any another sample angle + +**Z-axis mode (surface vertical)**:: + + >>> con naz 0 phi 0 alpha 1 # or any another sample angle + +Changing constrained values +--------------------------- + +Once constraints are chosen constrained values may be changed directly:: + + >>> con mu 10 + gam : 0.0000 + a_eq_b + mu : 10.0000 + +or via the associated scannable:: + + >>> pos mu_con 10 + mu_con: 10.00000 + +Configuring limits and cuts +--------------------------- + +Diffcalc maintains its own limits on axes. These limits will be used when +choosing solutions. If more than one detector solution is exists Diffcalc will +ask you to reduce the the limits until there is only one. However if more than +one solution for the sample settings is available it will choose one base on +heuristics. + +Use the ``hardware`` command to see the current limits and cuts:: + + >>> hardware + mu (cut: -180.0) + delta (cut: -180.0) + gam (cut: -180.0) + eta (cut: -180.0) + chi (cut: -180.0) + phi (cut: 0.0) + Note: When auto sector/transforms are used, + cuts are applied before checking limits. + +To set the limits:: + + >>> setmin delta -1 + >>> setmax delta 145 + +To set a cut:: + + >>> setcut phi -180 + +This causes requests to move phi to be between the configured -180 and +360 +degress above this. i.e. it might dive to -10 degrees rather than 350. + + +Moving in hkl space +------------------- + +Configure a mode, e.g. four-circle vertical:: + + >>> con gam 0 mu 0 a_eq_b + gam : 0.0000 + a_eq_b + mu : 0.0000 + +Simulate moving to a reflection:: + + >>> sim hkl [0 1 1] + sixc would move to: + mu : 0.0000 + delta : 90.0000 + gam : 0.0000 + eta : 45.0000 + chi : 45.0000 + phi : 90.0000 + + alpha : 30.0000 + beta : 30.0000 + naz : 35.2644 + psi : 90.0000 + qaz : 90.0000 + tau : 45.0000 + theta : 45.0000 + +Move to reflection:: + + >>> pos hkl [0 1 1] + hkl: h: 0.00000 k: 1.00000 l: 1.00000 + + >>> pos sixc + sixc: mu: 0.0000 delta: 90.0000 gam: 0.0000 eta: 45.0000 chi: 45.0000 phi: 90.0000 + +Simulate moving to a location:: + + >>> pos sixc [0 60 0 30 90 0] + sixc: mu: 0.0000 delta: 60.0000 gam: 0.0000 eta: 30.0000 chi: 90.0000 phi: 0.0000 + +Scanning in hkl space +===================== + +All scans described below use the same generic scanning mechanism +provided by the GDA system or by minigda. Here are some examples. + +Fixed hkl scans +--------------- + +In a 'fixed hkl scan' something (such as energy or Bin) is scanned, +and at each step hkl is 'moved' to keep the sample and detector +aligned. Also plonk the diffractometer scannable (sixc) on there with no +destination to monitor what is actually happening and then +throw on a detector (ct) with an exposure time if appropriate:: + + >>> #scan scannable_name start stop step [scannable_name [pos or time]].. + + >>> scan en 9 11 .5 hkl [1 0 0] sixc ct 1 + + >>> scan en 9 11 .5 hklverbose [1 0 0] sixc ct 1 + + >>> scan betain 4 5 .2 hkl [1 0 0] sixc ct 1 + + >>> scan alpha_par 0 10 2 hkl [1 0 0] sixc ct 1 + +Scanning hkl +------------ + +Hkl, or one component, may also be scanned directly:: + + >>> scan h .8 1.2 .1 hklverbose sixc ct 1 + +At each step, this will read the current hkl position, modify the h +component and then move to the resulting vector. There is a danger +that with this method k and l may drift. To get around this the start, +stop and step values may also be specified as vectors. So for example:: + + >>> scan hkl [1 0 0] [1 .3 0] [1 0.1 0] ct1 + +is equivilant to:: + + >>> pos hkl [1 0 0] + >>> scan k 0 .3 .1 ct1 + +but will not suffer from drifting. This method also allows scans along +any direction in hkl space to be performed. + +Multidimension scans +-------------------- + +Two and three dimensional scans:: + + >>> scan en 9 11 .5 h .9 1.1 .2 hklverbose sixc ct 1 + >>> scan h 1 3 1 k 1 3 1 l 1 3 1 hkl ct 1 + +Commands +======== + +Orientation Commands +-------------------- + ++-----------------------------+---------------------------------------------------+ +| **STATE** | ++-----------------------------+---------------------------------------------------+ +| **-- newub** {'name'} | start a new ub calculation name | ++-----------------------------+---------------------------------------------------+ +| **-- loadub** 'name' | num | load an existing ub calculation | ++-----------------------------+---------------------------------------------------+ +| **-- lastub** | load the last used ub calculation | ++-----------------------------+---------------------------------------------------+ +| **-- listub** | list the ub calculations available to load | ++-----------------------------+---------------------------------------------------+ +| **-- rmub** 'name'|num | remove existing ub calculation | ++-----------------------------+---------------------------------------------------+ +| **-- saveubas** 'name' | save the ub calculation with a new name | ++-----------------------------+---------------------------------------------------+ +| **LATTICE** | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** | interactively enter lattice parameters (Angstroms | +| | and Deg) | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a | assumes cubic | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b | assumes tetragonal | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b c | assumes ortho | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b c | assumes mon/hex with gam not equal to 90 | +| gamma | | ++-----------------------------+---------------------------------------------------+ +| **-- setlat** name a b c | arbitrary | +| alpha beta gamma | | ++-----------------------------+---------------------------------------------------+ +| **-- c2th** [h k l] | calculate two-theta angle for reflection | ++-----------------------------+---------------------------------------------------+ +| **-- hklangle** [h1 k1 l1] | calculate angle between [h1 k1 l1] and [h2 k2 l2] | +| [h2 k2 l2] | crystal planes | ++-----------------------------+---------------------------------------------------+ +| **REFERENCE (SURFACE)** | ++-----------------------------+---------------------------------------------------+ +| **-- setnphi** {[x y z]} | sets or displays n_phi reference | ++-----------------------------+---------------------------------------------------+ +| **-- setnhkl** {[h k l]} | sets or displays n_hkl reference | ++-----------------------------+---------------------------------------------------+ +| **REFLECTIONS** | ++-----------------------------+---------------------------------------------------+ +| **-- showref** | shows full reflection list | ++-----------------------------+---------------------------------------------------+ +| **-- addref** | add reflection interactively | ++-----------------------------+---------------------------------------------------+ +| **-- addref** [h k l] | add reflection with current position and energy | +| {'tag'} | | ++-----------------------------+---------------------------------------------------+ +| **-- addref** [h k l] (p1, | add arbitrary reflection | +| .., pN) energy {'tag'} | | ++-----------------------------+---------------------------------------------------+ +| **-- editref** num | interactively edit a reflection | ++-----------------------------+---------------------------------------------------+ +| **-- delref** num | deletes a reflection (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **-- clearref** | deletes all the reflections | ++-----------------------------+---------------------------------------------------+ +| **-- swapref** | swaps first two reflections used for calculating | +| | U matrix | ++-----------------------------+---------------------------------------------------+ +| **-- swapref** num1 num2 | swaps two reflections (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **CRYSTAL ORIENTATIONS** | ++-----------------------------+---------------------------------------------------+ +| **-- showorient** | shows full list of crystal orientations | ++-----------------------------+---------------------------------------------------+ +| **-- addorient** | add crystal orientation interactively | ++-----------------------------+---------------------------------------------------+ +| **-- addorient** [h k l] | add crystal orientation in laboratory frame | +| [x y z] {'tag'} | | ++-----------------------------+---------------------------------------------------+ +| **-- editorient** num | interactively edit a crystal orientation | ++-----------------------------+---------------------------------------------------+ +| **-- delorient** num | deletes a crystal orientation (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **-- clearorient** | deletes all the crystal orientations | ++-----------------------------+---------------------------------------------------+ +| **-- swaporient** | swaps first two crystal orientations used for | +| | calculating U matrix | ++-----------------------------+---------------------------------------------------+ +| **-- swaporient** num1 num2 | swaps two crystal orientations (numbered from 1) | ++-----------------------------+---------------------------------------------------+ +| **UB MATRIX** | ++-----------------------------+---------------------------------------------------+ +| **-- checkub** | show calculated and entered hkl values for | +| | reflections | ++-----------------------------+---------------------------------------------------+ +| **-- setu** | manually set u matrix | +| {[[..][..][..]]} | | ++-----------------------------+---------------------------------------------------+ +| **-- setub** | manually set ub matrix | +| {[[..][..][..]]} | | ++-----------------------------+---------------------------------------------------+ +| **-- calcub** | (re)calculate u matrix from ref1 and ref2 | ++-----------------------------+---------------------------------------------------+ +| **-- trialub** | (re)calculate u matrix from ref1 only (check | +| | carefully) | ++-----------------------------+---------------------------------------------------+ +| **-- refineub** {[h k l]} | refine unit cell dimensions and U matrix to match | +| {pos} | diffractometer angles for a given hkl value | ++-----------------------------+---------------------------------------------------+ +| **-- addmiscut** angle | apply miscut to U matrix using a specified miscut | +| {[x y z]} | angle in degrees and a rotation axis | +| | (default: [0 1 0]) | ++-----------------------------+---------------------------------------------------+ +| **-- setmiscut** angle | manually set U matrix using a specified miscut | +| {[x y z]} | angle in degrees and a rotation axis | +| | (default: [0 1 0]) | ++-----------------------------+---------------------------------------------------+ + +Motion commands +--------------- + ++-----------------------------+---------------------------------------------------+ +| **CONSTRAINTS** | ++-----------------------------+---------------------------------------------------+ +| **-- con** | list available constraints and values | ++-----------------------------+---------------------------------------------------+ +| **-- con** {val} | constrains and optionally sets one constraint | ++-----------------------------+---------------------------------------------------+ +| **-- con** {val} | clears and then fully constrains | +| {val} {val} | | ++-----------------------------+---------------------------------------------------+ +| **-- uncon** | remove constraint | ++-----------------------------+---------------------------------------------------+ +| **HKL** | ++-----------------------------+---------------------------------------------------+ +| **-- allhkl** [h k l] | print all hkl solutions ignoring limits | ++-----------------------------+---------------------------------------------------+ +| **HARDWARE** | ++-----------------------------+---------------------------------------------------+ +| **-- hardware** | show diffcalc limits and cuts | ++-----------------------------+---------------------------------------------------+ +| **-- setcut** {name {val}} | sets cut angle | ++-----------------------------+---------------------------------------------------+ +| **-- setmin** {axis {val}} | set lower limits used by auto sector code (None | +| | to clear) | ++-----------------------------+---------------------------------------------------+ +| **-- setmax** {name {val}} | sets upper limits used by auto sector code (None | +| | to clear) | ++-----------------------------+---------------------------------------------------+ +| **MOTION** | ++-----------------------------+---------------------------------------------------+ +| **-- sim** hkl scn | simulates moving scannable (not all) | ++-----------------------------+---------------------------------------------------+ +| **-- sixc** | show Eularian position | ++-----------------------------+---------------------------------------------------+ +| **-- pos** sixc [mu, delta, | move to Eularian position(None holds an axis | +| gam, eta, chi, phi] | still) | ++-----------------------------+---------------------------------------------------+ +| **-- sim** sixc [mu, delta, | simulate move to Eulerian positionsixc | +| gam, eta, chi, phi] | | ++-----------------------------+---------------------------------------------------+ +| **-- hkl** | show hkl position | ++-----------------------------+---------------------------------------------------+ +| **-- pos** hkl [h k l] | move to hkl position | ++-----------------------------+---------------------------------------------------+ +| **-- pos** {h | k | l} val | move h, k or l to val | ++-----------------------------+---------------------------------------------------+ +| **-- sim** hkl [h k l] | simulate move to hkl position | ++-----------------------------+---------------------------------------------------+ + +Good luck --- RobW + +References +========== + +.. [You1999] H. You. *Angle calculations for a '4S+2D' six-circle diffractometer.* + J. Appl. Cryst. (1999). **32**, 614-623. `(pdf link) + `__. +.. [Busing1967] W. R. Busing and H. A. Levy. *Angle calculations for 3- and 4-circle X-ray + and neutron diffractometers.* Acta Cryst. (1967). **22**, 457-464. `(pdf link) + `__. diff --git a/script/test/diffcalc (copy)/doc/source/youmanual_images/4s_2d_diffractometer.png b/script/test/diffcalc (copy)/doc/source/youmanual_images/4s_2d_diffractometer.png new file mode 100644 index 0000000000000000000000000000000000000000..5216255904d76d5ef67b1516b1fc1346caec1b50 GIT binary patch literal 336954 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%0As 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

CRPU80nw?5;lxxO<(Fa61o*w1*gi@hf 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 literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/doc/source/youmanual_template.rst b/script/test/diffcalc (copy)/doc/source/youmanual_template.rst new file mode 100644 index 0000000..1b32fff --- /dev/null +++ b/script/test/diffcalc (copy)/doc/source/youmanual_template.rst @@ -0,0 +1,565 @@ +################################ +Diffcalc User Guide (You Engine) +################################ + +.. rubric:: Diffcalc: A diffraction condition calculator for diffractometer control + +:Author: Rob Walton +:Contact: rob.walton (at) diamond (dot) ac (dot) uk +:Website: https://github.com/DiamondLightSource/diffcalc + +.. toctree:: + :maxdepth: 2 + :numbered: + +See also the `quickstart guide at github `_ + +Introduction +============ + +This manual assumes that you are running Diffcalc within OpenGDA or have started +it using IPython. It assumes that Diffcalc has been configured for the six +circle diffractometer pictured here: + +.. figure:: youmanual_images/4s_2d_diffractometer.png + :scale: 100 + :align: center + + 4s + 2d six-circle diffractometer, from H.You (1999) + +Your Diffcalc configuration may have been customised for the geometry of your +diffractometer and possibly the types of experiment you perform. For example, a +five-circle diffractometer might be missing the nu circle above. + +The laboratory frame is shown above. With all settings at zero as shown the +crystal cartesian frame aligns with the laboratory frame. Therefor a cubic +crystal mounted squarely in a way that the U matrix (defined below) is unitary +will have h||a||x, k||b||y & l||c||z, crystal and reciprocal-lattice coordinate +frames are defined with respect to the beam and to gravity to be (for a cubic +crystal): + +Overview +======== + +The following assumes that the diffractometer has been properly leveled, aligned +with the beam and zeroed. See the `SPEC fourc manual +`__. + +Before moving in hkl space you must calculate a UB matrix by specifying the +crystal's lattice parameters (which define the B matrix) and finding two +reflections (from which the U matrix defining any mismount can be inferred); +and, optionally for surface-diffraction experiments, determine how the surface +of the crystal is oriented with respect to the phi axis. + +Once a UB matrix has been calculated, the diffractometer may be driven in hkl +coordinates. A valid diffractometer setting maps easily into a single hkl value. +However for a diffractometer with more than three circles there are excess +degrees of freedom when calculating a diffractometer setting from an hkl value. +Diffcalc provides modes for using up the excess degrees of freedom. + +Diffcalc does not perform scans directly. Instead, Scannables that use diffcalc +to map between reciprocal lattice space and real diffractometer settings are +scanned using the Gda's (or minigda's) generic scan mechanism. + + +Theory +------ + +Thanks to Elias Vlieg for sharing his dos based ``DIF`` software that Diffcalc +has borrowed heavily from. The version of Diffcalc described here is based on papers by +pHH. You. [You1999]_ and Busing & Levy [Busing1967]_. (See also the THANKS.txt file.) + +Getting Help +============ + +There are few commands to remember. If a command is called without +arguments in some cases Diffcalc will prompt for arguments and provide sensible +defaults which can be chosen by pressing enter. + + +**Orientation**. The ``helpub`` command lists all commands related with crystal +orientation and the reference vector (often used with surfaces). See the +`Orientation Commands`_ section at the end of this manual:: + + >>> help ub + ... + + +**HKL movement**. The ``help hkl`` list all commands related to moving in reciprocal-lattice +space. See the `Motion Commands`_ section at the end of this manual:: + + >>> help hkl + ... + + +Call help on any command. e.g.:: + + ==> help loadub + +Diffcalc's Scannables +===================== + +To list and show the current positions of your beamline's scannables +use ``pos`` with no arguments:: + + >>> pos + +Results in: + +**Energy and wavelength scannables**:: + + energy 12.3984 + wl: 1.0000 + +**Diffractometer scannables**, as a group and in component axes (in +the real GDA these have limits):: + + sixc: mu: 0.0000 delta: 0.0000 gamma: 0.0000 omega: 0.0000 chi: 0.0000 phi: 0.0000 + mu: 0.0000 + chi: 0.0000 + delta: 0.0000 + gamma: 0.0000 + omega: 0.0000 + phi: 0.0000 + +**Dummy counter**, which in this example simply counts at 1hit/s:: + + ct: 0.0000 + +**Hkl scannable**, as a group and in component:: + + hkl: Error: No UB matrix + h: Error: No UB matrix + k: Error: No UB matrix + l: Error: No UB matrix + +**Parameter scannables**, used in some modes, these provide a +scannable alternative to the `Motion`_ section. Some constrain of +these constrain virtual angles:: + + alpha: --- + beta: --- + naz: --- + psi: --- + qaz: --- + +and some constrain physical angles:: + + phi_con: --- + chi_con: --- + delta_con:--- + eta_con: --- + gam_con: --- + mu_con: --- + + +Crystal orientation +=================== + +Before moving in hkl space you must calculate a UB matrix by specifying the +crystal's lattice parameters (which define the B matrix) and finding two +reflections (from which the U matrix can be inferred); and, optionally for +surface-diffraction experiments, determine how the surface of the crystal is +oriented with respect to the phi axis. + +Start a new UB calculation +-------------------------- + +A *UB calculation* contains the description of the crystal-under-test, +any saved reflections, reference angle direction, and a B & UB +matrix pair if they have been calculated or manually specified. +Starting a new UB calculation will clear all of these. + +Before starting a UB-calculation, the ``ub`` command used to summarise +the state of the current UB-calculation, will reflect that no +UB-calculation has been started:: + + ==> ub + +A new UB-calculation calculation may be started and lattice specified +explicitly:: + + ==> newub 'example' + ==> setlat '1Acube' 1 1 1 90 90 90 + +or interactively:: + + >>> newub + calculation name: example + crystal name: 1Acube + a [1]: 1 + b [1]: 1 + c [1]: 1 + alpha [90]: 90 + beta [90]: 90 + gamma [90]: 90 + +where a,b and c are the lengths of the three unit cell basis vectors +in Angstroms, and alpha, beta and gamma are angles in Degrees. + +The ``ub`` command will show the state of the current UB-calculation +(and the current energy for reference):: + + ==> ub + +Load a UB calculation +--------------------- + +To load the last used UB-calculation:: + + >>> lastub + Loading ub calculation: 'mono-Si' + +To load a previous UB-calculation:: + + >>> listub + UB calculations in: /Users/walton/.diffcalc/i16 + + 0) mono-Si 15 Feb 2017 (22:32) + 1) i16-32 13 Feb 2017 (18:32) + + >>> loadub 0 + +Generate a U matrix from two reflections +---------------------------------------- + +The normal way to calculate a U matrix is to find the position of **two** +reflections with known hkl values. Diffcalc allows many reflections to be +recorded but currently only uses the first two when calculating a UB matrix. + +Find U matrix from two reflections:: + + ==> pos wl 1 + ==> c2th [0 0 1] + 59.99999999999999 + + ==> pos sixc [0 60 0 30 90 0] + ==> addref [0 0 1] + + ==> pos sixc [0 90 0 45 45 90] + ==> addref [0 1 1] + +Check that it looks good:: + + ==> checkub + +Generate a U matrix from one reflection +--------------------------------------- + +To estimate based on first reflection only:: + + ==> trialub + +Manually specify U matrix +------------------------- + +Set U matrix manually (pretending sample is squarely mounted):: + + ==> setu [[1 0 0] [0 1 0] [0 0 1]] + +Edit reflection list +-------------------- + +Use ``showref`` to show the reflection list:: + + ==> showref + +Use ``swapref`` to swap reflections:: + + ==> swapref 1 2 + Recalculating UB matrix. + +Use ``delref`` to delete a reflection:: + + >>> delref 1 + +Calculate a UB matrix +--------------------- + +Unless a U or UB matrix has been manually specified, a new UB matrix will be +calculated after the second reflection has been found, or whenever one of the +first two reflections is changed. + +Use the command ``calcub`` to force the UB matrix to be calculated from the +first two reflections. + +If you have misidentified a reflection used for the orientation the +resulting UB matrix will be incorrect. Always use the ``checkub`` +command to check that the computed values agree with the estimated values:: + + ==> checkub + +Set the reference vector +------------------------- + +When performing surface experiments the reference vector should be set normal +to the surface. It can also be used to define other directions within the crystal +with which we want to orient the incident or diffracted beam. + +By default the reference vector is set parallel to the phi axis. That is, +along the z-axis of the phi coordinate frame. + +The `ub` command shows the current reference vector, along with any inferred +miscut, at the top its report (or it can be shown by calling ``setnphi`` or +``setnhkl'`` with no args):: + + >>> ub + ... + n_phi: 0.00000 0.00000 1.00000 <- set + n_hkl: -0.00000 0.00000 1.00000 + miscut: None + ... + +The ``<- set`` label here indicates that the reference vector is set in the phi +coordinate frame. In this case, therefor, its direction in the crystal's +reciprocal lattice space is inferred from the UB matrix. + +To set the reference vector in the phi coordinate frame use:: + + >>> setnphi [0 0 1] + ... + +This is useful if the surface normal has be found with a laser or by x-ray +occlusion. This vector must currently be manually calculated from the sample +angle settings required to level the surface (sigma and tau commands on the +way). + +To set the reference vector in the crystal's reciprocal lattice space use (this +is a quick way to determine the surface orientation if the surface is known to +be cleaved cleanly along a known axis):: + + >>> setnhkl [0 0 1] ... + +Motion +====== + +Once a UB matrix has been calculated, the diffractometer may be driven +in hkl coordinates. A given diffractometer setting maps easily into a +single hkl value. However for a diffractometer with more than three circles +there are excess degrees of freedom when calculating a diffractometer +setting from an hkl value. Diffcalc provides many for using up +the excess degrees of freedom. + +By default Diffcalc selects no mode. + +Constraining solutions for moving in hkl space +---------------------------------------------- + +To get help and see current constraints:: + + >>> help con + ... + + ==> con + +Three constraints can be given: zero or one from the DET and REF columns and the +remainder from the SAMP column. Not all combinations are currently available. +Use ``help con`` to see a summary if you run into troubles. + +To configure four-circle vertical scattering:: + + ==> con gam 0 mu 0 a_eq_b + +In the following the *scattering plane* is defined as the plane including the +scattering vector, or momentum transfer vector, and the incident beam. + +**DETECTOR COLUMN:** + +- **delta** - physical delta setting (vertical detector motion) *del=0 is equivalent to qaz=0* +- **gam** - physical gamma setting (horizontal detector motion) *gam=0 is equivalent to qaz=90* +- **qaz** - azimuthal rotation of scattering vector (about the beam, from horizontal) +- **naz** - azimuthal rotation of reference vector (about the beam, from horizontal) + +**REFERENCE COLUMN:** + +- **alpha** - incident angle to surface (if reference is normal to surface) +- **beta** - exit angle from surface (if reference is normal to surface) +- **psi** - azimuthal rotation about scattering vector of reference vector (from scattering plane) +- **a_eq_b** - bisecting mode with alpha=beta. *Equivalent to psi=90* + +**SAMPLE COLUMN:** + +- **mu, eta, chi & phi** - physical settings +- **mu_is_gam** - force mu to follow gamma (results in a 5-circle geometry) + +Diffcalc will report two other (un-constrainable) virtual angles: + +- **theta** - half of 2theta, the angle through the diffracted beam bends +- **tau** - longitude of reference vector from scattering vector (in scattering plane) + +Example constraint modes +------------------------ + +There is sometimes more than one way to get the same effect. + +**Vertical four-circle mode**:: + + >>> con gam 0 mu 0 a_eq_b # or equivalently: + >>> con qaz 90 mu 0 a_eq_b + + >>> con alpha 1 # replaces a_eq_b + +**Horizontal four-circle mode**:: + + >>> con del 0 eta 0 alpha 1 # or equivalently: + >>> con qaz 0 mu 0 alpha 1 + +**Surface vertical mode**:: + + >>> con naz 90 mu 0 alpha 1 + +**Surface horizontal mode**:: + + >>> con naz 0 eta 0 alpha 1 + +**Z-axis mode (surface horizontal)**:: + + >>> con chi (-sigma) phi (-tau) alpha 1 + +where sigma and tau are the offsets required in chi and phi to bring the surface +normal parallel to eta. Alpha will determine mu directly leaving eta to orient +the planes. Or:: + + >>> con naz 0 phi 0 alpha 1 # or any another sample angle + +**Z-axis mode (surface vertical)**:: + + >>> con naz 0 phi 0 alpha 1 # or any another sample angle + +Changing constrained values +--------------------------- + +Once constraints are chosen constrained values may be changed directly:: + + ==> con mu 10 + +or via the associated scannable:: + + ==> pos mu_con 10 + +Configuring limits and cuts +--------------------------- + +Diffcalc maintains its own limits on axes. These limits will be used when +choosing solutions. If more than one detector solution is exists Diffcalc will +ask you to reduce the the limits until there is only one. However if more than +one solution for the sample settings is available it will choose one base on +heuristics. + +Use the ``hardware`` command to see the current limits and cuts:: + + ==> hardware + +To set the limits:: + + ==> setmin delta -1 + ==> setmax delta 145 + +To set a cut:: + + ==> setcut phi -180 + +This causes requests to move phi to be between the configured -180 and +360 +degress above this. i.e. it might dive to -10 degrees rather than 350. + + +Moving in hkl space +------------------- + +Configure a mode, e.g. four-circle vertical:: + + ==> con gam 0 mu 0 a_eq_b + +Simulate moving to a reflection:: + + ==> sim hkl [0 1 1] + +Move to reflection:: + + ==> pos hkl [0 1 1] + + ==> pos sixc + +Simulate moving to a location:: + + ==> pos sixc [0 60 0 30 90 0] + +Scanning in hkl space +===================== + +All scans described below use the same generic scanning mechanism +provided by the GDA system or by minigda. Here are some examples. + +Fixed hkl scans +--------------- + +In a 'fixed hkl scan' something (such as energy or Bin) is scanned, +and at each step hkl is 'moved' to keep the sample and detector +aligned. Also plonk the diffractometer scannable (sixc) on there with no +destination to monitor what is actually happening and then +throw on a detector (ct) with an exposure time if appropriate:: + + >>> #scan scannable_name start stop step [scannable_name [pos or time]].. + + >>> scan en 9 11 .5 hkl [1 0 0] sixc ct 1 + + >>> scan en 9 11 .5 hklverbose [1 0 0] sixc ct 1 + + >>> scan betain 4 5 .2 hkl [1 0 0] sixc ct 1 + + >>> scan alpha_par 0 10 2 hkl [1 0 0] sixc ct 1 + +Scanning hkl +------------ + +Hkl, or one component, may also be scanned directly:: + + >>> scan h .8 1.2 .1 hklverbose sixc ct 1 + +At each step, this will read the current hkl position, modify the h +component and then move to the resulting vector. There is a danger +that with this method k and l may drift. To get around this the start, +stop and step values may also be specified as vectors. So for example:: + + >>> scan hkl [1 0 0] [1 .3 0] [1 0.1 0] ct1 + +is equivilant to:: + + >>> pos hkl [1 0 0] + >>> scan k 0 .3 .1 ct1 + +but will not suffer from drifting. This method also allows scans along +any direction in hkl space to be performed. + +Multidimension scans +-------------------- + +Two and three dimensional scans:: + + >>> scan en 9 11 .5 h .9 1.1 .2 hklverbose sixc ct 1 + >>> scan h 1 3 1 k 1 3 1 l 1 3 1 hkl ct 1 + +Commands +======== + +Orientation Commands +-------------------- + +==> UB_HELP_TABLE + +Motion commands +--------------- + +==> HKL_HELP_TABLE + +Good luck --- RobW + +References +========== + +.. [You1999] H. You. *Angle calculations for a '4S+2D' six-circle diffractometer.* + J. Appl. Cryst. (1999). **32**, 614-623. `(pdf link) + `__. +.. [Busing1967] W. R. Busing and H. A. Levy. *Angle calculations for 3- and 4-circle X-ray + and neutron diffractometers.* Acta Cryst. (1967). **22**, 457-464. `(pdf link) + `__. diff --git a/script/test/diffcalc (copy)/doc/tmp/constraints.txt b/script/test/diffcalc (copy)/doc/tmp/constraints.txt new file mode 100644 index 0000000..5c64bf2 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/tmp/constraints.txt @@ -0,0 +1,47 @@ +Take a six circle diffractometer with angles: + +delta, nu, mu, eta, chi, phi + +and virtual angles: + +qaz, naz, psi, alpha, beta. + +Many combinations of these can be constrained: + + +constrain +>>> con nu +>>> con psi +>>> con mu +>>> con nu mu a_eq_b + +real angles are always constrained to there last set position. If they are too far from here +an error will result. + +>>> pos nu 0 (moves, parameter tracks last set value (actually checks it when hkl moved)) +>>> pos psi 90 (does not move, sets parameter in diffcalc) +>>> pos mu 0 (moves) +>>> pos hkl [1 0 0] (moves based on last set nu, psi, mu) +>>> pos betain --> Exception, not constrained + +moving a virtual angle simply sets it to efftec the next hkl move + +Here we need onlu con, uncon and pos. + +...... + +However it would be nice to be able to pos betain for example and have it change +betain while staying on the same reflection. This might be enabled by locking the +the current hkl position. It would be trick to make this work with the physical angles though. + +>>> lock hkl +>>> pos psi 90 (moves immediately) +>>> scan psi 0 90 1 (scans, moving immediately) +>>> pos mu 0 (does not move immediately) (could work by listening to target positions, and triggering move when complete) +>>> pos c(mu) moves +>>> pos mu_c moves + +Only one thing would be varyable at a time unless they are put in a CoordinatedMotionGroup, +which would be hard for the real motors) + + diff --git a/script/test/diffcalc (copy)/doc/tmp/extensions_to_yous_paper.wxm b/script/test/diffcalc (copy)/doc/tmp/extensions_to_yous_paper.wxm new file mode 100644 index 0000000..1e866bf --- /dev/null +++ b/script/test/diffcalc (copy)/doc/tmp/extensions_to_yous_paper.wxm @@ -0,0 +1,525 @@ +/* [wxMaxima batch file version 1] [ DO NOT EDIT BY HAND! ]*/ +/* [ Created with wxMaxima version 11.08.0 ] */ + +/* [wxMaxima: input start ] */ +Rx:matrix([1,0,0],[0, cos(theta), -sin(theta)],[0, sin(theta), cos(theta)])$ +Ry:matrix([cos(theta), 0, sin(theta)], [0, 1, 0], [-sin(theta), 0, cos(theta)])$ +Rz:matrix([cos(theta), -sin(theta), 0], [sin(theta), cos(theta), 0], [0, 0, 1])$ +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +Equation 5: + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +PHI:subst(-phi, theta, Rz); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +CHI:subst(chi, theta, Ry); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +ETA:subst(-eta, theta, Rz); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +MU:subst(mu, theta, Rx); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +Generic V: + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +V:matrix([v11, v12, v13], [v21, v22, v23], [v31, v32, v33]); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] + + [wxMaxima: comment end ] */ + +/* [wxMaxima: section start ] +When one sample-orienting angle is given + [wxMaxima: section end ] */ + +/* [wxMaxima: subsect start ] +For mu fixed + [wxMaxima: subsect end ] */ + +/* [wxMaxima: input start ] */ +eq34:ETA.CHI.PHI; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +eq34_chi0:subst(0, chi, eq34); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*phi* with chi <> 0 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigreduce(trigsimp((eq34[3,2]=V[3,2])/(eq34[3,1]=V[3,1]))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +show that with phi with v31 == 0 and v12 <> 0, no special case is needed when using atan2 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +solve(cos(phi)=0, phi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +atan2(1234,0); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +show that with phi with v32 == 0 and v31 <> 0, no special case is needed when using atan2 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +solve(sin(phi)=0, phi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +atan2(0,1234); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*eta* with chi <> 0 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigreduce(trigsimp((-eq34[2,3]=V[2,3]) / (eq34[1, 3]=V[1,3]))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +eta with v23 == 0 and v13 == 0 (phi||eta degeneracy because chi == 0) +Then see equation for phi+eta above + [wxMaxima: comment end ] */ + +/* [wxMaxima: comment start ] +*chi* + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigsimp(sqrt((eq34[3,1]=V[3,1])^2+(eq34[3,2]=V[3,2])^2)/(eq34[3,3]=V[3,3])); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*phi + eta* with chi == 0 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigreduce(trigrat((eq34_chi0[1,2]=V[1,2])/(eq1:eq34_chi0[1,1]=V[1,1]))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: subsect start ] +For phi fixed + [wxMaxima: subsect end ] */ + +/* [wxMaxima: input start ] */ +eq36:MU.ETA.CHI; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +eq36_eta90:subst(%pi/2, eta, eq36); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*eta* + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigsimp((eq36[1,2]=V[1,2])/sqrt((eq36[2,2]=V[2,2])^2+(eq36[3,2]=V[3,2])^2)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*mu* with eta <> +-90 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigreduce(trigsimp((eq36[3,2]=V[3,2])/(eq36[2,2]=V[2,2]))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*chi* with eta <> +-90 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigreduce(trigsimp((eq36[1,3]=V[1,3])/(eq36[1,1]=V[1,1]))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*chi + mu* with eta == +-90 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +trigreduce(trigrat((eq36_eta90[2,3]=V[2,3])/(eq36_eta90[2,1]=V[2,1]))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: subsect start ] +For eta or chi fixed + [wxMaxima: subsect end ] */ + +/* [wxMaxima: input start ] */ +eq38:MU.ETA.CHI.PHI; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*chi* (eq. 39) given eta, and eta <> +-90 (with eta == +-90 chi||mu) + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +(eq38[1,3]=V[1,3])/cos(eta); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] + + [wxMaxima: comment end ] */ + +/* [wxMaxima: comment start ] +*eta* given chi, and chi <> 0 (with chi == 0 phi||eta) + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +(eq38[1,3]=V[1,3])/sin(chi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*mu* given that chi and eta have been given or calclulated respecitively + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +top:(eq38[3,3]=V[3,3])*sin(eta)*sin(chi) + (eq38[2,3]=V[2,3])*cos(chi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +bot:-(eq38[3,3]=V[3,3])*cos(chi)+(eq38[2,3]=V[2,3])*sin(eta)*sin(chi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +factor(top); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +factor(bot); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +trigsimp(top/bot); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*phi - mu* when phi || mu because chi == +-90, eta == 0 or eta == 180 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +phi_mu_parallel:subst(0, eta, subst(%pi/2, chi, eq38)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +trigreduce(trigrat((phi_mu_parallel[2,1]=V[2,1])/(phi_mu_parallel[2,2]=V[2,2]))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: section start ] +Two sample angles given + [wxMaxima: section end ] */ + +/* [wxMaxima: input start ] */ +THETA:subst(-theta, theta, Rz); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +PSI:subst(psi, theta, Rx); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +F:subst(xi, theta, Ry); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: section start ] +psi, mu, eta given + [wxMaxima: section end ] */ + +/* [wxMaxima: input start ] */ +eq49:transpose(PHI).transpose(CHI).transpose(ETA).transpose(MU).F; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*chi* + [wxMaxima: comment end ] */ + +/* [wxMaxima: comment start ] +using the linear combination identity + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +comb_idenity:sqrt(A^2+B^2)*sin(x+omega); +omega:atan(B/A); +A*sin(x)+B*cos(x)=comb_idenity; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +a:-sin(eta)*cos(mu)$ +b:-sin(mu)$ +V32=a*sin(chi)+b*cos(chi); +V32=subst(b, B, subst(a, A, comb_idenity)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +asin(%/(sqrt(sin(mu)^2+sin(eta)^2*cos(mu)^2))), triginverses=all; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*xi and phi* + + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +top:factor(eq49[3,1]*cos(xi)+eq49[3,3]*sin(xi)); +bot:factor(eq49[3,1]*sin(xi)-eq49[3,3]*cos(xi)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +factor(top/bot); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +When mu=-90 and eta = 0, used to for a surface normal vertical mode + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +eq49_vert:subst(-%pi/2, mu, subst(0, eta, eq49)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +(eq49_vert[2,2]=V[2,2]) / (eq49_vert[1,2]=V[1,2]); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: subsect start ] +mu, phi, qaz (for three circle on i10 and i06, not in paper ) + [wxMaxima: subsect end ] */ + +/* [wxMaxima: input start ] */ +thisV:transpose(MU).F.THETA; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +Nphi:matrix([n11, n12, n13], [n21, n22, n23], [n31, n32, n33]); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +eq:ETA.CHI.PHI.Nphi.transpose(PSI); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*chi* + [wxMaxima: comment end ] */ + +/* [wxMaxima: comment start ] +using the linear combination identity + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +comb_idenity:sqrt(A^2+B^2)*sin(x+omega); +omega:atan(B/A); +A*sin(x)+B*cos(x)=comb_idenity; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +on V31: + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +a:-1*(n21*sin(phi)+n11*cos(phi))$ +b:n31$ +V31=a*sin(chi)+b*cos(chi); +V31=subst(b, B, subst(a, A, comb_idenity)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +asin(%/sqrt((-n21*sin(phi)-n11*cos(phi))^2+n31^2)), triginverses=all; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +*xi and phi* + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +top:factor(eq49[3,1]*cos(xi)+eq49[3,3]*sin(xi)); +bot:factor(eq49[3,1]*sin(xi)-eq49[3,3]*cos(xi)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +factor(top/bot); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +solve(); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: section start ] +Fixed mu, chi, psi (probably with mu=chi=0) (not in paper) + [wxMaxima: section end ] */ + +/* [wxMaxima: input start ] */ +Vcalulated:Nphi.transpose(PSI).transpose(THETA); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +thisV:transpose(PHI).transpose(CHI).transpose(ETA).transpose(MU).F; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +v:thisVvert:subst(0, chi, subst(0, mu, thisV)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +acos(thisVvert[3,3]=V[3,3]), triginverses=all; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +top:(v[1,2]=V[1,2])*cos(phi) + (v[2,2]=V[2,2])*sin(phi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +bot:(v[1,2]=V[1,2])*sin(phi) - (v[2,2]=V[2,2])*cos(phi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +factor(top); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +factor(bot); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +trigsimp(top/bot); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: title start ] +5.5 subset with eta=chi=0 & phi set + [wxMaxima: title end ] */ + +/* [wxMaxima: input start ] */ +matrix([cos(theta)*sin(qaz)], [-sin(theta)], [cos(theta)*cos(qaz)]); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +h_phi:matrix([h1], [h2], [h3]); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +Z:MU.ETA.CHI.PHI; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +eq68yterm:-sin(theta) = ((Z.h_phi))[2][1]; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +expands(%); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +eq68_chi_eta_0:subst(0, chi, subst(0, eta, Z.h_phi)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +eq68_chi_eta_0_yterm:-sin(theta)=eq68_chi_eta_0[2]; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +subst(0, chi, subst(0, eta, eq68yterm)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +a; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +ratsubst(a, -h3, eq68_chi_eta_0_yterm); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +a:-h3; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +b:-h1*sin(phi) +h2*cos(phi); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +c:-sin(theta); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +ratsubst(cc, c, ratsubst(bb, b, ratsubst(aa, a, eq68_chi_eta_0_yterm))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +arcsin(c/sqrt(a^2+b^2)) - arctan(b, a); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +eq68_chi_eta_0[1] /eq68_chi_eta_0[2]; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +without chi=eta=0 + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +eq68yterm:-sin(theta) = ((Z.h_phi))[2][1]; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +expand(eq68yterm); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +solve for mu unknown + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +c:-sin(theta); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +a:sin(chi)*h2*sin(phi) + sin(chi)*h1*cos(phi) - cos(chi)*h3; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +b: -cos(chi)*sin(eta)*h2*sin(phi) -cos(eta)*h1*sin(phi) +cos(eta)*h2*cos(phi) -cos(chi)*sin(eta)*h1*cos(phi) - sin(chi)*sin(eta)*h3; +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +expand(subst(a, aa, subst(b, bb, -sin(theta)=aa*sin(mu) + bb*cos(mu)))); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +Check a and b expand to produce original (Okay) + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +expand(eq68yterm); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: comment start ] +Check against special case above (Okay) + [wxMaxima: comment end ] */ + +/* [wxMaxima: input start ] */ +subst(0, chi, subst(0, eta, a)); +/* [wxMaxima: input end ] */ + +/* [wxMaxima: input start ] */ +subst(0, chi, subst(0, eta, b)); +/* [wxMaxima: input end ] */ + +/* Maxima can't load/batch files which end with a comment! */ +"Created with wxMaxima"$ diff --git a/script/test/diffcalc (copy)/doc/tmp/i16_non_diffcalc_manual.txt b/script/test/diffcalc (copy)/doc/tmp/i16_non_diffcalc_manual.txt new file mode 100644 index 0000000..16aa9d6 --- /dev/null +++ b/script/test/diffcalc (copy)/doc/tmp/i16_non_diffcalc_manual.txt @@ -0,0 +1,71 @@ +Creating a UB Matrix +There are three euler modes: + +=bisecting: eta and delta are fixed, phi and chi are not fixed. +=fixed phi: phi and 2theta are fixed, eta and chi are not fixed. +=fixed psi: Not used for creating a UB matrix . +First creat a reflection file to store the reflections data using: + +reffile('name') + +Set the crystal lattice: + +latt([a]) assumes cubic +latt([a,b]) assumes tetragonal +latt([a,b,c]) assumes orthorhombic +latt([a,b,c,gam]) assumes monclinic/hexagonal gam different from 90. +latt([a,b,c,alp,bet,gam]) + +Use: + +c2th([h k l]) + +to calculate the 2 theta position for the hkl for example: + +c2th([0 0 2]) + +Then use the following to move the diffractometer to the correct positions: + +pos delta c2th([0 0 2]) eta c2th([0 0 2])/2 + +You can now scan eta, chi, and delta to optomise the peak intensity using the following scans for example: + +scancn eta 0.01 40 w 0.2 t 1 + +scancn chi 0.01 40 w 0.2 t 1 + +scancn delta 0.01 40 w 0.2 t 1 + +A few iterations may be required. + +Now that you have the first reflection it can be stored using the following command: + +saveref('reflable',[h k l]) + +saveref('Ti1',[0 0 2]) for the above example. + +Then create a dummy UB matrix using a dummy non-parallel hkl plane eg: + +ubm('Ti1',[2 0 0]) + +Useing the following command to calculate the direction of the second reflection: + +hkl_calc([0 2 2]) + +If the calculated positions can be safely achieved then type: + +pos hkl [0 2 2] + +Find the second reflection by scanning phi, once the reflection is found save the second reflection using: + +saveref('Ti2',[0 2 2]) for example. + +Now create a real UB-Matrix using: + +ubm('Ti1','Ti2') + +You can now drive in recriprocal space. + +To change the lattice parameters according the found Bragg peaks + +latt([latt()[0]*2/l()]) (cubic only) \ No newline at end of file diff --git a/script/test/diffcalc (copy)/install-jython-environment.sh b/script/test/diffcalc (copy)/install-jython-environment.sh new file mode 100755 index 0000000..4ac8ef1 --- /dev/null +++ b/script/test/diffcalc (copy)/install-jython-environment.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh + +JYTHON_URL='http://search.maven.org/remotecontent?filepath=org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar' +JAMA_JAR_URL='http://math.nist.gov/javanumerics/jama/Jama-1.0.3.jar' + +# Install Jama jar +mkdir -p lib +wget -P $HOME/lib $JAMA_JAR_URL +export CLASSPATH=$HOME/lib/Jama-1.0.3.jar:$CLASSPATH + +# Install Jython +wget $JYTHON_URL -O jython_installer.jar +java -jar jython_installer.jar -s -d $HOME/jython + +# Install nose for Jython +# TODO: move to a setup.py +$HOME/jython/bin/pip install nose +$HOME/jython/bin/pip install pytest==3.2.3 +$HOME/jython/bin/pip install pytest-xdist diff --git a/script/test/diffcalc (copy)/integration_checks.py b/script/test/diffcalc (copy)/integration_checks.py new file mode 100644 index 0000000..d3fdacc --- /dev/null +++ b/script/test/diffcalc (copy)/integration_checks.py @@ -0,0 +1,72 @@ +''' +Created on 9 Mar 2017 + +@author: zrb13439 + +''' + +''' +Integration tests for startup scripts (named so that nosetests will *not* pick +up by default. + +This is because these must be run with: + + $ nosetests --with-process-isolation integration_checks.py + +This is not a standard nose option. Install it with: + + $ pip install nosepipe + +nosepipe is described at https://github.com/dmccombs/nosepipe/ +''' + +from nose import SkipTest + + +def test_sixcircle_startup(): + import startup.sixcircle + startup.sixcircle.ct.pause = False + startup.sixcircle.demo.all() + + +def test_fivecircle_startup(): + import startup.fivecircle + startup.fivecircle.ct.pause = False + startup.fivecircle.demo.all() + + +def test_fourcircle_startup(): + import startup.fourcircle + startup.fourcircle.ct.pause = False + startup.fourcircle.demo.all() + + +def test_i13_startup(): + raise SkipTest('Still need to work out to use i13s very tight limits') + import startup.i13 + startup.i13.ct.pause = False + startup.i13.demo.all() + + +def test_i16_startup(): + import startup.i16 + startup.i16.ct.pause = False + startup.i16.demo.all() + + +def test_i21_startup_standard(): + import startup.i21 + startup.i21.ct.pause = False + startup.i21.demo.all() + + +def test_i21_startup_bespoke(): + import startup.i21 + startup.i21.ct.pause = False + startup.i21.demo.i21() + + +def test_sixcirle_api(): + import startup.api.sixcircle + startup.api.sixcircle.demo_all() + diff --git a/script/test/diffcalc (copy)/mock.py b/script/test/diffcalc (copy)/mock.py new file mode 100644 index 0000000..24799cf --- /dev/null +++ b/script/test/diffcalc (copy)/mock.py @@ -0,0 +1,2288 @@ +# mock.py +# Test tools for mocking and patching. +# Copyright (C) 2007-2012 Michael Foord & the mock team +# E-mail: fuzzyman AT voidspace DOT org DOT uk + +# mock 0.8.0 +# http://www.voidspace.org.uk/python/mock/ + +# Released subject to the BSD License +# Please see http://www.voidspace.org.uk/python/license.shtml + +# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml +# Comments, suggestions and bug reports welcome. + + +__all__ = ( + 'Mock', + 'MagicMock', + 'mocksignature', + 'patch', + 'sentinel', + 'DEFAULT', + 'ANY', + 'call', + 'create_autospec', + 'FILTER_DIR', + 'NonCallableMock', + 'NonCallableMagicMock', +) + + +__version__ = '0.8.0' + + +import pprint +import sys + +try: + import inspect +except ImportError: + # for alternative platforms that + # may not have inspect + inspect = None + +try: + from functools import wraps +except ImportError: + # Python 2.4 compatibility + def wraps(original): + def inner(f): + f.__name__ = original.__name__ + f.__doc__ = original.__doc__ + f.__module__ = original.__module__ + return f + return inner + +try: + unicode +except NameError: + # Python 3 + basestring = unicode = str + +try: + long +except NameError: + # Python 3 + long = int + +try: + BaseException +except NameError: + # Python 2.4 compatibility + BaseException = Exception + +try: + next +except NameError: + def next(obj): + return obj.next() + + +BaseExceptions = (BaseException,) +if 'java' in sys.platform: + # jython + import java + BaseExceptions = (BaseException, java.lang.Throwable) + +try: + _isidentifier = str.isidentifier +except AttributeError: + # Python 2.X + import keyword + import re + regex = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) + def _isidentifier(string): + if string in keyword.kwlist: + return False + return regex.match(string) + + +inPy3k = sys.version_info[0] == 3 + +# Needed to work around Python 3 bug where use of "super" interferes with +# defining __class__ as a descriptor +_super = super + +self = 'im_self' +builtin = '__builtin__' +if inPy3k: + self = '__self__' + builtin = 'builtins' + +FILTER_DIR = True + + +def _is_instance_mock(obj): + # can't use isinstance on Mock objects because they override __class__ + # The base class for all mocks is NonCallableMock + return issubclass(type(obj), NonCallableMock) + + +def _is_exception(obj): + return ( + isinstance(obj, BaseExceptions) or + isinstance(obj, ClassTypes) and issubclass(obj, BaseExceptions) + ) + + +class _slotted(object): + __slots__ = ['a'] + + +DescriptorTypes = ( + type(_slotted.a), + property, +) + + +# getsignature and mocksignature heavily "inspired" by +# the decorator module: http://pypi.python.org/pypi/decorator/ +# by Michele Simionato + +def _getsignature(func, skipfirst): + if inspect is None: + raise ImportError('inspect module not available') + + if inspect.isclass(func): + func = func.__init__ + # will have a self arg + skipfirst = True + elif not (inspect.ismethod(func) or inspect.isfunction(func)): + func = func.__call__ + + regargs, varargs, varkwargs, defaults = inspect.getargspec(func) + + # instance methods need to lose the self argument + if getattr(func, self, None) is not None: + regargs = regargs[1:] + + _msg = ("_mock_ is a reserved argument name, can't mock signatures using " + "_mock_") + assert '_mock_' not in regargs, _msg + if varargs is not None: + assert '_mock_' not in varargs, _msg + if varkwargs is not None: + assert '_mock_' not in varkwargs, _msg + if skipfirst: + regargs = regargs[1:] + + signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults, + formatvalue=lambda value: "") + return signature[1:-1], func + + +def _getsignature2(func, skipfirst, instance=False): + if inspect is None: + raise ImportError('inspect module not available') + + if isinstance(func, ClassTypes) and not instance: + try: + func = func.__init__ + except AttributeError: + return + skipfirst = True + elif not isinstance(func, FunctionTypes): + # for classes where instance is True we end up here too + try: + func = func.__call__ + except AttributeError: + return + + try: + regargs, varargs, varkwargs, defaults = inspect.getargspec(func) + except TypeError: + # C function / method, possibly inherited object().__init__ + return + + # instance methods and classmethods need to lose the self argument + if getattr(func, self, None) is not None: + regargs = regargs[1:] + if skipfirst: + # this condition and the above one are never both True - why? + regargs = regargs[1:] + + signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults, + formatvalue=lambda value: "") + return signature[1:-1], func + + +def _check_signature(func, mock, skipfirst, instance=False): + if not _callable(func): + return + + result = _getsignature2(func, skipfirst, instance) + if result is None: + return + signature, func = result + + # can't use self because "self" is common as an argument name + # unfortunately even not in the first place + src = "lambda _mock_self, %s: None" % signature + checksig = eval(src, {}) + _copy_func_details(func, checksig) + type(mock)._mock_check_sig = checksig + + +def _copy_func_details(func, funcopy): + funcopy.__name__ = func.__name__ + funcopy.__doc__ = func.__doc__ + #funcopy.__dict__.update(func.__dict__) + funcopy.__module__ = func.__module__ + if not inPy3k: + funcopy.func_defaults = func.func_defaults + return + funcopy.__defaults__ = func.__defaults__ + funcopy.__kwdefaults__ = func.__kwdefaults__ + + +def _callable(obj): + if isinstance(obj, ClassTypes): + return True + if getattr(obj, '__call__', None) is not None: + return True + return False + + +def _is_list(obj): + # checks for list or tuples + # XXXX badly named! + return type(obj) in (list, tuple) + + +def _instance_callable(obj): + """Given an object, return True if the object is callable. + For classes, return True if instances would be callable.""" + if not isinstance(obj, ClassTypes): + # already an instance + return getattr(obj, '__call__', None) is not None + + klass = obj + # uses __bases__ instead of __mro__ so that we work with old style classes + if klass.__dict__.get('__call__') is not None: + return True + + for base in klass.__bases__: + if _instance_callable(base): + return True + return False + + +def _set_signature(mock, original, instance=False): + # creates a function with signature (*args, **kwargs) that delegates to a + # mock. It still does signature checking by calling a lambda with the same + # signature as the original. This is effectively mocksignature2. + if not _callable(original): + return + + skipfirst = isinstance(original, ClassTypes) + result = _getsignature2(original, skipfirst, instance) + if result is None: + # was a C function (e.g. object().__init__ ) that can't be mocked + return + + signature, func = result + + src = "lambda %s: None" % signature + context = {'_mock_': mock} + checksig = eval(src, context) + _copy_func_details(func, checksig) + + name = original.__name__ + if not _isidentifier(name): + name = 'funcopy' + context = {'checksig': checksig, 'mock': mock} + src = """def %s(*args, **kwargs): + checksig(*args, **kwargs) + return mock(*args, **kwargs)""" % name + exec (src, context) + funcopy = context[name] + _setup_func(funcopy, mock) + return funcopy + + +def mocksignature(func, mock=None, skipfirst=False): + """ + mocksignature(func, mock=None, skipfirst=False) + + Create a new function with the same signature as `func` that delegates + to `mock`. If `skipfirst` is True the first argument is skipped, useful + for methods where `self` needs to be omitted from the new function. + + If you don't pass in a `mock` then one will be created for you. + + The mock is set as the `mock` attribute of the returned function for easy + access. + + Functions returned by `mocksignature` have many of the same attributes + and assert methods as a mock object. + + `mocksignature` can also be used with classes. It copies the signature of + the `__init__` method. + + When used with callable objects (instances) it copies the signature of the + `__call__` method. + """ + if mock is None: + mock = Mock() + signature, func = _getsignature(func, skipfirst) + src = "lambda %(signature)s: _mock_(%(signature)s)" % { + 'signature': signature + } + + funcopy = eval(src, dict(_mock_=mock)) + _copy_func_details(func, funcopy) + _setup_func(funcopy, mock) + return funcopy + + +def _setup_func(funcopy, mock): + funcopy.mock = mock + + # can't use isinstance with mocks + if not _is_instance_mock(mock): + return + + def assert_called_with(*args, **kwargs): + return mock.assert_called_with(*args, **kwargs) + def assert_called_once_with(*args, **kwargs): + return mock.assert_called_once_with(*args, **kwargs) + def assert_has_calls(*args, **kwargs): + return mock.assert_has_calls(*args, **kwargs) + def assert_any_call(*args, **kwargs): + return mock.assert_any_call(*args, **kwargs) + def reset_mock(): + funcopy.method_calls = _CallList() + funcopy.mock_calls = _CallList() + mock.reset_mock() + ret = funcopy.return_value + if _is_instance_mock(ret) and not ret is mock: + ret.reset_mock() + + funcopy.called = False + funcopy.call_count = 0 + funcopy.call_args = None + funcopy.call_args_list = _CallList() + funcopy.method_calls = _CallList() + funcopy.mock_calls = _CallList() + + funcopy.return_value = mock.return_value + funcopy.side_effect = mock.side_effect + funcopy._mock_children = mock._mock_children + + funcopy.assert_called_with = assert_called_with + funcopy.assert_called_once_with = assert_called_once_with + funcopy.assert_has_calls = assert_has_calls + funcopy.assert_any_call = assert_any_call + funcopy.reset_mock = reset_mock + + mock._mock_signature = funcopy + + +def _is_magic(name): + return '__%s__' % name[2:-2] == name + + +class _SentinelObject(object): + "A unique, named, sentinel object." + def __init__(self, name): + self.name = name + + def __repr__(self): + return 'sentinel.%s' % self.name + + +class _Sentinel(object): + """Access attributes to return a named object, usable as a sentinel.""" + def __init__(self): + self._sentinels = {} + + def __getattr__(self, name): + if name == '__bases__': + # Without this help(mock) raises an exception + raise AttributeError + return self._sentinels.setdefault(name, _SentinelObject(name)) + + +sentinel = _Sentinel() + +DEFAULT = sentinel.DEFAULT + + +class OldStyleClass: + pass +ClassType = type(OldStyleClass) + + +def _copy(value): + if type(value) in (dict, list, tuple, set): + return type(value)(value) + return value + + +ClassTypes = (type,) +if not inPy3k: + ClassTypes = (type, ClassType) + +_allowed_names = set( + [ + 'return_value', '_mock_return_value', 'side_effect', + '_mock_side_effect', '_mock_parent', '_mock_new_parent', + '_mock_name', '_mock_new_name' + ] +) + + +def _mock_signature_property(name): + _allowed_names.add(name) + _the_name = '_mock_' + name + def _get(self, name=name, _the_name=_the_name): + sig = self._mock_signature + if sig is None: + return getattr(self, _the_name) + return getattr(sig, name) + def _set(self, value, name=name, _the_name=_the_name): + sig = self._mock_signature + if sig is None: + self.__dict__[_the_name] = value + else: + setattr(sig, name, value) + + return property(_get, _set) + + + +class _CallList(list): + + def __contains__(self, value): + if not isinstance(value, list): + return list.__contains__(self, value) + len_value = len(value) + len_self = len(self) + if len_value > len_self: + return False + + for i in range(0, len_self - len_value + 1): + sub_list = self[i:i+len_value] + if sub_list == value: + return True + return False + + def __repr__(self): + return pprint.pformat(list(self)) + + +def _check_and_set_parent(parent, value, name, new_name): + if not _is_instance_mock(value): + return False + if ((value._mock_name or value._mock_new_name) or + (value._mock_parent is not None) or + (value._mock_new_parent is not None)): + return False + + _parent = parent + while _parent is not None: + # setting a mock (value) as a child or return value of itself + # should not modify the mock + if _parent is value: + return False + _parent = _parent._mock_new_parent + + if new_name: + value._mock_new_parent = parent + value._mock_new_name = new_name + if name: + value._mock_parent = parent + value._mock_name = name + return True + + + +class Base(object): + _mock_return_value = DEFAULT + _mock_side_effect = None + def __init__(self, *args, **kwargs): + pass + + + +class NonCallableMock(Base): + """A non-callable version of `Mock`""" + + def __new__(cls, *args, **kw): + # every instance has its own class + # so we can create magic methods on the + # class without stomping on other mocks + new = type(cls.__name__, (cls,), {'__doc__': cls.__doc__}) + instance = object.__new__(new) + return instance + + + def __init__( + self, spec=None, wraps=None, name=None, spec_set=None, + parent=None, _spec_state=None, _new_name='', _new_parent=None, + **kwargs + ): + if _new_parent is None: + _new_parent = parent + + __dict__ = self.__dict__ + __dict__['_mock_parent'] = parent + __dict__['_mock_name'] = name + __dict__['_mock_new_name'] = _new_name + __dict__['_mock_new_parent'] = _new_parent + + if spec_set is not None: + spec = spec_set + spec_set = True + + self._mock_add_spec(spec, spec_set) + + __dict__['_mock_children'] = {} + __dict__['_mock_wraps'] = wraps + __dict__['_mock_signature'] = None + + __dict__['_mock_called'] = False + __dict__['_mock_call_args'] = None + __dict__['_mock_call_count'] = 0 + __dict__['_mock_call_args_list'] = _CallList() + __dict__['_mock_mock_calls'] = _CallList() + + __dict__['method_calls'] = _CallList() + + if kwargs: + self.configure_mock(**kwargs) + + _super(NonCallableMock, self).__init__( + spec, wraps, name, spec_set, parent, + _spec_state + ) + + + def attach_mock(self, mock, attribute): + """ + Attach a mock as an attribute of this one, replacing its name and + parent. Calls to the attached mock will be recorded in the + `method_calls` and `mock_calls` attributes of this one.""" + mock._mock_parent = None + mock._mock_new_parent = None + mock._mock_name = '' + mock._mock_new_name = None + + setattr(self, attribute, mock) + + + def mock_add_spec(self, spec, spec_set=False): + """Add a spec to a mock. `spec` can either be an object or a + list of strings. Only attributes on the `spec` can be fetched as + attributes from the mock. + + If `spec_set` is True then only attributes on the spec can be set.""" + self._mock_add_spec(spec, spec_set) + + + def _mock_add_spec(self, spec, spec_set): + _spec_class = None + + if spec is not None and not _is_list(spec): + if isinstance(spec, ClassTypes): + _spec_class = spec + else: + _spec_class = _get_class(spec) + + spec = dir(spec) + + __dict__ = self.__dict__ + __dict__['_spec_class'] = _spec_class + __dict__['_spec_set'] = spec_set + __dict__['_mock_methods'] = spec + + + def __get_return_value(self): + ret = self._mock_return_value + if self._mock_signature is not None: + ret = self._mock_signature.return_value + + if ret is DEFAULT: + ret = self._get_child_mock( + _new_parent=self, _new_name='()' + ) + self.return_value = ret + return ret + + + def __set_return_value(self, value): + if self._mock_signature is not None: + self._mock_signature.return_value = value + else: + self._mock_return_value = value + _check_and_set_parent(self, value, None, '()') + + __return_value_doc = "The value to be returned when the mock is called." + return_value = property(__get_return_value, __set_return_value, + __return_value_doc) + + + @property + def __class__(self): + if self._spec_class is None: + return type(self) + return self._spec_class + + called = _mock_signature_property('called') + call_count = _mock_signature_property('call_count') + call_args = _mock_signature_property('call_args') + call_args_list = _mock_signature_property('call_args_list') + mock_calls = _mock_signature_property('mock_calls') + + + def __get_side_effect(self): + sig = self._mock_signature + if sig is None: + return self._mock_side_effect + return sig.side_effect + + def __set_side_effect(self, value): + value = _try_iter(value) + sig = self._mock_signature + if sig is None: + self._mock_side_effect = value + else: + sig.side_effect = value + + side_effect = property(__get_side_effect, __set_side_effect) + + + def reset_mock(self): + "Restore the mock object to its initial state." + self.called = False + self.call_args = None + self.call_count = 0 + self.mock_calls = _CallList() + self.call_args_list = _CallList() + self.method_calls = _CallList() + + for child in self._mock_children.values(): + child.reset_mock() + + ret = self._mock_return_value + if _is_instance_mock(ret) and ret is not self: + ret.reset_mock() + + + def configure_mock(self, **kwargs): + """Set attributes on the mock through keyword arguments. + + Attributes plus return values and side effects can be set on child + mocks using standard dot notation and unpacking a dictionary in the + method call: + + >>> attrs = {'method.return_value': 3, 'other.side_effect': KeyError} + >>> mock.configure_mock(**attrs)""" + for arg, val in sorted(kwargs.items(), + # we sort on the number of dots so that + # attributes are set before we set attributes on + # attributes + key=lambda entry: entry[0].count('.')): + args = arg.split('.') + final = args.pop() + obj = self + for entry in args: + obj = getattr(obj, entry) + setattr(obj, final, val) + + + def __getattr__(self, name): + if name == '_mock_methods': + raise AttributeError(name) + elif self._mock_methods is not None: + if name not in self._mock_methods or name in _all_magics: + raise AttributeError("Mock object has no attribute %r" % name) + elif _is_magic(name): + raise AttributeError(name) + + result = self._mock_children.get(name) + if result is None: + wraps = None + if self._mock_wraps is not None: + # XXXX should we get the attribute without triggering code + # execution? + wraps = getattr(self._mock_wraps, name) + + result = self._get_child_mock( + parent=self, name=name, wraps=wraps, _new_name=name, + _new_parent=self + ) + self._mock_children[name] = result + + elif isinstance(result, _SpecState): + result = create_autospec( + result.spec, result.spec_set, result.instance, + result.parent, result.name + ) + self._mock_children[name] = result + + return result + + + def __repr__(self): + _name_list = [self._mock_new_name] + _parent = self._mock_new_parent + last = self + + dot = '.' + if _name_list == ['()']: + dot = '' + seen = set() + while _parent is not None: + last = _parent + + _name_list.append(_parent._mock_new_name + dot) + dot = '.' + if _parent._mock_new_name == '()': + dot = '' + + _parent = _parent._mock_new_parent + + # use ids here so as not to call __hash__ on the mocks + if id(_parent) in seen: + break + seen.add(id(_parent)) + + _name_list = list(reversed(_name_list)) + _first = last._mock_name or 'mock' + if len(_name_list) > 1: + if _name_list[1] not in ('()', '().'): + _first += '.' + _name_list[0] = _first + name = ''.join(_name_list) + + name_string = '' + if name not in ('mock', 'mock.'): + name_string = ' name=%r' % name + + spec_string = '' + if self._spec_class is not None: + spec_string = ' spec=%r' + if self._spec_set: + spec_string = ' spec_set=%r' + spec_string = spec_string % self._spec_class.__name__ + return "<%s%s%s id='%s'>" % ( + type(self).__name__, + name_string, + spec_string, + id(self) + ) + + + def __dir__(self): + """Filter the output of `dir(mock)` to only useful members. + XXXX + """ + extras = self._mock_methods or [] + from_type = dir(type(self)) + from_dict = list(self.__dict__) + + if FILTER_DIR: + from_type = [e for e in from_type if not e.startswith('_')] + from_dict = [e for e in from_dict if not e.startswith('_') or + _is_magic(e)] + return sorted(set(extras + from_type + from_dict + + list(self._mock_children))) + + + def __setattr__(self, name, value): + if name in _allowed_names: + # property setters go through here + return object.__setattr__(self, name, value) + elif (self._spec_set and self._mock_methods is not None and + name not in self._mock_methods and + name not in self.__dict__): + raise AttributeError("Mock object has no attribute '%s'" % name) + elif name in _unsupported_magics: + msg = 'Attempting to set unsupported magic method %r.' % name + raise AttributeError(msg) + elif name in _all_magics: + if self._mock_methods is not None and name not in self._mock_methods: + raise AttributeError("Mock object has no attribute '%s'" % name) + + if not _is_instance_mock(value): + setattr(type(self), name, _get_method(name, value)) + original = value + real = lambda *args, **kw: original(self, *args, **kw) + value = mocksignature(value, real, skipfirst=True) + else: + # only set _new_name and not name so that mock_calls is tracked + # but not method calls + _check_and_set_parent(self, value, None, name) + setattr(type(self), name, value) + else: + if _check_and_set_parent(self, value, name, name): + self._mock_children[name] = value + return object.__setattr__(self, name, value) + + + def __delattr__(self, name): + if name in _all_magics and name in type(self).__dict__: + delattr(type(self), name) + if name not in self.__dict__: + # for magic methods that are still MagicProxy objects and + # not set on the instance itself + return + + return object.__delattr__(self, name) + + + def _format_mock_call_signature(self, args, kwargs): + name = self._mock_name or 'mock' + return _format_call_signature(name, args, kwargs) + + + def _format_mock_failure_message(self, args, kwargs): + message = 'Expected call: %s\nActual call: %s' + expected_string = self._format_mock_call_signature(args, kwargs) + call_args = self.call_args + if len(call_args) == 3: + call_args = call_args[1:] + actual_string = self._format_mock_call_signature(*call_args) + return message % (expected_string, actual_string) + + + def assert_called_with(_mock_self, *args, **kwargs): + """assert that the mock was called with the specified arguments. + + Raises an AssertionError if the args and keyword args passed in are + different to the last call to the mock.""" + self = _mock_self + if self.call_args is None: + expected = self._format_mock_call_signature(args, kwargs) + raise AssertionError('Expected call: %s\nNot called' % (expected,)) + + if self.call_args != (args, kwargs): + msg = self._format_mock_failure_message(args, kwargs) + raise AssertionError(msg) + + + def assert_called_once_with(_mock_self, *args, **kwargs): + """assert that the mock was called exactly once and with the specified + arguments.""" + self = _mock_self + if not self.call_count == 1: + msg = ("Expected to be called once. Called %s times." % + self.call_count) + raise AssertionError(msg) + return self.assert_called_with(*args, **kwargs) + + + def assert_has_calls(self, calls, any_order=False): + """assert the mock has been called with the specified calls. + The `mock_calls` list is checked for the calls. + + If `any_order` is False (the default) then the calls must be + sequential. There can be extra calls before or after the + specified calls. + + If `any_order` is True then the calls can be in any order, but + they must all appear in `mock_calls`.""" + if not any_order: + if calls not in self.mock_calls: + raise AssertionError( + 'Calls not found.\nExpected: %r\n' + 'Actual: %r' % (calls, self.mock_calls) + ) + return + + all_calls = list(self.mock_calls) + + not_found = [] + for kall in calls: + try: + all_calls.remove(kall) + except ValueError: + not_found.append(kall) + if not_found: + raise AssertionError( + '%r not all found in call list' % (tuple(not_found),) + ) + + + def assert_any_call(self, *args, **kwargs): + """assert the mock has been called with the specified arguments. + + The assert passes if the mock has *ever* been called, unlike + `assert_called_with` and `assert_called_once_with` that only pass if + the call is the most recent one.""" + kall = call(*args, **kwargs) + if kall not in self.call_args_list: + expected_string = self._format_mock_call_signature(args, kwargs) + raise AssertionError( + '%s call not found' % expected_string + ) + + + def _get_child_mock(self, **kw): + """Create the child mocks for attributes and return value. + By default child mocks will be the same type as the parent. + Subclasses of Mock may want to override this to customize the way + child mocks are made. + + For non-callable mocks the callable variant will be used (rather than + any custom subclass).""" + _type = type(self) + if not issubclass(_type, CallableMixin): + if issubclass(_type, NonCallableMagicMock): + klass = MagicMock + elif issubclass(_type, NonCallableMock) : + klass = Mock + else: + klass = _type.__mro__[1] + return klass(**kw) + + + +def _try_iter(obj): + if obj is None: + return obj + if _is_exception(obj): + return obj + if _callable(obj): + return obj + try: + return iter(obj) + except TypeError: + # XXXX backwards compatibility + # but this will blow up on first call - so maybe we should fail early? + return obj + + + +class CallableMixin(Base): + + def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, + wraps=None, name=None, spec_set=None, parent=None, + _spec_state=None, _new_name='', _new_parent=None, **kwargs): + self.__dict__['_mock_return_value'] = return_value + + _super(CallableMixin, self).__init__( + spec, wraps, name, spec_set, parent, + _spec_state, _new_name, _new_parent, **kwargs + ) + + self.side_effect = side_effect + + + def _mock_check_sig(self, *args, **kwargs): + # stub method that can be replaced with one with a specific signature + pass + + + def __call__(_mock_self, *args, **kwargs): + # can't use self in-case a function / method we are mocking uses self + # in the signature + _mock_self._mock_check_sig(*args, **kwargs) + return _mock_self._mock_call(*args, **kwargs) + + + def _mock_call(_mock_self, *args, **kwargs): + self = _mock_self + self.called = True + self.call_count += 1 + self.call_args = _Call((args, kwargs), two=True) + self.call_args_list.append(_Call((args, kwargs), two=True)) + + _new_name = self._mock_new_name + _new_parent = self._mock_new_parent + self.mock_calls.append(_Call(('', args, kwargs))) + + seen = set() + skip_next_dot = _new_name == '()' + do_method_calls = self._mock_parent is not None + name = self._mock_name + while _new_parent is not None: + this_mock_call = _Call((_new_name, args, kwargs)) + if _new_parent._mock_new_name: + dot = '.' + if skip_next_dot: + dot = '' + + skip_next_dot = False + if _new_parent._mock_new_name == '()': + skip_next_dot = True + + _new_name = _new_parent._mock_new_name + dot + _new_name + + if do_method_calls: + if _new_name == name: + this_method_call = this_mock_call + else: + this_method_call = _Call((name, args, kwargs)) + _new_parent.method_calls.append(this_method_call) + + do_method_calls = _new_parent._mock_parent is not None + if do_method_calls: + name = _new_parent._mock_name + '.' + name + + _new_parent.mock_calls.append(this_mock_call) + _new_parent = _new_parent._mock_new_parent + + # use ids here so as not to call __hash__ on the mocks + _new_parent_id = id(_new_parent) + if _new_parent_id in seen: + break + seen.add(_new_parent_id) + + ret_val = DEFAULT + effect = self.side_effect + if effect is not None: + if _is_exception(effect): + raise effect + + if not _callable(effect): + return next(effect) + + ret_val = effect(*args, **kwargs) + if ret_val is DEFAULT: + ret_val = self.return_value + + if (self._mock_wraps is not None and + self._mock_return_value is DEFAULT): + return self._mock_wraps(*args, **kwargs) + if ret_val is DEFAULT: + ret_val = self.return_value + return ret_val + + + +class Mock(CallableMixin, NonCallableMock): + """ + Create a new `Mock` object. `Mock` takes several optional arguments + that specify the behaviour of the Mock object: + + * `spec`: This can be either a list of strings or an existing object (a + class or instance) that acts as the specification for the mock object. If + you pass in an object then a list of strings is formed by calling dir on + the object (excluding unsupported magic attributes and methods). Accessing + any attribute not in this list will raise an `AttributeError`. + + If `spec` is an object (rather than a list of strings) then + `mock.__class__` returns the class of the spec object. This allows mocks + to pass `isinstance` tests. + + * `spec_set`: A stricter variant of `spec`. If used, attempting to *set* + or get an attribute on the mock that isn't on the object passed as + `spec_set` will raise an `AttributeError`. + + * `side_effect`: A function to be called whenever the Mock is called. See + the `side_effect` attribute. Useful for raising exceptions or + dynamically changing return values. The function is called with the same + arguments as the mock, and unless it returns `DEFAULT`, the return + value of this function is used as the return value. + + Alternatively `side_effect` can be an exception class or instance. In + this case the exception will be raised when the mock is called. + + If `side_effect` is an iterable then each call to the mock will return + the next value from the iterable. + + * `return_value`: The value returned when the mock is called. By default + this is a new Mock (created on first access). See the + `return_value` attribute. + + * `wraps`: Item for the mock object to wrap. If `wraps` is not None + then calling the Mock will pass the call through to the wrapped object + (returning the real result and ignoring `return_value`). Attribute + access on the mock will return a Mock object that wraps the corresponding + attribute of the wrapped object (so attempting to access an attribute that + doesn't exist will raise an `AttributeError`). + + If the mock has an explicit `return_value` set then calls are not passed + to the wrapped object and the `return_value` is returned instead. + + * `name`: If the mock has a name then it will be used in the repr of the + mock. This can be useful for debugging. The name is propagated to child + mocks. + + Mocks can also be called with arbitrary keyword arguments. These will be + used to set attributes on the mock after it is created. + """ + + + +def _dot_lookup(thing, comp, import_path): + try: + return getattr(thing, comp) + except AttributeError: + __import__(import_path) + return getattr(thing, comp) + + +def _importer(target): + components = target.split('.') + import_path = components.pop(0) + thing = __import__(import_path) + + for comp in components: + import_path += ".%s" % comp + thing = _dot_lookup(thing, comp, import_path) + return thing + + +def _is_started(patcher): + # XXXX horrible + return hasattr(patcher, 'is_local') + + +class _patch(object): + + attribute_name = None + + def __init__( + self, getter, attribute, new, spec, create, + mocksignature, spec_set, autospec, new_callable, kwargs + ): + if new_callable is not None: + if new is not DEFAULT: + raise ValueError( + "Cannot use 'new' and 'new_callable' together" + ) + if autospec is not False: + raise ValueError( + "Cannot use 'autospec' and 'new_callable' together" + ) + + self.getter = getter + self.attribute = attribute + self.new = new + self.new_callable = new_callable + self.spec = spec + self.create = create + self.has_local = False + self.mocksignature = mocksignature + self.spec_set = spec_set + self.autospec = autospec + self.kwargs = kwargs + self.additional_patchers = [] + + + def copy(self): + patcher = _patch( + self.getter, self.attribute, self.new, self.spec, + self.create, self.mocksignature, self.spec_set, + self.autospec, self.new_callable, self.kwargs + ) + patcher.attribute_name = self.attribute_name + patcher.additional_patchers = [ + p.copy() for p in self.additional_patchers + ] + return patcher + + + def __call__(self, func): + if isinstance(func, ClassTypes): + return self.decorate_class(func) + return self.decorate_callable(func) + + + def decorate_class(self, klass): + for attr in dir(klass): + if not attr.startswith(patch.TEST_PREFIX): + continue + + attr_value = getattr(klass, attr) + if not hasattr(attr_value, "__call__"): + continue + + patcher = self.copy() + setattr(klass, attr, patcher(attr_value)) + return klass + + + def decorate_callable(self, func): + if hasattr(func, 'patchings'): + func.patchings.append(self) + return func + + @wraps(func) + def patched(*args, **keywargs): + # don't use a with here (backwards compatability with Python 2.4) + extra_args = [] + entered_patchers = [] + + # can't use try...except...finally because of Python 2.4 + # compatibility + try: + try: + for patching in patched.patchings: + arg = patching.__enter__() + entered_patchers.append(patching) + if patching.attribute_name is not None: + keywargs.update(arg) + elif patching.new is DEFAULT: + extra_args.append(arg) + + args += tuple(extra_args) + return func(*args, **keywargs) + except: + if (patching not in entered_patchers and + _is_started(patching)): + # the patcher may have been started, but an exception + # raised whilst entering one of its additional_patchers + entered_patchers.append(patching) + # re-raise the exception + raise + finally: + for patching in reversed(entered_patchers): + patching.__exit__() + + patched.patchings = [self] + if hasattr(func, 'func_code'): + # not in Python 3 + patched.compat_co_firstlineno = getattr( + func, "compat_co_firstlineno", + func.func_code.co_firstlineno + ) + return patched + + + def get_original(self): + target = self.getter() + name = self.attribute + + original = DEFAULT + local = False + + try: + original = target.__dict__[name] + except (AttributeError, KeyError): + original = getattr(target, name, DEFAULT) + else: + local = True + + if not self.create and original is DEFAULT: + raise AttributeError( + "%s does not have the attribute %r" % (target, name) + ) + return original, local + + + def __enter__(self): + """Perform the patch.""" + new, spec, spec_set = self.new, self.spec, self.spec_set + autospec, kwargs = self.autospec, self.kwargs + new_callable = self.new_callable + self.target = self.getter() + + original, local = self.get_original() + + if new is DEFAULT and autospec is False: + inherit = False + if spec_set == True: + spec_set = original + elif spec == True: + # set spec to the object we are replacing + spec = original + + if (spec or spec_set) is not None: + if isinstance(original, ClassTypes): + # If we're patching out a class and there is a spec + inherit = True + + Klass = MagicMock + _kwargs = {} + if new_callable is not None: + Klass = new_callable + elif (spec or spec_set) is not None: + if not _callable(spec or spec_set): + Klass = NonCallableMagicMock + + if spec is not None: + _kwargs['spec'] = spec + if spec_set is not None: + _kwargs['spec_set'] = spec_set + + # add a name to mocks + if (isinstance(Klass, type) and + issubclass(Klass, NonCallableMock) and self.attribute): + _kwargs['name'] = self.attribute + + _kwargs.update(kwargs) + new = Klass(**_kwargs) + + if inherit and _is_instance_mock(new): + # we can only tell if the instance should be callable if the + # spec is not a list + if (not _is_list(spec or spec_set) and not + _instance_callable(spec or spec_set)): + Klass = NonCallableMagicMock + + _kwargs.pop('name') + new.return_value = Klass(_new_parent=new, _new_name='()', + **_kwargs) + elif autospec is not False: + # spec is ignored, new *must* be default, spec_set is treated + # as a boolean. Should we check spec is not None and that spec_set + # is a bool? mocksignature should also not be used. Should we + # check this? + if new is not DEFAULT: + raise TypeError( + "autospec creates the mock for you. Can't specify " + "autospec and new." + ) + spec_set = bool(spec_set) + if autospec is True: + autospec = original + + new = create_autospec(autospec, spec_set=spec_set, + _name=self.attribute, **kwargs) + elif kwargs: + # can't set keyword args when we aren't creating the mock + # XXXX If new is a Mock we could call new.configure_mock(**kwargs) + raise TypeError("Can't pass kwargs to a mock we aren't creating") + + new_attr = new + if self.mocksignature: + new_attr = mocksignature(original, new) + + self.temp_original = original + self.is_local = local + setattr(self.target, self.attribute, new_attr) + if self.attribute_name is not None: + extra_args = {} + if self.new is DEFAULT: + extra_args[self.attribute_name] = new + for patching in self.additional_patchers: + arg = patching.__enter__() + if patching.new is DEFAULT: + extra_args.update(arg) + return extra_args + + return new + + + def __exit__(self, *_): + """Undo the patch.""" + if not _is_started(self): + raise RuntimeError('stop called on unstarted patcher') + + if self.is_local and self.temp_original is not DEFAULT: + setattr(self.target, self.attribute, self.temp_original) + else: + delattr(self.target, self.attribute) + if not self.create and not hasattr(self.target, self.attribute): + # needed for proxy objects like django settings + setattr(self.target, self.attribute, self.temp_original) + + del self.temp_original + del self.is_local + del self.target + for patcher in reversed(self.additional_patchers): + if _is_started(patcher): + patcher.__exit__() + + start = __enter__ + stop = __exit__ + + + +def _get_target(target): + try: + target, attribute = target.rsplit('.', 1) + except (TypeError, ValueError): + raise TypeError("Need a valid target to patch. You supplied: %r" % + (target,)) + getter = lambda: _importer(target) + return getter, attribute + + +def _patch_object( + target, attribute, new=DEFAULT, spec=None, + create=False, mocksignature=False, spec_set=None, autospec=False, + new_callable=None, **kwargs + ): + """ + patch.object(target, attribute, new=DEFAULT, spec=None, create=False, + mocksignature=False, spec_set=None, autospec=False, + new_callable=None, **kwargs) + + patch the named member (`attribute`) on an object (`target`) with a mock + object. + + `patch.object` can be used as a decorator, class decorator or a context + manager. Arguments `new`, `spec`, `create`, `mocksignature`, `spec_set`, + `autospec` and `new_callable` have the same meaning as for `patch`. Like + `patch`, `patch.object` takes arbitrary keyword arguments for configuring + the mock object it creates. + + When used as a class decorator `patch.object` honours `patch.TEST_PREFIX` + for choosing which methods to wrap. + """ + getter = lambda: target + return _patch( + getter, attribute, new, spec, create, mocksignature, + spec_set, autospec, new_callable, kwargs + ) + + +def _patch_multiple(target, spec=None, create=False, + mocksignature=False, spec_set=None, autospec=False, + new_callable=None, **kwargs + ): + """Perform multiple patches in a single call. It takes the object to be + patched (either as an object or a string to fetch the object by importing) + and keyword arguments for the patches:: + + with patch.multiple(settings, FIRST_PATCH='one', SECOND_PATCH='two'): + ... + + Use `DEFAULT` as the value if you want `patch.multiple` to create + mocks for you. In this case the created mocks are passed into a decorated + function by keyword, and a dictionary is returned when `patch.multiple` is + used as a context manager. + + `patch.multiple` can be used as a decorator, class decorator or a context + manager. The arguments `spec`, `spec_set`, `create`, `mocksignature`, + `autospec` and `new_callable` have the same meaning as for `patch`. These + arguments will be applied to *all* patches done by `patch.multiple`. + + When used as a class decorator `patch.multiple` honours `patch.TEST_PREFIX` + for choosing which methods to wrap. + """ + if type(target) in (unicode, str): + getter = lambda: _importer(target) + else: + getter = lambda: target + + if not kwargs: + raise ValueError( + 'Must supply at least one keyword argument with patch.multiple' + ) + # need to wrap in a list for python 3, where items is a view + items = list(kwargs.items()) + attribute, new = items[0] + patcher = _patch( + getter, attribute, new, spec, create, mocksignature, spec_set, + autospec, new_callable, {} + ) + patcher.attribute_name = attribute + for attribute, new in items[1:]: + this_patcher = _patch( + getter, attribute, new, spec, create, mocksignature, spec_set, + autospec, new_callable, {} + ) + this_patcher.attribute_name = attribute + patcher.additional_patchers.append(this_patcher) + return patcher + + +def patch( + target, new=DEFAULT, spec=None, create=False, + mocksignature=False, spec_set=None, autospec=False, + new_callable=None, **kwargs + ): + """ + `patch` acts as a function decorator, class decorator or a context + manager. Inside the body of the function or with statement, the `target` + (specified in the form `'package.module.ClassName'`) is patched + with a `new` object. When the function/with statement exits the patch is + undone. + + The `target` is imported and the specified attribute patched with the new + object, so it must be importable from the environment you are calling the + decorator from. The target is imported when the decorated function is + executed, not at decoration time. + + If `new` is omitted, then a new `MagicMock` is created and passed in as an + extra argument to the decorated function. + + The `spec` and `spec_set` keyword arguments are passed to the `MagicMock` + if patch is creating one for you. + + In addition you can pass `spec=True` or `spec_set=True`, which causes + patch to pass in the object being mocked as the spec/spec_set object. + + `new_callable` allows you to specify a different class, or callable object, + that will be called to create the `new` object. By default `MagicMock` is + used. + + A more powerful form of `spec` is `autospec`. If you set `autospec=True` + then the mock with be created with a spec from the object being replaced. + All attributes of the mock will also have the spec of the corresponding + attribute of the object being replaced. Methods and functions being mocked + will have their arguments checked and will raise a `TypeError` if they are + called with the wrong signature (similar to `mocksignature`). For mocks + replacing a class, their return value (the 'instance') will have the same + spec as the class. + + Instead of `autospec=True` you can pass `autospec=some_object` to use an + arbitrary object as the spec instead of the one being replaced. + + If `mocksignature` is True then the patch will be done with a function + created by mocking the one being replaced. If the object being replaced is + a class then the signature of `__init__` will be copied. If the object + being replaced is a callable object then the signature of `__call__` will + be copied. + + By default `patch` will fail to replace attributes that don't exist. If + you pass in `create=True`, and the attribute doesn't exist, patch will + create the attribute for you when the patched function is called, and + delete it again afterwards. This is useful for writing tests against + attributes that your production code creates at runtime. It is off by by + default because it can be dangerous. With it switched on you can write + passing tests against APIs that don't actually exist! + + Patch can be used as a `TestCase` class decorator. It works by + decorating each test method in the class. This reduces the boilerplate + code when your test methods share a common patchings set. `patch` finds + tests by looking for method names that start with `patch.TEST_PREFIX`. + By default this is `test`, which matches the way `unittest` finds tests. + You can specify an alternative prefix by setting `patch.TEST_PREFIX`. + + Patch can be used as a context manager, with the with statement. Here the + patching applies to the indented block after the with statement. If you + use "as" then the patched object will be bound to the name after the + "as"; very useful if `patch` is creating a mock object for you. + + `patch` takes arbitrary keyword arguments. These will be passed to + the `Mock` (or `new_callable`) on construction. + + `patch.dict(...)`, `patch.multiple(...)` and `patch.object(...)` are + available for alternate use-cases. + """ + getter, attribute = _get_target(target) + return _patch( + getter, attribute, new, spec, create, mocksignature, + spec_set, autospec, new_callable, kwargs + ) + + +class _patch_dict(object): + """ + Patch a dictionary, or dictionary like object, and restore the dictionary + to its original state after the test. + + `in_dict` can be a dictionary or a mapping like container. If it is a + mapping then it must at least support getting, setting and deleting items + plus iterating over keys. + + `in_dict` can also be a string specifying the name of the dictionary, which + will then be fetched by importing it. + + `values` can be a dictionary of values to set in the dictionary. `values` + can also be an iterable of `(key, value)` pairs. + + If `clear` is True then the dictionary will be cleared before the new + values are set. + + `patch.dict` can also be called with arbitrary keyword arguments to set + values in the dictionary:: + + with patch.dict('sys.modules', mymodule=Mock(), other_module=Mock()): + ... + + `patch.dict` can be used as a context manager, decorator or class + decorator. When used as a class decorator `patch.dict` honours + `patch.TEST_PREFIX` for choosing which methods to wrap. + """ + + def __init__(self, in_dict, values=(), clear=False, **kwargs): + if isinstance(in_dict, basestring): + in_dict = _importer(in_dict) + self.in_dict = in_dict + # support any argument supported by dict(...) constructor + self.values = dict(values) + self.values.update(kwargs) + self.clear = clear + self._original = None + + + def __call__(self, f): + if isinstance(f, ClassTypes): + return self.decorate_class(f) + @wraps(f) + def _inner(*args, **kw): + self._patch_dict() + try: + return f(*args, **kw) + finally: + self._unpatch_dict() + + return _inner + + + def decorate_class(self, klass): + for attr in dir(klass): + attr_value = getattr(klass, attr) + if (attr.startswith(patch.TEST_PREFIX) and + hasattr(attr_value, "__call__")): + decorator = _patch_dict(self.in_dict, self.values, self.clear) + decorated = decorator(attr_value) + setattr(klass, attr, decorated) + return klass + + + def __enter__(self): + """Patch the dict.""" + self._patch_dict() + + + def _patch_dict(self): + values = self.values + in_dict = self.in_dict + clear = self.clear + + try: + original = in_dict.copy() + except AttributeError: + # dict like object with no copy method + # must support iteration over keys + original = {} + for key in in_dict: + original[key] = in_dict[key] + self._original = original + + if clear: + _clear_dict(in_dict) + + try: + in_dict.update(values) + except AttributeError: + # dict like object with no update method + for key in values: + in_dict[key] = values[key] + + + def _unpatch_dict(self): + in_dict = self.in_dict + original = self._original + + _clear_dict(in_dict) + + try: + in_dict.update(original) + except AttributeError: + for key in original: + in_dict[key] = original[key] + + + def __exit__(self, *args): + """Unpatch the dict.""" + self._unpatch_dict() + return False + + start = __enter__ + stop = __exit__ + + +def _clear_dict(in_dict): + try: + in_dict.clear() + except AttributeError: + keys = list(in_dict) + for key in keys: + del in_dict[key] + + +patch.object = _patch_object +patch.dict = _patch_dict +patch.multiple = _patch_multiple +patch.TEST_PREFIX = 'test' + +magic_methods = ( + "lt le gt ge eq ne " + "getitem setitem delitem " + "len contains iter " + "hash str sizeof " + "enter exit " + "divmod neg pos abs invert " + "complex int float index " + "trunc floor ceil " +) + +numerics = "add sub mul div floordiv mod lshift rshift and xor or pow " +inplace = ' '.join('i%s' % n for n in numerics.split()) +right = ' '.join('r%s' % n for n in numerics.split()) +extra = '' +if inPy3k: + extra = 'bool next ' +else: + extra = 'unicode long nonzero oct hex truediv rtruediv ' + +# not including __prepare__, __instancecheck__, __subclasscheck__ +# (as they are metaclass methods) +# __del__ is not supported at all as it causes problems if it exists + +_non_defaults = set('__%s__' % method for method in [ + 'cmp', 'getslice', 'setslice', 'coerce', 'subclasses', + 'format', 'get', 'set', 'delete', 'reversed', + 'missing', 'reduce', 'reduce_ex', 'getinitargs', + 'getnewargs', 'getstate', 'setstate', 'getformat', + 'setformat', 'repr', 'dir' +]) + + +def _get_method(name, func): + "Turns a callable object (like a mock) into a real function" + def method(self, *args, **kw): + return func(self, *args, **kw) + method.__name__ = name + return method + + +_magics = set( + '__%s__' % method for method in + ' '.join([magic_methods, numerics, inplace, right, extra]).split() +) + +_all_magics = _magics | _non_defaults + +_unsupported_magics = set([ + '__getattr__', '__setattr__', + '__init__', '__new__', '__prepare__' + '__instancecheck__', '__subclasscheck__', + '__del__' +]) + +_calculate_return_value = { + '__hash__': lambda self: object.__hash__(self), + '__str__': lambda self: object.__str__(self), + '__sizeof__': lambda self: object.__sizeof__(self), + '__unicode__': lambda self: unicode(object.__str__(self)), +} + +_return_values = { + '__int__': 1, + '__contains__': False, + '__len__': 0, + '__exit__': False, + '__complex__': 1j, + '__float__': 1.0, + '__bool__': True, + '__nonzero__': True, + '__oct__': '1', + '__hex__': '0x1', + '__long__': long(1), + '__index__': 1, +} + + +def _get_eq(self): + def __eq__(other): + ret_val = self.__eq__._mock_return_value + if ret_val is not DEFAULT: + return ret_val + return self is other + return __eq__ + +def _get_ne(self): + def __ne__(other): + if self.__ne__._mock_return_value is not DEFAULT: + return DEFAULT + return self is not other + return __ne__ + +def _get_iter(self): + def __iter__(): + ret_val = self.__iter__._mock_return_value + if ret_val is DEFAULT: + return iter([]) + # if ret_val was already an iterator, then calling iter on it should + # return the iterator unchanged + return iter(ret_val) + return __iter__ + +_side_effect_methods = { + '__eq__': _get_eq, + '__ne__': _get_ne, + '__iter__': _get_iter, +} + + + +def _set_return_value(mock, method, name): + fixed = _return_values.get(name, DEFAULT) + if fixed is not DEFAULT: + method.return_value = fixed + return + + return_calulator = _calculate_return_value.get(name) + if return_calulator is not None: + try: + return_value = return_calulator(mock) + except AttributeError: + # XXXX why do we return AttributeError here? + # set it as a side_effect instead? + return_value = AttributeError(name) + method.return_value = return_value + return + + side_effector = _side_effect_methods.get(name) + if side_effector is not None: + method.side_effect = side_effector(mock) + + + +class MagicMixin(object): + def __init__(self, *args, **kw): + _super(MagicMixin, self).__init__(*args, **kw) + self._mock_set_magics() + + + def _mock_set_magics(self): + these_magics = _magics + + if self._mock_methods is not None: + these_magics = _magics.intersection(self._mock_methods) + + remove_magics = set() + remove_magics = _magics - these_magics + + for entry in remove_magics: + if entry in type(self).__dict__: + # remove unneeded magic methods + delattr(self, entry) + + # don't overwrite existing attributes if called a second time + these_magics = these_magics - set(type(self).__dict__) + + _type = type(self) + for entry in these_magics: + setattr(_type, entry, MagicProxy(entry, self)) + + + +class NonCallableMagicMock(MagicMixin, NonCallableMock): + """A version of `MagicMock` that isn't callable.""" + def mock_add_spec(self, spec, spec_set=False): + """Add a spec to a mock. `spec` can either be an object or a + list of strings. Only attributes on the `spec` can be fetched as + attributes from the mock. + + If `spec_set` is True then only attributes on the spec can be set.""" + self._mock_add_spec(spec, spec_set) + self._mock_set_magics() + + + +class MagicMock(MagicMixin, Mock): + """ + MagicMock is a subclass of Mock with default implementations + of most of the magic methods. You can use MagicMock without having to + configure the magic methods yourself. + + If you use the `spec` or `spec_set` arguments then *only* magic + methods that exist in the spec will be created. + + Attributes and the return value of a `MagicMock` will also be `MagicMocks`. + """ + def mock_add_spec(self, spec, spec_set=False): + """Add a spec to a mock. `spec` can either be an object or a + list of strings. Only attributes on the `spec` can be fetched as + attributes from the mock. + + If `spec_set` is True then only attributes on the spec can be set.""" + self._mock_add_spec(spec, spec_set) + self._mock_set_magics() + + + +class MagicProxy(object): + def __init__(self, name, parent): + self.name = name + self.parent = parent + + def __call__(self, *args, **kwargs): + m = self.create_mock() + return m(*args, **kwargs) + + def create_mock(self): + entry = self.name + parent = self.parent + m = parent._get_child_mock(name=entry, _new_name=entry, + _new_parent=parent) + setattr(parent, entry, m) + _set_return_value(parent, m, entry) + return m + + def __get__(self, obj, _type=None): + return self.create_mock() + + + +class _ANY(object): + "A helper object that compares equal to everything." + + def __eq__(self, other): + return True + + def __ne__(self, other): + return False + + def __repr__(self): + return '' + +ANY = _ANY() + + + +def _format_call_signature(name, args, kwargs): + message = '%s(%%s)' % name + formatted_args = '' + args_string = ', '.join([repr(arg) for arg in args]) + kwargs_string = ', '.join([ + '%s=%r' % (key, value) for key, value in kwargs.items() + ]) + if args_string: + formatted_args = args_string + if kwargs_string: + if formatted_args: + formatted_args += ', ' + formatted_args += kwargs_string + + return message % formatted_args + + + +class _Call(tuple): + """ + A tuple for holding the results of a call to a mock, either in the form + `(args, kwargs)` or `(name, args, kwargs)`. + + If args or kwargs are empty then a call tuple will compare equal to + a tuple without those values. This makes comparisons less verbose:: + + _Call(('name', (), {})) == ('name',) + _Call(('name', (1,), {})) == ('name', (1,)) + _Call(((), {'a': 'b'})) == ({'a': 'b'},) + + The `_Call` object provides a useful shortcut for comparing with call:: + + _Call(((1, 2), {'a': 3})) == call(1, 2, a=3) + _Call(('foo', (1, 2), {'a': 3})) == call.foo(1, 2, a=3) + + If the _Call has no name then it will match any name. + """ + def __new__(cls, value=(), name=None, parent=None, two=False, + from_kall=True): + name = '' + args = () + kwargs = {} + _len = len(value) + if _len == 3: + name, args, kwargs = value + elif _len == 2: + first, second = value + if isinstance(first, basestring): + name = first + if isinstance(second, tuple): + args = second + else: + kwargs = second + else: + args, kwargs = first, second + elif _len == 1: + value, = value + if isinstance(value, basestring): + name = value + elif isinstance(value, tuple): + args = value + else: + kwargs = value + + if two: + return tuple.__new__(cls, (args, kwargs)) + + return tuple.__new__(cls, (name, args, kwargs)) + + + def __init__(self, value=(), name=None, parent=None, two=False, + from_kall=True): + self.name = name + self.parent = parent + self.from_kall = from_kall + + + def __eq__(self, other): + if other is ANY: + return True + try: + len_other = len(other) + except TypeError: + return False + + self_name = '' + if len(self) == 2: + self_args, self_kwargs = self + else: + self_name, self_args, self_kwargs = self + + other_name = '' + if len_other == 0: + other_args, other_kwargs = (), {} + elif len_other == 3: + other_name, other_args, other_kwargs = other + elif len_other == 1: + value, = other + if isinstance(value, tuple): + other_args = value + other_kwargs = {} + elif isinstance(value, basestring): + other_name = value + other_args, other_kwargs = (), {} + else: + other_args = () + other_kwargs = value + else: + # len 2 + # could be (name, args) or (name, kwargs) or (args, kwargs) + first, second = other + if isinstance(first, basestring): + other_name = first + if isinstance(second, tuple): + other_args, other_kwargs = second, {} + else: + other_args, other_kwargs = (), second + else: + other_args, other_kwargs = first, second + + if self_name and other_name != self_name: + return False + + # this order is important for ANY to work! + return (other_args, other_kwargs) == (self_args, self_kwargs) + + + def __ne__(self, other): + return not self.__eq__(other) + + + def __call__(self, *args, **kwargs): + if self.name is None: + return _Call(('', args, kwargs), name='()') + + name = self.name + '()' + return _Call((self.name, args, kwargs), name=name, parent=self) + + + def __getattr__(self, attr): + if self.name is None: + return _Call(name=attr, from_kall=False) + name = '%s.%s' % (self.name, attr) + return _Call(name=name, parent=self, from_kall=False) + + + def __repr__(self): + if not self.from_kall: + name = self.name or 'call' + if name.startswith('()'): + name = 'call%s' % name + return name + + if len(self) == 2: + name = 'call' + args, kwargs = self + else: + name, args, kwargs = self + if not name: + name = 'call' + elif not name.startswith('()'): + name = 'call.%s' % name + else: + name = 'call%s' % name + return _format_call_signature(name, args, kwargs) + + + def call_list(self): + """For a call object that represents multiple calls, `call_list` + returns a list of all the intermediate calls as well as the + final call.""" + vals = [] + thing = self + while thing is not None: + if thing.from_kall: + vals.append(thing) + thing = thing.parent + return _CallList(reversed(vals)) + + +call = _Call(from_kall=False) + + + +def create_autospec(spec, spec_set=False, instance=False, _parent=None, + _name=None, **kwargs): + """Create a mock object using another object as a spec. Attributes on the + mock will use the corresponding attribute on the `spec` object as their + spec. + + Functions or methods being mocked will have their arguments checked in a + similar way to `mocksignature` to check that they are called with the + correct signature. + + If `spec_set` is True then attempting to set attributes that don't exist + on the spec object will raise an `AttributeError`. + + If a class is used as a spec then the return value of the mock (the + instance of the class) will have the same spec. You can use a class as the + spec for an instance object by passing `instance=True`. The returned mock + will only be callable if instances of the mock are callable. + + `create_autospec` also takes arbitrary keyword arguments that are passed to + the constructor of the created mock.""" + if _is_list(spec): + # can't pass a list instance to the mock constructor as it will be + # interpreted as a list of strings + spec = type(spec) + + is_type = isinstance(spec, ClassTypes) + + _kwargs = {'spec': spec} + if spec_set: + _kwargs = {'spec_set': spec} + elif spec is None: + # None we mock with a normal mock without a spec + _kwargs = {} + + _kwargs.update(kwargs) + + Klass = MagicMock + if type(spec) in DescriptorTypes: + # descriptors don't have a spec + # because we don't know what type they return + _kwargs = {} + elif not _callable(spec): + Klass = NonCallableMagicMock + elif is_type and instance and not _instance_callable(spec): + Klass = NonCallableMagicMock + + _new_name = _name + if _parent is None: + # for a top level object no _new_name should be set + _new_name = '' + + mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name, + name=_name, **_kwargs) + + if isinstance(spec, FunctionTypes): + # should only happen at the top level because we don't + # recurse for functions + mock = _set_signature(mock, spec) + else: + _check_signature(spec, mock, is_type, instance) + + if _parent is not None and not instance: + _parent._mock_children[_name] = mock + + if is_type and not instance and 'return_value' not in kwargs: + # XXXX could give a name to the return_value mock? + mock.return_value = create_autospec(spec, spec_set, instance=True, + _name='()', _parent=mock) + + for entry in dir(spec): + if _is_magic(entry): + # MagicMock already does the useful magic methods for us + continue + + if isinstance(spec, FunctionTypes) and entry in FunctionAttributes: + # allow a mock to actually be a function from mocksignature + continue + + # XXXX do we need a better way of getting attributes without + # triggering code execution (?) Probably not - we need the actual + # object to mock it so we would rather trigger a property than mock + # the property descriptor. Likewise we want to mock out dynamically + # provided attributes. + # XXXX what about attributes that raise exceptions on being fetched + # we could be resilient against it, or catch and propagate the + # exception when the attribute is fetched from the mock + original = getattr(spec, entry) + + kwargs = {'spec': original} + if spec_set: + kwargs = {'spec_set': original} + + if not isinstance(original, FunctionTypes): + new = _SpecState(original, spec_set, mock, entry, instance) + mock._mock_children[entry] = new + else: + parent = mock + if isinstance(spec, FunctionTypes): + parent = mock.mock + + new = MagicMock(parent=parent, name=entry, _new_name=entry, + _new_parent=parent, **kwargs) + mock._mock_children[entry] = new + skipfirst = _must_skip(spec, entry, is_type) + _check_signature(original, new, skipfirst=skipfirst) + + # so functions created with mocksignature become instance attributes, + # *plus* their underlying mock exists in _mock_children of the parent + # mock. Adding to _mock_children may be unnecessary where we are also + # setting as an instance attribute? + if isinstance(new, FunctionTypes): + setattr(mock, entry, new) + + return mock + + +def _must_skip(spec, entry, is_type): + if not isinstance(spec, ClassTypes): + if entry in getattr(spec, '__dict__', {}): + # instance attribute - shouldn't skip + return False + # can't use type because of old style classes + spec = spec.__class__ + if not hasattr(spec, '__mro__'): + # old style class: can't have descriptors anyway + return is_type + + for klass in spec.__mro__: + result = klass.__dict__.get(entry, DEFAULT) + if result is DEFAULT: + continue + if isinstance(result, (staticmethod, classmethod)): + return False + return is_type + + # shouldn't get here unless function is a dynamically provided attribute + # XXXX untested behaviour + return is_type + + +def _get_class(obj): + try: + return obj.__class__ + except AttributeError: + # in Python 2, _sre.SRE_Pattern objects have no __class__ + return type(obj) + + +class _SpecState(object): + + def __init__(self, spec, spec_set=False, parent=None, + name=None, ids=None, instance=False): + self.spec = spec + self.ids = ids + self.spec_set = spec_set + self.parent = parent + self.instance = instance + self.name = name + + +FunctionTypes = ( + # python function + type(create_autospec), + # instance method + type(ANY.__eq__), + # unbound method + type(_ANY.__eq__), +) + +FunctionAttributes = set([ + 'func_closure', + 'func_code', + 'func_defaults', + 'func_dict', + 'func_doc', + 'func_globals', + 'func_name', +]) diff --git a/script/test/diffcalc (copy)/model/README.txt b/script/test/diffcalc (copy)/model/README.txt new file mode 100644 index 0000000..45d8fcb --- /dev/null +++ b/script/test/diffcalc (copy)/model/README.txt @@ -0,0 +1,13 @@ +Setting up the sixc vrml model to work via Epics pv's from b16's simulation running on the office network. + + +1. Synoptic +$ launcher --port 6064 + +2. Shell +export EPICS_CA_REPEATER_PORT=6065 +export EPICS_CA_SERVER_PORT=6064 + +3. Vrml viewer +export EPICS_CA_SERVER_PORT=6064 +dls-vrml-epics-viewer.py fivec.wrl fivec.wcfg diff --git a/script/test/diffcalc (copy)/model/fivec.fxw b/script/test/diffcalc (copy)/model/fivec.fxw new file mode 100644 index 0000000000000000000000000000000000000000..de9ae059d49ee17dcd1874c2ec0bf10fd0ca01ed GIT binary patch literal 120483 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 literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/model/sixc.wcfg b/script/test/diffcalc (copy)/model/sixc.wcfg new file mode 100644 index 0000000..2fa6961 --- /dev/null +++ b/script/test/diffcalc (copy)/model/sixc.wcfg @@ -0,0 +1,6 @@ +dad_alpha_frame rotation pv["DIFFSIM:SIXC:ALPHA.RBV"] +dad_delta_frame rotation pv["DIFFSIM:SIXC:DELTA.RBV"] +dad_gamma_frame rotation pv["DIFFSIM:SIXC:GAMMA.RBV"] +dad_omega_frame rotation pv["DIFFSIM:SIXC:OMEGA.RBV"] +dad_chi_frame rotation pv["DIFFSIM:SIXC:CHI.RBV"] +dad_phi_frame rotation pv["DIFFSIM:SIXC:PHI.RBV"] diff --git a/script/test/diffcalc (copy)/model/sixc.wrl b/script/test/diffcalc (copy)/model/sixc.wrl new file mode 100644 index 0000000..32ad769 --- /dev/null +++ b/script/test/diffcalc (copy)/model/sixc.wrl @@ -0,0 +1,1101 @@ +#VRML V2.0 utf8 +WorldInfo { + title "fourc" + info ["This Web3D Content was created with Flux Studio, a Web3D authoring tool" + "www.mediamachines.com" + "This Web3D Content was created with Flux Studio, a Web3D authoring tool"] +} +## Vizthumbnail Thumb_fourc_wrl36454531255104963.jpg +DirectionalLight { + intensity 1.000 + ambientIntensity 0.500 + direction -.621 -.23944 -.74634 + color 1 1 1 + on TRUE +} +DirectionalLight { + intensity 1.000 + ambientIntensity 0.000 + direction -.01373 -.46642 -.88446 + color 1 1 1 + on TRUE +} +DEF lab_x Shape { + appearance Appearance { + material DEF Rust Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .50196 .50196 .50196 + } + } + geometry DEF GeoCylinder15 Cylinder { + height 60.000 + radius 0.020 + } +} +DEF dad_lab_z Transform { + rotation 1 0 0 1.571 + children [ + DEF lab_z Shape { + appearance Appearance { + material USE Rust + } + geometry DEF GeoCylinder17 Cylinder { + height 40.000 + radius 0.020 + } + } + ] +} +DEF dad_lab_beam Transform { + translation 10 0 0 + rotation 0 0 1 1.571 + children [ + DEF lab_beam Shape { + appearance Appearance { + material DEF White Material { + ambientIntensity 0.500 + shininess 0.100 + transparency 0.500 + diffuseColor 1 1 1 + emissiveColor 1 1 1 + } + } + geometry DEF GeoCylinder20 Cylinder { + height 20.000 + radius 0.050 + } + } + ] +} +DEF dad_lab_beam0 Transform { + translation 20 0 0 + rotation 0 0 1 1.571 + children [ + DEF lab_beam0 Shape { + appearance Appearance { + material DEF Shiny_Black Material { + ambientIntensity 0.200 + shininess 0.100 + diffuseColor 1 1 1 + } + } + geometry DEF GeoCylinder22 Cylinder { + height 4.000 + radius 0.200 + } + } + ] +} +DEF dad_Cone2 Transform { + translation 17.5 0 0 + rotation 0 0 1 1.571 + children [ + DEF Cone2 Shape { + appearance Appearance { + material USE Shiny_Black + } + geometry DEF GeoCone2 Cone { + height 1.500 + bottomRadius 0.500 + } + } + ] +} +DEF dad_lab_y Transform { + rotation 0 0 -1 1.571 + children [ + DEF lab_y Shape { + appearance Appearance { + material USE Rust + } + geometry DEF GeoCylinder16 Cylinder { + height 60.000 + radius 0.020 + } + } + ] +} +DEF dad_alpha_frame Transform { + rotation 0 1 0 .175 + children [ + DEF dad_alpha_base Transform { + translation 0 -20.5 0 + children [ + DEF alpha_base Shape { + appearance Appearance { + material DEF Red Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .50196 0 0 + } + } + geometry DEF GeoCylinder1 Cylinder { + height 2.000 + radius 6.000 + } + } + ] + } + DEF dad_lab_post Transform { + translation 0 -10 -11 + children [ + DEF alpha_post Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox1 Box { + size 4 20 1 + } + } + ] + } + DEF dad_omega_frame Transform { + rotation 0 0 1 -.175 + children [ + DEF dad_chi_frame Transform { + rotation 1 0 0 -.785 + children [ + DEF dad_phi_frame Transform { + rotation 0 0 1 -.349 + children [ + DEF dad_Cylinder7 Transform { + translation 0 0 -4 + rotation 1 0 0 1.571 + children [ + DEF Cylinder7 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoCylinder7 Cylinder { + height 2.000 + radius 1.000 + } + } + ] + } + DEF dad_Cylinder8 Transform { + translation 0 0 -3.5 + rotation 1 0 0 1.571 + children [ + DEF Cylinder8 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoCylinder8 Cylinder { + height 6.000 + radius 0.250 + } + } + ] + } + DEF Box3 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoBox3 Box { + size 1 1 1 + } + } + DEF dad_Box11 Transform { + translation 0 1 -4 + children [ + DEF Box11 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoBox11 Box { + size .25 .5 2 + } + } + ] + } + DEF Box4 Shape { + appearance Appearance { + material DEF White_wire Material { + emissiveColor 1 1 1 + } + } + geometry IndexedLineSet { + coordIndex [ + 0 1 2 0 + -1 0 2 3 + 0 -1 1 5 + 6 1 -1 1 + 6 2 1 -1 + 2 6 7 2 + -1 2 7 3 + 2 -1 3 7 + 4 3 -1 3 + 4 0 3 -1 + 0 4 5 0 + -1 0 5 1 + 0 -1 6 5 + 4 6 -1 6 + 4 7 6 -1 + ] + coord Coordinate { + point [ + -.5 .5 -.5 + -.5 .5 .5 + .5 .5 .5 + .5 .5 -.5 + -.5 -.5 -.5 + -.5 -.5 .5 + .5 -.5 .5 + .5 -.5 -.5 + ] + } + } + } + ] + } + DEF dad_Box2 Transform { + translation 0 0 -5.5 + children [ + DEF Box2 Shape { + appearance Appearance { + material DEF Purple Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .9176 0 .874 + } + } + geometry DEF GeoBox2 Box { + size 2.8 2.8 1.5 + } + } + ] + } + DEF dad_Box6 Transform { + translation 0 1.25 -5.5 + children [ + DEF Box6 Shape { + appearance Appearance { + material USE Purple + } + geometry DEF GeoBox21 Box { + size .25 1 1.5 + } + } + ] + } + ] + } + DEF dad_Cylinder2 Transform { + rotation 0 0 1 1.571 + scale 1.1 1.5 1.1 + children [ + DEF Cylinder2 Shape { + appearance Appearance { + material DEF Green Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .00784 .65098 .02353 + } + } + geometry DEF Cylinder2_Geo IndexedFaceSet { + creaseAngle 0.524 + coordIndex [ + 49 50 48 -1 + 48 50 51 -1 + 48 51 47 -1 + 47 51 20 -1 + 47 20 21 -1 + 12 13 56 -1 + 56 13 14 -1 + 56 14 55 -1 + 55 14 15 -1 + 55 15 54 -1 + 54 15 16 -1 + 54 16 53 -1 + 53 16 17 -1 + 53 17 52 -1 + 52 17 18 -1 + 52 18 51 -1 + 51 18 19 -1 + 51 19 20 -1 + 5 6 62 -1 + 62 6 7 -1 + 62 7 61 -1 + 61 7 8 -1 + 61 8 60 -1 + 60 8 9 -1 + 60 9 59 -1 + 59 9 10 -1 + 59 10 58 -1 + 58 10 11 -1 + 58 11 57 -1 + 57 11 12 -1 + 57 12 56 -1 + 5 63 4 -1 + 4 63 64 -1 + 4 64 3 -1 + 3 64 1 -1 + 3 1 2 -1 + 2 1 35 -1 + 2 35 36 -1 + 28 41 27 -1 + 27 41 42 -1 + 27 42 26 -1 + 26 42 43 -1 + 26 43 25 -1 + 25 43 44 -1 + 25 44 24 -1 + 24 44 45 -1 + 24 45 23 -1 + 23 45 46 -1 + 23 46 22 -1 + 22 46 47 -1 + 22 47 21 -1 + 34 36 33 -1 + 33 36 37 -1 + 33 37 32 -1 + 32 37 38 -1 + 32 38 31 -1 + 31 38 39 -1 + 31 39 30 -1 + 30 39 40 -1 + 30 40 29 -1 + 29 40 41 -1 + 29 41 28 -1 + 1 64 0 -1 + 2 36 34 -1 + 63 5 62 -1 + 95 96 97 -1 + 97 96 65 -1 + 97 65 129 -1 + 129 65 66 -1 + 129 66 128 -1 + 128 66 67 -1 + 128 67 127 -1 + 127 67 126 -1 + 104 105 88 -1 + 88 105 106 -1 + 88 106 87 -1 + 87 106 107 -1 + 87 107 86 -1 + 86 107 108 -1 + 86 108 85 -1 + 85 108 109 -1 + 85 109 84 -1 + 84 109 110 -1 + 84 110 83 -1 + 83 110 111 -1 + 83 111 82 -1 + 82 111 112 -1 + 82 112 113 -1 + 98 99 93 -1 + 93 99 100 -1 + 93 100 92 -1 + 92 100 101 -1 + 92 101 91 -1 + 91 101 102 -1 + 91 102 90 -1 + 90 102 103 -1 + 90 103 89 -1 + 89 103 104 -1 + 89 104 88 -1 + 94 95 97 -1 + 94 97 98 -1 + 94 98 93 -1 + 81 82 78 -1 + 81 78 79 -1 + 81 79 80 -1 + 78 82 113 -1 + 78 113 114 -1 + 78 114 115 -1 + 78 115 77 -1 + 120 73 119 -1 + 119 73 74 -1 + 119 74 118 -1 + 118 74 75 -1 + 118 75 117 -1 + 117 75 76 -1 + 117 76 116 -1 + 116 76 77 -1 + 116 77 115 -1 + 126 68 125 -1 + 125 68 69 -1 + 125 69 124 -1 + 124 69 70 -1 + 124 70 123 -1 + 123 70 71 -1 + 123 71 122 -1 + 122 71 72 -1 + 122 72 121 -1 + 121 72 73 -1 + 121 73 120 -1 + 126 67 68 -1 + 0 64 65 -1 + 0 65 96 -1 + 64 63 66 -1 + 64 66 65 -1 + 63 62 67 -1 + 63 67 66 -1 + 62 61 68 -1 + 62 68 67 -1 + 61 60 69 -1 + 61 69 68 -1 + 60 59 70 -1 + 60 70 69 -1 + 59 58 71 -1 + 59 71 70 -1 + 58 57 72 -1 + 58 72 71 -1 + 57 56 73 -1 + 57 73 72 -1 + 56 55 74 -1 + 56 74 73 -1 + 55 54 75 -1 + 55 75 74 -1 + 54 53 76 -1 + 54 76 75 -1 + 53 52 77 -1 + 53 77 76 -1 + 52 51 78 -1 + 52 78 77 -1 + 51 50 79 -1 + 51 79 78 -1 + 50 49 80 -1 + 50 80 79 -1 + 49 48 81 -1 + 49 81 80 -1 + 48 47 82 -1 + 48 82 81 -1 + 47 46 83 -1 + 47 83 82 -1 + 46 45 84 -1 + 46 84 83 -1 + 45 44 85 -1 + 45 85 84 -1 + 44 43 86 -1 + 44 86 85 -1 + 43 42 87 -1 + 43 87 86 -1 + 42 41 88 -1 + 42 88 87 -1 + 41 40 89 -1 + 41 89 88 -1 + 40 39 90 -1 + 40 90 89 -1 + 39 38 91 -1 + 39 91 90 -1 + 38 37 92 -1 + 38 92 91 -1 + 37 36 93 -1 + 37 93 92 -1 + 36 35 94 -1 + 36 94 93 -1 + 35 1 95 -1 + 35 95 94 -1 + 1 0 96 -1 + 1 96 95 -1 + ] + coord DEF Cylinder2_Coord Coordinate { + point [ + 0 1 -6 + -1.17054 1 -5.88471 + -1.07212 1 -5.3899 + -1.05189 1 -5.39596 + .02145 1 -5.49809 + 1.09408 1 -5.38886 + 2.1248 1 -5.07248 + 3.07398 1 -4.56108 + 3.90517 1 -3.87434 + 4.5864 1 -3.03864 + 5.0915 1 -2.08609 + 5.40107 1 -1.0533 + 5.5032 1 .02003 + 5.39398 1 1.09267 + 5.07759 1 2.12338 + 4.5662 1 3.07257 + 3.87945 1 3.90375 + 3.04375 1 4.58498 + 2.0912 1 5.09009 + 1.05842 1 5.39966 + -.01492 1 5.50179 + -1.08755 1 5.39256 + -2.11827 1 5.07617 + -3.06746 1 4.56478 + -3.89864 1 3.87804 + -4.57987 1 3.04233 + -5.08498 1 2.08979 + -5.39454 1 1.057 + -5.49668 1 -.01633 + -5.38745 1 -1.08897 + -5.07106 1 -2.11968 + -4.55967 1 -3.06887 + -3.87292 1 -3.90005 + -3.03722 1 -4.58129 + -2.08467 1 -5.08639 + -2.2961 1 -5.54328 + -3.33342 1 -4.98882 + -4.24264 1 -4.24264 + -4.98882 1 -3.33342 + -5.54328 1 -2.2961 + -5.88471 1 -1.17054 + -6 1 -0 + -5.88471 1 1.17054 + -5.54328 1 2.2961 + -4.98882 1 3.33342 + -4.24264 1 4.24264 + -3.33342 1 4.98882 + -2.2961 1 5.54328 + -1.17054 1 5.88471 + -0 1 6 + 1.17054 1 5.88471 + 2.2961 1 5.54328 + 3.33342 1 4.98882 + 4.24264 1 4.24264 + 4.98882 1 3.33342 + 5.54328 1 2.2961 + 5.88471 1 1.17054 + 6 1 0 + 5.88471 1 -1.17054 + 5.54328 1 -2.2961 + 4.98882 1 -3.33342 + 4.24264 1 -4.24264 + 3.33342 1 -4.98882 + 2.2961 1 -5.54328 + 1.17054 1 -5.88471 + 1.17054 -1 -5.88471 + 2.2961 -1 -5.54328 + 3.33342 -1 -4.98882 + 4.24264 -1 -4.24264 + 4.98882 -1 -3.33342 + 5.54328 -1 -2.2961 + 5.88471 -1 -1.17054 + 6 -1 -0 + 5.88471 -1 1.17054 + 5.54328 -1 2.2961 + 4.98882 -1 3.33342 + 4.24264 -1 4.24264 + 3.33342 -1 4.98882 + 2.2961 -1 5.54328 + 1.17054 -1 5.88471 + -0 -1 6 + -1.17054 -1 5.88471 + -2.2961 -1 5.54328 + -3.33342 -1 4.98882 + -4.24264 -1 4.24264 + -4.98882 -1 3.33342 + -5.54328 -1 2.2961 + -5.88471 -1 1.17054 + -6 -1 -0 + -5.88471 -1 -1.17054 + -5.54328 -1 -2.2961 + -4.98882 -1 -3.33342 + -4.24264 -1 -4.24264 + -3.33342 -1 -4.98882 + -2.2961 -1 -5.54328 + -1.17054 -1 -5.88471 + 0 -1 -6 + 0 -1 -5.50037 + -1.05842 -1 -5.39966 + -2.0912 -1 -5.09009 + -3.04375 -1 -4.58498 + -3.87945 -1 -3.90375 + -4.5662 -1 -3.07257 + -5.07759 -1 -2.12338 + -5.39398 -1 -1.09267 + -5.5032 -1 -.02003 + -5.40107 -1 1.0533 + -5.0915 -1 2.08609 + -4.5864 -1 3.03864 + -3.90517 -1 3.87434 + -3.07398 -1 4.56108 + -2.1248 -1 5.07248 + -1.09408 -1 5.38886 + -.02145 -1 5.49809 + 1.05189 -1 5.39596 + 2.08467 -1 5.08639 + 3.03722 -1 4.58129 + 3.87292 -1 3.90005 + 4.55967 -1 3.06887 + 5.07106 -1 2.11968 + 5.38745 -1 1.08897 + 5.49668 -1 .01633 + 5.39454 -1 -1.057 + 5.08498 -1 -2.08979 + 4.57987 -1 -3.04233 + 3.89864 -1 -3.87804 + 3.06746 -1 -4.56478 + 2.11827 -1 -5.07617 + 1.08755 -1 -5.39256 + .01492 -1 -5.50179 + ] + } + } + } + ] + } + DEF dad_Cylinder3 Transform { + rotation 0 0 1 1.571 + scale 1.1 1.5 1.1 + children [ + DEF Cylinder3 Shape { + appearance Appearance { + material USE Green + } + geometry DEF Cylinder3_Geo IndexedFaceSet { + creaseAngle 0.524 + coordIndex [ + 0 1 2 -1 + 0 2 3 -1 + 4 5 1 -1 + 4 1 0 -1 + 6 7 5 -1 + 6 5 4 -1 + 8 9 7 -1 + 8 7 6 -1 + 10 11 9 -1 + 10 9 8 -1 + 12 13 11 -1 + 12 11 10 -1 + 14 15 13 -1 + 14 13 12 -1 + 16 17 15 -1 + 16 15 14 -1 + 18 19 17 -1 + 18 17 16 -1 + 20 21 19 -1 + 20 19 18 -1 + 22 23 21 -1 + 22 21 20 -1 + 24 25 23 -1 + 24 23 22 -1 + 26 27 25 -1 + 26 25 24 -1 + 28 29 27 -1 + 28 27 26 -1 + 30 31 29 -1 + 30 29 28 -1 + 32 33 31 -1 + 32 31 30 -1 + 34 35 33 -1 + 34 33 32 -1 + 36 37 35 -1 + 36 35 34 -1 + 38 39 37 -1 + 38 37 36 -1 + 40 41 39 -1 + 40 39 38 -1 + 42 43 41 -1 + 42 41 40 -1 + 44 45 43 -1 + 44 43 42 -1 + 46 47 45 -1 + 46 45 44 -1 + 48 49 47 -1 + 48 47 46 -1 + 50 51 49 -1 + 50 49 48 -1 + 52 53 51 -1 + 52 51 50 -1 + 54 55 53 -1 + 54 53 52 -1 + 56 57 55 -1 + 56 55 54 -1 + 58 59 57 -1 + 58 57 56 -1 + 60 61 59 -1 + 60 59 58 -1 + 62 63 61 -1 + 62 61 60 -1 + 3 2 63 -1 + 3 63 62 -1 + ] + coord DEF Cylinder3_Coord Coordinate { + point [ + 1.073 -1.00635 -5.39432 + 1.073 .99367 -5.39432 + 0 .98997 -5.5 + 0 -1.01006 -5.5 + 2.10476 -1.0024 -5.08134 + 2.10476 .99763 -5.08134 + 3.05564 -.99836 -4.57308 + 3.05564 1.00167 -4.57308 + 3.88909 -.99438 -3.88909 + 3.88909 1.00565 -3.88909 + 4.57308 -.99061 -3.05564 + 4.57308 1.00941 -3.05564 + 5.08134 -.98721 -2.10476 + 5.08134 1.01282 -2.10476 + 5.39432 -.9843 -1.073 + 5.39432 1.01573 -1.073 + 5.5 -.98199 -0 + 5.5 1.01803 -0 + 5.39432 -.98038 1.073 + 5.39432 1.01965 1.073 + 5.08134 -.97952 2.10476 + 5.08134 1.02051 2.10476 + 4.57308 -.97945 3.05564 + 4.57308 1.02058 3.05564 + 3.88909 -.98017 3.88909 + 3.88909 1.01986 3.88909 + 3.05564 -.98165 4.57308 + 3.05564 1.01838 4.57308 + 2.10476 -.98383 5.08134 + 2.10476 1.01619 5.08134 + 1.073 -.98664 5.39432 + 1.073 1.01338 5.39432 + -0 -.98997 5.5 + -0 1.01006 5.5 + -1.073 -.99367 5.39432 + -1.073 1.00635 5.39432 + -2.10476 -.99763 5.08134 + -2.10476 1.0024 5.08134 + -3.05564 -1.00167 4.57308 + -3.05564 .99836 4.57308 + -3.88909 -1.00565 3.88909 + -3.88909 .99438 3.88909 + -4.57308 -1.00941 3.05564 + -4.57308 .99061 3.05564 + -5.08134 -1.01282 2.10476 + -5.08134 .98721 2.10476 + -5.39432 -1.01573 1.073 + -5.39432 .9843 1.073 + -5.5 -1.01803 -0 + -5.5 .98199 -0 + -5.39432 -1.01965 -1.073 + -5.39432 .98038 -1.073 + -5.08134 -1.02051 -2.10476 + -5.08134 .97952 -2.10476 + -4.57308 -1.02058 -3.05564 + -4.57308 .97945 -3.05564 + -3.88909 -1.01986 -3.88909 + -3.88909 .98017 -3.88909 + -3.05564 -1.01838 -4.57308 + -3.05564 .98165 -4.57308 + -2.10476 -1.01619 -5.08134 + -2.10476 .98383 -5.08134 + -1.073 -1.01338 -5.39432 + -1.073 .98664 -5.39432 + ] + } + } + } + ] + } + DEF dad_Cylinder5 Transform { + translation 0 0 -8.4 + rotation 1 0 0 1.571 + children [ + DEF Cylinder5 Shape { + appearance Appearance { + material USE Green + } + geometry DEF GeoCylinder5 Cylinder { + height 4.250 + radius 2.000 + } + } + ] + } + DEF dad_Box10 Transform { + translation 0 2 -8.4 + children [ + DEF Box10 Shape { + appearance Appearance { + material USE Green + } + geometry DEF GeoBox10 Box { + size .25 1 4.25 + } + } + ] + } + ] + } + DEF dad_lab_posttop Transform { + translation 0 0 -11 + rotation -1 0 0 1.571 + children [ + DEF alpha_posttop Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoCylinder6 Cylinder { + height 1.000 + radius 2.000 + } + } + ] + } + DEF dad_alpha_arm Transform { + translation 0 -20.5 -5 + children [ + DEF alpha_arm Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox7 Box { + size 4 1 13 + } + } + ] + } + DEF dad_gamma_arm1 Transform { + translation -6 -20.5 0 + children [ + DEF gamma_arm1 Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox18 Box { + size 1 2 .25 + } + } + ] + } + DEF dad_Box12 Transform { + translation 0 2 -11 + children [ + DEF Box12 Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox12 Box { + size .25 1 1 + } + } + ] + } + ] +} +DEF dad_gamma_frame Transform { + rotation 0 1 0 .349 + children [ + DEF dad_lab_posttop0 Transform { + translation 0 0 -12.5 + rotation -1 0 0 1.571 + children [ + DEF gamma_posttop Shape { + appearance Appearance { + material DEF Blue Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor 0 0 .62745 + } + } + geometry DEF GeoCylinder4 Cylinder { + height 1.000 + radius 2.000 + } + } + ] + } + DEF dad_lab_post0 Transform { + translation 0 -11 -12.5 + children [ + DEF gamma_post Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox5 Box { + size 4 22 1 + } + } + ] + } + DEF dad_gamma_base Transform { + translation 0 -22.5 0 + children [ + DEF gamma_base Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoCylinder3 Cylinder { + height 2.000 + radius 8.000 + } + } + ] + } + DEF dad_gamma_arm Transform { + translation 0 -22.5 -6.5 + children [ + DEF gamma_arm Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox4 Box { + size 4 1 13 + } + } + ] + } + DEF dad_delta_frame Transform { + rotation 0 0 1 -.524 + children [ + DEF dad_delta_mount Transform { + translation 0 0 -15 + rotation -1 0 0 1.571 + children [ + DEF delta_mount Shape { + appearance Appearance { + material DEF Yellow Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .83529 .83529 0 + } + } + geometry DEF GeoCylinder2 Cylinder { + height 4.000 + radius 2.000 + } + } + ] + } + DEF dad_delta_arm Transform { + translation -10 0 -15 + children [ + DEF delta_arm Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoBox8 Box { + size 20 3 1 + } + } + ] + } + DEF dad_delta_arm0 Transform { + translation -20 0 -6.5 + children [ + DEF delta_arm0 Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoBox6 Box { + size 1 3 18 + } + } + ] + } + DEF dad_Cylinder14 Transform { + translation -18 0 0 + rotation .577 -.577 .577 2.094 + children [ + DEF Cylinder14 Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoCylinder14 Cylinder { + height 3.000 + radius 1.000 + } + } + ] + } + DEF dad_detector_beam Transform { + translation -10 0 0 + rotation 0 0 1 1.571 + children [ + DEF detector_beam Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoCylinder21 Cylinder { + height 20.000 + radius 0.050 + } + } + ] + } + DEF dad_Box13 Transform { + translation 0 2 -15 + children [ + DEF Box13 Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoBox13 Box { + size .25 1 4 + } + } + ] + } + ] + } + DEF dad_gamma_arm0 Transform { + translation -8 -22.5 0 + children [ + DEF gamma_arm0 Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox17 Box { + size 1 2 .25 + } + } + ] + } + DEF dad_Box14 Transform { + translation 0 2 -12.5 + children [ + DEF Box14 Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox14 Box { + size .25 1 1 + } + } + ] + } + ] +} +DEF VP Viewpoint { + description "VP" + jump TRUE + fieldOfView 1.165 + position 40 40 40 + orientation -.754 .657 -0 1.001 +} +DEF dad_gamma_arm2 Transform { + translation -9 -24.5 0 + children [ + DEF gamma_arm2 Shape { + appearance Appearance { + material DEF Black Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .20784 .20784 .20784 + } + } + geometry DEF GeoBox19 Box { + size 1 2 .25 + } + } + ] +} +DEF dad_gamma_base0 Transform { + translation 0 -24.5 0 + rotation 0 1 0 .349 + children [ + DEF gamma_base0 Shape { + appearance Appearance { + material USE Black + } + geometry DEF GeoCylinder9 Cylinder { + height 2.000 + radius 9.000 + } + } + ] +} +DEF dad_floor Transform { + translation 0 -25.5 0 + children [ + DEF floor Shape { + appearance Appearance { + material USE Black + } + geometry DEF GeoBox16 Box { + size 60 .2 40 + } + } + ] +} diff --git a/script/test/diffcalc (copy)/model/sixc_horizontal.fxw b/script/test/diffcalc (copy)/model/sixc_horizontal.fxw new file mode 100644 index 0000000000000000000000000000000000000000..9eea919011029bd4bd2d25f4349aab7105190105 GIT binary patch literal 128929 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 L3p>QbJPrImq{85u literal 0 HcmV?d00001 diff --git a/script/test/diffcalc (copy)/model/sixc_horizontal.wrl b/script/test/diffcalc (copy)/model/sixc_horizontal.wrl new file mode 100644 index 0000000..3ec9d25 --- /dev/null +++ b/script/test/diffcalc (copy)/model/sixc_horizontal.wrl @@ -0,0 +1,1106 @@ +#VRML V2.0 utf8 +WorldInfo { + title "fourc" + info ["This Web3D Content was created with Flux Studio, a Web3D authoring tool" + "www.mediamachines.com" + "This Web3D Content was created with Flux Studio, a Web3D authoring tool"] +} +## Vizthumbnail Thumb_sixc_horizontal_wrl8844311285236111.jpg +DEF dad_GROUND_ROTATED Transform { + rotation 1 0 0 1.571 + children [ + DirectionalLight { + intensity 1.000 + ambientIntensity 0.500 + direction -.621 -.23944 -.74634 + color 1 1 1 + on TRUE + } + DirectionalLight { + intensity 1.000 + ambientIntensity 0.000 + direction -.01373 -.46642 -.88446 + color 1 1 1 + on TRUE + } + DEF lab_x Shape { + appearance Appearance { + material DEF Rust Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .50196 .50196 .50196 + } + } + geometry DEF GeoCylinder15 Cylinder { + height 60.000 + radius 0.020 + } + } + DEF dad_lab_z Transform { + rotation 1 0 0 1.571 + children [ + DEF lab_z Shape { + appearance Appearance { + material USE Rust + } + geometry DEF GeoCylinder17 Cylinder { + height 40.000 + radius 0.020 + } + } + ] + } + DEF dad_lab_beam Transform { + translation 10 0 0 + rotation 0 0 1 1.571 + children [ + DEF lab_beam Shape { + appearance Appearance { + material DEF White Material { + ambientIntensity 0.500 + shininess 0.100 + transparency 0.500 + diffuseColor 1 1 1 + emissiveColor 1 1 1 + } + } + geometry DEF GeoCylinder20 Cylinder { + height 20.000 + radius 0.050 + } + } + ] + } + DEF dad_lab_beam0 Transform { + translation 20 0 0 + rotation 0 0 1 1.571 + children [ + DEF lab_beam0 Shape { + appearance Appearance { + material DEF Shiny_Black Material { + ambientIntensity 0.200 + shininess 0.100 + diffuseColor 1 1 1 + } + } + geometry DEF GeoCylinder22 Cylinder { + height 4.000 + radius 0.200 + } + } + ] + } + DEF dad_Cone2 Transform { + translation 17.5 0 0 + rotation 0 0 1 1.571 + children [ + DEF Cone2 Shape { + appearance Appearance { + material USE Shiny_Black + } + geometry DEF GeoCone2 Cone { + height 1.500 + bottomRadius 0.500 + } + } + ] + } + DEF dad_lab_y Transform { + rotation 0 0 -1 1.571 + children [ + DEF lab_y Shape { + appearance Appearance { + material USE Rust + } + geometry DEF GeoCylinder16 Cylinder { + height 60.000 + radius 0.020 + } + } + ] + } + DEF dad_alpha_frame Transform { + rotation 0 1 0 .175 + children [ + DEF dad_alpha_base Transform { + translation 0 -20.5 0 + children [ + DEF alpha_base Shape { + appearance Appearance { + material DEF Red Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .50196 0 0 + } + } + geometry DEF GeoCylinder1 Cylinder { + height 2.000 + radius 6.000 + } + } + ] + } + DEF dad_lab_post Transform { + translation 0 -10 -11 + children [ + DEF alpha_post Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox1 Box { + size 4 20 1 + } + } + ] + } + DEF dad_omega_frame Transform { + rotation 0 0 1 -.175 + children [ + DEF dad_chi_frame Transform { + rotation 1 0 0 -.785 + children [ + DEF dad_phi_frame Transform { + rotation 0 0 1 -.349 + children [ + DEF dad_Cylinder7 Transform { + translation 0 0 -4 + rotation 1 0 0 1.571 + children [ + DEF Cylinder7 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoCylinder7 Cylinder { + height 2.000 + radius 1.000 + } + } + ] + } + DEF dad_Cylinder8 Transform { + translation 0 0 -3.5 + rotation 1 0 0 1.571 + children [ + DEF Cylinder8 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoCylinder8 Cylinder { + height 6.000 + radius 0.250 + } + } + ] + } + DEF Box3 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoBox3 Box { + size 1 1 1 + } + } + DEF dad_Box11 Transform { + translation 0 1 -4 + children [ + DEF Box11 Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoBox11 Box { + size .25 .5 2 + } + } + ] + } + DEF Box4 Shape { + appearance Appearance { + material DEF White_wire Material { + emissiveColor 1 1 1 + } + } + geometry IndexedLineSet { + coordIndex [ + 0 1 2 0 + -1 0 2 3 + 0 -1 1 5 + 6 1 -1 1 + 6 2 1 -1 + 2 6 7 2 + -1 2 7 3 + 2 -1 3 7 + 4 3 -1 3 + 4 0 3 -1 + 0 4 5 0 + -1 0 5 1 + 0 -1 6 5 + 4 6 -1 6 + 4 7 6 -1 + ] + coord Coordinate { + point [ + -.5 .5 -.5 + -.5 .5 .5 + .5 .5 .5 + .5 .5 -.5 + -.5 -.5 -.5 + -.5 -.5 .5 + .5 -.5 .5 + .5 -.5 -.5 + ] + } + } + } + ] + } + DEF dad_Box2 Transform { + translation 0 0 -5.5 + children [ + DEF Box2 Shape { + appearance Appearance { + material DEF Purple Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .9176 0 .874 + } + } + geometry DEF GeoBox2 Box { + size 2.8 2.8 1.5 + } + } + ] + } + DEF dad_Box6 Transform { + translation 0 1.25 -5.5 + children [ + DEF Box6 Shape { + appearance Appearance { + material USE Purple + } + geometry DEF GeoBox21 Box { + size .25 1 1.5 + } + } + ] + } + ] + } + DEF dad_Cylinder2 Transform { + rotation 0 0 1 1.571 + scale 1.1 1.5 1.1 + children [ + DEF Cylinder2 Shape { + appearance Appearance { + material DEF Green Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .00784 .65098 .02353 + } + } + geometry DEF Cylinder2_Geo IndexedFaceSet { + creaseAngle 0.524 + coordIndex [ + 49 50 48 -1 + 48 50 51 -1 + 48 51 47 -1 + 47 51 20 -1 + 47 20 21 -1 + 12 13 56 -1 + 56 13 14 -1 + 56 14 55 -1 + 55 14 15 -1 + 55 15 54 -1 + 54 15 16 -1 + 54 16 53 -1 + 53 16 17 -1 + 53 17 52 -1 + 52 17 18 -1 + 52 18 51 -1 + 51 18 19 -1 + 51 19 20 -1 + 5 6 62 -1 + 62 6 7 -1 + 62 7 61 -1 + 61 7 8 -1 + 61 8 60 -1 + 60 8 9 -1 + 60 9 59 -1 + 59 9 10 -1 + 59 10 58 -1 + 58 10 11 -1 + 58 11 57 -1 + 57 11 12 -1 + 57 12 56 -1 + 5 63 4 -1 + 4 63 64 -1 + 4 64 3 -1 + 3 64 1 -1 + 3 1 2 -1 + 2 1 35 -1 + 2 35 36 -1 + 28 41 27 -1 + 27 41 42 -1 + 27 42 26 -1 + 26 42 43 -1 + 26 43 25 -1 + 25 43 44 -1 + 25 44 24 -1 + 24 44 45 -1 + 24 45 23 -1 + 23 45 46 -1 + 23 46 22 -1 + 22 46 47 -1 + 22 47 21 -1 + 34 36 33 -1 + 33 36 37 -1 + 33 37 32 -1 + 32 37 38 -1 + 32 38 31 -1 + 31 38 39 -1 + 31 39 30 -1 + 30 39 40 -1 + 30 40 29 -1 + 29 40 41 -1 + 29 41 28 -1 + 1 64 0 -1 + 2 36 34 -1 + 63 5 62 -1 + 95 96 97 -1 + 97 96 65 -1 + 97 65 129 -1 + 129 65 66 -1 + 129 66 128 -1 + 128 66 67 -1 + 128 67 127 -1 + 127 67 126 -1 + 104 105 88 -1 + 88 105 106 -1 + 88 106 87 -1 + 87 106 107 -1 + 87 107 86 -1 + 86 107 108 -1 + 86 108 85 -1 + 85 108 109 -1 + 85 109 84 -1 + 84 109 110 -1 + 84 110 83 -1 + 83 110 111 -1 + 83 111 82 -1 + 82 111 112 -1 + 82 112 113 -1 + 98 99 93 -1 + 93 99 100 -1 + 93 100 92 -1 + 92 100 101 -1 + 92 101 91 -1 + 91 101 102 -1 + 91 102 90 -1 + 90 102 103 -1 + 90 103 89 -1 + 89 103 104 -1 + 89 104 88 -1 + 94 95 97 -1 + 94 97 98 -1 + 94 98 93 -1 + 81 82 78 -1 + 81 78 79 -1 + 81 79 80 -1 + 78 82 113 -1 + 78 113 114 -1 + 78 114 115 -1 + 78 115 77 -1 + 120 73 119 -1 + 119 73 74 -1 + 119 74 118 -1 + 118 74 75 -1 + 118 75 117 -1 + 117 75 76 -1 + 117 76 116 -1 + 116 76 77 -1 + 116 77 115 -1 + 126 68 125 -1 + 125 68 69 -1 + 125 69 124 -1 + 124 69 70 -1 + 124 70 123 -1 + 123 70 71 -1 + 123 71 122 -1 + 122 71 72 -1 + 122 72 121 -1 + 121 72 73 -1 + 121 73 120 -1 + 126 67 68 -1 + 0 64 65 -1 + 0 65 96 -1 + 64 63 66 -1 + 64 66 65 -1 + 63 62 67 -1 + 63 67 66 -1 + 62 61 68 -1 + 62 68 67 -1 + 61 60 69 -1 + 61 69 68 -1 + 60 59 70 -1 + 60 70 69 -1 + 59 58 71 -1 + 59 71 70 -1 + 58 57 72 -1 + 58 72 71 -1 + 57 56 73 -1 + 57 73 72 -1 + 56 55 74 -1 + 56 74 73 -1 + 55 54 75 -1 + 55 75 74 -1 + 54 53 76 -1 + 54 76 75 -1 + 53 52 77 -1 + 53 77 76 -1 + 52 51 78 -1 + 52 78 77 -1 + 51 50 79 -1 + 51 79 78 -1 + 50 49 80 -1 + 50 80 79 -1 + 49 48 81 -1 + 49 81 80 -1 + 48 47 82 -1 + 48 82 81 -1 + 47 46 83 -1 + 47 83 82 -1 + 46 45 84 -1 + 46 84 83 -1 + 45 44 85 -1 + 45 85 84 -1 + 44 43 86 -1 + 44 86 85 -1 + 43 42 87 -1 + 43 87 86 -1 + 42 41 88 -1 + 42 88 87 -1 + 41 40 89 -1 + 41 89 88 -1 + 40 39 90 -1 + 40 90 89 -1 + 39 38 91 -1 + 39 91 90 -1 + 38 37 92 -1 + 38 92 91 -1 + 37 36 93 -1 + 37 93 92 -1 + 36 35 94 -1 + 36 94 93 -1 + 35 1 95 -1 + 35 95 94 -1 + 1 0 96 -1 + 1 96 95 -1 + ] + coord DEF Cylinder2_Coord Coordinate { + point [ + 0 1 -6 + -1.17054 1 -5.88471 + -1.07212 1 -5.3899 + -1.05189 1 -5.39596 + .02145 1 -5.49809 + 1.09408 1 -5.38886 + 2.1248 1 -5.07248 + 3.07398 1 -4.56108 + 3.90517 1 -3.87434 + 4.5864 1 -3.03864 + 5.0915 1 -2.08609 + 5.40107 1 -1.0533 + 5.5032 1 .02003 + 5.39398 1 1.09267 + 5.07759 1 2.12338 + 4.5662 1 3.07257 + 3.87945 1 3.90375 + 3.04375 1 4.58498 + 2.0912 1 5.09009 + 1.05842 1 5.39966 + -.01492 1 5.50179 + -1.08755 1 5.39256 + -2.11827 1 5.07617 + -3.06746 1 4.56478 + -3.89864 1 3.87804 + -4.57987 1 3.04233 + -5.08498 1 2.08979 + -5.39454 1 1.057 + -5.49668 1 -.01633 + -5.38745 1 -1.08897 + -5.07106 1 -2.11968 + -4.55967 1 -3.06887 + -3.87292 1 -3.90005 + -3.03722 1 -4.58129 + -2.08467 1 -5.08639 + -2.2961 1 -5.54328 + -3.33342 1 -4.98882 + -4.24264 1 -4.24264 + -4.98882 1 -3.33342 + -5.54328 1 -2.2961 + -5.88471 1 -1.17054 + -6 1 -0 + -5.88471 1 1.17054 + -5.54328 1 2.2961 + -4.98882 1 3.33342 + -4.24264 1 4.24264 + -3.33342 1 4.98882 + -2.2961 1 5.54328 + -1.17054 1 5.88471 + -0 1 6 + 1.17054 1 5.88471 + 2.2961 1 5.54328 + 3.33342 1 4.98882 + 4.24264 1 4.24264 + 4.98882 1 3.33342 + 5.54328 1 2.2961 + 5.88471 1 1.17054 + 6 1 0 + 5.88471 1 -1.17054 + 5.54328 1 -2.2961 + 4.98882 1 -3.33342 + 4.24264 1 -4.24264 + 3.33342 1 -4.98882 + 2.2961 1 -5.54328 + 1.17054 1 -5.88471 + 1.17054 -1 -5.88471 + 2.2961 -1 -5.54328 + 3.33342 -1 -4.98882 + 4.24264 -1 -4.24264 + 4.98882 -1 -3.33342 + 5.54328 -1 -2.2961 + 5.88471 -1 -1.17054 + 6 -1 -0 + 5.88471 -1 1.17054 + 5.54328 -1 2.2961 + 4.98882 -1 3.33342 + 4.24264 -1 4.24264 + 3.33342 -1 4.98882 + 2.2961 -1 5.54328 + 1.17054 -1 5.88471 + -0 -1 6 + -1.17054 -1 5.88471 + -2.2961 -1 5.54328 + -3.33342 -1 4.98882 + -4.24264 -1 4.24264 + -4.98882 -1 3.33342 + -5.54328 -1 2.2961 + -5.88471 -1 1.17054 + -6 -1 -0 + -5.88471 -1 -1.17054 + -5.54328 -1 -2.2961 + -4.98882 -1 -3.33342 + -4.24264 -1 -4.24264 + -3.33342 -1 -4.98882 + -2.2961 -1 -5.54328 + -1.17054 -1 -5.88471 + 0 -1 -6 + 0 -1 -5.50037 + -1.05842 -1 -5.39966 + -2.0912 -1 -5.09009 + -3.04375 -1 -4.58498 + -3.87945 -1 -3.90375 + -4.5662 -1 -3.07257 + -5.07759 -1 -2.12338 + -5.39398 -1 -1.09267 + -5.5032 -1 -.02003 + -5.40107 -1 1.0533 + -5.0915 -1 2.08609 + -4.5864 -1 3.03864 + -3.90517 -1 3.87434 + -3.07398 -1 4.56108 + -2.1248 -1 5.07248 + -1.09408 -1 5.38886 + -.02145 -1 5.49809 + 1.05189 -1 5.39596 + 2.08467 -1 5.08639 + 3.03722 -1 4.58129 + 3.87292 -1 3.90005 + 4.55967 -1 3.06887 + 5.07106 -1 2.11968 + 5.38745 -1 1.08897 + 5.49668 -1 .01633 + 5.39454 -1 -1.057 + 5.08498 -1 -2.08979 + 4.57987 -1 -3.04233 + 3.89864 -1 -3.87804 + 3.06746 -1 -4.56478 + 2.11827 -1 -5.07617 + 1.08755 -1 -5.39256 + .01492 -1 -5.50179 + ] + } + } + } + ] + } + DEF dad_Cylinder3 Transform { + rotation 0 0 1 1.571 + scale 1.1 1.5 1.1 + children [ + DEF Cylinder3 Shape { + appearance Appearance { + material USE Green + } + geometry DEF Cylinder3_Geo IndexedFaceSet { + creaseAngle 0.524 + coordIndex [ + 0 1 2 -1 + 0 2 3 -1 + 4 5 1 -1 + 4 1 0 -1 + 6 7 5 -1 + 6 5 4 -1 + 8 9 7 -1 + 8 7 6 -1 + 10 11 9 -1 + 10 9 8 -1 + 12 13 11 -1 + 12 11 10 -1 + 14 15 13 -1 + 14 13 12 -1 + 16 17 15 -1 + 16 15 14 -1 + 18 19 17 -1 + 18 17 16 -1 + 20 21 19 -1 + 20 19 18 -1 + 22 23 21 -1 + 22 21 20 -1 + 24 25 23 -1 + 24 23 22 -1 + 26 27 25 -1 + 26 25 24 -1 + 28 29 27 -1 + 28 27 26 -1 + 30 31 29 -1 + 30 29 28 -1 + 32 33 31 -1 + 32 31 30 -1 + 34 35 33 -1 + 34 33 32 -1 + 36 37 35 -1 + 36 35 34 -1 + 38 39 37 -1 + 38 37 36 -1 + 40 41 39 -1 + 40 39 38 -1 + 42 43 41 -1 + 42 41 40 -1 + 44 45 43 -1 + 44 43 42 -1 + 46 47 45 -1 + 46 45 44 -1 + 48 49 47 -1 + 48 47 46 -1 + 50 51 49 -1 + 50 49 48 -1 + 52 53 51 -1 + 52 51 50 -1 + 54 55 53 -1 + 54 53 52 -1 + 56 57 55 -1 + 56 55 54 -1 + 58 59 57 -1 + 58 57 56 -1 + 60 61 59 -1 + 60 59 58 -1 + 62 63 61 -1 + 62 61 60 -1 + 3 2 63 -1 + 3 63 62 -1 + ] + coord DEF Cylinder3_Coord Coordinate { + point [ + 1.073 -1.00635 -5.39432 + 1.073 .99367 -5.39432 + 0 .98997 -5.5 + 0 -1.01006 -5.5 + 2.10476 -1.0024 -5.08134 + 2.10476 .99763 -5.08134 + 3.05564 -.99836 -4.57308 + 3.05564 1.00167 -4.57308 + 3.88909 -.99438 -3.88909 + 3.88909 1.00565 -3.88909 + 4.57308 -.99061 -3.05564 + 4.57308 1.00941 -3.05564 + 5.08134 -.98721 -2.10476 + 5.08134 1.01282 -2.10476 + 5.39432 -.9843 -1.073 + 5.39432 1.01573 -1.073 + 5.5 -.98199 -0 + 5.5 1.01803 -0 + 5.39432 -.98038 1.073 + 5.39432 1.01965 1.073 + 5.08134 -.97952 2.10476 + 5.08134 1.02051 2.10476 + 4.57308 -.97945 3.05564 + 4.57308 1.02058 3.05564 + 3.88909 -.98017 3.88909 + 3.88909 1.01986 3.88909 + 3.05564 -.98165 4.57308 + 3.05564 1.01838 4.57308 + 2.10476 -.98383 5.08134 + 2.10476 1.01619 5.08134 + 1.073 -.98664 5.39432 + 1.073 1.01338 5.39432 + -0 -.98997 5.5 + -0 1.01006 5.5 + -1.073 -.99367 5.39432 + -1.073 1.00635 5.39432 + -2.10476 -.99763 5.08134 + -2.10476 1.0024 5.08134 + -3.05564 -1.00167 4.57308 + -3.05564 .99836 4.57308 + -3.88909 -1.00565 3.88909 + -3.88909 .99438 3.88909 + -4.57308 -1.00941 3.05564 + -4.57308 .99061 3.05564 + -5.08134 -1.01282 2.10476 + -5.08134 .98721 2.10476 + -5.39432 -1.01573 1.073 + -5.39432 .9843 1.073 + -5.5 -1.01803 -0 + -5.5 .98199 -0 + -5.39432 -1.01965 -1.073 + -5.39432 .98038 -1.073 + -5.08134 -1.02051 -2.10476 + -5.08134 .97952 -2.10476 + -4.57308 -1.02058 -3.05564 + -4.57308 .97945 -3.05564 + -3.88909 -1.01986 -3.88909 + -3.88909 .98017 -3.88909 + -3.05564 -1.01838 -4.57308 + -3.05564 .98165 -4.57308 + -2.10476 -1.01619 -5.08134 + -2.10476 .98383 -5.08134 + -1.073 -1.01338 -5.39432 + -1.073 .98664 -5.39432 + ] + } + } + } + ] + } + DEF dad_Box10 Transform { + translation 0 2 -8.4 + children [ + DEF Box10 Shape { + appearance Appearance { + material USE Green + } + geometry DEF GeoBox10 Box { + size .25 1 4.25 + } + } + ] + } + DEF dad_Cylinder5 Transform { + translation 0 0 -8.4 + rotation 1 0 0 1.571 + children [ + DEF Cylinder5 Shape { + appearance Appearance { + material USE Green + } + geometry DEF GeoCylinder5 Cylinder { + height 4.250 + radius 2.000 + } + } + ] + } + ] + } + DEF dad_lab_posttop Transform { + translation 0 0 -11 + rotation -1 0 0 1.571 + children [ + DEF alpha_posttop Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoCylinder6 Cylinder { + height 1.000 + radius 2.000 + } + } + ] + } + DEF dad_alpha_arm Transform { + translation 0 -20.5 -5 + children [ + DEF alpha_arm Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox7 Box { + size 4 1 13 + } + } + ] + } + DEF dad_gamma_arm1 Transform { + translation -6 -20.5 0 + children [ + DEF gamma_arm1 Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox18 Box { + size 1 2 .25 + } + } + ] + } + DEF dad_Box12 Transform { + translation 0 2 -11 + children [ + DEF Box12 Shape { + appearance Appearance { + material USE Red + } + geometry DEF GeoBox12 Box { + size .25 1 1 + } + } + ] + } + ] + } + DEF dad_gamma_frame Transform { + rotation 0 1 0 .349 + children [ + DEF dad_lab_posttop0 Transform { + translation 0 0 -12.5 + rotation -1 0 0 1.571 + children [ + DEF gamma_posttop Shape { + appearance Appearance { + material DEF Blue Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor 0 0 .62745 + } + } + geometry DEF GeoCylinder4 Cylinder { + height 1.000 + radius 2.000 + } + } + ] + } + DEF dad_lab_post0 Transform { + translation 0 -11 -12.5 + children [ + DEF gamma_post Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox5 Box { + size 4 22 1 + } + } + ] + } + DEF dad_gamma_base Transform { + translation 0 -22.5 0 + children [ + DEF gamma_base Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoCylinder3 Cylinder { + height 2.000 + radius 8.000 + } + } + ] + } + DEF dad_gamma_arm Transform { + translation 0 -22.5 -6.5 + children [ + DEF gamma_arm Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox4 Box { + size 4 1 13 + } + } + ] + } + DEF dad_delta_frame Transform { + rotation 0 0 1 -.524 + children [ + DEF dad_delta_mount Transform { + translation 0 0 -15 + rotation -1 0 0 1.571 + children [ + DEF delta_mount Shape { + appearance Appearance { + material DEF Yellow Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .83529 .83529 0 + } + } + geometry DEF GeoCylinder2 Cylinder { + height 4.000 + radius 2.000 + } + } + ] + } + DEF dad_delta_arm Transform { + translation -10 0 -15 + children [ + DEF delta_arm Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoBox8 Box { + size 20 3 1 + } + } + ] + } + DEF dad_delta_arm0 Transform { + translation -20 0 -6.5 + children [ + DEF delta_arm0 Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoBox6 Box { + size 1 3 18 + } + } + ] + } + DEF dad_Cylinder14 Transform { + translation -18 0 0 + rotation .577 -.577 .577 2.094 + children [ + DEF Cylinder14 Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoCylinder14 Cylinder { + height 3.000 + radius 1.000 + } + } + ] + } + DEF dad_detector_beam Transform { + translation -10 0 0 + rotation 0 0 1 1.571 + children [ + DEF detector_beam Shape { + appearance Appearance { + material USE White + } + geometry DEF GeoCylinder21 Cylinder { + height 20.000 + radius 0.050 + } + } + ] + } + DEF dad_Box13 Transform { + translation 0 2 -15 + children [ + DEF Box13 Shape { + appearance Appearance { + material USE Yellow + } + geometry DEF GeoBox13 Box { + size .25 1 4 + } + } + ] + } + ] + } + DEF dad_gamma_arm0 Transform { + translation -8 -22.5 0 + children [ + DEF gamma_arm0 Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox17 Box { + size 1 2 .25 + } + } + ] + } + DEF dad_Box14 Transform { + translation 0 2 -12.5 + children [ + DEF Box14 Shape { + appearance Appearance { + material USE Blue + } + geometry DEF GeoBox14 Box { + size .25 1 1 + } + } + ] + } + ] + } + DEF dad_gamma_arm2 Transform { + translation -9 -24.5 0 + children [ + DEF gamma_arm2 Shape { + appearance Appearance { + material DEF Black Material { + ambientIntensity 0.200 + shininess 0.200 + diffuseColor .20784 .20784 .20784 + } + } + geometry DEF GeoBox19 Box { + size 1 2 .25 + } + } + ] + } + DEF dad_gamma_base0 Transform { + translation 0 -24.5 0 + rotation 0 1 0 .349 + children [ + DEF gamma_base0 Shape { + appearance Appearance { + material USE Black + } + geometry DEF GeoCylinder9 Cylinder { + height 2.000 + radius 9.000 + } + } + ] + } + DEF dad_floor Transform { + translation 0 -25.5 0 + children [ + DEF floor Shape { + appearance Appearance { + material USE Black + } + geometry DEF GeoBox16 Box { + size 60 .2 40 + } + } + ] + } + DEF VP Viewpoint { + description "VP" + jump TRUE + fieldOfView 1.165 + position 40 40 40 + orientation -.754 .657 -0 1.001 + } + ] +} diff --git a/script/test/diffcalc (copy)/model/vrml_animator.py b/script/test/diffcalc (copy)/model/vrml_animator.py new file mode 100644 index 0000000..d283469 --- /dev/null +++ b/script/test/diffcalc (copy)/model/vrml_animator.py @@ -0,0 +1,134 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +import sys +import time +import threading +from math import pi +import socket + +from pivy.coin import * +from pivy.sogui import * + + +PORT = 4567 +TORAD = pi / 180 + + +def connect_to_socket(host, port): + print "Connecting to %s on port %d" % (host, port) + connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + print "Connected" + connection.connect((host, port)) + socketfile = connection.makefile('rw', 0) + return socketfile + + +def serve_socket_connection(port): + print ("Serving connection on all interfaces on %s port %d" % + (socket.gethostname(), port)) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((socket.gethostname(), port)) + sock.listen(1) + time.sleep(1) + (connection, addr) = sock.accept() + print 'Connected from ', addr, ' accepted' + socket_file = connection.makefile('rw', 0) # no buffering + return socket_file + + +def node_name(anglename): + return 'dad_' + anglename + '_frame' + + +class SceneUpdatingThread(threading.Thread): + + def __init__(self, scene, axisnames): + threading.Thread.__init__(self) + self.scene = scene + + # Infer rotation axes based on initial orientation + self.rotation_axes = {} + self.axies_nodes = {} + for axisname in axisnames: + node = self.scene.getByName(node_name(axisname)) + self.axies_nodes[axisname] = node + value = node.rotation.getValue() + self.rotation_axes[axisname] = value.getAxisAngle()[0] + + def run(self): + socket_file = serve_socket_connection(PORT) + + while True: + msg = socket_file.readline() + if msg == '': + print '***Socket closed' + socket_file = serve_socket_connection(PORT) + continue + print msg.strip() + d = eval(msg.strip()) # msg should be a dictionary representation + for axisname in d: + self.set_axis_rotation(axisname, d[axisname]) + + def set_axis_rotation(self, anglename, degrees): + nodename = node_name(anglename) + angle = degrees * TORAD + while angle < 0: + angle = 2 * pi + angle + node = self.scene.getByName(nodename) + getattr(node, 'rotation').setValue( + self.rotation_axes[anglename], angle) + + +class Animator(object): + + def __init__(self, filename, axisnames): + print "filename : " + filename + print " axes : " + ' '.join(axisnames) + # Create viewer + self.myWindow = SoGui.init(sys.argv[0]) # @UndefinedVariable + if self.myWindow is None: sys.exit(1) + viewer = SoGuiExaminerViewer(self.myWindow) # @UndefinedVariable + # load file into scene + so_input = SoInput() # @UndefinedVariable + so_input.openFile(filename) + self.scene = SoDB.readAll(so_input) # @UndefinedVariable + # Add scene to viewer + viewer.setSceneGraph(self.scene) + viewer.setTitle(' '.join(axisnames)) + viewer.show() + + self.start_update_scene_thread(axisnames) + + def start_update_scene_thread(self, axisnames): + t = SceneUpdatingThread(self.scene, axisnames) + t.setDaemon(True) + t.start() + + def show(self): + SoGui.show(self.myWindow) # @UndefinedVariable + SoGui.mainLoop() # @UndefinedVariable + +def main(): + animator = Animator(sys.argv[1], sys.argv[2:]) + animator.show() + +if __name__ == "__main__": + main() + diff --git a/script/test/diffcalc (copy)/numjy/__init__.py b/script/test/diffcalc (copy)/numjy/__init__.py new file mode 100644 index 0000000..273b233 --- /dev/null +++ b/script/test/diffcalc (copy)/numjy/__init__.py @@ -0,0 +1,36 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +try: + import Jama + from numjy import linalg + from numjy.jama_matrix_wrapper import matrix + JAMA = True +except ImportError: + JAMA = False + + +def hstack(list_of_column_matrices): + if not Jama: + raise Exception('Jama not available, use numpy directly') + ncol = len(list_of_column_matrices) + nrow = list_of_column_matrices[0].shape[0] + m = Jama.Matrix(nrow, ncol) + for c, column_matrix in enumerate(list_of_column_matrices): + m.setMatrix(0, nrow - 1, c, c, column_matrix.m) + return matrix(m) diff --git a/script/test/diffcalc (copy)/numjy/jama_matrix_wrapper.py b/script/test/diffcalc (copy)/numjy/jama_matrix_wrapper.py new file mode 100644 index 0000000..d5e7645 --- /dev/null +++ b/script/test/diffcalc (copy)/numjy/jama_matrix_wrapper.py @@ -0,0 +1,120 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +import Jama + + +class matrix(object): + + def __init__(self, a): + if isinstance(a, Jama.Matrix): + self.m = a + elif isinstance(a, basestring): + l = [] + for row in a.strip().split(';'): + l.append([float(element) + for element in row.replace(',', ' ').split()]) + self.m = Jama.Matrix(l) + elif isinstance(a, matrix): + self.m = Jama.Matrix(a.m) + elif isinstance(a, (list, tuple)): + if isinstance(a[0], (list, tuple)): + # a is a list of lists (not rigorous test!) + self.m = Jama.Matrix(a) + else: + # a is a row vector + self.m = Jama.Matrix([a]) + else: + # give it a go + self.m = Jama.Matrix(a) + + def __eq__(self, other): + nrow, ncol = self.shape + b = matrix(Jama.Matrix(nrow, ncol)) + for i in range(nrow): + for j in range(ncol): + b[i, j] = self[i, j] == other[i, j] + return b + + @property + def shape(self): + return self.m.getRowDimension(), self.m.getColumnDimension() + + def __len__(self): + return self.m.getRowDimension() + + def all(self): # @ReservedAssignment + for row in self.m.array: + if not all(row): + return False + return True + + def tolist(self): + l = [] + nrow, ncol = self.shape + for i in range(nrow): + row = [] + for j in range(ncol): + row.append(self[i, j]) + l.append(row) + return l + + def sum(self): # @ReservedAssignment + return sum(sum(row) for row in self.m.array) + + @property + def I(self): + return matrix(self.m.inverse()) + + @property + def T(self): + return matrix(self.m.transpose()) + + def _scaler(self, scaler): + return Jama.Matrix(self.shape[0], self.shape[1], scaler) + + def __add__(self, other): + v = other.m if isinstance(other, matrix) else self._scaler(other) + return matrix(self.m.plus(v)) + + def __sub__(self, other): + v = other.m if isinstance(other, matrix) else self._scaler(other) + return matrix(self.m.minus(v)) + + def __mul__(self, other): + return matrix(self.m.times(other.m if isinstance(other, matrix) else + other)) + + def __div__(self, other): + # dividend = other.I if isinstance(other, matrix) else 1. /float(other) + return self.__mul__(1. / float(other)) + + def __getitem__(self, key): + i, j = key + return self.m.get(i, j) + + def __setitem__(self, key, value): + i, j = key + self.m.set(i, j, value) + + def __str__(self): + insides = [' '.join([str(el) for el in row]) for row in self.tolist()] + return '[[' + ']\n ['.join(insides) + ']]' + + def __repr__(self): + return 'matrix(' + '\n '.join(self.__str__().split('\n')) + ')' diff --git a/script/test/diffcalc (copy)/numjy/linalg.py b/script/test/diffcalc (copy)/numjy/linalg.py new file mode 100644 index 0000000..218d824 --- /dev/null +++ b/script/test/diffcalc (copy)/numjy/linalg.py @@ -0,0 +1,20 @@ +### +# Copyright 2008-2011 Diamond Light Source Ltd. +# This file is part of Diffcalc. +# +# Diffcalc is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Diffcalc 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Diffcalc. If not, see . +### + +def norm(mat): + return mat.m.normF() diff --git a/script/test/diffcalc (copy)/setup.py b/script/test/diffcalc (copy)/setup.py new file mode 100644 index 0000000..0d549da --- /dev/null +++ b/script/test/diffcalc (copy)/setup.py @@ -0,0 +1,31 @@ +from setuptools import setup, find_packages + +setup( + name='diffcalc', + version='2.1', + + description='A diffraction condition calculator for X-ray or neutron diffractometer control.', + long_description=open('README.rst').read(), + url='https://github.com/DiamondLightSource/diffcalc', + + author='Rob Walton', + author_email='rob.walton@diamond.ac.uk', + + license='GNU', + + packages=find_packages(exclude=['docs']), + + install_requires=[ + 'numpy', + 'ipython', + 'pytest', + 'pytest-xdist', + 'nose' + ], + + entry_points={ + 'console_scripts': [ + 'diffcalc=diffcmd.diffcalc_launcher:main', + ], + }, +) \ No newline at end of file diff --git a/script/test/diffcalc (copy)/simplejson/__init__.py b/script/test/diffcalc (copy)/simplejson/__init__.py new file mode 100644 index 0000000..fe2bd5a --- /dev/null +++ b/script/test/diffcalc (copy)/simplejson/__init__.py @@ -0,0 +1,510 @@ +r"""JSON (JavaScript Object Notation) is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +:mod:`simplejson` exposes an API familiar to users of the standard library +:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained +version of the :mod:`json` library contained in Python 2.6, but maintains +compatibility with Python 2.4 and Python 2.5 and (currently) has +significant performance advantages, even without using the optional C +extension for speedups. + +Encoding basic Python object hierarchies:: + + >>> import simplejson as json + >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print json.dumps("\"foo\bar") + "\"foo\bar" + >>> print json.dumps(u'\u1234') + "\u1234" + >>> print json.dumps('\\') + "\\" + >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> json.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson as json + >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson as json + >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=' ') + >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson as json + >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj + True + >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' + True + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> json.load(io)[0] == 'streaming API' + True + +Specializing JSON object decoding:: + + >>> import simplejson as json + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> from decimal import Decimal + >>> json.loads('1.1', parse_float=Decimal) == Decimal('1.1') + True + +Specializing JSON object encoding:: + + >>> import simplejson as json + >>> def encode_complex(obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... raise TypeError(repr(o) + " is not JSON serializable") + ... + >>> json.dumps(2 + 1j, default=encode_complex) + '[2.0, 1.0]' + >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) + '[2.0, 1.0]' + >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) + '[2.0, 1.0]' + + +Using simplejson.tool from the shell to validate and pretty-print:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) +""" +__version__ = '2.6.2' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', + 'OrderedDict', 'simple_first', +] + +__author__ = 'Bob Ippolito ' + +from decimal import Decimal + +from decoder import JSONDecoder, JSONDecodeError +from encoder import JSONEncoder, JSONEncoderForHTML +def _import_OrderedDict(): + import collections + try: + return collections.OrderedDict + except AttributeError: + import ordered_dict + return ordered_dict.OrderedDict +OrderedDict = _import_OrderedDict() + +def _import_c_make_encoder(): + try: + from simplejson._speedups import make_encoder + return make_encoder + except ImportError: + return None + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, + use_decimal=True, + namedtuple_as_object=True, + tuple_as_array=True, + bigint_as_string=False, + item_sort_key=None, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, use_decimal=True, + namedtuple_as_object=True, tuple_as_array=True, + bigint_as_string=False, sort_keys=False, item_sort_key=None, + **kw): + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If *indent* is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *use_decimal* is true (default: ``True``) then decimal.Decimal + will be natively serialized to JSON with full precision. + + If *namedtuple_as_object* is true (default: ``True``), + :class:`tuple` subclasses with ``_asdict()`` methods will be encoded + as JSON objects. + + If *tuple_as_array* is true (default: ``True``), + :class:`tuple` (and subclasses) will be encoded as JSON arrays. + + If *bigint_as_string* is true (default: ``False``), ints 2**53 and higher + or lower than -2**53 will be encoded as strings. This is to avoid the + rounding that happens in Javascript otherwise. Note that this is still a + lossy operation that will not round-trip correctly and should be used + sparingly. + + If specified, *item_sort_key* is a callable used to sort the items in + each dictionary. This is useful if you want to sort items other than + in alphabetical order by key. This option takes precedence over + *sort_keys*. + + If *sort_keys* is true (default: ``False``), the output of dictionaries + will be sorted by item. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and use_decimal + and namedtuple_as_object and tuple_as_array + and not bigint_as_string and not item_sort_key and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, use_decimal=use_decimal, + namedtuple_as_object=namedtuple_as_object, + tuple_as_array=tuple_as_array, + bigint_as_string=bigint_as_string, + sort_keys=sort_keys, + item_sort_key=item_sort_key, + **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, use_decimal=True, + namedtuple_as_object=True, tuple_as_array=True, + bigint_as_string=False, sort_keys=False, item_sort_key=None, + **kw): + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is false then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *use_decimal* is true (default: ``True``) then decimal.Decimal + will be natively serialized to JSON with full precision. + + If *namedtuple_as_object* is true (default: ``True``), + :class:`tuple` subclasses with ``_asdict()`` methods will be encoded + as JSON objects. + + If *tuple_as_array* is true (default: ``True``), + :class:`tuple` (and subclasses) will be encoded as JSON arrays. + + If *bigint_as_string* is true (not the default), ints 2**53 and higher + or lower than -2**53 will be encoded as strings. This is to avoid the + rounding that happens in Javascript otherwise. + + If specified, *item_sort_key* is a callable used to sort the items in + each dictionary. This is useful if you want to sort items other than + in alphabetical order by key. This option takes precendence over + *sort_keys*. + + If *sort_keys* is true (default: ``False``), the output of dictionaries + will be sorted by item. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and use_decimal + and namedtuple_as_object and tuple_as_array + and not bigint_as_string and not sort_keys + and not item_sort_key and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + use_decimal=use_decimal, + namedtuple_as_object=namedtuple_as_object, + tuple_as_array=tuple_as_array, + bigint_as_string=bigint_as_string, + sort_keys=sort_keys, + item_sort_key=item_sort_key, + **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None, + object_pairs_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, object_pairs_hook=None, + use_decimal=False, namedtuple_as_object=True, tuple_as_array=True, + **kw): + """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + If *use_decimal* is true (default: ``False``) then it implies + parse_float=decimal.Decimal for parity with ``dump``. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, + use_decimal=use_decimal, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, object_pairs_hook=None, + use_decimal=False, **kw): + """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + If *use_decimal* is true (default: ``False``) then it implies + parse_float=decimal.Decimal for parity with ``dump``. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and object_pairs_hook is None + and not use_decimal and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if object_pairs_hook is not None: + kw['object_pairs_hook'] = object_pairs_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + if use_decimal: + if parse_float is not None: + raise TypeError("use_decimal=True implies parse_float=Decimal") + kw['parse_float'] = Decimal + return cls(encoding=encoding, **kw).decode(s) + + +def _toggle_speedups(enabled): + import simplejson.decoder as dec + import simplejson.encoder as enc + import simplejson.scanner as scan + c_make_encoder = _import_c_make_encoder() + if enabled: + dec.scanstring = dec.c_scanstring or dec.py_scanstring + enc.c_make_encoder = c_make_encoder + enc.encode_basestring_ascii = (enc.c_encode_basestring_ascii or + enc.py_encode_basestring_ascii) + scan.make_scanner = scan.c_make_scanner or scan.py_make_scanner + else: + dec.scanstring = dec.py_scanstring + enc.c_make_encoder = None + enc.encode_basestring_ascii = enc.py_encode_basestring_ascii + scan.make_scanner = scan.py_make_scanner + dec.make_scanner = scan.make_scanner + global _default_decoder + _default_decoder = JSONDecoder( + encoding=None, + object_hook=None, + object_pairs_hook=None, + ) + global _default_encoder + _default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, + ) + +def simple_first(kv): + """Helper function to pass to item_sort_key to sort simple + elements to the top, then container elements. + """ + return (isinstance(kv[1], (list, dict, tuple)), kv[0]) diff --git a/script/test/diffcalc (copy)/simplejson/_speedups.c b/script/test/diffcalc (copy)/simplejson/_speedups.c new file mode 100644 index 0000000..be68b2d --- /dev/null +++ b/script/test/diffcalc (copy)/simplejson/_speedups.c @@ -0,0 +1,2745 @@ +#include "Python.h" +#include "structmember.h" +#if PY_VERSION_HEX < 0x02070000 && !defined(PyOS_string_to_double) +#define PyOS_string_to_double json_PyOS_string_to_double +static double +json_PyOS_string_to_double(const char *s, char **endptr, PyObject *overflow_exception); +static double +json_PyOS_string_to_double(const char *s, char **endptr, PyObject *overflow_exception) { + double x; + assert(endptr == NULL); + assert(overflow_exception == NULL); + PyFPE_START_PROTECT("json_PyOS_string_to_double", return -1.0;) + x = PyOS_ascii_atof(s); + PyFPE_END_PROTECT(x) + return x; +} +#endif +#if PY_VERSION_HEX < 0x02060000 && !defined(Py_TYPE) +#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) +#endif +#if PY_VERSION_HEX < 0x02060000 && !defined(Py_SIZE) +#define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size) +#endif +#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) +typedef int Py_ssize_t; +#define PY_SSIZE_T_MAX INT_MAX +#define PY_SSIZE_T_MIN INT_MIN +#define PyInt_FromSsize_t PyInt_FromLong +#define PyInt_AsSsize_t PyInt_AsLong +#endif +#ifndef Py_IS_FINITE +#define Py_IS_FINITE(X) (!Py_IS_INFINITY(X) && !Py_IS_NAN(X)) +#endif + +#ifdef __GNUC__ +#define UNUSED __attribute__((__unused__)) +#else +#define UNUSED +#endif + +#define DEFAULT_ENCODING "utf-8" + +#define PyScanner_Check(op) PyObject_TypeCheck(op, &PyScannerType) +#define PyScanner_CheckExact(op) (Py_TYPE(op) == &PyScannerType) +#define PyEncoder_Check(op) PyObject_TypeCheck(op, &PyEncoderType) +#define PyEncoder_CheckExact(op) (Py_TYPE(op) == &PyEncoderType) + +static PyTypeObject PyScannerType; +static PyTypeObject PyEncoderType; + +typedef struct _PyScannerObject { + PyObject_HEAD + PyObject *encoding; + PyObject *strict; + PyObject *object_hook; + PyObject *pairs_hook; + PyObject *parse_float; + PyObject *parse_int; + PyObject *parse_constant; + PyObject *memo; +} PyScannerObject; + +static PyMemberDef scanner_members[] = { + {"encoding", T_OBJECT, offsetof(PyScannerObject, encoding), READONLY, "encoding"}, + {"strict", T_OBJECT, offsetof(PyScannerObject, strict), READONLY, "strict"}, + {"object_hook", T_OBJECT, offsetof(PyScannerObject, object_hook), READONLY, "object_hook"}, + {"object_pairs_hook", T_OBJECT, offsetof(PyScannerObject, pairs_hook), READONLY, "object_pairs_hook"}, + {"parse_float", T_OBJECT, offsetof(PyScannerObject, parse_float), READONLY, "parse_float"}, + {"parse_int", T_OBJECT, offsetof(PyScannerObject, parse_int), READONLY, "parse_int"}, + {"parse_constant", T_OBJECT, offsetof(PyScannerObject, parse_constant), READONLY, "parse_constant"}, + {NULL} +}; + +typedef struct _PyEncoderObject { + PyObject_HEAD + PyObject *markers; + PyObject *defaultfn; + PyObject *encoder; + PyObject *indent; + PyObject *key_separator; + PyObject *item_separator; + PyObject *sort_keys; + PyObject *skipkeys; + PyObject *key_memo; + PyObject *Decimal; + int fast_encode; + int allow_nan; + int use_decimal; + int namedtuple_as_object; + int tuple_as_array; + int bigint_as_string; + PyObject *item_sort_key; +} PyEncoderObject; + +static PyMemberDef encoder_members[] = { + {"markers", T_OBJECT, offsetof(PyEncoderObject, markers), READONLY, "markers"}, + {"default", T_OBJECT, offsetof(PyEncoderObject, defaultfn), READONLY, "default"}, + {"encoder", T_OBJECT, offsetof(PyEncoderObject, encoder), READONLY, "encoder"}, + {"indent", T_OBJECT, offsetof(PyEncoderObject, indent), READONLY, "indent"}, + {"key_separator", T_OBJECT, offsetof(PyEncoderObject, key_separator), READONLY, "key_separator"}, + {"item_separator", T_OBJECT, offsetof(PyEncoderObject, item_separator), READONLY, "item_separator"}, + {"sort_keys", T_OBJECT, offsetof(PyEncoderObject, sort_keys), READONLY, "sort_keys"}, + {"skipkeys", T_OBJECT, offsetof(PyEncoderObject, skipkeys), READONLY, "skipkeys"}, + {"key_memo", T_OBJECT, offsetof(PyEncoderObject, key_memo), READONLY, "key_memo"}, + {"item_sort_key", T_OBJECT, offsetof(PyEncoderObject, item_sort_key), READONLY, "item_sort_key"}, + {NULL} +}; + +static PyObject * +maybe_quote_bigint(PyObject *encoded, PyObject *obj); + +static Py_ssize_t +ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars); +static PyObject * +ascii_escape_unicode(PyObject *pystr); +static PyObject * +ascii_escape_str(PyObject *pystr); +static PyObject * +py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr); +void init_speedups(void); +static PyObject * +scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); +static PyObject * +scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); +static PyObject * +_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx); +static PyObject * +scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static int +scanner_init(PyObject *self, PyObject *args, PyObject *kwds); +static void +scanner_dealloc(PyObject *self); +static int +scanner_clear(PyObject *self); +static PyObject * +encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static int +encoder_init(PyObject *self, PyObject *args, PyObject *kwds); +static void +encoder_dealloc(PyObject *self); +static int +encoder_clear(PyObject *self); +static int +encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level); +static int +encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level); +static int +encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level); +static PyObject * +_encoded_const(PyObject *obj); +static void +raise_errmsg(char *msg, PyObject *s, Py_ssize_t end); +static PyObject * +encoder_encode_string(PyEncoderObject *s, PyObject *obj); +static int +_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr); +static PyObject * +_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr); +static PyObject * +encoder_encode_float(PyEncoderObject *s, PyObject *obj); +static int +_is_namedtuple(PyObject *obj); + +#define S_CHAR(c) (c >= ' ' && c <= '~' && c != '\\' && c != '"') +#define IS_WHITESPACE(c) (((c) == ' ') || ((c) == '\t') || ((c) == '\n') || ((c) == '\r')) + +#define MIN_EXPANSION 6 +#ifdef Py_UNICODE_WIDE +#define MAX_EXPANSION (2 * MIN_EXPANSION) +#else +#define MAX_EXPANSION MIN_EXPANSION +#endif + +static PyObject * +maybe_quote_bigint(PyObject *encoded, PyObject *obj) +{ + static PyObject *big_long = NULL; + static PyObject *small_long = NULL; + if (big_long == NULL) { + big_long = PyLong_FromLongLong(1LL << 53); + if (big_long == NULL) { + Py_DECREF(encoded); + return NULL; + } + } + if (small_long == NULL) { + small_long = PyLong_FromLongLong(-1LL << 53); + if (small_long == NULL) { + Py_DECREF(encoded); + return NULL; + } + } + if (PyObject_RichCompareBool(obj, big_long, Py_GE) || + PyObject_RichCompareBool(obj, small_long, Py_LE)) { + PyObject* quoted = PyString_FromFormat("\"%s\"", + PyString_AsString(encoded)); + Py_DECREF(encoded); + encoded = quoted; + } + return encoded; +} + +static int +_is_namedtuple(PyObject *obj) +{ + int rval = 0; + PyObject *_asdict = PyObject_GetAttrString(obj, "_asdict"); + if (_asdict == NULL) { + PyErr_Clear(); + return 0; + } + rval = PyCallable_Check(_asdict); + Py_DECREF(_asdict); + return rval; +} + +static int +_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr) +{ + /* PyObject to Py_ssize_t converter */ + *size_ptr = PyInt_AsSsize_t(o); + if (*size_ptr == -1 && PyErr_Occurred()) + return 0; + return 1; +} + +static PyObject * +_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr) +{ + /* Py_ssize_t to PyObject converter */ + return PyInt_FromSsize_t(*size_ptr); +} + +static Py_ssize_t +ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars) +{ + /* Escape unicode code point c to ASCII escape sequences + in char *output. output must have at least 12 bytes unused to + accommodate an escaped surrogate pair "\uXXXX\uXXXX" */ + output[chars++] = '\\'; + switch (c) { + case '\\': output[chars++] = (char)c; break; + case '"': output[chars++] = (char)c; break; + case '\b': output[chars++] = 'b'; break; + case '\f': output[chars++] = 'f'; break; + case '\n': output[chars++] = 'n'; break; + case '\r': output[chars++] = 'r'; break; + case '\t': output[chars++] = 't'; break; + default: +#ifdef Py_UNICODE_WIDE + if (c >= 0x10000) { + /* UTF-16 surrogate pair */ + Py_UNICODE v = c - 0x10000; + c = 0xd800 | ((v >> 10) & 0x3ff); + output[chars++] = 'u'; + output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; + output[chars++] = "0123456789abcdef"[(c ) & 0xf]; + c = 0xdc00 | (v & 0x3ff); + output[chars++] = '\\'; + } +#endif + output[chars++] = 'u'; + output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; + output[chars++] = "0123456789abcdef"[(c ) & 0xf]; + } + return chars; +} + +static PyObject * +ascii_escape_unicode(PyObject *pystr) +{ + /* Take a PyUnicode pystr and return a new ASCII-only escaped PyString */ + Py_ssize_t i; + Py_ssize_t input_chars; + Py_ssize_t output_size; + Py_ssize_t max_output_size; + Py_ssize_t chars; + PyObject *rval; + char *output; + Py_UNICODE *input_unicode; + + input_chars = PyUnicode_GET_SIZE(pystr); + input_unicode = PyUnicode_AS_UNICODE(pystr); + + /* One char input can be up to 6 chars output, estimate 4 of these */ + output_size = 2 + (MIN_EXPANSION * 4) + input_chars; + max_output_size = 2 + (input_chars * MAX_EXPANSION); + rval = PyString_FromStringAndSize(NULL, output_size); + if (rval == NULL) { + return NULL; + } + output = PyString_AS_STRING(rval); + chars = 0; + output[chars++] = '"'; + for (i = 0; i < input_chars; i++) { + Py_UNICODE c = input_unicode[i]; + if (S_CHAR(c)) { + output[chars++] = (char)c; + } + else { + chars = ascii_escape_char(c, output, chars); + } + if (output_size - chars < (1 + MAX_EXPANSION)) { + /* There's more than four, so let's resize by a lot */ + Py_ssize_t new_output_size = output_size * 2; + /* This is an upper bound */ + if (new_output_size > max_output_size) { + new_output_size = max_output_size; + } + /* Make sure that the output size changed before resizing */ + if (new_output_size != output_size) { + output_size = new_output_size; + if (_PyString_Resize(&rval, output_size) == -1) { + return NULL; + } + output = PyString_AS_STRING(rval); + } + } + } + output[chars++] = '"'; + if (_PyString_Resize(&rval, chars) == -1) { + return NULL; + } + return rval; +} + +static PyObject * +ascii_escape_str(PyObject *pystr) +{ + /* Take a PyString pystr and return a new ASCII-only escaped PyString */ + Py_ssize_t i; + Py_ssize_t input_chars; + Py_ssize_t output_size; + Py_ssize_t chars; + PyObject *rval; + char *output; + char *input_str; + + input_chars = PyString_GET_SIZE(pystr); + input_str = PyString_AS_STRING(pystr); + + /* Fast path for a string that's already ASCII */ + for (i = 0; i < input_chars; i++) { + Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; + if (!S_CHAR(c)) { + /* If we have to escape something, scan the string for unicode */ + Py_ssize_t j; + for (j = i; j < input_chars; j++) { + c = (Py_UNICODE)(unsigned char)input_str[j]; + if (c > 0x7f) { + /* We hit a non-ASCII character, bail to unicode mode */ + PyObject *uni; + uni = PyUnicode_DecodeUTF8(input_str, input_chars, "strict"); + if (uni == NULL) { + return NULL; + } + rval = ascii_escape_unicode(uni); + Py_DECREF(uni); + return rval; + } + } + break; + } + } + + if (i == input_chars) { + /* Input is already ASCII */ + output_size = 2 + input_chars; + } + else { + /* One char input can be up to 6 chars output, estimate 4 of these */ + output_size = 2 + (MIN_EXPANSION * 4) + input_chars; + } + rval = PyString_FromStringAndSize(NULL, output_size); + if (rval == NULL) { + return NULL; + } + output = PyString_AS_STRING(rval); + output[0] = '"'; + + /* We know that everything up to i is ASCII already */ + chars = i + 1; + memcpy(&output[1], input_str, i); + + for (; i < input_chars; i++) { + Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; + if (S_CHAR(c)) { + output[chars++] = (char)c; + } + else { + chars = ascii_escape_char(c, output, chars); + } + /* An ASCII char can't possibly expand to a surrogate! */ + if (output_size - chars < (1 + MIN_EXPANSION)) { + /* There's more than four, so let's resize by a lot */ + output_size *= 2; + if (output_size > 2 + (input_chars * MIN_EXPANSION)) { + output_size = 2 + (input_chars * MIN_EXPANSION); + } + if (_PyString_Resize(&rval, output_size) == -1) { + return NULL; + } + output = PyString_AS_STRING(rval); + } + } + output[chars++] = '"'; + if (_PyString_Resize(&rval, chars) == -1) { + return NULL; + } + return rval; +} + +static void +raise_errmsg(char *msg, PyObject *s, Py_ssize_t end) +{ + /* Use the Python function simplejson.decoder.errmsg to raise a nice + looking ValueError exception */ + static PyObject *JSONDecodeError = NULL; + PyObject *exc; + if (JSONDecodeError == NULL) { + PyObject *decoder = PyImport_ImportModule("simplejson.decoder"); + if (decoder == NULL) + return; + JSONDecodeError = PyObject_GetAttrString(decoder, "JSONDecodeError"); + Py_DECREF(decoder); + if (JSONDecodeError == NULL) + return; + } + exc = PyObject_CallFunction(JSONDecodeError, "(zOO&)", msg, s, _convertPyInt_FromSsize_t, &end); + if (exc) { + PyErr_SetObject(JSONDecodeError, exc); + Py_DECREF(exc); + } +} + +static PyObject * +join_list_unicode(PyObject *lst) +{ + /* return u''.join(lst) */ + static PyObject *joinfn = NULL; + if (joinfn == NULL) { + PyObject *ustr = PyUnicode_FromUnicode(NULL, 0); + if (ustr == NULL) + return NULL; + + joinfn = PyObject_GetAttrString(ustr, "join"); + Py_DECREF(ustr); + if (joinfn == NULL) + return NULL; + } + return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); +} + +static PyObject * +join_list_string(PyObject *lst) +{ + /* return ''.join(lst) */ + static PyObject *joinfn = NULL; + if (joinfn == NULL) { + PyObject *ustr = PyString_FromStringAndSize(NULL, 0); + if (ustr == NULL) + return NULL; + + joinfn = PyObject_GetAttrString(ustr, "join"); + Py_DECREF(ustr); + if (joinfn == NULL) + return NULL; + } + return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); +} + +static PyObject * +_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx) { + /* return (rval, idx) tuple, stealing reference to rval */ + PyObject *tpl; + PyObject *pyidx; + /* + steal a reference to rval, returns (rval, idx) + */ + if (rval == NULL) { + return NULL; + } + pyidx = PyInt_FromSsize_t(idx); + if (pyidx == NULL) { + Py_DECREF(rval); + return NULL; + } + tpl = PyTuple_New(2); + if (tpl == NULL) { + Py_DECREF(pyidx); + Py_DECREF(rval); + return NULL; + } + PyTuple_SET_ITEM(tpl, 0, rval); + PyTuple_SET_ITEM(tpl, 1, pyidx); + return tpl; +} + +#define APPEND_OLD_CHUNK \ + if (chunk != NULL) { \ + if (chunks == NULL) { \ + chunks = PyList_New(0); \ + if (chunks == NULL) { \ + goto bail; \ + } \ + } \ + if (PyList_Append(chunks, chunk)) { \ + goto bail; \ + } \ + Py_CLEAR(chunk); \ + } + +static PyObject * +scanstring_str(PyObject *pystr, Py_ssize_t end, char *encoding, int strict, Py_ssize_t *next_end_ptr) +{ + /* Read the JSON string from PyString pystr. + end is the index of the first character after the quote. + encoding is the encoding of pystr (must be an ASCII superset) + if strict is zero then literal control characters are allowed + *next_end_ptr is a return-by-reference index of the character + after the end quote + + Return value is a new PyString (if ASCII-only) or PyUnicode + */ + PyObject *rval; + Py_ssize_t len = PyString_GET_SIZE(pystr); + Py_ssize_t begin = end - 1; + Py_ssize_t next = begin; + int has_unicode = 0; + char *buf = PyString_AS_STRING(pystr); + PyObject *chunks = NULL; + PyObject *chunk = NULL; + + if (len == end) { + raise_errmsg("Unterminated string starting at", pystr, begin); + } + else if (end < 0 || len < end) { + PyErr_SetString(PyExc_ValueError, "end is out of bounds"); + goto bail; + } + while (1) { + /* Find the end of the string or the next escape */ + Py_UNICODE c = 0; + for (next = end; next < len; next++) { + c = (unsigned char)buf[next]; + if (c == '"' || c == '\\') { + break; + } + else if (strict && c <= 0x1f) { + raise_errmsg("Invalid control character at", pystr, next); + goto bail; + } + else if (c > 0x7f) { + has_unicode = 1; + } + } + if (!(c == '"' || c == '\\')) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + /* Pick up this chunk if it's not zero length */ + if (next != end) { + PyObject *strchunk; + APPEND_OLD_CHUNK + strchunk = PyString_FromStringAndSize(&buf[end], next - end); + if (strchunk == NULL) { + goto bail; + } + if (has_unicode) { + chunk = PyUnicode_FromEncodedObject(strchunk, encoding, NULL); + Py_DECREF(strchunk); + if (chunk == NULL) { + goto bail; + } + } + else { + chunk = strchunk; + } + } + next++; + if (c == '"') { + end = next; + break; + } + if (next == len) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + c = buf[next]; + if (c != 'u') { + /* Non-unicode backslash escapes */ + end = next + 1; + switch (c) { + case '"': break; + case '\\': break; + case '/': break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = 0; + } + if (c == 0) { + raise_errmsg("Invalid \\escape", pystr, end - 2); + goto bail; + } + } + else { + c = 0; + next++; + end = next + 4; + if (end >= len) { + raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); + goto bail; + } + /* Decode 4 hex digits */ + for (; next < end; next++) { + Py_UNICODE digit = buf[next]; + c <<= 4; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } +#ifdef Py_UNICODE_WIDE + /* Surrogate pair */ + if ((c & 0xfc00) == 0xd800) { + Py_UNICODE c2 = 0; + if (end + 6 >= len) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + if (buf[next++] != '\\' || buf[next++] != 'u') { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + end += 6; + /* Decode 4 hex digits */ + for (; next < end; next++) { + c2 <<= 4; + Py_UNICODE digit = buf[next]; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c2 |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c2 |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c2 |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } + if ((c2 & 0xfc00) != 0xdc00) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); + } + else if ((c & 0xfc00) == 0xdc00) { + raise_errmsg("Unpaired low surrogate", pystr, end - 5); + goto bail; + } +#endif + } + if (c > 0x7f) { + has_unicode = 1; + } + APPEND_OLD_CHUNK + if (has_unicode) { + chunk = PyUnicode_FromUnicode(&c, 1); + if (chunk == NULL) { + goto bail; + } + } + else { + char c_char = Py_CHARMASK(c); + chunk = PyString_FromStringAndSize(&c_char, 1); + if (chunk == NULL) { + goto bail; + } + } + } + + if (chunks == NULL) { + if (chunk != NULL) + rval = chunk; + else + rval = PyString_FromStringAndSize("", 0); + } + else { + APPEND_OLD_CHUNK + rval = join_list_string(chunks); + if (rval == NULL) { + goto bail; + } + Py_CLEAR(chunks); + } + + *next_end_ptr = end; + return rval; +bail: + *next_end_ptr = -1; + Py_XDECREF(chunk); + Py_XDECREF(chunks); + return NULL; +} + + +static PyObject * +scanstring_unicode(PyObject *pystr, Py_ssize_t end, int strict, Py_ssize_t *next_end_ptr) +{ + /* Read the JSON string from PyUnicode pystr. + end is the index of the first character after the quote. + if strict is zero then literal control characters are allowed + *next_end_ptr is a return-by-reference index of the character + after the end quote + + Return value is a new PyUnicode + */ + PyObject *rval; + Py_ssize_t len = PyUnicode_GET_SIZE(pystr); + Py_ssize_t begin = end - 1; + Py_ssize_t next = begin; + const Py_UNICODE *buf = PyUnicode_AS_UNICODE(pystr); + PyObject *chunks = NULL; + PyObject *chunk = NULL; + + if (len == end) { + raise_errmsg("Unterminated string starting at", pystr, begin); + } + else if (end < 0 || len < end) { + PyErr_SetString(PyExc_ValueError, "end is out of bounds"); + goto bail; + } + while (1) { + /* Find the end of the string or the next escape */ + Py_UNICODE c = 0; + for (next = end; next < len; next++) { + c = buf[next]; + if (c == '"' || c == '\\') { + break; + } + else if (strict && c <= 0x1f) { + raise_errmsg("Invalid control character at", pystr, next); + goto bail; + } + } + if (!(c == '"' || c == '\\')) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + /* Pick up this chunk if it's not zero length */ + if (next != end) { + APPEND_OLD_CHUNK + chunk = PyUnicode_FromUnicode(&buf[end], next - end); + if (chunk == NULL) { + goto bail; + } + } + next++; + if (c == '"') { + end = next; + break; + } + if (next == len) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + c = buf[next]; + if (c != 'u') { + /* Non-unicode backslash escapes */ + end = next + 1; + switch (c) { + case '"': break; + case '\\': break; + case '/': break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = 0; + } + if (c == 0) { + raise_errmsg("Invalid \\escape", pystr, end - 2); + goto bail; + } + } + else { + c = 0; + next++; + end = next + 4; + if (end >= len) { + raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); + goto bail; + } + /* Decode 4 hex digits */ + for (; next < end; next++) { + Py_UNICODE digit = buf[next]; + c <<= 4; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } +#ifdef Py_UNICODE_WIDE + /* Surrogate pair */ + if ((c & 0xfc00) == 0xd800) { + Py_UNICODE c2 = 0; + if (end + 6 >= len) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + if (buf[next++] != '\\' || buf[next++] != 'u') { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + end += 6; + /* Decode 4 hex digits */ + for (; next < end; next++) { + c2 <<= 4; + Py_UNICODE digit = buf[next]; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c2 |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c2 |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c2 |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } + if ((c2 & 0xfc00) != 0xdc00) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); + } + else if ((c & 0xfc00) == 0xdc00) { + raise_errmsg("Unpaired low surrogate", pystr, end - 5); + goto bail; + } +#endif + } + APPEND_OLD_CHUNK + chunk = PyUnicode_FromUnicode(&c, 1); + if (chunk == NULL) { + goto bail; + } + } + + if (chunks == NULL) { + if (chunk != NULL) + rval = chunk; + else + rval = PyUnicode_FromUnicode(NULL, 0); + } + else { + APPEND_OLD_CHUNK + rval = join_list_unicode(chunks); + if (rval == NULL) { + goto bail; + } + Py_CLEAR(chunks); + } + *next_end_ptr = end; + return rval; +bail: + *next_end_ptr = -1; + Py_XDECREF(chunk); + Py_XDECREF(chunks); + return NULL; +} + +PyDoc_STRVAR(pydoc_scanstring, + "scanstring(basestring, end, encoding, strict=True) -> (str, end)\n" + "\n" + "Scan the string s for a JSON string. End is the index of the\n" + "character in s after the quote that started the JSON string.\n" + "Unescapes all valid JSON string escape sequences and raises ValueError\n" + "on attempt to decode an invalid string. If strict is False then literal\n" + "control characters are allowed in the string.\n" + "\n" + "Returns a tuple of the decoded string and the index of the character in s\n" + "after the end quote." +); + +static PyObject * +py_scanstring(PyObject* self UNUSED, PyObject *args) +{ + PyObject *pystr; + PyObject *rval; + Py_ssize_t end; + Py_ssize_t next_end = -1; + char *encoding = NULL; + int strict = 1; + if (!PyArg_ParseTuple(args, "OO&|zi:scanstring", &pystr, _convertPyInt_AsSsize_t, &end, &encoding, &strict)) { + return NULL; + } + if (encoding == NULL) { + encoding = DEFAULT_ENCODING; + } + if (PyString_Check(pystr)) { + rval = scanstring_str(pystr, end, encoding, strict, &next_end); + } + else if (PyUnicode_Check(pystr)) { + rval = scanstring_unicode(pystr, end, strict, &next_end); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } + return _build_rval_index_tuple(rval, next_end); +} + +PyDoc_STRVAR(pydoc_encode_basestring_ascii, + "encode_basestring_ascii(basestring) -> str\n" + "\n" + "Return an ASCII-only JSON representation of a Python string" +); + +static PyObject * +py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr) +{ + /* Return an ASCII-only JSON representation of a Python string */ + /* METH_O */ + if (PyString_Check(pystr)) { + return ascii_escape_str(pystr); + } + else if (PyUnicode_Check(pystr)) { + return ascii_escape_unicode(pystr); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } +} + +static void +scanner_dealloc(PyObject *self) +{ + /* Deallocate scanner object */ + scanner_clear(self); + Py_TYPE(self)->tp_free(self); +} + +static int +scanner_traverse(PyObject *self, visitproc visit, void *arg) +{ + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + Py_VISIT(s->encoding); + Py_VISIT(s->strict); + Py_VISIT(s->object_hook); + Py_VISIT(s->pairs_hook); + Py_VISIT(s->parse_float); + Py_VISIT(s->parse_int); + Py_VISIT(s->parse_constant); + Py_VISIT(s->memo); + return 0; +} + +static int +scanner_clear(PyObject *self) +{ + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + Py_CLEAR(s->encoding); + Py_CLEAR(s->strict); + Py_CLEAR(s->object_hook); + Py_CLEAR(s->pairs_hook); + Py_CLEAR(s->parse_float); + Py_CLEAR(s->parse_int); + Py_CLEAR(s->parse_constant); + Py_CLEAR(s->memo); + return 0; +} + +static PyObject * +_parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON object from PyString pystr. + idx is the index of the first character after the opening curly brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing curly brace. + + Returns a new PyObject (usually a dict, but object_hook or + object_pairs_hook can change that) + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + PyObject *rval = NULL; + PyObject *pairs = NULL; + PyObject *item; + PyObject *key = NULL; + PyObject *val = NULL; + char *encoding = PyString_AS_STRING(s->encoding); + int strict = PyObject_IsTrue(s->strict); + int has_pairs_hook = (s->pairs_hook != Py_None); + Py_ssize_t next_idx; + if (has_pairs_hook) { + pairs = PyList_New(0); + if (pairs == NULL) + return NULL; + } + else { + rval = PyDict_New(); + if (rval == NULL) + return NULL; + } + + /* skip whitespace after { */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the object is non-empty */ + if (idx <= end_idx && str[idx] != '}') { + while (idx <= end_idx) { + PyObject *memokey; + + /* read key */ + if (str[idx] != '"') { + raise_errmsg( + "Expecting property name enclosed in double quotes", + pystr, idx); + goto bail; + } + key = scanstring_str(pystr, idx + 1, encoding, strict, &next_idx); + if (key == NULL) + goto bail; + memokey = PyDict_GetItem(s->memo, key); + if (memokey != NULL) { + Py_INCREF(memokey); + Py_DECREF(key); + key = memokey; + } + else { + if (PyDict_SetItem(s->memo, key, key) < 0) + goto bail; + } + idx = next_idx; + + /* skip whitespace between key and : delimiter, read :, skip whitespace */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + if (idx > end_idx || str[idx] != ':') { + raise_errmsg("Expecting ':' delimiter", pystr, idx); + goto bail; + } + idx++; + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* read any JSON data type */ + val = scan_once_str(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (has_pairs_hook) { + item = PyTuple_Pack(2, key, val); + if (item == NULL) + goto bail; + Py_CLEAR(key); + Py_CLEAR(val); + if (PyList_Append(pairs, item) == -1) { + Py_DECREF(item); + goto bail; + } + Py_DECREF(item); + } + else { + if (PyDict_SetItem(rval, key, val) < 0) + goto bail; + Py_CLEAR(key); + Py_CLEAR(val); + } + idx = next_idx; + + /* skip whitespace before } or , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the object is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == '}') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting ',' delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , delimiter */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + /* verify that idx < end_idx, str[idx] should be '}' */ + if (idx > end_idx || str[idx] != '}') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + + /* if pairs_hook is not None: rval = object_pairs_hook(pairs) */ + if (s->pairs_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->pairs_hook, pairs, NULL); + if (val == NULL) + goto bail; + Py_DECREF(pairs); + *next_idx_ptr = idx + 1; + return val; + } + + /* if object_hook is not None: rval = object_hook(rval) */ + if (s->object_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + if (val == NULL) + goto bail; + Py_DECREF(rval); + rval = val; + val = NULL; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(rval); + Py_XDECREF(key); + Py_XDECREF(val); + Py_XDECREF(pairs); + return NULL; +} + +static PyObject * +_parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON object from PyUnicode pystr. + idx is the index of the first character after the opening curly brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing curly brace. + + Returns a new PyObject (usually a dict, but object_hook can change that) + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + PyObject *rval = NULL; + PyObject *pairs = NULL; + PyObject *item; + PyObject *key = NULL; + PyObject *val = NULL; + int strict = PyObject_IsTrue(s->strict); + int has_pairs_hook = (s->pairs_hook != Py_None); + Py_ssize_t next_idx; + + if (has_pairs_hook) { + pairs = PyList_New(0); + if (pairs == NULL) + return NULL; + } + else { + rval = PyDict_New(); + if (rval == NULL) + return NULL; + } + + /* skip whitespace after { */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the object is non-empty */ + if (idx <= end_idx && str[idx] != '}') { + while (idx <= end_idx) { + PyObject *memokey; + + /* read key */ + if (str[idx] != '"') { + raise_errmsg( + "Expecting property name enclosed in double quotes", + pystr, idx); + goto bail; + } + key = scanstring_unicode(pystr, idx + 1, strict, &next_idx); + if (key == NULL) + goto bail; + memokey = PyDict_GetItem(s->memo, key); + if (memokey != NULL) { + Py_INCREF(memokey); + Py_DECREF(key); + key = memokey; + } + else { + if (PyDict_SetItem(s->memo, key, key) < 0) + goto bail; + } + idx = next_idx; + + /* skip whitespace between key and : delimiter, read :, skip + whitespace */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + if (idx > end_idx || str[idx] != ':') { + raise_errmsg("Expecting ':' delimiter", pystr, idx); + goto bail; + } + idx++; + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* read any JSON term */ + val = scan_once_unicode(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (has_pairs_hook) { + item = PyTuple_Pack(2, key, val); + if (item == NULL) + goto bail; + Py_CLEAR(key); + Py_CLEAR(val); + if (PyList_Append(pairs, item) == -1) { + Py_DECREF(item); + goto bail; + } + Py_DECREF(item); + } + else { + if (PyDict_SetItem(rval, key, val) < 0) + goto bail; + Py_CLEAR(key); + Py_CLEAR(val); + } + idx = next_idx; + + /* skip whitespace before } or , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the object is closed or we didn't get the , + delimiter */ + if (idx > end_idx) break; + if (str[idx] == '}') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting ',' delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , delimiter */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be '}' */ + if (idx > end_idx || str[idx] != '}') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + + /* if pairs_hook is not None: rval = object_pairs_hook(pairs) */ + if (s->pairs_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->pairs_hook, pairs, NULL); + if (val == NULL) + goto bail; + Py_DECREF(pairs); + *next_idx_ptr = idx + 1; + return val; + } + + /* if object_hook is not None: rval = object_hook(rval) */ + if (s->object_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + if (val == NULL) + goto bail; + Py_DECREF(rval); + rval = val; + val = NULL; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(rval); + Py_XDECREF(key); + Py_XDECREF(val); + Py_XDECREF(pairs); + return NULL; +} + +static PyObject * +_parse_array_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON array from PyString pystr. + idx is the index of the first character after the opening brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing brace. + + Returns a new PyList + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyList_New(0); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after [ */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the array is non-empty */ + if (idx <= end_idx && str[idx] != ']') { + while (idx <= end_idx) { + + /* read any JSON term and de-tuplefy the (rval, idx) */ + val = scan_once_str(s, pystr, idx, &next_idx); + if (val == NULL) { + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + raise_errmsg("Expecting object", pystr, idx); + } + goto bail; + } + + if (PyList_Append(rval, val) == -1) + goto bail; + + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace between term and , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the array is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == ']') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting ',' delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be ']' */ + if (idx > end_idx || str[idx] != ']') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_array_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON array from PyString pystr. + idx is the index of the first character after the opening brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing brace. + + Returns a new PyList + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyList_New(0); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after [ */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the array is non-empty */ + if (idx <= end_idx && str[idx] != ']') { + while (idx <= end_idx) { + + /* read any JSON term */ + val = scan_once_unicode(s, pystr, idx, &next_idx); + if (val == NULL) { + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + raise_errmsg("Expecting object", pystr, idx); + } + goto bail; + } + + if (PyList_Append(rval, val) == -1) + goto bail; + + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace between term and , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the array is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == ']') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting ',' delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be ']' */ + if (idx > end_idx || str[idx] != ']') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_constant(PyScannerObject *s, char *constant, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON constant from PyString pystr. + constant is the constant string that was found + ("NaN", "Infinity", "-Infinity"). + idx is the index of the first character of the constant + *next_idx_ptr is a return-by-reference index to the first character after + the constant. + + Returns the result of parse_constant + */ + PyObject *cstr; + PyObject *rval; + /* constant is "NaN", "Infinity", or "-Infinity" */ + cstr = PyString_InternFromString(constant); + if (cstr == NULL) + return NULL; + + /* rval = parse_constant(constant) */ + rval = PyObject_CallFunctionObjArgs(s->parse_constant, cstr, NULL); + idx += PyString_GET_SIZE(cstr); + Py_DECREF(cstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +_match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { + /* Read a JSON number from PyString pystr. + idx is the index of the first character of the number + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of that number: + PyInt, PyLong, or PyFloat. + May return other types if parse_int or parse_float are set + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + Py_ssize_t idx = start; + int is_float = 0; + PyObject *rval; + PyObject *numstr; + + /* read a sign if it's there, make sure it's not the end of the string */ + if (str[idx] == '-') { + idx++; + if (idx > end_idx) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + } + + /* read as many integer digits as we find as long as it doesn't start with 0 */ + if (str[idx] >= '1' && str[idx] <= '9') { + idx++; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + /* if it starts with 0 we only expect one integer digit */ + else if (str[idx] == '0') { + idx++; + } + /* no integer digits, error */ + else { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + /* if the next char is '.' followed by a digit then read all float digits */ + if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { + is_float = 1; + idx += 2; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + + /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ + if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { + + /* save the index of the 'e' or 'E' just in case we need to backtrack */ + Py_ssize_t e_start = idx; + idx++; + + /* read an exponent sign if present */ + if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; + + /* read all digits */ + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + + /* if we got a digit, then parse as float. if not, backtrack */ + if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { + is_float = 1; + } + else { + idx = e_start; + } + } + + /* copy the section we determined to be a number */ + numstr = PyString_FromStringAndSize(&str[start], idx - start); + if (numstr == NULL) + return NULL; + if (is_float) { + /* parse as a float using a fast path if available, otherwise call user defined method */ + if (s->parse_float != (PyObject *)&PyFloat_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + } + else { + /* rval = PyFloat_FromDouble(PyOS_ascii_atof(PyString_AS_STRING(numstr))); */ + double d = PyOS_string_to_double(PyString_AS_STRING(numstr), + NULL, NULL); + if (d == -1.0 && PyErr_Occurred()) + return NULL; + rval = PyFloat_FromDouble(d); + } + } + else { + /* parse as an int using a fast path if available, otherwise call user defined method */ + if (s->parse_int != (PyObject *)&PyInt_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + } + else { + rval = PyInt_FromString(PyString_AS_STRING(numstr), NULL, 10); + } + } + Py_DECREF(numstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +_match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { + /* Read a JSON number from PyUnicode pystr. + idx is the index of the first character of the number + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of that number: + PyInt, PyLong, or PyFloat. + May return other types if parse_int or parse_float are set + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + Py_ssize_t idx = start; + int is_float = 0; + PyObject *rval; + PyObject *numstr; + + /* read a sign if it's there, make sure it's not the end of the string */ + if (str[idx] == '-') { + idx++; + if (idx > end_idx) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + } + + /* read as many integer digits as we find as long as it doesn't start with 0 */ + if (str[idx] >= '1' && str[idx] <= '9') { + idx++; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + /* if it starts with 0 we only expect one integer digit */ + else if (str[idx] == '0') { + idx++; + } + /* no integer digits, error */ + else { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + /* if the next char is '.' followed by a digit then read all float digits */ + if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { + is_float = 1; + idx += 2; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + + /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ + if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { + Py_ssize_t e_start = idx; + idx++; + + /* read an exponent sign if present */ + if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; + + /* read all digits */ + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + + /* if we got a digit, then parse as float. if not, backtrack */ + if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { + is_float = 1; + } + else { + idx = e_start; + } + } + + /* copy the section we determined to be a number */ + numstr = PyUnicode_FromUnicode(&str[start], idx - start); + if (numstr == NULL) + return NULL; + if (is_float) { + /* parse as a float using a fast path if available, otherwise call user defined method */ + if (s->parse_float != (PyObject *)&PyFloat_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + } + else { + rval = PyFloat_FromString(numstr, NULL); + } + } + else { + /* no fast path for unicode -> int, just call */ + rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + } + Py_DECREF(numstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) +{ + /* Read one JSON term (of any kind) from PyString pystr. + idx is the index of the first character of the term + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of the term. + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t length = PyString_GET_SIZE(pystr); + PyObject *rval = NULL; + int fallthrough = 0; + if (idx >= length) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + if (Py_EnterRecursiveCall(" while decoding a JSON document")) + return NULL; + switch (str[idx]) { + case '"': + /* string */ + rval = scanstring_str(pystr, idx + 1, + PyString_AS_STRING(s->encoding), + PyObject_IsTrue(s->strict), + next_idx_ptr); + break; + case '{': + /* object */ + rval = _parse_object_str(s, pystr, idx + 1, next_idx_ptr); + break; + case '[': + /* array */ + rval = _parse_array_str(s, pystr, idx + 1, next_idx_ptr); + break; + case 'n': + /* null */ + if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { + Py_INCREF(Py_None); + *next_idx_ptr = idx + 4; + rval = Py_None; + } + else + fallthrough = 1; + break; + case 't': + /* true */ + if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { + Py_INCREF(Py_True); + *next_idx_ptr = idx + 4; + rval = Py_True; + } + else + fallthrough = 1; + break; + case 'f': + /* false */ + if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { + Py_INCREF(Py_False); + *next_idx_ptr = idx + 5; + rval = Py_False; + } + else + fallthrough = 1; + break; + case 'N': + /* NaN */ + if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { + rval = _parse_constant(s, "NaN", idx, next_idx_ptr); + } + else + fallthrough = 1; + break; + case 'I': + /* Infinity */ + if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { + rval = _parse_constant(s, "Infinity", idx, next_idx_ptr); + } + else + fallthrough = 1; + break; + case '-': + /* -Infinity */ + if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { + rval = _parse_constant(s, "-Infinity", idx, next_idx_ptr); + } + else + fallthrough = 1; + break; + default: + fallthrough = 1; + } + /* Didn't find a string, object, array, or named constant. Look for a number. */ + if (fallthrough) + rval = _match_number_str(s, pystr, idx, next_idx_ptr); + Py_LeaveRecursiveCall(); + return rval; +} + +static PyObject * +scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) +{ + /* Read one JSON term (of any kind) from PyUnicode pystr. + idx is the index of the first character of the term + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of the term. + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t length = PyUnicode_GET_SIZE(pystr); + PyObject *rval = NULL; + int fallthrough = 0; + if (idx >= length) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + if (Py_EnterRecursiveCall(" while decoding a JSON document")) + return NULL; + switch (str[idx]) { + case '"': + /* string */ + rval = scanstring_unicode(pystr, idx + 1, + PyObject_IsTrue(s->strict), + next_idx_ptr); + break; + case '{': + /* object */ + rval = _parse_object_unicode(s, pystr, idx + 1, next_idx_ptr); + break; + case '[': + /* array */ + rval = _parse_array_unicode(s, pystr, idx + 1, next_idx_ptr); + break; + case 'n': + /* null */ + if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { + Py_INCREF(Py_None); + *next_idx_ptr = idx + 4; + rval = Py_None; + } + else + fallthrough = 1; + break; + case 't': + /* true */ + if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { + Py_INCREF(Py_True); + *next_idx_ptr = idx + 4; + rval = Py_True; + } + else + fallthrough = 1; + break; + case 'f': + /* false */ + if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { + Py_INCREF(Py_False); + *next_idx_ptr = idx + 5; + rval = Py_False; + } + else + fallthrough = 1; + break; + case 'N': + /* NaN */ + if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { + rval = _parse_constant(s, "NaN", idx, next_idx_ptr); + } + else + fallthrough = 1; + break; + case 'I': + /* Infinity */ + if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { + rval = _parse_constant(s, "Infinity", idx, next_idx_ptr); + } + else + fallthrough = 1; + break; + case '-': + /* -Infinity */ + if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { + rval = _parse_constant(s, "-Infinity", idx, next_idx_ptr); + } + else + fallthrough = 1; + break; + default: + fallthrough = 1; + } + /* Didn't find a string, object, array, or named constant. Look for a number. */ + if (fallthrough) + rval = _match_number_unicode(s, pystr, idx, next_idx_ptr); + Py_LeaveRecursiveCall(); + return rval; +} + +static PyObject * +scanner_call(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Python callable interface to scan_once_{str,unicode} */ + PyObject *pystr; + PyObject *rval; + Py_ssize_t idx; + Py_ssize_t next_idx = -1; + static char *kwlist[] = {"string", "idx", NULL}; + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:scan_once", kwlist, &pystr, _convertPyInt_AsSsize_t, &idx)) + return NULL; + + if (PyString_Check(pystr)) { + rval = scan_once_str(s, pystr, idx, &next_idx); + } + else if (PyUnicode_Check(pystr)) { + rval = scan_once_unicode(s, pystr, idx, &next_idx); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } + PyDict_Clear(s->memo); + return _build_rval_index_tuple(rval, next_idx); +} + +static PyObject * +scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyScannerObject *s; + s = (PyScannerObject *)type->tp_alloc(type, 0); + if (s != NULL) { + s->encoding = NULL; + s->strict = NULL; + s->object_hook = NULL; + s->pairs_hook = NULL; + s->parse_float = NULL; + s->parse_int = NULL; + s->parse_constant = NULL; + } + return (PyObject *)s; +} + +static int +scanner_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Initialize Scanner object */ + PyObject *ctx; + static char *kwlist[] = {"context", NULL}; + PyScannerObject *s; + + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:make_scanner", kwlist, &ctx)) + return -1; + + if (s->memo == NULL) { + s->memo = PyDict_New(); + if (s->memo == NULL) + goto bail; + } + + /* PyString_AS_STRING is used on encoding */ + s->encoding = PyObject_GetAttrString(ctx, "encoding"); + if (s->encoding == NULL) + goto bail; + if (s->encoding == Py_None) { + Py_DECREF(Py_None); + s->encoding = PyString_InternFromString(DEFAULT_ENCODING); + } + else if (PyUnicode_Check(s->encoding)) { + PyObject *tmp = PyUnicode_AsEncodedString(s->encoding, NULL, NULL); + Py_DECREF(s->encoding); + s->encoding = tmp; + } + if (s->encoding == NULL || !PyString_Check(s->encoding)) + goto bail; + + /* All of these will fail "gracefully" so we don't need to verify them */ + s->strict = PyObject_GetAttrString(ctx, "strict"); + if (s->strict == NULL) + goto bail; + s->object_hook = PyObject_GetAttrString(ctx, "object_hook"); + if (s->object_hook == NULL) + goto bail; + s->pairs_hook = PyObject_GetAttrString(ctx, "object_pairs_hook"); + if (s->pairs_hook == NULL) + goto bail; + s->parse_float = PyObject_GetAttrString(ctx, "parse_float"); + if (s->parse_float == NULL) + goto bail; + s->parse_int = PyObject_GetAttrString(ctx, "parse_int"); + if (s->parse_int == NULL) + goto bail; + s->parse_constant = PyObject_GetAttrString(ctx, "parse_constant"); + if (s->parse_constant == NULL) + goto bail; + + return 0; + +bail: + Py_CLEAR(s->encoding); + Py_CLEAR(s->strict); + Py_CLEAR(s->object_hook); + Py_CLEAR(s->pairs_hook); + Py_CLEAR(s->parse_float); + Py_CLEAR(s->parse_int); + Py_CLEAR(s->parse_constant); + return -1; +} + +PyDoc_STRVAR(scanner_doc, "JSON scanner object"); + +static +PyTypeObject PyScannerType = { + PyObject_HEAD_INIT(NULL) + 0, /* tp_internal */ + "simplejson._speedups.Scanner", /* tp_name */ + sizeof(PyScannerObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + scanner_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + scanner_call, /* tp_call */ + 0, /* tp_str */ + 0,/* PyObject_GenericGetAttr, */ /* tp_getattro */ + 0,/* PyObject_GenericSetAttr, */ /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + scanner_doc, /* tp_doc */ + scanner_traverse, /* tp_traverse */ + scanner_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + scanner_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + scanner_init, /* tp_init */ + 0,/* PyType_GenericAlloc, */ /* tp_alloc */ + scanner_new, /* tp_new */ + 0,/* PyObject_GC_Del, */ /* tp_free */ +}; + +static PyObject * +encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyEncoderObject *s; + s = (PyEncoderObject *)type->tp_alloc(type, 0); + if (s != NULL) { + s->markers = NULL; + s->defaultfn = NULL; + s->encoder = NULL; + s->indent = NULL; + s->key_separator = NULL; + s->item_separator = NULL; + s->sort_keys = NULL; + s->skipkeys = NULL; + s->key_memo = NULL; + s->item_sort_key = NULL; + s->Decimal = NULL; + } + return (PyObject *)s; +} + +static int +encoder_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* initialize Encoder object */ + static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", "key_memo", "use_decimal", "namedtuple_as_object", "tuple_as_array", "bigint_as_string", "item_sort_key", "Decimal", NULL}; + + PyEncoderObject *s; + PyObject *markers, *defaultfn, *encoder, *indent, *key_separator; + PyObject *item_separator, *sort_keys, *skipkeys, *allow_nan, *key_memo; + PyObject *use_decimal, *namedtuple_as_object, *tuple_as_array; + PyObject *bigint_as_string, *item_sort_key, *Decimal; + + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOOOOOOOOO:make_encoder", kwlist, + &markers, &defaultfn, &encoder, &indent, &key_separator, &item_separator, + &sort_keys, &skipkeys, &allow_nan, &key_memo, &use_decimal, + &namedtuple_as_object, &tuple_as_array, &bigint_as_string, + &item_sort_key, &Decimal)) + return -1; + + s->markers = markers; + s->defaultfn = defaultfn; + s->encoder = encoder; + s->indent = indent; + s->key_separator = key_separator; + s->item_separator = item_separator; + s->sort_keys = sort_keys; + s->skipkeys = skipkeys; + s->key_memo = key_memo; + s->fast_encode = (PyCFunction_Check(s->encoder) && PyCFunction_GetFunction(s->encoder) == (PyCFunction)py_encode_basestring_ascii); + s->allow_nan = PyObject_IsTrue(allow_nan); + s->use_decimal = PyObject_IsTrue(use_decimal); + s->namedtuple_as_object = PyObject_IsTrue(namedtuple_as_object); + s->tuple_as_array = PyObject_IsTrue(tuple_as_array); + s->bigint_as_string = PyObject_IsTrue(bigint_as_string); + s->item_sort_key = item_sort_key; + s->Decimal = Decimal; + + Py_INCREF(s->markers); + Py_INCREF(s->defaultfn); + Py_INCREF(s->encoder); + Py_INCREF(s->indent); + Py_INCREF(s->key_separator); + Py_INCREF(s->item_separator); + Py_INCREF(s->sort_keys); + Py_INCREF(s->skipkeys); + Py_INCREF(s->key_memo); + Py_INCREF(s->item_sort_key); + Py_INCREF(s->Decimal); + return 0; +} + +static PyObject * +encoder_call(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Python callable interface to encode_listencode_obj */ + static char *kwlist[] = {"obj", "_current_indent_level", NULL}; + PyObject *obj; + PyObject *rval; + Py_ssize_t indent_level; + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:_iterencode", kwlist, + &obj, _convertPyInt_AsSsize_t, &indent_level)) + return NULL; + rval = PyList_New(0); + if (rval == NULL) + return NULL; + if (encoder_listencode_obj(s, rval, obj, indent_level)) { + Py_DECREF(rval); + return NULL; + } + return rval; +} + +static PyObject * +_encoded_const(PyObject *obj) +{ + /* Return the JSON string representation of None, True, False */ + if (obj == Py_None) { + static PyObject *s_null = NULL; + if (s_null == NULL) { + s_null = PyString_InternFromString("null"); + } + Py_INCREF(s_null); + return s_null; + } + else if (obj == Py_True) { + static PyObject *s_true = NULL; + if (s_true == NULL) { + s_true = PyString_InternFromString("true"); + } + Py_INCREF(s_true); + return s_true; + } + else if (obj == Py_False) { + static PyObject *s_false = NULL; + if (s_false == NULL) { + s_false = PyString_InternFromString("false"); + } + Py_INCREF(s_false); + return s_false; + } + else { + PyErr_SetString(PyExc_ValueError, "not a const"); + return NULL; + } +} + +static PyObject * +encoder_encode_float(PyEncoderObject *s, PyObject *obj) +{ + /* Return the JSON representation of a PyFloat */ + double i = PyFloat_AS_DOUBLE(obj); + if (!Py_IS_FINITE(i)) { + if (!s->allow_nan) { + PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant"); + return NULL; + } + if (i > 0) { + return PyString_FromString("Infinity"); + } + else if (i < 0) { + return PyString_FromString("-Infinity"); + } + else { + return PyString_FromString("NaN"); + } + } + /* Use a better float format here? */ + return PyObject_Repr(obj); +} + +static PyObject * +encoder_encode_string(PyEncoderObject *s, PyObject *obj) +{ + /* Return the JSON representation of a string */ + if (s->fast_encode) + return py_encode_basestring_ascii(NULL, obj); + else + return PyObject_CallFunctionObjArgs(s->encoder, obj, NULL); +} + +static int +_steal_list_append(PyObject *lst, PyObject *stolen) +{ + /* Append stolen and then decrement its reference count */ + int rval = PyList_Append(lst, stolen); + Py_DECREF(stolen); + return rval; +} + +static int +encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level) +{ + /* Encode Python object obj to a JSON term, rval is a PyList */ + int rv = -1; + if (Py_EnterRecursiveCall(" while encoding a JSON document")) + return rv; + do { + if (obj == Py_None || obj == Py_True || obj == Py_False) { + PyObject *cstr = _encoded_const(obj); + if (cstr != NULL) + rv = _steal_list_append(rval, cstr); + } + else if (PyString_Check(obj) || PyUnicode_Check(obj)) + { + PyObject *encoded = encoder_encode_string(s, obj); + if (encoded != NULL) + rv = _steal_list_append(rval, encoded); + } + else if (PyInt_Check(obj) || PyLong_Check(obj)) { + PyObject *encoded = PyObject_Str(obj); + if (encoded != NULL) { + if (s->bigint_as_string) { + encoded = maybe_quote_bigint(encoded, obj); + if (encoded == NULL) + break; + } + rv = _steal_list_append(rval, encoded); + } + } + else if (PyFloat_Check(obj)) { + PyObject *encoded = encoder_encode_float(s, obj); + if (encoded != NULL) + rv = _steal_list_append(rval, encoded); + } + else if (s->namedtuple_as_object && _is_namedtuple(obj)) { + PyObject *newobj = PyObject_CallMethod(obj, "_asdict", NULL); + if (newobj != NULL) { + rv = encoder_listencode_dict(s, rval, newobj, indent_level); + Py_DECREF(newobj); + } + } + else if (PyList_Check(obj) || (s->tuple_as_array && PyTuple_Check(obj))) { + rv = encoder_listencode_list(s, rval, obj, indent_level); + } + else if (PyDict_Check(obj)) { + rv = encoder_listencode_dict(s, rval, obj, indent_level); + } + else if (s->use_decimal && PyObject_TypeCheck(obj, s->Decimal)) { + PyObject *encoded = PyObject_Str(obj); + if (encoded != NULL) + rv = _steal_list_append(rval, encoded); + } + else { + PyObject *ident = NULL; + PyObject *newobj; + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(obj); + if (ident == NULL) + break; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + Py_DECREF(ident); + break; + } + if (PyDict_SetItem(s->markers, ident, obj)) { + Py_DECREF(ident); + break; + } + } + newobj = PyObject_CallFunctionObjArgs(s->defaultfn, obj, NULL); + if (newobj == NULL) { + Py_XDECREF(ident); + break; + } + rv = encoder_listencode_obj(s, rval, newobj, indent_level); + Py_DECREF(newobj); + if (rv) { + Py_XDECREF(ident); + rv = -1; + } + else if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) { + Py_XDECREF(ident); + rv = -1; + } + Py_XDECREF(ident); + } + } + } while (0); + Py_LeaveRecursiveCall(); + return rv; +} + +static int +encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level) +{ + /* Encode Python dict dct a JSON term, rval is a PyList */ + static PyObject *open_dict = NULL; + static PyObject *close_dict = NULL; + static PyObject *empty_dict = NULL; + static PyObject *iteritems = NULL; + PyObject *kstr = NULL; + PyObject *ident = NULL; + PyObject *iter = NULL; + PyObject *item = NULL; + PyObject *items = NULL; + PyObject *encoded = NULL; + int skipkeys; + Py_ssize_t idx; + + if (open_dict == NULL || close_dict == NULL || empty_dict == NULL || iteritems == NULL) { + open_dict = PyString_InternFromString("{"); + close_dict = PyString_InternFromString("}"); + empty_dict = PyString_InternFromString("{}"); + iteritems = PyString_InternFromString("iteritems"); + if (open_dict == NULL || close_dict == NULL || empty_dict == NULL || iteritems == NULL) + return -1; + } + if (PyDict_Size(dct) == 0) + return PyList_Append(rval, empty_dict); + + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(dct); + if (ident == NULL) + goto bail; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + goto bail; + } + if (PyDict_SetItem(s->markers, ident, dct)) { + goto bail; + } + } + + if (PyList_Append(rval, open_dict)) + goto bail; + + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level += 1; + /* + newline_indent = '\n' + (_indent * _current_indent_level) + separator = _item_separator + newline_indent + buf += newline_indent + */ + } + + if (PyCallable_Check(s->item_sort_key)) { + if (PyDict_CheckExact(dct)) + items = PyDict_Items(dct); + else + items = PyMapping_Items(dct); + PyObject_CallMethod(items, "sort", "OO", Py_None, s->item_sort_key); + } + else if (PyObject_IsTrue(s->sort_keys)) { + /* First sort the keys then replace them with (key, value) tuples. */ + Py_ssize_t i, nitems; + if (PyDict_CheckExact(dct)) + items = PyDict_Keys(dct); + else + items = PyMapping_Keys(dct); + if (items == NULL) + goto bail; + if (!PyList_Check(items)) { + PyErr_SetString(PyExc_ValueError, "keys must return list"); + goto bail; + } + if (PyList_Sort(items) < 0) + goto bail; + nitems = PyList_GET_SIZE(items); + for (i = 0; i < nitems; i++) { + PyObject *key, *value; + key = PyList_GET_ITEM(items, i); + value = PyDict_GetItem(dct, key); + item = PyTuple_Pack(2, key, value); + if (item == NULL) + goto bail; + PyList_SET_ITEM(items, i, item); + Py_DECREF(key); + } + } + else { + if (PyDict_CheckExact(dct)) + items = PyDict_Items(dct); + else + items = PyMapping_Items(dct); + } + if (items == NULL) + goto bail; + iter = PyObject_GetIter(items); + Py_DECREF(items); + if (iter == NULL) + goto bail; + + skipkeys = PyObject_IsTrue(s->skipkeys); + idx = 0; + while ((item = PyIter_Next(iter))) { + PyObject *encoded, *key, *value; + if (!PyTuple_Check(item) || Py_SIZE(item) != 2) { + PyErr_SetString(PyExc_ValueError, "items must return 2-tuples"); + goto bail; + } + key = PyTuple_GET_ITEM(item, 0); + if (key == NULL) + goto bail; + value = PyTuple_GET_ITEM(item, 1); + if (value == NULL) + goto bail; + + encoded = PyDict_GetItem(s->key_memo, key); + if (encoded != NULL) { + Py_INCREF(encoded); + } + else if (PyString_Check(key) || PyUnicode_Check(key)) { + Py_INCREF(key); + kstr = key; + } + else if (PyFloat_Check(key)) { + kstr = encoder_encode_float(s, key); + if (kstr == NULL) + goto bail; + } + else if (key == Py_True || key == Py_False || key == Py_None) { + /* This must come before the PyInt_Check because + True and False are also 1 and 0.*/ + kstr = _encoded_const(key); + if (kstr == NULL) + goto bail; + } + else if (PyInt_Check(key) || PyLong_Check(key)) { + kstr = PyObject_Str(key); + if (kstr == NULL) + goto bail; + } + else if (skipkeys) { + Py_DECREF(item); + continue; + } + else { + /* TODO: include repr of key */ + PyErr_SetString(PyExc_TypeError, "keys must be a string"); + goto bail; + } + + if (idx) { + if (PyList_Append(rval, s->item_separator)) + goto bail; + } + + if (encoded == NULL) { + encoded = encoder_encode_string(s, kstr); + Py_CLEAR(kstr); + if (encoded == NULL) + goto bail; + if (PyDict_SetItem(s->key_memo, key, encoded)) + goto bail; + } + if (PyList_Append(rval, encoded)) { + goto bail; + } + Py_CLEAR(encoded); + if (PyList_Append(rval, s->key_separator)) + goto bail; + if (encoder_listencode_obj(s, rval, value, indent_level)) + goto bail; + Py_CLEAR(item); + idx += 1; + } + Py_CLEAR(iter); + if (PyErr_Occurred()) + goto bail; + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) + goto bail; + Py_CLEAR(ident); + } + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level -= 1; + /* + yield '\n' + (_indent * _current_indent_level) + */ + } + if (PyList_Append(rval, close_dict)) + goto bail; + return 0; + +bail: + Py_XDECREF(encoded); + Py_XDECREF(items); + Py_XDECREF(iter); + Py_XDECREF(kstr); + Py_XDECREF(ident); + return -1; +} + + +static int +encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level) +{ + /* Encode Python list seq to a JSON term, rval is a PyList */ + static PyObject *open_array = NULL; + static PyObject *close_array = NULL; + static PyObject *empty_array = NULL; + PyObject *ident = NULL; + PyObject *iter = NULL; + PyObject *obj = NULL; + int is_true; + int i = 0; + + if (open_array == NULL || close_array == NULL || empty_array == NULL) { + open_array = PyString_InternFromString("["); + close_array = PyString_InternFromString("]"); + empty_array = PyString_InternFromString("[]"); + if (open_array == NULL || close_array == NULL || empty_array == NULL) + return -1; + } + ident = NULL; + is_true = PyObject_IsTrue(seq); + if (is_true == -1) + return -1; + else if (is_true == 0) + return PyList_Append(rval, empty_array); + + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(seq); + if (ident == NULL) + goto bail; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + goto bail; + } + if (PyDict_SetItem(s->markers, ident, seq)) { + goto bail; + } + } + + iter = PyObject_GetIter(seq); + if (iter == NULL) + goto bail; + + if (PyList_Append(rval, open_array)) + goto bail; + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level += 1; + /* + newline_indent = '\n' + (_indent * _current_indent_level) + separator = _item_separator + newline_indent + buf += newline_indent + */ + } + while ((obj = PyIter_Next(iter))) { + if (i) { + if (PyList_Append(rval, s->item_separator)) + goto bail; + } + if (encoder_listencode_obj(s, rval, obj, indent_level)) + goto bail; + i++; + Py_CLEAR(obj); + } + Py_CLEAR(iter); + if (PyErr_Occurred()) + goto bail; + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) + goto bail; + Py_CLEAR(ident); + } + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level -= 1; + /* + yield '\n' + (_indent * _current_indent_level) + */ + } + if (PyList_Append(rval, close_array)) + goto bail; + return 0; + +bail: + Py_XDECREF(obj); + Py_XDECREF(iter); + Py_XDECREF(ident); + return -1; +} + +static void +encoder_dealloc(PyObject *self) +{ + /* Deallocate Encoder */ + encoder_clear(self); + Py_TYPE(self)->tp_free(self); +} + +static int +encoder_traverse(PyObject *self, visitproc visit, void *arg) +{ + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + Py_VISIT(s->markers); + Py_VISIT(s->defaultfn); + Py_VISIT(s->encoder); + Py_VISIT(s->indent); + Py_VISIT(s->key_separator); + Py_VISIT(s->item_separator); + Py_VISIT(s->sort_keys); + Py_VISIT(s->skipkeys); + Py_VISIT(s->key_memo); + Py_VISIT(s->item_sort_key); + return 0; +} + +static int +encoder_clear(PyObject *self) +{ + /* Deallocate Encoder */ + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + Py_CLEAR(s->markers); + Py_CLEAR(s->defaultfn); + Py_CLEAR(s->encoder); + Py_CLEAR(s->indent); + Py_CLEAR(s->key_separator); + Py_CLEAR(s->item_separator); + Py_CLEAR(s->sort_keys); + Py_CLEAR(s->skipkeys); + Py_CLEAR(s->key_memo); + Py_CLEAR(s->item_sort_key); + Py_CLEAR(s->Decimal); + return 0; +} + +PyDoc_STRVAR(encoder_doc, "_iterencode(obj, _current_indent_level) -> iterable"); + +static +PyTypeObject PyEncoderType = { + PyObject_HEAD_INIT(NULL) + 0, /* tp_internal */ + "simplejson._speedups.Encoder", /* tp_name */ + sizeof(PyEncoderObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + encoder_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + encoder_call, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + encoder_doc, /* tp_doc */ + encoder_traverse, /* tp_traverse */ + encoder_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + encoder_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + encoder_init, /* tp_init */ + 0, /* tp_alloc */ + encoder_new, /* tp_new */ + 0, /* tp_free */ +}; + +static PyMethodDef speedups_methods[] = { + {"encode_basestring_ascii", + (PyCFunction)py_encode_basestring_ascii, + METH_O, + pydoc_encode_basestring_ascii}, + {"scanstring", + (PyCFunction)py_scanstring, + METH_VARARGS, + pydoc_scanstring}, + {NULL, NULL, 0, NULL} +}; + +PyDoc_STRVAR(module_doc, +"simplejson speedups\n"); + +void +init_speedups(void) +{ + PyObject *m; + PyScannerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&PyScannerType) < 0) + return; + PyEncoderType.tp_new = PyType_GenericNew; + if (PyType_Ready(&PyEncoderType) < 0) + return; + + + m = Py_InitModule3("_speedups", speedups_methods, module_doc); + Py_INCREF((PyObject*)&PyScannerType); + PyModule_AddObject(m, "make_scanner", (PyObject*)&PyScannerType); + Py_INCREF((PyObject*)&PyEncoderType); + PyModule_AddObject(m, "make_encoder", (PyObject*)&PyEncoderType); +} diff --git a/script/test/diffcalc (copy)/simplejson/decoder.py b/script/test/diffcalc (copy)/simplejson/decoder.py new file mode 100644 index 0000000..1c1526c --- /dev/null +++ b/script/test/diffcalc (copy)/simplejson/decoder.py @@ -0,0 +1,427 @@ +"""Implementation of JSONDecoder +""" +import re +import sys +import struct + +from simplejson.scanner import make_scanner +def _import_c_scanstring(): + try: + from simplejson._speedups import scanstring + return scanstring + except ImportError: + return None +c_scanstring = _import_c_scanstring() + +__all__ = ['JSONDecoder'] + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + # The struct module in Python 2.4 would get frexp() out of range here + # when an endian is specified in the format string. Fixed in Python 2.5+ + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +class JSONDecodeError(ValueError): + """Subclass of ValueError with the following additional properties: + + msg: The unformatted error message + doc: The JSON document being parsed + pos: The start index of doc where parsing failed + end: The end index of doc where parsing failed (may be None) + lineno: The line corresponding to pos + colno: The column corresponding to pos + endlineno: The line corresponding to end (may be None) + endcolno: The column corresponding to end (may be None) + + """ + def __init__(self, msg, doc, pos, end=None): + ValueError.__init__(self, errmsg(msg, doc, pos, end=end)) + self.msg = msg + self.doc = doc + self.pos = pos + self.end = end + self.lineno, self.colno = linecol(doc, pos) + if end is not None: + self.endlineno, self.endcolno = linecol(doc, end) + else: + self.endlineno, self.endcolno = None, None + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + # Note that this function is called from _speedups + lineno, colno = linecol(doc, pos) + if end is None: + #fmt = '{0}: line {1} column {2} (char {3})' + #return fmt.format(msg, lineno, colno, pos) + fmt = '%s: line %d column %d (char %d)' + return fmt % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' + #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) + fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' + return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, +} + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, + _b=BACKSLASH, _m=STRINGCHUNK.match): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise JSONDecodeError( + "Unterminated string starting at", s, begin) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == '"': + break + elif terminator != '\\': + if strict: + msg = "Invalid control character %r at" % (terminator,) + #msg = "Invalid control character {0!r} at".format(terminator) + raise JSONDecodeError(msg, s, end) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise JSONDecodeError( + "Unterminated string starting at", s, begin) + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: " + repr(esc) + raise JSONDecodeError(msg, s, end) + end += 1 + else: + # Unicode escape sequence + esc = s[end + 1:end + 5] + next_end = end + 5 + if len(esc) != 4: + msg = "Invalid \\uXXXX escape" + raise JSONDecodeError(msg, s, end) + uni = int(esc, 16) + # Check for surrogate pair on UCS-4 systems + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise JSONDecodeError(msg, s, end) + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise JSONDecodeError(msg, s, end) + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + char = unichr(uni) + end = next_end + # Append the unescaped character + _append(char) + return u''.join(chunks), end + + +# Use speedup if available +scanstring = c_scanstring or py_scanstring + +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + +def JSONObject((s, end), encoding, strict, scan_once, object_hook, + object_pairs_hook, memo=None, + _w=WHITESPACE.match, _ws=WHITESPACE_STR): + # Backwards compatibility + if memo is None: + memo = {} + memo_get = memo.setdefault + pairs = [] + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + 1 + pairs = {} + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + 1 + elif nextchar != '"': + raise JSONDecodeError( + "Expecting property name enclosed in double quotes", + s, end) + end += 1 + while True: + key, end = scanstring(s, end, encoding, strict) + key = memo_get(key, key) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise JSONDecodeError("Expecting ':' delimiter", s, end) + + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration: + raise JSONDecodeError("Expecting object", s, end) + pairs.append((key, value)) + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise JSONDecodeError("Expecting ',' delimiter", s, end - 1) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise JSONDecodeError( + "Expecting property name enclosed in double quotes", + s, end - 1) + + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + pairs = dict(pairs) + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + +def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + values = [] + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return values, end + 1 + _append = values.append + while True: + try: + value, end = scan_once(s, end) + except StopIteration: + raise JSONDecodeError("Expecting object", s, end) + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise JSONDecodeError("Expecting ',' delimiter", s, end) + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return values, end + +class JSONDecoder(object): + """Simple JSON decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + + """ + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True, + object_pairs_hook=None): + """ + *encoding* determines the encoding used to interpret any + :class:`str` objects decoded by this instance (``'utf-8'`` by + default). It has no effect when decoding :class:`unicode` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as :class:`unicode`. + + *object_hook*, if specified, will be called with the result of every + JSON object decoded and its return value will be used in place of the + given :class:`dict`. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + *object_pairs_hook* is an optional function that will be called with + the result of any object literal decode with an ordered list of pairs. + The return value of *object_pairs_hook* will be used instead of the + :class:`dict`. This feature can be used to implement custom decoders + that rely on the order that the key and value pairs are decoded (for + example, :func:`collections.OrderedDict` will remember the order of + insertion). If *object_hook* is also defined, the *object_pairs_hook* + takes priority. + + *parse_float*, if specified, will be called with the string of every + JSON float to be decoded. By default, this is equivalent to + ``float(num_str)``. This can be used to use another datatype or parser + for JSON floats (e.g. :class:`decimal.Decimal`). + + *parse_int*, if specified, will be called with the string of every + JSON int to be decoded. By default, this is equivalent to + ``int(num_str)``. This can be used to use another datatype or parser + for JSON integers (e.g. :class:`float`). + + *parse_constant*, if specified, will be called with one of the + following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This + can be used to raise an exception if invalid JSON numbers are + encountered. + + *strict* controls the parser's behavior when it encounters an + invalid control character in a string. The default setting of + ``True`` means that unescaped control characters are parse errors, if + ``False`` then control characters will be allowed in strings. + + """ + self.encoding = encoding + self.object_hook = object_hook + self.object_pairs_hook = object_pairs_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.strict = strict + self.parse_object = JSONObject + self.parse_array = JSONArray + self.parse_string = scanstring + self.memo = {} + self.scan_once = make_scanner(self) + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s) + end = _w(s, end).end() + if end != len(s): + raise JSONDecodeError("Extra data", s, end, len(s)) + return obj + + def raw_decode(self, s, idx=0, _w=WHITESPACE.match): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` + beginning with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + Optionally, ``idx`` can be used to specify an offset in ``s`` where + the JSON document begins. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + obj, end = self.scan_once(s, idx=_w(s, idx).end()) + except StopIteration: + raise JSONDecodeError("No JSON object could be decoded", s, idx) + return obj, end diff --git a/script/test/diffcalc (copy)/simplejson/encoder.py b/script/test/diffcalc (copy)/simplejson/encoder.py new file mode 100644 index 0000000..6b4a6a4 --- /dev/null +++ b/script/test/diffcalc (copy)/simplejson/encoder.py @@ -0,0 +1,567 @@ +"""Implementation of JSONEncoder +""" +import re +from decimal import Decimal + +def _import_speedups(): + try: + from simplejson import _speedups + return _speedups.encode_basestring_ascii, _speedups.make_encoder + except ImportError: + return None, None +c_encode_basestring_ascii, c_make_encoder = _import_speedups() + +from simplejson.decoder import PosInf + +ESCAPE = re.compile(ur'[\x00-\x1f\\"\b\f\n\r\t\u2028\u2029]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + u'\u2028': '\\u2028', + u'\u2029': '\\u2029', +} +for i in range(0x20): + #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +FLOAT_REPR = repr + +def encode_basestring(s): + """Return a JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + return ESCAPE_DCT[match.group(0)] + return u'"' + ESCAPE.sub(replace, s) + u'"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + #return '\\u{0:04x}'.format(n) + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = ( + c_encode_basestring_ascii or py_encode_basestring_ascii) + +class JSONEncoder(object): + """Extensible JSON encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict, namedtuple | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None, + use_decimal=True, namedtuple_as_object=True, + tuple_as_array=True, bigint_as_string=False, + item_sort_key=None): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is true, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a string, then JSON array elements and object members + will be pretty-printed with a newline followed by that string repeated + for each level of nesting. ``None`` (the default) selects the most compact + representation without any newlines. For backwards compatibility with + versions of simplejson earlier than 2.1.0, an integer is also accepted + and is converted to a string with that many spaces. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + If use_decimal is true (not the default), ``decimal.Decimal`` will + be supported directly by the encoder. For the inverse, decode JSON + with ``parse_float=decimal.Decimal``. + + If namedtuple_as_object is true (the default), objects with + ``_asdict()`` methods will be encoded as JSON objects. + + If tuple_as_array is true (the default), tuple (and subclasses) will + be encoded as JSON arrays. + + If bigint_as_string is true (not the default), ints 2**53 and higher + or lower than -2**53 will be encoded as strings. This is to avoid the + rounding that happens in Javascript otherwise. + + If specified, item_sort_key is a callable used to sort the items in + each dictionary. This is useful if you want to sort items other than + in alphabetical order by key. + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.use_decimal = use_decimal + self.namedtuple_as_object = namedtuple_as_object + self.tuple_as_array = tuple_as_array + self.bigint_as_string = bigint_as_string + self.item_sort_key = item_sort_key + if indent is not None and not isinstance(indent, basestring): + indent = indent * ' ' + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + elif indent is not None: + self.item_separator = ',' + if default is not None: + self.default = default + self.encoding = encoding + + def default(self, o): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + + """ + raise TypeError(repr(o) + " is not JSON serializable") + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> from simplejson import JSONEncoder + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + if self.ensure_ascii: + return ''.join(chunks) + else: + return u''.join(chunks) + + def iterencode(self, o, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, + _repr=FLOAT_REPR, _inf=PosInf, _neginf=-PosInf): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on + # the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + key_memo = {} + if (_one_shot and c_make_encoder is not None + and self.indent is None): + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan, key_memo, self.use_decimal, + self.namedtuple_as_object, self.tuple_as_array, + self.bigint_as_string, self.item_sort_key, + Decimal) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot, self.use_decimal, + self.namedtuple_as_object, self.tuple_as_array, + self.bigint_as_string, self.item_sort_key, + Decimal=Decimal) + try: + return _iterencode(o, 0) + finally: + key_memo.clear() + + +class JSONEncoderForHTML(JSONEncoder): + """An encoder that produces JSON safe to embed in HTML. + + To embed JSON content in, say, a script tag on a web page, the + characters &, < and > should be escaped. They cannot be escaped + with the usual entities (e.g. &) because they are not expanded + within