Repository: stfc/PSyclone

Issue:
Outline script that will translate TL kernels to AD kernels
An initial outline script is required for the adjoint project. This should take a TL kernel file as input, parse it with fparser2, translate it to language-level PSyIR, provide hooks/comments for translating to LFRic-specific PSyIR, provide hooks/comments for performing transformations to convert to an AD kernel and then write out the resultant transformed PSyIR to file.

Discussion:

Patch:
diff --git a/bin/psyad b/bin/psyad
new file mode 100755
index 000000000..8bed931ac
--- /dev/null
+++ b/bin/psyad
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, Science and Technology Facilities Council
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Authors R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+'''Top-level executable driver script wrapper for PSyAD : the PSyclone
+Adjoint support. Transforms an LFRic tangent linear kernel to its
+adjoint.
+
+'''
+import sys
+from psyclone.psyad import main
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
diff --git a/changelog b/changelog
index 8af0ffa3c..0fc38fd4d 100644
--- a/changelog
+++ b/changelog
@@ -13,6 +13,9 @@
         5) PR #1222 for #1171. Adds a new class to encapsulate all constants
         related to the LFRic API.
 
+        6) PR #1233 for #1231. Adds psyadd driver script that will process an
+        LFRic tangent-linear kernel and produce its adjoint.
+
 release 2.0.0 28th April 2021
 
 	1) #778 for #713. Use 'make' to execute all examples.
