fue 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fue/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ fue — Python interface to the FUE exact maximum likelihood estimation engine.
3
+
4
+ Typical usage::
5
+
6
+ import fue
7
+
8
+ ts = fue.TimeSeries.from_array(data, freq=12, start=(1990, 1))
9
+
10
+ m = fue.Model(ts, ar=[[1]], ma=[[1]], d=1, D=1)
11
+ m.fit()
12
+ print(m.summary())
13
+
14
+ m.plot_residuals()
15
+ m.residuals.plot_acf()
16
+ """
17
+
18
+ from .series import TimeSeries
19
+ from .intervention import Intervention
20
+ from .model import Model, FixedFreqFactor
21
+ from .forecast import ForecastResult
22
+ from .diagnostics import acf, pacf, jarque_bera, ljung_box
23
+ from .inp import load, load_fuf
24
+ from .report import write_out, write_fuf, write_fuf_out
25
+ from .report_forecast import write_forecast_report
26
+ from . import datasets
27
+
28
+ __version__ = "0.1.0"
29
+ __all__ = ["TimeSeries", "Intervention", "Model", "FixedFreqFactor",
30
+ "ForecastResult", "acf", "pacf", "jarque_bera", "ljung_box",
31
+ "load", "load_fuf", "write_out", "write_fuf", "write_fuf_out",
32
+ "write_forecast_report", "datasets"]
fue/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
fue/_build_cffi.py ADDED
@@ -0,0 +1,174 @@
1
+ """
2
+ cffi build script for the FUE estimation engine.
3
+
4
+ Activated in pyproject.toml via:
5
+ cffi_modules = ["src/fue/_build_cffi.py:ffi"]
6
+
7
+ This file is run at build time (pip install / python setup.py build_ext).
8
+ It compiles csrc/fue_api.c + csrc/internal/*.c against GSL and produces
9
+ the _fue_engine extension module imported by _engine.py at runtime.
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import cffi
15
+
16
+ # Compute paths relative to the project root (CWD during pip/setup.py build).
17
+ # os.path.relpath normalises away the ".." components that confuse distutils.
18
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
19
+ _ROOT = os.path.relpath(os.path.join(_THIS_DIR, "..", "..", "csrc"))
20
+ _INTERN = os.path.join(_ROOT, "internal")
21
+
22
+ # cffi cannot process C preprocessor directives (#define, #ifdef, etc.).
23
+ # We provide a macro-expanded cdef string with literal values instead.
24
+ # Macro values: FUE_MAX_DETVARS=64, FUE_MAX_FACTORS=8, FUE_MAX_POLYORD=16.
25
+ _CDEF = """
26
+ typedef struct {
27
+ int type;
28
+ int obs_index;
29
+ double harmonic;
30
+
31
+ int nomega;
32
+ double omega[16];
33
+ int omega_free[16];
34
+
35
+ int ndelta;
36
+ double delta[16];
37
+ int delta_free[16];
38
+
39
+ double *indicator_data;
40
+ } FueIntervention;
41
+
42
+ typedef struct {
43
+ int order;
44
+ double coefs[16];
45
+ int coef_free[16];
46
+ } FueFactor;
47
+
48
+ typedef struct {
49
+ int nobs;
50
+ double *data;
51
+ int sper;
52
+ int numbering;
53
+ int begyear;
54
+ int begtime;
55
+
56
+ double boxlam;
57
+ double refactor;
58
+
59
+ int nrdiff;
60
+ int nadiff;
61
+
62
+ double mu0;
63
+ int estimate_mu;
64
+
65
+ int ninterventions;
66
+ FueIntervention interventions[64];
67
+
68
+ int nar1;
69
+ FueFactor ar1[8];
70
+
71
+ int nar2;
72
+ FueFactor ar2[8];
73
+
74
+ int nma1;
75
+ FueFactor ma1[8];
76
+
77
+ int nma2;
78
+ FueFactor ma2[8];
79
+
80
+ int nar1f;
81
+ double ar1f_freq[8];
82
+ double ar1f_coef[8];
83
+ int ar1f_free[8];
84
+
85
+ int nma1f;
86
+ double ma1f_freq[8];
87
+ double ma1f_coef[8];
88
+ int ma1f_free[8];
89
+
90
+ int ifadf[8];
91
+
92
+ int maxits;
93
+ double grtol;
94
+ double sptol;
95
+ double xitol;
96
+ int chkma;
97
+ int eml;
98
+ } FueModelSpec;
99
+
100
+ typedef struct {
101
+ int ifault;
102
+ int npar;
103
+ double *params;
104
+ double *std_errors;
105
+ double *cov_matrix;
106
+ double *residuals;
107
+ int nresiduals;
108
+ double sigma2;
109
+ double loglik;
110
+ double aic;
111
+ double bic;
112
+ } FueResult;
113
+
114
+ FueResult *fue_estimate(const FueModelSpec *spec);
115
+ void fue_defaults(FueModelSpec *spec);
116
+ void fue_result_free(FueResult *r);
117
+ const char *fue_strerror(int ifault);
118
+ """
119
+
120
+ _SOURCES = [
121
+ os.path.join(_ROOT, "fue_api.c"),
122
+ os.path.join(_INTERN, "elfvarma.c"),
123
+ os.path.join(_INTERN, "usmelard.c"),
124
+ os.path.join(_INTERN, "qnewtopt.c"),
125
+ os.path.join(_INTERN, "nlatools.c"),
126
+ os.path.join(_INTERN, "drvmlest.c"),
127
+ ]
128
+
129
+ if sys.platform == "win32":
130
+ _libraries = ["gsl", "gslcblas"] # no -lm on Windows
131
+ _compile_args = ["/O2", "/W2"]
132
+ # conda-build sets LIBRARY_INC / LIBRARY_LIB; prefer those when present.
133
+ _lib_inc = os.environ.get("LIBRARY_INC")
134
+ _lib_lib = os.environ.get("LIBRARY_LIB")
135
+ if _lib_inc and _lib_lib:
136
+ _include_dirs = [_ROOT, _INTERN, _lib_inc]
137
+ _library_dirs = [_lib_lib]
138
+ else:
139
+ # GSL via vcpkg (x64-windows-static-md triplet) — used by cibuildwheel.
140
+ # Set GSL_ROOT to override the default vcpkg install prefix.
141
+ _vcpkg_default = os.path.join(
142
+ os.environ.get("VCPKG_INSTALLATION_ROOT", r"C:\vcpkg"),
143
+ "installed", "x64-windows-static-md",
144
+ )
145
+ _gsl_root = os.environ.get("GSL_ROOT", _vcpkg_default)
146
+ _include_dirs = [_ROOT, _INTERN, os.path.join(_gsl_root, "include")]
147
+ _library_dirs = [os.path.join(_gsl_root, "lib")]
148
+ else:
149
+ _libraries = ["gsl", "gslcblas", "m"]
150
+ _compile_args = ["-O2", "-std=c99", "-Wall"]
151
+ # conda-build sets PREFIX to the environment where host deps are installed.
152
+ _prefix = os.environ.get("PREFIX")
153
+ if _prefix:
154
+ _include_dirs = [_ROOT, _INTERN, os.path.join(_prefix, "include")]
155
+ _library_dirs = [os.path.join(_prefix, "lib")]
156
+ else:
157
+ _include_dirs = [_ROOT, _INTERN]
158
+ _library_dirs = []
159
+
160
+ ffi = cffi.FFI()
161
+ ffi.cdef(_CDEF)
162
+
163
+ ffi.set_source(
164
+ "fue._fue_engine",
165
+ r'#include "fue_api.h"',
166
+ sources=_SOURCES,
167
+ include_dirs=_include_dirs,
168
+ library_dirs=_library_dirs,
169
+ libraries=_libraries,
170
+ extra_compile_args=_compile_args,
171
+ )
172
+
173
+ if __name__ == "__main__":
174
+ ffi.compile(verbose=True)
fue/_engine.py ADDED
@@ -0,0 +1,130 @@
1
+ """
2
+ Bridge between the Python Model API and the cffi-compiled C extension.
3
+
4
+ Falls back to the pure-Python estimator (cast_us.estimate_py) when the
5
+ C extension (_fue_engine) is not available.
6
+ """
7
+
8
+ import numpy as np
9
+
10
+
11
+ def estimate(model):
12
+ """
13
+ Estimate model parameters by exact ML.
14
+
15
+ Tries the C extension first; falls back to the pure-Python estimator
16
+ (scipy L-BFGS-B + elf_scalar) when the extension is not compiled.
17
+ """
18
+ try:
19
+ from fue._fue_engine import ffi, lib
20
+ except ImportError:
21
+ from .cast_us import estimate_py
22
+ return estimate_py(model)
23
+
24
+ spec = ffi.new("FueModelSpec *")
25
+ lib.fue_defaults(spec)
26
+
27
+ ts = model.series
28
+
29
+ # Keep the numpy array alive via ffi.from_buffer so spec.data stays valid.
30
+ _data = np.ascontiguousarray(ts.data, dtype=np.float64)
31
+ _data_buf = ffi.from_buffer("double[]", _data)
32
+
33
+ spec.nobs = ts.nobs
34
+ spec.data = _data_buf
35
+ spec.sper = ts.freq if ts.freq > 0 else 1
36
+ spec.numbering = 1 if ts.numbering else 0
37
+ spec.begyear = ts.start[0]
38
+ spec.begtime = ts.start[1]
39
+
40
+ spec.boxlam = model.boxlam
41
+ spec.refactor = model.refactor
42
+ spec.nrdiff = model.d
43
+ spec.nadiff = model.D
44
+ spec.mu0 = model.mu0
45
+ spec.estimate_mu = 1 if model.estimate_mu else 0
46
+ spec.chkma = 1 if model.chkma else 0
47
+ spec.eml = 1 if model.eml else 0
48
+
49
+ def _fill_factors(spec_arr, factors, free_lists):
50
+ for i, factor in enumerate(factors):
51
+ spec_arr[i].order = len(factor)
52
+ free = free_lists[i] if free_lists is not None else None
53
+ for j, v in enumerate(factor):
54
+ spec_arr[i].coefs[j] = v
55
+ spec_arr[i].coef_free[j] = (
56
+ 0 if (free is not None and not free[j]) else 1
57
+ )
58
+
59
+ spec.nar1 = len(model.ar); _fill_factors(spec.ar1, model.ar, model.ar_free)
60
+ spec.nma1 = len(model.ma); _fill_factors(spec.ma1, model.ma, model.ma_free)
61
+ spec.nar2 = len(model.ar_s); _fill_factors(spec.ar2, model.ar_s, model.ar_s_free)
62
+ spec.nma2 = len(model.ma_s); _fill_factors(spec.ma2, model.ma_s, model.ma_s_free)
63
+
64
+ for i, v in enumerate(model.ifadf[:8]):
65
+ spec.ifadf[i] = 1 if v else 0
66
+
67
+ spec.nar1f = len(model.ar_f)
68
+ for i, ff in enumerate(model.ar_f):
69
+ spec.ar1f_freq[i] = ff.freq
70
+ spec.ar1f_coef[i] = ff.coef
71
+ spec.ar1f_free[i] = 1 if ff.free else 0
72
+
73
+ spec.nma1f = len(model.ma_f)
74
+ for i, ff in enumerate(model.ma_f):
75
+ spec.ma1f_freq[i] = ff.freq
76
+ spec.ma1f_coef[i] = ff.coef
77
+ spec.ma1f_free[i] = 1 if ff.free else 0
78
+
79
+ spec.ninterventions = len(model.interventions)
80
+ # Custom indicator buffers must stay alive until fue_estimate returns.
81
+ _custom_bufs = []
82
+ for i, itv in enumerate(model.interventions):
83
+ spec.interventions[i].type = itv.type_code
84
+ spec.interventions[i].obs_index = itv.at
85
+ spec.interventions[i].harmonic = itv.harmonic
86
+ spec.interventions[i].nomega = len(itv.omega)
87
+ for j, v in enumerate(itv.omega):
88
+ spec.interventions[i].omega[j] = v
89
+ spec.interventions[i].omega_free[j] = 1 if itv.omega_free[j] else 0
90
+ spec.interventions[i].ndelta = len(itv.delta)
91
+ for j, v in enumerate(itv.delta):
92
+ spec.interventions[i].delta[j] = v
93
+ spec.interventions[i].delta_free[j] = 1 if itv.delta_free[j] else 0
94
+ if itv.type == "custom" and itv.data is not None:
95
+ _arr = np.ascontiguousarray(itv.data, dtype=np.float64)
96
+ _buf = ffi.from_buffer("double[]", _arr)
97
+ spec.interventions[i].indicator_data = _buf
98
+ _custom_bufs.append((_arr, _buf))
99
+ else:
100
+ spec.interventions[i].indicator_data = ffi.NULL
101
+
102
+ # _data, _data_buf, and _custom_bufs remain alive until fue_estimate returns.
103
+ raw = lib.fue_estimate(spec)
104
+ if raw == ffi.NULL:
105
+ return {'ifault': -1, 'npar': 0, 'nresiduals': 0,
106
+ 'sigma2': 0.0, 'loglik': 0.0, 'aic': 0.0, 'bic': 0.0,
107
+ 'params': np.array([]), 'std_errors': np.array([]),
108
+ 'cov_matrix': np.zeros((0, 0)), 'residuals': np.array([])}
109
+
110
+ n = raw.npar
111
+ nr = raw.nresiduals
112
+ try:
113
+ data = {
114
+ 'ifault': raw.ifault,
115
+ 'npar': n,
116
+ 'nresiduals': nr,
117
+ 'sigma2': raw.sigma2,
118
+ 'loglik': raw.loglik,
119
+ 'aic': raw.aic,
120
+ 'bic': raw.bic,
121
+ 'params': np.array([raw.params[i] for i in range(n)], dtype=float),
122
+ 'std_errors': np.array([raw.std_errors[i] for i in range(n)], dtype=float),
123
+ 'cov_matrix': np.array([raw.cov_matrix[i] for i in range(n * n)],
124
+ dtype=float).reshape(n, n) if n > 0 else np.zeros((0, 0)),
125
+ 'residuals': np.array([raw.residuals[i] for i in range(nr)], dtype=float),
126
+ }
127
+ finally:
128
+ lib.fue_result_free(raw)
129
+
130
+ return data