diff --git a/doc/sphinx/source/tutorials/evolution.rst b/doc/sphinx/source/tutorials/evolution.rst index 68a1f4de23..4908956240 100644 --- a/doc/sphinx/source/tutorials/evolution.rst +++ b/doc/sphinx/source/tutorials/evolution.rst @@ -14,6 +14,9 @@ Under the hood this command will take the PDF at the fitting scale and the theor used to run the fit, it will download the corresponding Evolution Kernel Operator and will produce a series of LHAPDF ``.dat`` files which can be used to prepare the grid. +While the default underlying evolution code is ``EKO``, it is possible to also evolve the PDF using +``HOPPET`` by adding the flag ``--hoppet``. + It is also possible to evolve any other PDF or fit by directly accessing the evolution functions. In the following example, we use the function :py:func:`evolven3fit.evolve.evolve_exportgrids_into_lhapdf` to evolve the hessian version of ``PDF4LHC21`` using the settings of the NNPDF4.0 with MHOU fit. diff --git a/n3fit/src/evolven3fit/evolve.py b/n3fit/src/evolven3fit/evolve.py index 5f2b54ba3a..406627876d 100644 --- a/n3fit/src/evolven3fit/evolve.py +++ b/n3fit/src/evolven3fit/evolve.py @@ -24,6 +24,8 @@ from validphys.pdfbases import PIDS_DICT from validphys.utils import yaml_safe +from . import hoppet_evolve + _logger = logging.getLogger(__name__) LOG_FILE = "evolven3fit.log" @@ -82,6 +84,11 @@ def __post_init__(self): if self.labels is None: self.labels = [PIDS_DICT[i] for i in self.pids] + # Setting the charm to 0 gets matching results with EKO's expanded inversion + + # self.pdfgrid[:,self.pids.index(-4)] = 0.0 + # self.pdfgrid[:,self.pids.index(4)] = 0.0 + @property def pdfvalues(self): """Return the PDF, i.e., pdfgrid / xgrid, @@ -90,58 +97,13 @@ def pdfvalues(self): return self.pdfgrid.T / self.xgrid -def evolve_exportgrid(eko_path: pathlib.Path, exportgrids: list[ExportGrid]): +def _evolve_exportgrids_with_eko(eko_op: pathlib.Path, exportgrids: list[ExportGrid]): """ - Takes the path to an EKO and a list of exportgrids, - returns a tuple with an info file and the - evolved exportgrid as a dictionary of the form: - - .. code-block:: python - - { - (Q_1^2, nf1): (replica, flavours, x), - (Q_2^2, nf1): (replica, flavours, x), - ... - (Q_3^2, nf2): (replica, flavours, x), - } - - - with the output grouped by nf and sorted in ascending order by Q2. - - Parameters - ---------- - eko_path: pathlib.Path - Path to the evolution eko - exportgrids: list[ExportGrid] - List of ExportGrid objects to be evolved - - Returns - ------- - info_file: eko_box.info_file - Dict-like object with the info file information. - evolved_replicas: dict - a dictionary containing all evolved PDFs. - The format of the output is - { (q2, flavour number): np.ndarray(replica, flavours, x) } + Evolves the set of exportgrids using EKO. """ - # Check that all exportgrid objects have been evaluated for 1) The same value of Q, the same value of x ref = exportgrids[0] - hessian_fit = ref.hessian - - for egrid in exportgrids: - assert egrid.q20 == ref.q20, "Different values of q0 found among the exportgrids" - np.testing.assert_allclose( - ref.xgrid, egrid.xgrid, err_msg="ExportGrids are not all evaluate at the same x nodes" - ) - assert ( - hessian_fit == egrid.hessian - ), "Trying to evolve hessian and non-hessian fit at the same time" - - # Read the EKO and the operator and theory cards - eko_op = eko.EKO.read(eko_path) theory = eko_op.theory_card op = eko_op.operator_card - assert ref.q20 == op.mu20, f"The EKO can only evolve from {op.mu20}, PDF asked for {ref.q20}" _logger.debug(f"Theory card: {json.dumps(theory.raw)}") @@ -155,7 +117,6 @@ def evolve_exportgrid(eko_path: pathlib.Path, exportgrids: list[ExportGrid]): if XGrid(x_grid) != eko_original_xgrid: new_xgrid = XGrid(x_grid) new_metadata = dataclasses.replace(eko_op.metadata, xgrid=new_xgrid) - new_operators = {} for target_key in eko_op.operators: elem = eko_op[target_key.ep] @@ -194,17 +155,84 @@ def evolve_exportgrid(eko_path: pathlib.Path, exportgrids: list[ExportGrid]): # output is a dictionary {(Q2, nf): (replica, flavour, x)} all_evolved, _ = apply.apply_grids(eko_op, np.array(all_replicas)) + info = info_file.build(theory, op, 1, info_update={}) + # sort the output in terms of (Q2, nf) but grouped by nf sorted_evolved = dict(sorted(all_evolved.items(), key=lambda item: (item[0][1], item[0][0]))) - info = info_file.build(theory, op, 1, info_update={}) + return info, sorted_evolved + + +def evolve_exportgrid( + eko_path: pathlib.Path, exportgrids: list[ExportGrid], theory_id: int = -1, hoppet: bool = False +): + """ + Takes the path to an EKO and a list of exportgrids, + returns a tuple with an info file and the + evolved exportgrid as a dictionary of the form: + + .. code-block:: python + + { + (Q_1^2, nf1): (replica, flavours, x), + (Q_2^2, nf1): (replica, flavours, x), + ... + (Q_3^2, nf2): (replica, flavours, x), + } + + + with the output grouped by nf and sorted in ascending order by Q2. + + Parameters + ---------- + eko_path: pathlib.Path + Path to the evolution eko + exportgrids: list[ExportGrid] + List of ExportGrid objects to be evolved + theory_id: int + Theory ID of evolution (only needed for hoppet) + hoppet: bool + Whether to use HOPPET evolution + + Returns + ------- + info_file: eko_box.info_file + Dict-like object with the info file information. + evolved_replicas: dict + a dictionary containing all evolved PDFs. + The format of the output is + { (q2, flavour number): np.ndarray(replica, flavours, x) } + """ + # Check that all exportgrid objects have been evaluated for 1) The same value of Q, the same value of x + ref = exportgrids[0] + hessian_fit = ref.hessian + + for egrid in exportgrids: + assert egrid.q20 == ref.q20, "Different values of q0 found among the exportgrids" + np.testing.assert_allclose( + ref.xgrid, egrid.xgrid, err_msg="ExportGrids are not all evaluate at the same x nodes" + ) + assert ( + hessian_fit == egrid.hessian + ), "Trying to evolve hessian and non-hessian fit at the same time" + + if hoppet: + info, sorted_evolved = hoppet_evolve.evolve_exportgrids_with_hoppet( + eko_path, exportgrids, theory_id + ) + else: + # We read the EKO to a temporary directory that will vanish upon exiting + with tempfile.TemporaryDirectory() as temp_dir: + eko_op = eko.EKO.read(eko_path, dest=pathlib.Path(temp_dir)) + info, sorted_evolved = _evolve_exportgrids_with_eko(eko_op, exportgrids) + info["NumMembers"] = "REPLACE_NREP" if hessian_fit: info["ErrorType"] = "hessian" else: info["ErrorType"] = "replicas" - info["XMin"] = float(x_grid[0]) - info["XMax"] = float(x_grid[-1]) + info["XMin"] = float(ref.xgrid[0]) + info["XMax"] = float(ref.xgrid[-1]) info["Flavors"] = basis_rotation.flavor_basis_pids info.setdefault("NumFlavors", 5) @@ -217,6 +245,8 @@ def evolve_exportgrids_into_lhapdf( output_files: list[pathlib.Path], info_file: pathlib.Path, finalize: bool = False, + theory_id: int = -1, + hoppet: bool = False, ): """ Exportgrid evolution function. @@ -237,6 +267,10 @@ def evolve_exportgrids_into_lhapdf( path to the info file finalize: bool If True, try to finalize the info file, otherwise keep placeholders to be filled at a later step + theory_id: int + Theory ID of evolution (only needed for hoppet) + hoppet: bool + Whether to use HOPPET evolution """ if len(exportgrids) != len(output_files): raise ValueError("The length of output_files and exportgrids must be equal") @@ -248,56 +282,61 @@ def evolve_exportgrids_into_lhapdf( # all evolved is a dictionary {(Q2, nf): (replica, flavour, x)} # ordered first by nf and then by Q2 in ascending order - info, all_evolved = evolve_exportgrid(eko_path, exportgrids) + info, all_evolved = evolve_exportgrid(eko_path, exportgrids, theory_id=theory_id, hoppet=hoppet) # The ``dump`` functions from eko's genpdf are very opinionated regarding the output folder of the files # therefore we create a temporary directory where to put stuff and then move it to the right place - temp_dir = tempfile.TemporaryDirectory() - temp_path = pathlib.Path(temp_dir.name) - - if finalize: - info["NumMembers"] = len(exportgrids) - - genpdf.export.dump_info(temp_path, info) - temp_info = temp_path / f"{temp_path.stem}.info" - shutil.move(temp_info, info_file) - - # Dump LHAPDF files as .dat files in blocks of nf - targetgrid = exportgrids[0].xgrid.tolist() - q2block_per_nf = defaultdict(list) - for q2, nf in all_evolved.keys(): - q2block_per_nf[nf].append(q2) - - for enum, (exportgrid, output_file) in enumerate(zip(exportgrids, output_files)): - replica_idx = exportgrid.replica - if replica_idx is None and exportgrid.hessian: - replica_idx = enum - blocks = [] - - for nf, q2grid in q2block_per_nf.items(): - - def pdf_xq2(pid, x, Q2): - x_idx = targetgrid.index(x) - pid_idx = info["Flavors"].index(pid) - ret = x * all_evolved[(Q2, nf)][enum][pid_idx][x_idx] - return ret - - block = genpdf.generate_block( - pdf_xq2, xgrid=targetgrid, sorted_q2grid=q2grid, pids=info["Flavors"] - ) - blocks.append(block) - - dat_path = dump_evolved_replica(blocks, temp_path, replica_idx, exportgrid.hessian) - if not dat_path.exists(): - raise FileNotFoundError( - "The expected {dat_path} file was not found after dumping the blocks" - ) - shutil.move(dat_path, output_file) - - temp_dir.cleanup() - - -def evolve_fit(fit_folder, force, eko_path, hessian_fit=False): + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = pathlib.Path(temp_dir) + + if finalize: + info["NumMembers"] = len(exportgrids) + + genpdf.export.dump_info(temp_path, info) + temp_info = temp_path / f"{temp_path.stem}.info" + shutil.move(temp_info, info_file) + + # Dump LHAPDF files as .dat files in blocks of nf + targetgrid = exportgrids[0].xgrid.tolist() + q2block_per_nf = defaultdict(list) + for q2, nf in all_evolved.keys(): + q2block_per_nf[nf].append(q2) + + for enum, (exportgrid, output_file) in enumerate(zip(exportgrids, output_files)): + replica_idx = exportgrid.replica + if replica_idx is None and exportgrid.hessian: + replica_idx = enum + blocks = [] + + for nf, q2grid in q2block_per_nf.items(): + + def pdf_xq2(pid, x, Q2): + x_idx = targetgrid.index(x) + pid_idx = info["Flavors"].index(pid) + ret = x * all_evolved[(Q2, nf)][enum][pid_idx][x_idx] + return ret + + block = genpdf.generate_block( + pdf_xq2, xgrid=targetgrid, sorted_q2grid=q2grid, pids=info["Flavors"] + ) + blocks.append(block) + + dat_path = dump_evolved_replica(blocks, temp_path, replica_idx, exportgrid.hessian) + if not dat_path.exists(): + raise FileNotFoundError( + "The expected {dat_path} file was not found after dumping the blocks" + ) + shutil.move(dat_path, output_file) + + +def evolve_fit( + fit_folder: pathlib.Path, + theory_id: int, + force: bool = False, + hessian_fit: bool = False, + eko_path: pathlib.Path = None, + hoppet: bool = False, +): """ Evolves all the fitted replica in fit_folder/nnfit @@ -306,13 +345,16 @@ def evolve_fit(fit_folder, force, eko_path, hessian_fit=False): fit_folder: str or pathlib.Path path to the folder containing the fit + theory_id: int + Theory ID of evolution force: bool whether to force the evolution to be done again - eko_path: str or pathlib.Path - path where the eko is stored (if None the eko will be - recomputed) hessian_fit: bool wether the fit is hessian + eko_path: str or pathlib.Path + path where the eko is stored (if None the eko will be recomputed) + hoppet: bol + Whether to use HOPPET evolution """ fit_folder = pathlib.Path(fit_folder) log_file = fit_folder / LOG_FILE @@ -348,7 +390,9 @@ def evolve_fit(fit_folder, force, eko_path, hessian_fit=False): output_files.append(exportgrid_file.with_suffix(".dat")) info_path = fit_folder / "nnfit" / f"{fit_folder.name}.info" - evolve_exportgrids_into_lhapdf(eko_path, exportgrids, output_files, info_path) + evolve_exportgrids_into_lhapdf( + eko_path, exportgrids, output_files, info_path, theory_id=theory_id, hoppet=hoppet + ) def dump_evolved_replica(evolved_blocks, dump_folder, replica_num, hessian_fit=False): diff --git a/n3fit/src/evolven3fit/hoppet_evolve.py b/n3fit/src/evolven3fit/hoppet_evolve.py new file mode 100644 index 0000000000..299a924cd4 --- /dev/null +++ b/n3fit/src/evolven3fit/hoppet_evolve.py @@ -0,0 +1,251 @@ +"""Evolution of n3fit export grids with HOPPET. + +This module mirrors the LHAPDF-writing part of :mod:`evolven3fit.evolve`, +but uses HOPPET for the DGLAP evolution instead of applying a precomputed EKO. +""" + +from collections import defaultdict +import dataclasses +import functools +import importlib +import pathlib +import tempfile + +from ekobox import info_file +import numpy as np + +from eko import EKO, basis_rotation +from eko.interpolation import InterpolatorDispatcher +from nnpdf_data import THEORY_CARDS_PATH +from nnpdf_data.theorydbutils import fetch_theory + +from . import eko_utils + +HOPPET_QCD_PIDS = (-6, -5, -4, -3, -2, -1, 21, 1, 2, 3, 4, 5, 6) + + +def _import_hoppet(): + """Import hoppet lazily since it is not part of the default installation.""" + try: + return importlib.import_module("hoppet") + except ModuleNotFoundError as e: + raise ModuleNotFoundError("Can't evolve with hoppet without having hoppet installed") from e + + +@dataclasses.dataclass(frozen=True) +class HoppetTheory: + """HOPPET settings translated from an NNPDF theory card.""" + + alphas: float + qref: float + q0: float + nloop: int + mur_over_q: float + masses: tuple[float, float, float] + mass_scheme: str + fns: str + max_nf_pdf: int + + +# TODO: +# Hoppet asks for a function with a signature (x, Q), in this case Q would be fixed +# so we need to create an interpolator in x following the exportgrid. +# It would be nice if +# a) The exportgrid itself offered the interpolation (this is useful!) +# b) We had some very fine interpolation ready to go +# c) Pass directly the output of the NN!! +# d) This could also be done by eko +# e) So, on second thoughts, we want an exportgrid-compatible NN-interface :) # TODO +# we could +def _make_hoppet_pdf_callback(exportgrid, interpolation_degree=4): + """Creates an interpolation pdf(x) given an exportgrid. + NOTE: the function includes Q but this is dropped anyway + """ + xgrid = exportgrid.xgrid + pid_columns = [exportgrid.pids.index(pid) for pid in HOPPET_QCD_PIDS] + pdfgrid = exportgrid.pdfgrid[:, pid_columns] + + dispatcher = InterpolatorDispatcher(xgrid, interpolation_degree, mode_N=False) + + @functools.cache # assuming that an infinite cache is safe here + def pdf_callback(x, _q): + x = float(x) + if x <= xgrid[0]: + return pdfgrid[0].tolist() + if x >= xgrid[-1]: + return pdfgrid[-1].tolist() + weights = dispatcher.get_interpolation([x])[0] + return (weights @ pdfgrid).tolist() + + return pdf_callback + + +def _nnpdf_theory_to_hoppet(nnpdf_theory, polarized=False): + """Translation layer between the NNPDF theory card and hoppet's parameters.""" + if polarized: + raise NotImplementedError("Polarized not implemented for hoppet") + + if nnpdf_theory.QED != 0: + raise NotImplementedError("QED evolution not yet supported for hoppet") + + if nnpdf_theory.PTO > 2: + raise NotImplementedError("Only up to NNLO for now with hoppet") + + masses = (nnpdf_theory.mc, nnpdf_theory.mb, nnpdf_theory.mt) + # Note: for hoppet this is both the masses and the thresholds + + nloop = nnpdf_theory.PTO + 1 + fns = nnpdf_theory.FNS.split("-")[0] + + return HoppetTheory( + alphas=nnpdf_theory.alphas, + qref=nnpdf_theory.Qref, + q0=nnpdf_theory.Q0, + nloop=nloop, + mur_over_q=nnpdf_theory.XIR, + masses=masses, + mass_scheme=nnpdf_theory.HQ, + fns=fns, + max_nf_pdf=nnpdf_theory.MaxNfPdf, + ) + + +def _build_cards(nnpdf_theory, x_grid, eko_path: pathlib.Path = None): + """Build the operator and theory cards that will later be used to define the + operators by hoppet. If an EKO is given, use that for the operator card instead.""" + theory_card, op_card = eko_utils.construct_eko_cards(nnpdf_theory.asdict(), x_grid) + if eko_path is None: + return theory_card, op_card + + # If we have an eko, load it and read the operator card + # this is a EKO-sized GBs penalty on /tmp, isn't there a get-eko-metadata function?? + # we should clean after oulseves quickly + with tempfile.TemporaryDirectory() as temp_dir: + op_card_eko = EKO.read(eko_path, dest=pathlib.Path(temp_dir)).operator_card + return theory_card, op_card_eko + + +def _configure_hoppet(hp, theory: HoppetTheory, x_grid, q_values, dy=0.025): + """ + Calling a few hoppet function that need to be called before starting. + We use hoppet's extended start to make sure we have the same + range as the lhapdf grid that we use in NNPDF. + """ + hp.SetExactDGLAP(True, True) + + xmin = float(x_grid[0]) + ymax = -np.log(xmin) + + qmin = min(np.min(q_values), theory.q0) + qmax = max(np.max(q_values), theory.q0) + hp.StartExtended( + ymax, + dy, + qmin, + qmax, + dy / 4.0, # hoppet's suggestion + theory.nloop, + -6, # interpolation order + hp.factscheme_MSbar, + ) + + masses = list(theory.masses) + if theory.max_nf_pdf < 5: + masses[1] = max(qmax * 2.0, masses[1]) + if theory.max_nf_pdf < 6: + masses[2] = max(qmax * 2.0, masses[2]) + + hp.SetPoleMassVFN(*masses) + if theory.fns == "FFNS": + hp.SetFFN(theory.max_nf_pdf) + elif theory.mass_scheme == "MSBAR": + hp.SetMSbarMassVFN(*masses) + else: + hp.SetPoleMassVFN(*masses) + + +def _nudge_q_at_threshold(q, nf, q_by_nf): + """Nudge Q below/above threshold for the changes of NF.""" + matching_nfs = [other_nf for other_q, other_nf in q_by_nf if np.isclose(other_q, q)] + if len(matching_nfs) < 2: + return q + + if nf == min(matching_nfs): + return np.nextafter(q, q - 1.0) + elif nf == max(matching_nfs): + return np.nextafter(q, q + 1.0) + else: + return NotImplementedError("If you are playing with a 3-way threshold you can modify this") + + +def _evolve_one_exportgrid(hp, theory, exportgrid, q_by_nf): + """ + Receives one single exportgrid, and calls hoppet-evolve on it. + Note, at this point hoppet must have already been started. + """ + hp.Evolve( + theory.alphas, + theory.qref, + theory.nloop, + theory.mur_over_q, + _make_hoppet_pdf_callback(exportgrid), + np.sqrt(exportgrid.q20), + ) + + evolved = {} + for q, nf in q_by_nf: + q_eval = _nudge_q_at_threshold(q, nf, q_by_nf) + evolved[(q, nf)] = _eval_pdf_grid(hp, exportgrid.xgrid, q_eval) + return evolved + + +def _eval_pdf_grid(hp, x_grid, q): + """Evaluate the PDF grid at the given value of Q. + This output is (flavour, x) to match what the rest of the evolution functions expect. + """ + values = [] + for x in x_grid: + xpdf = hp.Eval(x, q) + # This can be avoided by having the translation layer ready, later + xpdf_dict = dict(zip(HOPPET_QCD_PIDS, xpdf)) + xpdf_dict[22] = 0.0 + values.append([xpdf_dict[i] / x for i in basis_rotation.flavor_basis_pids]) + + return np.array(values).T + + +def evolve_exportgrids_with_hoppet(eko_path, exportgrids, theory_id): + """ """ + ref = exportgrids[0] + nnpdf_theory = fetch_theory(THEORY_CARDS_PATH, theory_id, as_dict=False) + theory_card, op_card = _build_cards(nnpdf_theory, ref.xgrid, eko_path) + + # Build the theory and operator cards from the NNPDF theories. + # _if_ an EKO path is given, extract the operator card directly from there. + # This ensures that the xgrid/qgrid is exactly the same as the eko + hoppet_theory = _nnpdf_theory_to_hoppet(nnpdf_theory) + hoppet_module = _import_hoppet() + + eko_q_by_nf = op_card.raw["mugrid"] + q_values = sorted({q for q, _ in eko_q_by_nf}) + all_evolved = defaultdict(list) + + try: + _configure_hoppet(hoppet_module, hoppet_theory, ref.xgrid, q_values) + + for exportgrid in exportgrids: + evolved_replica = _evolve_one_exportgrid( + hoppet_module, hoppet_theory, exportgrid, eko_q_by_nf + ) + + for q, nf in eko_q_by_nf: + all_evolved[(q**2, nf)].append(evolved_replica[(q, nf)]) + finally: + hoppet_module.DeleteAll() + + info = info_file.build(theory_card, op_card, 1, info_update={}) + sorted_evolved = { + key: np.array(value) + for key, value in sorted(all_evolved.items(), key=lambda item: (item[0][1], item[0][0])) + } + return info, sorted_evolved diff --git a/n3fit/src/n3fit/scripts/evolven3fit.py b/n3fit/src/n3fit/scripts/evolven3fit.py index b3cc79f402..82b2006b86 100644 --- a/n3fit/src/n3fit/scripts/evolven3fit.py +++ b/n3fit/src/n3fit/scripts/evolven3fit.py @@ -8,7 +8,7 @@ import pathlib import sys -from evolven3fit import cli, eko_utils, evolve, utils +from evolven3fit import cli, eko_utils, evolve, hoppet_evolve, utils import numpy as np from eko.runner.managed import solve @@ -103,6 +103,9 @@ def construct_evolven3fit_parser(subparsers): action="store_true", help="Force the evolution to be done even if it has already been done", ) + parser.add_argument( + "--hoppet", action="store_true", help="Use Hoppet instead of EKO for the evolution" + ) return parser @@ -129,10 +132,12 @@ def main(): if args.actions == "evolve": fit_folder = pathlib.Path(args.fit_folder) + # TODO for now we require the EKO to exist to be able to do hoppet later + # just because these are the only ones we can test + theoryID = utils.get_theoryID_from_runcard(fit_folder) if args.load is None: # no path provided to load the eko, get it from the theory utils.check_filter(fit_folder) - theoryID = utils.get_theoryID_from_runcard(fit_folder) _logger.info(f"Loading eko from theory {theoryID}") eko_path = loader.check_eko(theoryID) else: @@ -140,7 +145,17 @@ def main(): # to be used for the evolution will be loaded from that path. eko_path = args.load - cli.cli_evolven3fit(fit_folder, args.force, eko_path, args.hessian_fit) + utils.check_nnfit_folder(fit_folder) + + evolve.evolve_fit( + fit_folder, + theoryID, + force=args.force, + hessian_fit=args.hessian_fit, + eko_path=eko_path, + hoppet=args.hoppet, + ) + else: # If we are in the business of producing an eko, do some checks before starting: # 1. load the nnpdf theory early to check for inconsistent options and theory problems diff --git a/nnpdf_data/nnpdf_data/theorydbutils.py b/nnpdf_data/nnpdf_data/theorydbutils.py index 0508a11d0f..430de2b778 100644 --- a/nnpdf_data/nnpdf_data/theorydbutils.py +++ b/nnpdf_data/nnpdf_data/theorydbutils.py @@ -19,12 +19,15 @@ class TheoryNotFoundInDatabase(Exception): @lru_cache -def _parse_theory_card(theory_card): +def _parse_theory_card(theory_card, as_dict=True): """Read the theory card using validobj parsing Returns the theory as a dictionary """ tcard = parse_yaml_inp(theory_card, TheoryCard) - return tcard.asdict() + if as_dict: + return tcard.asdict() + else: + return tcard @lru_cache @@ -43,7 +46,7 @@ def _get_available_theory_cards(path): return available_theories -def fetch_theory(theory_database: Path, theoryID: int): +def fetch_theory(theory_database: Path, theoryID: int, as_dict=True): """Looks in the theory card folder and returns a dictionary of theory info for the theory number specified by `theoryID`. @@ -73,8 +76,12 @@ def fetch_theory(theory_database: Path, theoryID: int): except KeyError as e: raise TheoryNotFoundInDatabase(f"Theorycard for theory not found: {e}") - tdict = _parse_theory_card(theoryfile) - if tdict["ID"] != theoryID: + tdict = _parse_theory_card(theoryfile, as_dict=as_dict) + if as_dict: + parsed_tid = tdict["ID"] + else: + parsed_tid = tdict.ID + if parsed_tid != theoryID: raise ValueError(f"The theory ID in {theoryfile} doesn't correspond with its ID entry") return tdict diff --git a/pyproject.toml b/pyproject.toml index 0e979d54b3..8d47674202 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,8 @@ torch = {version = "*", optional = true} jax = {version = "*", optional = true} # parallel hyperopt pymongo = {version = "<4", optional = true} +# HOPPET evolution backend +hoppet = {version = "*", optional = true} # Optional dependencies [tool.poetry.extras] @@ -115,6 +117,7 @@ torch = ["torch"] jax = ["jax"] hyperopt = ["hyperopt", "setuptools"] parallelhyperopt = ["pymongo", "hyperopt", "setuptools"] +hoppet = ["hoppet"] [tool.poetry-dynamic-versioning] enable = true