diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a434d82e..4e6d48125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,7 @@ jobs: - name: Install julia uses: julia-actions/setup-julia@v1 with: - version: 1.7 + version: 1.9 - name: Cache uses: actions/cache@v3 @@ -113,8 +113,11 @@ jobs: - name: Install dependencies run: .github/workflows/install_deps.sh + - name: Install PEtabJL dependencies + run: julia -e 'using Pkg; Pkg.add("PEtab"); Pkg.add("OrdinaryDiffEq"), Pkg.add("Sundials")' + - name: Run tests - timeout-minutes: 20 + timeout-minutes: 25 run: tox -e julia - name: Coverage diff --git a/doc/example/boehm_JProteomeRes2014/Boehm_validation/Gradient.csv b/doc/example/boehm_JProteomeRes2014/Boehm_validation/Gradient.csv new file mode 100644 index 000000000..1699b6631 --- /dev/null +++ b/doc/example/boehm_JProteomeRes2014/Boehm_validation/Gradient.csv @@ -0,0 +1,2 @@ +Epo_degradation_BaF3,k_exp_hetero,k_exp_homo,k_imp_hetero,k_imp_homo,k_phos,sd_pSTAT5A_rel,sd_pSTAT5B_rel,sd_rSTAT5A_rel +-226.01431847863805,0.03263295726678998,0.05074978988074719,-273.3251522968395,-4.9353233689552945e-5,85.33308210815652,-57.82373022138993,6.47910944243548,1.5327970845176904 diff --git a/doc/example/boehm_JProteomeRes2014/Boehm_validation/Hessian.csv b/doc/example/boehm_JProteomeRes2014/Boehm_validation/Hessian.csv new file mode 100644 index 000000000..3cd15a7d3 --- /dev/null +++ b/doc/example/boehm_JProteomeRes2014/Boehm_validation/Hessian.csv @@ -0,0 +1,10 @@ +Epo_degradation_BaF3,k_exp_hetero,k_exp_homo,k_imp_hetero,k_imp_homo,k_phos,sd_pSTAT5A_rel,sd_pSTAT5B_rel,sd_rSTAT5A_rel +2348.781845248937,-0.034169780647049425,-103.24202100672939,2772.675796487632,7.194796260472683e-5,-938.3560377189202,996.659728857624,31.143959976782426,13.030712117968559 +-0.034169780647049425,0.07739858784002018,-0.15899501912479183,0.7386693785162849,-4.187803035168086e-8,-0.17134098751539498,-0.2564122376627509,0.03372069395138544,0.07241122210747836 +-103.24202100672939,-0.15899501912479183,132.944794430612,95.49012010074641,-2.508362770407691e-6,-22.250883365424308,11.34374075166351,-3.886786248018129,-7.690665859911962 +2772.675796487632,0.7386693785162849,95.49012010074641,3563.661917245381,1.092950395076315e-5,-1110.2044314822922,1202.406495619125,41.717823566223906,14.58452306207908 +7.194796260472683e-5,-4.187803035168086e-8,-2.508362770407691e-6,1.092950395076315e-5,0.00011364116982094509,-0.00016740979955329535,0.0001089554236616266,0.00011027946613432109,8.045150373694503e-6 +-938.3560377189202,-0.17134098751539498,-22.250883365424308,-1110.2044314822922,-0.00016740979955329535,965.7762449769793,-416.0316410076297,-6.62255869644657,29.6808324053416 +996.659728857624,-0.2564122376627509,11.34374075166351,1202.406495619125,0.0001089554236616266,-416.0316410076297,435.9488576495015,0.0,0.0 +31.143959976782426,0.03372069395138544,-3.886786248018129,41.717823566223906,0.00011027946613432109,-6.62255869644657,0.0,139.82333729022352,0.0 +13.030712117968559,0.07241122210747836,-7.690665859911962,14.58452306207908,8.045150373694503e-6,29.6808324053416,0.0,0.0,162.6019478340014 diff --git a/doc/example/boehm_JProteomeRes2014/Boehm_validation/ObjectiveValue.csv b/doc/example/boehm_JProteomeRes2014/Boehm_validation/ObjectiveValue.csv new file mode 100644 index 000000000..8344862a3 --- /dev/null +++ b/doc/example/boehm_JProteomeRes2014/Boehm_validation/ObjectiveValue.csv @@ -0,0 +1,2 @@ +ObjectiveValue +149.05022585636098 diff --git a/doc/example/boehm_JProteomeRes2014/Boehm_validation/Parameter.csv b/doc/example/boehm_JProteomeRes2014/Boehm_validation/Parameter.csv new file mode 100644 index 000000000..48fc5a4f2 --- /dev/null +++ b/doc/example/boehm_JProteomeRes2014/Boehm_validation/Parameter.csv @@ -0,0 +1,2 @@ +Epo_degradation_BaF3,k_exp_hetero,k_exp_homo,k_imp_hetero,k_imp_homo,k_phos,sd_pSTAT5A_rel,sd_pSTAT5B_rel,sd_rSTAT5A_rel +-1.665827601408055,-4.999704893599998,-2.2096987817000167,-1.78600654750001,4.9901140088,4.1977354885,0.5857552705999998,0.8189828191999999,0.49868440400000047 diff --git a/doc/example/conversion_reaction/PEtabJl_module.jl b/doc/example/conversion_reaction/PEtabJl_module.jl new file mode 100644 index 000000000..155ba776b --- /dev/null +++ b/doc/example/conversion_reaction/PEtabJl_module.jl @@ -0,0 +1,20 @@ +module MyPEtabJlModule + +using OrdinaryDiffEq +using Sundials +using PEtab + +pathYaml = "/Users/pauljonasjost/Documents/GitHub_Folders/pyPESTO/test/julia/../../doc/example/conversion_reaction/conversion_reaction.yaml" +petabModel = readPEtabModel(pathYaml, verbose=true) + +# A full list of options for createPEtabODEProblem can be found at https://sebapersson.github.io/PEtab.jl/dev/API_choosen/#PEtab.setupPEtabODEProblem +petabProblem = createPEtabODEProblem( + petabModel, + odeSolverOptions=ODESolverOptions(Rodas5P(), abstol=1e-08, reltol=1e-08, maxiters=Int64(1e4)), + gradientMethod=:ForwardDiff, + hessianMethod=:ForwardDiff, + sparseJacobian=nothing, + verbose=true +) + +end diff --git a/pypesto/objective/julia/__init__.py b/pypesto/objective/julia/__init__.py index 0512fc866..fbd2dcc82 100644 --- a/pypesto/objective/julia/__init__.py +++ b/pypesto/objective/julia/__init__.py @@ -3,3 +3,4 @@ =============== """ from .base import JuliaObjective, display_source_ipython +from .petabJl import PEtabJlObjective diff --git a/pypesto/objective/julia/petabJl.py b/pypesto/objective/julia/petabJl.py new file mode 100644 index 000000000..344c78f71 --- /dev/null +++ b/pypesto/objective/julia/petabJl.py @@ -0,0 +1,215 @@ +"""Interface to PEtab.jl.""" +import logging +import os + +import numpy as np + +from .base import JuliaObjective, _read_source + +logger = logging.getLogger(__name__) + +PEtabProblemJl = ["PEtab.jl::PEtabODEProblem"] + + +class PEtabJlObjective(JuliaObjective): + """ + Wrapper around an objective defined in PEtab.jl. + + Parameters + ---------- + module: + Name of the julia module containing the objective. + source_file: + Julia source file. Defaults to "{module}.jl". + petab_problem_name: + Name of the petab problem variable in the julia module. + """ + + def __init__( + self, + module: str, + source_file: str = None, + petab_problem_name: str = "petabProblem", + precompile: bool = True, + force_compile: bool = False, + ): + """Initialize objective.""" + # lazy imports + try: + from julia import Main, Pkg # noqa: F401 + + Pkg.activate(".") + except ImportError: + raise ImportError( + "Install PyJulia, e.g. via `pip install pypesto[julia]`, " + "and see the class documentation", + ) + + self.module = module + self.source_file = source_file + self._petab_problem_name = petab_problem_name + if precompile: + self.precompile_model(force_compile=force_compile) + + if self.source_file is None: + self.source_file = f"{module}.jl" + + # Include module if not already included + _read_source(module, source_file) + + petab_jl_problem = self.get(petab_problem_name) + self.petab_jl_problem = petab_jl_problem + + # get functions + fun = self.petab_jl_problem.computeCost + grad = self.petab_jl_problem.computeGradient + hess = self.petab_jl_problem.computeHessian + x_names = np.asarray(self.petab_jl_problem.θ_estNames) + + # call the super super super constructor + super(JuliaObjective, self).__init__( + fun=fun, grad=grad, hess=hess, x_names=x_names + ) + + def __getstate__(self): + """Get state for pickling.""" + # if not dumped, dump it via JLD2 + return { + 'module': self.module, + 'source_file': self.source_file, + '_petab_problem_name': self._petab_problem_name, + } + + def __setstate__(self, state): + """Set state from pickling.""" + for key, value in state.items(): + setattr(self, key, value) + # lazy imports + try: + from julia import Main # noqa: F401 + from julia import Pkg + + Pkg.activate(".") + except ImportError: + raise ImportError( + "Install PyJulia, e.g. via `pip install pypesto[julia]`, " + "and see the class documentation", + ) + # Include module if not already included + _read_source(self.module, self.source_file) + + petab_jl_problem = self.get(self._petab_problem_name) + self.petab_jl_problem = petab_jl_problem + + # get functions + fun = self.petab_jl_problem.computeCost + grad = self.petab_jl_problem.computeGradient + hess = self.petab_jl_problem.computeHessian + x_names = np.asarray(self.petab_jl_problem.θ_estNames) + + # call the super super constructor + super(JuliaObjective, self).__init__(fun, grad, hess, x_names) + + def __deepcopy__(self, memodict=None): + """Deepcopy.""" + return PEtabJlObjective( + module=self.module, + source_file=self.source_file, + petab_problem_name=self._petab_problem_name, + precompile=False, + ) + + def precompile_model(self, force_compile: bool = False): + """ + Use Julias PrecompilationTools to precompile the relevant code. + + Only needs to be done once, and speeds up Julia loading drastically. + """ + directory = os.path.dirname(self.source_file) + # check whether precompilation is necessary, if the directory exists + if ( + os.path.exists(f"{directory}/{self.module}_pre") + and not force_compile + ): + logger.info("Precompilation module already exists.") + return None + # lazy imports + try: + from julia import Main # noqa: F401 + except ImportError: + raise ImportError( + "Install PyJulia, e.g. via `pip install pypesto[julia]`, " + "and see the class documentation", + ) + # setting up a local project, where the precompilation will be done in + from julia import Pkg + + Pkg.activate(".") + # create a Project f"{self.module}_pre". + try: + Pkg.generate(f"{directory}/{self.module}_pre") + except Exception: + logger.info("Module is already generated. Skipping generate...") + # Adjust the precompilation file + write_precompilation_module( + module=self.module, + source_file_orig=self.source_file, + ) + # add a new line at the top of the original module to use the + # precompiled module + with open(self.source_file, "r") as read_f: + if read_f.readline().endswith("_pre\n"): + with open("dummy_temp_file.jl", "w+") as write_f: + write_f.write(f"using {self.module}_pre\n\n") + write_f.write(read_f.read()) + os.remove(self.source_file) + os.rename("dummy_temp_file.jl", self.source_file) + + try: + Pkg.develop(path=f"{directory}/{self.module}_pre") + except Exception: + logger.info("Module is already developed. Skipping develop...") + Pkg.activate(f"{directory}/{self.module}_pre/") + # add dependencies + Pkg.add("PrecompileTools") + Pkg.add("OrdinaryDiffEq") + Pkg.add("PEtab") + Pkg.add("Sundials") + Pkg.precompile() + + +def write_precompilation_module(module, source_file_orig): + """Write the precompilation module for the PEtabJl module.""" + # read the original source file + with open(source_file_orig) as f: + lines = np.array(f.readlines()) + # path to the yaml file + yaml_path = "\t".join(lines[["yaml" in line for line in lines]]) + # packages + packages = "\t\t".join( + lines[[line.startswith("using ") for line in lines]] + ) + # get everything in between the packages and the end line + start = int(np.argwhere([line.startswith("using ") for line in lines])[-1]) + end = int(np.argwhere([line.startswith("end") for line in lines])[0]) + petab_loading = "\t\t".join(lines[start:end]) + + content = ( + f"module {module}_pre\n\n" + f"using PrecompileTools\n\n" + f"# Reduce time for reading a PEtabModel and for " + f"building a PEtabODEProblem\n" + f"@setup_workload begin\n" + f"\t{yaml_path}" + f"\t@compile_workload begin\n" + f"\t\t{packages}" + f"\t\t{petab_loading}" + f"\tend\n" + f"end\n\n" + f"end\n" + ) + # get the directory of the source file + directory = os.path.dirname(source_file_orig) + # write file + with open(f"{directory}/{module}_pre/src/{module}_pre.jl", "w") as f: + f.write(content) diff --git a/pypesto/objective/julia/petab_jl_importer.py b/pypesto/objective/julia/petab_jl_importer.py new file mode 100644 index 000000000..3b8f2c1d2 --- /dev/null +++ b/pypesto/objective/julia/petab_jl_importer.py @@ -0,0 +1,322 @@ +"""Contains the PetabJlImporter class.""" +from __future__ import annotations + +import logging +import os.path +from typing import Iterable, List, Optional, Tuple, Union + +import numpy as np + +from pypesto.objective.julia import PEtabJlObjective +from pypesto.problem import Problem + +logger = logging.getLogger(__name__) + +PEtabProblemJl = ["PEtab.jl::PEtabODEProblem"] + + +class PetabJlImporter: + """ + Importer for PEtab models in Julia, using PEtab.jl. + + Create an `objective.JuliaObjective` or a `pypesto.Problem` from PEtab + files or from a julia module. + """ + + def __init__( + self, + module: str = None, + source_file: str = None, + petab_problem_name: str = "petabProblem", + ): + """ + Initialize importer. + + Parameters + ---------- + module: + Name of the Julia model + source_file: + Path to the Julia source file. + petab_problem: + Wrapper around the PEtab.jl problem. + """ + self.module = module + self.source_file = source_file + self._petab_problem_name = petab_problem_name + # placeholder for the petab.jl problem + self.petab_jl_problem = None + + @staticmethod + def from_yaml( + yaml_file: str, + odeSolverOptions: Optional[dict] = None, + gradientMethod: Optional[str] = None, + hessianMethod: Optional[str] = None, + sparseJacobian: Optional[bool] = None, + verbose: Optional[bool] = None, + directory: Optional[str] = None, + ) -> PetabJlImporter: + """ + Create a `PetabJlImporter` from a yaml file. + + Writes the Julia module to a file in `directory` and returns a + `PetabJlImporter` for that module. + + Parameters + ---------- + yaml_file: + The yaml file of the PEtab problem + odeSolverOptions: + Dictionary like options for the ode solver in julia + gradientMethod, hessianMethod: + Julia methods to compute gradient and hessian + sparseJacobian: + Whether to compute sparse Jacobians + verbose: + Whether to have a more informative log. + directory: + Where to write the julia file, defaults to the directory of the + yaml file. + """ + # get default values + options = _get_default_options( + odeSolverOptions=odeSolverOptions, + gradientMethod=gradientMethod, + hessianMethod=hessianMethod, + sparseJacobian=sparseJacobian, + verbose=verbose, + ) + + # write julia module + source_file, module = _write_julia_file( + yaml_file=yaml_file, options=options, directory=directory + ) + + return PetabJlImporter( + module=module, + source_file=source_file, + ) + + def create_objective( + self, precompile: Optional[bool] = True + ) -> PEtabJlObjective: + """ + Create a `pypesto.objective.PEtabJlObjective` from the PEtab.jl problem. + + The objective function will be the negative log likelihood or the + negative log posterior, depending on the PEtab.jl problem. + + Parameters + ---------- + precompile: + Whether to precompile the julia module for speed up in + multistart optimization. + + """ + # lazy imports + try: + from julia import Main # noqa: F401 + except ImportError: + raise ImportError( + "Install PyJulia, e.g. via `pip install pypesto[julia]`, " + "and see the class documentation", + ) + if self.source_file is None: + self.source_file = f"{self.module}.jl" + + if not os.path.exists(self.source_file): + raise ValueError( + "The julia file does not exist. You can create " + "it from a petab yaml file path using " + "`PetabJlImporter.from_yaml(yaml_file)`" + ) + + obj = PEtabJlObjective( + module=self.module, + source_file=self.source_file, + petab_problem_name=self._petab_problem_name, + precompile=precompile, + ) + + self.petab_jl_problem = obj.petab_jl_problem + return obj + + def create_problem( + self, + x_guesses: Optional[Iterable[float]] = None, + lb_init: Union[np.ndarray, List[float], None] = None, + ub_init: Union[np.ndarray, List[float], None] = None, + precompile: Optional[bool] = True, + ) -> Problem: + """ + Create a `pypesto.Problem` from the PEtab.jl problem. + + Parameters + ---------- + x_guesses: + Guesses for the parameter values, shape (g, dim), where g denotes the + number of guesses. These are used as start points in the optimization. + lb_init, ub_init: + The lower and upper bounds for initialization, typically for defining + search start points. + If not set, set to lb, ub. + precompile: + Whether to precompile the julia module for speed up in + multistart optimization. + """ + obj = self.create_objective(precompile=precompile) + lb = np.asarray(self.petab_jl_problem.lowerBounds) + ub = np.asarray(self.petab_jl_problem.upperBounds) + + return Problem( + objective=obj, + lb=lb, + ub=ub, + x_guesses=x_guesses, + x_names=obj.x_names, + lb_init=lb_init, + ub_init=ub_init, + ) + + +def _get_default_options( + odeSolverOptions: Union[dict, None] = None, + gradientMethod: Union[str, None] = None, + hessianMethod: Union[str, None] = None, + sparseJacobian: Union[str, None] = None, + verbose: Union[str, None] = None, +) -> dict: + """ + If values are not specified, get default values for the options. + + Additionally check that the values are valid. + + Parameters + ---------- + odeSolverOptions: + Options for the ODE solver. + gradientMethod: + Method for gradient calculation. + hessianMethod: + Method for hessian calculation. + sparseJacobian: + Whether the jacobian should be sparse. + verbose: + Whether to print verbose output. + + Returns + ------- + dict: + The options. + """ + # get default values + if odeSolverOptions is None: + odeSolverOptions = { + "solver": "Rodas5P", + "abstol": 1e-8, + "reltol": 1e-8, + "maxiters": "Int64(1e4)", + } + if not odeSolverOptions["solver"].endswith("()"): + odeSolverOptions["solver"] += "()" # add parentheses + if gradientMethod is None: + gradientMethod = "nothing" + if hessianMethod is None: + hessianMethod = "nothing" + if sparseJacobian is None: + sparseJacobian = "nothing" + if verbose is None: + verbose = "true" + + # check values for gradientMethod and hessianMethod + allowed_gradient_methods = [ + "ForwardDiff", + "ForwardEquations", + "Adjoint", + "Zygote", + ] + if gradientMethod not in allowed_gradient_methods: + logger.warning( + f"gradientMethod {gradientMethod} is not in " + f"{allowed_gradient_methods}. Defaulting to ForwardDiff." + ) + gradientMethod = "ForwardDiff" + allowed_hessian_methods = ["ForwardDiff", "BlocForwardDiff", "GaussNewton"] + if hessianMethod not in allowed_hessian_methods: + logger.warning( + f"hessianMethod {hessianMethod} is not in " + f"{allowed_hessian_methods}. Defaulting to ForwardDiff." + ) + hessianMethod = "ForwardDiff" + + # fill options + options = { + "odeSolverOptions": odeSolverOptions, + "gradientMethod": gradientMethod, + "hessianMethod": hessianMethod, + "sparseJacobian": sparseJacobian, + "verbose": verbose, + } + return options + + +def _write_julia_file( + yaml_file: str, options: dict, directory: str +) -> Tuple[str, str]: + """ + Write the Julia file. + + Parameters + ---------- + yaml_file: + The yaml file of the PEtab problem. + options: + The options. + dir: + The directory to write the file to. + + Returns + ------- + source_file: + The name/path of the file. + module: + The module name. + """ + if directory is None: + directory = os.path.dirname(yaml_file) # directory of the yaml file + source_file = os.path.join(directory, "PEtabJl_module.jl") + module = "MyPEtabJlModule" + + link_to_options = ( + "https://sebapersson.github.io/" + "PEtab.jl/dev/API_choosen/#PEtab.setupPEtabODEProblem" + ) + odeSolvOpt_str = ", ".join( + [f"{k}={v}" for k, v in options["odeSolverOptions"].items()] + ) + # delete "solver=" from string + odeSolvOpt_str = odeSolvOpt_str.replace("solver=", "") + + content = ( + f"module {module}\n\n" + f"using OrdinaryDiffEq\n" + f"using Sundials\n" + f"using PEtab\n\n" + f"pathYaml = \"{yaml_file}\"\n" + f"petabModel = readPEtabModel(pathYaml, verbose=true)\n\n" + f"# A full list of options for createPEtabODEProblem can be " + f"found at {link_to_options}\n" + f"petabProblem = createPEtabODEProblem(\n\t" + f"petabModel,\n\t" + f"odeSolverOptions=ODESolverOptions({odeSolvOpt_str}),\n\t" + f"gradientMethod=:{options['gradientMethod']},\n\t" + f"hessianMethod=:{options['hessianMethod']},\n\t" + f"sparseJacobian={options['sparseJacobian']},\n\t" + f"verbose={options['verbose']}\n)\n\nend\n" + ) + # write file + with open(source_file, "w") as f: + f.write(content) + + return source_file, module diff --git a/test/julia/test_pyjulia.py b/test/julia/test_pyjulia.py index 5405ca60a..f72b4d601 100644 --- a/test/julia/test_pyjulia.py +++ b/test/julia/test_pyjulia.py @@ -1,8 +1,11 @@ +import os + import numpy as np from pypesto import Problem, optimize from pypesto.engine import MultiProcessEngine, SingleCoreEngine from pypesto.objective.julia import JuliaObjective, display_source_ipython +from pypesto.objective.julia.petab_jl_importer import PetabJlImporter # The pyjulia wrapper appears to ignore global noqas, thus per line here @@ -13,13 +16,14 @@ def test_pyjulia_pipeline(): rng = np.random.default_rng(42) assert display_source_ipython( # noqa: S101 - "doc/example/model_julia/LR.jl" + f"{os.path.dirname(__file__)}/../../doc/example/model_julia/LR.jl" ) # define objective obj = JuliaObjective( module="LR", - source_file="doc/example/model_julia/LR.jl", + source_file=f"{os.path.dirname(__file__)}/../../" + f"doc/example/model_julia/LR.jl", fun="fun", grad="grad", ) @@ -33,13 +37,17 @@ def test_pyjulia_pipeline(): # define problem lb, ub = [-5.0] * n_p, [5.0] * n_p - problem = Problem(obj, lb=lb, ub=ub) + # create 10 random starting points within the bounds + x_guesses = rng.uniform(lb, ub, size=(10, n_p)) + problem = Problem(obj, lb=lb, ub=ub, x_guesses=x_guesses) # optimize - result = optimize.minimize(problem, engine=SingleCoreEngine()) + result = optimize.minimize(problem, engine=SingleCoreEngine(), n_starts=10) # use parallelization - result2 = optimize.minimize(problem, engine=MultiProcessEngine()) + result2 = optimize.minimize( + problem, engine=MultiProcessEngine(), n_starts=10 + ) # check results match assert np.allclose( # noqa: S101 @@ -52,3 +60,86 @@ def test_pyjulia_pipeline(): # check with analytical value p_opt = obj.get("p_opt") assert np.allclose(result.optimize_result[0].x, p_opt) # noqa: S101 + + +def test_petabJL_interface(): + """Test the interface to PEtab.jl with provided solutions from julia.""" + model_name = "boehm_JProteomeRes2014" + examples_dir = f"{os.path.dirname(__file__)}/../../doc/example" + yaml_file = f"{examples_dir}/{model_name}/{model_name}.yaml" + + importer = PetabJlImporter.from_yaml(yaml_file) + + problem = importer.create_problem(precompile=False) + + parameters = np.genfromtxt( + f'{examples_dir}/{model_name}/Boehm_validation/Parameter.csv', + delimiter=',', + skip_header=1, + ) + + # check objective function + obj_ref = np.genfromtxt( + f'{examples_dir}/{model_name}/Boehm_validation/ObjectiveValue.csv', + delimiter=',', + skip_header=1, + ) + obj = problem.objective(parameters, sensi_orders=(0,)) + + assert np.allclose(obj, obj_ref) # noqa: S101 + + # check gradient value + grad_ref = np.genfromtxt( + f'{examples_dir}/{model_name}/Boehm_validation/Gradient.csv', + delimiter=',', + skip_header=1, + ) + grad = problem.objective(parameters, sensi_orders=(1,)) + + assert np.allclose(grad, grad_ref) # noqa: S101 + + # check hessian value + hess_ref = np.genfromtxt( + f'{examples_dir}/{model_name}/Boehm_validation/Hessian.csv', + delimiter=',', + skip_header=1, + ) + hess = problem.objective(parameters, sensi_orders=(2,)) + + assert np.allclose(hess, hess_ref) # noqa: S101 + + +def test_petabJL_from_module(): + """Test that PEtab.jl is integrated properly.""" + # create objective + module = "MyPEtabJlModule" + source_file = ( + f"{os.path.dirname(__file__)}/../../doc/" + f"example/conversion_reaction/PEtabJl_module.jl" + ) + + importer = PetabJlImporter(module=module, source_file=source_file) + + problem = importer.create_problem(precompile=False) + + # optimize with single core + optimize.minimize(problem, engine=SingleCoreEngine(), n_starts=2) + # optimize with multi core + optimize.minimize( + problem, engine=MultiProcessEngine(n_procs=1), n_starts=2 + ) + + +def test_petabJL_from_yaml(): + """Test that PEtab.jl from yaml file is running smoothly.""" + yaml_file = ( + f"{os.path.dirname(__file__)}/../../doc/" + f"example/conversion_reaction/conversion_reaction.yaml" + ) + + importer = PetabJlImporter.from_yaml(yaml_file) + + problem = importer.create_problem(precompile=False) + + # optimize with single core + optimize.minimize(problem, engine=SingleCoreEngine(), n_starts=1)