diff --git a/doc/psyad/user_guide/Makefile b/doc/psyad/user_guide/Makefile
new file mode 100644
index 000000000..d4bb2cbb9
--- /dev/null
+++ b/doc/psyad/user_guide/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/psyad/user_guide/conf.py b/doc/psyad/user_guide/conf.py
new file mode 100644
index 000000000..3706b201e
--- /dev/null
+++ b/doc/psyad/user_guide/conf.py
@@ -0,0 +1,55 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# 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.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'PSyAD'
+copyright = '2021, Rupert Ford and Andrew Porter'
+author = 'Rupert Ford and Andrew Porter'
+
+# The full version, including alpha/beta/rc tags
+release = '0.0.1'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+# -- 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 = 'alabaster'
+
+# 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']
diff --git a/doc/psyad/user_guide/index.rst b/doc/psyad/user_guide/index.rst
new file mode 100644
index 000000000..627b4de42
--- /dev/null
+++ b/doc/psyad/user_guide/index.rst
@@ -0,0 +1,72 @@
+.. -----------------------------------------------------------------------------
+.. BSD 3-Clause License
+..
+.. Copyright (c) 2021, Science and Technology Facilities Council.
+.. All rights reserved.
+..
+.. Redistribution and use in source and binary forms, with or without
+.. modification, are permitted provided that the following conditions are met:
+..
+.. * Redistributions of source code must retain the above copyright notice,
+..   this list of conditions and the following disclaimer.
+..
+.. * Redistributions in binary form must reproduce the above copyright notice,
+..   this list of conditions and the following disclaimer in the documentation
+..   and/or other materials provided with the distribution.
+..
+.. * Neither the name of the copyright holder nor the names of its
+..   contributors may be used to endorse or promote products derived from
+..   this software without specific prior written permission.
+..
+.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+.. "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+.. LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+.. FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+.. COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+.. INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+.. BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+.. LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+.. CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.. LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+.. ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+.. POSSIBILITY OF SUCH DAMAGE.
+.. -----------------------------------------------------------------------------
+.. Written by R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+.. only:: html
+
+    .. image:: ../../logo/psyclone_v1.0.png
+        :width: 75%
+        :align: center
+        :alt: PSyclone
+
+
+PSyAD User Guide
+================
+
+.. only:: html
+
+    PSyAD is PSyclone's Adjoint code generator. PSyAD takes an LFRic
+    tangent linear kernel and translates it into its adjoint.
+
+.. toctree::
+    :hidden:
+    :caption: Table of Contents
+
+.. toctree::
+    :maxdepth: 2
+    :caption: Introduction
+
+    introduction
+    psyad_command
+
+.. when generating latex the index and module index are generated
+   automatically and the entries below are rendered as plain text.
+.. only:: html
+	  
+    Indices and tables
+    ==================
+
+    * :ref:`genindex`
+    * :ref:`modindex`
+    * :ref:`search`
diff --git a/doc/psyad/user_guide/introduction.rst b/doc/psyad/user_guide/introduction.rst
new file mode 100644
index 000000000..eed2495bd
--- /dev/null
+++ b/doc/psyad/user_guide/introduction.rst
@@ -0,0 +1,42 @@
+.. -----------------------------------------------------------------------------
+.. BSD 3-Clause License
+..
+.. Copyright (c) 2021, Science and Technology Facilities Council.
+.. All rights reserved.
+..
+.. Redistribution and use in source and binary forms, with or without
+.. modification, are permitted provided that the following conditions are met:
+..
+.. * Redistributions of source code must retain the above copyright notice, this
+..   list of conditions and the following disclaimer.
+..
+.. * Redistributions in binary form must reproduce the above copyright notice,
+..   this list of conditions and the following disclaimer in the documentation
+..   and/or other materials provided with the distribution.
+..
+.. * Neither the name of the copyright holder nor the names of its
+..   contributors may be used to endorse or promote products derived from
+..   this software without specific prior written permission.
+..
+.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+.. "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+.. LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+.. FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+.. COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+.. INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+.. BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+.. LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+.. CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.. LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+.. ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+.. POSSIBILITY OF SUCH DAMAGE.
+.. -----------------------------------------------------------------------------
+.. Written by R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+.. _introduction:
+
+Introduction
+============
+
+PSyAD is PSyclone's Adjoint code generator. PSyAD takes an LFRic
+tangent linear kernel and translates it into its adjoint.
diff --git a/doc/psyad/user_guide/pip_requirements.txt b/doc/psyad/user_guide/pip_requirements.txt
new file mode 100644
index 000000000..ce0fe6458
--- /dev/null
+++ b/doc/psyad/user_guide/pip_requirements.txt
@@ -0,0 +1,39 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, Science and Technology Facilities Council.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Authors: A. R. Porter and R. W. Ford, STFC Daresbury Lab
+
+# pip requirements file specifying the packages required to build
+# the PSyAD documentation. Used by readthedocs.
+sphinx
+sphinxcontrib.bibtex
diff --git a/doc/psyad/user_guide/psyad_command.rst b/doc/psyad/user_guide/psyad_command.rst
new file mode 100644
index 000000000..4cb598bb4
--- /dev/null
+++ b/doc/psyad/user_guide/psyad_command.rst
@@ -0,0 +1,108 @@
+.. -----------------------------------------------------------------------------
+.. BSD 3-Clause License
+..
+.. Copyright (c) 2021, Science and Technology Facilities Council.
+.. All rights reserved.
+..
+.. Redistribution and use in source and binary forms, with or without
+.. modification, are permitted provided that the following conditions are met:
+..
+.. * Redistributions of source code must retain the above copyright notice, this
+..   list of conditions and the following disclaimer.
+..
+.. * Redistributions in binary form must reproduce the above copyright notice,
+..   this list of conditions and the following disclaimer in the documentation
+..   and/or other materials provided with the distribution.
+..
+.. * Neither the name of the copyright holder nor the names of its
+..   contributors may be used to endorse or promote products derived from
+..   this software without specific prior written permission.
+..
+.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+.. "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+.. LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+.. FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+.. COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+.. INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+.. BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+.. LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+.. CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.. LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+.. ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+.. POSSIBILITY OF SUCH DAMAGE.
+.. -----------------------------------------------------------------------------
+.. Written by R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+.. _psyad_command:
+
+The psyad command
+=================
+
+The simplest way to run PSyAD is to use the ``psyad`` command. If you
+installed PSyclone using ``pip`` then this command should be available
+on your PATH (see the PSyclone user guide for more
+details). Alternatively it can be found in the ``<PSYCLONEHOME>/bin``
+directory. The command takes an LFRic tangent linear kernel file as
+input and outputs its adjoint. This section walks through the
+functionality of the command.
+
+Running
+-------
+
+The ``psyad`` command is an executable script designed to be run from the
+command line, e.g.:
+::
+
+  > psyad <args>
+
+The optional ``-h`` argument gives a description of the options provided
+by the command:
+
+.. parsed-literal::
+		
+  > psyad -h
+    usage: psyad [-h] [-v] [-oad OAD] filename
+
+    Run the PSyclone adjoint code generator on an LFRic tangent-linear kernel file
+
+    positional arguments:
+      filename       LFRic tangent-linear kernel source
+
+    optional arguments:
+      -h, --help     show this help message and exit
+      -v, --verbose  increase the verbosity of the output
+      -oad OAD       filename for the transformed code
+
+Basic Use
+---------
+
+The simplest way to use ``psyad`` is to provide it with an LFRic
+tangent-linear kernel file::
+
+    > psyad tl_kern.f90
+
+If the kernel file is invalid for some reason, the command should
+return with an appropriate error.
+
+If the kernel file is valid then the adjoint kernel code will be
+output to the terminal screen.
+
+File output
+-----------
+
+By default the adjoint kernel code is
+output to the terminal. This can instead be output to a file by using the
+``-oad <file>`` option. For example, the
+following will output the adjoint kernel code to the file 'ad_kern.f90'.
+::
+
+    > psyad -oad ad_kern.f90 tl_kern.f90
+
+Processing information
+----------------------
+
+To see more information about what the psyad script is doing
+internally you can specify the ``-v`` option. For example
+::
+
+   > psyad -oad ad_kern.f90 -v tl_kern.f90
diff --git a/setup.py b/setup.py
index 98bef1b2e..a85602406 100644
--- a/setup.py
+++ b/setup.py
@@ -167,7 +167,7 @@ if __name__ == '__main__':
                      "pytest-pylint", "pytest-flakes", "pytest-pep257"],
         },
         include_package_data=True,
