kaneko-fa-selector 0.2.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.
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: kaneko-fa-selector
3
+ Version: 0.2.0
4
+ Summary: KANEKO flame arrester preselection rule engine (R1-R8): JSON in, JSON out
5
+ Author-email: stkengaposa832@gmail.com
6
+ Keywords: kaneko,flame arrester,rule engine,preselection
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Manufacturing
9
+ Classifier: Topic :: Scientific/Engineering
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Operating System :: OS Independent
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: PyYAML>=6.0
20
+ Provides-Extra: app
21
+ Requires-Dist: streamlit>=1.35; extra == "app"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Requires-Dist: build>=1.0; extra == "dev"
25
+ Requires-Dist: twine>=5.0; extra == "dev"
26
+
27
+ # KANEKO FA Selector
28
+
29
+ Thư viện Python tiền chọn flame arrester KANEKO theo bộ luật R1–R8 (deterministic).
30
+ Gọi một hàm, nhận JSON. Kèm app demo Streamlit chạy trên nền thư viện và CLI.
31
+
32
+ > **Giới hạn kỹ thuật quan trọng.** Đây là bản demo / tiền chọn, không phải công cụ thiết kế đã được phê duyệt.
33
+ > Tài liệu nguồn mô tả bước R5 (lưu lượng / ΔP) nhưng **không** cung cấp điểm đường cong hay hệ số `(a, b)`,
34
+ > nên thư viện cố ý không bịa giá trị ΔP và không bao giờ báo R5 "đạt". Tập tra cứu khí chỉ là một phần nhỏ
35
+ > của bảng 215 chất; chất không nhận diện được sẽ không bị đoán. Acetylene luôn chuyển kỹ thuật.
36
+ > **Mọi model đề xuất đều là tiền chọn và phải được kỹ thuật KANEKO xác nhận trước khi báo giá.**
37
+
38
+ ## Cài đặt
39
+
40
+ Thư viện chỉ phụ thuộc PyYAML, chạy trên Python 3.10 trở lên.
41
+
42
+ ```bash
43
+ # Từ PyPI (sau khi đã phát hành, xem mục "Phát hành lên PyPI")
44
+ pip install kaneko-fa-selector
45
+
46
+ # Từ thư mục mã nguồn này
47
+ pip install . # bản cố định
48
+ pip install -e ".[app,dev]" # editable (sửa code có hiệu lực ngay) + Streamlit + pytest + build + twine
49
+
50
+ # Từ file wheel đã build (xem mục "Đóng gói")
51
+ pip install dist/kaneko_fa_selector-0.2.0-py3-none-any.whl
52
+ ```
53
+
54
+ Không cần cài cũng chạy được: thêm thư mục `src` vào `PYTHONPATH` (app demo và test đã tự làm việc này).
55
+
56
+ ## Đóng gói
57
+
58
+ ```powershell
59
+ .\build_dist.ps1
60
+ ```
61
+
62
+ Script tự cài công cụ `build` vào `.venv` (hoặc Python mặc định nếu không có `.venv`), xoá `dist/` cũ rồi tạo:
63
+
64
+ - `dist/kaneko_fa_selector-0.2.0-py3-none-any.whl`: file cài đặt, dùng cho `pip install` / `requirements.txt`
65
+ - `dist/kaneko_fa_selector-0.2.0.tar.gz`: mã nguồn đóng gói (sdist)
66
+
67
+ Lệnh tương đương: `python -m build --outdir dist`. Trước khi build bản mới, tăng `__version__` trong
68
+ `src/kaneko_fa_selector/__init__.py` (tên file wheel đổi theo).
69
+
70
+ ## Phát hành lên PyPI
71
+
72
+ PyPI là kho **công khai**: ai cũng tải được package, đọc được README và các bảng dữ liệu trong `engine.py`.
73
+ Chỉ phát hành khi chắc chắn dữ liệu KANEKO trong thư viện được phép công khai. Nếu chỉ dùng nội bộ, dùng wheel
74
+ hoặc Git riêng như mục "Dùng trong dự án khác".
75
+
76
+ Chuẩn bị một lần:
77
+
78
+ 1. Tạo tài khoản tại https://pypi.org/account/register/ và https://test.pypi.org/account/register/ (để thử), bật xác thực hai lớp.
79
+ 2. Tạo API token quyền upload tại https://pypi.org/manage/account/token/ (và trang tương ứng trên TestPyPI).
80
+ Lần đầu chọn scope "Entire account"; sau khi project đã tồn tại thì tạo token riêng cho project.
81
+ Token bắt đầu bằng `pypi-` và chỉ hiển thị một lần.
82
+ 3. Khai báo giấy phép: thêm `license = "MIT"` (hoặc giấy phép bạn chọn) vào `[project]` trong `pyproject.toml`
83
+ và tạo file `LICENSE`. Không bắt buộc về kỹ thuật nhưng package công khai nên có.
84
+ 4. Kiểm tra lại `authors` trong `pyproject.toml`: email ghi ở đó sẽ hiển thị công khai trên trang PyPI.
85
+
86
+ Mỗi lần phát hành:
87
+
88
+ ```powershell
89
+ # 1. Tăng __version__ trong src/kaneko_fa_selector/__init__.py (PyPI không cho đăng lại phiên bản đã có)
90
+
91
+ # 2. Thử trên TestPyPI
92
+ $env:TEST_PYPI_TOKEN = "pypi-..."
93
+ .\publish.ps1 -Test
94
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ kaneko-fa-selector
95
+
96
+ # 3. Phát hành thật
97
+ $env:PYPI_TOKEN = "pypi-..."
98
+ .\publish.ps1
99
+ pip install kaneko-fa-selector
100
+ ```
101
+
102
+ `publish.ps1` build lại `dist/`, chạy `twine check --strict` rồi `twine upload`. Token chỉ nằm trong biến môi trường
103
+ của phiên PowerShell hiện tại, không ghi ra file. Nếu Windows chặn chạy script:
104
+ `powershell -ExecutionPolicy Bypass -File .\publish.ps1 -Test`.
105
+
106
+ ## Dùng trong dự án khác
107
+
108
+ Đã phát hành lên PyPI thì chỉ cần một dòng trong `requirements.txt` của dự án kia:
109
+
110
+ ```
111
+ kaneko-fa-selector>=0.2.0
112
+ ```
113
+
114
+ Chưa phát hành, hoặc muốn dùng nội bộ, chọn một trong ba cách:
115
+
116
+ 1. **Chép file wheel vào dự án đó** (ví dụ thư mục `libs/`), đường dẫn tương đối tính từ nơi chạy `pip`:
117
+
118
+ ```
119
+ libs/kaneko_fa_selector-0.2.0-py3-none-any.whl
120
+ ```
121
+
122
+ 2. **Trỏ thẳng tới file wheel trên máy** bằng URL `file:` (đường dẫn có dấu tiếng Việt / khoảng trắng phải percent-encode):
123
+
124
+ ```
125
+ kaneko-fa-selector @ file:///C:/duong/dan/toi/kaneko_fa_selector_app/dist/kaneko_fa_selector-0.2.0-py3-none-any.whl
126
+ ```
127
+
128
+ Lấy URL đúng bằng:
129
+ `python -c "from pathlib import Path; print(Path('dist/kaneko_fa_selector-0.2.0-py3-none-any.whl').resolve().as_uri())"`
130
+
131
+ 3. **Khi đã đưa mã nguồn lên Git** (GitHub / GitLab):
132
+
133
+ ```
134
+ kaneko-fa-selector @ git+https://github.com/<tai-khoan>/<repo>.git
135
+ ```
136
+
137
+ Rồi trong dự án kia chạy `pip install -r requirements.txt`. Không dùng `requirements.txt` thì cài trực tiếp:
138
+
139
+ ```bash
140
+ pip install "C:\...\dist\kaneko_fa_selector-0.2.0-py3-none-any.whl" # từ wheel
141
+ pip install "C:\...\kaneko_fa_selector_app" # từ thư mục mã nguồn
142
+ ```
143
+
144
+ Kiểm tra nhanh sau khi cài:
145
+
146
+ ```bash
147
+ python -c "import kaneko_fa_selector as k; print(k.__version__)"
148
+ kaneko-fa-selector '{"gas_name": "methane", "operating_pressure_kpag": 8}'
149
+ ```
150
+
151
+ ## Dùng nhanh
152
+
153
+ ```python
154
+ from kaneko_fa_selector import select, select_json
155
+
156
+ # 1) Nhận chuỗi JSON
157
+ print(select_json({"gas_name": "methane", "operating_pressure_kpag": 8,
158
+ "service_location": "End-of-line", "requested_dn": 50}))
159
+
160
+ # 2) Nhận dict để xử lý tiếp trong Python
161
+ result = select(gas_name="hydrogen", operating_pressure_kpag=5,
162
+ service_location="In-line", pipe_geometry="Exact L/D known", ld_ratio=20,
163
+ ibexu_requirement="yes")
164
+ print(result["status"], result["primary_candidate"]["model_code"])
165
+
166
+ # 3) Đầu vào là chuỗi JSON (ví dụ nhận từ AI agent / HTTP)
167
+ print(select_json('{"gas_name": "ethanol", "operating_pressure_kpag": 12}'))
168
+ ```
169
+
170
+ Trường bắt buộc duy nhất là `operating_pressure_kpag`; các trường khác có mặc định bảo thủ.
171
+
172
+ Lỗi đầu vào:
173
+
174
+ - `select` ném `SelectorInputError` (thuộc tính `.errors` là danh sách thông báo).
175
+ - `select_json` mặc định **không** ném mà trả JSON với `"status": "INPUT_ERROR"` và danh sách `errors`.
176
+ Dùng `select_json(..., strict=True)` nếu muốn ném exception.
177
+
178
+ ## CLI
179
+
180
+ ```bash
181
+ python -m kaneko_fa_selector '{"gas_name": "methane", "operating_pressure_kpag": 8}'
182
+ python -m kaneko_fa_selector --file input.json --compact
183
+ type input.json | python -m kaneko_fa_selector
184
+ python -m kaneko_fa_selector --fields # mô tả các trường đầu vào
185
+ kaneko-fa-selector ... # lệnh tương đương sau khi pip install
186
+ ```
187
+
188
+ Đầu ra luôn là JSON UTF-8 (thêm `--ascii` để escape tiếng Việt). Đầu vào từ stdin / file là UTF-8 và
189
+ chấp nhận BOM (PowerShell hay chèn BOM khi pipe). Mã thoát `0` khi engine chạy xong
190
+ (kể cả `TECHNICAL_REVIEW` / `NO_CANDIDATE`), `2` khi đầu vào sai.
191
+
192
+ ## Trường đầu vào
193
+
194
+ | Trường | Kiểu | Mặc định | Ghi chú |
195
+ |---|---|---|---|
196
+ | `gas_name` | chuỗi | `""` | Tra trong tập demo (không phân biệt hoa/thường). Không nhận diện được → `TECHNICAL_REVIEW`. |
197
+ | `manual_gas_group` | `IIA1` `IIA` `IIB1` `IIB2` `IIB3` `IIB` `IIC` | `null` | Ưu tiên hơn `gas_name`. Acetylene vẫn luôn dừng. |
198
+ | `operating_pressure_kpag` | số ≥ 0 | **bắt buộc** | Áp suất làm việc kPaG. |
199
+ | `service_location` | `End-of-line` \| `In-line` | `In-line` | Alias: `eol`, `end_of_line`, `inline`, `in_line`. |
200
+ | `pipe_geometry` | `Short, unobstructed` \| `Exact L/D known` \| `Long / obstacles / unknown` | `Long / obstacles / unknown` | Alias: `short`, `exact_ld`, `long`, `unknown`. |
201
+ | `ld_ratio` | số > 0 | `null` | Chỉ dùng khi `pipe_geometry = Exact L/D known`. Thiếu → mặc định DETONATION. |
202
+ | `ibexu_requirement` | `yes` \| `no` \| `unknown` | `unknown` | Nhận cả `true`/`false`, `Có`/`Không`/`Chưa rõ`. |
203
+ | `requested_dn` | số hoặc chuỗi | `null` | Chèn **nguyên văn** vào mã model (quy định nguồn), chỉ dùng để ưu tiên xếp hạng. |
204
+ | `ifrt_low_pressure` | bool | `false` | Dùng bảng IFRT áp thấp IBExU. |
205
+ | `flow_rate_m3h` | số > 0 | `null` | R5 hiện chưa tính ΔP; chỉ sinh cảnh báo. |
206
+ | `max_delta_p_kpa` | số > 0 | `null` | Như trên. |
207
+ | `families` | danh sách con của `IFRF` `IFRT` `FCRT` (hoặc chuỗi `"IFRF,IFRT"`) | cả ba | Luôn được sắp về thứ tự IFRF → IFRT → FCRT. |
208
+
209
+ Quy ước: trường tuỳ chọn không có dữ liệu thì để `null`, **không** dùng `0` (tránh L/D = 0 vô tình thành DEFLAGRATION).
210
+ Trường lạ bị từ chối để bắt lỗi gõ sai tên.
211
+
212
+ ## Cấu trúc kết quả
213
+
214
+ | Khoá | Ý nghĩa |
215
+ |---|---|
216
+ | `status` | `PRELIMINARY` (có model tiền chọn) · `NO_CANDIDATE` (không model nào qua lọc) · `TECHNICAL_REVIEW` (dừng ở R1) · `INPUT_ERROR` (chỉ `select_json` / CLI) |
217
+ | `decision` | `REVIEW_REQUIRED` · `TECHNICAL_REVIEW` · `STOP` |
218
+ | `gas_group` | Nhóm khí đã xác định, hoặc `null` |
219
+ | `device_type` | `DEFLAGRATION` · `DETONATION` · `null` |
220
+ | `primary_candidate` | Model tiền chọn (R6) hoặc `null`: `series`, `dn`, `pressure_kpag`, `certified`, `gas_letter`, `reason`, `delta_p_status`, `delta_p`, `model_code` |
221
+ | `candidates` | Mọi ứng viên qua bộ lọc R3, đã xếp hạng |
222
+ | `upgrade_suggestions` | Gợi ý tham khảo R7 (tối đa 10, chỉ khi DEFLAGRATION) |
223
+ | `warnings` | Cảnh báo / việc cần kỹ thuật xác nhận (đã khử trùng lặp) |
224
+ | `steps` | Trace từng luật: `rule`, `status`, `detail` theo thứ tự R1 → R2 → R4 → R3 → R5 → R6 → R7 → R8 |
225
+ | `final_note` | Ghi chú cuối |
226
+ | `engine_version` | Phiên bản thư viện |
227
+ | `input` | Đầu vào sau chuẩn hoá (giá trị engine thực sự dùng) |
228
+ | `errors` | Chỉ có khi `status = INPUT_ERROR` |
229
+
230
+ Ví dụ rút gọn với đầu vào `{"gas_name": "methane", "operating_pressure_kpag": 8, "service_location": "End-of-line", "requested_dn": 50}`:
231
+
232
+ ```json
233
+ {
234
+ "status": "PRELIMINARY",
235
+ "decision": "REVIEW_REQUIRED",
236
+ "gas_group": "IIA1",
237
+ "device_type": "DEFLAGRATION",
238
+ "primary_candidate": {"series": "IFRF", "dn": 50, "pressure_kpag": 50.0, "certified": false,
239
+ "model_code": "IFRF-50-BB-{IIA1:chưa xác nhận}-P50", "...": "..."},
240
+ "candidates": ["... 20 ứng viên ..."],
241
+ "warnings": ["Kết quả là TIỀN CHỌN; kỹ thuật KANEKO phải xác nhận model trước khi báo giá.", "..."],
242
+ "steps": [{"rule": "R1", "status": "PASS", "detail": "Nhóm khí IIA1."}, "..."],
243
+ "final_note": "Chỉ là tiền chọn. Không dùng để đặt hàng/báo giá trước khi kỹ thuật KANEKO xác nhận."
244
+ }
245
+ ```
246
+
247
+ ## App demo Streamlit
248
+
249
+ ```bash
250
+ pip install -e ".[app]"
251
+ streamlit run app.py
252
+ ```
253
+
254
+ ## Test
255
+
256
+ ```bash
257
+ pip install -e ".[dev]"
258
+ pytest
259
+ ```
260
+
261
+ ## Cấu trúc thư mục
262
+
263
+ - `src/kaneko_fa_selector/__init__.py`: API công khai `select`, `select_json`, `load_config`
264
+ - `src/kaneko_fa_selector/engine.py`: luật R1–R8 và các bảng tra (nguồn sự thật của logic)
265
+ - `src/kaneko_fa_selector/inputs.py`: chuẩn hoá / kiểm tra đầu vào, `SelectorInputError`
266
+ - `src/kaneko_fa_selector/__main__.py`: CLI
267
+ - `src/kaneko_fa_selector/rules/kaneko_fa_rules.yaml`: cấu hình tham chiếu (đi kèm package)
268
+ - `src/selector.py`: shim tương thích cho code cũ `from selector import ...`
269
+ - `app.py`: demo Streamlit
270
+ - `tests/`: test thư viện, CLI và app demo
271
+ - `pyproject.toml`: metadata và cấu hình đóng gói; `requirements.txt`: cài editable kèm Streamlit cho demo
272
+ - `build_dist.ps1`: build wheel + sdist vào `dist/` (thư mục `dist/` được `.gitignore` bỏ qua)
273
+ - `source_rule_engine.docx`, `docs_flowchart.png`, `van_relation.md`: tài liệu nguồn và sơ đồ dòng sản phẩm
274
+
275
+ ## Đã triển khai từ tài liệu nguồn
276
+
277
+ - R1: nhóm khí, tra theo tên hoặc chọn tay; chất lạ và acetylene chuyển kỹ thuật
278
+ - R2: loại thiết bị theo end-of-line, L/D (ngưỡng 30 cho IIC, 50 cho nhóm khác) và điều kiện ống; mặc định bảo thủ DETONATION
279
+ - R4: nhánh IBExU (`yes` chỉ bản chứng nhận, `no` chỉ bản thường, `unknown` cả hai, ưu tiên bản thường)
280
+ - R3: lọc dòng sản phẩm theo bảng rating áp suất trong nguồn
281
+ - R5: báo rõ chưa có hệ số / điểm đường cong; không bịa ΔP
282
+ - R6: chọn ứng viên theo DN khách → DN nhỏ nhất → bản thường trước → rating thấp hơn
283
+ - R7: gợi ý nâng cấp khi kết quả là DEFLAGRATION
284
+ - R8: ghép mã model `{series}-{dn}-BB-{chữ nhóm}-P{rating}[-EX]` kèm cảnh báo
285
+
286
+ ## Lưu ý khi đọc logic
287
+
288
+ - File YAML chỉ để tham chiếu / hiển thị. Các bảng tra thực sự nằm trong `engine.py`; sửa YAML không đổi kết quả.
289
+ - Loại thiết bị ở R2 (DEFLAGRATION / DETONATION) chưa được dùng để lọc dòng sản phẩm ở R3; engine xét mọi dòng trong `families`.
290
+ - Bước quy đổi cột biểu đồ A/B/C (`gas_column`) có trong code để tham chiếu nhưng `run_engine` chưa gọi.
291
+ - R7 chỉ là lát cắt của danh sách ứng viên (DN ≥ model chính), không tra bảng model Detonation riêng.
292
+ - Khi có `requested_dn`, mọi mã model đều mang DN đó; cột `dn` trong ứng viên mới là DN catalog thật.
293
+
294
+ ## Cần bổ sung để dùng thật
295
+
296
+ 1. Bảng đầy đủ 215 chất (EN/JP/MESG).
297
+ 2. Đường cong lưu lượng KANEKO đã số hoá và/hoặc hệ số `(a, b)` được phê duyệt cho mọi tổ hợp, kèm khoảng Q hợp lệ.
298
+ 3. Catalog DN / áp suất / model và ma trận chứng nhận chính xác.
299
+ 4. Kỹ thuật KANEKO rà soát chính thức và bộ test case được chủ sở hữu phê duyệt.
@@ -0,0 +1,273 @@
1
+ # KANEKO FA Selector
2
+
3
+ Thư viện Python tiền chọn flame arrester KANEKO theo bộ luật R1–R8 (deterministic).
4
+ Gọi một hàm, nhận JSON. Kèm app demo Streamlit chạy trên nền thư viện và CLI.
5
+
6
+ > **Giới hạn kỹ thuật quan trọng.** Đây là bản demo / tiền chọn, không phải công cụ thiết kế đã được phê duyệt.
7
+ > Tài liệu nguồn mô tả bước R5 (lưu lượng / ΔP) nhưng **không** cung cấp điểm đường cong hay hệ số `(a, b)`,
8
+ > nên thư viện cố ý không bịa giá trị ΔP và không bao giờ báo R5 "đạt". Tập tra cứu khí chỉ là một phần nhỏ
9
+ > của bảng 215 chất; chất không nhận diện được sẽ không bị đoán. Acetylene luôn chuyển kỹ thuật.
10
+ > **Mọi model đề xuất đều là tiền chọn và phải được kỹ thuật KANEKO xác nhận trước khi báo giá.**
11
+
12
+ ## Cài đặt
13
+
14
+ Thư viện chỉ phụ thuộc PyYAML, chạy trên Python 3.10 trở lên.
15
+
16
+ ```bash
17
+ # Từ PyPI (sau khi đã phát hành, xem mục "Phát hành lên PyPI")
18
+ pip install kaneko-fa-selector
19
+
20
+ # Từ thư mục mã nguồn này
21
+ pip install . # bản cố định
22
+ pip install -e ".[app,dev]" # editable (sửa code có hiệu lực ngay) + Streamlit + pytest + build + twine
23
+
24
+ # Từ file wheel đã build (xem mục "Đóng gói")
25
+ pip install dist/kaneko_fa_selector-0.2.0-py3-none-any.whl
26
+ ```
27
+
28
+ Không cần cài cũng chạy được: thêm thư mục `src` vào `PYTHONPATH` (app demo và test đã tự làm việc này).
29
+
30
+ ## Đóng gói
31
+
32
+ ```powershell
33
+ .\build_dist.ps1
34
+ ```
35
+
36
+ Script tự cài công cụ `build` vào `.venv` (hoặc Python mặc định nếu không có `.venv`), xoá `dist/` cũ rồi tạo:
37
+
38
+ - `dist/kaneko_fa_selector-0.2.0-py3-none-any.whl`: file cài đặt, dùng cho `pip install` / `requirements.txt`
39
+ - `dist/kaneko_fa_selector-0.2.0.tar.gz`: mã nguồn đóng gói (sdist)
40
+
41
+ Lệnh tương đương: `python -m build --outdir dist`. Trước khi build bản mới, tăng `__version__` trong
42
+ `src/kaneko_fa_selector/__init__.py` (tên file wheel đổi theo).
43
+
44
+ ## Phát hành lên PyPI
45
+
46
+ PyPI là kho **công khai**: ai cũng tải được package, đọc được README và các bảng dữ liệu trong `engine.py`.
47
+ Chỉ phát hành khi chắc chắn dữ liệu KANEKO trong thư viện được phép công khai. Nếu chỉ dùng nội bộ, dùng wheel
48
+ hoặc Git riêng như mục "Dùng trong dự án khác".
49
+
50
+ Chuẩn bị một lần:
51
+
52
+ 1. Tạo tài khoản tại https://pypi.org/account/register/ và https://test.pypi.org/account/register/ (để thử), bật xác thực hai lớp.
53
+ 2. Tạo API token quyền upload tại https://pypi.org/manage/account/token/ (và trang tương ứng trên TestPyPI).
54
+ Lần đầu chọn scope "Entire account"; sau khi project đã tồn tại thì tạo token riêng cho project.
55
+ Token bắt đầu bằng `pypi-` và chỉ hiển thị một lần.
56
+ 3. Khai báo giấy phép: thêm `license = "MIT"` (hoặc giấy phép bạn chọn) vào `[project]` trong `pyproject.toml`
57
+ và tạo file `LICENSE`. Không bắt buộc về kỹ thuật nhưng package công khai nên có.
58
+ 4. Kiểm tra lại `authors` trong `pyproject.toml`: email ghi ở đó sẽ hiển thị công khai trên trang PyPI.
59
+
60
+ Mỗi lần phát hành:
61
+
62
+ ```powershell
63
+ # 1. Tăng __version__ trong src/kaneko_fa_selector/__init__.py (PyPI không cho đăng lại phiên bản đã có)
64
+
65
+ # 2. Thử trên TestPyPI
66
+ $env:TEST_PYPI_TOKEN = "pypi-..."
67
+ .\publish.ps1 -Test
68
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ kaneko-fa-selector
69
+
70
+ # 3. Phát hành thật
71
+ $env:PYPI_TOKEN = "pypi-..."
72
+ .\publish.ps1
73
+ pip install kaneko-fa-selector
74
+ ```
75
+
76
+ `publish.ps1` build lại `dist/`, chạy `twine check --strict` rồi `twine upload`. Token chỉ nằm trong biến môi trường
77
+ của phiên PowerShell hiện tại, không ghi ra file. Nếu Windows chặn chạy script:
78
+ `powershell -ExecutionPolicy Bypass -File .\publish.ps1 -Test`.
79
+
80
+ ## Dùng trong dự án khác
81
+
82
+ Đã phát hành lên PyPI thì chỉ cần một dòng trong `requirements.txt` của dự án kia:
83
+
84
+ ```
85
+ kaneko-fa-selector>=0.2.0
86
+ ```
87
+
88
+ Chưa phát hành, hoặc muốn dùng nội bộ, chọn một trong ba cách:
89
+
90
+ 1. **Chép file wheel vào dự án đó** (ví dụ thư mục `libs/`), đường dẫn tương đối tính từ nơi chạy `pip`:
91
+
92
+ ```
93
+ libs/kaneko_fa_selector-0.2.0-py3-none-any.whl
94
+ ```
95
+
96
+ 2. **Trỏ thẳng tới file wheel trên máy** bằng URL `file:` (đường dẫn có dấu tiếng Việt / khoảng trắng phải percent-encode):
97
+
98
+ ```
99
+ kaneko-fa-selector @ file:///C:/duong/dan/toi/kaneko_fa_selector_app/dist/kaneko_fa_selector-0.2.0-py3-none-any.whl
100
+ ```
101
+
102
+ Lấy URL đúng bằng:
103
+ `python -c "from pathlib import Path; print(Path('dist/kaneko_fa_selector-0.2.0-py3-none-any.whl').resolve().as_uri())"`
104
+
105
+ 3. **Khi đã đưa mã nguồn lên Git** (GitHub / GitLab):
106
+
107
+ ```
108
+ kaneko-fa-selector @ git+https://github.com/<tai-khoan>/<repo>.git
109
+ ```
110
+
111
+ Rồi trong dự án kia chạy `pip install -r requirements.txt`. Không dùng `requirements.txt` thì cài trực tiếp:
112
+
113
+ ```bash
114
+ pip install "C:\...\dist\kaneko_fa_selector-0.2.0-py3-none-any.whl" # từ wheel
115
+ pip install "C:\...\kaneko_fa_selector_app" # từ thư mục mã nguồn
116
+ ```
117
+
118
+ Kiểm tra nhanh sau khi cài:
119
+
120
+ ```bash
121
+ python -c "import kaneko_fa_selector as k; print(k.__version__)"
122
+ kaneko-fa-selector '{"gas_name": "methane", "operating_pressure_kpag": 8}'
123
+ ```
124
+
125
+ ## Dùng nhanh
126
+
127
+ ```python
128
+ from kaneko_fa_selector import select, select_json
129
+
130
+ # 1) Nhận chuỗi JSON
131
+ print(select_json({"gas_name": "methane", "operating_pressure_kpag": 8,
132
+ "service_location": "End-of-line", "requested_dn": 50}))
133
+
134
+ # 2) Nhận dict để xử lý tiếp trong Python
135
+ result = select(gas_name="hydrogen", operating_pressure_kpag=5,
136
+ service_location="In-line", pipe_geometry="Exact L/D known", ld_ratio=20,
137
+ ibexu_requirement="yes")
138
+ print(result["status"], result["primary_candidate"]["model_code"])
139
+
140
+ # 3) Đầu vào là chuỗi JSON (ví dụ nhận từ AI agent / HTTP)
141
+ print(select_json('{"gas_name": "ethanol", "operating_pressure_kpag": 12}'))
142
+ ```
143
+
144
+ Trường bắt buộc duy nhất là `operating_pressure_kpag`; các trường khác có mặc định bảo thủ.
145
+
146
+ Lỗi đầu vào:
147
+
148
+ - `select` ném `SelectorInputError` (thuộc tính `.errors` là danh sách thông báo).
149
+ - `select_json` mặc định **không** ném mà trả JSON với `"status": "INPUT_ERROR"` và danh sách `errors`.
150
+ Dùng `select_json(..., strict=True)` nếu muốn ném exception.
151
+
152
+ ## CLI
153
+
154
+ ```bash
155
+ python -m kaneko_fa_selector '{"gas_name": "methane", "operating_pressure_kpag": 8}'
156
+ python -m kaneko_fa_selector --file input.json --compact
157
+ type input.json | python -m kaneko_fa_selector
158
+ python -m kaneko_fa_selector --fields # mô tả các trường đầu vào
159
+ kaneko-fa-selector ... # lệnh tương đương sau khi pip install
160
+ ```
161
+
162
+ Đầu ra luôn là JSON UTF-8 (thêm `--ascii` để escape tiếng Việt). Đầu vào từ stdin / file là UTF-8 và
163
+ chấp nhận BOM (PowerShell hay chèn BOM khi pipe). Mã thoát `0` khi engine chạy xong
164
+ (kể cả `TECHNICAL_REVIEW` / `NO_CANDIDATE`), `2` khi đầu vào sai.
165
+
166
+ ## Trường đầu vào
167
+
168
+ | Trường | Kiểu | Mặc định | Ghi chú |
169
+ |---|---|---|---|
170
+ | `gas_name` | chuỗi | `""` | Tra trong tập demo (không phân biệt hoa/thường). Không nhận diện được → `TECHNICAL_REVIEW`. |
171
+ | `manual_gas_group` | `IIA1` `IIA` `IIB1` `IIB2` `IIB3` `IIB` `IIC` | `null` | Ưu tiên hơn `gas_name`. Acetylene vẫn luôn dừng. |
172
+ | `operating_pressure_kpag` | số ≥ 0 | **bắt buộc** | Áp suất làm việc kPaG. |
173
+ | `service_location` | `End-of-line` \| `In-line` | `In-line` | Alias: `eol`, `end_of_line`, `inline`, `in_line`. |
174
+ | `pipe_geometry` | `Short, unobstructed` \| `Exact L/D known` \| `Long / obstacles / unknown` | `Long / obstacles / unknown` | Alias: `short`, `exact_ld`, `long`, `unknown`. |
175
+ | `ld_ratio` | số > 0 | `null` | Chỉ dùng khi `pipe_geometry = Exact L/D known`. Thiếu → mặc định DETONATION. |
176
+ | `ibexu_requirement` | `yes` \| `no` \| `unknown` | `unknown` | Nhận cả `true`/`false`, `Có`/`Không`/`Chưa rõ`. |
177
+ | `requested_dn` | số hoặc chuỗi | `null` | Chèn **nguyên văn** vào mã model (quy định nguồn), chỉ dùng để ưu tiên xếp hạng. |
178
+ | `ifrt_low_pressure` | bool | `false` | Dùng bảng IFRT áp thấp IBExU. |
179
+ | `flow_rate_m3h` | số > 0 | `null` | R5 hiện chưa tính ΔP; chỉ sinh cảnh báo. |
180
+ | `max_delta_p_kpa` | số > 0 | `null` | Như trên. |
181
+ | `families` | danh sách con của `IFRF` `IFRT` `FCRT` (hoặc chuỗi `"IFRF,IFRT"`) | cả ba | Luôn được sắp về thứ tự IFRF → IFRT → FCRT. |
182
+
183
+ Quy ước: trường tuỳ chọn không có dữ liệu thì để `null`, **không** dùng `0` (tránh L/D = 0 vô tình thành DEFLAGRATION).
184
+ Trường lạ bị từ chối để bắt lỗi gõ sai tên.
185
+
186
+ ## Cấu trúc kết quả
187
+
188
+ | Khoá | Ý nghĩa |
189
+ |---|---|
190
+ | `status` | `PRELIMINARY` (có model tiền chọn) · `NO_CANDIDATE` (không model nào qua lọc) · `TECHNICAL_REVIEW` (dừng ở R1) · `INPUT_ERROR` (chỉ `select_json` / CLI) |
191
+ | `decision` | `REVIEW_REQUIRED` · `TECHNICAL_REVIEW` · `STOP` |
192
+ | `gas_group` | Nhóm khí đã xác định, hoặc `null` |
193
+ | `device_type` | `DEFLAGRATION` · `DETONATION` · `null` |
194
+ | `primary_candidate` | Model tiền chọn (R6) hoặc `null`: `series`, `dn`, `pressure_kpag`, `certified`, `gas_letter`, `reason`, `delta_p_status`, `delta_p`, `model_code` |
195
+ | `candidates` | Mọi ứng viên qua bộ lọc R3, đã xếp hạng |
196
+ | `upgrade_suggestions` | Gợi ý tham khảo R7 (tối đa 10, chỉ khi DEFLAGRATION) |
197
+ | `warnings` | Cảnh báo / việc cần kỹ thuật xác nhận (đã khử trùng lặp) |
198
+ | `steps` | Trace từng luật: `rule`, `status`, `detail` theo thứ tự R1 → R2 → R4 → R3 → R5 → R6 → R7 → R8 |
199
+ | `final_note` | Ghi chú cuối |
200
+ | `engine_version` | Phiên bản thư viện |
201
+ | `input` | Đầu vào sau chuẩn hoá (giá trị engine thực sự dùng) |
202
+ | `errors` | Chỉ có khi `status = INPUT_ERROR` |
203
+
204
+ Ví dụ rút gọn với đầu vào `{"gas_name": "methane", "operating_pressure_kpag": 8, "service_location": "End-of-line", "requested_dn": 50}`:
205
+
206
+ ```json
207
+ {
208
+ "status": "PRELIMINARY",
209
+ "decision": "REVIEW_REQUIRED",
210
+ "gas_group": "IIA1",
211
+ "device_type": "DEFLAGRATION",
212
+ "primary_candidate": {"series": "IFRF", "dn": 50, "pressure_kpag": 50.0, "certified": false,
213
+ "model_code": "IFRF-50-BB-{IIA1:chưa xác nhận}-P50", "...": "..."},
214
+ "candidates": ["... 20 ứng viên ..."],
215
+ "warnings": ["Kết quả là TIỀN CHỌN; kỹ thuật KANEKO phải xác nhận model trước khi báo giá.", "..."],
216
+ "steps": [{"rule": "R1", "status": "PASS", "detail": "Nhóm khí IIA1."}, "..."],
217
+ "final_note": "Chỉ là tiền chọn. Không dùng để đặt hàng/báo giá trước khi kỹ thuật KANEKO xác nhận."
218
+ }
219
+ ```
220
+
221
+ ## App demo Streamlit
222
+
223
+ ```bash
224
+ pip install -e ".[app]"
225
+ streamlit run app.py
226
+ ```
227
+
228
+ ## Test
229
+
230
+ ```bash
231
+ pip install -e ".[dev]"
232
+ pytest
233
+ ```
234
+
235
+ ## Cấu trúc thư mục
236
+
237
+ - `src/kaneko_fa_selector/__init__.py`: API công khai `select`, `select_json`, `load_config`
238
+ - `src/kaneko_fa_selector/engine.py`: luật R1–R8 và các bảng tra (nguồn sự thật của logic)
239
+ - `src/kaneko_fa_selector/inputs.py`: chuẩn hoá / kiểm tra đầu vào, `SelectorInputError`
240
+ - `src/kaneko_fa_selector/__main__.py`: CLI
241
+ - `src/kaneko_fa_selector/rules/kaneko_fa_rules.yaml`: cấu hình tham chiếu (đi kèm package)
242
+ - `src/selector.py`: shim tương thích cho code cũ `from selector import ...`
243
+ - `app.py`: demo Streamlit
244
+ - `tests/`: test thư viện, CLI và app demo
245
+ - `pyproject.toml`: metadata và cấu hình đóng gói; `requirements.txt`: cài editable kèm Streamlit cho demo
246
+ - `build_dist.ps1`: build wheel + sdist vào `dist/` (thư mục `dist/` được `.gitignore` bỏ qua)
247
+ - `source_rule_engine.docx`, `docs_flowchart.png`, `van_relation.md`: tài liệu nguồn và sơ đồ dòng sản phẩm
248
+
249
+ ## Đã triển khai từ tài liệu nguồn
250
+
251
+ - R1: nhóm khí, tra theo tên hoặc chọn tay; chất lạ và acetylene chuyển kỹ thuật
252
+ - R2: loại thiết bị theo end-of-line, L/D (ngưỡng 30 cho IIC, 50 cho nhóm khác) và điều kiện ống; mặc định bảo thủ DETONATION
253
+ - R4: nhánh IBExU (`yes` chỉ bản chứng nhận, `no` chỉ bản thường, `unknown` cả hai, ưu tiên bản thường)
254
+ - R3: lọc dòng sản phẩm theo bảng rating áp suất trong nguồn
255
+ - R5: báo rõ chưa có hệ số / điểm đường cong; không bịa ΔP
256
+ - R6: chọn ứng viên theo DN khách → DN nhỏ nhất → bản thường trước → rating thấp hơn
257
+ - R7: gợi ý nâng cấp khi kết quả là DEFLAGRATION
258
+ - R8: ghép mã model `{series}-{dn}-BB-{chữ nhóm}-P{rating}[-EX]` kèm cảnh báo
259
+
260
+ ## Lưu ý khi đọc logic
261
+
262
+ - File YAML chỉ để tham chiếu / hiển thị. Các bảng tra thực sự nằm trong `engine.py`; sửa YAML không đổi kết quả.
263
+ - Loại thiết bị ở R2 (DEFLAGRATION / DETONATION) chưa được dùng để lọc dòng sản phẩm ở R3; engine xét mọi dòng trong `families`.
264
+ - Bước quy đổi cột biểu đồ A/B/C (`gas_column`) có trong code để tham chiếu nhưng `run_engine` chưa gọi.
265
+ - R7 chỉ là lát cắt của danh sách ứng viên (DN ≥ model chính), không tra bảng model Detonation riêng.
266
+ - Khi có `requested_dn`, mọi mã model đều mang DN đó; cột `dn` trong ứng viên mới là DN catalog thật.
267
+
268
+ ## Cần bổ sung để dùng thật
269
+
270
+ 1. Bảng đầy đủ 215 chất (EN/JP/MESG).
271
+ 2. Đường cong lưu lượng KANEKO đã số hoá và/hoặc hệ số `(a, b)` được phê duyệt cho mọi tổ hợp, kèm khoảng Q hợp lệ.
272
+ 3. Catalog DN / áp suất / model và ma trận chứng nhận chính xác.
273
+ 4. Kỹ thuật KANEKO rà soát chính thức và bộ test case được chủ sở hữu phê duyệt.
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "kaneko-fa-selector"
7
+ dynamic = ["version"]
8
+ description = "KANEKO flame arrester preselection rule engine (R1-R8): JSON in, JSON out"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["PyYAML>=6.0"]
12
+ authors = [{ email = "stkengaposa832@gmail.com" }]
13
+ keywords = ["kaneko", "flame arrester", "rule engine", "preselection"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Manufacturing",
17
+ "Topic :: Scientific/Engineering",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Operating System :: OS Independent",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ app = ["streamlit>=1.35"]
29
+ dev = ["pytest>=8.0", "build>=1.0", "twine>=5.0"]
30
+
31
+ [project.scripts]
32
+ kaneko-fa-selector = "kaneko_fa_selector.__main__:main"
33
+
34
+ [tool.setuptools]
35
+ package-dir = {"" = "src"}
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.setuptools.package-data]
41
+ kaneko_fa_selector = ["rules/*.yaml"]
42
+
43
+ [tool.setuptools.dynamic]
44
+ version = {attr = "kaneko_fa_selector.__version__"}
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+