pybhatlib 0.1.0__tar.gz

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.
Files changed (124) hide show
  1. pybhatlib-0.1.0/.gitignore +49 -0
  2. pybhatlib-0.1.0/CLAUDE.md +42 -0
  3. pybhatlib-0.1.0/IMPLEMENTATION_PLAN.md +497 -0
  4. pybhatlib-0.1.0/LICENSE +21 -0
  5. pybhatlib-0.1.0/PKG-INFO +103 -0
  6. pybhatlib-0.1.0/README.md +65 -0
  7. pybhatlib-0.1.0/examples/data/TRAVELMODE.csv +1126 -0
  8. pybhatlib-0.1.0/examples/data/generate_travelmode.py +248 -0
  9. pybhatlib-0.1.0/examples/mnp_ate_analysis.py +95 -0
  10. pybhatlib-0.1.0/examples/mnp_flexible_cov.py +46 -0
  11. pybhatlib-0.1.0/examples/mnp_iid.py +45 -0
  12. pybhatlib-0.1.0/examples/mnp_random_coefficients.py +54 -0
  13. pybhatlib-0.1.0/examples/morp_example.py +171 -0
  14. pybhatlib-0.1.0/examples/tutorials/_convert_to_ipynb.py +224 -0
  15. pybhatlib-0.1.0/examples/tutorials/python_scripts/t00_quickstart.py +129 -0
  16. pybhatlib-0.1.0/examples/tutorials/python_scripts/t01a_vectorization.py +142 -0
  17. pybhatlib-0.1.0/examples/tutorials/python_scripts/t01b_ldlt.py +130 -0
  18. pybhatlib-0.1.0/examples/tutorials/python_scripts/t01c_truncated_mvn.py +115 -0
  19. pybhatlib-0.1.0/examples/tutorials/python_scripts/t02a_gradcovcor.py +151 -0
  20. pybhatlib-0.1.0/examples/tutorials/python_scripts/t02b_spherical.py +141 -0
  21. pybhatlib-0.1.0/examples/tutorials/python_scripts/t02c_chain_rules.py +174 -0
  22. pybhatlib-0.1.0/examples/tutorials/python_scripts/t03a_mvncd_methods.py +131 -0
  23. pybhatlib-0.1.0/examples/tutorials/python_scripts/t03b_mvncd_gradients.py +146 -0
  24. pybhatlib-0.1.0/examples/tutorials/python_scripts/t03c_mvncd_rect.py +156 -0
  25. pybhatlib-0.1.0/examples/tutorials/python_scripts/t03d_univariate_cdfs.py +160 -0
  26. pybhatlib-0.1.0/examples/tutorials/python_scripts/t04c_mnp_heteronly.py +154 -0
  27. pybhatlib-0.1.0/examples/tutorials/python_scripts/t04f_mnp_control_options.py +189 -0
  28. pybhatlib-0.1.0/examples/tutorials/python_scripts/t04g_mnp_forecasting.py +164 -0
  29. pybhatlib-0.1.0/examples/tutorials/python_scripts/t05b_morp_ate_predict.py +171 -0
  30. pybhatlib-0.1.0/examples/tutorials/python_scripts/t06a_backend_switching.py +139 -0
  31. pybhatlib-0.1.0/examples/tutorials/python_scripts/t06b_custom_specs.py +222 -0
  32. pybhatlib-0.1.0/examples/tutorials/python_scripts/t06c_gradient_verification.py +269 -0
  33. pybhatlib-0.1.0/examples/tutorials/t00_quickstart.ipynb +115 -0
  34. pybhatlib-0.1.0/examples/tutorials/t01a_vectorization.ipynb +131 -0
  35. pybhatlib-0.1.0/examples/tutorials/t01b_ldlt.ipynb +99 -0
  36. pybhatlib-0.1.0/examples/tutorials/t01c_truncated_mvn.ipynb +99 -0
  37. pybhatlib-0.1.0/examples/tutorials/t02a_gradcovcor.ipynb +115 -0
  38. pybhatlib-0.1.0/examples/tutorials/t02b_spherical.ipynb +115 -0
  39. pybhatlib-0.1.0/examples/tutorials/t02c_chain_rules.ipynb +115 -0
  40. pybhatlib-0.1.0/examples/tutorials/t03a_mvncd_methods.ipynb +99 -0
  41. pybhatlib-0.1.0/examples/tutorials/t03b_mvncd_gradients.ipynb +115 -0
  42. pybhatlib-0.1.0/examples/tutorials/t03c_mvncd_rect.ipynb +99 -0
  43. pybhatlib-0.1.0/examples/tutorials/t03d_univariate_cdfs.ipynb +115 -0
  44. pybhatlib-0.1.0/examples/tutorials/t04c_mnp_heteronly.ipynb +115 -0
  45. pybhatlib-0.1.0/examples/tutorials/t04f_mnp_control_options.ipynb +131 -0
  46. pybhatlib-0.1.0/examples/tutorials/t04g_mnp_forecasting.ipynb +115 -0
  47. pybhatlib-0.1.0/examples/tutorials/t05b_morp_ate_predict.ipynb +115 -0
  48. pybhatlib-0.1.0/examples/tutorials/t06a_backend_switching.ipynb +115 -0
  49. pybhatlib-0.1.0/examples/tutorials/t06b_custom_specs.ipynb +131 -0
  50. pybhatlib-0.1.0/examples/tutorials/t06c_gradient_verification.ipynb +131 -0
  51. pybhatlib-0.1.0/pyproject.toml +69 -0
  52. pybhatlib-0.1.0/src/pybhatlib/__init__.py +5 -0
  53. pybhatlib-0.1.0/src/pybhatlib/_version.py +1 -0
  54. pybhatlib-0.1.0/src/pybhatlib/backend/__init__.py +9 -0
  55. pybhatlib-0.1.0/src/pybhatlib/backend/_array_api.py +84 -0
  56. pybhatlib-0.1.0/src/pybhatlib/backend/_numpy_backend.py +263 -0
  57. pybhatlib-0.1.0/src/pybhatlib/backend/_torch_backend.py +257 -0
  58. pybhatlib-0.1.0/src/pybhatlib/gradmvn/__init__.py +24 -0
  59. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_bivariate_trunc.py +167 -0
  60. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_mvncd.py +826 -0
  61. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_mvncd_grad.py +145 -0
  62. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_mvncd_ssj.py +117 -0
  63. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_other_dists.py +169 -0
  64. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_partial_cdf.py +117 -0
  65. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_truncated.py +268 -0
  66. pybhatlib-0.1.0/src/pybhatlib/gradmvn/_univariate.py +193 -0
  67. pybhatlib-0.1.0/src/pybhatlib/io/__init__.py +6 -0
  68. pybhatlib-0.1.0/src/pybhatlib/io/_data_loader.py +44 -0
  69. pybhatlib-0.1.0/src/pybhatlib/io/_spec_parser.py +166 -0
  70. pybhatlib-0.1.0/src/pybhatlib/matgradient/__init__.py +15 -0
  71. pybhatlib-0.1.0/src/pybhatlib/matgradient/_chain_rules.py +91 -0
  72. pybhatlib-0.1.0/src/pybhatlib/matgradient/_gomegxomegax.py +83 -0
  73. pybhatlib-0.1.0/src/pybhatlib/matgradient/_gradcovcor.py +123 -0
  74. pybhatlib-0.1.0/src/pybhatlib/matgradient/_spherical.py +190 -0
  75. pybhatlib-0.1.0/src/pybhatlib/models/__init__.py +1 -0
  76. pybhatlib-0.1.0/src/pybhatlib/models/_base.py +14 -0
  77. pybhatlib-0.1.0/src/pybhatlib/models/mnp/__init__.py +14 -0
  78. pybhatlib-0.1.0/src/pybhatlib/models/mnp/_mnp_ate.py +193 -0
  79. pybhatlib-0.1.0/src/pybhatlib/models/mnp/_mnp_control.py +88 -0
  80. pybhatlib-0.1.0/src/pybhatlib/models/mnp/_mnp_forecast.py +119 -0
  81. pybhatlib-0.1.0/src/pybhatlib/models/mnp/_mnp_loglik.py +565 -0
  82. pybhatlib-0.1.0/src/pybhatlib/models/mnp/_mnp_model.py +419 -0
  83. pybhatlib-0.1.0/src/pybhatlib/models/mnp/_mnp_results.py +181 -0
  84. pybhatlib-0.1.0/src/pybhatlib/models/morp/__init__.py +17 -0
  85. pybhatlib-0.1.0/src/pybhatlib/models/morp/_morp_ate.py +119 -0
  86. pybhatlib-0.1.0/src/pybhatlib/models/morp/_morp_control.py +56 -0
  87. pybhatlib-0.1.0/src/pybhatlib/models/morp/_morp_forecast.py +121 -0
  88. pybhatlib-0.1.0/src/pybhatlib/models/morp/_morp_loglik.py +256 -0
  89. pybhatlib-0.1.0/src/pybhatlib/models/morp/_morp_model.py +276 -0
  90. pybhatlib-0.1.0/src/pybhatlib/models/morp/_morp_results.py +154 -0
  91. pybhatlib-0.1.0/src/pybhatlib/optim/__init__.py +5 -0
  92. pybhatlib-0.1.0/src/pybhatlib/optim/_convergence.py +56 -0
  93. pybhatlib-0.1.0/src/pybhatlib/optim/_scipy_optim.py +156 -0
  94. pybhatlib-0.1.0/src/pybhatlib/optim/_torch_optim.py +140 -0
  95. pybhatlib-0.1.0/src/pybhatlib/utils/__init__.py +6 -0
  96. pybhatlib-0.1.0/src/pybhatlib/utils/_qmc.py +52 -0
  97. pybhatlib-0.1.0/src/pybhatlib/utils/_seeds.py +23 -0
  98. pybhatlib-0.1.0/src/pybhatlib/utils/_validation.py +34 -0
  99. pybhatlib-0.1.0/src/pybhatlib/vecup/__init__.py +24 -0
  100. pybhatlib-0.1.0/src/pybhatlib/vecup/_ldlt.py +160 -0
  101. pybhatlib-0.1.0/src/pybhatlib/vecup/_mask.py +74 -0
  102. pybhatlib-0.1.0/src/pybhatlib/vecup/_nondiag.py +42 -0
  103. pybhatlib-0.1.0/src/pybhatlib/vecup/_truncnorm.py +153 -0
  104. pybhatlib-0.1.0/src/pybhatlib/vecup/_vec_ops.py +196 -0
  105. pybhatlib-0.1.0/tests/__init__.py +1 -0
  106. pybhatlib-0.1.0/tests/conftest.py +99 -0
  107. pybhatlib-0.1.0/tests/test_backend/__init__.py +1 -0
  108. pybhatlib-0.1.0/tests/test_backend/test_array_api.py +75 -0
  109. pybhatlib-0.1.0/tests/test_gradmvn/__init__.py +1 -0
  110. pybhatlib-0.1.0/tests/test_gradmvn/test_bivariate_trunc.py +91 -0
  111. pybhatlib-0.1.0/tests/test_gradmvn/test_mvncd.py +66 -0
  112. pybhatlib-0.1.0/tests/test_gradmvn/test_mvncd_methods.py +129 -0
  113. pybhatlib-0.1.0/tests/test_gradmvn/test_mvncd_rect.py +100 -0
  114. pybhatlib-0.1.0/tests/test_gradmvn/test_other_dists.py +59 -0
  115. pybhatlib-0.1.0/tests/test_integration/__init__.py +1 -0
  116. pybhatlib-0.1.0/tests/test_matgradient/__init__.py +1 -0
  117. pybhatlib-0.1.0/tests/test_matgradient/test_gradcovcor.py +49 -0
  118. pybhatlib-0.1.0/tests/test_models/__init__.py +1 -0
  119. pybhatlib-0.1.0/tests/test_models/test_mnp_control.py +39 -0
  120. pybhatlib-0.1.0/tests/test_models/test_morp.py +210 -0
  121. pybhatlib-0.1.0/tests/test_vecup/__init__.py +1 -0
  122. pybhatlib-0.1.0/tests/test_vecup/test_ldlt.py +64 -0
  123. pybhatlib-0.1.0/tests/test_vecup/test_ldlt_rank2.py +73 -0
  124. pybhatlib-0.1.0/tests/test_vecup/test_vec_ops.py +100 -0
