From d18a0311a80cb57a90f21a7bf4a46ee944e05369 Mon Sep 17 00:00:00 2001
From: Duc Le <duc.le@stfc.ac.uk>
Date: Sat, 26 May 2018 00:32:35 +0100
Subject: [PATCH] Re #22359 - changed code to use an input yaml instrument file

Refactored code ISISDisk.py and ISISFermi.py into Instruments.py
Deprecated ISISDisk and ISISFermi
Changed GUI to use Instruments
---
 scripts/PyChop/ISISDisk.py    |   3 +-
 scripts/PyChop/ISISFermi.py   |   2 +-
 scripts/PyChop/Instruments.py | 798 ++++++++++++++++++++++++++++++++++
 scripts/PyChop/MulpyRep.py    |  25 +-
 scripts/PyChop/PyChop2.py     |   3 +
 scripts/PyChop/PyChopGui.py   |  22 +-
 scripts/PyChop/__init__.py    |   2 +-
 scripts/PyChop/let.yaml       | 138 ++++++
 scripts/PyChop/maps.yaml      | 130 ++++++
 9 files changed, 1100 insertions(+), 23 deletions(-)
 create mode 100644 scripts/PyChop/Instruments.py
 create mode 100644 scripts/PyChop/let.yaml
 create mode 100644 scripts/PyChop/maps.yaml

diff --git a/scripts/PyChop/ISISDisk.py b/scripts/PyChop/ISISDisk.py
index c9d0bfdf4a6..c73c86acc3f 100644
--- a/scripts/PyChop/ISISDisk.py
+++ b/scripts/PyChop/ISISDisk.py
@@ -20,7 +20,7 @@ class ISISDisk:
 
     def __init__(self, instname=None, variant=None, freq=None):
         warnings.warn("The ISISDisk class is deprecated and will be removed in the next Mantid version. "
-                      "Please use the Instrument class or the official PyChop2 CLI interface.", DeprecationWarning)
+                      "Please use the Instrument class or the official PyChop CLI interface.", DeprecationWarning)
         if instname:
             self.setInstrument(instname, variant)
             self.freq = 0
@@ -465,6 +465,7 @@ class ISISDisk:
         dist, samDist, DetDist, fracEi = tuple([self.dist, self.chop_samp, self.samp_det, self.frac_ei])
         modSamDist = dist[-1] + samDist
         totDist = modSamDist + DetDist
+        print(chop_times)
         for i in range(len(dist)):
             plt.plot([-20000, 120000], [dist[i], dist[i]], c='k', linewidth=1.)
             for j in range(len(chop_times[i][:])):
diff --git a/scripts/PyChop/ISISFermi.py b/scripts/PyChop/ISISFermi.py
index 950eef7b8f6..1b49160c4ab 100644
--- a/scripts/PyChop/ISISFermi.py
+++ b/scripts/PyChop/ISISFermi.py
@@ -85,7 +85,7 @@ class ISISFermi:
 
     def __init__(self, instname=None, choppername='', freq=0):
         warnings.warn("The ISISFermi class is deprecated and will be removed in the next Mantid version. "
-                      "Please use the Instrument class or the official PyChop2 CLI interface.", DeprecationWarning)
+                      "Please use the Instrument class or the official PyChop CLI interface.", DeprecationWarning)
         if instname:
             self.setInstrument(instname, choppername, freq)
             self.diskchopper_phase = self.__DiskChopperMode[instname]
