diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1f99e500845b784221e3a01bb33b59d1d8839ef7..bda5761d0d80f67fc004382159644faf962a3008 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,6 +11,7 @@ stages: - build # - test - publish + - publish-test variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache" @@ -41,3 +42,12 @@ publish: rules: - if: $CI_COMMIT_TAG - if: $CI_PIPELINE_SOURCE == "release" + +publish-test: + stage: publish-test + script: + - TWINE_PASSWORD=${TEST_PYPI_TOKEN} TWINE_USERNAME=__token__ python3 -m twine upload --repository testpypi dist/* + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + dependencies: + - build diff --git a/examples/example1pf_bad.mod b/examples/example1pf_bad.mod new file mode 100644 index 0000000000000000000000000000000000000000..cf3a8e6f581cd8fa6cb77152dfa31267ede28f1f --- /dev/null +++ b/examples/example1pf_bad.mod @@ -0,0 +1,60 @@ +// cyclic reduction algorithm +var y, c, k, a, h, b; +varexo e, u; + +verbatim; +% I want these comments included in +% example1.m 1999q1 1999y +% +var = 1; +end; + +parameters beta, rho, alpha, delta, theta, psi, tau; + +alpha = 0.36; +rho = 0.95; +tau = 0.025; +beta = 0.99; +delta = 0.025; +psi = 0; +theta = 2.95; + +phi = 0.1; + +model; +c*theta*h^(1+psi)=(1-alpha)*y; +k = beta*(((exp(b)*c)/(exp(b(+1))*c(+1))) + *(exp(b(+1))*alpha*y(+1)+(1-delta)*k)); +y = exp(a)*(k(-1)^alpha)*(h^(1-alpha)); +k = exp(b)*(y-c)+(1-delta)*k(-1); +a = rho*a(-1)+tau*b(-1) + e; +b = tau*a(-1)+rho*b(-1) + u; +end; + +steady_state_model; + K_Y = beta*alpha /(1 - beta*(1 - delta)); + H_Y = K_Y^(-alpha/(1 - alpha)); + C_Y = 1 - delta*K_Y; + y = (theta*C_Y*H_Y^(1 + psi)/(1 - alpha))^(-1/(1 + psi)); + c = C_Y*y; + k = K_Y*y; + h = H_Y*y; + a = 0; + b = 0; +end; + +spooky; + +shocks; + var e; + periods 1; + values 0.01; + var u; + periods 2; + values 0.015; +end; + +perfect_foresight_setup(periods = 100); +perfect_foresight_solver; + + diff --git a/src/dynare/dynare.py b/src/dynare/dynare.py index b4888dedef9e530ed491b91eeeb4a192163891e9..bb17fab2ff7bf4bacb4e9577ce6bca87daea49c1 100644 --- a/src/dynare/dynare.py +++ b/src/dynare/dynare.py @@ -1,204 +1,198 @@ import logging -import time -import os from pathlib import Path -from juliacall import Main as jl +from juliacall import Main as jl, JuliaError from .dynare_context import Context +from .errors import DynareError logger = logging.getLogger("dynare.dynare") def dynare(model: str | Path) -> Context: - if isinstance(model, str): - model = Path(model) - jl.seval("using Serialization") - jl.seval("using Dynare") - # Double-escape '\' as julia will also interpret the string. Only relevant on Windows - resolved_path = str(model.resolve()).replace("\\", "\\\\") - jl.seval(f'@dynare "{resolved_path}"') - - jls_path = model.parent / model.stem / "output" / f"{model.stem}.jls" - - timeout = int(os.getenv("DYNARE_WAIT", 600)) - start_time = time.time() + try: + if isinstance(model, str): + model = Path(model) + jl.seval("using Serialization") + jl.seval("using Dynare") + # Double-escape '\' as julia will also interpret the string. Only relevant on Windows + resolved_path = str(model.resolve()).replace("\\", "\\\\") + + jl.seval( + "using DataFrames, Tables, PythonCall, Dynare, LinearRationalExpectations" + ) - while not jls_path.exists(): - if time.time() - start_time > timeout: - logger.error( - f"Timeout reached: {timeout} seconds. The file {jls_path.resolve()} was not created." + # Convert the Julia AxisArrayTable fields of a Context to a Pandas DataFrame with PythonCall.pytable + jl.seval( + """ + using Dynare: EstimationResults, EstimatedParameters, SymbolTable, ModFileInfo + + struct PyWork + analytical_steadystate_variables::Vector{Int} + data::Py + datafile::String + params::Vector{Float64} + residuals::Vector{Float64} + dynamic_variables::Vector{Float64} + exogenous_variables::Vector{Float64} + observed_variables::Vector{String} + Sigma_m::Matrix{Float64} + jacobian::Matrix{Float64} + qr_jacobian::Matrix{Float64} + model_has_trend::Vector{Bool} + histval::Matrix{Union{Float64,Missing}} + homotopy_setup::Vector{NamedTuple{(:name, :type, :index, :endvalue, :startvalue), Tuple{Symbol, SymbolType, Int64, Float64, Union{Float64, Missing}}}} + initval_endogenous::Matrix{Union{Float64,Missing}} + initval_exogenous::Matrix{Union{Float64,Missing}} + initval_exogenous_deterministic::Matrix{Union{Float64,Missing}} + endval_endogenous::Matrix{Union{Float64,Missing}} + endval_exogenous::Matrix{Union{Float64,Missing}} + endval_exogenous_deterministic::Matrix{Union{Float64,Missing}} + scenario::Dict{PeriodsSinceEpoch, Dict{PeriodsSinceEpoch, Dict{Symbol, Pair{Float64, Symbol}}}} + shocks::Vector{Float64} + perfect_foresight_setup::Dict{String,Any} + estimated_parameters::EstimatedParameters + end + + function convert_to_pywork(work::Work)::PyWork + # Convert the AxisArrayTable data to a Pandas DataFrame using pytable + py_data = pytable(work.data) + + return PyWork( + work.analytical_steadystate_variables, + py_data, + work.datafile, + work.params, + work.residuals, + work.dynamic_variables, + work.exogenous_variables, + work.observed_variables, + work.Sigma_m, + work.jacobian, + work.qr_jacobian, + work.model_has_trend, + work.histval, + work.homotopy_setup, + work.initval_endogenous, + work.initval_exogenous, + work.initval_exogenous_deterministic, + work.endval_endogenous, + work.endval_exogenous, + work.endval_exogenous_deterministic, + work.scenario, + work.shocks, + work.perfect_foresight_setup, + work.estimated_parameters ) - raise TimeoutError( - f"Timeout reached: {timeout} seconds. The file {jls_path.resolve()} was not created." + end + + struct PySimulation + firstperiod::PeriodsSinceEpoch + lastperiod::PeriodsSinceEpoch + name::String + statement::String + data::Py + end + + function convert_to_pysimulation(simulation::Simulation)::PySimulation + # Convert the AxisArrayTable data to a Pandas DataFrame using pytable + py_data = pytable(simulation.data) + + return PySimulation( + simulation.firstperiod, + simulation.lastperiod, + simulation.name, + simulation.statement, + py_data ) - logger.debug(f"Waiting for the file {jls_path.resolve()} to be created") - time.sleep(1) + end - jl.seval("using DataFrames, Tables, PythonCall, Dynare, LinearRationalExpectations") + + # Define the PyModelResult structure with Pandas DataFrame fields + mutable struct PyModelResult + irfs::Dict{Symbol, Py} + trends::Trends + stationary_variables::Vector{Bool} + estimation::EstimationResults + filter::Py # Pandas DataFrame + forecast::Vector{Py} # Vector of Pandas DataFrames + initial_smoother::Py # Pandas DataFrame + linearrationalexpectations::LinearRationalExpectationsResults + simulations::Vector{PySimulation} + smoother::Py # Pandas DataFrame + solution_derivatives::Vector{Matrix{Float64}} + # sparsegrids::SparsegridsResults + end - # Convert the Julia AxisArrayTable fields of a Context to a Pandas DataFrame with PythonCall.pytable - jl.seval( - """ - using Dynare: EstimationResults, EstimatedParameters, SymbolTable, ModFileInfo - - struct PyWork - analytical_steadystate_variables::Vector{Int} - data::Py - datafile::String - params::Vector{Float64} - residuals::Vector{Float64} - dynamic_variables::Vector{Float64} - exogenous_variables::Vector{Float64} - observed_variables::Vector{String} - Sigma_m::Matrix{Float64} - jacobian::Matrix{Float64} - qr_jacobian::Matrix{Float64} - model_has_trend::Vector{Bool} - histval::Matrix{Union{Float64,Missing}} - homotopy_setup::Vector{NamedTuple{(:name, :type, :index, :endvalue, :startvalue), Tuple{Symbol, SymbolType, Int64, Float64, Union{Float64, Missing}}}} - initval_endogenous::Matrix{Union{Float64,Missing}} - initval_exogenous::Matrix{Union{Float64,Missing}} - initval_exogenous_deterministic::Matrix{Union{Float64,Missing}} - endval_endogenous::Matrix{Union{Float64,Missing}} - endval_exogenous::Matrix{Union{Float64,Missing}} - endval_exogenous_deterministic::Matrix{Union{Float64,Missing}} - scenario::Dict{PeriodsSinceEpoch, Dict{PeriodsSinceEpoch, Dict{Symbol, Pair{Float64, Symbol}}}} - shocks::Vector{Float64} - perfect_foresight_setup::Dict{String,Any} - estimated_parameters::EstimatedParameters - end - - function convert_to_pywork(work::Work)::PyWork - # Convert the AxisArrayTable data to a Pandas DataFrame using pytable - py_data = pytable(work.data) - - return PyWork( - work.analytical_steadystate_variables, - py_data, - work.datafile, - work.params, - work.residuals, - work.dynamic_variables, - work.exogenous_variables, - work.observed_variables, - work.Sigma_m, - work.jacobian, - work.qr_jacobian, - work.model_has_trend, - work.histval, - work.homotopy_setup, - work.initval_endogenous, - work.initval_exogenous, - work.initval_exogenous_deterministic, - work.endval_endogenous, - work.endval_exogenous, - work.endval_exogenous_deterministic, - work.scenario, - work.shocks, - work.perfect_foresight_setup, - work.estimated_parameters - ) - end - - struct PySimulation - firstperiod::PeriodsSinceEpoch - lastperiod::PeriodsSinceEpoch - name::String - statement::String - data::Py - end - - function convert_to_pysimulation(simulation::Simulation)::PySimulation - # Convert the AxisArrayTable data to a Pandas DataFrame using pytable - py_data = pytable(simulation.data) - - return PySimulation( - simulation.firstperiod, - simulation.lastperiod, - simulation.name, - simulation.statement, - py_data - ) - end + function convert_to_pymodelresult(model_result::ModelResults)::PyModelResult + py_irfs = Dict{Symbol, Py}() + for (key, axis_array_table) in model_result.irfs + py_irfs[key] = pytable(axis_array_table) + end + py_forecast = [pytable(forecast) for forecast in model_result.forecast] - # Define the PyModelResult structure with Pandas DataFrame fields - mutable struct PyModelResult - irfs::Dict{Symbol, Py} - trends::Trends - stationary_variables::Vector{Bool} - estimation::EstimationResults - filter::Py # Pandas DataFrame - forecast::Vector{Py} # Vector of Pandas DataFrames - initial_smoother::Py # Pandas DataFrame - linearrationalexpectations::LinearRationalExpectationsResults - simulations::Vector{PySimulation} - smoother::Py # Pandas DataFrame - solution_derivatives::Vector{Matrix{Float64}} - # sparsegrids::SparsegridsResults - end - - function convert_to_pymodelresult(model_result::ModelResults)::PyModelResult - py_irfs = Dict{Symbol, Py}() - for (key, axis_array_table) in model_result.irfs - py_irfs[key] = pytable(axis_array_table) + return PyModelResult( + py_irfs, + model_result.trends, + model_result.stationary_variables, + model_result.estimation, + pytable(model_result.filter), + py_forecast, + pytable(model_result.initial_smoother), + model_result.linearrationalexpectations, + [convert_to_pysimulation(simulation) for simulation in model_result.simulations], + pytable(model_result.smoother), + model_result.solution_derivatives, + # model_result.sparsegrids + ) end - py_forecast = [pytable(forecast) for forecast in model_result.forecast] - - return PyModelResult( - py_irfs, - model_result.trends, - model_result.stationary_variables, - model_result.estimation, - pytable(model_result.filter), - py_forecast, - pytable(model_result.initial_smoother), - model_result.linearrationalexpectations, - [convert_to_pysimulation(simulation) for simulation in model_result.simulations], - pytable(model_result.smoother), - model_result.solution_derivatives, - # model_result.sparsegrids - ) - end - - struct PyResults - model_results::Vector{PyModelResult} - end - - struct PyContext - symboltable::SymbolTable - models::Vector{Model} - modfileinfo::ModFileInfo - results::PyResults # Now holds PyModelResult instead of ModelResult - work::PyWork - workspaces::Dict - end + struct PyResults + model_results::Vector{PyModelResult} + end + + struct PyContext + symboltable::SymbolTable + models::Vector{Model} + modfileinfo::ModFileInfo + results::PyResults # Now holds PyModelResult instead of ModelResult + work::PyWork + workspaces::Dict + end - function convert_to_pycontext(ctx::Context)::PyContext - # Convert each ModelResults in the Context to PyModelResult - py_model_results = [convert_to_pymodelresult(model_result) for model_result in ctx.results.model_results] + function convert_to_pycontext(ctx::Context)::PyContext + # Convert each ModelResults in the Context to PyModelResult + py_model_results = [convert_to_pymodelresult(model_result) for model_result in ctx.results.model_results] - # Create a PyResults structure with the converted PyModelResults - py_results = PyResults(py_model_results) + # Create a PyResults structure with the converted PyModelResults + py_results = PyResults(py_model_results) - # Convert the Work structure - py_work = convert_to_pywork(ctx.work) + # Convert the Work structure + py_work = convert_to_pywork(ctx.work) - # Return a new PyContext with PyResults and PyWork - return PyContext( - ctx.symboltable, - ctx.models, - ctx.modfileinfo, - py_results, # PyResults instead of Results - py_work, - ctx.workspaces + # Return a new PyContext with PyResults and PyWork + return PyContext( + ctx.symboltable, + ctx.models, + ctx.modfileinfo, + py_results, # PyResults instead of Results + py_work, + ctx.workspaces + ) + end + """ + ) + context = jl.seval( + f"""ctx = @dynare "{resolved_path}"; + if !(ctx isa Dynare.Context) + throw(error("Failed to produce a Dynare context.")) + else + convert_to_pycontext(ctx) + end + """ ) - end - """ - ) - context = jl.seval( - f"""ctx = Serialization.deserialize("{jls_path.resolve()}"); - convert_to_pycontext(ctx)""" - ) - return Context.from_julia(context) + return Context.from_julia(context) + except JuliaError as e: + raise DynareError.from_julia_error(e) + except Exception as e: + raise DynareError(f"An unexpected error occurred: {e}") from e diff --git a/src/dynare/errors.py b/src/dynare/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..2dfb6a2c228603fa619ac013d1936f07c95faaab --- /dev/null +++ b/src/dynare/errors.py @@ -0,0 +1,17 @@ +from typing import Self + + +class DynareError(Exception): + """Exception raised for errors occurring during the execution of the dynare Julia command.""" + + def __init__(self, message): + self.message = message + super().__init__(self.message) + + def __str__(self): + return f"DynareError: {self.message}" + + @classmethod + def from_julia_error(cls, julia_error) -> Self: + message = f"JuliaError:\n{str(julia_error)}" + return cls(message) diff --git a/test/test_bad_model.py b/test/test_bad_model.py new file mode 100644 index 0000000000000000000000000000000000000000..8f8836aa4dd109dc3a035f42816a97e7a17c26b8 --- /dev/null +++ b/test/test_bad_model.py @@ -0,0 +1,10 @@ +from pathlib import Path +from dynare import dynare + + +def test_dynare(): + print(dynare(Path(__file__).parent.parent / "examples" / "example1pf_bad.mod")) + + +if __name__ == "__main__": + test_dynare()