Contributor how to guides
=========================

Adding class methods to the ``ValueSequence`` and ``MultiPathSequence`` classes
-------------------------------------------------------------------------------

Adding class methods to :py:class:`hpcflow.app.ValueSequence`
#############################################################

``ValueSequence`` exposes class methods that can be used to generate sequences of values (i.e. multiple elements) within a task. Within a YAML workflow template, the requirement to generate a sequence via a class method is written using a double-colon syntax as follows:

.. code-block:: yaml

    tasks:
      - objective: t1
        sequences:
          - path: inputs.p1
            values::from_range:
              start: 0
              stop: 10
              step: 1

In the case above, we are telling |app_name| to use the method :py:meth:`hpcflow.app.ValueSequence.from_range` to generate multiple elements for the task. The inner block containing ``start``, ``stop``, and ``step`` is then passed as a ``dict`` of keyword arguments to that class method.

Follow these steps to add a new sequence-generating class method to :py:class:`hpcflow.app.ValueSequence`:

1. Consider how to name your new method. For consistency with existing methods, please consider a name that is prefixed by ``from...``. For this example, let's consider a method named ``from_my_new_method``.

2. Add a new method that generates a list of values using your new approach, named ``_values_from_my_new_method``` (i.e. your new method name, prefixed by ``_values_``). If your new technique is parametrised by two arguments, ``arg_1`` (an integer), and ``arg_2`` (a list of strings), the signature of this method should look like this (the ``**kwargs`` is optional—consider if it is necessary to include):

   .. code-block:: python
 
       @classmethod
       def _values_from_my_new_method(
           cls,
           arg_1: int,
           arg_2: list[str],
           **kwargs,
       ) -> Self:
           pass # implementation here

3. Add a new method that can be used within the API:

   .. code-block:: python
 
       @classmethod
       def from_my_new_method(
           cls,
           path: str,
           arg_1: int,
           arg_2: list[str],
           nesting_order: float = 0,
           label: str | int | None = None,          
           **kwargs,
       ) -> Self:
           """
           Build a sequence from ...
           """
           args = {"arg_1": arg_1, "arg_2": arg_2, **kwargs}
           values = cls._values_from_my_new_method(**args)
           obj = cls(values=values, path=path, nesting_order=nesting_order, label=label)
           obj._values_method = "from_my_new_method"
           obj._values_method_args = args
           return obj
           
   Note that in addition to ``arg_1`` and ``arg_2``, this method signature must include some positional and keyword arguments: ``path``, ``nesting_order``, and ``label``, which  should be passed directly to the constructor. We use the method added previously (``_values_from_my_new_method``, in this case) to generate the values, and then pass those values  on to the constructor. Note also that after object construction, we must assign two attributes:
 
   - ``_values_method`` which should be the name of this method
   - ``_values_method_args``, which should be a mapping of argument names and values used to parametrise the value-generating method (``_values_from_my_new_method``, in this case).

4. Write some tests. Please include somewhere within ``hpcflow/tests/unit`` at least one test to convince yourself that your new method generates the correct sequence of values:

   .. code-block:: python

    from hpcflow.app import app as hf

    def test_sequence_from_my_new_method():
        seq = hf.ValueSequence.from_my_new_method(path="inputs.p1", arg_1=9, arg_2=['a', 'b'])
        
        # check the expected number of values generated:
        assert len(seq.values) == 2
        
        # check the expected values, if possible:
        assert seq.values == ["val_a", "val_b"]

        # check the correct attributes are set:
        assert seq._values_method == "from_my_new_method"
        assert seq._values_method_args == {"arg_1": 9, "arg_2": ['a', 'b']}


Adding class methods to :py:class:`hpcflow.app.MultiPathSequence`
#################################################################

``MultiPathSequence`` exposes class methods that can be used to generate multiple sequences of values (i.e. multiple elements) within a task, corresponding to multiple paths (i.e. inputs or resources). Within a YAML workflow template, the requirement to generate a multi-path sequence via a class method is written using a double-colon syntax as follows:

.. code-block:: yaml

    tasks:
      - objective: t1
        multi_path_sequences:
          - paths: [inputs.p1, inputs.p2]
            values::from_latin_hypercube:
              num_samples: 5

In the case above, we are telling |app_name| to use the method :py:meth:`hpcflow.app.MultiPathSequence.from_latin_hypercube` to generate five elements for the task, by combining five values for the input ``p1`` with five values for the input ``p2``, where all ten values are generated at the same time via a Latin hypercube sampling. The inner block containing ``num_samples``, is passed as a ``dict`` of keyword arguments to that class method.

The same process as above can be used for adding new class methods to ``MultiPathSequence``, with two exceptions. Firstly, a ``paths`` postitional argument (note: plural) must be specified in the values-generating method as defined in step 2. above. Thus the method should look like this:

.. code-block:: python

    @classmethod
    def _values_from_my_new_method(
        cls,
        paths: Seqeuence[str], # <- note additional `paths` argument
        arg_1: int,
        arg_2: list[str],
        **kwargs,
    ) -> Self:
        pass # implementation here

The reason for including the ``paths`` argument is so we can know, for example, for how many paths the ``MultiPathSequence`` should generate values for.

Secondly, the ``path`` (note: singular) argument in the public-facing method (``from_my_new_method`` in this case) should be replaced by ``paths`` (note: plural), which should have the type annotation: ``Sequence[str]``.