import os
import sys
from desilike.parameter import ParameterCollection, Parameter
from desilike.utils import BaseClass, import_class, is_sequence
[docs]
def find_module_from_file(fn):
"""
Return full module name corresponding to ``fn``.
It first checks if there is an '__init__.py' file in the same directory as ``fn``,
and if so, it recursively finds the module name of that directory and combines it
with the basename of ``fn``.
>>> find_module_from_file('base.py')
'desilike.bindings.base'
If there is no '__init__.py' in the same directory as the given file, it returns ``None``.
"""
dirname = os.path.dirname(fn)
if os.path.isfile(os.path.join(dirname, '__init__.py')):
module = find_module_from_file(dirname) or os.path.basename(dirname)
basename = os.path.splitext(os.path.basename(fn))[0]
return '.'.join([module, basename])
[docs]
def load_from_file(fn, obj):
"""Load object ``obj`` from file ``fn``."""
import importlib.util
spec = importlib.util.spec_from_file_location('bindings', fn)
foo = importlib.util.module_from_spec(spec)
spec.loader.exec_module(foo)
return getattr(foo, obj)
[docs]
class BaseLikelihoodGenerator(BaseClass):
"""Base class to write necessary files for likelihoods to be imported by external inference codes."""
line_delimiter = '\n\n'
def __init__(self, factory, dirname='.'):
"""
Initialize :class:`BaseLikelihoodGenerator`.
Parameters
----------
factory : callable
A callable that returns a likelihood object adapted to the external inference code,
and takes as input a callable that returns a :class:`BaseLikelihood`,
a dictionary of optional arguments (passed to the latter callable),
and the name of the module where it is called.
and returns a likelihood object adapted to the external inference code.
dirname : str, Path, default='.'
Base directory for the file structure: bindings are saved in dirname/{code}_bindings/,
with {code} the directory where ``factory`` is defined: 'cobaya', 'cosmosis', 'montepython'...
"""
self.factory = factory
self.header = '# NOTE: This code has been automatically generated by {}.{}\n'.format(self.__class__.__module__, self.__class__.__name__)
self.header += 'from {} import {}'.format(self.factory.__module__, self.factory.__name__)
self.dirname = os.path.abspath(os.path.join(dirname, os.path.basename(os.path.dirname(sys.modules[self.factory.__module__].__file__))) + '_bindings')
[docs]
def get_code(self, likelihood, name_like=None, kw_like=None, module=None, fn=None, **kwargs):
"""
Internal method to write code to generate likelihood object to be imported by the external inference code.
Parameters
----------
likelihood : type, callable
Callable that returns a :class:`BaseLikelihood`, given some optional arguments (see ``kw_like``).
name_like : str, default=None
Likelihood name, defaults to ``likelihood`` name.
kw_like : dict, default=None
Optional arguments for ``likelihood``.
module : str, default=None
Full module name where ``likelihood`` is defined.
If ``None``, the full module name is searched with :func:`find_module_from_file`; if not in a package,
absolute path to file where ``likelihood`` object is defined will be used to import it in the generated code.
fn : Path, str, default=None
Where to save the likelihood definition for the inference code.
It is prefixed by :attr:`dirname`.
**kwargs : dict
Other optional arguments for likelihood factory, e.g. ``kw_cobaya``.
Returns
-------
cls, name, fn, code : callable, str, str
Callable that generates :class:`BaseLikelihood`, likelihood name, file name where the code is to be written, and code itself.
"""
self.kw_like = kw_like = kw_like or {}
cls = import_class(likelihood)
if name_like is None:
name_like = cls.__name__
src_fn = os.path.abspath(sys.modules[cls.__module__].__file__)
# src_dir = os.path.dirname(src_fn)
if fn is None:
if src_fn.startswith(self.dirname): # likelihood defined somewhere in dirname
fn = os.path.join(self.dirname, os.path.relpath(src_fn, os.path.commonpath([self.dirname, src_fn])))
else:
fn = os.path.join(self.dirname, os.path.basename(src_fn))
else:
fn = os.path.abspath(os.path.join(self.dirname, os.path.normpath(fn)))
if module is None:
module = find_module_from_file(src_fn)
if module is not None: # check if this is a package, then assumed in pythonpath
code = 'from {} import {}\n'.format(module, cls.__name__)
else:
# code = 'import sys\n'
# code += "sys.path.insert(0, '{}')\n".format(src_dir)
# code += 'from {} import {}\n'.format(os.path.splitext(os.path.basename(fn))[0], cls.__name__)
code = 'from desilike.bindings.base import load_from_file\n'
code += "{} = load_from_file('{}', '{}')\n".format(cls.__name__, src_fn, cls.__name__)
code += "{} = {}({}, '{}', kw_like={}, module=__name__".format(name_like, self.factory.__name__, cls.__name__, name_like, kw_like)
if kwargs:
code += ", " + ", ".join(['{}={}'.format(name, value) for name, value in kwargs.items()])
code += ")"
return cls, name_like, fn, code
def __call__(self, likelihood, name_like=None, kw_like=None, module=None, overwrite=True, **kwargs):
"""
Generate file structure and code containing definition of likelihood such that it can be imported by the external inference code.
Parameters
----------
likelihood : list, type, callable
List of (or single) callable(s) that returns a :class:`BaseLikelihood`, given some optional arguments (see ``kw_like``).
name_like : str, default=None
Likelihood name, defaults to ``likelihood`` name.
kw_like : dict, default=None
Optional arguments for (each of) ``likelihood``.
module : str, default=None
Module where ``likelihood`` is defined.
If ``None``, absolute path to file where ``likelihood`` object is defined
will be used to import it in the generated code.
**kwargs : dict
Other optional arguments for likelihood factory, e.g. ``kw_cobaya``.
"""
if not is_sequence(likelihood):
likelihood = [likelihood]
if not is_sequence(module):
module = [module] * len(likelihood)
if len(module) != len(likelihood):
raise ValueError('Number of provided likelihood modules is not the same as the number of likelihoods')
if not is_sequence(name_like):
name_like = [name_like] * len(likelihood)
if len(name_like) != len(likelihood):
raise ValueError('Number of provided likelihood names name_like is not the same as the number of likelihoods')
if not is_sequence(kw_like):
kw_like = [kw_like] * len(likelihood)
if len(kw_like) != len(likelihood):
raise ValueError('Number of provided likelihood kwargs kw_like is not the same as the number of likelihoods')
txt = {}
for likelihood, name_like, kw_like, module in zip(likelihood, name_like, kw_like, module):
fn, code = self.get_code(likelihood, name_like=name_like, kw_like=kw_like, module=module, **kwargs)[2:]
txt[fn] = txt.get(fn, []) + [code]
for fn in txt:
self.log_info('Saving likelihood in {}'.format(fn))
current = ''
if not overwrite:
try:
with open(fn, 'r') as file: current = file.read()
except IOError:
pass
with open(fn, 'w' if overwrite else 'a') as file:
for line in [self.header] + txt[fn]:
if line not in current:
file.write(line + self.line_delimiter)
[docs]
def get_likelihood_params(likelihood, derived=False):
"""
Given a :class:`BaseLikelihood` instance,
return its cosmological parameters and its "nuisance" parameters.
"""
all_params = ParameterCollection()
for param in likelihood.all_params:
if param.solved: continue
if param.derived and (not param.depends):
if isinstance(derived, bool):
if not derived: continue
else:
if param.ndim > derived: continue
all_params.set(param)
cosmo_names = likelihood.runtime_info.pipeline.get_cosmo_requires().get('params', {})
cosmo_params, nuisance_params = ParameterCollection(), ParameterCollection()
for param in all_params:
if param.basename in cosmo_names:
cosmo_params.set(param)
else:
nuisance_params.set(param)
return cosmo_params, nuisance_params