diff --git a/scripts/PyChop/Instruments.py b/scripts/PyChop/Instruments.py
new file mode 100644
index 00000000000..975eccae0d3
--- /dev/null
+++ b/scripts/PyChop/Instruments.py
@@ -0,0 +1,798 @@
+"""
+This module is a wrapper around a set of instrument parameters (to be read from a YAML file)
+and methods which then call either Chop.py or MulpyRep.py to do the resolution calculations.
+"""
+
+from __future__ import (absolute_import, division, print_function)
+from six import string_types
+import numpy as np
+import yaml
+import warnings
+import copy
+try:
+    from . import Chop, MulpyRep
+except:
+    import Chop
+    import MulpyRep
+from scipy.interpolate import interp1d
+from scipy.special import erf
+from scipy import constants
+
+# Some global constants
+SIGMA2FWHM = 2 * np.sqrt(2 * np.log(2))
+SIGMA2FWHMSQ = SIGMA2FWHM ** 2
+E2V = np.sqrt((constants.e / 1000) * 2 / constants.neutron_mass) # v = E2V * sqrt(E)    veloc in m/s, E in meV
+E2L = 1.e23 * constants.h**2 / (2 * constants.m_n * constants.e) # lam = sqrt(E2L / E)  lam in Angst, E in meV
+E2K = constants.e * 2 * constants.m_n / constants.hbar**2 / 1e23 # k = sqrt(E2K * E)    k in 1/Angst, E in meV
+
+def wrap_attributes(obj, inval, allowed_var_names):
+    for key in allowed_var_names:
+        if inval:
+            if hasattr(inval, key):
+                setattr(obj, key, getattr(inval, key))
+            elif hasattr(inval, '__getitem__') and key in inval:
+                setattr(obj, key, inval[key])
+
+def argparser(args, kwargs, argnames, defaults=None):
+    argdict = {key: val for key, val in zip(argnames, defaults if defaults else [None] * len(argnames))}
+    for key in kwargs:
+        argdict[key] = kwargs[key]
+    for idx in range(len(args)):
+        argdict[argnames[idx]] = args[idx]
+    return argdict
+
+def soft_hat(x, p):
+    """
+    ! Soft hat function, from Herbert subroutine library.
+    ! For rescaling t-mod at low energy to account for broader moderator term
+    """
+    x = np.array(x)
+    sig2fwhh = np.sqrt(8*np.log(2))
+    height, grad, x1, x2 = tuple(p[:4])
+    sig1, sig2 = tuple(np.abs(p[4:6]/sig2fwhh))
+    # linearly interpolate sig for x1<x<x2
+    sig = ((x2-x)*sig1-(x1-x)*sig2)/(x2-x1)
+    if np.shape(sig):
+        sig[x < x1] = sig1
+        sig[x > x2] = sig2
+    # calculate blurred hat function with gradient
+    e1 = (x1-x) / (np.sqrt(2)*sig)
+    e2 = (x2-x) / (np.sqrt(2)*sig)
+    y = (erf(e2)-erf(e1)) * ((height+grad*(x-(x2+x1)/2))/2)
+    y = y + 1
+    return y
+
+
+class FermiChopper(object):
+    """
+    Class which represents a Fermi chopper package
+    """
+
+    __allowed_var_names = ['name', 'pslit', 'pslat', 'radius', 'rho', 'tjit', 'fluxcorr']
+
+    def __init__(self, inval=None):
+        wrap_attributes(self, inval, self.__allowed_var_names)
+
+    def __repr__(self):
+        return self.name if self.name else 'Undefined Fermi chopper package'
+
+    def getWidthSquared(self, Ei, freq):
+        return Chop.tchop(freq, Ei, self.pslit / 1000., self.radius / 1000., self.rho / 1000.)
+
+    def getWidth(self, *args):
+        """ Calculates the chopper time width in seconds for a given neutron energy (Ei) """
+        return np.sqrt(self.getWidthSquared(*args))
+
+    def getTransmission(self, Ei, freq):
+        """ Calculates the chopper transmission """
+        dslat = (self.pslit + self.pslat) / 1000
+        return Chop.achop(Ei, freq, dslat, self.pslit / 1000., self.radius / 1000., self.rho / 1000.) / dslat
+
+
+class ChopperSystem(object):
+    """
+    Class which represents a set (list) of choppers in a line
+    """
+
+    __allowed_var_names = ['name', 'chop_sam', 'sam_det', 'aperture_width', 'aperture_height', 'choppers', 'variants',
+                           'frequency_matrix', 'constant_frequencies', 'max_frequencies', 'default_frequencies',
+                           'source_rep', 'emission_time', 'overlap_ei_frac', 'ei_limits', 'flux_ref_slot', 'flux_ref_freq']
+
+    def __init__(self, inval=None):
+        # Default values
+        self.source_rep = 50
+        self.emission_time = 0
+        self.overlap_ei_frac = 0.9
+        self._ei = 22
+        # Parse input values (if any)
+        wrap_attributes(self, inval, self.__allowed_var_names)
+        self._parse_choppers()
+        self._parse_variants()
+        self.phase = self.defaultPhase
+        self.frequency = self.default_frequencies
+        
+    def __repr__(self):
+        return self.name if self.name else 'Undefined disk chopper system'
+
+    def _parse_choppers(self):
+        """Parses the choppers list to determine how to handle resolution and flux calculations"""
+        if not self.choppers:
+            return
+        self.distance = []
+        self.nslot = []
+        self.slot_ang_pos = []
+        self.slot_width = []
+        self.guide_width = []
+        self.radius = []
+        self.numDisk = []
+        self.isFermi = False
+        self.isPhaseIndependent = []
+        self.defaultPhase = []
+        for idx, chopper in enumerate(self.choppers):
+            self.distance.append(chopper['distance'])
+            if 'packages' in chopper:
+                self.isFermi = True
+                self.idxFermi = idx
+                self.packages = {key: FermiChopper(val) for key, val in list(chopper['packages'].items())}
+                self.nslot.append(1)    # Assume Fermi chopper is curved, will not transmit PI pulse.
+                self.slot_ang_pos.append(None)
+                self.slot_width.append(10.)
+                self.guide_width.append(10.)
+                self.radius.append(290.)
+                self.numDisk.append(1)
+            else:
+                self.nslot.append(
+                    chopper['nslot'] if ('nslot' in chopper and chopper['nslot']) else len(chopper['slot_ang_pos']))
+                self.slot_ang_pos.append(chopper['slot_ang_pos'] if ('slot_ang_pos' in chopper) else None)
+                self.slot_width.append(chopper['slot_width'])
+                self.guide_width.append(chopper['guide_width'])
+                self.radius.append(chopper['radius'])
+                self.numDisk.append(2 if ('isDouble' in chopper and chopper['isDouble']) else 1)
+                self.isPhaseIndependent.append(
+                    True if ('isPhaseIndependent' in chopper and chopper['isPhaseIndependent']) else False)
+                self.defaultPhase.append(chopper['defaultPhase'] if 'defaultPhase' in chopper else None)
+        if not any(self.slot_ang_pos):
+            self.slot_ang_pos = None
+        self.isPhaseIndependent = [ii for ii in range(len(self.isPhaseIndependent)) if self.isPhaseIndependent[ii]]
+        self.defaultPhase = [self.defaultPhase[ii] for ii in self.isPhaseIndependent]
+        self._instpar = [self.distance, self.nslot, self.slot_ang_pos, self.slot_width, self.guide_width, self.radius, 
+                         self.numDisk, self.sam_det, self.chop_sam, self.source_rep, self.emission_time, 
+                         self.overlap_ei_frac, self.isPhaseIndependent]
+        if self.isFermi:
+            self.package = self.packages.keys()[0]
+
+    def _parse_variants(self):
+        if 'variants' not in self.__dict__:
+            return
+        variant_keys = []
+        for var in self.variants:
+            if ('default' in self.variants[var].keys() and self.variants[var]['default']) or var is None:
+                self._default_variant = self._variant = var
+            if var:
+                [variant_keys.append(key) for key in self.variants[var].keys() if 'default' not in key]
+        self._variant_defaults = {}
+        for key in set(variant_keys):
+            self._variant_defaults[key] = copy.deepcopy(getattr(self, key))
+        if '_variant' not in self.__dict__:
+            self._default_variant = self.variants.keys()[0]
+            warnings.warn('No default variants defined. Using ''%s'' as default' % (self._default_variant), SyntaxWarning)
+            self.variant = self._default_variant
+
+    # Define getters/setters here to be backwards compatible with old PyChop2. Actually use properties underneath
+    def setChopper(self, *args, **kwargs):
+        """
+        Set the chopper package type (Fermi instruments) or variant (LET).
+
+        maps = Instrument('MAPS')
+        maps.setChopper('A', 400)                     # Sets package A at 400 Hz.
+        maps.setChopper(package='A', freq=400)        # Explicit keywords
+        let = Instrument('LET')
+        let.setChopper('High resolution', [280, 140]) # Change to the high resolution variant at 280 Hz
+        let.setChopper(variant='High resolution')
+        """
+        argdict = argparser(args, kwargs, ['package' if self.isFermi else 'variant', 'freq'])
+        if self.isFermi:
+            self.package = argdict['package']
+            if hasattr(self, 'variants') and argdict['package'] in self.variants:
+                self.variant = argdict['package']
+        elif argdict['variant']:
+            self.variant = argdict['variant']
+        if argdict['freq']:
+            self.frequency = argdict['freq']
+
+    def getChopper(self):
+        return self.package if self.isFermi else self.variant
+
+    def setFrequency(self, *args, **kwargs):
+        """
+        Set the chopper frequency(ies) and (optionally) phase(s).
+
+        maps = Instrument('MAPS', 'A')
+        maps.setFrequency(400)                        # Sets frequency to 400 Hz.
+        maps.setFrequency([400, 100], 1)              # Sets Fermi to 400Hz, disk to 100Hz, and multi-rep mode
+        maps.setFrequency(freq=[400, 100], phase=1)
+        let = Instrument('LET')
+        let.setFrequency([240, 120])                  # Sets resolution chopper to 240Hz, pulse removal to 120Hz
+        let.setFrequency([240, 120], phase=-20000)    # Additionally sets the frame overlap phase to -20000us
+        """
+        argdict = argparser(args, kwargs, ['freq', 'phase'])
+        if argdict['freq']:
+            self.frequency = argdict['freq']
+        if argdict['phase']:
+            self.phase = argdict['phase']
+
+    def getFrequency(self):
+        return self.frequency
+
+    def setEi(self, Ei):
+        """Sets the (focussed) incident energy"""
+        self.ei = Ei
+
+    def getEi(self):
+        return self.ei
+
+    def getAllowedEi(self, Ei_in=None):
+        return self._MulpyRepDriver(Ei_in, calc_res=False)[0]
+
+    def plotMultiRepFrame(self, h_plt=None, Ei_in=None, frequency=None):
+        """
+        Plots the time-distance diagram into a given Matplotlib axes, i
+        for a give focused incident energy (in meV) and chopper frequencies (in Hz).
+        """
+        if h_plt is None:
+            try:
+                from matplotlib import pyplot
+            except ImportError:
+                raise RuntimeError('plotMultiRepFrame: Cannot import matplotlib')
+            plt = pyplot
+        else:
+            plt = h_plt
+        Ei = self.ei if Ei_in is None else Ei_in
+        if Ei is None:
+            raise ValueError('Incident energy has not been specified')
+        if frequency:
+            oldfreq = self.freq
+            self.setFrequency(frequency)
+        Eis, _,  _, lines, chop_times = tuple(self._MulpyRepDriver(Ei_in, calc_res=False))
+        if frequency:
+            self.setFrequency(oldfreq)
+        modSamDist = self.distance[-1] + self.chop_sam
+        totDist = modSamDist + self.sam_det
+        for i in range(len(self.distance)):
+            plt.plot([-20000, 120000], [self.distance[i], self.distance[i]], c='k', linewidth=1.)
+            for j in range(len(chop_times[i][:])):
+                plt.plot(chop_times[i][j], [self.distance[i], self.distance[i]], c='white', linewidth=1.)
+        plt.plot([-20000, 120000], [totDist, totDist], c='k', linewidth=2.)
+        for i in range(len(lines)):
+            x0 = (-lines[i][0][1] / lines[i][0][0] - lines[i][1][1] / lines[i][1][0]) / 2.
+            x1 = ((modSamDist-lines[i][0][1]) / lines[i][0][0] + (modSamDist-lines[i][1][1]) / lines[i][1][0]) / 2.
+            plt.plot([x0, x1], [0, modSamDist], c='b')
+            x2 = ((totDist-lines[i][0][1]) / lines[i][0][0] + (totDist-lines[i][1][1]) / lines[i][1][0]) / 2.
+            lineM = totDist / x2
+            plt.plot([x1, x2], [modSamDist, totDist], c='b')
+            newline = [lineM * np.sqrt(1 + self.overlap_ei_frac), modSamDist - lineM * np.sqrt(1 + self.overlap_ei_frac) * x1]
+            x3 = (totDist-newline[1]) / (newline[0])
+            plt.plot([x1, x3], [modSamDist, totDist], c='r')
+            newline = [lineM * np.sqrt(1 - self.overlap_ei_frac), modSamDist - lineM * np.sqrt(1 - self.overlap_ei_frac) * x1]
+            x4 = (totDist-newline[1]) / (newline[0])
+            plt.plot([x1, x4], [modSamDist, totDist], c='r')
+            plt.text(x2, totDist+0.2, "{:3.1f}".format(Eis[i]))
+        xmax = (1.e6 / self.source_rep) if hasattr(self, 'source_rep') and self.source_rep else np.max(chop_times)
+        if h_plt is None:
+            plt.xlim(0, xmax)
+            plt.xlabel(r'TOF ($\mu$sec)')
+            plt.ylabel('Distance (m)')
+            plt.show()
+        else:
+            plt.set_xlim(0, xmax)
+            plt.set_xlabel(r'TOF ($\mu$sec)')
+            plt.set_ylabel(r'Distance (m)')
+
+    def getWidthSquared(self, Ei_in=None):
+        return self.getWidth(Ei_in, squared=True)
+
+    def getWidth(self, Ei_in=None, squared=False):
+        """Returns the chopper time width (FWHM) at the (final) chopper in microseconds"""
+        Ei = self.ei if Ei_in is None else Ei_in
+        if self.isFermi:
+            return self._ChopDriver(Ei_in, squared), None
+        else:
+            chop_times = self._MulpyRepDriver(Ei_in, calc_res=False)[1]
+            # Output of MulpyRep is FWHM in us - want it in seconds for later calculations
+            wd = ((chop_times[1][1] - chop_times[1][0]) / 2. / 1.e6, (chop_times[0][1] - chop_times[0][0]) / 2. / 1.e6)
+            return (wd[0]**2, wd[1]**2) if squared else wd
+
+    def getDistances(self):
+        """ Returns the (mod->final_chop, aperture->final, chop->sam, sam->det, mod-first_chop) distances for instrument """
+        mod_chop = self.choppers[-1]['distance']
+        ap_chop = self.choppers[-1]['aperture_distance'] if ('aperture_distance' in self.choppers[-1]) else mod_chop
+        return (mod_chop, ap_chop, self.chop_sam, self.sam_det, self.choppers[0]['distance'])
+
+    def getTransmission(self, Ei_in=None, frequency=None, hires=False):
+        """ Calculates the flux transmission fraction through the chopper system at specified Ei and frequency """
+        Ei = self.ei if Ei_in is None else Ei_in
+        freq = frequency if frequency is not None else self._long_frequency[-1]
+        if self.isFermi:
+            x0, x1 = (self.choppers[-1]['distance'], self.chop_sam)
+            magic = (84403.06 / x0 / (x1 + x0))          # Some magical conversion factor (??)
+            fudge = self.packages[self.package].fluxcorr # A chopper package dependent fudge factor
+            return self.packages[self.package].getTransmission(Ei, freq) * magic / fudge
+        else:
+            return (self.slot_width[-1] / self.flux_ref_slot) * (self.flux_ref_freq / freq)
+
+    def _get_state(self, Ei_in=None):
+        return hash((self.variant, self.package, tuple(self.frequency), tuple(self.phase), Ei_in if Ei_in else self.ei))
+
+    def _removeLowIntensityReps(self, Eis, lines, Ei=None):
+        # Removes reps with Ei where there are no neutrons
+        if not hasattr(self, 'ei_limits') or not self.ei_limits:
+            return Eis, lines
+        Eis = np.array(Eis)
+        idx = (Eis >= self.ei_limits[0]) * (Eis <= self.ei_limits[1])
+        # Always keeps desired rep even if outside of range
+        if Ei:
+            idx += ((np.abs(Eis - Ei) / np.abs(Eis)) < 0.1) 
+        Eis = Eis[idx]
+        lines = np.array(lines)[idx]
+        return Eis, lines
+
+    def _MulpyRepDriver(self, Ei_in=None, calc_res=True):
+        """Private method to calculate resolution for given Ei from chopper opening times"""
+        Ei = self.ei if Ei_in is None else Ei_in
+        if '_saved_state' not in self.__dict__ or (self._saved_state[0] != self._get_state(Ei)):
+            Eis, all_times, chop_times, lastChopDist, lines = MulpyRep.calcChopTimes(Ei, self._long_frequency, self._instpar, self.phase)
+            Eis, lines = self._removeLowIntensityReps(Eis, lines, Ei)
+            self._saved_state = [self._get_state(Ei), Eis, chop_times, lastChopDist, lines, all_times]
+        else:
+            Eis, chop_times, lastChopDist, lines, all_times = tuple(self._saved_state[1:])
+        if calc_res:
+            res_el, percent, chop_width, mod_width = MulpyRep.calcRes(Eis, chop_times, lastChopDist, self.chop_sam, self.sam_det)
+            return res_el, percent, chop_width, mod_width
+        else:
+            return [Eis, chop_times, lastChopDist, lines, all_times]
+
+    def _ChopDriver(self, Ei_in=None, squared=False):
+        """Private method to calculate resolution for given Ei from Fermi chopper"""
+        Ei = self.ei if Ei_in is None else Ei_in
+        if squared:
+            return self.packages[self.package].getWidthSquared(Ei, self._long_frequency[-1]) * SIGMA2FWHMSQ
+        else:
+            return self.packages[self.package].getWidth(Ei, self._long_frequency[-1]) * SIGMA2FWHM
+
+    @property
+    def frequency(self):
+        return self._frequency
+
+    @frequency.setter
+    def frequency(self, value):
+        freq = self.default_frequencies
+        if not hasattr(value, '__len__'):
+            value = [value]
+        freq = [value[i] if i < len(value) else freq[i] for i in range(len(freq))]
+        if self.max_frequencies and not (freq <= self.max_frequencies):
+            raise ValueError('Value of frequencies outside maximum allowed')
+        self._frequency = freq
+        if hasattr(self, 'constant_frequencies') and self.constant_frequencies:
+            f0 = self.constant_frequencies
+        else:
+            f0 = [0] * np.shape(self.frequency_matrix)[0]
+        self._long_frequency = np.dot(self.frequency_matrix, freq) + f0
+
+    @property
+    def phase(self):
+        return self._phase
+
+    @phase.setter
+    def phase(self, value):
+        phase = self.defaultPhase
+        if not hasattr(value, '__len__'):
+            value = [value]
+        self._phase = [value[i] if i < len(value) else phase[i] for i in range(len(phase))]
+
+    @property
+    def ei(self):
+        return self._ei
+
+    @ei.setter
+    def ei(self, value):
+        if value < 0:
+            raise ValueError('Incident neutron energy cannot be less than zero')
+        self._ei = value
+
+    @property
+    def package(self):
+        return self._package if self.isFermi else None
+
+    @package.setter
+    def package(self, value):
+        if not self.isFermi:
+            raise AttributeError('Cannot set Fermi chopper package on this instrument')
+        if value not in self.packages.keys():
+            raise ValueError('Fermi package ''%s'' not recognised. Allowed values are: %s' 
+                             % (value, ', '.join(self.packages.keys())))
+        self._package = value
+
+    @property
+    def variant(self):
+        return self._variant if hasattr(self, 'variants') and self.variants else None
+
+    def getAllowedChopper(self):
+        return self.packages.keys() if self.isFermi else (self.variants.keys() if self.variants else None)
+
+    @variant.setter
+    def variant(self, value):
+        if 'variants' not in self.__dict__:
+            raise AttributeError('This instrument has no variants to set')
+        for prop in self._variant_defaults:
+            setattr(self, prop, copy.deepcopy(self._variant_defaults[prop]))
+        self._variant = value
+        if value not in self.variants.keys():
+            raise ValueError('Variant ''%s'' not recognised. Allowed values are: %s' % (value, ', '.join(self.variants.keys())))
+        for prop in self.variants[value]:
+            if prop == 'choppers':
+                for idx, chopper in enumerate(self.variants[value][prop]):
+                    if chopper:
+                        for ky in chopper:
+                            self.choppers[idx][ky] = chopper[ky]
+            elif prop in self.__allowed_var_names:
+                setattr(self, prop, self.variants[value][prop])
+        self._parse_choppers()
+
+    @property
+    def tjit(self):
+        return self.packages[self.package].tjit if self.isFermi else 0.
+
+    @tjit.setter
+    def tjit(self, value):
+        self.packages[self.package].tjit = value
+        
+
+class Moderator(object):
+    """
+    Class which represents a neutron moderator
+    """
+
+    __allowed_var_names = ['name', 'imod', 'ch_mod', 'mod_pars', 'mod_scale_fn', 'mod_scale_par', 'theta',
+                           'source_rep', 'emission_time', 'measured_flux']
+
+    def __init__(self, inval=None):
+        wrap_attributes(self, inval, self.__allowed_var_names)
+        if hasattr(self, 'measured_flux') and 'scale_factor' in self.measured_flux:
+            self.measured_flux['flux'] = np.array(self.measured_flux['flux']) * float(self.measured_flux['scale_factor'])
+
+    def __repr__(self):
+        return self.name if self.name else 'Undefined neutron moderator'
+
+    def getWidthSquared(self, Ei):
+        """ Returns the squared time gaussian sigma width due to the sample in s^2 """
+        if self.imod == 0:
+            # CHOP outputs the Gaussian sigma^2 in s^2, we want FWHM^2 in s^2
+            tsqmod = Chop.tchi(self.mod_pars / 1000., Ei) * SIGMA2FWHMSQ
+        elif self.imod == 1:
+            tsqmod = Chop.tikeda(tuple(self.mod_pars), Ei) * SIGMA2FWHMSQ
+        elif self.imod == 2:
+            d0 = self.mod_pars[0]
+            if hasattr(self, 'mod_scale_fn') and self.mod_scale_fn:
+                try:
+                    d0 *= globals()[self.mod_scale_fn](Ei, self.mod_scale_par)
+                except KeyError:
+                    pass
+            tsqmod = Chop.tchi_2(d0 / 1000., self.mod_pars[1] / 1000., Ei) * SIGMA2FWHMSQ
+        elif self.imod == 3:
+            # Mode for LET - output of polynomial is FWHM in us
+            tsqmod = np.polyval(self.mod_pars, np.sqrt(E2L / Ei))**2 / 1e12
+        else:
+            raise RuntimeError('PyChop: Undefined moderator time profile type %d' % (self.imod))
+        return tsqmod
+
+    def getWidth(self, Ei):
+        """ Calculates the moderator time width in seconds for a given neutron energy (Ei) """
+        if self.imod == 3:
+            # Mode for LET - output of polynomial is FWHM in us
+            return np.polyval(self.mod_pars, np.sqrt(E2L / Ei)) / 1e6
+        else:
+            return np.sqrt(self.getWidthSquared(Ei))
+
+    def getFlux(self, Ei):
+        """ Returns the white beam flux estimate from either measured data (prefered) or analytical model (backup) """
+        return self.getMeasuredFlux(Ei) if (hasattr(self, 'measured_flux') and self.measured_flux) else self.getAnalyticFlux(Ei)
+
+    def getAnalyticFlux(self, Ei):
+        """ Estimate white beam flux from TGP's model of the moderators (ISIS TS1 only) """
+        if all([self.name != modtype for modtype in ['AP', 'CH4', 'H2']]):
+            raise AttributeError('No analytical model for moderator %s' % (self.name))
+        return Chop.flux_calc(np.array(Ei), self.name, self.theta_m * np.pi / 180.)
+
+    def getMeasuredFlux(self, Ei):
+        """ Interpolates flux from a table of measured flux """
+        if not self.measured_flux:
+            raise AttributeError('This instrument does not have a table of measured flux')
+        f = interp1d(self.measured_flux['wavelength'], self.measured_flux['flux'], kind='cubic')
+        return f(np.sqrt(E2L / np.array(Ei)))
+
+    @property
+    def theta_m(self):
+        return self.theta if (hasattr(self, 'theta') and self.theta) else 0.
+
+
+class Sample(object):
+    """
+    Class which represents a sample shape
+    """
+
+    __allowed_var_names = ['name', 'sx', 'sy', 'sz', 'isam', 'gamma']
+
+    def __init__(self, inval=None):
+        wrap_attributes(self, inval, self.__allowed_var_names)
+
+    def __repr__(self):
+        return self.name if self.name else 'Undefined sample'
+
+    def getWidthSquared(self):
+        """ Returns the squared time FWHM due to the sample in s^2 """
+        # At the moment this routine only returns a non-zero y (beam-axis) width
+        return Chop.sam0(self.sx / 1000., self.sy / 1000., self.sz / 1000., self.isam)[1] * SIGMA2FWHMSQ
+
+    def getWidth(self):
+        return np.sqrt(self.getWidthSquared)
+
+    @property
+    def gamma_deg(self):
+        return self.gamma if (hasattr(self, 'gamma') and self.gamma) else 0.
+
+
+class Detector(object):
+    """
+    Class which represents a sample shape
+    """
+
+    __allowed_var_names = ['name', 'idet', 'dd', 'tbin', 'phi']
+
+    def __init__(self, inval=None):
+        wrap_attributes(self, inval, self.__allowed_var_names)
+
+    def __repr__(self):
+        return self.name if self.name else 'Undefined detector'
+
+    def getWidthSquared(self, Ei, en=0):
+        """ Returns the squared time FWHM due to the detector in s^2 """
+        return self.getWidth(Ei, en) ** 2
+
+    def getWidth(self, Ei, en=0):
+        return Chop.detect2(1., 1., np.sqrt(E2K * (Ei-en)), self.idet, self.dd)[3] * SIGMA2FWHM
+
+    @property
+    def phi_deg(self):
+        return self.phi if (hasattr(self, 'phi') and self.phi) else 0.
+
+
+class Instrument(object):
+    """
+    Class which represents a direct geometry neutron spectrometer
+    """
+
+    __allowed_var_names = ['name', 'sample', 'chopper_system', 'moderator', 'detector']
+
+    __child_methods = ['setChopper', 'getChopper', 'setFrequency', 'getFrequency', 'setEi', 'getEi',
+                       'getAllowedEi', 'plotMultiRepFrame']
+
+    __child_properties = ['package', 'variant', 'frequency', 'phase', 'ei', 'tjit']
+
+    def __init__(self, instrument, chopper=None, freq=None):
+        if isinstance(instrument, string_types):
+            try:
+                with open(instrument) as f:
+                    instrument = yaml.safe_load(f)
+            except (OSError, IOError) as e:
+                raise RuntimeError('Cannot open file %s . Error is %s' % (instrument, e))
+        if ((hasattr(instrument, 'moderator') or hasattr(instrument, 'chopper_system')) or
+            ('moderator' in instrument or 'chopper_system' in instrument)):
+            wrap_attributes(self, instrument, self.__allowed_var_names)
+            if 'source_rep' in self.moderator:
+                self.chopper_system['source_rep'] = self.moderator['source_rep']
+            if 'emission_time' in self.moderator:
+                self.chopper_system['emission_time'] = self.moderator['emission_time']
+        else:
+            raise RuntimeError('Input to Instrument must be an Instrument object, a dictionary or a filename string')
+        # If we have just loaded a YAML file or constructed from a dictionary, need to convert to correct class
+        for elem_nm, classref in zip(['sample', 'chopper_system', 'moderator', 'detector'], 
+                                     [Sample, ChopperSystem, Moderator, Detector]):
+            try:
+                element = getattr(self, elem_nm)
+                if isinstance(element, dict): 
+                    setattr(self, elem_nm, classref(element))
+                setattr(self, 'has_' + elem_nm, True)
+            except AttributeError:
+                setattr(self, 'has_' + elem_nm, False)
+        if not self.has_chopper_system or not self.has_moderator:
+            raise AttributeError('No chopper system or moderator found in input.')
+        for method in self.__child_methods:
+            setattr(self, method, getattr(self.chopper_system, method))
+        for prop in self.__child_properties:
+            getter = lambda obj, prop=prop: ChopperSystem.__dict__[prop].__get__(self.chopper_system, ChopperSystem)
+            setter = lambda obj, val, prop=prop: ChopperSystem.__dict__[prop].__set__(self.chopper_system, val)
+            setattr(type(self), prop, property(getter, setter))
+        # Now reset default chopper/variant and frequency
+        if chopper or freq:
+            self.setChopper(chopper if chopper else self.getChopper(), freq if freq else self.frequency)
+
+    def setInstrument(self, instrument):
+        self.__dict__.clear()
+        self.__init__(instrument)
+
+    def getFlux(self, Ei_in=None, frequency=None):
+        """ Returns the monochromatic flux estimate in n/cm^2/s """
+        Ei = self.ei if Ei_in is None else Ei_in
+        return self.moderator.getFlux(Ei) * self.chopper_system.getTransmission(Ei, frequency)
+
+    def getMultiRepFlux(self, Ei_in=None, frequency=None):
+        Ei = self.ei if Ei_in is None else Ei_in
+        if frequency:
+            oldfreq = self.frequency
+            self.frequency = frequency
+        return self.getFlux(self.getAllowedEi(Ei), frequency)
+        if frequency:
+            self.frequency = oldfreq
+
+    def getResFlux(self, Etrans=None, Ei_in=None, frequency=None):
+        """ Returns the resolution and flux as a tuple. """
+        return self.getResolution(Etrans, Ei_in, frequency), self.getFlux(Ei_in, frequency)
+
+    def getWidths(self, Ei_in=None, frequency=None):
+        """ Returns the time FWHM of different components for one rep (Ei) in microseconds """
+        return {k: np.sqrt(v)*1e6 for k, v in list(self.getVanVar(Ei_in, frequency)[1].items())}
+
+    def getMultiWidths(self, Ei_in=None, frequency=None):
+        """ Returns the time FWHM of different components for each possible rep (Ei) in seconds"""
+        Ei = self.ei if Ei_in is None else Ei_in
+        Eis = self.getAllowedEi(Ei)
+        outdic = {'Eis': Eis}
+        widths = [self.getWidths(ei, frequency) for ei in Eis]
+        for ky in widths[0]:
+            outdic[ky] = np.hstack(np.array([w[ky] for w in widths]))
+        return outdic
+
+    def getResolution(self, Etrans=None, Ei_in=None, frequency=None):
+        """
+        Calculates resolution (energy) widths
+
+        van = getResolution()
+        van = getResolution(etrans)
+        van = getResolution(etrans, ei, omega)
+
+        Inputs:
+            etrans - list of numpy array of energy transfers to calculate for (meV) [default: linspace(0.05Ei, 0.95Ei, 19)]
+            ei - incident energy in meV [default: preset energy]
+            omega - chopper frequncy in Hz  [default: preset frequency]
+
+        Output:
+            van - the incoherent (Vanadium) energy FWHM at etrans in meV
+        """
+        Ei = self.ei if Ei_in is None else Ei_in
+        # If not set, sets energy transfers to values to compare exactly to RAE's original implementation.
+        if Etrans is None:
+            Etrans = np.linspace(0.05*Ei, 0.95*Ei+0.05*0.05*Ei, 19, endpoint=True)
+        v_van, _ = self.getVanVar(Ei, frequency, Etrans)
+        x2 = self.chopper_system.sam_det
+        Ef = Ei - np.array(Etrans)
+        van = (2 * E2V * np.sqrt(Ef**3 * v_van)) / x2
+        return van
+
+    def getMultiRepResolution(self, Etrans=None, Ei_in=None, frequency=None):
+        """ Returns a list of FWHM in meV for all allowed Ei's in multirep mode (in same order as getAllowedEi) 
+            The input energy transfer is interpreted as fractions of Ei. e.g. linspace(0,0.9,100) """
+        if Etrans is None:
+            Etrans = np.linspace(0.05, 0.95, 19, endpoint=True)
+        Ei = self.ei if Ei_in is None else Ei_in
+        return [self.getResolution(Etrans * ei, ei, frequency) for ei in self.getAllowedEi(Ei)]
+
+    def getVanVar(self, Ei_in=None, frequency=None, Etrans=0):
+        """ Calculates the time squared FWHM in s^2 at the sample (Vanadium widths) for different components """
+        Ei = self.ei if Ei_in is None else Ei_in
+        Etrans = np.array(Etrans if np.shape(Etrans) else [Etrans])
+        if frequency and frequency != self.frequency:
+            oldfreq = self.frequency
+            self.frequency = frequency
+        tsqmod = self.moderator.getWidthSquared(Ei)
+        tsqchp = self.chopper_system.getWidthSquared(Ei)
+        tsqjit = self.tjit**2
+        # Gets distances: x0=mod-final chopper, xa=aperture-final, x1=final-sample, x2=sample-det, xm=mod-first chopper
+        x0, xa, x1, x2, xm = self.chopper_system.getDistances()
+        # For Disk chopper spectrometers, the opening times of the first chopper can be the effective moderator time
+        if tsqchp[1] is not None:
+            frac_dist = 1 - (xm / x0)
+            tsmeff = tsqmod * frac_dist**2   # Effective moderator time at first chopper
+            x0 -= xm                         # Propagate from first chopper, not from moderator (after rescaling tmod)
+            tsqmod = tsmeff if (tsqchp[1] > tsmeff) else tsqchp[1]
+        tsqchp = tsqchp[0]
+        # Propagate the time widths to the detector position
+        omega = self.frequency[0] * 2 * np.pi
+        vi = E2V * np.sqrt(Ei)
+        vf = E2V * np.sqrt(Ei - Etrans)
+        vratio = (vi / vf)**3
+        tanthm = np.tan(self.moderator.theta_m * np.pi / 180.)
+        g1, g2 = tuple(1. - ((omega * tanthm / vi) * np.array([xa + x1, x0 - xa])))
+        f1, f2 = tuple(1. + (x1 / x0) * np.array([g1, g2]))
+        g1, g2, f1, f2 = tuple(np.array([g1, g2, f1, f2]) / (omega * (xa + x1)))
+        modfac = (x1 + vratio * x2) / x0
+        chpfac = 1. + modfac
+        apefac = f1 + ((vratio * x2 / x0) * g1)
+        tsqmod *= modfac**2
+        tsqchp *= chpfac**2
+        tsqjit *= chpfac**2
+        tsqape = apefac**2 * (self.aperture_width**2 / 12.) * SIGMA2FWHMSQ
+        vsqvan = tsqmod + tsqchp + tsqjit + tsqape
+        outdic = {'moderator': tsqmod, 'chopper': tsqchp, 'jitter': tsqjit, 'aperture': tsqape}
+        if self.has_detector:
+            phi = self.detector.phi_deg * np.pi / 180.
+            tsqdet = (1. / vf)**2 * np.array([self.detector.getWidthSquared(Ei, en) for en in Etrans])
+            vsqvan += tsqdet
+            outdic['detector'] = tsqdet
+        else:
+            phi = 0.
+        if self.has_sample:
+            gam = self.sample.gamma_deg * np.pi / 180.
+            bb = (-np.sin(gam) / vi) + (np.sin(gam - phi) / vf) - (f2 * np.cos(gam))
+            samfac = bb - ((vratio * x2 / x0) * g2 * np.cos(gam))
+            tsqsam = samfac**2 * self.sample.getWidthSquared()
+            vsqvan += tsqsam
+            outdic['sample'] = tsqsam
+        if frequency:
+            self.frequency = oldfreq
+        return vsqvan, outdic
+
+    @property
+    def aperture_width(self):
+        if hasattr(self.chopper_system, 'aperture_width') and self.chopper_system.aperture_width:
+            return self.chopper_system.aperture_width
+        return 0.
+
+    @property
+    def aperture_height(self):
+        if hasattr(self.chopper_system, 'aperture_height') and self.chopper_system.aperture_height:
+            return self.chopper_system.aperture_height 
+        return 0.
+
+    @property
+    def instname(self):
+        return self.name
+
+    @classmethod
+    def calculate(cls, *args, **kwargs):
+        """
+        ! Calculates the resolution and flux directly (without setting up a PyChop2 object)
+        !
+        ! PyChop2.calculate('mari', 's', 250., 55.)      # Instname, Chopper Type, Freq, Ei in order
+        ! PyChop2.calculate('let', 180, 2.2)             # For LET, chopper type is not needed.
+        ! PyChop2.calculate('let', [160., 80.], 1.)      # For LET, specify resolution and pulse remover freq
+        ! PyChop2.calculate('let', 'High flux', 80, 2.2) # LET default is medium flux configuration
+        ! PyChop2.calculate(inst='mari', package='s', freq=250., ei=55.) # With keyword arguments
+        ! PyChop2.calculate(inst='let', variant='High resolution', freq=[160., 80.], ei=2.2)
+        !
+        ! For LET, the allowed variant names are:
+        !   'High resolution'
+        !   'High flux'
+        !   'Intermediate'
+        ! You have to use these strings exactly.
+        !
+        ! By default this function returns the elastic resolution and flux only.
+        ! If you want the inelastic resolution, specify the inelastic energy transfer
+        ! as either the last positional argument, or as a keyword argument, e.g.:
+        !
+        ! PyChop2.calculate('merlin', 'g', 450., 60., range(55))
+        ! PyChop2.calculate('maps', 'a', 450., 600., etrans=np.linspace(0,550,55))
+        !
+        ! The results are returned as tuple: (resolution, flux)
+        """
+        argdict = argparser(args, kwargs, ['inst', 'package', 'frequency', 'ei', 'etrans', 'variant'])
+        if argdict['inst'] is None:
+            raise RuntimeError('You must specify the instrument name')
+        obj = cls(argdict['inst'])
+        obj.setChopper(argdict['package'], argdict['frequency'])
+        obj.ei = argdict['ei']
+        if argdict['variant']:
+            obj.variant = argdict['variant']
+        return obj.getResolution(argdict['etrans'] if argdict['etrans'] else 0.), obj.getFlux()
+
+    def __repr__(self):
+        return self.name if self.name else 'Undefined instrument'
diff --git a/scripts/PyChop/MulpyRep.py b/scripts/PyChop/MulpyRep.py
index efda669ad6b..5cdedea031f 100644
--- a/scripts/PyChop/MulpyRep.py
+++ b/scripts/PyChop/MulpyRep.py
@@ -184,19 +184,24 @@ def calcChopTimes(efocus, freq, instrumentpars, chop2Phase=5):
 
     chop_times = []             # empty list to hold each chopper opening period
 
