bulanik 0.3.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 (262) hide show
  1. bulanik-0.3.0/.gitignore +14 -0
  2. bulanik-0.3.0/CHANGELOG.md +127 -0
  3. bulanik-0.3.0/CITATION.cff +29 -0
  4. bulanik-0.3.0/CONTRIBUTING.md +51 -0
  5. bulanik-0.3.0/LICENSE +21 -0
  6. bulanik-0.3.0/PKG-INFO +113 -0
  7. bulanik-0.3.0/README.md +81 -0
  8. bulanik-0.3.0/conftest.py +32 -0
  9. bulanik-0.3.0/docs/scripts/_figutil.py +177 -0
  10. bulanik-0.3.0/docs/scripts/api_index.py +124 -0
  11. bulanik-0.3.0/docs/scripts/build_figures.py +46 -0
  12. bulanik-0.3.0/docs/scripts/fig_anfis_fit.py +50 -0
  13. bulanik-0.3.0/docs/scripts/fig_example_plotting.py +22 -0
  14. bulanik-0.3.0/docs/scripts/fig_it2_fou.py +31 -0
  15. bulanik-0.3.0/docs/scripts/fig_mamdani_aggregate.py +58 -0
  16. bulanik-0.3.0/docs/scripts/fig_mcdm_dematel.py +32 -0
  17. bulanik-0.3.0/docs/scripts/fig_mcdm_ranking.py +61 -0
  18. bulanik-0.3.0/docs/scripts/fig_mcdm_weights.py +35 -0
  19. bulanik-0.3.0/docs/scripts/fig_membership_shapes.py +50 -0
  20. bulanik-0.3.0/docs/scripts/fig_numbers_interval.py +55 -0
  21. bulanik-0.3.0/docs/scripts/fig_numbers_it2.py +44 -0
  22. bulanik-0.3.0/docs/scripts/fig_numbers_orthopair.py +138 -0
  23. bulanik-0.3.0/docs/scripts/fig_numbers_threepart.py +68 -0
  24. bulanik-0.3.0/docs/scripts/fig_numbers_triangular.py +133 -0
  25. bulanik-0.3.0/docs/site/about/changelog.md +1 -0
  26. bulanik-0.3.0/docs/site/about/citing.md +21 -0
  27. bulanik-0.3.0/docs/site/about/contributing.md +1 -0
  28. bulanik-0.3.0/docs/site/about/license.md +7 -0
  29. bulanik-0.3.0/docs/site/about/references.md +220 -0
  30. bulanik-0.3.0/docs/site/api/aggregate.md +1 -0
  31. bulanik-0.3.0/docs/site/api/anfis.md +1 -0
  32. bulanik-0.3.0/docs/site/api/index.md +32 -0
  33. bulanik-0.3.0/docs/site/api/inference.md +28 -0
  34. bulanik-0.3.0/docs/site/api/lumix.md +1 -0
  35. bulanik-0.3.0/docs/site/api/mcdm.md +52 -0
  36. bulanik-0.3.0/docs/site/api/numbers.md +1 -0
  37. bulanik-0.3.0/docs/site/api/plot.md +7 -0
  38. bulanik-0.3.0/docs/site/api/scales.md +1 -0
  39. bulanik-0.3.0/docs/site/api/sets.md +1 -0
  40. bulanik-0.3.0/docs/site/api/sezgi.md +1 -0
  41. bulanik-0.3.0/docs/site/api/type2.md +1 -0
  42. bulanik-0.3.0/docs/site/assets/extra.css +25 -0
  43. bulanik-0.3.0/docs/site/assets/figures/alpha-cut-arithmetic-dark.svg +4762 -0
  44. bulanik-0.3.0/docs/site/assets/figures/alpha-cut-arithmetic-light.svg +4762 -0
  45. bulanik-0.3.0/docs/site/assets/figures/anfis-fit-dark.svg +9176 -0
  46. bulanik-0.3.0/docs/site/assets/figures/anfis-fit-light.svg +9176 -0
  47. bulanik-0.3.0/docs/site/assets/figures/defuzzification-dark.svg +1442 -0
  48. bulanik-0.3.0/docs/site/assets/figures/defuzzification-light.svg +1442 -0
  49. bulanik-0.3.0/docs/site/assets/figures/dematel-cause-effect-dark.svg +2289 -0
  50. bulanik-0.3.0/docs/site/assets/figures/dematel-cause-effect-light.svg +2289 -0
  51. bulanik-0.3.0/docs/site/assets/figures/example-plotting-membership-dark.svg +856 -0
  52. bulanik-0.3.0/docs/site/assets/figures/example-plotting-membership-light.svg +856 -0
  53. bulanik-0.3.0/docs/site/assets/figures/it2-fou-dark.svg +5721 -0
  54. bulanik-0.3.0/docs/site/assets/figures/it2-fou-light.svg +5721 -0
  55. bulanik-0.3.0/docs/site/assets/figures/it2-trapezoid-dark.svg +4224 -0
  56. bulanik-0.3.0/docs/site/assets/figures/it2-trapezoid-light.svg +4224 -0
  57. bulanik-0.3.0/docs/site/assets/figures/iv-orthopair-operations-dark.svg +2645 -0
  58. bulanik-0.3.0/docs/site/assets/figures/iv-orthopair-operations-light.svg +2641 -0
  59. bulanik-0.3.0/docs/site/assets/figures/iv-orthopair-regions-dark.svg +3706 -0
  60. bulanik-0.3.0/docs/site/assets/figures/iv-orthopair-regions-light.svg +3703 -0
  61. bulanik-0.3.0/docs/site/assets/figures/linguistic-scale-ifn-dark.svg +1985 -0
  62. bulanik-0.3.0/docs/site/assets/figures/linguistic-scale-ifn-light.svg +1985 -0
  63. bulanik-0.3.0/docs/site/assets/figures/linguistic-scale-tfn-dark.svg +2779 -0
  64. bulanik-0.3.0/docs/site/assets/figures/linguistic-scale-tfn-light.svg +2779 -0
  65. bulanik-0.3.0/docs/site/assets/figures/mamdani-aggregate-dark.svg +8137 -0
  66. bulanik-0.3.0/docs/site/assets/figures/mamdani-aggregate-light.svg +8137 -0
  67. bulanik-0.3.0/docs/site/assets/figures/mamdani-surface-dark.svg +6404 -0
  68. bulanik-0.3.0/docs/site/assets/figures/mamdani-surface-light.svg +6404 -0
  69. bulanik-0.3.0/docs/site/assets/figures/mcdm-pipeline-dark.svg +2104 -0
  70. bulanik-0.3.0/docs/site/assets/figures/mcdm-pipeline-light.svg +2104 -0
  71. bulanik-0.3.0/docs/site/assets/figures/mcdm-ranking-methods-dark.svg +2395 -0
  72. bulanik-0.3.0/docs/site/assets/figures/mcdm-ranking-methods-light.svg +2395 -0
  73. bulanik-0.3.0/docs/site/assets/figures/mcdm-weights-compare-dark.svg +1858 -0
  74. bulanik-0.3.0/docs/site/assets/figures/mcdm-weights-compare-light.svg +1858 -0
  75. bulanik-0.3.0/docs/site/assets/figures/membership-shapes-dark.svg +11093 -0
  76. bulanik-0.3.0/docs/site/assets/figures/membership-shapes-light.svg +11093 -0
  77. bulanik-0.3.0/docs/site/assets/figures/neutrosophic-radar-dark.svg +1789 -0
  78. bulanik-0.3.0/docs/site/assets/figures/neutrosophic-radar-light.svg +1789 -0
  79. bulanik-0.3.0/docs/site/assets/figures/orthopair-aggregation-dark.svg +4829 -0
  80. bulanik-0.3.0/docs/site/assets/figures/orthopair-aggregation-light.svg +4829 -0
  81. bulanik-0.3.0/docs/site/assets/figures/orthopair-operators-dark.svg +7682 -0
  82. bulanik-0.3.0/docs/site/assets/figures/orthopair-operators-light.svg +7682 -0
  83. bulanik-0.3.0/docs/site/assets/figures/orthopair-regions-dark.svg +6484 -0
  84. bulanik-0.3.0/docs/site/assets/figures/orthopair-regions-light.svg +6484 -0
  85. bulanik-0.3.0/docs/site/assets/figures/picture-simplex-dark.svg +5122 -0
  86. bulanik-0.3.0/docs/site/assets/figures/picture-simplex-light.svg +5122 -0
  87. bulanik-0.3.0/docs/site/assets/figures/spherical-octant-dark.svg +3267 -0
  88. bulanik-0.3.0/docs/site/assets/figures/spherical-octant-light.svg +3267 -0
  89. bulanik-0.3.0/docs/site/assets/figures/tfn-anatomy-dark.svg +4812 -0
  90. bulanik-0.3.0/docs/site/assets/figures/tfn-anatomy-light.svg +4812 -0
  91. bulanik-0.3.0/docs/site/assets/mathjax-config.js +12 -0
  92. bulanik-0.3.0/docs/site/concepts/choosing.md +117 -0
  93. bulanik-0.3.0/docs/site/concepts/conventions.md +249 -0
  94. bulanik-0.3.0/docs/site/examples/01-fuzzy-numbers.md +37 -0
  95. bulanik-0.3.0/docs/site/examples/02-scale-and-experts.md +36 -0
  96. bulanik-0.3.0/docs/site/examples/03-fuzzy-ahp.md +34 -0
  97. bulanik-0.3.0/docs/site/examples/04-swara-topsis-supplier.md +35 -0
  98. bulanik-0.3.0/docs/site/examples/05-intuitionistic-pipeline.md +36 -0
  99. bulanik-0.3.0/docs/site/examples/06-zimmermann-lumix.md +37 -0
  100. bulanik-0.3.0/docs/site/examples/07-bwm-lumix.md +38 -0
  101. bulanik-0.3.0/docs/site/examples/08-fuzzy-bwm-sezgi.md +38 -0
  102. bulanik-0.3.0/docs/site/examples/09-weights-to-lumix.md +39 -0
  103. bulanik-0.3.0/docs/site/examples/10-mamdani-tipper.md +34 -0
  104. bulanik-0.3.0/docs/site/examples/11-sugeno-controller.md +34 -0
  105. bulanik-0.3.0/docs/site/examples/12-tune-sugeno-with-sezgi.md +38 -0
  106. bulanik-0.3.0/docs/site/examples/13-tsukamoto.md +33 -0
  107. bulanik-0.3.0/docs/site/examples/14-it2-vs-t1-noise.md +34 -0
  108. bulanik-0.3.0/docs/site/examples/15-anfis-sinc.md +35 -0
  109. bulanik-0.3.0/docs/site/examples/16-anfis-with-sezgi.md +38 -0
  110. bulanik-0.3.0/docs/site/examples/17-plotting.md +45 -0
  111. bulanik-0.3.0/docs/site/examples/18-interval-valued-ahp-codas.md +37 -0
  112. bulanik-0.3.0/docs/site/examples/19-it2-ahp-marcos.md +36 -0
  113. bulanik-0.3.0/docs/site/examples/20-ranking-methods-compared.md +35 -0
  114. bulanik-0.3.0/docs/site/examples/21-critic-piprecia.md +36 -0
  115. bulanik-0.3.0/docs/site/examples/22-fuzzy-dematel.md +35 -0
  116. bulanik-0.3.0/docs/site/examples/23-interval-valued-ahp.md +35 -0
  117. bulanik-0.3.0/docs/site/examples/24-fuzzy-ranking.md +35 -0
  118. bulanik-0.3.0/docs/site/examples/index.md +39 -0
  119. bulanik-0.3.0/docs/site/guide/aggregation.md +177 -0
  120. bulanik-0.3.0/docs/site/guide/anfis.md +254 -0
  121. bulanik-0.3.0/docs/site/guide/benchmarks.md +315 -0
  122. bulanik-0.3.0/docs/site/guide/bridge-lumix.md +245 -0
  123. bulanik-0.3.0/docs/site/guide/bridge-sezgi.md +232 -0
  124. bulanik-0.3.0/docs/site/guide/fuzzy-topsis.md +144 -0
  125. bulanik-0.3.0/docs/site/guide/gallery.md +193 -0
  126. bulanik-0.3.0/docs/site/guide/mamdani.md +277 -0
  127. bulanik-0.3.0/docs/site/guide/numbers-interval.md +241 -0
  128. bulanik-0.3.0/docs/site/guide/numbers-it2.md +200 -0
  129. bulanik-0.3.0/docs/site/guide/numbers-orthopair.md +187 -0
  130. bulanik-0.3.0/docs/site/guide/numbers-threepart.md +184 -0
  131. bulanik-0.3.0/docs/site/guide/numbers-triangular.md +240 -0
  132. bulanik-0.3.0/docs/site/guide/ranking-more.md +596 -0
  133. bulanik-0.3.0/docs/site/guide/ranking.md +362 -0
  134. bulanik-0.3.0/docs/site/guide/sets-and-scales.md +220 -0
  135. bulanik-0.3.0/docs/site/guide/sugeno-tsukamoto.md +207 -0
  136. bulanik-0.3.0/docs/site/guide/type2.md +248 -0
  137. bulanik-0.3.0/docs/site/guide/weighting-more.md +448 -0
  138. bulanik-0.3.0/docs/site/guide/weighting.md +263 -0
  139. bulanik-0.3.0/docs/site/index.md +87 -0
  140. bulanik-0.3.0/docs/site/install.md +72 -0
  141. bulanik-0.3.0/docs/site/quickstart.md +162 -0
  142. bulanik-0.3.0/examples/01_fuzzy_numbers.py +32 -0
  143. bulanik-0.3.0/examples/02_scale_and_experts.py +28 -0
  144. bulanik-0.3.0/examples/03_fuzzy_ahp.py +23 -0
  145. bulanik-0.3.0/examples/04_swara_topsis_supplier.py +36 -0
  146. bulanik-0.3.0/examples/05_intuitionistic_pipeline.py +41 -0
  147. bulanik-0.3.0/examples/06_zimmermann_lumix.py +33 -0
  148. bulanik-0.3.0/examples/07_bwm_lumix.py +24 -0
  149. bulanik-0.3.0/examples/08_fuzzy_bwm_sezgi.py +30 -0
  150. bulanik-0.3.0/examples/09_weights_to_lumix.py +60 -0
  151. bulanik-0.3.0/examples/10_mamdani_tipper.py +45 -0
  152. bulanik-0.3.0/examples/11_sugeno_controller.py +38 -0
  153. bulanik-0.3.0/examples/12_tune_sugeno_with_sezgi.py +42 -0
  154. bulanik-0.3.0/examples/13_tsukamoto.py +18 -0
  155. bulanik-0.3.0/examples/14_it2_vs_t1_noise.py +59 -0
  156. bulanik-0.3.0/examples/15_anfis_sinc.py +30 -0
  157. bulanik-0.3.0/examples/16_anfis_with_sezgi.py +25 -0
  158. bulanik-0.3.0/examples/17_plotting.py +63 -0
  159. bulanik-0.3.0/examples/18_interval_valued_ahp_codas.py +64 -0
  160. bulanik-0.3.0/examples/19_it2_ahp_marcos.py +75 -0
  161. bulanik-0.3.0/examples/20_ranking_methods_compared.py +56 -0
  162. bulanik-0.3.0/examples/21_critic_piprecia.py +46 -0
  163. bulanik-0.3.0/examples/22_fuzzy_dematel.py +72 -0
  164. bulanik-0.3.0/examples/23_interval_valued_ahp.py +87 -0
  165. bulanik-0.3.0/examples/24_fuzzy_ranking.py +71 -0
  166. bulanik-0.3.0/mkdocs.yml +160 -0
  167. bulanik-0.3.0/pyproject.toml +91 -0
  168. bulanik-0.3.0/src/bulanik/__init__.py +66 -0
  169. bulanik-0.3.0/src/bulanik/_linalg.py +135 -0
  170. bulanik-0.3.0/src/bulanik/_validate.py +93 -0
  171. bulanik-0.3.0/src/bulanik/aggregate.py +226 -0
  172. bulanik-0.3.0/src/bulanik/anfis.py +873 -0
  173. bulanik-0.3.0/src/bulanik/inference/__init__.py +49 -0
  174. bulanik-0.3.0/src/bulanik/inference/_common.py +255 -0
  175. bulanik-0.3.0/src/bulanik/inference/mamdani.py +338 -0
  176. bulanik-0.3.0/src/bulanik/inference/rules.py +708 -0
  177. bulanik-0.3.0/src/bulanik/inference/sugeno.py +339 -0
  178. bulanik-0.3.0/src/bulanik/inference/tsukamoto.py +207 -0
  179. bulanik-0.3.0/src/bulanik/lumix.py +510 -0
  180. bulanik-0.3.0/src/bulanik/mcdm/__init__.py +94 -0
  181. bulanik-0.3.0/src/bulanik/mcdm/_common.py +230 -0
  182. bulanik-0.3.0/src/bulanik/mcdm/bwm.py +195 -0
  183. bulanik-0.3.0/src/bulanik/mcdm/dematel.py +274 -0
  184. bulanik-0.3.0/src/bulanik/mcdm/fuzzy.py +463 -0
  185. bulanik-0.3.0/src/bulanik/mcdm/fuzzy_ranking.py +541 -0
  186. bulanik-0.3.0/src/bulanik/mcdm/outranking.py +236 -0
  187. bulanik-0.3.0/src/bulanik/mcdm/ranking.py +512 -0
  188. bulanik-0.3.0/src/bulanik/mcdm/results.py +192 -0
  189. bulanik-0.3.0/src/bulanik/mcdm/scoring.py +806 -0
  190. bulanik-0.3.0/src/bulanik/mcdm/weighting.py +1015 -0
  191. bulanik-0.3.0/src/bulanik/numbers/__init__.py +66 -0
  192. bulanik-0.3.0/src/bulanik/numbers/_base.py +230 -0
  193. bulanik-0.3.0/src/bulanik/numbers/it2.py +469 -0
  194. bulanik-0.3.0/src/bulanik/numbers/iv_neutrosophic.py +369 -0
  195. bulanik-0.3.0/src/bulanik/numbers/iv_orthopair.py +605 -0
  196. bulanik-0.3.0/src/bulanik/numbers/orthopair.py +594 -0
  197. bulanik-0.3.0/src/bulanik/numbers/threepart.py +795 -0
  198. bulanik-0.3.0/src/bulanik/numbers/triangular.py +552 -0
  199. bulanik-0.3.0/src/bulanik/plot/__init__.py +83 -0
  200. bulanik-0.3.0/src/bulanik/plot/_style.py +605 -0
  201. bulanik-0.3.0/src/bulanik/plot/inference.py +262 -0
  202. bulanik-0.3.0/src/bulanik/plot/mcdm.py +364 -0
  203. bulanik-0.3.0/src/bulanik/plot/numbers.py +1067 -0
  204. bulanik-0.3.0/src/bulanik/plot/sets.py +319 -0
  205. bulanik-0.3.0/src/bulanik/py.typed +0 -0
  206. bulanik-0.3.0/src/bulanik/scales.py +245 -0
  207. bulanik-0.3.0/src/bulanik/sets.py +668 -0
  208. bulanik-0.3.0/src/bulanik/sezgi.py +702 -0
  209. bulanik-0.3.0/src/bulanik/type2.py +1093 -0
  210. bulanik-0.3.0/tests/benchmarks/THIRD_PARTY_NOTICES.md +89 -0
  211. bulanik-0.3.0/tests/benchmarks/_bench_support.py +85 -0
  212. bulanik-0.3.0/tests/benchmarks/_hdi_analysis.py +45 -0
  213. bulanik-0.3.0/tests/benchmarks/_inference_cases.py +111 -0
  214. bulanik-0.3.0/tests/benchmarks/_mcdm_cases.py +208 -0
  215. bulanik-0.3.0/tests/benchmarks/data/anfis_mackey_glass_results.json +394 -0
  216. bulanik-0.3.0/tests/benchmarks/data/anfis_regression_results.json +780 -0
  217. bulanik-0.3.0/tests/benchmarks/data/hdi_2023_extract.csv +194 -0
  218. bulanik-0.3.0/tests/benchmarks/data/hdi_results.json +85 -0
  219. bulanik-0.3.0/tests/benchmarks/data/inference_results.json +1095 -0
  220. bulanik-0.3.0/tests/benchmarks/data/mcdm_results.json +6211 -0
  221. bulanik-0.3.0/tests/benchmarks/test_bench_anfis_mackey_glass.py +57 -0
  222. bulanik-0.3.0/tests/benchmarks/test_bench_fixtures.py +53 -0
  223. bulanik-0.3.0/tests/benchmarks/test_bench_hdi.py +42 -0
  224. bulanik-0.3.0/tests/benchmarks/test_bench_inference.py +55 -0
  225. bulanik-0.3.0/tests/benchmarks/test_bench_mcdm_frozen.py +69 -0
  226. bulanik-0.3.0/tests/conftest.py +75 -0
  227. bulanik-0.3.0/tests/strategies.py +132 -0
  228. bulanik-0.3.0/tests/test_aggregate.py +201 -0
  229. bulanik-0.3.0/tests/test_ahp_interval.py +222 -0
  230. bulanik-0.3.0/tests/test_ahp_numbers.py +120 -0
  231. bulanik-0.3.0/tests/test_anfis.py +511 -0
  232. bulanik-0.3.0/tests/test_api_index.py +43 -0
  233. bulanik-0.3.0/tests/test_bwm_core.py +74 -0
  234. bulanik-0.3.0/tests/test_critic_piprecia.py +422 -0
  235. bulanik-0.3.0/tests/test_dematel.py +221 -0
  236. bulanik-0.3.0/tests/test_docs.py +53 -0
  237. bulanik-0.3.0/tests/test_electre.py +243 -0
  238. bulanik-0.3.0/tests/test_examples.py +89 -0
  239. bulanik-0.3.0/tests/test_fuzzy_mcdm.py +340 -0
  240. bulanik-0.3.0/tests/test_fuzzy_ranking.py +366 -0
  241. bulanik-0.3.0/tests/test_it2_number.py +235 -0
  242. bulanik-0.3.0/tests/test_iv_neutrosophic.py +193 -0
  243. bulanik-0.3.0/tests/test_iv_orthopair.py +304 -0
  244. bulanik-0.3.0/tests/test_linalg.py +85 -0
  245. bulanik-0.3.0/tests/test_lumix_bridge.py +186 -0
  246. bulanik-0.3.0/tests/test_mamdani.py +310 -0
  247. bulanik-0.3.0/tests/test_orthopair.py +484 -0
  248. bulanik-0.3.0/tests/test_plot.py +1059 -0
  249. bulanik-0.3.0/tests/test_ranking.py +186 -0
  250. bulanik-0.3.0/tests/test_rules.py +253 -0
  251. bulanik-0.3.0/tests/test_scales.py +70 -0
  252. bulanik-0.3.0/tests/test_scoring.py +697 -0
  253. bulanik-0.3.0/tests/test_sets.py +319 -0
  254. bulanik-0.3.0/tests/test_sezgi_bridge.py +226 -0
  255. bulanik-0.3.0/tests/test_sugeno.py +138 -0
  256. bulanik-0.3.0/tests/test_threepart.py +511 -0
  257. bulanik-0.3.0/tests/test_triangular.py +200 -0
  258. bulanik-0.3.0/tests/test_tsukamoto.py +105 -0
  259. bulanik-0.3.0/tests/test_type2.py +545 -0
  260. bulanik-0.3.0/tests/test_validate.py +69 -0
  261. bulanik-0.3.0/tests/test_weighting.py +315 -0
  262. bulanik-0.3.0/uv.lock +3124 -0
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .coverage
6
+ htmlcov/
7
+ dist/
8
+ .superpowers/
9
+ .hypothesis/
10
+ *.egg-info/
11
+ build/
12
+ /site/
13
+ /.cache/
14
+ /benchmarks/.cache/
@@ -0,0 +1,127 @@
1
+ # Changelog
2
+
3
+ All notable changes to bulanik are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.3.0] - 2026-11-05
8
+
9
+ Interval-valued AHP and fuzzy versions of three more ranking methods. The public v0.2 API is unchanged apart from
10
+ the two changes listed below.
11
+
12
+ ### Added
13
+
14
+ - `ahp_interval_valued`: AHP over interval-valued orthopair comparison matrices, with the
15
+ `multiplicative` and `possibility` methods.
16
+ - `fuzzy_codas`, `fuzzy_waspas` (with `combine="fuzzy"` or `combine="scores"`) and
17
+ `fuzzy_cocoso`, also reachable through `rank_fuzzy(method=...)`.
18
+ - An `IVQROFN` score named `"defuzzified"` (q = 1).
19
+ - Examples 23 and 24 (24 executed examples in all).
20
+
21
+ ### Changed
22
+
23
+ - Crisp `codas` accepts zeros in benefit columns.
24
+ - `ANFIS.fit` results differ from v0.2: `lr` is now the largest step tried, and hybrid training
25
+ solves the consequents once more per `fit`, so `history` values change.
26
+
27
+ ### Fixed
28
+
29
+ - Crisp `cocoso` treats rounding-size gaps at a column extreme as zero.
30
+ - `ANFIS.fit` kept gradient steps that raised the training error and could return a model far worse
31
+ than least squares alone, with no warning. A step that raises the error is now halved or dropped,
32
+ and hybrid training ends on solved consequents.
33
+
34
+ [0.3.0]: https://github.com/tdelphi1981/bulanik/releases/tag/v0.3.0
35
+
36
+ ## [0.2.0] - 2026-11-04
37
+
38
+ Interval-valued and interval type-2 numbers, six new ranking methods, new weighting
39
+ methods, DEMATEL and three new figures. The public v0.1 API is unchanged: this release
40
+ only adds names.
41
+
42
+ ### Added
43
+
44
+ - Interval-valued numbers: `IVQROFN` (interval-valued q-rung orthopair) with the aliases
45
+ `IVIFN` (q = 1), `IVPFN` (q = 2) and `IVFFN` (q = 3), and `IVNeutrosophicFN` (interval
46
+ neutrosophic). Frozen and validated, with the algebraic and Einstein operator families,
47
+ scores, accuracies and distances. The aliases are separate classes: they neither compare
48
+ equal to nor mix with an `IVQROFN` of the same rung.
49
+ - `IT2TrFN`: interval type-2 trapezoidal fuzzy numbers (upper and lower trapezoid with
50
+ heights, lower nested inside upper), with the arithmetic of Chen and Lee (2010a) and the
51
+ DTraT score of Kahraman et al. (2014).
52
+ - Ranking methods: CODAS, COPRAS, MARCOS, CoCoSo, WISP and ELECTRE I (with net concordance
53
+ and discordance indices for a complete ranking), all registered in
54
+ `rank_fuzzy(method=...)`. Zero, negative and constant columns, and signed default
55
+ scores, raise a `ValueError` naming the method where the method cannot handle them
56
+ (constant columns are neutral in CoCoSo, CRITIC and COPRAS; ELECTRE and CoCoSo accept zeros
57
+ and negatives); a magnitude spread so large that a scaled entry underflows to 0 raises the
58
+ same way in CODAS, MARCOS and WISP.
59
+ - Weighting methods: CRITIC (objective), PIPRECIA (crisp and `TFN` judgements, with an
60
+ optional inverse pass) and `ahp_fuzzy_numbers` (AHP over a comparison matrix of any one
61
+ fuzzy number kind). PIPRECIA requires every judgement `s_j` to lie in (0, 2) and names the
62
+ offending index otherwise.
63
+ - DEMATEL: `dematel` for crisp or fuzzy direct-relation matrices, `cfcs` (Opricovic and
64
+ Tzeng 2003) to convert triangular matrices from several experts, and `DEMATELResult` with
65
+ `as_weights()` so the factor weights feed every ranking method. A singular or
66
+ ill-conditioned $I - N$ (spectral radius numerically 1) raises instead of returning a huge
67
+ total relation.
68
+ - `bulanik.plot`: `dematel` (cause-effect diagram), `iv_orthopair` (interval rectangles over
69
+ the feasible region) and `it2_number` (upper and lower trapezoids with the footprint of
70
+ uncertainty).
71
+ - Documentation: guide pages for interval-valued numbers, interval type-2 numbers, the new
72
+ ranking and weighting methods and DEMATEL; new gallery figures; API reference for all new
73
+ names; examples 18 to 22 (22 executed examples in all); the reference list extended to
74
+ every source cited in the v0.2 docstrings, each DOI checked against Crossref.
75
+
76
+ ### Changed
77
+
78
+ - The dark categorical plot palette, slot 6 (green), is `#2ea32e` instead of `#008300`,
79
+ which was too dark on the slate surface. Figures that use the sixth colour change in dark
80
+ mode only.
81
+
82
+ ### Notes on sourcing
83
+
84
+ - The 7-term interval type-2 linguistic scale in example 19 and in the gallery is
85
+ illustrative: it follows the style of the type-2 literature, and its values are not a
86
+ transcription of any published table.
87
+ - The CRITIC tests are checked against values derived by hand from the published formulas,
88
+ not against a worked example printed in the paper.
89
+ - The DEMATEL tests include a cross-check against the vignette of the R package `dematel`.
90
+
91
+ [0.2.0]: https://github.com/tdelphi1981/bulanik/releases/tag/v0.2.0
92
+
93
+ ## [0.1.0] - 2026-11-03
94
+
95
+ First release.
96
+
97
+ ### Added
98
+
99
+ - Fuzzy numbers: triangular (`TFN`) and trapezoidal (`TrFN`); intuitionistic (`IFN`),
100
+ Pythagorean (`PFN`), Fermatean (`FFN`) and q-rung orthopair (`QROFN`); spherical
101
+ (`SphericalFN`), picture (`PictureFN`) and single-valued neutrosophic
102
+ (`NeutrosophicFN`). Immutable, validated on construction, with named operator
103
+ families (`algebraic`, `einstein`), scores, accuracies and distances.
104
+ - Aggregation: weighted average and weighted geometric mean, and expert aggregation of
105
+ `[expert][alternative][criterion]` ratings.
106
+ - Fuzzy sets: membership functions (triangular, trapezoidal, gaussian, bell, sigmoid,
107
+ linear ramps), linguistic variables, t-norms and t-conorms by name, and linguistic
108
+ scales (`SAATY_TFN`, `FUZZY_BWM_TFN` and illustrative IF/TFN scales).
109
+ - Fuzzy inference: Mamdani, Sugeno (zero and first order) and Tsukamoto systems from
110
+ plain-text rules, with `explain` and a no-rule-fired policy.
111
+ - Interval type-2 inference: interval type-2 membership functions and Mamdani and
112
+ Sugeno systems, with KM, EKM, Nie-Tan and BMM type reduction.
113
+ - ANFIS: grid-partition first-order Sugeno ANFIS with hybrid (Jang) or gradient
114
+ training, exported to a Sugeno system.
115
+ - MCDM: AHP, fuzzy AHP (Buckley), SWARA and entropy weights; best-worst consistency
116
+ indices (crisp and fuzzy); TOPSIS, VIKOR, WASPAS and EDAS on crisp matrices;
117
+ `DecisionMatrix` and `rank_fuzzy` for fuzzy-number matrices; Chen's fuzzy TOPSIS.
118
+ - `bulanik.lumix` (extra `lumix`): linear best-worst method, Zimmermann fuzzy
119
+ programming and MCDM scores as LumiX data.
120
+ - `bulanik.sezgi` (extra `sezgi`): nonlinear and fuzzy best-worst method and ANFIS
121
+ training with sezgi metaheuristics.
122
+ - `bulanik.plot` (extra `plot`): figures for numbers, scales, inference, type-2 sets and
123
+ MCDM results in matching light and dark themes.
124
+ - Documentation site (MkDocs Material) with a user guide, API reference, 17 executed
125
+ examples and a verified reference list; 17 runnable scripts in `examples/`.
126
+
127
+ [0.1.0]: https://github.com/tdelphi1981/bulanik/releases/tag/v0.1.0
@@ -0,0 +1,29 @@
1
+ cff-version: 1.2.0
2
+ message: >-
3
+ If you use bulanik in your research, please cite it using the metadata
4
+ below.
5
+ type: software
6
+ title: bulanik
7
+ version: 0.3.0
8
+ date-released: "2026-11-05"
9
+ abstract: >-
10
+ Easy-to-use fuzzy numbers, fuzzy inference and fuzzy multi-criteria
11
+ decision making in pure Python, with optional bridges to LumiX and sezgi
12
+ and publication-quality figures.
13
+ authors:
14
+ - family-names: Berber
15
+ given-names: Tolga
16
+ email: tolga.berber@fen.ktu.edu.tr
17
+ orcid: "https://orcid.org/0000-0002-6487-5581"
18
+ affiliation: "Department of Computer Science, Karadeniz Technical University"
19
+ license: MIT
20
+ repository-code: "https://github.com/tdelphi1981/bulanik"
21
+ url: "https://tdelphi1981.github.io/bulanik/"
22
+ keywords:
23
+ - fuzzy-logic
24
+ - fuzzy-inference
25
+ - fuzzy-numbers
26
+ - mcdm
27
+ - anfis
28
+ - optimization
29
+ - python
@@ -0,0 +1,51 @@
1
+ # Contributing to bulanik
2
+
3
+ ## Development setup
4
+
5
+ ```bash
6
+ git clone https://github.com/tdelphi1981/bulanik
7
+ cd bulanik
8
+ uv sync # dev group: pytest, hypothesis, coverage, matplotlib, lumix-opt, ortools, sezgi
9
+ ```
10
+
11
+ ## Tests
12
+
13
+ ```bash
14
+ uv run python -m pytest -q
15
+ ```
16
+
17
+ The suite runs every script in `examples/`. Coverage is measured only when you ask for
18
+ it, and then it must stay at or above 95% (`fail_under` in `pyproject.toml`):
19
+
20
+ ```bash
21
+ uv run python -m pytest -q --cov=bulanik
22
+ ```
23
+
24
+ Do not run pytest with `-W error`: OR-Tools' SWIG bindings emit warnings that
25
+ turn into crashes. Property tests use Hypothesis.
26
+
27
+ ## Documentation
28
+
29
+ The site is MkDocs Material, sources in `docs/site/`, configuration in `mkdocs.yml`.
30
+
31
+ ```bash
32
+ uv sync --group docs
33
+ uv run mkdocs serve # live preview
34
+ uv run mkdocs build --strict # must finish with zero warnings
35
+ ```
36
+
37
+ `tests/test_docs.py` runs the strict build only when the docs group is installed
38
+ (`uv sync --group docs`); without it the test is skipped. Deselect it with
39
+ `-m "not docs"`. Building the site needs **Python 3.11 or newer** (the docs group pins a
40
+ matplotlib release without Python 3.10 wheels) and **network access** (the privacy plugin
41
+ downloads external assets such as the MathJax script). Python snippets in the docs are executed at
42
+ build time with markdown-exec, so the output shown is real.
43
+
44
+ ## Style
45
+
46
+ - Python 3.10+, type hints on public API, no runtime dependencies in the core.
47
+ - Numbers and sets are immutable; invalid input raises a specific error, never returns silently.
48
+ - Docstrings use Google style (`Args:`, `Returns:`, `Raises:`, `Examples:`).
49
+ - Cited works need a verified reference: add them to `docs/site/about/references.md` with a
50
+ DOI that resolves (check it against Crossref or the publisher), never from memory.
51
+ - Keep commits focused; do not change library behaviour in documentation commits.
bulanik-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tolga Berber
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.
bulanik-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.5
2
+ Name: bulanik
3
+ Version: 0.3.0
4
+ Summary: Easy-to-use fuzzy numbers, fuzzy inference and fuzzy MCDM, with LumiX and sezgi bridges
5
+ Project-URL: Homepage, https://github.com/tdelphi1981/bulanik
6
+ Project-URL: Documentation, https://tdelphi1981.github.io/bulanik/
7
+ Project-URL: Repository, https://github.com/tdelphi1981/bulanik
8
+ Project-URL: Issues, https://github.com/tdelphi1981/bulanik/issues
9
+ Project-URL: Changelog, https://github.com/tdelphi1981/bulanik/blob/main/CHANGELOG.md
10
+ Author-email: Tolga Berber <tolga.berber@fen.ktu.edu.tr>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: fuzzy,fuzzy-inference,fuzzy-logic,mcdm,optimization
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: lumix
25
+ Requires-Dist: lumix-opt>=0.3.2; extra == 'lumix'
26
+ Requires-Dist: ortools>=9.8; extra == 'lumix'
27
+ Provides-Extra: plot
28
+ Requires-Dist: matplotlib>=3.8; extra == 'plot'
29
+ Provides-Extra: sezgi
30
+ Requires-Dist: sezgi>=0.1.6; extra == 'sezgi'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # bulanik
34
+
35
+ Easy-to-use fuzzy numbers, fuzzy inference and fuzzy multi-criteria decision making
36
+ (MCDM) in pure Python, with no runtime dependencies. *Bulanık* is Turkish for *fuzzy*.
37
+ Optional bridges connect it to [LumiX](https://pypi.org/project/lumix-opt/) (optimization
38
+ modelling) and [sezgi](https://github.com/tdelphi1981/sezgi) (metaheuristics), and an
39
+ optional plotting module draws the results in a light and a dark theme.
40
+
41
+ **Documentation:** <https://tdelphi1981.github.io/bulanik/>
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install bulanik # core, no runtime dependencies
47
+ pip install "bulanik[lumix]" # + the LumiX bridge (bulanik.lumix)
48
+ pip install "bulanik[sezgi]" # + the sezgi bridge (bulanik.sezgi)
49
+ pip install "bulanik[plot]" # + figures (bulanik.plot, needs matplotlib)
50
+ pip install "bulanik[lumix,sezgi,plot]" # everything
51
+ ```
52
+
53
+ Python 3.10 to 3.13. For development, clone the repository and run `uv sync` (see
54
+ [Contributing](https://github.com/tdelphi1981/bulanik/blob/main/CONTRIBUTING.md)).
55
+
56
+ ## Quickstart
57
+
58
+ ```python
59
+ from bulanik import IFN, TFN
60
+ from bulanik.aggregate import wa
61
+
62
+ a, b = IFN(0.6, 0.3), IFN(0.5, 0.4)
63
+ print(a + b) # IFN(mu=0.8, nu=0.12)
64
+ print(a.add(b, ops="einstein")) # the Einstein operator family
65
+ print(wa([a, b], [0.7, 0.3])) # weighted average
66
+ print((TFN(1, 2, 3) + TFN(3, 4, 5)).score()) # crisp score for ranking
67
+ ```
68
+
69
+ Every operator family, score and method is chosen by name (`ops="einstein"`,
70
+ `score("normalized")`); an unknown name raises a `ValueError` that lists the valid ones.
71
+
72
+ ## What is in it
73
+
74
+ - **Fuzzy numbers:** triangular and trapezoidal; intuitionistic, Pythagorean, Fermatean
75
+ and q-rung orthopair; spherical, picture and neutrosophic; their interval-valued
76
+ versions (`IVIFN`, `IVPFN`, `IVFFN`, `IVQROFN`, `IVNeutrosophicFN`) and interval type-2
77
+ trapezoidal numbers (`IT2TrFN`). Immutable and validated.
78
+ - **Aggregation, sets and scales:** weighted average and geometric mean, expert
79
+ aggregation, membership functions, linguistic variables and linguistic scales.
80
+ - **Fuzzy inference:** Mamdani, Sugeno and Tsukamoto systems from plain-text rules;
81
+ interval type-2 systems with KM, EKM, Nie-Tan and BMM type reduction; ANFIS.
82
+ - **Decision making:** AHP, fuzzy AHP (also over any number kind), SWARA, entropy, CRITIC
83
+ and PIPRECIA weights; DEMATEL with CFCS; best-worst consistency; TOPSIS, VIKOR, WASPAS,
84
+ EDAS, CODAS, COPRAS, MARCOS, CoCoSo, WISP and ELECTRE, on crisp matrices or fuzzy
85
+ ratings (fuzzy CODAS, WASPAS and CoCoSo included), and Chen's fuzzy TOPSIS. Interval-valued
86
+ AHP (`ahp_interval_valued`) with the multiplicative and possibility methods.
87
+ - **Bridges:** with LumiX, the linear best-worst method, Zimmermann fuzzy programming and
88
+ MCDM scores as optimization data; with sezgi, nonlinear and fuzzy BWM and ANFIS training.
89
+ - **Figures:** `bulanik.plot` for numbers, scales, inference, rankings and DEMATEL.
90
+
91
+ The [documentation](https://tdelphi1981.github.io/bulanik/) has a user guide, an API
92
+ reference, and 24 runnable examples (also in [`examples/`](https://github.com/tdelphi1981/bulanik/tree/main/examples)).
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ uv sync
98
+ uv run python -m pytest -q
99
+ ```
100
+
101
+ The dev group installs LumiX, OR-Tools, sezgi and matplotlib, so the bridge tests and
102
+ every example run; without an extra, its tests and examples are skipped. Coverage is
103
+ measured on the pure-Python core.
104
+
105
+ ## Citing
106
+
107
+ Please cite bulanik using [`CITATION.cff`](https://github.com/tdelphi1981/bulanik/blob/main/CITATION.cff) (GitHub's "Cite this repository"),
108
+ and the original papers behind the methods you use, listed with DOIs in the
109
+ [references](https://tdelphi1981.github.io/bulanik/about/references.html).
110
+
111
+ ## License
112
+
113
+ MIT. See [LICENSE](https://github.com/tdelphi1981/bulanik/blob/main/LICENSE).
@@ -0,0 +1,81 @@
1
+ # bulanik
2
+
3
+ Easy-to-use fuzzy numbers, fuzzy inference and fuzzy multi-criteria decision making
4
+ (MCDM) in pure Python, with no runtime dependencies. *Bulanık* is Turkish for *fuzzy*.
5
+ Optional bridges connect it to [LumiX](https://pypi.org/project/lumix-opt/) (optimization
6
+ modelling) and [sezgi](https://github.com/tdelphi1981/sezgi) (metaheuristics), and an
7
+ optional plotting module draws the results in a light and a dark theme.
8
+
9
+ **Documentation:** <https://tdelphi1981.github.io/bulanik/>
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install bulanik # core, no runtime dependencies
15
+ pip install "bulanik[lumix]" # + the LumiX bridge (bulanik.lumix)
16
+ pip install "bulanik[sezgi]" # + the sezgi bridge (bulanik.sezgi)
17
+ pip install "bulanik[plot]" # + figures (bulanik.plot, needs matplotlib)
18
+ pip install "bulanik[lumix,sezgi,plot]" # everything
19
+ ```
20
+
21
+ Python 3.10 to 3.13. For development, clone the repository and run `uv sync` (see
22
+ [Contributing](https://github.com/tdelphi1981/bulanik/blob/main/CONTRIBUTING.md)).
23
+
24
+ ## Quickstart
25
+
26
+ ```python
27
+ from bulanik import IFN, TFN
28
+ from bulanik.aggregate import wa
29
+
30
+ a, b = IFN(0.6, 0.3), IFN(0.5, 0.4)
31
+ print(a + b) # IFN(mu=0.8, nu=0.12)
32
+ print(a.add(b, ops="einstein")) # the Einstein operator family
33
+ print(wa([a, b], [0.7, 0.3])) # weighted average
34
+ print((TFN(1, 2, 3) + TFN(3, 4, 5)).score()) # crisp score for ranking
35
+ ```
36
+
37
+ Every operator family, score and method is chosen by name (`ops="einstein"`,
38
+ `score("normalized")`); an unknown name raises a `ValueError` that lists the valid ones.
39
+
40
+ ## What is in it
41
+
42
+ - **Fuzzy numbers:** triangular and trapezoidal; intuitionistic, Pythagorean, Fermatean
43
+ and q-rung orthopair; spherical, picture and neutrosophic; their interval-valued
44
+ versions (`IVIFN`, `IVPFN`, `IVFFN`, `IVQROFN`, `IVNeutrosophicFN`) and interval type-2
45
+ trapezoidal numbers (`IT2TrFN`). Immutable and validated.
46
+ - **Aggregation, sets and scales:** weighted average and geometric mean, expert
47
+ aggregation, membership functions, linguistic variables and linguistic scales.
48
+ - **Fuzzy inference:** Mamdani, Sugeno and Tsukamoto systems from plain-text rules;
49
+ interval type-2 systems with KM, EKM, Nie-Tan and BMM type reduction; ANFIS.
50
+ - **Decision making:** AHP, fuzzy AHP (also over any number kind), SWARA, entropy, CRITIC
51
+ and PIPRECIA weights; DEMATEL with CFCS; best-worst consistency; TOPSIS, VIKOR, WASPAS,
52
+ EDAS, CODAS, COPRAS, MARCOS, CoCoSo, WISP and ELECTRE, on crisp matrices or fuzzy
53
+ ratings (fuzzy CODAS, WASPAS and CoCoSo included), and Chen's fuzzy TOPSIS. Interval-valued
54
+ AHP (`ahp_interval_valued`) with the multiplicative and possibility methods.
55
+ - **Bridges:** with LumiX, the linear best-worst method, Zimmermann fuzzy programming and
56
+ MCDM scores as optimization data; with sezgi, nonlinear and fuzzy BWM and ANFIS training.
57
+ - **Figures:** `bulanik.plot` for numbers, scales, inference, rankings and DEMATEL.
58
+
59
+ The [documentation](https://tdelphi1981.github.io/bulanik/) has a user guide, an API
60
+ reference, and 24 runnable examples (also in [`examples/`](https://github.com/tdelphi1981/bulanik/tree/main/examples)).
61
+
62
+ ## Development
63
+
64
+ ```bash
65
+ uv sync
66
+ uv run python -m pytest -q
67
+ ```
68
+
69
+ The dev group installs LumiX, OR-Tools, sezgi and matplotlib, so the bridge tests and
70
+ every example run; without an extra, its tests and examples are skipped. Coverage is
71
+ measured on the pure-Python core.
72
+
73
+ ## Citing
74
+
75
+ Please cite bulanik using [`CITATION.cff`](https://github.com/tdelphi1981/bulanik/blob/main/CITATION.cff) (GitHub's "Cite this repository"),
76
+ and the original papers behind the methods you use, listed with DOIs in the
77
+ [references](https://tdelphi1981.github.io/bulanik/about/references.html).
78
+
79
+ ## License
80
+
81
+ MIT. See [LICENSE](https://github.com/tdelphi1981/bulanik/blob/main/LICENSE).
@@ -0,0 +1,32 @@
1
+ """Root pytest configuration: doctest collection over ``src/bulanik``.
2
+
3
+ ``bulanik.lumix`` and ``bulanik.sezgi`` import optional extras at module level,
4
+ so collecting their doctests would fail without the extra. Skip a bridge module
5
+ when its dependencies are not importable; likewise ``bulanik.plot`` without
6
+ matplotlib. With matplotlib, doctests and tests draw on the non-interactive
7
+ Agg backend and every figure is closed after each test.
8
+ """
9
+
10
+ import importlib.util
11
+
12
+ import pytest
13
+
14
+ collect_ignore = []
15
+
16
+ if any(importlib.util.find_spec(m) is None for m in ("lumix", "ortools")):
17
+ collect_ignore.append("src/bulanik/lumix.py")
18
+ if importlib.util.find_spec("sezgi") is None:
19
+ collect_ignore.append("src/bulanik/sezgi.py")
20
+ if importlib.util.find_spec("matplotlib") is None:
21
+ collect_ignore.append("src/bulanik/plot")
22
+ else:
23
+ import matplotlib
24
+
25
+ matplotlib.use("Agg")
26
+
27
+ @pytest.fixture(autouse=True)
28
+ def _close_matplotlib_figures():
29
+ yield
30
+ import matplotlib.pyplot as plt
31
+
32
+ plt.close("all")
@@ -0,0 +1,177 @@
1
+ """Shared plumbing for the docs figure scripts.
2
+
3
+ All plotting goes through `bulanik.plot`; this module only
4
+
5
+ - renders a figure in both house themes and saves `<name>-light.svg` and
6
+ `<name>-dark.svg` into `docs/site/assets/figures/` with deterministic output
7
+ (fixed SVG id salt, no creation date), and
8
+ - offers page-level layout helpers: multi-panel figures and annotations
9
+ (notes, brackets, arrows, markers) drawn with the active theme's tokens from
10
+ `bulanik.plot.current_theme()`.
11
+
12
+ A figure is a function `draw() -> Figure | Axes` that runs inside the theme.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import io
18
+ import os
19
+ import re
20
+ import sys
21
+ from collections.abc import Callable, Mapping
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ os.environ.setdefault("MPLBACKEND", "Agg")
26
+
27
+ import matplotlib as mpl # noqa: E402
28
+ import matplotlib.pyplot as plt # noqa: E402
29
+ from matplotlib import patheffects # noqa: E402
30
+ from matplotlib.axes import Axes # noqa: E402
31
+ from matplotlib.figure import Figure # noqa: E402
32
+
33
+ from bulanik import plot # noqa: E402
34
+
35
+ FIGURES = Path(__file__).resolve().parents[1] / "site" / "assets" / "figures"
36
+ THEMES = ("light", "dark")
37
+ Draw = Callable[[], "Figure | Axes"]
38
+
39
+
40
+ def tokens() -> Mapping[str, Any]:
41
+ """The active theme's tokens (`bulanik.plot.current_theme()`)."""
42
+ return plot.current_theme()
43
+
44
+
45
+ def render(name: str, draw: Draw, out: Path = FIGURES) -> list[Path]:
46
+ """Draw the figure once per theme and save `<name>-<theme>.svg`."""
47
+ out.mkdir(parents=True, exist_ok=True)
48
+ paths = []
49
+ for mode in THEMES:
50
+ with plot.theme(mode), mpl.rc_context({"svg.hashsalt": f"bulanik-{name}"}):
51
+ res = draw()
52
+ fig = res.figure if isinstance(res, Axes) else res
53
+ path = out / f"{name}-{mode}.svg"
54
+ buffer = io.StringIO()
55
+ fig.savefig(buffer, format="svg", metadata={"Date": None, "Creator": "bulanik docs"})
56
+ plt.close(fig)
57
+ path.write_text(compact(buffer.getvalue()), encoding="utf-8")
58
+ paths.append(path)
59
+ return paths
60
+
61
+
62
+ _DECIMALS = re.compile(r"(\d\.\d{2})\d+")
63
+ _PATH_DATA = re.compile(r' d="[^"]*"') # only path geometry; transforms keep full precision
64
+
65
+
66
+ def compact(svg: str) -> str:
67
+ """Round path coordinates to 0.01 pt: invisible at any zoom, and the files shrink by ~40%.
68
+
69
+ Haloed labels are written as outline paths (not glyph references), so the
70
+ coordinate text dominates the file size.
71
+ """
72
+ return _PATH_DATA.sub(lambda m: _DECIMALS.sub(r"\1", m.group(0)), svg)
73
+
74
+
75
+ def the_text(ax: Axes, *, startswith: str | None = None, equals: str | None = None):
76
+ """The one text of `ax` that matches; fail loudly on zero or several matches."""
77
+ found = [
78
+ t
79
+ for t in ax.texts
80
+ if (t.get_text().startswith(startswith) if startswith is not None else t.get_text() == equals)
81
+ ]
82
+ if len(found) != 1:
83
+ raise AssertionError(f"expected exactly one text matching {startswith or equals!r}, found {len(found)}")
84
+ return found[0]
85
+
86
+
87
+ def run(figures: Mapping[str, Draw], argv: list[str] | None = None) -> None:
88
+ """Command-line entry: render all figures, or only the names given."""
89
+ names = (argv if argv is not None else sys.argv[1:]) or list(figures)
90
+ for name in names:
91
+ for path in render(name, figures[name]):
92
+ print(path.relative_to(Path.cwd()) if path.is_relative_to(Path.cwd()) else path)
93
+
94
+
95
+ # -- layout ---------------------------------------------------------------------------
96
+
97
+
98
+ def panels(nrows: int, ncols: int, *, figsize: tuple[float, float], **kw: Any) -> tuple[Figure, Any]:
99
+ """A constrained-layout figure with a grid of axes, in the active theme."""
100
+ fig, axes = plt.subplots(nrows, ncols, figsize=figsize, layout="constrained", **kw)
101
+ return fig, axes
102
+
103
+
104
+ def halo(width: float = 3.0) -> list[Any]:
105
+ """Surface-coloured outline so lines never cut through a label."""
106
+ return [patheffects.withStroke(linewidth=width, foreground=tokens()["surface"], alpha=0.9)]
107
+
108
+
109
+ def note(ax: Axes, x: float, y: float, text: str, *, ink: str = "text_secondary", size: float = 8.5, **kw: Any) -> Any:
110
+ """Annotation text in a text token (never a series colour), with a halo."""
111
+ kw.setdefault("ha", "center")
112
+ kw.setdefault("va", "bottom")
113
+ kw.setdefault("zorder", 9)
114
+ return ax.text(x, y, text, color=tokens()[ink], fontsize=size, path_effects=halo(), **kw)
115
+
116
+
117
+ def bracket(ax: Axes, x0: float, x1: float, y: float, text: str, *, tick: float = 0.035, below: bool = False) -> None:
118
+ """A hairline bracket over [x0, x1] at height y with a centred label."""
119
+ t = tokens()
120
+ s = -1 if below else 1
121
+ ax.plot([x0, x0, x1, x1], [y - s * tick, y, y, y - s * tick], color=t["muted"], lw=0.8,
122
+ solid_joinstyle="miter", zorder=7, clip_on=False)
123
+ note(ax, (x0 + x1) / 2, y + s * 0.02, text, va="top" if below else "bottom", ink="text_secondary")
124
+
125
+
126
+ def arrow(ax: Axes, start: tuple[float, float], end: tuple[float, float], color: str, *,
127
+ lw: float = 1.2, shrink: float = 6.0, alpha: float = 1.0, rad: float = 0.0,
128
+ shrink_end: float | None = None) -> None:
129
+ """A thin arrow in a series colour (a mark, so it may carry the hue).
130
+
131
+ `shrink_end` (points) stops the head short of the target marker; it defaults to `shrink`.
132
+ """
133
+ ax.annotate(
134
+ "", xy=end, xytext=start, zorder=6,
135
+ arrowprops=dict(arrowstyle="-|>,head_length=0.45,head_width=0.22", color=color, lw=lw,
136
+ shrinkA=shrink, shrinkB=shrink if shrink_end is None else shrink_end, alpha=alpha,
137
+ connectionstyle=f"arc3,rad={rad}"),
138
+ )
139
+
140
+
141
+ def dot(ax: Axes, x: float, y: float, color: str, *, size: float = 8.0, marker: str = "o", zorder: float = 8) -> None:
142
+ """A marker with the house 2 px surface ring."""
143
+ ax.plot([x], [y], marker=marker, ms=size, mfc=color, mec=tokens()["surface"], mew=1.6,
144
+ linestyle="none", zorder=zorder, clip_on=False)
145
+
146
+
147
+ def key(ax: Axes, items: list[tuple[str, str, str]], *, loc: str = "upper right", **kw: Any) -> Any:
148
+ """A compact legend: (marker, colour, text) per entry, text in ink tokens."""
149
+ from matplotlib.lines import Line2D
150
+
151
+ t = tokens()
152
+ handles = [
153
+ Line2D([], [], marker=m, linestyle="none", ms=7, mfc=c, mec=t["surface"], mew=1.4, label=s)
154
+ if m else Line2D([], [], color=c, lw=1.8, label=s)
155
+ for m, c, s in items
156
+ ]
157
+ kw.setdefault("fontsize", 8.5)
158
+ leg = ax.legend(handles=handles, loc=loc, frameon=False, labelcolor=t["text_secondary"],
159
+ handletextpad=0.4, borderaxespad=0.3, **kw)
160
+ leg.set_zorder(10)
161
+ return leg
162
+
163
+
164
+ def label(ax: Axes, x: float, y: float, text: str, *, dx: float = 7, dy: float = 0, ha: str = "left",
165
+ va: str = "center", size: float = 8.5, ink: str = "text_secondary") -> Any:
166
+ """Point label offset by (dx, dy) points, in a text token with a halo."""
167
+ return ax.annotate(text, (x, y), xytext=(dx, dy), textcoords="offset points", ha=ha, va=va,
168
+ fontsize=size, color=tokens()[ink], path_effects=halo(), zorder=9)
169
+
170
+
171
+ def callout(ax: Axes, xy: tuple[float, float], at: tuple[float, float], text: str, *, ha: str = "left",
172
+ va: str = "center", size: float = 8.5, ink: str = "text_secondary") -> Any:
173
+ """A label placed at data point `at`, tied to `xy` by a hairline leader."""
174
+ t = tokens()
175
+ return ax.annotate(text, xy, xytext=at, textcoords="data", ha=ha, va=va, fontsize=size, color=t[ink],
176
+ path_effects=halo(), zorder=9,
177
+ arrowprops=dict(arrowstyle="-", color=t["muted"], lw=0.6, shrinkA=3, shrinkB=6))