riverine.mixes
==============

.. py:module:: riverine.mixes

.. autoapi-nested-parse::

   A module for handling mixes.



Classes
-------

.. autoapisummary::

   riverine.mixes.Mix


Functions
---------

.. autoapisummary::

   riverine.mixes._format_title
   riverine.mixes.split_mix
   riverine.mixes.master_mix


Module Contents
---------------

.. py:function:: _format_title(raw_title: str, level: int, tablefmt: str | tabulate.TableFormat) -> str

.. py:class:: Mix(*args, **kwargs)

   Bases: :py:obj:`riverine.components.AbstractComponent`


   Class denoting a Mix, a collection of source components mixed to
   some volume or concentration.


   .. py:attribute:: __hash__


   .. py:attribute:: actions
      :type:  riverine.units.Sequence[riverine.actions.AbstractAction]


   .. py:attribute:: name
      :type:  str
      :value: ''


      Name of the component.


   .. py:attribute:: test_tube_name
      :type:  str | None

      A short name, eg, for labelling a test tube.


   .. py:attribute:: fixed_concentration
      :type:  str | riverine.units.DecimalQuantity | None


   .. py:attribute:: reference
      :type:  riverine.references.Reference | None
      :value: None



   .. py:attribute:: min_volume
      :type:  riverine.units.DecimalQuantity


   .. py:attribute:: plate
      :type:  str | None


   .. py:attribute:: well
      :type:  riverine.locations.WellPos | None


   .. py:property:: is_mix
      :type: bool



   .. py:property:: fixed_total_volume
      :type: riverine.units.DecimalQuantity



   .. py:property:: buffer_name
      :type: str



   .. py:method:: __eq__(other: object) -> bool


   .. py:method:: __attrs_post_init__() -> None


   .. py:method:: printed_name(tablefmt: str | tabulate.TableFormat) -> str


   .. py:property:: concentration
      :type: riverine.units.DecimalQuantity


      Effective concentration of the mix.  Calculated in order:

      1. If the mix has a fixed concentration, then that concentration.
      2. If `fixed_concentration` is a string, then the final concentration of
         the component with that name.
      3. If `fixed_concentration` is none, then the final concentration of the first
         mix component.


   .. py:method:: _get_concentration(_cache_key=None) -> riverine.units.DecimalQuantity


   .. py:property:: total_volume
      :type: riverine.units.DecimalQuantity


      Total volume of the mix.  If the mix has a fixed total volume, then that,
      otherwise, the sum of the transfer volumes of each component.


   .. py:method:: _get_total_volume(_cache_key=None) -> riverine.units.DecimalQuantity


   .. py:property:: buffer_volume
      :type: riverine.units.Quantity


      The volume of buffer to be added to the mix, in addition to the components.


   .. py:method:: _get_buffer_volume(_cache_key=None) -> riverine.units.Quantity


   .. py:method:: table(tablefmt: tabulate.TableFormat | str = 'pipe', raise_failed_validation: bool = False, stralign='default', missingval='', showindex='default', disable_numparse=False, colalign=None, _cache_key=None) -> str

      Generate a table describing the mix.

      :param tablefmt: The output format for the table.
      :param validate: Ensure volumes make sense.



   .. py:method:: mixlines(tablefmt: str | tabulate.TableFormat = 'pipe', _cache_key=None) -> list[riverine.printing.MixLine]


   .. py:method:: has_fixed_concentration_action() -> bool


   .. py:method:: has_fixed_total_volume() -> bool


   .. py:method:: validate(tablefmt: str | tabulate.TableFormat | None = None, mixlines: riverine.units.Sequence[riverine.printing.MixLine] | None = None, raise_errors: bool = False, _cache_key=None) -> list[riverine.units.VolumeError]


   .. py:method:: all_components_polars(_cache_key=None) -> polars.DataFrame


   .. py:method:: all_components() -> pandas.DataFrame

      Return a Series of all component names, and their concentrations (as pint nM).



   .. py:method:: _repr_markdown_() -> str


   .. py:method:: _repr_html_() -> str


   .. py:method:: infoline(_cache_key=None) -> str


   .. py:method:: __repr__() -> str


   .. py:method:: __str__() -> str


   .. py:method:: with_experiment(experiment: riverine.experiments.Experiment, *, inplace: bool = True) -> Mix


   .. py:method:: with_reference(reference: riverine.references.Reference, *, inplace: bool = True) -> Mix


   .. py:property:: location
      :type: tuple[str, riverine.locations.WellPos | None]



   .. py:method:: vol_to_tube_names(tablefmt: str | tabulate.TableFormat = 'pipe', validate: bool = True) -> dict[riverine.units.DecimalQuantity, list[str]]

      :return:
           dict mapping a volume `vol` to a list of names of strands in this mix that should be pipetted
           with volume `vol`



   .. py:method:: _tube_map_from_mixline(mixline: riverine.printing.MixLine) -> str


   .. py:method:: tubes_markdown(tablefmt: str | tabulate.TableFormat = 'pipe') -> str

      :param tablefmt: table format (see :meth:`PlateMap.to_table` for description)

      :returns: * a Markdown (or other format according to `tablefmt`)
                * *string indicating which strands in test tubes to pipette, grouped by the volume*
                * *of each*



   .. py:method:: display_instructions(plate_type: riverine.locations.PlateType = PlateType.wells96, raise_failed_validation: bool = False, combine_plate_actions: bool = True, well_marker: None | str | Callable[[str], str] = None, title_level: Literal[1, 2, 3, 4, 5, 6] = 3, warn_unsupported_title_format: bool = True, tablefmt: str | tabulate.TableFormat = 'unsafehtml', include_plate_maps: bool = True) -> None

      Displays in a Jupyter notebook the result of calling :meth:`Mix.instructions()`.

      :param plate_type: 96-well or 384-well plate; default is 96-well.
      :param raise_failed_validation: If validation fails (volumes don't make sense), raise an exception.
      :param combine_plate_actions: If True, then if multiple actions in the Mix take the same volume from the same plate,
                                    they will be combined into a single :class:`PlateMap`.
      :param well_marker: By default the strand's name is put in the relevant plate entry. If `well_marker` is specified
                          and is a string, then that string is put into every well with a strand in the plate map instead.
                          This is useful for printing plate maps that just put,
                          for instance, an `'X'` in the well to pipette (e.g., specify ``well_marker='X'``),
                          e.g., for experimental mixes that use only some strands in the plate.
                          To enable the string to depend on the well position
                          (instead of being the same string in every well), `well_marker` can also be a function
                          that takes as input a string representing the well (such as ``"B3"`` or ``"E11"``),
                          and outputs a string. For example, giving the identity function
                          ``mix.to_table(well_marker=lambda x: x)`` puts the well address itself in the well.
      :param title_level: The "title" is the first line of the returned string, which contains the plate's name
                          and volume to pipette. The `title_level` controls the size, with 1 being the largest size,
                          (header level 1, e.g., # title in Markdown or <h1>title</h1> in HTML).
      :param warn_unsupported_title_format: If True, prints a warning if `tablefmt` is a currently unsupported option for the title.
                                            The currently supported formats for the title are 'github', 'html', 'unsafehtml', 'rst',
                                            'latex', 'latex_raw', 'latex_booktabs', "latex_longtable". If `tablefmt` is another valid
                                            option, then the title will be the Markdown format, i.e., same as for `tablefmt` = 'github'.
      :param tablefmt: By default set to `'github'` to create a Markdown table. For other options see
                       https://github.com/astanin/python-tabulate#readme
      :param include_plate_maps: If True, include plate maps as part of displayed instructions, otherwise only include the
                                 more compact mixing table (which is always displayed regardless of this parameter).

      :returns: * pipetting instructions in the form of strings combining results of :meth:`Mix.table` and
                * :meth:`Mix.plate_maps`



   .. py:method:: generate_picklist(experiment: riverine.experiments.Experiment | None, _cache_key=None) -> Mix.generate_picklist.PickList | None

      :param experiment: experiment to use for generating picklist

      :rtype: picklist for the mix



   .. py:method:: instructions(*, plate_type: riverine.locations.PlateType = PlateType.wells96, raise_failed_validation: bool = False, combine_plate_actions: bool = True, well_marker: None | str | Callable[[str], str] = None, title_level: Literal[1, 2, 3, 4, 5, 6] = 3, warn_unsupported_title_format: bool = True, tablefmt: str | tabulate.TableFormat = 'pipe', include_plate_maps: bool = True) -> str

      Returns string combiniing the string results of calling :meth:`Mix.table` and
      :meth:`Mix.plate_maps` (then calling :meth:`PlateMap.to_table` on each :class:`PlateMap`).

      :param plate_type: 96-well or 384-well plate; default is 96-well.

      raise_failed_validation:
          If validation fails (volumes don't make sense), raise an exception.

      combine_plate_actions:
          If True, then if multiple actions in the Mix take the same volume from the same plate,
          they will be combined into a single :class:`PlateMap`.

      well_marker:
          By default the strand's name is put in the relevant plate entry. If `well_marker` is specified
          and is a string, then that string is put into every well with a strand in the plate map instead.
          This is useful for printing plate maps that just put,
          for instance, an `'X'` in the well to pipette (e.g., specify ``well_marker='X'``),
          e.g., for experimental mixes that use only some strands in the plate.
          To enable the string to depend on the well position
          (instead of being the same string in every well), `well_marker` can also be a function
          that takes as input a string representing the well (such as ``"B3"`` or ``"E11"``),
          and outputs a string. For example, giving the identity function
          ``mix.to_table(well_marker=lambda x: x)`` puts the well address itself in the well.

      title_level:
          The "title" is the first line of the returned string, which contains the plate's name
          and volume to pipette. The `title_level` controls the size, with 1 being the largest size,
          (header level 1, e.g., # title in Markdown or <h1>title</h1> in HTML).

      warn_unsupported_title_format:
          If True, prints a warning if `tablefmt` is a currently unsupported option for the title.
          The currently supported formats for the title are 'github', 'html', 'unsafehtml', 'rst',
          'latex', 'latex_raw', 'latex_booktabs', "latex_longtable". If `tablefmt` is another valid
          option, then the title will be the Markdown format, i.e., same as for `tablefmt` = 'github'.

      tablefmt:
          By default set to `'github'` to create a Markdown table. For other options see
          https://github.com/astanin/python-tabulate#readme

      include_plate_maps:
          If True, include plate maps as part of displayed instructions, otherwise only include the
          more compact mixing table (which is always displayed regardless of this parameter).

      :returns: * pipetting instructions in the form of strings combining results of :meth:`Mix.table` and
                * :meth:`Mix.plate_maps`



   .. py:method:: plate_maps(plate_type: riverine.locations.PlateType = PlateType.wells96, validate: bool = True, combine_plate_actions: bool = True) -> list[PlateMap]

      Similar to :meth:`table`, but indicates only the strands to mix from each plate,
      in the form of a :class:`PlateMap`.

      NOTE: this ignores any strands in the :class:`Mix` that are in test tubes. To get a list of strand
      names in test tubes, call :meth:`Mix.vol_to_tube_names` or :meth:`Mix.tubes_markdown`.

      By calling :meth:`PlateMap.to_markdown` on each plate map,
      one can create a Markdown representation of each plate map, for example,

      .. code-block::

          plate 1, 5 uL each
          |     | 1    | 2      | 3      | 4    | 5        | 6   | 7   | 8   | 9   | 10   | 11   | 12   |
          |-----|------|--------|--------|------|----------|-----|-----|-----|-----|------|------|------|
          | A   | mon0 | mon0_F |        | adp0 |          |     |     |     |     |      |      |      |
          | B   | mon1 | mon1_Q | mon1_F | adp1 | adp_sst1 |     |     |     |     |      |      |      |
          | C   | mon2 | mon2_F | mon2_Q | adp2 | adp_sst2 |     |     |     |     |      |      |      |
          | D   | mon3 | mon3_Q | mon3_F | adp3 | adp_sst3 |     |     |     |     |      |      |      |
          | E   | mon4 |        | mon4_Q | adp4 | adp_sst4 |     |     |     |     |      |      |      |
          | F   |      |        |        | adp5 |          |     |     |     |     |      |      |      |
          | G   |      |        |        |      |          |     |     |     |     |      |      |      |
          | H   |      |        |        |      |          |     |     |     |     |      |      |      |

      or, with the `well_marker` parameter of :meth:`PlateMap.to_markdown` set to ``'X'``, for instance
      (in case you don't need to see the strand names and just want to see which wells are marked):

      .. code-block::

          plate 1, 5 uL each
          |     | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 9   | 10   | 11   | 12   |
          |-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|------|------|------|
          | A   | *   | *   |     | *   |     |     |     |     |     |      |      |      |
          | B   | *   | *   | *   | *   | *   |     |     |     |     |      |      |      |
          | C   | *   | *   | *   | *   | *   |     |     |     |     |      |      |      |
          | D   | *   | *   | *   | *   | *   |     |     |     |     |      |      |      |
          | E   | *   |     | *   | *   | *   |     |     |     |     |      |      |      |
          | F   |     |     |     | *   |     |     |     |     |     |      |      |      |
          | G   |     |     |     |     |     |     |     |     |     |      |      |      |
          | H   |     |     |     |     |     |     |     |     |     |      |      |      |

      :param plate_type: 96-well or 384-well plate; default is 96-well.
      :param validate: Ensure volumes make sense.
      :param combine_plate_actions: If True, then if multiple actions in the Mix take the same volume from the same plate,
                                    they will be combined into a single :class:`PlateMap`.

      :rtype: A list of all plate maps.



   .. py:method:: _plate_map_from_mixline(mixline: riverine.printing.MixLine, plate_type: riverine.locations.PlateType, existing_plate_map: PlateMap | None) -> PlateMap


   .. py:method:: _update_volumes(consumed_volumes: dict[str, riverine.units.Quantity] | None = None, made_volumes: dict[str, riverine.units.Quantity] | None = None, _cache_key=None) -> Tuple[dict[str, riverine.units.Quantity], dict[str, riverine.units.Quantity]]

      Given a



   .. py:method:: _unstructure(experiment: riverine.experiments.Experiment | None = None) -> dict[str, Any]


   .. py:method:: _structure(d: dict[str, Any], experiment: riverine.experiments.Experiment | None = None) -> Mix
      :classmethod:



.. py:function:: split_mix(mix: Mix, num_tubes: int | None = None, names: Iterable[str] | None = None, excess: float | riverine.units.Decimal = Decimal(0.05)) -> Mix

   A "split mix" is a :any:`Mix` that involves creating a large volume mix and splitting it into several
   test tubes with identical contents. The advantage of specifying a split mix is that one can give
   the desired volumes/concentrations in the individual test tubes (post splitting) and the number of
   test tubes, and the correct amounts in the larger mix will automatically be calculated.

   The :meth:`Mix.instructions` method of a split mix includes the additional instruction at the end
   to aliquot from the larger mix.

   :param mix: The :any:`Mix` object describing what each
               individual smaller test tube should contain after the split.
   :param num_tubes: The number of test tubes into which to split the large mix. Should not be specified if `names`
                     is specified; in that case `num_tubes` is assumed to be the number of strings in `names`.
   :param excess: A fraction (between 0 and 1) indicating how much extra of the large mix to make. This is useful
                  when `num_tubes` is large, since the aliquots prior to the last test tube may take a small amount
                  of extra volume, resulting in the final test tube receiving significantly less volume if the
                  large mix contained only just enough total volume.

                  For example, if the total volume is 100 uL and `num_tubes` is 20, then each aliquot
                  from the large mix to test tubes would be 100/20 = 5 uL. But if due to pipetting imprecision 5.05 uL
                  is actually taken, then the first 19 aliquots will total to 19*5.05 = 95.95 uL, so there will only be
                  100 - 95.95 = 4.05 uL left for the last test tube. But by setting `excess` to 0.05,
                  then to make 20 test tubes of 5 uL each, we would have 5*20*1.05 = 105 uL total, and in this case
                  even assuming pipetting error resulting in taking 95.95 uL for the first 19 samples, there is still
                  105 - 95.95 = 9.05 uL left, more than enough for the 20'th test tube.

                  Note: using `excess` > 0 means than the test tube with the large mix should *not* be
                  reused as one of the final test tubes, since it will have too much volume at the end.
   :param names: Names of smaller individual test tubes (will be printed in instructions).

   :returns: * A "large" mix, from which `num_tubes` aliquots can be made to create each of the identical
             * *"small" mixes.*


.. py:function:: master_mix(mixes: Iterable[Mix], name: str = 'master mix', excess: float | riverine.units.Decimal = Decimal(0.05), exclude_shared_components: Iterable[str | riverine.components.Component] = ()) -> tuple[Mix, list[Mix]]

   Create a "master mix" useful for saving pipetting steps when creating :any:`Mix`'s in `mixes`
   by grouping components shared among each :any:`Mix`'s in `mixes` into a single large master mix
   from which the shared components can be pipetted to create the downstream mixes.

   Components are considered "shared" if they appear in *all* :any:`Mix`'s in `mixes`.

   To ensure sufficient volume for the last mix when the number of mixes is large (due to slight pipetting
   error from the master mix adding up over many steps), the parameter `excess`
   can be used to control how much of a slight excess of necessary volume is included in the master mix.

   Shared Components may be excluded from the master mix by putting them or their names in the parameter
   `exclude_shared_components`.

   Example:

   .. code-block:: python

       # staple mix to be shared in all mixes
       staples = [Strand(f"stap{i}", concentration="1uM") for i in range(5)]
       staple_mix = Mix(
           actions=[FixedConcentration(components=staples, fixed_concentration="100 nM")],
           name="staple mix",
       )

       # "adapter" mixes that are different between mixes
       num_variants = 3
       adapter_mixes = {}
       for adp_idx in range(num_variants):
           adapters = [Strand(f'adp_{adp_idx}_{i}', concentration="1uM") for i in range(5)]
           adapter_mix = Mix(
               actions=[FixedConcentration(components=adapters, fixed_concentration="50 nM")],
               name=f"adapters {adp_idx} mix",
           )
           adapter_mixes[adp_idx] = adapter_mix

       m13 = Strand("m13 100nM", concentration="100 nM")
       mixes = [Mix(
           actions=[
               FixedConcentration(components=[m13], fixed_concentration=f"1 nM"),
               FixedConcentration(components=[staple_mix], fixed_concentration=f"10 nM"),
               FixedConcentration(components=[adapter_mixes[adp_idx]], fixed_concentration=f"10 nM"),
           ],
           name="mm",
           fixed_total_volume=f"100 uL",
       ) for adp_idx, adapter_mix in adapter_mixes.items()]
       mm, final_mixes = master_mix(mixes=mixes, name='origami master mix', excess=0.1)

       print(mm.instructions())
       for mix in final_mixes:
           print(mix.instructions())

   This should print the following. Note that only 63 uL of master mix are strictly required, but
   the total master mix volume is 10% higher (69.3 uL) due to the parameter `excess` = 0.1.

   .. code-block::

       ## Mix "origami master mix":
       | Component   | [Src]     | [Dest]     | #   | Ea Tx Vol   | Tot Tx Vol   | Location  | Note  |
       |:------------|:----------|:-----------|:----|:------------|:-------------|:----------|:------|
       | staple mix  | 100.00 nM | 47.62 nM   |     | 33.00 µl    | 33.00 µl     |           |       |
       | m13 100nM   | 100.00 nM | 4.76 nM    |     | 3.30 µl     | 3.30 µl      |           |       |
       | 10x buffer  | 100.00 mM | 47.62 mM   |     | 33.00 µl    | 33.00 µl     |           |       |
       | Buffer      |           |            |     | 0.00 µl     | 0.00 µl      |           |       |
       | *Total:*    |           | *47.62 nM* | *4* |             | *69.30 µl*   |           |       |

       Aliquot 21.00 µl from this mix into 3 different test tubes.

       ## Mix "mix0":
       | Component          | [Src]     | [Dest]     | #   | Ea Tx Vol   | Tot Tx Vol   | Location  | Note  |
       |:-------------------|:----------|:-----------|:----|:------------|:-------------|:----------|:------|
       | origami master mix | 47.62 nM  | 10.00 nM   |     | 21.00 µl    | 21.00 µl     |           |       |
       | Mg++               | 125.00 mM | 12.50 mM   |     | 10.00 µl    | 10.00 µl     |           |       |
       | adapters 0 mix     | 50.00 nM  | 20.00 nM   |     | 40.00 µl    | 40.00 µl     |           |       |
       | Buffer             |           |            |     | 29.00 µl    | 29.00 µl     |           |       |
       | *Total:*           |           | *10.00 nM* | *4* |             | *100.00 µl*  |           |       |

       ## Mix "mix1":
       | Component          | [Src]     | [Dest]     | #   | Ea Tx Vol   | Tot Tx Vol   | Location  | Note  |
       |:-------------------|:----------|:-----------|:----|:------------|:-------------|:----------|:------|
       | origami master mix | 47.62 nM  | 10.00 nM   |     | 21.00 µl    | 21.00 µl     |           |       |
       | Mg++               | 125.00 mM | 12.50 mM   |     | 10.00 µl    | 10.00 µl     |           |       |
       | adapters 1 mix     | 55.00 nM  | 20.00 nM   |     | 36.36 µl    | 36.36 µl     |           |       |
       | Buffer             |           |            |     | 32.64 µl    | 32.64 µl     |           |       |
       | *Total:*           |           | *10.00 nM* | *4* |             | *100.00 µl*  |           |       |

       ## Mix "mix2":
       | Component          | [Src]     | [Dest]     | #   | Ea Tx Vol   | Tot Tx Vol   | Location  | Note  |
       |:-------------------|:----------|:-----------|:----|:------------|:-------------|:----------|:------|
       | origami master mix | 47.62 nM  | 10.00 nM   |     | 21.00 µl    | 21.00 µl     |           |       |
       | Mg++               | 125.00 mM | 12.50 mM   |     | 10.00 µl    | 10.00 µl     |           |       |
       | adapters 2 mix     | 60.00 nM  | 20.00 nM   |     | 33.33 µl    | 33.33 µl     |           |       |
       | Buffer             |           |            |     | 35.67 µl    | 35.67 µl     |           |       |
       | *Total:*           |           | *10.00 nM* | *4* |             | *100.00 µl*  |           |       |


   :param mixes: the list of :any:`Mix`'s of which to calculate a shared master mix
   :param name: name of the master mix
   :param excess: fraction of "excess" volume to include in master mix to ensure sufficient volume in all downstream
                  mixes; see parameter `excess` of :func:`split_mix` for explanation
   :param exclude_shared_components: names of shared components (or Components themselves) to exclude from master mix;
                                     raises exception if any element of `exclude_shared_components` is not shared by all :any:`Mix`'s
                                     in the parameter `mixes`

   :returns: * pair `(master_mix, final_mixes)`, where `master_mix` is the master mix to use in
             * downstream `final_mixes`. Length of `final_mixes` is the same as parameter `mixes`, and
             * they use the same names, but each :any:`Mix` in `final_mixes` will be created by a single
             * pipetting step from `master_mix` rather than individual pipetting steps for each shared component.