-        scripts=['bin/psyclone', 'bin/genkernelstub'],
+        scripts=['bin/psyclone', 'bin/genkernelstub', 'bin/psyad'],
         data_files=[
             ('share/psyclone',
              ['config/psyclone.cfg'])]+EXAMPLES+TUTORIAL+LIBS,)
diff --git a/src/psyclone/psyad/__init__.py b/src/psyclone/psyad/__init__.py
new file mode 100644
index 000000000..482514a25
--- /dev/null
+++ b/src/psyclone/psyad/__init__.py
@@ -0,0 +1,41 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, Science and Technology Facilities Council
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Authors R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+'''PSyAD, the PSyclone adjoint generation module.'''
+
+from psyclone.psyad.tl2ad import generate_adjoint_str, generate_adjoint
+from psyclone.psyad.main import main
+
+__all__ = ["main", "generate_adjoint_str", "generate_adjoint"]
diff --git a/src/psyclone/psyad/main.py b/src/psyclone/psyad/main.py
new file mode 100644
index 000000000..047ee44f8
--- /dev/null
+++ b/src/psyclone/psyad/main.py
@@ -0,0 +1,92 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, Science and Technology Facilities Council.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Authors R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+'''Top-level driver functions for PSyAD : the PSyclone Adjoint
+support. Transforms an LFRic tangent linear kernel to its adjoint.
+
+'''
+from __future__ import absolute_import, print_function
+import argparse
+import logging
+import sys
+
+import six
+
+from psyclone.generator import write_unicode_file
+from psyclone.psyad import generate_adjoint_str
+
+
+def main(args):
+    '''Takes an LFRic tangent linear kernel source file as input and
+    produces its adjoint.
+
+    :param list args: the list of command-line arguments that PSyAD has \
+                      been invoked with.
+
+    '''
+    logger = logging.getLogger(__name__)
+
+    parser = argparse.ArgumentParser(
+        description="Run the PSyclone adjoint code generator on an LFRic "
+        "tangent-linear kernel file")
+    parser.add_argument(
+        '-v', '--verbose', help='increase the verbosity of the output',
+        action='store_true')
+    parser.add_argument('-oad', help='filename for the transformed code')
+    parser.add_argument('filename', help='LFRic tangent-linear kernel source')
+
+    args = parser.parse_args(args)
+
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG)
+
+    # TL Fortran code
+    filename = args.filename
+    logger.info("Reading kernel file %s", filename)
+    with open(filename) as my_file:
+        tl_fortran_str = my_file.read()
+        tl_fortran_str = six.text_type(tl_fortran_str)
+
+    ad_fortran_str = generate_adjoint_str(tl_fortran_str)
+
+    # AD Fortran code
+    if args.oad:
+        logger.info("Writing adjoint of kernel to file %s", args.oad)
+        write_unicode_file(ad_fortran_str, args.oad)
+    else:
+        print(ad_fortran_str, file=sys.stdout)
+
+
+__all__ = ["main"]
diff --git a/src/psyclone/psyad/tl2ad.py b/src/psyclone/psyad/tl2ad.py
new file mode 100644
index 000000000..b7f96cda0
--- /dev/null
+++ b/src/psyclone/psyad/tl2ad.py
@@ -0,0 +1,106 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, Science and Technology Facilities Council.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Authors R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+'''The implementation of PSyAD : the PSyclone Adjoint
+support. Transforms an LFRic tangent linear kernel to its adjoint.
+
+'''
+import logging
+from psyclone.psyir.frontend.fortran import FortranReader
+from psyclone.psyir.backend.fortran import FortranWriter
+
+
+def generate_adjoint_str(tl_fortran_str):
+    '''Takes an LFRic tangent-linear kernel encoded as a string as input
+    and returns its adjoint encoded as a string.
+
+    :param str tl_fortran_str: Fortran implementation of an LFRic \
+        tangent-linear kernel.
+
+    :returns: a string containing the Fortran implementation of the \
+        supplied tangent-linear kernel.
+    :rtype: str
+
+    '''
+    logger = logging.getLogger(__name__)
+    logger.debug(tl_fortran_str)
+
+    # TL Language-level PSyIR
+    reader = FortranReader()
+    tl_psyir = reader.psyir_from_source(tl_fortran_str)
+
+    # Addressing issue #1238 will allow the view() method to be output
+    # to the logger.
+    # logger.debug(tl_psyir.view())
+
+    # TL to AD translation
+    ad_psyir = generate_adjoint(tl_psyir)
+
+    # AD Fortran code
+    writer = FortranWriter()
+    adjoint_fortran_str = writer(ad_psyir)
+    logger.debug(adjoint_fortran_str)
+
+    return adjoint_fortran_str
+
+
+def generate_adjoint(tl_psyir):
+    '''Takes an LFRic tangent-linear kernel represented in language-level PSyIR
+    and returns its adjoint represented in language-level PSyIR.
+
+    :param tl_psyir: language-level PSyIR containing the LFRic \
+        tangent-linear kernel.
+    :type tl_psyir: :py:class:`psyclone.psyir.Node`
+
+    :returns: language-level PSyIR containing the adjoint of the \
+        supplied tangent-linear kernel.
+    :rtype: :py:class:`psyclone.psyir.Node`
+
+    '''
+    logger = logging.getLogger(__name__)
+
+    # TL LFRic-specific PSyIR
+    logger.debug(
+        "Translation from generic PSyIR to LFRic-specific PSyIR should be "
+        "done now.")
+
+    # Transform from TL to AD
+    logger.debug("Transformation from TL to AD should be done now.")
+    ad_psyir = tl_psyir
+
+    return ad_psyir
+
+
+__all__ = ["generate_adjoint_str", "generate_adjoint"]
diff --git a/src/psyclone/psyir/nodes/node.py b/src/psyclone/psyir/nodes/node.py
index 22b2c0a82..d9dfe9176 100644
--- a/src/psyclone/psyir/nodes/node.py
+++ b/src/psyclone/psyir/nodes/node.py
@@ -40,6 +40,8 @@
 This module contains the abstract Node implementation.
 
 '''
+from __future__ import print_function
+
 import copy
 import six
 from psyclone.psyir.symbols import SymbolError
@@ -807,11 +809,12 @@ class Node(object):
         '''
         from psyclone.psyir.nodes import Schedule
         if not isinstance(self.parent, Schedule) or index is None:
