diff --git a/.travis.yml b/.travis.yml
index 51b66ff..2b60153 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,23 @@
 language: python
-
 python:
 - '2.7'
 - '3.8'
-
 install:
 - pip install -r requirements.txt
 - pip install -r test_requirements.txt
 - pip install -e .
-
 script:
-- pytest --cov=src/ --cov-report term-missing --cov-report term:skip-covered --cov-config=tox.ini --cov-fail-under=100 -svv .
-- flake8 src
\ No newline at end of file
+- pytest --cov=src/ --cov-report term-missing --cov-report term:skip-covered --cov-config=tox.ini
+  --cov-fail-under=100 -svv .
+- flake8 src
+deploy:
+- provider: pypi
+  user: CitrineInformatics
+  password: "$PYPI_PASSWORD"
+  distributions: sdist bdist_wheel
+  skip_existing: true
+  on:
+    tags: true
+env:
+  global:
+    secure: M9HI5bpY5t4Rd17rjdrSo6QF4jV2JMi3hOgMzMGgly4jP1NoafOBSpJHy4TisBeE8eyrkJPJurMnF0Fw7kACtpCMT6h8tLE4r6Ss97542MJnFLV33hILdcwJZjAYfaQ0q5lrWmtNr420y5kvD1EjL3jQnYYVJC2wjAb4b5C8Ji5i/43n5Vuk3aO2BG8IzNXF0buqtAWIwyclOpr/QjkSWxf+ptj4Fv64Cy84aVTFMOGL523DRVkZgCl5vdq6gSpFTk9S/a2G16LM6GPU3ohKRzIGRzRv8HsGL4oyi2c+NjzysIdOq7yx/8crM4fADAl+xG3bh8biYJY6PVfxrR4ttNdL6UFxg0xUwPVpmngL+YjUS6DOi7cVs9VoaqsPq5mrrcZ5HvL+RaJt43o0okxBqvmZW/SRJs0fAdDLu1UKIxzH9MTpdQmuUhCyyh6muePnnzvNkSzXnZRSO2z/DUmLsRiA3qMJkY/CSIWvxqj9+tVvAT9jRjb+loWBrra3FR5+sMikmoT/Qd27xCYSrwTMBy7jgw4mOh6xNK6FmT1nS/JDpH0KrZDXwjsB2uHSw9G1EnrBF7fhXCTx7SodD9ypwKrB0aIUrRj2iUcsAKGN4mVz92KvF/OhhuLmH2+hiivSM98k2R36bZfhEuAqFTSN5DM/UoqLXckdr63vahxHlEA=
diff --git a/MANIFEST.in b/MANIFEST.in
index 11dad2a..d5c2f9c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,6 @@
-include src/dftinpgen/*.txt
-include src/dftinpgen/data/*.json
-include src/dftinpgen/qe/settings/*.json
-include src/dftinpgen/qe/settings/calculation_presets/*.json
+include src/dftinputgen/*.txt
+include src/dftinputgen/data/*.json
+include src/dftinputgen/qe/settings/*.json
+include src/dftinputgen/qe/settings/calculation_presets/*.json
+include src/dftinputgen/gpaw/settings/*.json
+include src/dftinputgen/gpaw/settings/calculation_presets/*.json
diff --git a/README.md b/README.md
index 6ac8cf1..6f21c20 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,10 @@ documentation.
 
 1. [pw.x](https://www.quantum-espresso.org/Doc/INPUT_PW.html) from the
    [Quantum Espresso package](https://www.quantum-espresso.org/)
-2. (under development) Post-processing utilities for pw.x:
+2. [GPAW](https://wiki.fysik.dtu.dk/gpaw/index.html) including functions for
+`surface relaxation`, `adsorbate vibration calculations`, and `lattice
+parameter optimization` (fcc, bcc, and hcp) 
+3. (under development) Post-processing utilities for pw.x:
    [dos.x](https://www.quantum-espresso.org/Doc/INPUT_DOS.html),
    [bands.x](https://www.quantum-espresso.org/Doc/INPUT_BANDS.html),
    [projwfc.x](https://www.quantum-espresso.org/Doc/INPUT_PROJWFC.html)
diff --git a/docs/src/conf.py b/docs/src/conf.py
index 81cbe9e..4f535a3 100644
--- a/docs/src/conf.py
+++ b/docs/src/conf.py
@@ -23,7 +23,9 @@
 author = "Vinay Hegde <vhegde@citrine.io>"
 
 # The full version, including alpha/beta/rc tags
-release = "0.1.0"
+version_file = os.path.join("..", "..", "src", "dftinputgen", "VERSION.txt")
+with open(version_file, "r") as fr:
+    release = fr.read().strip()
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/docs/src/developer_notes/index.rst b/docs/src/developer_notes/index.rst
index 2a179a9..8b1a0cd 100644
--- a/docs/src/developer_notes/index.rst
+++ b/docs/src/developer_notes/index.rst
@@ -20,10 +20,20 @@ Code Organization
         settings/
             tags_and_groups.json
             ...
-            base_recipes/
+            calculation_presets/
                 scf.json
                 vc-relax.json
                 ...
+    gpaw/
+        gpaw.py
+        ...
+        settings/
+            tags_and_groups.json
+            ...
+            calculation_presets/
+                bulk_opt.json
+                bulk_opt_hcp.json
+                ...
     vasp/
         ...
 
@@ -33,6 +43,8 @@ Code Organization
 - ``dftinputgen.qe``: derived classes that can generate input files for various
   DFT-based and postprocessing codes in the Quantum Espresso suite (more
   :ref:`here <ssec-qe>`)
+- ``dftinputgen.gpaw``: derived classes that can generate input python scripts
+  for the GPAW package (more :ref:`here <ssec-gpaw>`).
 - ``dftinputgen.vasp``: [under development] derived classes that can generate
   input files for the VASP package.
 - ``dftinputgen.utils``: general-purpose helper functions, e.g. chemical formula
diff --git a/docs/src/module_reference/gpaw/gpaw.rst b/docs/src/module_reference/gpaw/gpaw.rst
new file mode 100644
index 0000000..cd33043
--- /dev/null
+++ b/docs/src/module_reference/gpaw/gpaw.rst
@@ -0,0 +1,49 @@
+.. _sssec-gpaw:
+
+Input GPAW
+++++++++++
+
+The :class:`GPAWInputGenerator <dftinputgen.gpaw.GPAWInputGenerator>` class
+(derived from :class:`DftInputGenerator <dftinputgen.base.DftInputGenerator>`)
+implements functionality to generate python scripts for the `GPAW`_
+package. 
+
+Python scripts are written that use `ASE`_ with GPAW specified via a
+calculator object. This calculator object is where all settings for
+the DFT calculation is given. An example of the calculator object
+that is automatically written is given below.
+
+.. code-block:: python
+
+   slab.calc = GPAW(
+   h=0.16,
+   kpts={'size': [4, 4, 1]},
+   occupations={'name': 'fermi-dirac', 'width': 0.05},
+   poissonsolver={'dipolelayer': 'xy'},
+   xc='BEEF-vdW'
+   )
+
+For each calculator setting, a list of valid parameters is looked up from
+a ``GPAW_TAGS`` dictionary which is then defined within the written
+calculator object (more information about the valid calculator parameters
+and defaults provided as ``calculation_presets`` is :ref:`here
+<sssec-gpaw-input-settings>`).
+
+Currently supported calculations include relaxations, bulk optimizations,
+and total energy.
+
+**Note:** The scripts are written assuming that the crystal structure
+is stored in an `ASE readable format`_ (e.g. traj, cif, etc..) with 
+the desired initial magnetic moments defined.
+
+.. _`GPAW`: https://wiki.fysik.dtu.dk/gpaw/index.html
+.. _`ASE`: https://wiki.fysik.dtu.dk/ase/
+.. _`ASE readable format`: https://wiki.fysik.dtu.dk/ase/ase/io/io.html
+
+Interfaces
+==========
+
+.. automodule:: dftinputgen.gpaw.gpaw
+    :members:
+    :inherited-members:
+    :undoc-members:
diff --git a/docs/src/module_reference/gpaw/index.rst b/docs/src/module_reference/gpaw/index.rst
new file mode 100644
index 0000000..b72945e
--- /dev/null
+++ b/docs/src/module_reference/gpaw/index.rst
@@ -0,0 +1,21 @@
+.. _ssec-gpaw:
+
+Input generators for GPAW
++++++++++++++++++++++++++
+
+``dftinputgen`` provides an input generator class for the `GPAW`_ package.
+More information :ref:`here <sssec-gpaw>` 
+
+.. _`GPAW`: https://wiki.fysik.dtu.dk/gpaw/index.html
+
+
+.. automodule:: dftinputgen.gpaw
+    :members:
+    :undoc-members:
+
+.. toctree::
+    :maxdepth: 2
+    :hidden:
+
+    gpaw
+    settings
diff --git a/docs/src/module_reference/gpaw/settings.rst b/docs/src/module_reference/gpaw/settings.rst
new file mode 100644
index 0000000..a46288f
--- /dev/null
+++ b/docs/src/module_reference/gpaw/settings.rst
@@ -0,0 +1,33 @@
+.. _sssec-gpaw-input-settings:
+
+Input settings
+++++++++++++++
+
+All input settings have been parsed from the `GPAW manual`_
+(last updated January 2021).
+The calculator parameter names are stored in `tags_and_groups.json`_.
+These parameter names are then made available to the user via
+a module level variable ``GPAW_TAGS``.
+
+The settings module also makes available a few sets of default settings 
+to be used for common DFT calculations such as ``surface_relax``,
+and ``bulk_opt``.
+The defaults are stored in JSON files in the `calculation_presets`_ module.
+These can be accessed by the user via a module level variable ``GPAW_PRESETS``.
+Note that these presets are only reasonable defaults and are not meant to be
+prescriptive.
+
+.. _`GPAW manual`: https://wiki.fysik.dtu.dk/gpaw/documentation/manual.html#parameters
+.. _`tags_and_groups.json`: https://github.com/CitrineInformatics/dft-input-gen/blob/master/src/dftinputgen/qe/settings/tags_and_groups.json 
+.. _`calculation_presets`: https://github.com/CitrineInformatics/dft-input-gen/tree/master/src/dftinputgen/qe/settings/calculation_presets
+
+.. automodule:: dftinputgen.gpaw.settings
+    :members:
+    :undoc-members:
+    :special-members:
+
+.. automodule:: dftinputgen.gpaw.settings.calculation_presets
+    :members:
+    :undoc-members:
+    :special-members:
+
diff --git a/docs/src/module_reference/index.rst b/docs/src/module_reference/index.rst
index 2ac7eb8..630831a 100644
--- a/docs/src/module_reference/index.rst
+++ b/docs/src/module_reference/index.rst
@@ -9,5 +9,6 @@ Module reference
 
     base
     qe/index
+    gpaw/index
     utils
     data
diff --git a/setup.py b/setup.py
index 3996cd5..794b1bb 100644
--- a/setup.py
+++ b/setup.py
@@ -20,6 +20,7 @@
     author_email="vhegde@citrine.io",
     packages=find_packages(where="src"),
     package_dir={"": "src"},
+    include_package_data=True,
     install_requires=["six", "numpy", "ase <= 3.17"],
     entry_points={"console_scripts": ["dftinputgen = dftinputgen.cli:driver"]},
     classifiers=[
diff --git a/src/dftinputgen/VERSION.txt b/src/dftinputgen/VERSION.txt
index 6e8bf73..d917d3e 100644
--- a/src/dftinputgen/VERSION.txt
+++ b/src/dftinputgen/VERSION.txt
@@ -1 +1 @@
-0.1.0
+0.1.2
diff --git a/src/dftinputgen/cli.py b/src/dftinputgen/cli.py
index f5a719d..0e883fa 100644
--- a/src/dftinputgen/cli.py
+++ b/src/dftinputgen/cli.py
@@ -2,6 +2,8 @@
 
 from dftinputgen.demo.pwx import build_pwx_parser
 from dftinputgen.demo.pwx import generate_pwx_input_files
+from dftinputgen.demo.gpaw import build_gpaw_parser
+from dftinputgen.demo.gpaw import generate_gpaw_input_files
 
 
 def get_parser():
@@ -28,6 +30,12 @@ def get_parser():
     # other subparsers, to be added similarly, go here
     # e.g. ones for gpaw/vasp
 
+    # gpaw subparser
+    gpaw_help = "Generate a python input file for GPAW"
+    gpaw_parser = subparsers.add_parser("gpaw", help=gpaw_help)
+    build_gpaw_parser(gpaw_parser)
+    gpaw_parser.set_defaults(func=generate_gpaw_input_files)
+
     return parser
 
 
diff --git a/src/dftinputgen/demo/gpaw.py b/src/dftinputgen/demo/gpaw.py
new file mode 100644
index 0000000..a6c9e02
--- /dev/null
+++ b/src/dftinputgen/demo/gpaw.py
@@ -0,0 +1,117 @@
+"""Demo generating input files for doing a calculation with pw.x."""
+
+import json
+import argparse
+
+from dftinputgen.utils import read_crystal_structure
+from dftinputgen.gpaw import GPAWInputGenerator
+
+
+def _get_default_parser():
+    description = "Input file generation for gpaw."
+    return argparse.ArgumentParser(description=description)
+
+
+def build_gpaw_parser(parser):
+    """Adds GPAW arguments to the input `argparse.ArgumentParser` object."""
+    # Required:
+    crystal_structure = "(REQUIRED) File with the input crystal structure"
+    parser.add_argument(
+        "-i",
+        "--crystal-structure",
+        type=read_crystal_structure,
+        help=crystal_structure,
+        required=True,
+    )
+
+    # Optional:
+    calculation_presets = "Preset group of tags and default values to use"
+    parser.add_argument(
+        "-pre",
+        "--calculation-presets",
+        choices=[
+            "surface_relax",
+            "bulk_opt",
+            "bulk_opt_hcp",
+            "molecule",
+            "surface_adsorbate_vibrations",
+        ],
+        default=None,
+        help=calculation_presets,
+    )
+
+    custom_settings_file = "JSON file with custom DFT settings to use"
+    parser.add_argument(
+        "-file",
+        "--custom-settings-file",
+        default=None,
+        help=custom_settings_file,
+    )
+
+    custom_settings_dict = """JSON string with a dictionary of custom DFT
+    settings to use. Example: '{"pseudo_dir": "/path/to/pseudo_dir/"}'"""
+    parser.add_argument(
+        "-dict",
+        "--custom-settings-dict",
+        default="{}",
+        type=json.loads,
+        help=custom_settings_dict,
+    )
+
+    write_location = "Directory to write the input file(s) in"
+    parser.add_argument("-loc", "--write-location", help=write_location)
+
+    gpaw_input_file = "Name of the GPAW input file to be written"
+    parser.add_argument("-o", "--gpaw-input-file", help=gpaw_input_file)
+
+    gpaw_restart_file = "Name of the GPAW restart file that the written script\
+        will read from"
+    parser.add_argument("-r", "--gpaw-restart-file", help=gpaw_restart_file)
+
+    input_struct_filename = "Name of the input structure that the written\
+        script will read from"
+    parser.add_argument(
+        "-is", "--input_struct_filename", help=input_struct_filename
+    )
+
+    overwrite_files = "To overwrite files or not, that is the question"
+    parser.add_argument("-ov", "--overwrite_files", help=overwrite_files)
+
+    from_scratch = "Whether the calculation should be run from scratch"
+    parser.add_argument("-fs", "--from-scratch", help=from_scratch)
+
+
+def generate_gpaw_input_files(args):
+    """Write input files for the input crystal structure."""
+    gig = GPAWInputGenerator(
+        crystal_structure=args.crystal_structure,
+        calculation_presets=args.calculation_presets,
+        custom_sett_file=args.custom_settings_file,
+        custom_sett_dict=args.custom_settings_dict,
+        write_location=args.write_location,
+        gpaw_input_file=args.gpaw_input_file,
+        gpaw_restart_file=args.gpaw_restart_file,
+        input_struct_filename=args.input_struct_filename,
+        overwrite_files=args.overwrite_files,
+        from_scratch=args.from_scratch,
+    )
+    gig.write_input_files()
+
+
+def run_demo(*sys_args):
+    """End-to-end run of pw.x input file generation."""
+    parser = _get_default_parser()
+    build_gpaw_parser(parser)
+    args = parser.parse_args(*sys_args)
+    generate_gpaw_input_files(args)
+
+
+if __name__ == "__main__":
+    """
+    When run as a script, this module will generate input files to use with
+    GPAW, for a specified crystal structure, calculation presets, and any
+    custom DFT settings on top of preset defaults.
+
+    For a list of optional arguments, run this script with "-h" argument.
+    """
+    run_demo()  # pragma: no cover
diff --git a/src/dftinputgen/gpaw/__init__.py b/src/dftinputgen/gpaw/__init__.py
new file mode 100644
index 0000000..0dad217
--- /dev/null
+++ b/src/dftinputgen/gpaw/__init__.py
@@ -0,0 +1 @@
+from dftinputgen.gpaw.gpaw import GPAWInputGenerator  # noqa: F401
diff --git a/src/dftinputgen/gpaw/gpaw.py b/src/dftinputgen/gpaw/gpaw.py
new file mode 100644
index 0000000..133178e
--- /dev/null
+++ b/src/dftinputgen/gpaw/gpaw.py
@@ -0,0 +1,389 @@
+import os
+
+from dftinputgen.gpaw.settings import GPAW_TAGS
+from dftinputgen.gpaw.settings.calculation_presets import GPAW_PRESETS
+
+from dftinputgen.base import DftInputGenerator
+from dftinputgen.base import DftInputGeneratorError
+
+
+class GPAWInputGeneratorError(DftInputGeneratorError):
+    pass
+
+
+class GPAWInputGenerator(DftInputGenerator):
+    """Base class to generate python scripts for GPAW"""
+
+    def __init__(
+        self,
+        crystal_structure=None,
+        calculation_presets=None,
+        custom_sett_file=None,
+        custom_sett_dict=None,
+        write_location=None,
+        gpaw_input_file=None,
+        gpaw_restart_file=None,
+        input_struct_filename=None,
+        overwrite_files=None,
+        from_scratch=None,
+        **kwargs,
+    ):
+        """
+        Constructor.
+
+        Parameters
+        ----------
+
+        crystal_structure: :class:`ase.Atoms` object
+            :class:`ase.Atoms` object from `ase.io.read([crystal structure
+            file])`.
+
+        calculation_presets: str, optional
+            The "base" calculation settings to use--must be one of the
+            pre-defined groups of tags and values provided for GPAW.
+
+            Pre-defined settings for some common calculation types are in
+            INSTALL_PATH/qe/settings/calculation_presets/
+
+        custom_sett_file: str, optional
+            Location of a JSON file with custom calculation settings as a
+            dictionary of tags and values.
+
+            NB: Custom settings specified here always OVERRIDE those in
+            `calculation_presets` in case of overlap.
+
+        custom_sett_dict: dict, optional
+            Dictionary with custom calculation settings as tags and values/
+
+            NB: Custom settings specified here always OVERRIDE those in
+            `calculation_presets` and `custom_sett_file`.
+
+        write_location: str, optional
+            Path to the directory in which to write the input file(s).
+
+            Default: Current working directory.
+
+        gpaw_input_file: str, optional
+            Name of the file in which to write the GPAW python script
+
+            Default: "[`calculation_presets`]_in.py" if `calculation_presets`
+            is specified by the user, else "gpaw_in.py".
+
+        gpaw_restart_file: str, optional
+            Name of the gpaw restart file that the written gpaw script will
+            read from.
+
+            Default: "output.gpw"
+
+        input_struct_filename: str, optional
+            Name of the input structure file readable by `ase.io.read` that the
+            written gpaw script will call from if no restart files present.
+
+            Default: "input.traj"
+
+        overwrite_files: bool, optional
+            To overwrite files or not, that is the question.
+
+            Default: True
+
+        from_scratch: bool, optional
+            Even if restart files are found, whether to restart
+            the job from scratch (ie. fresh GPAW calculator)
+            each time the job is resubmitted
+
+        **kwargs:
+            Arbitrary keyword arguments.
+
+        """
+
+        super(GPAWInputGenerator, self).__init__(
+            crystal_structure=crystal_structure,
+            calculation_presets=calculation_presets,
+            custom_sett_file=custom_sett_file,
+            custom_sett_dict=custom_sett_dict,
+            write_location=write_location,
+            overwrite_files=overwrite_files,
+            **kwargs,
+        )
+
+        self._calculation_settings = self._get_calculation_settings()
+
+        self._gpaw_input_file = self._get_default_input_filename()
+        self.gpaw_input_file = gpaw_input_file
+
+        self._gpaw_restart_file = "output.gpw"
+        self.gpaw_restart_file = gpaw_restart_file
+
+        self._struct_filename = "input.traj"
+        self.struct_filename = input_struct_filename
+
+        self._from_scratch = False
+        self.from_scratch = from_scratch
+
+    @property
+    def dft_package(self):
+        return "GPAW"
+
+    @property
+    def gpaw_input_file(self):
+        """Name of the gpaw input file to write to."""
+        return self._gpaw_input_file
+
+    @gpaw_input_file.setter
+    def gpaw_input_file(self, gpaw_input_file):
+        if gpaw_input_file is not None:
+            self._gpaw_input_file = gpaw_input_file
+
+    @property
+    def gpaw_restart_file(self):
+        """Name of gpaw output file to read from if restarting a calculation"""
+        return self._gpaw_restart_file
+
+    @gpaw_restart_file.setter
+    def gpaw_restart_file(self, gpaw_restart_file):
+        if gpaw_restart_file is not None:
+            self._gpaw_restart_file = gpaw_restart_file
+
+    @property
+    def struct_filename(self):
+        """Name of the structure file that the gpaw script will read from."""
+        return self._struct_filename
+
+    @struct_filename.setter
+    def struct_filename(self, struct_filename):
+        if struct_filename is not None:
+            self._struct_filename = struct_filename
+
+    @property
+    def from_scratch(self):
+        """Whether will always start jobs from scratch, even in presence of output file"""
+        return self._from_scratch
+
+    @from_scratch.setter
+    def from_scratch(self, from_scratch):
+        if from_scratch is not None:
+            self._from_scratch = from_scratch
+
+    @property
+    def calculation_settings(self):
+        """Dictionary of all calculation settings to use as input for gpaw."""
+        return self._get_calculation_settings()
+
+    def _get_calculation_settings(self):
+        """Load all calculation settings: user-input and auto-determined."""
+        calc_sett = {}
+        if self.calculation_presets is not None:
+            calc_sett.update(GPAW_PRESETS[self.calculation_presets])
+        if self.custom_sett_from_file is not None:
+            calc_sett.update(self.custom_sett_from_file)
+        if self.custom_sett_dict is not None:
+            calc_sett.update(self.custom_sett_dict)
+        return calc_sett
+
+    def _get_default_input_filename(self):
+        if self.calculation_presets is None:
+            return "gpaw_in.py"
+        return "{}_in.py".format(self.calculation_presets)
+
+    @property
+    def calculator_object_as_str(self):
+        top = "GPAW("
+
+        calc_sett = self.calculation_settings
+
+        params = []
+        for p in GPAW_TAGS["parameters"]:
+            if p in calc_sett:
+                if type(calc_sett[p]) is str:
+                    params.append(f"    {p}='{str(calc_sett[p])}'")
+                else:
+                    params.append(f"    {p}={str(calc_sett[p])}")
+        params.append(f'    txt="output.txt"')
+
+        return "\n".join([top, ",\n".join(params), ")"])
+
+    @property
+    def gpaw_input_as_str(self):
+        header = f"""import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = {self.from_scratch}
+slab = None
+
+if os.path.isfile("{self.gpaw_restart_file}"):
+    try:
+        slab, _ = restart("{self.gpaw_restart_file}", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("{self.struct_filename}")
+else:
+    slab = read("{self.struct_filename}")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = {self.calculator_object_as_str}
+"""
+
+        calc_sett = self.calculation_settings
+        try:
+            calc_type = calc_sett["calculation"]
+        except KeyError:
+            # if no input settings found will return calc object
+            # without any settings defined
+            calc_type = None
+        if calc_type == "relax":
+            define_relax_fn = f"""def relax(atoms, fmax=0.05, step=0.04):
+    atoms.calc.attach(atoms.calc.write, 5, "{self.gpaw_restart_file}")
+
+    def _check_file_exists(filename):
+        #Check if file exists and is not empty
+        if os.path.isfile(filename):
+            return os.path.getsize(filename) > 0
+        else:
+            return False
+
+    # check if it is a restart
+    barrier()
+    if _check_file_exists("output.traj"):
+        latest = read("output.traj", index=":")
+        # check if already restarted previously and extend history if needed
+        if not (_check_file_exists("history.traj")):
+            barrier()
+            write("history.traj", latest)
+        else:
+            hist = read("history.traj", index=":")
+            hist.extend(latest)
+            write("history.traj", hist)
+
+    dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step)
+    # if history exists, read in hessian
+    if _check_file_exists("history.traj"):
+        dyn.replay_trajectory("history.traj")
+    # optimize
+    dyn.run(fmax=fmax)
+"""
+            return "\n".join([header, define_relax_fn, "relax(slab)",])
+
+        elif calc_type == "bulk_opt":
+            define_bulk_opt_fn = """def optimize_bulk(atoms, step=0.05):
+    cell = atoms.get_cell()
+    name = atoms.get_chemical_formula(mode='hill')
+    vol=atoms.get_volume()
+    volumes =[]
+    energies=[]
+    for x in np.linspace(1-2*step,1+2*step,5):
+        atoms.set_cell(cell*x, scale_atoms=True)
+        atoms.calc.set(txt=name+'_'+str(x)+'.txt')
+        energies.append(atoms.get_potential_energy())
+        volumes.append(atoms.get_volume())
+    eos = EquationOfState(volumes, energies)
+    v0,e0,B= eos.fit()
+    atoms.set_cell(np.cbrt(v0/vol)*cell,scale_atoms=True)
+    x0=np.cbrt(v0/vol)
+    atoms.calc.set(txt='output.txt')
+    dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log')
+    dyn.run(fmax=0.05)
+    atoms.calc.write('output.gpw')
+"""
+            return "\n".join(
+                [header, define_bulk_opt_fn, "optimize_bulk(slab)",]
+            )
+        elif calc_type == "bulk_opt_hcp":
+            define_bulk_opt_hcp_fn = """def optimize_bulk_hcp(atoms, step=0.05, nstep=5):
+    # Get initial cell
+    cell = atoms.get_cell()
+    a_in = cell[0]
+    b_in = cell[1]
+    c_in = cell[2]
+    name = atoms.get_chemical_formula(mode='hill')
+    vol=atoms.get_volume()
+
+    # Optimize in ab first
+    volumes_ab =[]
+    energies_ab=[]
+    scale_factors = np.linspace(1-2*step,1+2*step,nstep)
+    for x in scale_factors:
+        atoms.set_cell([a_in*x,b_in*x,c_in], scale_atoms=True)
+        atoms.calc.set(txt=name+'_'+'ab'+'_'+str(x)+'.txt')
+        energies_ab.append(atoms.get_potential_energy())
+        volumes_ab.append(atoms.get_volume())
+
+    # Fit in ab
+    eos = EquationOfState(volumes_ab, energies_ab)
+    v0,e0,B= eos.fit()
+    a_fit = a_in*np.sqrt(v0/vol)
+    b_fit = b_in*np.sqrt(v0/vol)
+
+    # Generate new cell with fit ab parameters
+    atoms.set_cell([a_fit,b_fit,c_in], scale_atoms=True)
+    vol_ab = atoms.get_volume()
+
+    # Optimize c
+    energies_c = []
+    volumes_c = []
+    for x in scale_factors:
+        atoms.set_cell([a_fit,b_fit,c_in*x], scale_atoms=True)
+        atoms.calc.set(txt=name+'_'+'c'+'_'+str(x)+'.txt')
+        energies_c.append(atoms.get_potential_energy())
+        volumes_c.append(atoms.get_volume())
+
+    # Fit in c
+    eos_c = EquationOfState(volumes_c,energies_c,eos='birchmurnaghan')
+    v0, e0, B = eos_c.fit()
+    c_scaling = v0/vol_ab
+    c_fit = c_in*c_scaling
+
+    # Update cell with optimized parameters and relax atoms
+    atoms.set_cell([a_fit,b_fit,c_fit], scale_atoms=True)
+    atoms.calc.set(txt='output.txt')
+    dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log')
+    dyn.run(fmax=0.05)
+    atoms.calc.write("output.gpw")
+"""
+            return "\n".join(
+                [header, define_bulk_opt_hcp_fn, "optimize_bulk_hcp(slab)",]
+            )
+
+        elif calc_type == "surface_adsorbate_vibrations":
+            define_surf_ads_vib = """def calculate_vibrations(atoms):
+    indices_to_perturb = np.where(atoms.get_tags() <= 0)[0].tolist()
+    barrier()
+    vib = Vibrations(atoms, indices = indices_to_perturb, name = "vib.log")
+    vib.clean(empty_files=True)
+    vib.run()
+    vib.summary(log="vib.summary")
+"""
+            return "\n".join(
+                [header, define_surf_ads_vib, "calculate_vibrations(slab)"]
+            )
+
+        # if not a relax or bulk_opt calculation,
+        # defaults to getting total energy of static structure
+        return "\n".join([header, "slab.get_total_energy()",])
+
+    def write_gpaw_input(self, write_location=None, filename=None):
+        if write_location is None:
+            msg = "Location to write files not specified"
+            raise GPAWInputGeneratorError(msg)
+        if filename is None:
+            msg = "Name of the input file to write into not specified"
+            raise GPAWInputGeneratorError(msg)
+        with open(os.path.join(write_location, filename), "w") as fw:
+            fw.write(self.gpaw_input_as_str)
+
+    def write_input_files(self):
+        self.write_gpaw_input(
+            write_location=self.write_location, filename=self.gpaw_input_file,
+        )
diff --git a/src/dftinputgen/gpaw/settings/__init__.py b/src/dftinputgen/gpaw/settings/__init__.py
new file mode 100644
index 0000000..bbbdc13
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/__init__.py
@@ -0,0 +1,11 @@
+import json
+import pkg_resources
+
+__all__ = ["GPAW_TAGS"]
+
+tags_file = pkg_resources.resource_filename(
+    "dftinputgen.gpaw.settings", "tags_and_groups.json"
+)
+
+with open(tags_file, "r") as fr:
+    GPAW_TAGS = json.load(fr)
diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/__init__.py b/src/dftinputgen/gpaw/settings/calculation_presets/__init__.py
new file mode 100644
index 0000000..3191b35
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/calculation_presets/__init__.py
@@ -0,0 +1,23 @@
+import os
+import json
+import pkg_resources
+
+
+__all__ = ["GPAW_PRESETS"]
+
+
+GPAW_PRESETS = {}
+
+
+preset_listdir = pkg_resources.resource_listdir(
+    "dftinputgen.gpaw.settings", "calculation_presets"
+)
+for filename in preset_listdir:
+    root, ext = os.path.splitext(filename)
+    if not ext == ".json":
+        continue
+    resource = pkg_resources.resource_filename(
+        "dftinputgen.gpaw.settings.calculation_presets", filename
+    )
+    with open(resource, "r") as fr:
+        GPAW_PRESETS[root] = json.load(fr)
diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt.json b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt.json
new file mode 100644
index 0000000..c9344ed
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt.json
@@ -0,0 +1,12 @@
+{
+    "kpts": {
+        "size": [12,12,12]
+    },
+    "xc": "BEEF-vdW",
+    "h": 0.16,
+    "occupations": {
+        "name": "fermi-dirac",
+        "width": 0.05
+    },
+    "calculation": "bulk_opt"
+}
diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt_hcp.json b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt_hcp.json
new file mode 100644
index 0000000..039f8de
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt_hcp.json
@@ -0,0 +1,12 @@
+{
+    "kpts": {
+        "size": [12,12,6]
+    },
+    "xc": "BEEF-vdW",
+    "h": 0.16,
+    "occupations": {
+        "name": "fermi-dirac",
+        "width": 0.05
+    },
+    "calculation": "bulk_opt_hcp"
+}
diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/molecule.json b/src/dftinputgen/gpaw/settings/calculation_presets/molecule.json
new file mode 100644
index 0000000..aff579f
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/calculation_presets/molecule.json
@@ -0,0 +1,9 @@
+{
+    "xc": "BEEF-vdW",
+    "h": 0.16,
+    "occupations": {
+        "name": "fermi-dirac",
+        "width": 0.05
+    },
+    "calculation": "relax"
+}
diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/surface_adsorbate_vibrations.json b/src/dftinputgen/gpaw/settings/calculation_presets/surface_adsorbate_vibrations.json
new file mode 100644
index 0000000..d047cbb
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/calculation_presets/surface_adsorbate_vibrations.json
@@ -0,0 +1,19 @@
+{
+    "kpts": {
+        "size": [
+            4,
+            4,
+            1
+        ]
+    },
+    "xc": "BEEF-vdW",
+    "h": 0.16,
+    "occupations": {
+        "name": "fermi-dirac",
+        "width": 0.05
+    },
+    "poissonsolver": {
+        "dipolelayer": "xy"
+    },
+    "calculation": "surface_adsorbate_vibrations"
+}
\ No newline at end of file
diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/surface_relax.json b/src/dftinputgen/gpaw/settings/calculation_presets/surface_relax.json
new file mode 100644
index 0000000..4194010
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/calculation_presets/surface_relax.json
@@ -0,0 +1,15 @@
+{
+    "kpts": {
+        "size": [4,4,1]
+    },
+    "xc": "BEEF-vdW",
+    "h": 0.16,
+    "occupations": {
+        "name": "fermi-dirac",
+        "width": 0.05
+    },
+    "poissonsolver": {
+        "dipolelayer": "xy"
+    },
+    "calculation": "relax"
+}
diff --git a/src/dftinputgen/gpaw/settings/tags_and_groups.json b/src/dftinputgen/gpaw/settings/tags_and_groups.json
new file mode 100644
index 0000000..e6105cb
--- /dev/null
+++ b/src/dftinputgen/gpaw/settings/tags_and_groups.json
@@ -0,0 +1,28 @@
+{
+    "parameters" : [
+        "basis",
+        "charge",
+        "communicator",
+        "convergence",
+        "eigensolver",
+        "external",
+        "fixdensity",
+        "gpts",
+        "h",
+        "hund",
+        "idiotproof",
+        "kpts",
+        "maxiter",
+        "mode",
+        "nbands",
+        "occupations",
+        "parallel",
+        "poissonsolver",
+        "random",
+        "setups",
+        "spinpol",
+        "symmetry",
+        "txt",
+        "xc"
+    ]
+}
diff --git a/tests/demo/files/gpaw_li_relax_custom.py b/tests/demo/files/gpaw_li_relax_custom.py
new file mode 100644
index 0000000..0784078
--- /dev/null
+++ b/tests/demo/files/gpaw_li_relax_custom.py
@@ -0,0 +1,70 @@
+# fmt: off
+import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = True
+slab = None
+
+if os.path.isfile("Li.gpw"):
+    try:
+        slab, _ = restart("Li.gpw", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("Li110.traj")
+else:
+    slab = read("Li110.traj")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = GPAW(
+    h=0.18,
+    kpts={'size': [6, 6, 1]},
+    occupations={'name': 'fermi-dirac', 'width': 0.05},
+    poissonsolver={'dipolelayer': 'xy'},
+    xc='PBE',
+    txt="output.txt"
+)
+
+def relax(atoms, fmax=0.05, step=0.04):
+    atoms.calc.attach(atoms.calc.write, 5, "Li.gpw")
+
+    def _check_file_exists(filename):
+        #Check if file exists and is not empty
+        if os.path.isfile(filename):
+            return os.path.getsize(filename) > 0
+        else:
+            return False
+
+    # check if it is a restart
+    barrier()
+    if _check_file_exists("output.traj"):
+        latest = read("output.traj", index=":")
+        # check if already restarted previously and extend history if needed
+        if not (_check_file_exists("history.traj")):
+            barrier()
+            write("history.traj", latest)
+        else:
+            hist = read("history.traj", index=":")
+            hist.extend(latest)
+            write("history.traj", hist)
+
+    dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step)
+    # if history exists, read in hessian
+    if _check_file_exists("history.traj"):
+        dyn.replay_trajectory("history.traj")
+    # optimize
+    dyn.run(fmax=fmax)
+
+relax(slab)
diff --git a/tests/demo/files/li110.traj b/tests/demo/files/li110.traj
new file mode 100644
index 0000000..8eb08e8
Binary files /dev/null and b/tests/demo/files/li110.traj differ
diff --git a/tests/demo/files/test_sett_gpaw.json b/tests/demo/files/test_sett_gpaw.json
new file mode 100644
index 0000000..d11dfa1
--- /dev/null
+++ b/tests/demo/files/test_sett_gpaw.json
@@ -0,0 +1,6 @@
+{
+    "h": 0.18,
+    "kpts": {"size": [6,6,1]},
+    "occupations": {"name": "fermi-dirac", "width": 0.05},
+    "poissonsolver": {"dipolelayer": "xy"}
+}
diff --git a/tests/demo/test_gpaw_demo.py b/tests/demo/test_gpaw_demo.py
new file mode 100644
index 0000000..303919a
--- /dev/null
+++ b/tests/demo/test_gpaw_demo.py
@@ -0,0 +1,107 @@
+import os
+import json
+import pytest
+import argparse
+
+from dftinputgen.utils import read_crystal_structure
+from dftinputgen.demo.gpaw import _get_default_parser
+from dftinputgen.demo.gpaw import build_gpaw_parser
+from dftinputgen.demo.gpaw import run_demo
+
+
+files_dir = os.path.join(os.path.dirname(__file__), "files")
+li_file = os.path.join(files_dir, "li110.traj")
+li_struct = read_crystal_structure(li_file)
+sett_file = os.path.join(files_dir, "test_sett_gpaw.json")
+li_gpaw_script = os.path.join(files_dir, "gpaw_li_relax_custom.py")
+
+
+def test_get_parser_required_missing(capsys):
+    parser = _get_default_parser()
+    build_gpaw_parser(parser)
+
+    # input structure (required) missing: error
+    with pytest.raises(SystemExit):
+        parser.parse_args()
+    stderr = capsys.readouterr().err
+    assert "required" in stderr
+
+
+def test_get_parser_default_args():
+    parser = _get_default_parser()
+    build_gpaw_parser(parser)
+    args = parser.parse_args(["-i", li_file])
+    assert args.crystal_structure == li_struct
+    assert args.calculation_presets is None
+    assert args.custom_settings_file is None
+    assert args.custom_settings_dict == {}
+    assert not args.overwrite_files
+
+
+def test_get_parser_input_args(capsys):
+    parser = _get_default_parser()
+    build_gpaw_parser(parser)
+
+    # invalid choice for `calculation_presets`
+    with pytest.raises(SystemExit):
+        parser.parse_args(["-i", li_file, "-pre", "unsupported"])
+    stderr = capsys.readouterr().err
+    assert "invalid choice" in stderr
+
+    # all ok
+    args = parser.parse_args(
+        [
+            "-i",
+            li_file,
+            "-o",
+            "gpaw.in",
+            "-loc",
+            "/path/to/location",
+            "-dict",
+            '{"key_1": "val", "key_2": 0}',
+            "-file",
+            "some_file",
+            "-pre",
+            "bulk_opt_hcp",
+        ]
+    )
+    assert args.calculation_presets == "bulk_opt_hcp"
+    assert args.custom_settings_file == "some_file"
+    assert args.custom_settings_dict == {"key_1": "val", "key_2": 0}
+    assert args.write_location == "/path/to/location"
+    assert args.gpaw_input_file == "gpaw.in"
+
+
+def test_run_demo():
+    import tempfile
+
+    _tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=True)
+    filename = _tmp_file.name
+    write_location = os.path.dirname(filename)
+    args = [
+        "-i",
+        li_file,
+        "-pre",
+        "surface_relax",
+        "-file",
+        sett_file,
+        "-dict",
+        '{"xc": "PBE"}',
+        "-loc",
+        write_location,
+        "-o",
+        os.path.basename(filename),
+        "-is",
+        "Li110.traj",
+        "-r",
+        "Li.gpw",
+        "-fs",
+        "True",
+    ]
+    run_demo(args)
+
+    with open(filename, "r") as fr:
+        test = fr.read()
+    with open(li_gpaw_script, "r") as fr:
+        reference = "\n".join(fr.read().splitlines()[1:])
+    assert test == reference
diff --git a/tests/gpaw/files/TEST_bulk_opt.py b/tests/gpaw/files/TEST_bulk_opt.py
new file mode 100644
index 0000000..660eae5
--- /dev/null
+++ b/tests/gpaw/files/TEST_bulk_opt.py
@@ -0,0 +1,59 @@
+# fmt: off
+import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = False
+slab = None
+
+if os.path.isfile("output.gpw"):
+    try:
+        slab, _ = restart("output.gpw", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("input.traj")
+else:
+    slab = read("input.traj")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = GPAW(
+    h=0.16,
+    kpts={'size': [12, 12, 12]},
+    occupations={'name': 'fermi-dirac', 'width': 0.05},
+    xc='BEEF-vdW',
+    txt="output.txt"
+)
+
+def optimize_bulk(atoms, step=0.05):
+    cell = atoms.get_cell()
+    name = atoms.get_chemical_formula(mode='hill')
+    vol=atoms.get_volume()
+    volumes =[]
+    energies=[]
+    for x in np.linspace(1-2*step,1+2*step,5):
+        atoms.set_cell(cell*x, scale_atoms=True)
+        atoms.calc.set(txt=name+'_'+str(x)+'.txt')
+        energies.append(atoms.get_potential_energy())
+        volumes.append(atoms.get_volume())
+    eos = EquationOfState(volumes, energies)
+    v0,e0,B= eos.fit()
+    atoms.set_cell(np.cbrt(v0/vol)*cell,scale_atoms=True)
+    x0=np.cbrt(v0/vol)
+    atoms.calc.set(txt='output.txt')
+    dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log')
+    dyn.run(fmax=0.05)
+    atoms.calc.write('output.gpw')
+
+optimize_bulk(slab)
diff --git a/tests/gpaw/files/TEST_bulk_opt_hcp.py b/tests/gpaw/files/TEST_bulk_opt_hcp.py
new file mode 100644
index 0000000..9e9d478
--- /dev/null
+++ b/tests/gpaw/files/TEST_bulk_opt_hcp.py
@@ -0,0 +1,90 @@
+# fmt: off
+import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = False
+slab = None
+
+if os.path.isfile("output.gpw"):
+    try:
+        slab, _ = restart("output.gpw", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("input.traj")
+else:
+    slab = read("input.traj")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = GPAW(
+    h=0.16,
+    kpts={'size': [12, 12, 6]},
+    occupations={'name': 'fermi-dirac', 'width': 0.05},
+    xc='BEEF-vdW',
+    txt="output.txt"
+)
+
+def optimize_bulk_hcp(atoms, step=0.05, nstep=5):
+    # Get initial cell
+    cell = atoms.get_cell()
+    a_in = cell[0]
+    b_in = cell[1]
+    c_in = cell[2]
+    name = atoms.get_chemical_formula(mode='hill')
+    vol=atoms.get_volume()
+
+    # Optimize in ab first
+    volumes_ab =[]
+    energies_ab=[]
+    scale_factors = np.linspace(1-2*step,1+2*step,nstep)
+    for x in scale_factors:
+        atoms.set_cell([a_in*x,b_in*x,c_in], scale_atoms=True)
+        atoms.calc.set(txt=name+'_'+'ab'+'_'+str(x)+'.txt')
+        energies_ab.append(atoms.get_potential_energy())
+        volumes_ab.append(atoms.get_volume())
+
+    # Fit in ab
+    eos = EquationOfState(volumes_ab, energies_ab)
+    v0,e0,B= eos.fit()
+    a_fit = a_in*np.sqrt(v0/vol)
+    b_fit = b_in*np.sqrt(v0/vol)
+
+    # Generate new cell with fit ab parameters
+    atoms.set_cell([a_fit,b_fit,c_in], scale_atoms=True)
+    vol_ab = atoms.get_volume()
+
+    # Optimize c
+    energies_c = []
+    volumes_c = []
+    for x in scale_factors:
+        atoms.set_cell([a_fit,b_fit,c_in*x], scale_atoms=True)
+        atoms.calc.set(txt=name+'_'+'c'+'_'+str(x)+'.txt')
+        energies_c.append(atoms.get_potential_energy())
+        volumes_c.append(atoms.get_volume())
+
+    # Fit in c
+    eos_c = EquationOfState(volumes_c,energies_c,eos='birchmurnaghan')
+    v0, e0, B = eos_c.fit()
+    c_scaling = v0/vol_ab
+    c_fit = c_in*c_scaling
+
+    # Update cell with optimized parameters and relax atoms
+    atoms.set_cell([a_fit,b_fit,c_fit], scale_atoms=True)
+    atoms.calc.set(txt='output.txt')
+    dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log')
+    dyn.run(fmax=0.05)
+    atoms.calc.write("output.gpw")
+
+optimize_bulk_hcp(slab)
diff --git a/tests/gpaw/files/TEST_defaults.py b/tests/gpaw/files/TEST_defaults.py
new file mode 100644
index 0000000..8143e36
--- /dev/null
+++ b/tests/gpaw/files/TEST_defaults.py
@@ -0,0 +1,35 @@
+# fmt: off
+import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = False
+slab = None
+
+if os.path.isfile("output.gpw"):
+    try:
+        slab, _ = restart("output.gpw", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("input.traj")
+else:
+    slab = read("input.traj")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = GPAW(
+    txt="output.txt"
+)
+
+slab.get_total_energy()
diff --git a/tests/gpaw/files/TEST_relax.py b/tests/gpaw/files/TEST_relax.py
new file mode 100644
index 0000000..f175e3e
--- /dev/null
+++ b/tests/gpaw/files/TEST_relax.py
@@ -0,0 +1,70 @@
+# fmt: off
+import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = False
+slab = None
+
+if os.path.isfile("output.gpw"):
+    try:
+        slab, _ = restart("output.gpw", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("input.traj")
+else:
+    slab = read("input.traj")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = GPAW(
+    h=0.16,
+    kpts={'size': [4, 4, 1]},
+    occupations={'name': 'fermi-dirac', 'width': 0.05},
+    poissonsolver={'dipolelayer': 'xy'},
+    xc='BEEF-vdW',
+    txt="output.txt"
+)
+
+def relax(atoms, fmax=0.05, step=0.04):
+    atoms.calc.attach(atoms.calc.write, 5, "output.gpw")
+
+    def _check_file_exists(filename):
+        #Check if file exists and is not empty
+        if os.path.isfile(filename):
+            return os.path.getsize(filename) > 0
+        else:
+            return False
+
+    # check if it is a restart
+    barrier()
+    if _check_file_exists("output.traj"):
+        latest = read("output.traj", index=":")
+        # check if already restarted previously and extend history if needed
+        if not (_check_file_exists("history.traj")):
+            barrier()
+            write("history.traj", latest)
+        else:
+            hist = read("history.traj", index=":")
+            hist.extend(latest)
+            write("history.traj", hist)
+
+    dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step)
+    # if history exists, read in hessian
+    if _check_file_exists("history.traj"):
+        dyn.replay_trajectory("history.traj")
+    # optimize
+    dyn.run(fmax=fmax)
+
+relax(slab)
diff --git a/tests/gpaw/files/TEST_relax_custom.py b/tests/gpaw/files/TEST_relax_custom.py
new file mode 100644
index 0000000..61bf54a
--- /dev/null
+++ b/tests/gpaw/files/TEST_relax_custom.py
@@ -0,0 +1,70 @@
+# fmt: off
+import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = True
+slab = None
+
+if os.path.isfile("C3N4.gpw"):
+    try:
+        slab, _ = restart("C3N4.gpw", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("C3N4.traj")
+else:
+    slab = read("C3N4.traj")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = GPAW(
+    h=0.18,
+    kpts={'size': [6, 6, 1]},
+    occupations={'name': 'fermi-dirac', 'width': 0.05},
+    poissonsolver={'dipolelayer': 'xy'},
+    xc='PBE',
+    txt="output.txt"
+)
+
+def relax(atoms, fmax=0.05, step=0.04):
+    atoms.calc.attach(atoms.calc.write, 5, "C3N4.gpw")
+
+    def _check_file_exists(filename):
+        #Check if file exists and is not empty
+        if os.path.isfile(filename):
+            return os.path.getsize(filename) > 0
+        else:
+            return False
+
+    # check if it is a restart
+    barrier()
+    if _check_file_exists("output.traj"):
+        latest = read("output.traj", index=":")
+        # check if already restarted previously and extend history if needed
+        if not (_check_file_exists("history.traj")):
+            barrier()
+            write("history.traj", latest)
+        else:
+            hist = read("history.traj", index=":")
+            hist.extend(latest)
+            write("history.traj", hist)
+
+    dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step)
+    # if history exists, read in hessian
+    if _check_file_exists("history.traj"):
+        dyn.replay_trajectory("history.traj")
+    # optimize
+    dyn.run(fmax=fmax)
+
+relax(slab)
diff --git a/tests/gpaw/files/TEST_surface_adsorbate_vibrations.py b/tests/gpaw/files/TEST_surface_adsorbate_vibrations.py
new file mode 100644
index 0000000..f0f7724
--- /dev/null
+++ b/tests/gpaw/files/TEST_surface_adsorbate_vibrations.py
@@ -0,0 +1,48 @@
+# fmt: off
+import os
+import numpy as np
+
+from gpaw import GPAW
+from gpaw import restart
+from ase.io import read
+from ase.io import write
+from ase.optimize import BFGS
+from ase.parallel import barrier
+from ase.eos import EquationOfState
+from ase.vibrations import Vibrations
+
+from_scratch = False
+slab = None
+
+if os.path.isfile("output.gpw"):
+    try:
+        slab, _ = restart("output.gpw", txt="output.txt")
+    except:
+        from_scratch = True
+        if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0:
+            slab = read("output.traj")
+        else:
+            slab = read("input.traj")
+else:
+    slab = read("input.traj")
+    from_scratch = True
+
+if from_scratch:
+    slab.calc = GPAW(
+    h=0.16,
+    kpts={'size': [4, 4, 1]},
+    occupations={'name': 'fermi-dirac', 'width': 0.05},
+    poissonsolver={'dipolelayer': 'xy'},
+    xc='BEEF-vdW',
+    txt="output.txt"
+)
+
+def calculate_vibrations(atoms):
+    indices_to_perturb = np.where(atoms.get_tags() <= 0)[0].tolist()
+    barrier()
+    vib = Vibrations(atoms, indices = indices_to_perturb, name = "vib.log")
+    vib.clean(empty_files=True)
+    vib.run()
+    vib.summary(log="vib.summary")
+
+calculate_vibrations(slab)
diff --git a/tests/gpaw/files/cu_bulk.traj b/tests/gpaw/files/cu_bulk.traj
new file mode 100644
index 0000000..c6c344c
Binary files /dev/null and b/tests/gpaw/files/cu_bulk.traj differ
diff --git a/tests/gpaw/test_gpaw.py b/tests/gpaw/test_gpaw.py
new file mode 100644
index 0000000..674c3fc
--- /dev/null
+++ b/tests/gpaw/test_gpaw.py
@@ -0,0 +1,186 @@
+"""Unit tests for the `GPAWInputGenerator` class."""
+
+import os
+import pytest
+
+from ase import io as ase_io
+
+from dftinputgen.gpaw.gpaw import GPAWInputGenerator, GPAWInputGeneratorError
+
+test_data_dir = os.path.join(os.path.dirname(__file__), "files")
+cu_bulk_struct = ase_io.read(os.path.join(test_data_dir, "cu_bulk.traj"))
+with open(os.path.join(test_data_dir, "TEST_bulk_opt.py"), "r") as fr:
+    bulk_opt_in = fr.read()
+with open(os.path.join(test_data_dir, "TEST_bulk_opt_hcp.py"), "r") as fr:
+    bulk_opt_hcp_in = fr.read()
+with open(os.path.join(test_data_dir, "TEST_relax.py"), "r") as fr:
+    relax_in = fr.read()
+with open(os.path.join(test_data_dir, "TEST_relax_custom.py"), "r") as fr:
+    relax_custom_in = fr.read()
+with open(os.path.join(test_data_dir, "TEST_defaults.py"), "r") as fr:
+    defaults_in = fr.read()
+with open(
+    os.path.join(test_data_dir, "TEST_surface_adsorbate_vibrations.py"), "r"
+) as fr:
+    vib_in = fr.read()
+
+
+def test_dft_package_name():
+    gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct)
+    assert gig.dft_package == "GPAW"
+
+
+def test_gpaw_input_file():
+    # default "[calculation_presets]_in.py"
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt"
+    )
+    assert gig.gpaw_input_file == "bulk_opt_in.py"
+    # otherwise use any user-input file name
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt"
+    )
+    gig.gpaw_input_file = "test.py"
+    assert gig.gpaw_input_file == "test.py"
+
+
+def test_bulk_opt_calculation_presets_settings():
+    # Tests bulk_opt settings (fcc/bcc)
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt"
+    )
+    cs = gig.calculation_settings
+    assert cs["calculation"] == "bulk_opt"
+    assert cs["kpts"]["size"] == [12, 12, 12]
+    assert cs["xc"] == "BEEF-vdW"
+    assert cs["h"] == 0.16
+    assert cs["occupations"]["name"] == "fermi-dirac"
+    assert cs["occupations"]["width"] == 0.05
+
+
+def test_bulk_opt_hcp_calculation_presets_settings():
+    # Tests bulk_opt_hcp settings (hcp)
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt_hcp"
+    )
+    cs = gig.calculation_settings
+    assert cs["calculation"] == "bulk_opt_hcp"
+    assert cs["kpts"]["size"] == [12, 12, 6]
+    assert cs["xc"] == "BEEF-vdW"
+    assert cs["h"] == 0.16
+    assert cs["occupations"]["name"] == "fermi-dirac"
+    assert cs["occupations"]["width"] == 0.05
+
+
+def test_molecule_calculation_presets_settings():
+    # Tests molecule settings
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="molecule"
+    )
+    cs = gig.calculation_settings
+    assert cs["xc"] == "BEEF-vdW"
+    assert cs["h"] == 0.16
+    assert cs["occupations"]["name"] == "fermi-dirac"
+    assert cs["occupations"]["width"] == 0.05
+    assert cs["calculation"] == "relax"
+
+
+def test_surface_relax_calculation_presets_settings():
+    # Tests relax presets
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="surface_relax"
+    )
+    cs = gig.calculation_settings
+    assert cs["xc"] == "BEEF-vdW"
+    assert cs["h"] == 0.16
+    assert cs["occupations"]["name"] == "fermi-dirac"
+    assert cs["occupations"]["width"] == 0.05
+    assert cs["calculation"] == "relax"
+    assert cs["poissonsolver"]["dipolelayer"] == "xy"
+
+
+def test_calculator_object_as_str():
+    # Tests generation of calculator object as string
+    gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct)
+    assert gig.calculator_object_as_str == 'GPAW(\n    txt="output.txt"\n)'
+    gig.calculation_presets = "bulk_opt"
+    calc_obj = "\n".join(["GPAW(", "\n".join(bulk_opt_in.splitlines()[31:37])])
+    assert gig.calculator_object_as_str == calc_obj
+
+
+def test_gpaw_input_as_str():
+    # Test defaults
+    gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct)
+    assert gig.gpaw_input_as_str == "\n".join(defaults_in.splitlines()[1:])
+    # Test generation of full bulk opt script
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt"
+    )
+    assert gig.gpaw_input_as_str == "\n".join(bulk_opt_in.splitlines()[1:])
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt_hcp"
+    )
+    assert gig.gpaw_input_as_str == "\n".join(bulk_opt_hcp_in.splitlines()[1:])
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct, calculation_presets="surface_relax"
+    )
+    assert gig.gpaw_input_as_str == "\n".join(relax_in.splitlines()[1:])
+    gig = GPAWInputGenerator(
+        crystal_structure=cu_bulk_struct,
+        calculation_presets="surface_adsorbate_vibrations",
+    )
+    assert gig.gpaw_input_as_str == "\n".join(vib_in.splitlines()[1:])
+
+
+def test_write_gpaw_input():
+    gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct)
+    gig.calculation_presets = "surface_relax"
+    # no `write_location` input: error
+    with pytest.raises(GPAWInputGeneratorError, match="Location to write"):
+        gig.write_gpaw_input()
+    # no input filename: error
+    with pytest.raises(GPAWInputGeneratorError, match="file to write"):
+        gig.write_gpaw_input(write_location="/path/to/write_location")
+
+    import tempfile
+
+    _tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=True)
+    filename = _tmp_file.name
+    write_location = os.path.dirname(filename)
+    gig.gpaw_restart_file = "C3N4.gpw"
+    gig.struct_filename = "C3N4.traj"
+    gig.from_scratch = True
+    gig.custom_sett_dict = {
+        "kpts": {"size": [6, 6, 1]},
+        "xc": "PBE",
+        "h": 0.18,
+    }
+    gig.write_gpaw_input(write_location=write_location, filename=filename)
+    with open(filename, "r") as fr:
+        written_file = fr.read()
+        assert written_file == "\n".join(relax_custom_in.splitlines()[1:])
+
+
+def test_write_input_files():
+    import tempfile
+
+    _tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=True)
+    filename = _tmp_file.name
+    write_location = os.path.dirname(filename)
+    gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct)
+    gig.calculation_presets = "surface_relax"
+    gig.struct_filename = "C3N4.traj"
+    gig.gpaw_restart_file = "C3N4.gpw"
+    gig.from_scratch = True
+    gig.custom_sett_dict = {
+        "kpts": {"size": [6, 6, 1]},
+        "xc": "PBE",
+        "h": 0.18,
+    }
+    gig.write_location = write_location
+    gig.gpaw_input_file = filename
+    gig.write_input_files()
+    with open(filename, "r") as fr:
+        written_file = fr.read()
+        check = "\n".join(relax_custom_in.splitlines()[1:])
+        assert written_file == check
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 727cfeb..3822461 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -27,7 +27,7 @@ def test_driver(capsys):
 
     # subparser: invalid dft package choice error
     with pytest.raises(SystemExit):
-        driver(["gpaw"])
+        driver(["vasp"])
     assert "invalid choice" in capsys.readouterr().err
 
     # pw.x package: missing required arguments error
@@ -55,3 +55,25 @@ def test_driver(capsys):
             filename,
         ]
     )
+
+    # gpaw package: missing required arguments error
+    # only tests that the arguments have been passed on to the pw.x subparser
+    with pytest.raises(SystemExit):
+        driver(["gpaw"])
+    assert "required" in capsys.readouterr().err
+
+    # gpaw package: minimal working example
+
+    driver(
+        [
+            "gpaw",
+            "-i",
+            test_struct,
+            "-pre",
+            "surface_relax",
+            "-loc",
+            write_location,
+            "-o",
+            filename,
+        ]
+    )
diff --git a/tox.ini b/tox.ini
index c473d4a..d39df15 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,8 @@ doctests = True
 # D107: __init__ is self explanatory
 # D301: backslash is used in making docstrings for sphinx to parse
 # D401: Imperative mood requirement basically gets in the way
-ignore = D100,D104,D105,D107,D301,D401
+# W503: Line break before binary operator is now PEP-8 compliant
+ignore = D100,D104,D105,D107,D301,D401,W503
 
 exclude = tests/*