+    # figures out phase information
+    if not (hasattr(ph_ind_v, '__len__') and len(ph_ind_v) == len(dist)):
+        ph_ind = [i==(ph_ind_v[-1] if hasattr(ph_ind_v, '__len__') else ph_ind_v) for i in range(len(dist))]
+    chop2Phase = phase = chop2Phase if hasattr(chop2Phase, '__len__') else [chop2Phase]
+    if len(chop2Phase) != len(dist):
+        if len(chop2Phase) == len(ph_ind_v):
+            phase = [False] * len(dist)
+            for i in range(len(ph_ind_v)):
+                phase[ph_ind_v[i]] = chop2Phase[i]
+        else:
+            phase = [chop2Phase[-1] if ph_ind[i] else False for i in range(len(dist))]
+
     # first we optimise on the main Ei
     for i in range(len(dist)):
-        if hasattr(ph_ind_v, '__len__') and len(ph_ind_v) == len(dist):
-            ph_ind = ph_ind_v[i]
-        else:
-            ph_ind = ph_ind_v if not hasattr(ph_ind_v, '__len__') else ph_ind_v[-1]
         # loop over each chopper
-        # chopper2 is a special case
-        if isinstance(ph_ind, string_types) and i == int(ph_ind) and chop2Phase is not None:
-            islt = int(chop2Phase)
-        else:
-            islt = 0
-        if i == ph_ind and chop2Phase is not None:
+        # checks whether this chopper should have an independently set phase / delay
+        islt = int(phase[i]) if (ph_ind[i] and isinstance(phase[i], string_types)) else 0
+        if ph_ind[i] and not isinstance(phase[i], string_types):
             # effective chopper velocity (if 2 disks effective velocity is double)
             chopVel = 2*np.pi*radius[i] * numDisk[i] * freq[i]
             # full opening time
