From 20e018f225c6cf8886a6b6f4db865be59ad7f215 Mon Sep 17 00:00:00 2001
From: Daniel Sali <daniel.sali@alphacruncher.com>
Date: Mon, 6 Jan 2025 20:05:08 +0100
Subject: [PATCH] Remove wait on .jls file, define DynareError

---
 examples/example1pf_bad.mod |  60 ++++++
 src/dynare/dynare.py        | 366 ++++++++++++++++++------------------
 src/dynare/errors.py        |  17 ++
 test/test_bad_model.py      |  10 +
 4 files changed, 269 insertions(+), 184 deletions(-)
 create mode 100644 examples/example1pf_bad.mod
 create mode 100644 src/dynare/errors.py
 create mode 100644 test/test_bad_model.py

diff --git a/examples/example1pf_bad.mod b/examples/example1pf_bad.mod
new file mode 100644
index 0000000..cf3a8e6
--- /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 b4888de..d0be355 100644
--- a/src/dynare/dynare.py
+++ b/src/dynare/dynare.py
@@ -1,204 +1,202 @@
 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()
-
-    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."
+    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"
+        )
+
+        # 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
+
+        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]
+
+            # 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)
 
-        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
+            # 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
+        """
         )
-    end
+        jl.seval(f'@dynare "{resolved_path}"')
 
-    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]
-
-        # 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)
-
-        # 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
+        jls_path = model.parent / model.stem / "output" / f"{model.stem}.jls"
+
+        if not jls_path.exists():
+            raise DynareError(
+                f"Model evaluation failed. No JLS file found at {jls_path}"
+            )
+
+        context = jl.seval(
+            f"""ctx = Serialization.deserialize("{jls_path.resolve()}");
+                convert_to_pycontext(ctx)"""
         )
-    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 0000000..2dfb6a2
--- /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 0000000..8f8836a
--- /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()
-- 
GitLab