diff --git a/pyproject.toml b/pyproject.toml index 87c892f..19f98f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: BSD License", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", @@ -104,10 +105,10 @@ dependencies = [ name.echo.features = ["echo"] [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] +python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.10", "3.11", "3.12"] +python = ["3.10", "3.11", "3.12", "3.13"] feature = ["echo"] [tool.ruff] diff --git a/src/alhambra_mixes/mixes.py b/src/alhambra_mixes/mixes.py index aefd6fb..7712a7b 100644 --- a/src/alhambra_mixes/mixes.py +++ b/src/alhambra_mixes/mixes.py @@ -631,9 +631,15 @@ def _tube_map_from_mixline(self, mixline: MixLine) -> str: def tubes_markdown(self, tablefmt: str | TableFormat = "pipe") -> str: """ - :param tablefmt: + + Parameters + ---------- + + tablefmt: table format (see :meth:`PlateMap.to_table` for description) - :return: + + Returns + ------- a Markdown (or other format according to `tablefmt`) string indicating which strands in test tubes to pipette, grouped by the volume of each @@ -660,14 +666,20 @@ def display_instructions( """ Displays in a Jupyter notebook the result of calling :meth:`Mix.instructions()`. - :param plate_type: + Parameters + ---------- + + plate_type: 96-well or 384-well plate; default is 96-well. - :param raise_failed_validation: + + raise_failed_validation: If validation fails (volumes don't make sense), raise an exception. - :param combine_plate_actions: + + 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: + + 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, @@ -678,22 +690,28 @@ def display_instructions( 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: + + 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

title

in HTML). - :param warn_unsupported_title_format: + + 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: + + 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: + + 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). - :return: + + Returns + ------- pipetting instructions in the form of strings combining results of :meth:`Mix.table` and :meth:`Mix.plate_maps` """ @@ -714,9 +732,14 @@ def display_instructions( def generate_picklist(self, experiment: Experiment | None, _cache_key=None) -> PickList | None: """ - :param experiment: + Parameters + ---------- + + experiment: experiment to use for generating picklist - :return: + + Returns + ------- picklist for the mix """ @@ -747,14 +770,21 @@ def instructions( 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: + Parameters + ---------- + + plate_type: 96-well or 384-well plate; default is 96-well. - :param raise_failed_validation: + + + raise_failed_validation: If validation fails (volumes don't make sense), raise an exception. - :param combine_plate_actions: + + 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: + + 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, @@ -765,22 +795,28 @@ def instructions( 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: + + 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

title

in HTML). - :param warn_unsupported_title_format: + + 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: + + 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: + + 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). - :return: + + Returns + ------- pipetting instructions in the form of strings combining results of :meth:`Mix.table` and :meth:`Mix.plate_maps` """ @@ -1122,7 +1158,10 @@ def to_table( which creates a Markdown format. To create other formats such as HTML, change the value of `tablefmt`; see https://github.com/astanin/python-tabulate#readme for other possible formats. - :param well_marker: + Parameters + ---------- + + 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, @@ -1133,29 +1172,39 @@ def to_table( 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: + + 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

title

in HTML). - :param warn_unsupported_title_format: + + 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: + + tablefmt: By default set to `'github'` to create a Markdown table. For other options see https://github.com/astanin/python-tabulate#readme - :param stralign: + + stralign: See https://github.com/astanin/python-tabulate#readme - :param missingval: + + missingval: See https://github.com/astanin/python-tabulate#readme - :param showindex: + + showindex: See https://github.com/astanin/python-tabulate#readme - :param disable_numparse: + + disable_numparse: See https://github.com/astanin/python-tabulate#readme - :param colalign: + + colalign: See https://github.com/astanin/python-tabulate#readme - :return: + + Returns + ------- a string representation of this plate map """ if title_level not in [1, 2, 3, 4, 5, 6]: diff --git a/src/alhambra_mixes/printing.py b/src/alhambra_mixes/printing.py index 79bc0dc..26802ff 100644 --- a/src/alhambra_mixes/printing.py +++ b/src/alhambra_mixes/printing.py @@ -19,11 +19,17 @@ def emphasize(text: str, tablefmt: str | TableFormat, strong: bool = False) -> s surrounds with pair of *'s; if `strong` is True, with double *'s. For `tablefmt` = `'html'`, uses ```` or ````. - :param text: + Parameters + ---------- + + text: text to emphasize - :param tablefmt: + + tablefmt: format in which to add emphasis markup - :return: + + Returns + ------- emphasized version of `text` """ # formats a title for a table produced using tabulate, diff --git a/src/alhambra_mixes/quantitate.py b/src/alhambra_mixes/quantitate.py index cef5045..59cc918 100644 --- a/src/alhambra_mixes/quantitate.py +++ b/src/alhambra_mixes/quantitate.py @@ -139,11 +139,17 @@ def hydrate( """ Indicates how much buffer/water volume to add to a dry DNA sample to reach a particular concentration. - :param target_conc: + Parameters + ---------- + + target_conc: target concentration. If float/int, units are µM (micromolar). - :param nmol: + + nmol: number of nmol (nanomoles) of dry product. - :return: + + Returns + ------- number of µL (microliters) of water/buffer to pipette to reach `target_conc` concentration """ target_conc = parse_conc(target_conc) @@ -162,13 +168,20 @@ def dilute( """ Indicates how much buffer/water volume to add to a wet DNA sample to reach a particular concentration. - :param target_conc: + Parameters + ---------- + + target_conc: target concentration. If float/int, units are µM (micromolar). - :param start_conc: + + start_conc: current concentration of sample. If float/int, units are µM (micromolar). - :param vol: + + vol: current volume of sample. If float/int, units are µL (microliters) - :return: + + Returns + ------- number of µL (microliters) of water/buffer to add to dilate to concentration `target_conc` """ target_conc = parse_conc(target_conc) @@ -198,12 +211,18 @@ def measure_conc( """ Calculates concentration of DNA sample given an absorbance reading on a NanoDrop machine. - :param absorbance: + Parameters + ---------- + + absorbance: UV absorbance at 260 nm. Can either be a single float/int or a nonempty sequence of floats/ints representing repeated measurements; if the latter then an average is taken. - :param ext_coef: + + ext_coef: Extinction coefficient in L/mol*cm. - :return: + + Returns + ------- concentration of DNA sample """ if isinstance(absorbance, (float, int)): @@ -242,19 +261,26 @@ def measure_conc_and_dilute( Calculates concentration of DNA sample given an absorbance reading on a NanoDrop machine, then calculates the amount of buffer/water that must be added to dilute it to a target concentration. - :param absorbance: + Parameters + ---------- + + absorbance: UV absorbance at 260 nm. Can either be a single float/int or a nonempty sequence of floats/ints representing repeated measurements; if the latter then an average is taken. - :param ext_coef: + + ext_coef: Extinction coefficient in L/mol*cm. - :param target_conc: + + target_conc: target concentration. If float/int, units are µM (micromolar). - :param vol: + + vol: current volume of sample. If float/int, units are µL (microliters) NOTE: This is the volume *before* samples are taken to measure absorbance. It is assumed that each sample taken to measure absorbance is 1 µL. If that is not the case, then set the parameter `vol_removed` to the total volume removed. - :param vol_removed: + + vol_removed: Total volume removed from `vol` to measure absorbance. For example, if two samples were taken, one at 1 µL and one at 1.5 µL, then set `vol_removed` = 2.5 µL. @@ -264,7 +290,9 @@ def measure_conc_and_dilute( then it is assumed the number of samples is 1 (i.e., `vol_removed` = 1 µL), otherwise if `absorbance` is a list, then the length of the list is assumed to be the number of samples taken, each at 1 µL. - :return: + + Returns + ------- The pair (current concentration of DNA sample, volume to add to reach `target_conc`) """ if vol_removed is None: @@ -324,25 +352,34 @@ def measure_conc_and_dilute_from_specs( {'mon0': (, ), 'adp0': (, )} - :param filename: + Parameters + ---------- + + filename: IDT specs file (e.g., coa.csv) - :param target_conc: + + target_conc: target concentration to dilute to from measured concentration - :param absorbances: + + absorbances: measured absorbance of each strand. Should be a dict mapping each strand name (as it appears in the "Sequence name" column of `filename`) to an absorbance or nonempty list of absorbances, meaning UV absorbance at 260 nm. If a list then an average is taken. - :param vols_removed: + + vols_removed: dict mapping each strand name to the volume that was removed to take absorbance measurements. For any strand name not appearing as a key in the dict, it is assumed that 1 microliter was taken for each absorbance measurement made. - :param enforce_utf8: + + enforce_utf8: If `filename` is a text CSV file and this paramter is True, it enforces that `filename` is valid UTF-8, raising an exception if not. This helps to avoid accidentally dropping Unicode characters such as µ, which would silently convert a volume from µL to L. If do not want to convert the specs file to UTF-8 and you are certain that no important Unicode characters would be dropped, then you can set this parameter to false. - :return: + + Returns + ------- dict mapping each strand name to a pair `(conc, vol)`, where `conc` is its measured concentration and `vol` is the volume that should be subsequently added to reach concentration `target_conc` """ @@ -385,19 +422,26 @@ def display_measure_conc_and_dilute_from_specs( Like :meth:`measure_conc_and_dilute_from_specs`, but displays the value in a Jupyter notebook instead of returning it. - :param filename: + Parameters + ---------- + + filename: IDT specs file (e.g., coa.csv) - :param target_conc: + + target_conc: target concentration to dilute to from measured concentration - :param absorbances: + + absorbances: measured absorbance of each strand. Should be a dict mapping each strand name (as it appears in the "Sequence name" column of `filename`) to an absorbance or nonempty list of absorbances, meaning UV absorbance at 260 nm. If a list then an average is taken. - :param vols_removed: + + vols_removed: dict mapping each strand name to the volume that was removed to take absorbance measurements. For any strand name not appearing as a key in the dict, it is assumed that 1 microliter was taken for each absorbance measurement made. - :param enforce_utf8: + + enforce_utf8: If `filename` is a text CSV file and this paramter is True, it enforces that `filename` is valid UTF-8, raising an exception if not. This helps to avoid accidentally dropping Unicode characters such as µ, which would silently convert a volume from µL to L. @@ -451,19 +495,27 @@ def hydrate_and_measure_conc_and_dilute( `target_conc_low` with a subsequent dilution step. (As opposed to requiring a vacufuge to concentrate the sample higher). - :param nmol: + Parameters + ---------- + + nmol: number of nmol (nanomoles) of dry product. - :param target_conc_high: + + target_conc_high: target concentration for initial hydration. Should be higher than `target_conc_low`, - :param target_conc_low: + + target_conc_low: the "real" target concentration that we will try to hit after the second addition of water/buffer. - :param absorbance: + + absorbance: UV absorbance at 260 nm. Can either be a single float/int or a nonempty sequence of floats/ints representing repeated measurements; if the latter then an average is taken. - :param ext_coef: + + ext_coef: Extinction coefficient in L/mol*cm. - :param vol_removed: + + vol_removed: Total volume removed from `vol` to measure absorbance. For example, if two samples were taken, one at 1 µL and one at 1.5 µL, then set `vol_removed` = 2.5 µL. @@ -569,20 +621,27 @@ def hydrate_and_measure_conc_and_dilute_from_specs( Instead of returning a dictionary, these methods display the result in the Jupyter notebook, as nicely-formatted Markdown. - :param filename: + Parameters + ---------- + + filename: path to IDT Excel/CSV spreadsheet with specs of strands (e.g., coa.csv) - :param target_conc_high: + + target_conc_high: target concentration for initial hydration. Should be higher than `target_conc_low`, - :param target_conc_low: + + target_conc_low: the "real" target concentration that we will try to hit after the second addition of water/buffer. - :param absorbances: + + absorbances: UV absorbances at 260 nm. Is a dict mapping each strand name to an "absorbance" as defined in the `absobance` parameter of :func:`hydrate_and_measure_conc_and_dilute`. In other words the value to which each strand name maps can either be a single float/int, or a nonempty sequence of floats/ints representing repeated measurements; if the latter then an average is taken. - :param vols_removed: + + vols_removed: Total volumes removed from `vol` to measure absorbance; is a dict mapping strand names (should be subset of strand names that are keys in `absorbances`). Can be None, or can have strictly fewer strand names than in `absorbances`; @@ -595,13 +654,16 @@ def hydrate_and_measure_conc_and_dilute_from_specs( then it is assumed the number of samples is 1 (i.e., `vol_removed` = 1 µL), otherwise if `absorbance` is a list, then the length of the list is assumed to be the number of samples taken, each at 1 µL. - :param enforce_utf8: + + enforce_utf8: If `filename` is a text CSV file and this paramter is True, it enforces that `filename` is valid UTF-8, raising an exception if not. This helps to avoid accidentally dropping Unicode characters such as µ, which would silently convert a volume from µL to L. If do not want to convert the specs file to UTF-8 and you are certain that no important Unicode characters would be dropped, then you can set this parameter to false. - :return: + + Returns + ------- dict mapping each strand name in keys of `absorbances` to a pair (`conc`, `vol_to_add`), where `conc` is the measured concentration according to the absorbance value(s) of that strandm and `vol_to_add` is the volume needed to add to reach concentration `target_conc_low`. @@ -660,20 +722,28 @@ def hydrate_from_specs( Indicates how much volume to add to a dry DNA sample to reach a particular concentration, given data in an Excel file in the IDT format. - :param filename: + Parameters + ---------- + + filename: path to IDT Excel/CSV spreadsheet with specs of strands (e.g., coa.csv) - :param target_conc: + + target_conc: target concentration. If float/int, units are µM (micromolar). - :param strands: + + strands: strands to hydrate. Can be list of strand names (strings), or list of of ints indicating which rows in the Excel spreadsheet to hydrate - :param enforce_utf8: + + enforce_utf8: If `filename` is a text CSV file and this paramter is True, it enforces that `filename` is valid UTF-8, raising an exception if not. This helps to avoid accidentally dropping Unicode characters such as µ, which would silently convert a volume from µL to L. If do not want to convert the specs file to UTF-8 and you are certain that no important Unicode characters would be dropped, then you can set this parameter to false. - :return: + + Returns + ------- dict mapping each strand name to an amount of µL (microliters) of water/buffer to pipette to reach `target_conc` concentration for that strand """ @@ -805,20 +875,27 @@ def measure_conc_from_specs( Indicates the concentrations of DNA samples, given data in an Excel file in the IDT format and measured absorbances from a Nanodrop machine. - :param filename: + Parameters + ---------- + + filename: path to IDT Excel/CSV spreadsheet with specs of strands (e.g., coa.csv) - :param absorbances: + + absorbances: dict mapping each strand name to its absorbance value. Each absorbance value represents UV absorbance at 260 nm. Each can either be a single float/int or a nonempty sequence of floats/ints representing repeated measurements; if the latter then an average is taken. - :param enforce_utf8: + + enforce_utf8: If `filename` is a text CSV file and this paramter is True, it enforces that `filename` is valid UTF-8, raising an exception if not. This helps to avoid accidentally dropping Unicode characters such as µ, which would silently convert a volume from µL to L. If do not want to convert the specs file to UTF-8 and you are certain that no important Unicode characters would be dropped, then you can set this parameter to false. - :return: + + Returns + ------- dict mapping each strand name to a concentration for that strand """ name_key = "Sequence Name" @@ -891,11 +968,16 @@ def display_hydrate_from_specs( given data in an Excel file in the IDT format, displaying the result in a jupyter notebook. - :param filename: + Parameters + ---------- + + filename: path to IDT Excel/CSV spreadsheet with specs of strands (e.g., coa.csv) - :param target_conc: + + target_conc: target concentration. If float/int, units are µM (micromolar). - :param strands: + + strands: strands to hydrate. Can be list of strand names (strings), or list of of ints indicating which rows in the Excel spreadsheet to hydrate """ @@ -926,9 +1008,13 @@ def display_measure_conc_from_specs( given data in an Excel/CSV file in the IDT format, displaying the result in a jupyter notebook. - :param filename: + Parameters + ---------- + + filename: path to IDT Excel/CSV spreadsheet with specs of strands (e.g., coa.csv) - :param absorbances: + + absorbances: dict mapping each strand name to its absorbance value. Each absorbance value represents UV absorbance at 260 nm. Each can either be a single float/int or a nonempty sequence of floats/ints diff --git a/src/alhambra_mixes/references.py b/src/alhambra_mixes/references.py index eb0b5ae..3b07325 100644 --- a/src/alhambra_mixes/references.py +++ b/src/alhambra_mixes/references.py @@ -73,23 +73,44 @@ def plate_map( plate_type: PlateType = PlateType.wells96, ) -> PlateMap: """ - :param name: + Return a :class:`PlateMap` for a given plate name in the Reference. + + Parameters + ---------- + + name: Name of plate to make a :class:`PlateMap` for. - :param plate_type: + + plate_type: Either :data:`PlateType.wells96` or :data:`PlateType.wells384`; default is :data:`PlateType.wells96`. - :return: + + Returns + ------- a :class:`PlateMap` consisting of all strands in this Reference object from plate named `name`. Currently always makes a 96-well plate. + + Raises + ------ + ValueError: + If `name` is not the name of a plate in the reference. """ well_to_strand_name = {} + found_plate_name = False + available_plate_names = set() for row in self.df.itertuples(): + available_plate_names.add(row.Plate) if row.Plate == name: # type: ignore + found_plate_name = True well = row.Well # type: ignore sequence = row.Sequence # type: ignore strand = Strand(name=row.Name, sequence=sequence) # type: ignore well_to_strand_name[well] = strand.name + if not found_plate_name: + raise ValueError(f'Plate "{name}" not found in reference file.' + f'\nAvailable plate names: {", ".join(available_plate_names)}') + plate_map = PlateMap( plate_name=name, plate_type=plate_type, diff --git a/src/alhambra_mixes/units.py b/src/alhambra_mixes/units.py index 01f27ff..c7b30ee 100644 --- a/src/alhambra_mixes/units.py +++ b/src/alhambra_mixes/units.py @@ -207,9 +207,14 @@ def normalize(quantity: DecimalQuantity) -> DecimalQuantity: https://pint.readthedocs.io/en/0.18/tutorial.html#simplifying-units) and eliminate trailing zeros. - :param quantity: + Parameters + ---------- + + quantity: a pint DecimalQuantity - :return: + + Returns + ------- `quantity` normalized to be compact and without trailing zeros. """ quantity = cast(DecimalQuantity, quantity.to_compact()) diff --git a/tests/test_references.py b/tests/test_references.py index dea8d1f..e9a300c 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -1,3 +1,4 @@ +import pytest from alhambra_mixes import Reference @@ -17,3 +18,11 @@ def test_idt(): eq = (dfo == dfp).all() print(eq) assert eq.loc[["Well", "Sequence", "Concentration (nM)"]].all() + + +def test_raise_error_if_plate_name_not_found(): + r_order = Reference.compile(("tests/data/holes-order.xlsx", "200 µM")) + + # This should raise an error because the plate name "fake plate name" is not found in the reference + with pytest.raises(ValueError): + r_order.plate_map("fake plate name")