diff --git a/scripts/PyChop/PyChop2.py b/scripts/PyChop/PyChop2.py
index 94b72016269..29bde1f06a7 100644
--- a/scripts/PyChop/PyChop2.py
+++ b/scripts/PyChop/PyChop2.py
@@ -8,6 +8,7 @@ direct geometry time-of-flight inelastic neutron spectrometers.
 from __future__ import (absolute_import, division, print_function)
 from .ISISFermi import ISISFermi
 from .ISISDisk import ISISDisk
+import warnings
 
 
 class PyChop2:
@@ -35,6 +36,8 @@ class PyChop2:
         'MARI': ISISDisk}
 
     def __init__(self, instname, *args):
+        warnings.warn("The PyChop2 class is deprecated and will be removed in the next Mantid version. "
+                      "Please use the Instrument class or the official PyChop CLI interface.", DeprecationWarning)
         instname = instname.upper()
         if instname not in self.__Classes.keys():
             raise ValueError('Instrument %s not recognised' % (instname))
diff --git a/scripts/PyChop/PyChopGui.py b/scripts/PyChop/PyChopGui.py
index a2a1bbe1d01..3c9da5650a7 100755
--- a/scripts/PyChop/PyChopGui.py
+++ b/scripts/PyChop/PyChopGui.py
@@ -12,7 +12,7 @@ from __future__ import (absolute_import, division, print_function)
 import sys
 import re
 import numpy as np