@@ -0,0 +1,49 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ *.egg
10
+
11
+ # Virtual environments
12
+ .venv/
13
+ venv/
14
+ env/
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+
22
+ # Testing
23
+ .pytest_cache/
24
+ .coverage
25
+ htmlcov/
26
+
27
+ # Data & results (large files)
28
+ *.dat
29
+ *.pkl
30
+ *.pdf
31
+ results/
32
+
33
+ # OS
34
+ .DS_Store
35
+ Thumbs.db
36
+ nul
37
+
38
+ # LaTeX build artifacts
39
+ *.aux
40
+ *.log
41
+ *.nav
42
+ *.snm
43
+ *.toc
44
+ *.out
45
+ *.bbl
46
+ *.blg
47
+ *.fdb_latexmk
48
+ *.fls
49
+ *.synctex.gz
@@ -0,0 +1,42 @@
1
+ # pybhatlib
2
+
3
+ Python reimplementation of BHATLIB (Bhat, Clower, Haddad, Jones) for statistical
4
+ and econometric matrix-based inference methods.
5
+
6
+ ## Build & Test
7
+
8
+ ```bash
9
+ pip install -e ".[dev]" # Install in editable mode with dev deps
10
+ pip install -e ".[torch]" # Optional PyTorch backend
11
+ pytest tests/ # Run all tests
12
+ pytest tests/ -m "not slow" # Skip integration tests
13
+ pytest tests/ -m torch # Only PyTorch backend tests
14
+ ```
15
+
16
+ ## Architecture
17
+
18
+ Build dependency chain: `backend → vecup → matgradient → gradmvn → optim/io → models/mnp`
19
+
20
+ - `backend/` — Array backend abstraction (NumPy default, optional PyTorch)
21
+ - `vecup/` — Low-level matrix ops (GAUSS Vecup.src): vecdup, matdupfull, LDLT, truncated MVN
22
+ - `matgradient/` — Matrix gradients (GAUSS Matgradient.src): gradcovcor, gomegxomegax, spherical param
23
+ - `gradmvn/` — Distributions & gradients (GAUSS Gradmvn.src): MVNCD (Bhat 2018), truncated, partial CDF
24
+ - `models/mnp/` — Multinomial Probit: IID, flexible covariance, random coefficients, mixture-of-normals
25
+ - `optim/` — Optimizer wrappers (scipy.optimize + optional PyTorch)
26
+ - `io/` — Data loading and spec parsing
27
+ - `utils/` — QMC sequences, seeds, validation
28
+
29
+ ## Key Conventions
30
+
31
+ 1. **Row-based arrangement**: All matrix vectorization proceeds row-by-row (matching BHATLIB)
32
+ 2. **Upper triangular**: Symmetric matrices stored as vectors of upper diagonal elements
33
+ 3. **Covariance parameterization**: Ω = ω Ω* ω (std devs × correlation × std devs)
34
+ 4. **`xp` parameter pattern**: Numerical functions accept optional `xp` kwarg for backend selection
35
+ 5. **Analytic gradients**: Primary approach; autograd is secondary (PyTorch only)
36
+
37
+ ## Coding Standards
38
+
39
+ - PEP 8, type hints on all public functions
40
+ - Docstrings (NumPy style) on all public functions
41
+ - Private modules prefixed with underscore (e.g., `_vec_ops.py`)
42
+ - All tests use numerical gradient verification via finite differences where applicable
@@ -0,0 +1,497 @@
1
+ # pybhatlib — Full Implementation Plan
2
+
3
+ ## Context
4
+
5
+ **BHATLIB** is an open-source GAUSS library by Bhat, Clower, Haddad, and Jones (UT Austin / Aptech Systems) for statistical and econometric matrix-based inference methods. It provides:
6
+ - Efficient matrix operations and gradient-enabled routines for multivariate distribution evaluation
7
+ - Bhat's (2018) analytic approximation to the Multivariate Normal CDF (MVNCD)
8
+ - Pre-built models: Multinomial Probit (MNP), Multivariate Ordered Probit (MORP), MDCEV
9
+
10
+ **pybhatlib** is a Python reimplementation making these methods accessible beyond the GAUSS ecosystem.
11
+
12
+ ### User Decisions
13
+ - **Scope**: MNP model first + all 3 core computational libraries
14
+ - **Optimizer**: Both scipy.optimize AND optional PyTorch backend
15
+ - **Array backend**: NumPy by default, optional PyTorch tensor support
16
+ - **Packaging**: pip-installable from the start (pyproject.toml)
17
+
18
+ ---
19
+
20
+ ## BHATLIB Technical Summary (from paper)
21
+
22
+ ### Core Computational Libraries
23
+
24
+ **1. Vecup.src** — Low-level matrix manipulations and gradient functions:
25
+ - `vecdup(r)`: Extract upper triangular elements (incl. diagonal) → column vector. For 3×3 → 6×1.
26
+ - `vecndup(r)`: Extract upper diagonal elements (excl. diagonal) → column vector. For 3×3 → 3×1.
27
+ - `matdupfull(r)`: Expand column vector of upper diagonal elements → full symmetric matrix (inverse of vecdup).
28
+ - `matdupdiagonefull(r)`: Convert column vector → symmetric matrix with unit diagonal.
29
+ - `nondiag`: Extract non-diagonal elements of a matrix into a vector.
30
+ - `vecsymmetry`: Takes square symmetric matrix, produces matrix where each row unrolls the symmetric elements.
31
+ - Tools for truncated MVN mean/covariance computation
32
+ - LDLT factorization utilities
33
+ - Mask matrix operations for mixed models
34
+
35
+ **2. Matgradient.src** — Higher-level matrix operations and matrix gradients:
36
+ - `gradcovcor(CAPOMEGA)`: For Ω = ω Ω* ω (covariance = stddev × correlation × stddev), computes:
37
+ - `glitomega`: K × [K×(K+1)/2] gradient of Ω elements w.r.t. K std dev elements
38
+ - `gomegastar`: [K×(K-1)/2] × [K×(K+1)/2] gradient of Ω elements w.r.t. correlation elements
39
+ - `gomegxomegax`: Gradient of A = XΩX' w.r.t. symmetric matrix Ω
40
+ - Matrix chain rule operations following row-based arrangement convention
41
+
42
+ **3. Gradmvn.src** — Probability distributions and their gradients:
43
+ - MVNCD using Bhat's (2018) analytic ME approximation with LDLT decomposition
44
+ - Truncated (both-end) density and CDF via combinatorial methods
45
+ - Partial cumulative normal distribution functions (mixed point/interval)
46
+ - Gradient procedures for all distribution functions
47
+ - Additional distributions: logistic, skew-normal, skew-t, Gumbel, reverse-Gumbel
48
+
49
+ ### Key Conventions
50
+ 1. **Row-based matrix arrangement**: Vectorizing proceeds row by row
51
+ 2. **Symmetric matrices**: Only upper diagonal elements stored as vectors
52
+ 3. **Covariance parameterization**: Ω = ω Ω* ω (std devs × correlation × std devs)
53
+ 4. **Positive-definiteness**: Spherical/radial parameterization Ω* = f(Θ) for unconstrained optimization
54
+ 5. **Gradient chain rules**: dA/dω = dA/dΩ × dΩ/dω (row-based ordering)
55
+
56
+ ### MNP Model
57
+ - **Theory**: U_qi = V_qi + ξ_qi, V_qi = β'x_qi, ξ_q ~ MVN(0, Λ)
58
+ - **Generalized MNP**: β_q = b + β̃_q, β̃_q ~ MVN(0, Ω), Ω = LL'
59
+ - **Mixture-of-normals**: β_q = Σ π_h β_qh, β_qh ~ MVN(b_h, Ω_qh), π₁ < π₂ < ... < π_H
60
+ - **Key procedures**: mnpFit, mnpATEFit, mnpControlCreate
61
+ - **Control fields**: IID, mix, indep, correst, heteronly, rannddiag, nseg
62
+ - **Output**: coefficients, SE, t-stats, p-values, gradient, log-likelihood, covariance matrix, convergence info
63
+
64
+ ### BHATLIB Estimation Workflow
65
+ 1. Load library and prepare environment
66
+ 2. Specify data file and key variables (dvunordname, davunordname)
67
+ 3. Define independent variables matrix (ivunord) with "sero"/"uno" keywords
68
+ 4. Set coefficient names (var_unordnames)
69
+ 5. Configure control structure (mCtl.IID, mix, ranvars, etc.)
70
+ 6. Call fit procedure (mnpFit) → returns results structure
71
+ 7. Post-estimation: ATE analysis (mnpATEFit), goodness-of-fit
72
+
73
+ ### Validation Data (Table 1 from BHATLIB paper)
74
+ Using TRAVELMODE.csv (1,125 workers, 3 modes: DA 78.22%, SR 7.65%, TR 14.13%):
75
+
76
+ | Model | Description | Log-likelihood |
77
+ |-------|------------|----------------|
78
+ | (a)(i) | IID errors | -670.956 |
79
+ | (a)(ii) | Flexible covariance | -661.111 |
80
+ | (b) | + AGE45 variable | -659.285 |
81
+ | (c) | + Random coeff on OVTT | -635.871 |
82
+ | (d) | 2-segment mixture-of-normals | -634.975 |
83
+
84
+ ATE output (Figure 12): predicted shares at base level = [0.692, 0.141, 0.167]
85
+
86
+ ---
87
+
88
+ ## Package Structure
89
+
90
+ ```
91
+ C:\Users\chois\Gitsrcs\pybhatlib\
92
+ ├── pyproject.toml
93
+ ├── README.md
94
+ ├── LICENSE
95
+ ├── CLAUDE.md
96
+ ├── .gitignore
97
+ ├── examples/
98
+ │ ├── data/TRAVELMODE.csv
99
+ │ ├── mnp_iid.py
100
+ │ ├── mnp_flexible_cov.py
101
+ │ ├── mnp_random_coefficients.py
102
+ │ └── mnp_ate_analysis.py
103
+ ├── src/pybhatlib/
104
+ │ ├── __init__.py
105
+ │ ├── _version.py
106
+ │ ├── backend/
107
+ │ │ ├── __init__.py
108
+ │ │ ├── _array_api.py # get_backend(), set_backend(), array_namespace()
109
+ │ │ ├── _numpy_backend.py # NumpyBackend: numpy + scipy.linalg + scipy.stats
110
+ │ │ └── _torch_backend.py # TorchBackend: torch + torch.linalg (optional)
111
+ │ ├── vecup/ # Low-level matrix ops (GAUSS Vecup.src)
112
+ │ │ ├── __init__.py
113
+ │ │ ├── _vec_ops.py # vecdup, vecndup, matdupfull, matdupdiagonefull, vecsymmetry
114
+ │ │ ├── _nondiag.py # nondiag
115
+ │ │ ├── _ldlt.py # ldlt_decompose, ldlt_rank1_update
116
+ │ │ ├── _truncnorm.py # truncated_mvn_moments
117
+ │ │ └── _mask.py # Mask matrix ops for mixed models
118
+ │ ├── matgradient/ # Matrix gradients (GAUSS Matgradient.src)
119
+ │ │ ├── __init__.py
120
+ │ │ ├── _gradcovcor.py # gradcovcor: dΩ/dω, dΩ/dΩ*
121
+ │ │ ├── _gomegxomegax.py # dA/dΩ for A=XΩX'
122
+ │ │ ├── _spherical.py # theta_to_corr, grad_corr_theta (PD parameterization)
123
+ │ │ └── _chain_rules.py # Matrix chain rule helpers
124
+ │ ├── gradmvn/ # Distributions & gradients (GAUSS Gradmvn.src)
125
+ │ │ ├── __init__.py
126
+ │ │ ├── _mvncd.py # Bhat (2018) MVNCD analytic approximation
127
+ │ │ ├── _mvncd_grad.py # Gradients of MVNCD
128
+ │ │ ├── _truncated.py # Truncated MVN density/CDF
129
+ │ │ ├── _partial_cdf.py # Partial cumulative normal (mixed point/interval)
130
+ │ │ ├── _univariate.py # Standard normal PDF/CDF wrappers
131
+ │ │ └── _other_dists.py # Logistic, skew-normal, skew-t, Gumbel
132
+ │ ├── models/
133
+ │ │ ├── __init__.py
134
+ │ │ ├── _base.py # BaseModel ABC
135
+ │ │ └── mnp/
136
+ │ │ ├── __init__.py
137
+ │ │ ├── _mnp_control.py # MNPControl dataclass
138
+ │ │ ├── _mnp_results.py # MNPResults dataclass + summary()
139
+ │ │ ├── _mnp_model.py # MNPModel class with fit()
140
+ │ │ ├── _mnp_loglik.py # Log-likelihood & gradient
141
+ │ │ ├── _mnp_ate.py # ATE post-estimation
142
+ │ │ └── _mnp_forecast.py # Prediction
143
+ │ ├── optim/
144
+ │ │ ├── __init__.py
145
+ │ │ ├── _scipy_optim.py # scipy.optimize.minimize wrapper (BFGS, L-BFGS-B)
146
+ │ │ ├── _torch_optim.py # PyTorch optimizer wrapper (L-BFGS, Adam)
147
+ │ │ └── _convergence.py # Convergence diagnostics
148
+ │ ├── io/
149
+ │ │ ├── __init__.py
150
+ │ │ ├── _data_loader.py # CSV/DAT/XLSX via pandas
151
+ │ │ └── _spec_parser.py # Parse ivunord-style specs ("sero"/"uno")
152
+ │ └── utils/
153
+ │ ├── __init__.py
154
+ │ ├── _qmc.py # Quasi-Monte Carlo sequences
155
+ │ ├── _seeds.py # Random seed management
156
+ │ └── _validation.py # Input validation
157
+ └── tests/
158
+ ├── conftest.py # Backend fixtures, test matrices, TRAVELMODE data
159
+ ├── test_backend/
160
+ ├── test_vecup/
161
+ ├── test_matgradient/
162
+ ├── test_gradmvn/
163
+ ├── test_models/
164
+ └── test_integration/ # End-to-end against BHATLIB paper Table 1
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Detailed Function Signatures
170
+
171
+ ### Backend Abstraction (`backend/`)
172
+
173
+ ```python
174
+ # _array_api.py
175
+ BackendName = Literal["numpy", "torch"]
176
+
177
+ class ArrayNamespace(Protocol):
178
+ """Protocol: zeros, ones, eye, array, arange, concatenate, stack, diag,
179
+ triu_indices, sqrt, exp, log, abs, sum, dot, matmul, transpose,
180
+ solve, cholesky, det, inv, eigh, normal_pdf, normal_cdf, normal_ppf"""
181
+
182
+ def get_backend(name: BackendName | None = None) -> ArrayNamespace: ...
183
+ def set_backend(name: BackendName) -> None: ...
184
+ def array_namespace(*arrays) -> ArrayNamespace: ...
185
+
186
+ # _numpy_backend.py
187
+ class NumpyBackend:
188
+ """Wraps numpy + scipy.linalg + scipy.stats."""
189
+ float64 = np.float64
190
+ # All standard ops + solve, cholesky, ldlt, normal_pdf/cdf/ppf
191
+
192
+ # _torch_backend.py
193
+ class TorchBackend:
194
+ """Wraps torch + torch.linalg. Lazy import. Device support."""
195
+ def __init__(self, device="cpu", dtype=None): ...
196
+ ```
197
+
198
+ ### vecup Module
199
+
200
+ ```python
201
+ # _vec_ops.py — All accept optional xp kwarg
202
+ def vecdup(r: NDArray, *, xp=None) -> NDArray:
203
+ """(K,K) → (K*(K+1)//2, 1) upper triangular elements incl diagonal."""
204
+
205
+ def vecndup(r: NDArray, *, xp=None) -> NDArray:
206
+ """(K,K) → (K*(K-1)//2, 1) upper triangular elements excl diagonal."""
207
+
208
+ def matdupfull(r: NDArray, *, xp=None) -> NDArray:
209
+ """(K*(K+1)//2,) → (P,P) full symmetric matrix. Inverse of vecdup."""
210
+
211
+ def matdupdiagonefull(r: NDArray, *, xp=None) -> NDArray:
212
+ """(K*(K-1)//2,) → (P,P) symmetric matrix with unit diagonal."""
213
+
214
+ def vecsymmetry(r: NDArray, *, xp=None) -> NDArray:
215
+ """(K,K) → (K*(K+1)//2, K*K) position pattern matrix."""
216
+
217
+ # _nondiag.py
218
+ def nondiag(r: NDArray, *, xp=None) -> NDArray:
219
+ """(K,K) → (K*(K-1),1) non-diagonal elements row-by-row."""
220
+
221
+ # _ldlt.py
222
+ def ldlt_decompose(A: NDArray, *, xp=None) -> tuple[NDArray, NDArray]:
223
+ """A = LDL^T. Returns (L, D)."""
224
+
225
+ def ldlt_rank1_update(L, D, v, alpha=1.0, *, xp=None) -> tuple[NDArray, NDArray]:
226
+ """Rank-1 update: LDL^T + alpha*vv^T."""
227
+
228
+ # _truncnorm.py
229
+ def truncated_mvn_moments(mu, sigma, lower, upper, *, xp=None) -> tuple[NDArray, NDArray]:
230
+ """Returns (mu_trunc, sigma_trunc)."""
231
+ ```
232
+
233
+ ### matgradient Module
234
+
235
+ ```python
236
+ # _gradcovcor.py
237
+ @dataclass
238
+ class GradCovCorResult:
239
+ glitomega: NDArray # K × [K×(K+1)/2]
240
+ gomegastar: NDArray # [K×(K-1)/2] × [K×(K+1)/2]
241
+
242
+ def gradcovcor(capomega: NDArray, *, xp=None) -> GradCovCorResult:
243
+ """For Ω = ω Ω* ω, compute dΩ/dω and dΩ/dΩ*."""
244
+
245
+ # _gomegxomegax.py
246
+ def gomegxomegax(X: NDArray, omega: NDArray, *, xp=None) -> NDArray:
247
+ """dA/dΩ for A=XΩX'. Returns [K(K+1)/2, N(N+1)/2]."""
248
+
249
+ # _spherical.py
250
+ def theta_to_corr(theta: NDArray, K: int, *, xp=None) -> NDArray:
251
+ """Unconstrained Θ → PD correlation matrix Ω*."""
252
+
253
+ def grad_corr_theta(theta: NDArray, K: int, *, xp=None) -> NDArray:
254
+ """dΩ*/dΘ Jacobian."""
255
+
256
+ # _chain_rules.py
257
+ def chain_grad(dA_dOmega: NDArray, dOmega_dparam: NDArray, *, xp=None) -> NDArray:
258
+ """dA/dparam = dA/dOmega @ dOmega/dparam (row-based order)."""
259
+ ```
260
+
261
+ ### gradmvn Module
262
+
263
+ ```python
264
+ # _mvncd.py
265
+ def mvncd(a: NDArray, sigma: NDArray, *, method="me", xp=None) -> float:
266
+ """P(X₁≤a₁,...,X_K≤a_K) for X~MVN(0,Σ). Bhat (2018) ME approximation."""
267
+
268
+ def mvncd_batch(a: NDArray, sigma: NDArray, *, method="me", xp=None) -> NDArray:
269
+ """Vectorized MVNCD for N observations."""
270
+
271
+ # _mvncd_grad.py
272
+ @dataclass
273
+ class MVNCDGradResult:
274
+ prob: float
275
+ grad_a: NDArray # (K,)
276
+ grad_sigma: NDArray # (K*(K-1)//2,)
277
+
278
+ def mvncd_grad(a, sigma, *, method="me", xp=None) -> MVNCDGradResult: ...
279
+
280
+ # _truncated.py
281
+ def truncated_mvn_pdf(x, mu, sigma, lower, upper, *, xp=None) -> float: ...
282
+ def truncated_mvn_cdf(x, mu, sigma, lower, upper, *, xp=None) -> float: ...
283
+ def truncated_mvn_pdf_grad(x, mu, sigma, lower, upper, *, xp=None) -> tuple: ...
284
+
285
+ # _partial_cdf.py
286
+ def partial_mvn_cdf(points, lower, upper, mu, sigma, point_indices=None, range_indices=None, *, xp=None) -> float: ...
287
+
288
+ # _other_dists.py
289
+ def mv_logistic_cdf(x, sigma, *, xp=None) -> float: ...
290
+ def skew_normal_pdf(x, alpha, *, xp=None) -> float: ...
291
+ def skew_normal_cdf(x, alpha, *, xp=None) -> float: ...
292
+ def skew_t_pdf(x, alpha, nu, *, xp=None) -> float: ...
293
+ def gumbel_pdf(x, mu=0.0, beta=1.0, *, xp=None) -> float: ...
294
+ def gumbel_cdf(x, mu=0.0, beta=1.0, *, xp=None) -> float: ...
295
+ def reverse_gumbel_pdf(x, mu=0.0, beta=1.0, *, xp=None) -> float: ...
296
+ def reverse_gumbel_cdf(x, mu=0.0, beta=1.0, *, xp=None) -> float: ...
297
+ ```
298
+
299
+ ### MNP Model
300
+
301
+ ```python
302
+ # _mnp_control.py
303
+ @dataclass
304
+ class MNPControl:
305
+ iid: bool = False
306
+ mix: bool = False
307
+ indep: bool = False
308
+ correst: NDArray | None = None
309
+ heteronly: bool = False
310
+ rannddiag: bool = False
311
+ nseg: int = 1
312
+ maxiter: int = 200
313
+ tol: float = 1e-5
314
+ optimizer: Literal["bfgs", "lbfgsb", "torch_adam", "torch_lbfgs"] = "bfgs"
315
+ verbose: int = 1
316
+ seed: int | None = None
317
+
318
+ # _mnp_results.py
319
+ @dataclass
320
+ class MNPResults:
321
+ b: NDArray # Estimated coefficients (parametrized)
322
+ b_original: NDArray # Unparametrized coefficients
323
+ se: NDArray # Standard errors
324
+ t_stat: NDArray # t-statistics
325
+ p_value: NDArray # p-values
326
+ gradient: NDArray # Gradient at convergence
327
+ ll: float # Mean log-likelihood
328
+ ll_total: float # Total log-likelihood
329
+ n_obs: int # Number of observations
330
+ param_names: list[str] # Parameter names
331
+ corr_matrix: NDArray # Correlation matrix of parameters
332
+ cov_matrix: NDArray # Var-cov matrix of parameters
333
+ n_iterations: int
334
+ convergence_time: float
335
+ converged: bool
336
+ return_code: int
337
+ lambda_hat: NDArray | None # Kernel error covariance (if IID=False)
338
+ omega_hat: NDArray | None # Random coeff covariance (if mix=True)
339
+ cholesky_L: NDArray | None # Cholesky of Omega (if mix=True)
340
+ segment_probs: NDArray | None # Mixture probabilities (if nseg>1)
341
+ segment_means: list[NDArray] | None
342
+ segment_covs: list[NDArray] | None
343
+ control: MNPControl
344
+ data_path: str
345
+
346
+ def summary(self) -> str: ...
347
+ def to_dataframe(self) -> pd.DataFrame: ...
348
+
349
+ # _mnp_model.py
350
+ class MNPModel:
351
+ def __init__(self, data, alternatives, availability="none", spec=None,
352
+ var_names=None, mix=False, ranvars=None, control=None): ...
353
+ def fit(self) -> MNPResults: ...
354
+
355
+ # _mnp_loglik.py
356
+ def mnp_loglik(theta, X, y, avail, control, *, return_gradient=False, xp=None) -> float | tuple: ...
357
+
358
+ # _mnp_ate.py
359
+ @dataclass
360
+ class ATEResult:
361
+ n_obs: int
362
+ predicted_shares: NDArray
363
+ base_shares: NDArray | None
364
+ treatment_shares: NDArray | None
365
+ pct_ate: NDArray | None
366
+
367
+ def mnp_ate(results: MNPResults, changevar=None, changeval=None) -> ATEResult: ...
368
+ ```
369
+
370
+ ### Optimization
371
+
372
+ ```python
373
+ # _scipy_optim.py
374
+ @dataclass
375
+ class OptimResult:
376
+ x: NDArray; fun: float; grad: NDArray; hess_inv: NDArray
377
+ n_iter: int; converged: bool; return_code: int; message: str
378
+
379
+ def minimize_scipy(func, x0, method="BFGS", maxiter=200, tol=1e-5, verbose=1, jac=True) -> OptimResult: ...
380
+
381
+ # _torch_optim.py
382
+ def minimize_torch(func, x0, method="lbfgs", maxiter=200, tol=1e-5, verbose=1, device="cpu") -> OptimResult: ...
383
+ ```
384
+
385
+ ### I/O
386
+
387
+ ```python
388
+ # _data_loader.py
389
+ def load_data(path, *, file_type=None) -> pd.DataFrame: ...
390
+
391
+ # _spec_parser.py
392
+ def parse_spec(spec, data, alternatives, nseg=1) -> tuple[NDArray, list[str]]: ...
393
+ ```
394
+
395
+ ---
396
+
397
+ ## pyproject.toml
398
+
399
+ ```toml
400
+ [build-system]
401
+ requires = ["hatchling", "hatch-vcs"]
402
+ build-backend = "hatchling.build"
403
+
404
+ [project]
405
+ name = "pybhatlib"
406
+ dynamic = ["version"]
407
+ description = "Python implementation of BHATLIB: matrix-based inference for advanced econometric models"
408
+ readme = "README.md"
409
+ license = "MIT"
410
+ requires-python = ">=3.10"
411
+ authors = [{ name = "Seongjin Choi", email = "choi@umn.edu" }]
412
+ keywords = ["discrete choice", "multinomial probit", "econometrics", "MVNCD", "covariance matrix"]
413
+ dependencies = ["numpy>=1.24", "scipy>=1.10", "pandas>=2.0"]
414
+
415
+ [project.optional-dependencies]
416
+ torch = ["torch>=2.0"]
417
+ dev = ["pytest>=7.0", "pytest-cov", "ruff", "mypy", "pre-commit"]
418
+ docs = ["sphinx", "sphinx-rtd-theme", "numpydoc"]
419
+ all = ["pybhatlib[torch,dev,docs]"]
420
+
421
+ [tool.hatch.version]
422
+ source = "vcs"
423
+
424
+ [tool.hatch.build.targets.wheel]
425
+ packages = ["src/pybhatlib"]
426
+
427
+ [tool.pytest.ini_options]
428
+ testpaths = ["tests"]
429
+ markers = ["torch: tests requiring PyTorch", "slow: long-running integration tests"]
430
+
431
+ [tool.ruff]
432
+ line-length = 88
433
+ target-version = "py310"
434
+
435
+ [tool.mypy]
436
+ python_version = "3.10"
437
+ warn_return_any = true
438
+ ```
439
+
440
+ ---
441
+
442
+ ## Implementation Phases
443
+
444
+ ### Phase 0: Project Scaffolding
445
+ - Create pyproject.toml, README.md, LICENSE, .gitignore, CLAUDE.md
446
+ - Create all directories and `__init__.py` files
447
+ - Add TRAVELMODE.csv to examples/data/
448
+ - Initialize git, set up tests/conftest.py
449
+ - Verify `pip install -e .` works
450
+
451
+ ### Phase 1: Backend Abstraction
452
+ - _array_api.py, _numpy_backend.py, _torch_backend.py
453
+ - Tests verifying both backends produce identical results
454
+
455
+ ### Phase 2: vecup Module
456
+ - _vec_ops.py, _nondiag.py, _ldlt.py, _truncnorm.py, _mask.py
457
+ - Tests against paper examples (p. 6)
458
+
459
+ ### Phase 3: matgradient Module
460
+ - _gradcovcor.py, _gomegxomegax.py, _spherical.py, _chain_rules.py
461
+ - Numerical gradient verification via finite differences
462
+
463
+ ### Phase 4: gradmvn Module
464
+ - _mvncd.py (Bhat 2018 ME approximation), _mvncd_grad.py
465
+ - _truncated.py, _partial_cdf.py, _univariate.py, _other_dists.py
466
+ - Validate against scipy.stats for K≤3
467
+
468
+ ### Phase 5: Optimization & I/O
469
+ - _scipy_optim.py, _torch_optim.py, _convergence.py
470
+ - _data_loader.py, _spec_parser.py
471
+
472
+ ### Phase 6: MNP Model
473
+ - _mnp_control.py, _mnp_results.py, _mnp_model.py
474
+ - _mnp_loglik.py (IID, flexible cov, random coefficients, mixture-of-normals)
475
+ - _mnp_ate.py, _mnp_forecast.py
476
+
477
+ ### Phase 7: Integration Testing & Validation
478
+ - Replicate Table 1 results from BHATLIB paper
479
+ - ATE validation against Figure 12
480
+ - Cross-backend verification (NumPy ≈ PyTorch)
481
+
482
+ ---
483
+
484
+ ## Build Order (dependency graph)
485
+
486
+ ```
487
+ backend → vecup → matgradient → gradmvn → optim/io → models/mnp
488
+ ```
489
+
490
+ ## Key References
491
+
492
+ 1. Bhat, C. R. (2018). "New matrix-based methods for the analytic evaluation of the MVNCD." TR Part B, 109: 238–256.
493
+ 2. Bhat, C. R. (2015). "A new GHDM to jointly model mixed types of dependent variables." TR Part B, 79: 50–77.
494
+ 3. Bhat, C. R. (2024). "Transformation-based flexible error structures for choice modeling." J. Choice Modelling, 53: 100522.
495
+ 4. Higham, N. J. (2009). "Cholesky factorization." WIREs Comp. Stats., 1(2): 251–254.
496
+ 5. Bhat, C. R. (2014). "The CML inference approach." Found. Trends Econometrics, 7(1): 1–117.
497
+ 6. Saxena, S., Bhat, C. R., Pinjari, A. R. (2023). "Separation-based parameterization strategies." J. Choice Modelling, 47: 100411.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Seongjin Choi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.