.. _packaging:

Packaging your Nek5000 user source code
=======================================

This document describes how to package a Nek5000 source code in your own
repository to via ``snek5000``'s API. What is described is the bare minimum to
get started.  Assume your files are named ``canonical.usr``, ``canonical.par``
etc. and you wish to package it under a name say ``snek5000_canonical``.

.. note::
   The package does not have to contain `snek5000` or `canonical`.
   It could be anything. Make sure the ``short_name`` variable (in solver.py_ )
   and entry point key (in setup.cfg_) matches.

First create a directory structure, (preferably in a git / mercurial
repository) like this::

   .
   ├── LICENSE
   ├── README.md
   ├── setup.cfg
   ├── setup.py
   └── src
       └── snek5000_canonical
           ├── Snakefile
           ├── __init__.py
           ├── canonical.usr
           ├── output.py
           ├── solver.py
           └── templates
               └── __init__.py


Start populating with the following boilerplate.

setup.py
--------

.. code-block:: python

   from setuptools import setup

   setup()


.. _setup.cfg:

setup.cfg
---------

.. code-block:: cfg

   [metadata]
   name = snek5000_canonical
   version = 0.0.1
   description = My Nek5000 user files
   long_description = file: README.md
   long_description_content_type = text/markdown
   license = ...
   license_file = LICENSE

   [options]
   package_dir=
       =src
   packages=find:
   install_requires =
       snek5000

   [options.entry_points]
   snek5000.solvers =
       canonical = snek5000_canonical.solver

   [options.packages.find]
   where=src

   [options.package_data]
   snek5000_canonical = templates/*.j2, *.usr, Snakefile, etc/*.yml


.. _solver.py:

src/snek5000_canonical/solver.py
--------------------------------

.. code-block:: python

   from snek5000.info import InfoSolverMake
   from snek5000.solvers.base import SimulNek
   # To use KTH Framework import SimulKTH instead
   # from snek5000.solvers.kth import SimulKTH


   class InfoSolverCanonical(InfoSolverMake):
       """Contain the information on a :class:`snek5000_canonical.solver.Simul`
       instance.

       """

       def _init_root(self):
           super()._init_root()
           self.module_name = "snek5000_canonical.solver"
           self.class_name = "Simul"
           self.short_name = "canonical"

           self.classes.Output.module_name = "snek5000_canonical.output"
           self.classes.Output.class_name = "OutputCanonical"


   class SimulCanonical(SimulNek):
       """A solver which compiles and runs using a Snakefile.

       """
       InfoSolver = InfoSolverCanonical

       @staticmethod
       def _complete_params_with_default(params):
           """Add missing default parameters."""
           params = SimulNek._complete_params_with_default(params)
           # Extend with new default parameters here, for example:

           # params.nek.velocity._set_attrib("advection", True)
           return params

       @classmethod
       def create_default_params(cls):
           """Set default values of parameters as given in reference
           implementation.

           """
           params = super().create_default_params()
           # Re-define default values for parameters here, if necessary
           # following ``canonical.par``, ``canonical.box`` and ``SIZE`` files
           return params


   Simul = SimulCanonical


.. note::

   As you might have guessed all additional files except for Fortran code can
   be generated by snek5000 and need not be packaged. See
   :class:`snek5000.operators.Operators` and
   :class:`snek5000.solvers.kth.SimulKTH` to see how to set the ``params`` instead.

src/snek5000_canonical/output.py
--------------------------------

.. code-block:: python

   from snek5000 import mpi
   from snek5000.output.base import Output as OutputBase
   from snek5000_canonical.templates import box, size, makefile_usr


   class OutputCanonical(OutputBase):

       @property
       def makefile_usr_sources(self):
           """
           Sources for inclusion to makefile_usr.inc
           Dict[directory]  -> list of source files
           """
           return {}

           # For example this is the list of extra files required for the KTH
           # Framework:

           # return {
           #     "toolbox": [
           #         ("frame.f", "FRAMELP"),
           #         ("mntrlog_block.f", "MNTRLOGD"),
           #         ("mntrlog.f", "MNTRLOGD"),
           #         ("mntrtmr_block.f", "MNTRLOGD", "MNTRTMRD"),
           #         ("mntrtmr.f", "MNTRLOGD", "MNTRTMRD", "FRAMELP"),
           #         ("rprm_block.f", "RPRMD"),
           #         ("rprm.f", "RPRMD", "FRAMELP"),
           #         ("io_tools_block.f", "IOTOOLD"),
           #         ("io_tools.f", "IOTOOLD"),
           #         ("chkpoint.f", "CHKPOINTD"),
           #         ("chkpt_mstp.f", "CHKPTMSTPD", "CHKPOINTD"),
           #         ("map2D.f", "MAP2D", "FRAMELP"),
           #         ("stat.f", "STATD", "MAP2D", "FRAMELP"),
           #         ("stat_IO.f", "STATD", "MAP2D", "FRAMELP"),
           #         ("math_tools.f",),
           #     ],
           # }

       def post_init(self):
           super().post_init()

           # Write additional source files to compile the simulation
           if mpi.rank == 0 and self._has_to_save and self.sim.params.NEW_DIR_RESULTS:
               self.write_box(box)
               self.write_size(size)
               self.write_makefile_usr(makefile_usr)


src/snek5000_canonical/templates/__init__.py
--------------------------------------------

Define your own Jinja templates or re-purpose them from :mod:`snek5000.assets`
if you may.


.. code-block:: python

   import jinja2

   env = jinja2.Environment(
       loader=jinja2.PackageLoader("snek5000", "assets"),
       #  loader=jinja2.PackageLoader("snek5000_canonical", "templates"),
       undefined=jinja2.StrictUndefined,
   )

   box = env.get_template("box.j2")
   size = env.get_template("SIZE.j2")
   makefile_usr = env.get_template("makefile_usr.inc.j2")

.. _user_snakefile:

src/snek5000_canonical/Snakefile
--------------------------------


.. code-block:: python

   import snek5000
   from snek5000_canonical.output import OutputCanonical as Output


   # Case name
   CASE = "canonical"
   CONFIG = Output.get_configfile()

   snek5000.ensure_env()

   configfile: CONFIG

   # Necessary to pass configuration to other Snakemake modules
   Output.update_snakemake_config(config, CASE)

   # default rule
   rule all:
       input:
           "nek5000",


   # shorthand for mesh
   rule mesh:
       input:
           f"{CASE}.re2",
           f"{CASE}.ma2",


   # compiler and run rules
   # ======================
   module compiler:
       snakefile:
           snek5000.get_asset("compiler.smk")
       config:
           config


   use rule * from compiler


   # I/O rules
   # =========
   module io:
       snakefile:
           snek5000.get_asset("io.smk")
       config:
           config


   use rule * from io


   # internal rules
   # ==============
   module internal:
       snakefile:
           snek5000.get_asset("internal.smk")
       config:
           config


   use rule * from internal as internal_*