-from .PyChop2 import PyChop2
+from .Instruments import Instrument
 from PyQt4 import QtGui, QtCore
 from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
 from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
@@ -26,7 +26,7 @@ class PyChopGui(QtGui.QMainWindow):
     at spallation sources by calculating the resolution and flux at a given neutron energies.
     """
 
-    instruments = ['MAPS', 'MARI', 'MERLIN', 'LET']
+    instruments = {'MAPS': 'maps.yaml', 'MARI': 'mari.yaml', 'MERLIN': 'merlin.yaml', 'LET': 'let.yaml'}
     choppers = {
         'MAPS':['A', 'B', 'S'],
         'MARI':['A', 'B', 'R', 'G', 'S'],
@@ -95,7 +95,7 @@ class PyChopGui(QtGui.QMainWindow):
         else:
             self.widgets['MultiRepCheck'].setEnabled(False)
             self.widgets['MultiRepCheck'].setChecked(False)
-        self.engine.setInstrument(str(instname))
+        self.engine.setInstrument(self.folder + self.instruments[str(instname)])
         self.engine.setChopper(str(self.widgets['ChopperCombo']['Combo'].currentText()))
         self.engine.setFrequency(float(self.widgets['FrequencyCombo']['Combo'].currentText()))
         val = self.flxslder.val * self.maxE[self.engine.instname] / 100
@@ -134,17 +134,17 @@ class PyChopGui(QtGui.QMainWindow):
         if 'LET' in str(self.engine.instname):
             freqpr = float(self.widgets['PulseRemoverCombo']['Combo'].currentText())
             chop2phase = float(self.widgets['Chopper2Phase']['Edit'].text()) % 1e5
-            self.engine.setFrequency([freq_in, freqpr], Chopper2Phase=chop2phase)
+            self.engine.setFrequency([freq_in, freqpr], phase=chop2phase)
         elif 'MERLIN' in str(self.engine.instname) and 'G' in self.engine.getChopper():
             chop2phase = float(self.widgets['Chopper2Phase']['Edit'].text()) % 2e4
-            self.engine.setFrequency(freq_in, Chopper2Phase=chop2phase)
+            self.engine.setFrequency(freq_in, phase=chop2phase)
         elif 'MAPS' in str(self.engine.instname): 
             freqpr = float(self.widgets['PulseRemoverCombo']['Combo'].currentText())
-            chop2phase = int(self.widgets['Chopper2Phase']['Edit'].text())
-            self.engine.setFrequency([freq_in, freqpr], Chopper2Phase=chop2phase)
+            chop2phase = str(self.widgets['Chopper2Phase']['Edit'].text())
+            self.engine.setFrequency([freq_in, freqpr], phase=chop2phase)
         elif 'MARI' in str(self.engine.instname):
-            chop2phase = int(self.widgets['Chopper2Phase']['Edit'].text())
-            self.engine.setFrequency(freq_in, Chopper2Phase=chop2phase)
+            chop2phase = str(self.widgets['Chopper2Phase']['Edit'].text())
+            self.engine.setFrequency(freq_in, phase=chop2phase)
         else:
             self.engine.setFrequency(freq_in)
 
@@ -772,7 +772,9 @@ class PyChopGui(QtGui.QMainWindow):
 
     def __init__(self):
         super(PyChopGui, self).__init__()
-        self.engine = PyChop2('MAPS', 'A', 50)
+        import sys, os
+        self.folder = os.path.dirname(sys.modules[self.__module__].__file__) + '/'
+        self.engine = Instrument(self.folder+self.instruments['MAPS'], 'A', 50)
         self.drawLayout()
         self.setInstrument('MAPS')
         self.xlim = 0
diff --git a/scripts/PyChop/__init__.py b/scripts/PyChop/__init__.py
index 15798b1b95c..e694aedeb47 100644
--- a/scripts/PyChop/__init__.py
+++ b/scripts/PyChop/__init__.py
@@ -19,7 +19,7 @@ PyChop2.showGUI()
 
 from __future__ import (absolute_import, division, print_function)
 import warnings
-from .PyChop2 import PyChop2  # noqa: F401
+from .Instruments import Instrument as PyChop2  # noqa: F401
 # If the system doesn't have matplotlib, don't import the GUI.
 try:
     from .PyChopGui import show as showGUI
diff --git a/scripts/PyChop/let.yaml b/scripts/PyChop/let.yaml
new file mode 100644
index 00000000000..5b0bed2544e
--- /dev/null
+++ b/scripts/PyChop/let.yaml
@@ -0,0 +1,138 @@
+name: LET
+# Input file for PyChop2 for the LET spectrometer at ISIS.
+# At a minium, the "chopper_system" and "moderator" entries must be present
+
+chopper_system:
+  name: LET chopper system
+  chop_sam: 1.5                 # Distance (x1) from final chopper to sample (m)
+  sam_det: 3.5                  # Distance (x2) from sample to detector (m)
+  choppers:
+    -                           # Each entry must have a dash on an otherwise empty line!
+      name: LET Disk 1 Resolution
+      distance: 7.83            # Distance from moderator to this chopper in metres
+      slot_width: 40            # Slot width in mm
+      guide_width: 40           # Width of guide after chopper in mm
+      nslot: 6                  # Number of slots. (Assume all slots equally spaced)
+      radius: 290               # Disk radius
+      isDouble: True            # Is this a double disk system?
+      isPhaseIndependent: False # Is this disk to be phased independently?
+    -
+      name: LET Disk 2 Frame Overlap
+      distance: 8.4             # Distance from moderator to this chopper in metres
+      slot_width: 890           # Slot width in mm
+      guide_width: 40           # Width of guide after chopper in mm
+      nslot: 1                  # Number of slots. (Assume all slots equally spaced)
+      radius: 545               # Disk radius
+      isDouble: False           # Is this a double disk system?
+      isPhaseIndependent: True  # Is this disk to be phased independently?
+      defaultPhase: 5           # What is the default phase for this disk (either a time in microseconds
+                                #   or a slot index for the desired rep to go through).
+    -
+      name: LET Disk 3 Pulse Remover
+      distance: 11.75           # Distance from moderator to this chopper in metres
+      slot_width: 56            # Slot width in mm
+      guide_width: 40           # Width of guide after chopper in mm
+      nslot: 2                  # Number of slots. (Assume all slots equally spaced)
+      radius: 290               # Disk radius
+      isDouble: False           # Is this a double disk system?
+      isPhaseIndependent: False # Is this disk to be phased independently?
+    -
+      name: LET Disk 4 Contamination Remover
+      distance: 15.66           # Distance from moderator to this chopper in metres
+      slot_width: 56            # Slot width in mm
+      guide_width: 40           # Width of guide after chopper in mm
+      nslot: 6                  # Number of slots. (Assume all slots equally spaced)
+      radius: 290               # Disk radius
+      isDouble: False           # Is this a double disk system?
+      isPhaseIndependent: False # Is this disk to be phased independently?
+    -
+      name: LET Disk 5 Resolution
+      distance: 23.5            # Distance from moderator to this chopper in metres
+      slot_width: 20            # Slot width in mm
+      guide_width: 20           # Width of guide after chopper in mm
+      nslot: 2                  # Number of slots. (Assume all slots equally spaced)
+      radius: 290               # Disk radius
+      isDouble: True            # Is this a double disk system?
+      isPhaseIndependent: False # Is this disk to be phased independently?
+  # Now define how the frequencies of the choppers should be related
+  # This is an NxM matrix A where N is the number of choppers and M is the number of indepdent frequencies
+  # Such that A.F will give the N required frequencies for each chopper from the M input frequencies
+  frequency_matrix:             # LET is run with two main frequencies:
+    [[0, 0.5],                  #   f1: The frequency of the resolution chopper (Disk 5)
+     [0, 0],                    #   f2: The frequency of the pulse removal chopper (Disk 3)
+     [0, 1],                    # Disk 4 is usually run at half f1, and disk 1 at half of f2
+     [0.5, 0],
+     [1, 0]]
+  constant_frequencies:         # This specifies if a chopper should be run at a fixed constant frequency
+    [0, 10., 0., 0., 0.]        # On LET, Disk 2, the frame-overlap chopper, should always run at 10 Hz
+  max_frequencies:
+    [300, 300]                  # Maximum frequencies (Hz)
+  default_frequencies:
+    [200, 100]
+  overlap_ei_frac: 0.9          # Fraction of energy loss Ei to plot ToF lines in time-distance plots
+  ei_limits: [0, 30]            # Limits on ei for multirep calculations (reps outside range ignored)
+  flux_ref_slot: 20             # Reference slot width (mm) for flux transmission calculation (disk choppers only)
+  flux_ref_freq: 150            # Reference final chopper freq (Hz) for flux transmission calc (disk choppers only)
+  # Can define variants which overide one of the above parameters
+  variants:                     # On LET the final chopper has 3 sets of slots with different widths
+    Intermediate:               # This is the default mode (uses the 20mm slot).
+      default: True             # If this key is omitted, the variant without any option is used as default
+    High Flux:
+      choppers:                 # Note that we need to leave the required number of dashes to indicate
+        -                       #   choppers whos parameters have not changed.
+        -
+        -
+        -
+        -
+          slot_width: 31
+    High Resolution:
+      frequency_matrix:
+        [[0.5, 0],
+         [0, 0],
+         [0, 1],
+         [0, 1],
+         [1, 0]]
+      choppers:                 # Note that we need to leave the required number of dashes to indicate
+        -                       #   choppers whos parameters have not changed.
+        -
+        -
+        -
+        -
+          slot_width: 15
+
+moderator:
+  name: TS2 Hydrogen            # A==water, AP==poisoned water, CH4==methane, H2==hydrogen. This is only used for analytical calculations
+                                # of the flux distribution for ISIS TS1 moderators. If measured_flux is defined below, name can be anything
+  imod: 3                       # Moderator time profile type: 0==chi^2, 1==Ikeda-Carpenter, 2==modified chi^2
+  mod_pars: [-3.143, 49.28, .535] # imod==3 is polynomial. Pars are coeff from highest order to lowest
+  theta: 32.0                   # Angle beamline makes with moderator face (degrees)
+  source_rep: 10                # Frequency of source (Hz)
+  emission_time: 3500           # Time width of source pulse in microseconds
+  measured_flux:                # Table of measured flux vs wavelength. Wavelength in Angstrom. Flux in n/cm^2/s/uA/Angstrom
+    scale_factor: 63276.8362    # A factor to scale the flux values below by
+    wavelength: [0.5,   0.6,   0.7,   0.8,   0.9,   1. ,   1.1,   1.2,   1.3,
+                 1.4,   1.5,   1.6,   1.7,   1.8,   1.9,   2. ,   2.1,   2.2,
+                 2.3,   2.4,   2.5,   2.6,   2.7,   2.8,   2.9,   3. ,   3.1,
+                 3.2,   3.3,   3.4,   3.5,   3.6,   3.7,   3.8,   3.9,   4. ,
+                 4.1,   4.2,   4.3,   4.4,   4.5,   4.6,   4.7,   4.8,   4.9,
+                 5.0,   5.1,   5.2,   5.3,   5.4,   5.5,   5.6,   5.7,   5.8,
+                 5.9,   6.0,   6.1,   6.2,   6.3,   6.4,   6.5,   6.6,   6.7,
+                 6.8,   6.9,   7.0,   7.1,   7.2,   7.3,   7.4,   7.5,   7.6,
+                 7.7,   7.8,   7.9,   8.0,   8.1,   8.2,   8.3,   8.4,   8.5,
+                 8.6,   8.7,   8.8,   8.9,   9.0,   9.1,   9.2,   9.3,   9.4,
+                 9.5,   9.6,   9.7,   9.8,   9.9,  10.0,  10.1,  10.2,  10.3,
+                10.4,  10.5,  10.6,  10.7,  10.8,  10.9,  11.0,  11.1,  11.2,
+                11.3,  11.4,  11.5,  11.6,  11.7,  11.8,  11.9]
+    flux: [0.0889, 0.1003, 0.1125, 0.1213, 0.1274, 0.1358, 0.1455, 0.1562, 0.1702,
+           0.1902, 0.2149, 0.2496, 0.2938, 0.3537, 0.4315, 0.5244, 0.6415, 0.7856,
+           0.9341, 1.0551, 1.1437, 1.1955, 1.2004, 1.1903, 1.1662, 1.1428, 1.1176,
+           1.0875, 1.0641, 1.0562, 1.0242, 0.9876, 0.9586, 0.9415, 0.924, 0.8856,
+           0.8865, 0.8727, 0.842, 0.8125, 0.7849, 0.7596, 0.7417, 0.7143, 0.6869,
+           0.6608, 0.6341, 0.6073, 0.581, 0.5548, 0.5304, 0.507, 0.4849, 0.4639,
+           0.4445, 0.425, 0.407, 0.3902, 0.3737, 0.3579, 0.3427, 0.3274, 0.3129,
+           0.2989, 0.2854, 0.2724, 0.2601, 0.2483, 0.2371, 0.2267, 0.2167, 0.2072,
+           0.1984, 0.19, 0.1821, 0.1743, 0.1669, 0.1599, 0.1532, 0.1467, 0.1404,
+           0.1346, 0.1291, 0.1238, 0.1189, 0.1141, 0.1097, 0.1053, 0.1014, 0.0975,
+           0.0938, 0.0902, 0.0866, 0.0834, 0.0801, 0.077, 0.0741, 0.0712, 0.0686,
+           0.066, 0.0637, 0.0614, 0.0593, 0.0571, 0.0551, 0.0532, 0.0512, 0.0494,
+           0.0477, 0.0461, 0.0445, 0.043, 0.0415, 0.0401, 0.0387]
diff --git a/scripts/PyChop/maps.yaml b/scripts/PyChop/maps.yaml
new file mode 100644
index 00000000000..30b95dbd2ea
--- /dev/null
+++ b/scripts/PyChop/maps.yaml
@@ -0,0 +1,130 @@
+name: MAPS
+# Input file for PyChop2 for the MAPS spectrometer at ISIS.
+
+chopper_system:
+  name: MAPS chopper system
+  chop_sam: 1.9                 # Distance (x1) from final chopper to sample (m)
+  sam_det: 6.0                  # Distance (x2) from sample to detector (m)
+  aperture_width: 0.094         # Width of aperture at moderator face (m)
+  aperture_height: 0.094        # Height of aperture at moderator face (m)
+  choppers:
+    -                           # Each entry must have a dash on an otherwise empty line!
+      name: MAPS Disk
+      distance: 8.831           # Distance from moderator to this chopper in metres
+      slot_width: 68            # Slot width in mm
+      guide_width: 50           # Width of guide after chopper in mm
+      nslot: 4                  # Number of slots. If slot_ang_pos is specified can ommit this entry
+                                # Next line has the angular position of the slots in degrees
+                                #   Must be monotonically increasing. Can omit if nslot is specifed,
+                                #   in which case it will be assumed that the slits are equally spaced.
+      slot_ang_pos: [-180., -39.1, 0., 39.1]
+      radius: 375               # Disk radius
+      isDouble: False           # Is this a double disk system?
+      isPhaseIndependent: True  # Is this disk to be phased independently?
+      defaultPhase: "1"         # What is the default phase for this disk (either a time in microseconds
+                                #   or a slot index [as a string] for the desired rep to go through)
+    -
+      name: MAPS Fermi
+      distance: 10.1            # Distance from moderator to this chopper in metres
+      aperture_distance: 8.27   # Distance from aperture (moderator face) to this chopper (only for Fermi)
+      packages:                 # A hash of chopper packages
+        A:
+          name: MAPS A (500meV)
+          pslit: 1.087          # Neutron transparent slit width (mm)
+          pslat: 0.534          # Neutron absorbing slat width (mm)
+          radius: 49.0          # Chopper package radius (mm)
+          rho: 1300.0           # Chopper package curvature (mm)
+          tjit: 0.0             # Jitter time (us)
+          fluxcorr: 3.0         # (Empirical/Fudge) factor to scale calculated flux by
+          isPi: False           # Should the PI pulse (at 180 deg rotation) be transmitted by this package?
+        B:
+          name: MAPS B (200meV)
+          pslit: 1.812          # Neutron transparent slit width (mm)
+          pslat: 0.534          # Neutron absorbing slat width (mm)
+          radius: 49.0          # Chopper package radius (mm)
+          rho: 920.0            # Chopper package curvature (mm)
+          tjit: 0.0             # Jitter time (us)
+          fluxcorr: 3.0         # (Empirical/Fudge) factor to scale calculated flux by
+          isPi: False           # Should the PI pulse (at 180 deg rotation) be transmitted by this package?
+        S:
+          name: MAPS S (Sloppy)
+          pslit: 2.899          # Neutron transparent slit width (mm)
+          pslat: 0.534          # Neutron absorbing slat width (mm)
+          radius: 49.0          # Chopper package radius (mm)
+          rho: 1300.0           # Chopper package curvature (mm)
+          tjit: 0.0             # Jitter time (us)
+          fluxcorr: 2.5         # (Empirical/Fudge) factor to scale calculated flux by
+          isPi: False           # Should the PI pulse (at 180 deg rotation) be transmitted by this package?
+  # Now define how the frequencies of the choppers should be related
+  # This is an NxM matrix A where N is the number of choppers and M is the number of indepdent frequencies
+  # Such that A.F will give the N required frequencies for each chopper from the M input frequencies
+  frequency_matrix:
+    [[0, 1],                    # f1 is the Fermi frequency
+     [1, 0]]                    # f2 is the Disk frequency
+  max_frequencies:
+    [600, 100]                  # Maximum frequencies (Hz)
+  default_frequencies:
+    [400, 50]
+
+sample: 
+  name: MAPS Sample Can
+  isam: 0                       # Sample type: 0==flat plate, 1==ellipse, 2==annulus, 3==sphere, 4==solid cylinder
+  sx: 2.0                       # Thickness (mm) 
+  sy: 48.0                      # Width (mm)
+  sz: 48.0                      # Height (mm)
+  gamma: 0.0                    # Angle of x-axis to ki (degrees)
+
+detector:
+  name: He3 PSD tubes
+  idet: 2                       # Detector type: 1==He tube binned together, 2==He tube
+  dd: 0.025                     # Detector depth (diameter for tube) in metres
+  tbin: 0.0                     # Detector time bins (microseconds)
+  phi: 0.0                      # Detector scattering angle (degrees)
+
+moderator:
+  name: AP                      # A==water, AP==poisoned water, CH4==methane, H2==hydrogen. This is only used for analytical calculations
+                                # of the flux distribution for ISIS TS1 moderators. If measured_flux is defined below, name can be anything
+  imod: 2                       # Moderator time profile type: 0==chi^2, 1==Ikeda-Carpenter, 2==modified chi^2
+  mod_pars: [38.6, 0.5226]      # Parameters for time profile
+  mod_scale_fn: soft_hat        # Function to modify the parameters depending on energy (ommit or leave blank for no modification)
+  mod_scale_par: [1.0, 0.0, 0.0, 150.0, 0.01, 70.0]
+  theta: 32.0                   # Angle beamline makes with moderator face (degrees)
+  source_rep: 50                # Frequency of source (Hz)
+  measured_flux:                # Table of measured flux vs wavelength. Wavelength in Angstrom. Flux in n/cm^2/s/uA/Angstrom
+    wavelength: [0.0181, 0.0511, 0.0841, 0.1170, 0.1500, 0.1829, 0.2159, 0.2489, 0.2818, 0.3148, 0.3477, 0.3807, 0.4137, 0.4466, 0.4796, 
+                 0.5126, 0.5455, 0.5785, 0.6114, 0.6444, 0.6774, 0.7103, 0.7433, 0.7762, 0.8092, 0.8422, 0.8751, 0.9081, 0.9411, 0.9740, 
+                 1.0070, 1.0399, 1.0729, 1.1059, 1.1388, 1.1718, 1.2047, 1.2377, 1.2707, 1.3036, 1.3366, 1.3696, 1.4025, 1.4355, 1.4684, 
+                 1.5014, 1.5344, 1.5673, 1.6003, 1.6332, 1.6662, 1.6992, 1.7321, 1.7651, 1.7980, 1.8310, 1.8640, 1.8969, 1.9299, 1.9629, 
+                 1.9958, 2.0288, 2.0617, 2.0947, 2.1277, 2.1606, 2.1936, 2.2266, 2.2595, 2.2925, 2.3254, 2.3584, 2.3914, 2.4243, 2.4573, 
+                 2.4902, 2.5232, 2.5562, 2.5891, 2.6221, 2.6551, 2.6880, 2.7210, 2.7539, 2.7869, 2.8199, 2.8528, 2.8858, 2.9187, 2.9517, 
+                 2.9847, 3.0176, 3.0506, 3.0835, 3.1165, 3.1495, 3.1824, 3.2154, 3.2484, 3.2813, 3.3143, 3.3472, 3.3802, 3.4132, 3.4461, 
+                 3.4791, 3.5120, 3.5450, 3.5780, 3.6109, 3.6439, 3.6769, 3.7098, 3.7428, 3.7757, 3.8087, 3.8417, 3.8746, 3.9076, 3.9406, 
+                 3.9735, 4.0065, 4.0394, 4.0724, 4.1054, 4.1383, 4.1713, 4.2042, 4.2372, 4.2702, 4.3031, 4.3361, 4.3690, 4.4020, 4.4350, 
+                 4.4679, 4.5009, 4.5339, 4.5668, 4.5998, 4.6327, 4.6657, 4.6987, 4.7316, 4.7646, 4.7976, 4.8305, 4.8635, 4.8964, 4.9294, 
+                 4.9624, 4.9953, 5.0283, 5.0612, 5.0942, 5.1272, 5.1601, 5.1931, 5.2260, 5.2590, 5.2920, 5.3249, 5.3579, 5.3909, 5.4238, 
+                 5.4568, 5.4897, 5.5227, 5.5557, 5.5886, 5.6216, 5.6546, 5.6875, 5.7205, 5.7534, 5.7864, 5.8194, 5.8523, 5.8853, 5.9182, 
+                 5.9512, 5.9842, 6.0171, 6.0501, 6.0831, 6.1160, 6.1490, 6.1819, 6.2149, 6.2479, 6.2808, 6.3138, 6.3467, 6.3797, 6.4127, 
+                 6.4456, 6.4786, 6.5115, 6.5445, 6.5750]
+    flux: [2.60775e+08, 8.08616e+07, 4.59455e+07, 3.16183e+07, 2.37830e+07, 1.91458e+07, 1.58204e+07, 1.36088e+07, 1.19225e+07, 
+           1.06105e+07, 9.55533e+06, 8.77411e+06, 8.21901e+06, 7.67656e+06, 7.15055e+06, 6.70757e+06, 6.39276e+06, 6.22826e+06, 
+           6.19798e+06, 6.27731e+06, 6.47994e+06, 6.91604e+06, 7.50573e+06, 8.09035e+06, 8.76875e+06, 9.49975e+06, 1.01658e+07, 
+           1.07548e+07, 1.13597e+07, 1.19941e+07, 1.25374e+07, 1.28821e+07, 1.31248e+07, 1.32727e+07, 1.32305e+07, 1.30834e+07, 
+           1.27595e+07, 1.24351e+07, 1.20115e+07, 1.15789e+07, 1.11218e+07, 1.07191e+07, 1.03272e+07, 9.93856e+06, 9.59376e+06, 
+           9.19836e+06, 8.81571e+06, 8.50457e+06, 8.29274e+06, 8.05058e+06, 7.88437e+06, 7.63907e+06, 7.32047e+06, 6.99681e+06, 
+           6.68968e+06, 6.45270e+06, 6.24161e+06, 6.01323e+06, 5.74713e+06, 5.48816e+06, 5.27153e+06, 5.01780e+06, 4.76127e+06, 
+           4.48172e+06, 4.21345e+06, 3.97093e+06, 3.74819e+06, 3.53683e+06, 3.32935e+06, 3.12404e+06, 2.92801e+06, 2.74479e+06, 
+           2.61634e+06, 2.48606e+06, 2.38826e+06, 2.29410e+06, 2.17636e+06, 2.07461e+06, 1.97063e+06, 1.87220e+06, 1.77780e+06, 
+           1.70202e+06, 1.62584e+06, 1.55763e+06, 1.48989e+06, 1.42924e+06, 1.37959e+06, 1.34056e+06, 1.31926e+06, 1.28573e+06, 
+           1.25559e+06, 1.22426e+06, 1.18988e+06, 1.15714e+06, 1.13032e+06, 1.09423e+06, 1.06161e+06, 1.02650e+06, 9.95519e+05, 
+           9.56437e+05, 9.24815e+05, 8.90446e+05, 8.56656e+05, 8.28196e+05, 8.01094e+05, 7.79358e+05, 7.56306e+05, 7.35949e+05, 
+           7.24375e+05, 7.02174e+05, 6.86458e+05, 6.65894e+05, 6.43176e+05, 6.24539e+05, 6.01304e+05, 5.82505e+05, 5.61653e+05, 
+           5.41996e+05, 5.25903e+05, 5.10613e+05, 4.96677e+05, 4.82118e+05, 4.64661e+05, 4.65809e+05, 4.68617e+05, 4.56137e+05, 
+           4.42141e+05, 4.27460e+05, 4.10041e+05, 3.98628e+05, 3.84161e+05, 3.71166e+05, 3.57501e+05, 3.45980e+05, 3.35925e+05, 
+           3.23733e+05, 3.13815e+05, 3.03413e+05, 2.91757e+05, 2.82348e+05, 2.72917e+05, 2.57271e+05, 2.41863e+05, 2.58619e+05, 
+           2.53316e+05, 2.43464e+05, 2.35779e+05, 2.29787e+05, 2.22481e+05, 2.14144e+05, 2.08181e+05, 2.01866e+05, 1.95864e+05, 
+           1.89808e+05, 1.84740e+05, 1.78016e+05, 1.73397e+05, 1.68777e+05, 1.65549e+05, 1.61071e+05, 1.56594e+05, 1.52364e+05, 
+           1.49546e+05, 1.46162e+05, 1.43155e+05, 1.40167e+05, 1.37583e+05, 1.35294e+05, 1.32192e+05, 1.30639e+05, 1.27633e+05, 
+           1.25179e+05, 1.23187e+05, 1.21203e+05, 1.18074e+05, 1.15095e+05, 1.12187e+05, 1.10561e+05, 1.08411e+05, 1.05109e+05, 
+           1.03695e+05, 1.01165e+05, 9.87797e+04, 9.77841e+04, 9.40768e+04, 9.27353e+04, 9.14937e+04, 8.88289e+04, 8.74353e+04, 
+           8.53251e+04, 8.30339e+04, 8.22249e+04, 7.94099e+04, 7.79037e+04, 7.62865e+04, 7.47047e+04, 7.38535e+04, 7.17228e+04, 
+           6.98927e+04, 6.89509e+04]
-- 
GitLab