-            print("{0}{1}".format(self.indent(indent),
-                                  self.node_str(colour=True)))
+            result = "{0}{1}".format(self.indent(indent),
+                                     self.node_str(colour=True))
         else:
-            print("{0}{1}: {2}".format(self.indent(indent), index,
-                                       self.node_str(colour=True)))
+            result = "{0}{1}: {2}".format(self.indent(indent), index,
+                                          self.node_str(colour=True))
+        print(six.text_type(result))
         for idx, entity in enumerate(self._children):
             entity.view(indent=indent + 1, index=idx)
 


Test patch:
diff --git a/src/psyclone/tests/psyad/main_test.py b/src/psyclone/tests/psyad/main_test.py
new file mode 100644
index 000000000..06c87b9ff
--- /dev/null
+++ b/src/psyclone/tests/psyad/main_test.py
@@ -0,0 +1,203 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, Science and Technology Facilities Council.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Authors R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+'''A module to perform pytest tests on the code in the main.py file
+within the psyad directory.
+
+'''
+from __future__ import print_function, absolute_import
+import logging
+import six
+import pytest
+from psyclone.psyad import main
+
+# main function
+
+
+# -h description
+def test_main_h_option(capsys):
+    '''Test that the -h script option works as expected'''
+
+    with pytest.raises(SystemExit) as info:
+        main(["-h", "filename"])
+    assert str(info.value) == "0"
+    output, error = capsys.readouterr()
+    assert error == ""
+    # The name of the executable is replaced with either pytest or -c
+    # when using pytest, therefore we split this test into sections.
+    expected1 = "usage: "
+    expected2 = (
+        "[-h] [-v] [-oad OAD] filename\n\n"
+        "Run the PSyclone adjoint code generator on an LFRic tangent-linear "
+        "kernel file\n\n"
+        "positional arguments:\n"
+        "  filename       LFRic tangent-linear kernel source\n\n"
+        "optional arguments:\n"
+        "  -h, --help     show this help message and exit\n"
+        "  -v, --verbose  increase the verbosity of the output\n"
+        "  -oad OAD       filename for the transformed code\n")
+    assert expected1 in output
+    assert expected2 in output
+
+
+# no filename
+def test_main_no_filename(capsys):
+    '''Test that the main() function raises an exception if the filename
+    argument is not supplied.
+
+    '''
+    with pytest.raises(SystemExit) as info:
+        main([])
+    assert str(info.value) == "2"
+    output, error = capsys.readouterr()
+    assert output == ""
+    # Python2 returns a different message to Python3. Also, the name
+    # of the executable is replaced with either pytest or -c when
+    # using pytest, therefore we split the test into sections.
+    expected1 = "usage: "
+    expected2 = "[-h] [-v] [-oad OAD] filename"
+    if six.PY2:
+        expected3 = "error: too few arguments\n"
+    else:
+        expected3 = "error: the following arguments are required: filename\n"
+    assert expected1 in error
+    assert expected2 in error
+    assert expected3 in error
+
+
+# invalid filename
+def test_main_invalid_filename():
+    '''Test that the the main() function raises an exception if the
+    filename does not exist.
+
+    '''
+    # FileNotFoundError does not exist in Python2
+    try:
+        FileNotFoundError
+    except NameError:
+        # pylint: disable=redefined-builtin
+        FileNotFoundError = IOError
+    with pytest.raises(FileNotFoundError) as info:
+        main(["does_not_exist.f90"])
+    assert ("[Errno 2] No such file or directory: 'does_not_exist.f90'"
+            in str(info.value))
+
+
+# writing to stdout
+def test_main_stdout(tmpdir, capsys):
+    '''Test that the the main() function returns its output to stdout by
+    default.
+
+    '''
+    tl_code = (
+        "program test\n"
+        "integer :: a\n"
+        "a = 0.0\n"
+        "end program test\n")
+    expected = (
+        "program test\n"
+        "  integer :: a\n\n"
+        "  a = 0.0\n\n"
+        "end program test\n")
+    filename = six.text_type(tmpdir.join("tl.f90"))
+    with open(filename, "a") as my_file:
+        my_file.write(tl_code)
+    main([filename])
+    output, error = capsys.readouterr()
+    assert error == ""
+    assert expected in output
+
+
+# using -oad
+def test_main_fileout(tmpdir, capsys):
+    '''Test that the the main() function returns its output to a file if
+    specified.
+
+    '''
+    tl_code = (
+        "program test\n"
+        "integer :: a\n"
+        "a = 0.0\n"
+        "end program test\n")
+    expected = (
+        "program test\n"
+        "  integer :: a\n\n"
+        "  a = 0.0\n\n"
+        "end program test\n")
+    filename_in = str(tmpdir.join("tl.f90"))
+    filename_out = str(tmpdir.join("ad.f90"))
+    with open(filename_in, "a") as my_file:
+        my_file.write(tl_code)
+    main([filename_in, "-oad", filename_out])
+    output, error = capsys.readouterr()
+    assert error == ""
+    assert output == ""
+    with open(filename_out, 'r') as my_file:
+        data = my_file.read()
+    assert expected in data
+
+
+# -v output
+@pytest.mark.xfail(reason="issue #1235: caplog returns an empty string in "
+                   "github actions.", strict=False)
+def test_main_verbose(tmpdir, capsys, caplog):
+    '''Test that the the main() function outputs additional information if
+    the -v flag is set. Actually -v seems to have no effect here as
+    pytest takes control of the logging and we have to set it to the
+    required level manually.
+
+    '''
+    tl_code = (
+        "program test\n"
+        "integer :: a\n"
+        "a = 0.0\n"
+        "end program test\n")
+    filename_in = str(tmpdir.join("tl.f90"))
+    filename_out = str(tmpdir.join("ad.f90"))
+    with open(filename_in, "a") as my_file:
+        my_file.write(tl_code)
+    with caplog.at_level(logging.DEBUG):
+        main([filename_in, "-v", "-oad", filename_out])
+
+    output, error = capsys.readouterr()
+    assert error == ""
+    assert output == ""
+    assert ("INFO     psyclone.psyad.main:main.py:77 Reading kernel file /"
+            in caplog.text)
+    assert "/tl.f90" in caplog.text
+    assert "/tl.f90" in caplog.text
+    assert ("INFO     psyclone.psyad.main:main.py:86 Writing adjoint of "
+            "kernel to file /" in caplog.text)
+    assert "/ad.f90" in caplog.text
diff --git a/src/psyclone/tests/psyad/tl2ad_test.py b/src/psyclone/tests/psyad/tl2ad_test.py
new file mode 100644
index 000000000..8b75ee024
--- /dev/null
+++ b/src/psyclone/tests/psyad/tl2ad_test.py
@@ -0,0 +1,156 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021, Science and Technology Facilities Council.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Authors R. W. Ford and A. R. Porter, STFC Daresbury Lab
+
+'''A module to perform pytest tests on the code in the tl2ad.py file
+within the psyad directory.
+
+'''
+from __future__ import print_function, absolute_import
+import logging
+import pytest
+import six
+
+from psyclone.psyir.frontend.fortran import FortranReader
+from psyclone.psyir.backend.fortran import FortranWriter
+from psyclone.psyad import generate_adjoint_str, generate_adjoint
+
+
+# 1: generate_adjoint_str function
+
+# expected output
+@pytest.mark.xfail(reason="issue #1235: caplog returns an empty string in "
+                   "github actions.", strict=False)
+def test_generate_adjoint_str(caplog):
+    '''Test that the generate_adjoint_str() function works as expected
+    including logging.
+
+    '''
+    tl_code = (
+        "program test\n"
+        "integer :: a\n"
+        "a = 0.0\n"
+        "end program test\n")
+    expected = (
+        "program test\n"
+        "  integer :: a\n\n"
+        "  a = 0.0\n\n"
+        "end program test\n")
+
+    with caplog.at_level(logging.INFO):
+        result = generate_adjoint_str(tl_code)
+
+    assert caplog.text == ""
+    assert expected in result
+
+    with caplog.at_level(logging.DEBUG):
+        result = generate_adjoint_str(tl_code)
+
+    assert "DEBUG    psyclone.psyad.tl2ad:tl2ad.py:58" in caplog.text
+    assert tl_code in caplog.text
+    assert "DEBUG    psyclone.psyad.tl2ad:tl2ad.py:74" in caplog.text
+    assert expected in caplog.text
+    assert expected in result
+
+
+# 2: generate_adjoint function
+def test_generate_adjoint():
+    '''Test that the generate_adjoint() function works as expected.'''
+
+    tl_fortran_str = (
+        "program test\n"
+        "integer :: a\n"
+        "a = 0.0\n"
+        "end program test\n")
+    expected_ad_fortran_str = (
+        "program test\n"
+        "  integer :: a\n\n"
+        "  a = 0.0\n\n"
+        "end program test\n")
+    reader = FortranReader()
+    tl_psyir = reader.psyir_from_source(tl_fortran_str)
+
+    ad_psyir = generate_adjoint(tl_psyir)
+
+    writer = FortranWriter()
+    ad_fortran_str = writer(ad_psyir)
+    assert expected_ad_fortran_str in ad_fortran_str
+
+
+# generate_adjoint function logging
+@pytest.mark.xfail(reason="issue #1235: caplog returns an empty string in "
+                   "github actions.", strict=False)
+def test_generate_adjoint_logging(caplog):
+    '''Test that logging works as expected in the generate_adjoint()
+    function.
+
+    '''
+    tl_fortran_str = (
+        "program test\n"
+        "integer :: a\n"
+        "a = 0.0\n"
+        "end program test\n")
+    expected_ad_fortran_str = (
+        "program test\n"
+        "  integer :: a\n\n"
+        "  a = 0.0\n\n"
+        "end program test\n")
+    reader = FortranReader()
+    tl_psyir = reader.psyir_from_source(tl_fortran_str)
+
+    with caplog.at_level(logging.INFO):
+        ad_psyir = generate_adjoint(tl_psyir)
+    assert caplog.text == ""
+
+    writer = FortranWriter()
+    ad_fortran_str = writer(ad_psyir)
+    assert expected_ad_fortran_str in ad_fortran_str
+
+    with caplog.at_level(logging.DEBUG):
+        ad_psyir = generate_adjoint(tl_psyir)
+    # Python2 and 3 report different line numbers
+    if six.PY2:
+        line_number = 96
+    else:
+        line_number = 95
+    assert (
+        "DEBUG    psyclone.psyad.tl2ad:tl2ad.py:{0} Translation from generic "
+        "PSyIR to LFRic-specific PSyIR should be done now.".format(line_number)
+        in caplog.text)
+    assert (
+        "DEBUG    psyclone.psyad.tl2ad:tl2ad.py:100 Transformation from TL to "
+        "AD should be done now." in caplog.text)
+
+    ad_fortran_str = writer(ad_psyir)
+    assert expected_ad_fortran_str in ad_fortran_str