From b86290c331611e366281e270bb6bc56646884913 Mon Sep 17 00:00:00 2001 From: AgentLabCn <130165633+AgentLabCn@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:51:51 +0800 Subject: [PATCH] =?UTF-8?q?GA=E5=9F=BA=E6=9C=AC=E6=AD=A3=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 34 ++++ build_factory_data.py | 73 ++++++++ data/2025/ProductionLine.csv | 226 +++++++++++------------ data/2025/ProductionLine未修改.csv | 106 ----------- data/2025/benchmark未修改.csv | 28 --- data/2025/factory_data.csv | 28 +++ data/2025/factory_data_bak.csv | 28 +++ data/2025/model_params.json | 35 +--- ga_calibration_all_factories.py | 75 ++++++++ ga_calibration_by_one_factory.py | 280 +++++++++++++++++++++++++++++ ga_two_factor_calibration.py | 121 ------------- production_line.py | 31 +++- schedule_pulp.py | 173 ------------------ simulation_model.py | 225 +++++++++++++++++++---- 14 files changed, 848 insertions(+), 615 deletions(-) create mode 100644 AGENTS.md create mode 100644 build_factory_data.py delete mode 100644 data/2025/ProductionLine未修改.csv delete mode 100644 data/2025/benchmark未修改.csv create mode 100644 data/2025/factory_data.csv create mode 100644 data/2025/factory_data_bak.csv create mode 100644 ga_calibration_all_factories.py create mode 100644 ga_calibration_by_one_factory.py delete mode 100644 ga_two_factor_calibration.py delete mode 100644 schedule_pulp.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fe33552 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Core simulation: `simulation_model.py` (Mesa `Model`), `production_line.py`, `demand_agent.py`. +- UI & orchestration: `app.py` (Solara dashboard, matplotlib plotting, loads `data/` based on `year.json`). +- Optimization helpers: `genetic_calibration.py`, `ga_two_factor_calibration.py`, `build_distance_matrix.py`. +- Data: `data//` CSV/JSON inputs and outputs; temporary artifacts can live in `output/`. + +## Setup, Build, and Run +- Create env: `conda env create -f environment.yml` then `conda activate gw`. +- Launch dashboard locally: `solara run app.py --host 0.0.0.0 --port 8765` (loads year set in `year.json`). +- Recompute distance matrix when locations change: `python build_distance_matrix.py`. +- Run calibration scripts headless: `python genetic_calibration.py` or `python ga_two_factor_calibration.py` (writes tuned params under `data/`). + +## Coding Style & Naming Conventions +- Follow Python 3.13 + PEP 8; 4-space indents; prefer type hints and docstrings for public functions/classes. +- Use `snake_case` for variables/functions/modules, `CapWords` for classes, and descriptive names for data columns. +- Keep plotting/fonts consistent with existing matplotlib config (Chinese labels enabled); reuse helpers instead of duplicating logic. +- Guard data-file access with clear error messages and UTF-8/GBK fallbacks as seen in `app.py`. + +## Testing Guidelines +- No formal suite exists yet; add `pytest`-style tests under `tests/` when feasible, mocking file I/O and using small sample CSVs. +- For quick checks, run calibration scripts on a narrowed product set and validate outputs in `output/` or `data/` before committing. +- Validate UI flows by running `solara` and confirming charts render and download links work for the target year. + +## Commit & Pull Request Guidelines +- Commit messages: short, imperative summaries (e.g., `Add capacity cap to Shenzhen lines`); keep body focused on rationale and data assumptions. +- PRs should include: scope summary, affected year(s) in `data/`, before/after metrics or screenshots for UI changes, and steps to reproduce or rerun (`solara run ...`, script commands). +- Link related issues/requests and note any regenerated files (e.g., `distance_matrix.csv`, calibration outputs) so reviewers can verify. + +## Data & Configuration Tips +- Set `year.json` to the active dataset before running any scripts; keep year directories consistent (`data/2025`, `data/2026`, etc.). +- Preserve CSV headers and encodings; when adding factories/products, update `factory_mapping.json` and rerun distance matrix generation. +- Avoid committing large intermediate artifacts under `output/` unless they are required inputs; prefer deterministic script outputs for reviewability. diff --git a/build_factory_data.py b/build_factory_data.py new file mode 100644 index 0000000..107463f --- /dev/null +++ b/build_factory_data.py @@ -0,0 +1,73 @@ +import json +import os +import pandas as pd +from pathlib import Path + + +def load_factory_mapping(year: int) -> dict: + path = os.path.join("data", str(year), "factory_mapping.json") + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def load_model_params(year: int) -> dict: + path = os.path.join("data", str(year), "model_params.json") + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + +def load_benchmark_factories(year: int) -> list[str]: + path = Path("data") / str(year) / "benchmark.csv" + encodings = ("utf-8", "utf-8-sig", "gbk") + last_error = None + df = None + for enc in encodings: + try: + df = pd.read_csv(path, encoding=enc) + break + except UnicodeDecodeError as exc: + last_error = exc + continue + if df is None: + raise last_error if last_error else FileNotFoundError(f"Missing {path}") + # Assume first column holds factory name (aligns with factory_mapping keys) + factory_col = df.columns[0] + return df[factory_col].astype(str).str.strip().unique().tolist() + + +def build_factory_dataframe(year: int) -> pd.DataFrame: + mapping = load_factory_mapping(year) + params = load_model_params(year) + default_factor = params.get("factor_default", 1) + benchmark_factories = load_benchmark_factories(year) + rows = [] + for cn_name in benchmark_factories: + code = mapping.get(cn_name) + if code is None: + raise KeyError(f"benchmark.csv中的工厂“{cn_name}”未在 factory_mapping.json 中找到映射。") + factor = params.get(f"factor_{code}", default_factor if default_factor is not None else 1) + rows.append( + { + "工厂中文名": cn_name, + "工厂英文名": code, + "工厂平均磨合系数": float(factor), + "最小误差": 1_000_000, + } + ) + return pd.DataFrame(rows) + + +def write_factory_csv(df: pd.DataFrame, year: int) -> str: + out_path = os.path.join("data", str(year), "factory_data.csv") + df.to_csv(out_path, index=False, encoding="utf-8-sig") + return out_path + + +def main(): + year = 2025 + df = build_factory_dataframe(year) + out_path = write_factory_csv(df, year) + print(f"factory_data.csv generated at: {out_path}") + + +if __name__ == "__main__": + main() diff --git a/data/2025/ProductionLine.csv b/data/2025/ProductionLine.csv index 7cfb596..05b9eee 100644 --- a/data/2025/ProductionLine.csv +++ b/data/2025/ProductionLine.csv @@ -1,113 +1,113 @@ -区域名,工厂名,是否新工厂,产线ID,生产型号,开始月份,结束月份 -西北东部,艾郎玉门,是,艾郎玉门1,GWBD-A2,1,12 -西北东部,艾郎玉门,是,艾郎玉门2,GWBD-A2,1,12 -华北,艾郎张北,是,艾郎张北1,GWBD-A2,1,12 -华北,艾郎张北,是,艾郎张北2,GWBD-A2,1,12 -西北西部,时代巴里坤,是,时代巴里坤1,GWBD-A2,5,12 -西北西部,时代巴里坤,是,时代巴里坤2,GWBD-A2,5,12 -西北西部,时代巴里坤,是,时代巴里坤3,GW99A,3,4 -西北西部,时代巴里坤,是,时代巴里坤4,GW99A,6,9 -华南,时代百色,是,时代百色1,GWBD-A3,5,12 -华南,时代百色,是,时代百色2,GWBD-A3,5,12 -华东南北部,时代射阳,是,时代射阳1,GW130,9,12 -华东南北部,时代射阳,是,时代射阳2,GW130,9,12 -东北,时代松原,是,时代松原1,GWBD-A3,6,12 -东北,时代松原,是,时代松原2,GWBD-A3,6,12 -华东南南部,时代株洲,是,时代株洲1,GW99A,4,12 -华东南南部,时代株洲,是,时代株洲2,GW99A,4,12 -华东南北部,双瑞大丰,是,双瑞大丰1,GW110.5,2,11 -华东南北部,双瑞大丰,是,双瑞大丰2,GW110.5,2,11 -华东南北部,双瑞大丰,是,双瑞大丰3,GW93,1,1 -华东南北部,双瑞东营,否,双瑞东营1,GWBD-A2,10,12 -华东南北部,双瑞东营,否,双瑞东营2,GWBD-A2,10,12 -华东南北部,双瑞东营,否,双瑞东营3,GWBD-A2,10,12 -华东南北部,双瑞东营,否,双瑞东营4,GWBD-A2,10,12 -华东南北部,天顺濮阳,否,天顺濮阳1,GW99A,8,12 -华东南北部,天顺濮阳,否,天顺濮阳2,GW99A,8,12 -华东南北部,天顺沙洋,否,天顺沙洋1,GW93,2,2 -华东南北部,天顺沙洋,是,天顺沙洋2,GW93,5,5 -华东南北部,天顺沙洋,否,天顺沙洋3,GW93,11,11 -华东南北部,天顺沙洋,否,天顺沙洋4,GW99,10,11 -华东南北部,天顺沙洋,否,天顺沙洋5,GW99A,5,12 -华东南北部,天顺沙洋,否,天顺沙洋6,GW99A,5,12 -东北,中材白城,否,中材白城1,GWBD-A3,8,8 -华东南北部,中材阜宁,是,中材阜宁1,GW110.5,4,11 -华东南北部,中材阜宁,是,中材阜宁2,GW110.5,4,11 -华东南北部,中材阜宁,是,中材阜宁3,GW110.5,4,11 -华东南北部,中材阜宁,是,中材阜宁4,GW83.4,1,11 -华东南北部,中材阜宁,否,中材阜宁5,GW93,6,8 -华东南北部,中材阜宁,否,中材阜宁6,GW99A,1,7 -华东南北部,中材阜宁,否,中材阜宁7,GW99A,1,7 -华东南北部,中材阜宁,否,中材阜宁8,SI90.2,1,12 -华东南北部,中材阜宁,否,中材阜宁9,SI90.2,1,12 -华东南北部,中材阜宁,是,中材阜宁10,SI90.2,3,12 -华东南北部,中材阜宁,是,中材阜宁11,SI90.2,3,12 -华东南北部,中材阜宁,是,中材阜宁12,SI90.2,3,12 -华东南北部,中材阜宁,是,中材阜宁13,SI90.2,3,12 -西北西部,中材哈密,否,中材哈密1,GWBD-A3,7,11 -西北西部,中材哈密,否,中材哈密2,GWBD-A3,7,11 -华北,中材邯郸,否,中材邯郸1,GW99A,1,12 -华北,中材邯郸,否,中材邯郸2,GW99A,1,12 -华北,中材邯郸,是,中材邯郸3,GWBD-A2,4,12 -华北,中材邯郸,是,中材邯郸4,GWBD-A2,4,12 -西北东部,中材酒泉,否,中材酒泉1,GW99,1,12 -西北东部,中材酒泉,否,中材酒泉2,GW99,1,12 -西北东部,中材酒泉,否,中材酒泉3,GWBD-A2,2,12 -西北东部,中材酒泉,否,中材酒泉4,GWBD-A2,2,12 -西北东部,中材酒泉,否,中材酒泉5,GWBD-A2,2,12 -西北东部,中材酒泉,是,中材酒泉6,GWBD-A2,2,12 -西北东部,中材酒泉,否,中材酒泉7,SI90.2,1,1 -西北东部,中材酒泉,否,中材酒泉8,SI90.2,3,4 -西北东部,中材酒泉,否,中材酒泉9,SI90.2,3,4 -西北东部,中材酒泉,否,中材酒泉10,SI90.2,9,9 -西北东部,中材酒泉,是,中材酒泉11,SI90.2,4,12 -西北东部,中材酒泉,是,中材酒泉12,SI90.2,4,12 -华东南北部,中材连云港,是,中材连云港1,GW93,4,4 -华东南北部,中材连云港,否,中材连云港2,GW93,1,12 -华东南北部,中材连云港,否,中材连云港3,GW93,1,12 -华东南南部,中材萍乡,否,中材萍乡1,GW83.4,2,3 -华东南南部,中材萍乡,是,中材萍乡2,GW93,1,10 -华东南南部,中材萍乡,是,中材萍乡3,GW93,1,10 -华东南南部,中材萍乡,是,中材萍乡4,GWBD-A3,2,12 -华东南南部,中材萍乡,是,中材萍乡5,GWBD-A3,2,12 -东北,中材锡林,否,中材锡林1,GW99,1,12 -东北,中材锡林,否,中材锡林2,GW99,1,12 -东北,中材锡林,是,中材锡林3,GWBD-A2,2,12 -东北,中材锡林,是,中材锡林4,GWBD-A2,2,12 -东北,中材锡林,是,中材锡林5,GWBD-A2,2,12 -东北,中材锡林,否,中材锡林6,SI90.2,1,1 -东北,中材锡林,否,中材锡林7,SI90.2,1,1 -东北,中材兴安盟,是,中材兴安盟1,GW93,1,9 -东北,中材兴安盟,是,中材兴安盟2,GW93,1,9 -华南,中材阳江,是,中材阳江1,GW130,11,12 -华南,中材阳江,是,中材阳江2,GW130,11,12 -华南,中材阳江,否,中材阳江3,SI122,1,12 -华南,中材阳江,否,中材阳江4,SI122,1,12 -西北西部,中材伊吾,否,中材伊吾1,GWBD-A2,3,12 -西北西部,中材伊吾,否,中材伊吾2,GWBD-A2,3,12 -西北西部,中材伊吾,是,中材伊吾3,GWBD-A3,7,9 -华南,中材玉溪,否,中材玉溪1,GW93,3,3 -西北西部,重通昌吉,否,重通昌吉1,GW99,11,11 -西北西部,重通昌吉,是,重通昌吉2,GW99A,2,10 -西北西部,重通昌吉,否,重通昌吉3,GWBD-A2,1,12 -西北西部,重通昌吉,否,重通昌吉4,GWBD-A2,1,12 -西北西部,重通昌吉,否,重通昌吉5,GWBD-A2,1,12 -东北,重通大安,是,重通大安1,GWBD-A2,3,12 -东北,重通大安,是,重通大安2,GWBD-A2,4,12 -华东南北部,重通如东,是,重通如东1,GW110.5,6,10 -华东南北部,重通如东,是,重通如东2,GW110.5,6,10 -华东南北部,重通如东,否,重通如东3,GW76,1,6 -华东南北部,重通如东,否,重通如东4,GW76,1,6 -华东南北部,重通如东,否,重通如东5,GW76,11,12 -华东南北部,重通如东,否,重通如东6,GW76,11,12 -华东南北部,重通如东,否,重通如东7,GW81,1,12 -华东南北部,重通如东,否,重通如东8,GW81,1,12 -华东南北部,重通如东,否,重通如东9,GW86,10,12 -华东南北部,重通如东,否,重通如东10,GWBD-D,10,10 -华东南北部,重通如东,是,重通如东11,GW83.3,7,9 -西北东部,重通武威,否,重通武威1,GW83.4,1,1 -西北东部,重通武威,否,重通武威2,GW99A,1,1 -西北东部,重通武威,否,重通武威3,GWBD-A2,1,12 -西北东部,重通武威,否,重通武威4,GWBD-A2,1,12 -西北东部,重通武威,否,重通武威5,GWBD-A3,4,12 -西北东部,重通武威,否,重通武威6,GWBD-A3,4,12 +,,Ƿ¹,ID,ͺ,ʼ·,·,ĥϵ,ϵСֵ,ϵֵ +,,,1,GWBD-A2,1,12,1.07380061238005,0.5,3 +,,,2,GWBD-A2,1,12,1.3086756673917384,0.5,3 +,ű,,ű1,GWBD-A2,1,12,1.0005776141701213,0.5,3 +,ű,,ű2,GWBD-A2,1,12,1.2558624416001731,0.5,3 +,ʱ,,ʱ1,GWBD-A2,2,12,2.877701634836078,0.5,3 +,ʱ,,ʱ2,GWBD-A2,2,12,2.148476381702223,0.5,3 +,ʱ,,ʱ3,GW99A,1,4,2.8216375801071467,0.5,3 +,ʱ,,ʱ4,GW99A,1,9,3.0,0.5,3 +,ʱɫ,,ʱɫ1,GWBD-A3,2,12,1.2209565088767191,0.5,3 +,ʱɫ,,ʱɫ2,GWBD-A3,2,12,2.13505523373714,0.5,3 +ϱ,ʱ,,ʱ1,GW130,6,12,2.977100640862942,0.5,3 +ϱ,ʱ,,ʱ2,GW130,8,12,1.4108697454394656,0.5,3 +,ʱԭ,,ʱԭ1,GWBD-A3,4,12,1.085052120762868,0.5,3 +,ʱԭ,,ʱԭ2,GWBD-A3,4,12,1.4804124608992453,0.5,3 +ϲ,ʱ,,ʱ1,GW99A,2,12,0.8320501509514534,0.5,3 +ϲ,ʱ,,ʱ2,GW99A,2,12,1.4984461065581405,0.5,3 +ϱ,˫,,˫1,GW110.5,2,11,0.6454103640653669,0.5,3 +ϱ,˫,,˫2,GW110.5,2,11,0.6530684517974175,0.5,3 +ϱ,˫,,˫3,GW93,1,1,2.085029344273428,0.5,3 +ϱ,˫Ӫ,,˫Ӫ1,GWBD-A2,6,12,2.925213291566798,0.5,3 +ϱ,˫Ӫ,,˫Ӫ2,GWBD-A2,6,12,2.975043017037072,0.5,3 +ϱ,˫Ӫ,,˫Ӫ3,GWBD-A2,7,12,2.9397351966791594,0.5,3 +ϱ,˫Ӫ,,˫Ӫ4,GWBD-A2,7,12,2.616967714504933,0.5,3 +ϱ,˳,,˳1,GW99A,8,12,2.790744177175357,0.5,3 +ϱ,˳,,˳2,GW99A,8,12,0.6411549575195108,0.5,3 +ϱ,˳ɳ,,˳ɳ1,GW93,2,2,0.8340035880432236,0.5,3 +ϱ,˳ɳ,,˳ɳ2,GW93,5,5,1.1301574312833809,0.5,3 +ϱ,˳ɳ,,˳ɳ3,GW93,11,11,2.025384406078239,0.5,3 +ϱ,˳ɳ,,˳ɳ4,GW99,10,11,1.0429815540113208,0.5,3 +ϱ,˳ɳ,,˳ɳ5,GW99A,5,12,1.123628778712014,0.5,3 +ϱ,˳ɳ,,˳ɳ6,GW99A,5,12,0.979164298651741,0.5,3 +,вİ׳,,вİ׳1,GWBD-A3,8,8,1.763581199916676,0.5,3 +ϱ,вĸ,,вĸ1,GW110.5,1,11,2.959144662478874,0.5,3 +ϱ,вĸ,,вĸ2,GW110.5,1,11,2.92540109166913,0.5,3 +ϱ,вĸ,,вĸ3,GW110.5,1,11,1.1010708194893164,0.5,3 +ϱ,вĸ,,вĸ4,GW83.4,1,11,2.694547075119285,0.5,3 +ϱ,вĸ,,вĸ5,GW93,6,8,2.3745893939183,0.5,3 +ϱ,вĸ,,вĸ6,GW99A,1,7,1.8223796976955784,0.5,3 +ϱ,вĸ,,вĸ7,GW99A,1,7,2.468575784243612,0.5,3 +ϱ,вĸ,,вĸ8,SI90.2,1,12,0.7272902159645727,0.5,3 +ϱ,вĸ,,вĸ9,SI90.2,1,12,2.181467973934617,0.5,3 +ϱ,вĸ,,вĸ10,SI90.2,1,12,1.5244257562540415,0.5,3 +ϱ,вĸ,,вĸ11,SI90.2,1,12,1.4259873327753854,0.5,3 +ϱ,вĸ,,вĸ12,SI90.2,2,12,1.253548054276315,0.5,3 +ϱ,вĸ,,вĸ13,SI90.2,2,12,0.6644941460935946,0.5,3 +,вĹ,,вĹ1,GWBD-A3,7,11,2.3686587068604954,0.5,3 +,вĹ,,вĹ2,GWBD-A3,7,11,1.1015355190681149,0.5,3 +,вĺ,,вĺ1,GW99A,1,12,1.925686336774216,0.5,3 +,вĺ,,вĺ2,GW99A,1,12,1.166441845882635,0.5,3 +,вĺ,,вĺ3,GWBD-A2,2,12,2.1539372175131355,0.5,3 +,вĺ,,вĺ4,GWBD-A2,3,12,0.6366978576009893,0.5,3 +,вľȪ,,вľȪ1,GW99,1,12,1.1568681400683591,0.5,3 +,вľȪ,,вľȪ2,GW99,1,12,1.1064271136987875,0.5,3 +,вľȪ,,вľȪ3,GWBD-A2,1,12,0.9954163068389692,0.5,3 +,вľȪ,,вľȪ4,GWBD-A2,1,12,1.2193407437521984,0.5,3 +,вľȪ,,вľȪ5,GWBD-A2,2,12,0.959238907870878,0.5,3 +,вľȪ,,вľȪ6,GWBD-A2,3,12,1.218588133158403,0.5,3 +,вľȪ,,вľȪ7,SI90.2,1,1,1.2323208798346863,0.5,3 +,вľȪ,,вľȪ8,SI90.2,1,4,1.1083005779929354,0.5,3 +,вľȪ,,вľȪ9,SI90.2,3,4,1.0979168774354644,0.5,3 +,вľȪ,,вľȪ10,SI90.2,9,9,0.6134714785903335,0.5,3 +,вľȪ,,вľȪ11,SI90.2,4,12,0.9060377208252096,0.5,3 +,вľȪ,,вľȪ12,SI90.2,4,12,0.8588420476065148,0.5,3 +ϱ,вƸ,,вƸ1,GW93,1,4,0.6959224018718012,0.5,3 +ϱ,вƸ,,вƸ2,GW93,1,12,2.388840113565708,0.5,3 +ϱ,вƸ,,вƸ3,GW93,1,12,2.023924901276327,0.5,3 +ϲ,вƼ,,вƼ1,GW83.4,1,3,2.7904582895129884,0.5,3 +ϲ,вƼ,,вƼ2,GW93,1,10,1.6925578180285972,0.5,3 +ϲ,вƼ,,вƼ3,GW93,1,10,0.8982597799795793,0.5,3 +ϲ,вƼ,,вƼ4,GWBD-A3,1,12,1.3711168227538089,0.5,3 +ϲ,вƼ,,вƼ5,GWBD-A3,1,12,1.219691503974376,0.5,3 +,в,,в1,GW99,1,12,2.9557253831441828,0.5,3 +,в,,в2,GW99,1,12,2.462362813243516,0.5,3 +,в,,в3,GWBD-A2,1,12,0.5982166642652442,0.5,3 +,в,,в4,GWBD-A2,1,12,2.644080826558284,0.5,3 +,в,,в5,GWBD-A2,2,12,0.5145312169222124,0.5,3 +,в,,в6,SI90.2,1,1,0.9769481674143102,0.5,3 +,в,,в7,SI90.2,1,1,1.238170985113167,0.5,3 +,в˰,,в˰1,GW93,1,9,1.0683346058426026,0.5,3 +,в˰,,в˰2,GW93,1,9,0.942117080768859,0.5,3 +,в,,в1,GW130,1,12,1.633431132348781,0.5,3 +,в,,в2,GW130,11,12,1.081038974714554,0.5,3 +,в,,в3,SI122,1,12,2.975783026146584,0.5,3 +,в,,в4,SI122,1,12,1.6018405358345469,0.5,3 +,в,,в1,GWBD-A2,1,12,2.1810444878994515,0.5,3 +,в,,в2,GWBD-A2,1,12,1.613919507763036,0.5,3 +,в,,в3,GWBD-A3,7,9,0.5,0.5,3 +,вϪ,,вϪ1,GW93,3,3,1.890269922258364,0.5,3 +,ͨ,,ͨ1,GW99,11,11,0.5574628792523443,0.5,3 +,ͨ,,ͨ2,GW99A,1,10,2.1349045617509983,0.5,3 +,ͨ,,ͨ3,GWBD-A2,1,12,2.3607990485978285,0.5,3 +,ͨ,,ͨ4,GWBD-A2,1,12,0.5733216621135748,0.5,3 +,ͨ,,ͨ5,GWBD-A2,1,12,1.6300547095229956,0.5,3 +,ͨ,,ͨ1,GWBD-A2,1,12,2.9190166685770587,0.5,3 +,ͨ,,ͨ2,GWBD-A2,2,12,0.8731389130785212,0.5,3 +ϱ,ͨ綫,,ͨ綫1,GW110.5,6,10,1.6258348677031254,0.5,3 +ϱ,ͨ綫,,ͨ綫2,GW110.5,6,10,1.376510869994889,0.5,3 +ϱ,ͨ綫,,ͨ綫3,GW76,1,6,1.221180288569112,0.5,3 +ϱ,ͨ綫,,ͨ綫4,GW76,11,12,0.9304707434434764,0.5,3 +ϱ,ͨ綫,,ͨ綫5,GW76,11,12,1.0654063065239228,0.5,3 +ϱ,ͨ綫,,ͨ綫6,GW81,1,12,1.1315290493015806,0.5,3 +ϱ,ͨ綫,,ͨ綫7,GW81,1,12,1.1404930117692385,0.5,3 +ϱ,ͨ綫,,ͨ綫8,GW81,1,12,1.3014091708393016,0.5,3 +ϱ,ͨ綫,,ͨ綫9,GW86,10,12,0.5738779043252348,0.5,3 +ϱ,ͨ綫,,ͨ綫10,GWBD-D,4,10,0.9593048041664796,0.5,3 +ϱ,ͨ綫,,ͨ綫11,GW83.3,7,9,2.1135988495299616,0.5,3 +,ͨ,,ͨ1,GW83.4,1,1,1.0090685780114246,0.5,3 +,ͨ,,ͨ2,GW99A,1,1,0.9928464185174258,0.5,3 +,ͨ,,ͨ3,GWBD-A2,1,12,0.7701006517043388,0.5,3 +,ͨ,,ͨ4,GWBD-A2,1,12,2.1830368275462857,0.5,3 +,ͨ,,ͨ5,GWBD-A3,2,12,0.6427854133739357,0.5,3 +,ͨ,,ͨ6,GWBD-A3,2,12,0.7212222730295578,0.5,3 diff --git a/data/2025/ProductionLine未修改.csv b/data/2025/ProductionLine未修改.csv deleted file mode 100644 index e8071c1..0000000 --- a/data/2025/ProductionLine未修改.csv +++ /dev/null @@ -1,106 +0,0 @@ -区域名,工厂名,是否新工厂,产线ID,生产型号,开始月份,结束月份 -西北东部,艾郎玉门,否,艾郎玉门1,GWBD-A2,1,12 -西北东部,艾郎玉门,否,艾郎玉门2,GWBD-A2,1,12 -华北,艾郎张北,否,艾郎张北1,GWBD-A2,1,12 -华北,艾郎张北,否,艾郎张北2,GWBD-A2,1,12 -西北西部,时代巴里坤,是,时代巴里坤1,GWBD-A2,5,12 -西北西部,时代巴里坤,是,时代巴里坤2,GWBD-A2,5,12 -西北西部,时代巴里坤,是,时代巴里坤3,GW99A,3,9 -华南,时代百色,是,时代百色1,GWBD-A3,5,12 -华南,时代百色,是,时代百色2,GWBD-A3,5,12 -华东南北部,时代射阳,是,时代射阳1,GW130,9,12 -华东南北部,时代射阳,是,时代射阳2,GW130,9,12 -东北,时代松原,是,时代松原1,GWBD-A3,5,12 -东北,时代松原,是,时代松原2,GWBD-A3,5,12 -华东南南部,时代株洲,否,时代株洲1,GW99A,4,12 -华东南南部,时代株洲,否,时代株洲2,GW99A,4,12 -华东南北部,双瑞大丰,否,双瑞大丰1,GW110.5,2,11 -华东南北部,双瑞大丰,否,双瑞大丰2,GW110.5,2,11 -华东南北部,双瑞大丰,否,双瑞大丰3,GW93,1,1 -华东南北部,双瑞东营,是,双瑞东营1,GWBD-A2,9,12 -华东南北部,双瑞东营,是,双瑞东营2,GWBD-A2,9,12 -华东南北部,双瑞东营,是,双瑞东营3,GWBD-A2,9,12 -华东南北部,双瑞东营,是,双瑞东营4,GWBD-A2,9,12 -华东南北部,天顺濮阳,是,天顺濮阳1,GW99A,8,12 -华东南北部,天顺濮阳,是,天顺濮阳2,GW99A,8,12 -华东南北部,天顺沙洋,否,天顺沙洋1,GW93,2,11 -华东南北部,天顺沙洋,否,天顺沙洋2,GW99,10,11 -华东南北部,天顺沙洋,是,天顺沙洋3,GW99A,5,12 -华东南北部,天顺沙洋,是,天顺沙洋4,GW99A,5,12 -东北,中材白城,是,中材白城1,GWBD-A3,8,8 -华东南北部,中材阜宁,否,中材阜宁1,GW110.5,4,12 -华东南北部,中材阜宁,是,中材阜宁2,GW110.5,4,12 -华东南北部,中材阜宁,是,中材阜宁3,GW110.5,4,12 -华东南北部,中材阜宁,否,中材阜宁4,GW83.4,1,11 -华东南北部,中材阜宁,否,中材阜宁5,GW93,1,8 -华东南北部,中材阜宁,否,中材阜宁6,GW99A,1,7 -华东南北部,中材阜宁,否,中材阜宁7,GW99A,1,7 -华东南北部,中材阜宁,否,中材阜宁8,SI90.2,1,12 -华东南北部,中材阜宁,否,中材阜宁9,SI90.2,1,12 -华东南北部,中材阜宁,否,中材阜宁10,SI90.2,1,12 -华东南北部,中材阜宁,否,中材阜宁11,SI90.2,1,12 -华东南北部,中材阜宁,是,中材阜宁12,SI90.2,3,12 -华东南北部,中材阜宁,是,中材阜宁13,SI90.2,3,12 -西北西部,中材哈密,是,中材哈密1,GWBD-A3,7,12 -西北西部,中材哈密,是,中材哈密2,GWBD-A3,7,12 -华北,中材邯郸,否,中材邯郸1,GW99A,1,12 -华北,中材邯郸,否,中材邯郸2,GW99A,1,12 -华北,中材邯郸,是,中材邯郸3,GWBD-A2,4,12 -华北,中材邯郸,是,中材邯郸4,GWBD-A2,4,12 -西北东部,中材酒泉,否,中材酒泉1,GW99,1,12 -西北东部,中材酒泉,否,中材酒泉2,GW99,1,12 -西北东部,中材酒泉,否,中材酒泉3,GWBD-A2,1,12 -西北东部,中材酒泉,否,中材酒泉4,GWBD-A2,1,12 -西北东部,中材酒泉,否,中材酒泉5,GWBD-A2,1,12 -西北东部,中材酒泉,是,中材酒泉6,GWBD-A2,2,12 -西北东部,中材酒泉,否,中材酒泉7,SI90.2,1,12 -西北东部,中材酒泉,否,中材酒泉8,SI90.2,1,12 -西北东部,中材酒泉,否,中材酒泉9,SI90.2,4,12 -西北东部,中材酒泉,否,中材酒泉10,SI90.2,4,12 -华东南北部,中材连云港,否,中材连云港1,GW93,4,4 -华东南北部,中材连云港,否,中材连云港2,GW93,1,12 -华东南北部,中材连云港,否,中材连云港3,GW93,1,12 -华东南南部,中材萍乡,否,中材萍乡1,GW83.4,2,6 -华东南南部,中材萍乡,否,中材萍乡2,GW93,1,10 -华东南南部,中材萍乡,否,中材萍乡3,GW93,1,10 -华东南南部,中材萍乡,否,中材萍乡4,GWBD-A3,2,12 -华东南南部,中材萍乡,否,中材萍乡5,GWBD-A3,2,12 -东北,中材锡林,否,中材锡林1,GW99,1,12 -东北,中材锡林,否,中材锡林2,GW99,1,12 -东北,中材锡林,否,中材锡林3,GWBD-A2,1,12 -东北,中材锡林,否,中材锡林4,GWBD-A2,1,12 -东北,中材锡林,是,中材锡林5,GWBD-A2,4,12 -东北,中材锡林,否,中材锡林6,SI90.2,1,1 -东北,中材锡林,否,中材锡林7,SI90.2,1,1 -东北,中材兴安盟,否,中材兴安盟1,GW93,1,9 -东北,中材兴安盟,否,中材兴安盟2,GW93,1,9 -华南,中材阳江,是,中材阳江1,GW130,11,12 -华南,中材阳江,是,中材阳江2,GW130,11,12 -华南,中材阳江,否,中材阳江3,SI122,1,12 -华南,中材阳江,否,中材阳江4,SI122,1,12 -西北西部,中材伊吾,否,中材伊吾1,GWBD-A2,3,12 -西北西部,中材伊吾,否,中材伊吾2,GWBD-A2,3,12 -西北西部,中材伊吾,是,中材伊吾3,GWBD-A3,7,9 -华南,中材玉溪,否,中复玉溪1,GW93,3,3 -西北西部,重通昌吉,否,重通昌吉1,GW99,11,11 -西北西部,重通昌吉,否,重通昌吉2,GW99A,2,10 -西北西部,重通昌吉,否,重通昌吉3,GWBD-A2,1,12 -西北西部,重通昌吉,否,重通昌吉4,GWBD-A2,1,12 -西北西部,重通昌吉,是,重通昌吉5,GWBD-A2,11,12 -东北,重通大安,是,重通大安1,GWBD-A2,3,12 -东北,重通大安,是,重通大安2,GWBD-A2,4,12 -华东南北部,重通如东,是,重通如东1,GW110.5,6,10 -华东南北部,重通如东,是,重通如东2,GW110.5,6,10 -华东南北部,重通如东,否,重通如东3,GW76,1,12 -华东南北部,重通如东,否,重通如东4,GW76,1,12 -华东南北部,重通如东,否,重通如东5,GW81,1,12 -华东南北部,重通如东,否,重通如东6,GW81,1,12 -华东南北部,重通如东,否,重通如东7,GW86,3,12 -华东南北部,重通如东,是,重通如东8,GWBD-D,6,11 -华东南北部,重通如东,否,重通如东9,GW83.3,7,9 -西北东部,重通武威,否,重通武威1,GW83.4,1,1 -西北东部,重通武威,否,重通武威2,GW99A,1,1 -西北东部,重通武威,否,重通武威3,GWBD-A2,1,12 -西北东部,重通武威,否,重通武威4,GWBD-A2,1,12 -西北东部,重通武威,否,重通武威5,GWBD-A3,4,12 -西北东部,重通武威,否,重通武威6,GWBD-A3,4,12 diff --git a/data/2025/benchmark未修改.csv b/data/2025/benchmark未修改.csv deleted file mode 100644 index fd87df1..0000000 --- a/data/2025/benchmark未修改.csv +++ /dev/null @@ -1,28 +0,0 @@ -工厂,1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月,全年 -艾郎玉门,2,6,12,13,12,12,9,14,12,12,12,12,128 -艾郎张北,3,6,12,14,12,12,12,12,12,12,12,12,131 -时代巴里坤,0,0,7,2,2,9,6,16,14,13,12,12,93 -时代百色,0,0,0,0,5,9,12,8,15,13,12,12,86 -时代射阳,0,0,0,0,0,0,0,0,1,0,6,6,13 -时代松原,0,0,0,0,0,6,5,10,15,16,12,12,76 -时代株洲,0,0,0,7,9,14,14,14,14,14,14,14,114 -双瑞大丰,1,2,11,6,7,10,8,10,10,10,6,0,81 -双瑞东营,0,0,0,0,0,0,0,0,3,15,14,14,46 -天顺濮阳,2,0,0,0,0,0,0,12,11,9,10,11,55 -天顺沙洋,1,4,0,0,9,13,13,13,13,17,19,11,113 -中材白城,0,0,0,0,0,0,0,3,0,0,0,0,3 -中材阜宁,34,18,31,50,42,45,52,48,46,43,49,31,489 -中材哈密,0,0,0,0,0,0,8,9,9,6,7,0,39 -中材邯郸,12,6,3,17,18,27,18,21,23,19,20,14,198 -中材酒泉,11,20,52,56,43,45,50,53,59,52,52,40,533 -中材连云港,6,9,13,16,13,12,5,5,5,0,0,7,91 -中材萍乡,2,17,21,19,24,27,29,28,17,13,14,5,216 -中材锡林,28,6,17,25,36,32,27,36,27,30,32,21,317 -中材兴安盟,2,8,10,18,14,18,12,11,14,0,0,0,107 -中材阳江,4,1,5,5,5,5,5,5,5,5,7,7,59 -中材伊吾,0,0,10,14,5,4,8,16,16,8,15,10,106 -中材玉溪,0,0,3,0,0,0,0,0,0,0,0,0,3 -重通昌吉,13,9,19,21,22,23,23,28,27,18,23,19,245 -重通大安,0,0,2,9,6,12,11,10,11,11,13,12,97 -重通如东,24,7,24,19,23,26,27,24,30,33,34,35,306 -重通武威,22,7,14,27,31,29,29,28,30,28,29,29,303 diff --git a/data/2025/factory_data.csv b/data/2025/factory_data.csv new file mode 100644 index 0000000..bc25beb --- /dev/null +++ b/data/2025/factory_data.csv @@ -0,0 +1,28 @@ +,Ӣ,ƽĥϵ,С +,AilangYumen,1.1912381398858942,0.0383592943400758 +ű,AilangZhangbei,1.1282200278851473,0.0383834069943968 +ʱ,ShidaiBalikun,2.711953899161362,0.0372831200518159 +ʱɫ,ShidaiBaise,1.6780058713069297,0.0383834069943968 +ʱ,ShidaiSheyang,2.193985193151204,0.0381459900903132 +ʱԭ,ShidaiSongyuan,1.2827322908310568,0.0352284680862539 +ʱ,ShidaiZhuzhou,1.1652481287547969,0.0351743203712874 +˫,ShuangruiDafeng,1.1278360533787375,0.0345299766068848 +˫Ӫ,ShuangruiDongying,2.8642398049469904,0.0345680805544538 +˳,TianshunPuyang,1.715949567347434,0.0345299766068848 +˳ɳ,TianshunShayang,1.1892200094633198,0.0350377531255757 +вİ׳,ZhongcaiBaicheng,1.763581199916676,0.0352284680862539 +вĸ,ZhongcaiFuning,1.855609384916356,0.0344731713353556 +вĹ,ZhongcaiHami,1.7350971129643051,0.0344731713353556 +вĺ,ZhongcaiHandan,1.4706908144427435,0.03437964346405 +вľȪ,ZhongcaiJiuquan,1.0393974106393948,0.0335524068644557 +вƸ,ZhongcaiLianyungang,1.7028958055712788,0.034108309859383 +вƼ,ZhongcaiPingxiang,1.59441684284987,0.0334380950217488 +в,ZhongcaiXilin,1.6271480080944165,0.0313354095754113 +в˰,ZhongcaiXinganmeng,1.0052258433057308,0.0334092499773274 +в,ZhongcaiYangjiang,1.8230234172611164,0.0302713991840371 +в,ZhongcaiYiwu,1.4316546652208293,0.0332345469724357 +вϪ,ZhongcaiYuxi,1.890269922258364,0.0302713991840371 +ͨ,ChongtongChangji,1.4513085722475485,0.0303973755004896 +ͨ,ChongtongDaan,1.89607779082779,0.0301428254177608 +ͨ綫,ChongtongRudong,1.2217832605605747,0.0383834069943968 +ͨ,ChongtongWuwei,1.0531766936971614,0.028380612159397973 diff --git a/data/2025/factory_data_bak.csv b/data/2025/factory_data_bak.csv new file mode 100644 index 0000000..82621c5 --- /dev/null +++ b/data/2025/factory_data_bak.csv @@ -0,0 +1,28 @@ +工厂中文名,工厂英文名,工厂磨合系数,最小误差 +艾郎玉门,AilangYumen,1.120967033,0.0596663792039552 +艾郎张北,AilangZhangbei,1.08697518,0.0833197284386998 +时代巴里坤,ShidaiBalikun,1.0187437400861858,0.0842943873080955 +时代百色,ShidaiBaise,0.9004477644319165,0.0596663792039552 +时代射阳,ShidaiSheyang,0.8752331051886796,0.060527705646677 +时代松原,ShidaiSongyuan,0.8553030538583956,0.0833197284386998 +时代株洲,ShidaiZhuzhou,0.8,0.0464062054499543 +双瑞大丰,ShuangruiDafeng,0.8017981082680407,0.0464062054499543 +双瑞东营,ShuangruiDongying,1.4195340001397942,0.0454229212808294 +天顺濮阳,TianshunPuyang,1.11666927,0.0454229212808294 +天顺沙洋,TianshunShayang,1.038912690288306,0.0454229212808294 +中材白城,ZhongcaiBaicheng,1.6168654948182766,0.0454229212808294 +中材阜宁,ZhongcaiFuning,1.1496044423912932,0.0409542399205325 +中材哈密,ZhongcaiHami,1.5404408015710631,0.0406113043924118 +中材邯郸,ZhongcaiHandan,1.1753569549522398,0.0386749691227432 +中材酒泉,ZhongcaiJiuquan,0.979817498,10000.0 +中材连云港,ZhongcaiLianyungang,1.5319296291566933,0.039797303578411 +中材萍乡,ZhongcaiPingxiang,1.11513483685553,0.0406113043924118 +中材锡林,ZhongcaiXilin,1.029797177,0.0386749691227432 +中材兴安盟,ZhongcaiXinganmeng,0.9871605854669068,0.0379826880566304 +中材阳江,ZhongcaiYangjiang,1.2807141141229423,0.0198011028048272 +中材伊吾,ZhongcaiYiwu,1.241739515,0.0198011028048272 +中材玉溪,ZhongcaiYuxi,1.705602538488603,0.0198011028048272 +重通昌吉,ChongtongChangji,1.07645347634814,0.0373780017376583 +重通大安,ChongtongDaan,0.9935803675177745,0.0186556274325477 +重通如东,ChongtongRudong,0.9317437521783424,0.0149035093013446 +重通武威,ChongtongWuwei,0.8658294473858272,0.014659040410011074 diff --git a/data/2025/model_params.json b/data/2025/model_params.json index 6f5a806..60d122b 100644 --- a/data/2025/model_params.json +++ b/data/2025/model_params.json @@ -1,6 +1,6 @@ { - "holiday_days_1": 5, - "holiday_days_2": 12, + "holiday_days_1": 10, + "holiday_days_2": 6, "holiday_days_3": 2, "holiday_days_4": 2, "holiday_days_5": 2, @@ -11,37 +11,6 @@ "holiday_days_10": 7, "holiday_days_11": 2, "holiday_days_12": 2, - "month1": 235.2024236560018, - "month2": 80.13430957369465, - "month3": 40.81815120444523, - "month4": 35.573307322844045, - "factor_AilangYumen": 1.1209670325377996, - "factor_AilangZhangbei": 1.0869751796074336, - "factor_ShidaiBalikun": 1.064344356385733, - "factor_ShidaiBaise": 0.925644739081089, - "factor_ShidaiSheyang": 2.2354053357800834, - "factor_ShidaiSongyuan": 0.8850280359740502, - "factor_ShidaiZhuzhou": 0.8, - "factor_ShuangruiDafeng": 1.3485391587995974, - "factor_ShuangruiDongying": 1.4720254802974653, - "factor_TianshunPuyang": 1.1166692701637255, - "factor_TianshunShayang": 1.060367420503057, - "factor_ZhongcaiBaicheng": 1.8461095046937892, - "factor_ZhongcaiFuning": 1.3124938381864457, - "factor_ZhongcaiHami": 1.4843503931169255, - "factor_ZhongcaiHandan": 1.210604184057787, - "factor_ZhongcaiJiuquan": 0.9798174978126551, - "factor_ZhongcaiLianyungang": 1.6036280376162886, - "factor_ZhongcaiPingxiang": 1.132040522281658, - "factor_ZhongcaiXilin": 1.0297971770720127, - "factor_ZhongcaiXinganmeng": 0.9769716839603864, - "factor_ZhongcaiYangjiang": 2.386768108574698, - "factor_ZhongcaiYiwu": 1.2417395146436796, - "factor_ZhongcaiYuxi": 1.7093165019081604, - "factor_ChongtongChangji": 1.0895870479908367, - "factor_ChongtongDaan": 1.0187316107483142, - "factor_ChongtongRudong": 1.0270541553649646, - "factor_ChongtongWuwei": 0.8811374580269447, "product_set": [ "GWBD-A2", "GWBD-A3", diff --git a/ga_calibration_all_factories.py b/ga_calibration_all_factories.py new file mode 100644 index 0000000..5d91c00 --- /dev/null +++ b/ga_calibration_all_factories.py @@ -0,0 +1,75 @@ +import argparse +import json +import subprocess +import sys +from concurrent.futures import ProcessPoolExecutor, as_completed +from pathlib import Path + +import pandas as pd + + +def load_factory_codes(year: int) -> list[str]: + csv_path = Path("data") / str(year) / "factory_data.csv" + encodings = ("utf-8", "utf-8-sig", "gbk") + last_error = None + df = None + for enc in encodings: + try: + df = pd.read_csv(csv_path, encoding=enc) + break + except UnicodeDecodeError as exc: + last_error = exc + continue + if df is None: + raise last_error if last_error else FileNotFoundError(f"Missing {csv_path}") + col_map = {c.strip(): c for c in df.columns} + if "工厂英文名" not in col_map: + raise ValueError(f"{csv_path} 缺少字段: 工厂英文名") + return df[col_map["工厂英文名"]].astype(str).str.strip().tolist() + + +def run_single(factory_code: str, python_exe: str, script_path: Path) -> tuple[str, int, str]: + cmd = [python_exe, str(script_path), "--factory", factory_code] + result = subprocess.run(cmd, capture_output=True, text=True) + output = (result.stdout or "") + (result.stderr or "") + return factory_code, result.returncode, output + + +def main(): + parser = argparse.ArgumentParser(description="Parallel GA calibration for all factories.") + parser.add_argument("--max-workers", type=int, default=6, help="并行进程数上限(默认 6)") + args = parser.parse_args() + + year = json.load(open("year.json", "r", encoding="utf-8"))["year"] + codes = load_factory_codes(year) + script_path = Path("ga_calibration_by_one_factory.py") + if not script_path.exists(): + raise FileNotFoundError(f"找不到 {script_path}") + + python_exe = sys.executable + results = [] + with ProcessPoolExecutor(max_workers=args.max_workers) as executor: + future_map = { + executor.submit(run_single, code, python_exe, script_path): code for code in codes + } + for future in as_completed(future_map): + code = future_map[future] + try: + factory, rc, out = future.result() + except Exception as exc: + results.append((code, 1, f"Exception: {exc}")) + continue + results.append((factory, rc, out)) + status = "OK" if rc == 0 else f"FAIL({rc})" + print(f"[{status}] {factory}") + + print("\n详细输出:") + for factory, rc, out in results: + status = "OK" if rc == 0 else f"FAIL({rc})" + print(f"--- {factory} [{status}] ---") + print(out.strip()) + print("---------------") + + +if __name__ == "__main__": + main() diff --git a/ga_calibration_by_one_factory.py b/ga_calibration_by_one_factory.py new file mode 100644 index 0000000..ebff21d --- /dev/null +++ b/ga_calibration_by_one_factory.py @@ -0,0 +1,280 @@ +import argparse +import json +import random +from pathlib import Path +from typing import List, Tuple + +import pandas as pd +import time +import os + +from simulation_model import SimulationModel + +POP_SIZE = 20 +GENERATIONS = 50 +MUTATION_RATE = 0.2 +MUTATION_STD = 0.05 # for factors +LOCK_TIMEOUT = 120 # seconds +STAGNATION_WINDOW = 5 # generations without improvement before injecting LHS samples + + +def clip(val: float, bounds: Tuple[float, float]) -> float: + lo, hi = bounds + return max(lo, min(hi, val)) + + +def load_factory_row(csv_path: Path, factory_code: str) -> tuple[pd.Series, pd.DataFrame, dict]: + df, cols, _ = read_csv_with_encoding(csv_path, required={"工厂中文名", "工厂英文名", "工厂平均磨合系数", "最小误差"}) + row = df.loc[df[cols["工厂英文名"]].astype(str).str.strip() == factory_code] + if row.empty: + raise ValueError(f"在 {csv_path} 中找不到工厂英文名: {factory_code}") + return row.iloc[0], df, cols + + +def _csv_lock_path(csv_path: Path) -> Path: + return csv_path.with_suffix(csv_path.suffix + ".lock") + + +def _acquire_lock(lock_path: Path, timeout: float = LOCK_TIMEOUT, interval: float = 0.5): + start = time.time() + while True: + try: + fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_RDWR) + os.write(fd, str(os.getpid()).encode()) + return fd + except FileExistsError: + if time.time() - start > timeout: + raise TimeoutError(f"获取锁超时: {lock_path}") + time.sleep(interval) + + +def _release_lock(lock_path: Path, fd: int): + try: + os.close(fd) + finally: + if lock_path.exists(): + try: + lock_path.unlink() + except Exception: + pass + + +def update_factory_csv(csv_path: Path, factory_code: str, new_factor: float, new_error: float) -> None: + lock_path = _csv_lock_path(csv_path) + fd = _acquire_lock(lock_path) + try: + df, cols, enc = read_csv_with_encoding(csv_path, required={"工厂中文名", "工厂英文名", "工厂平均磨合系数", "最小误差"}) + mask = df[cols["工厂英文名"]].astype(str).str.strip() == factory_code + if not mask.any(): + raise ValueError(f"在 {csv_path} 中找不到工厂英文名: {factory_code}") + df.loc[mask, cols["工厂平均磨合系数"]] = float(new_factor) + df.loc[mask, cols["最小误差"]] = float(new_error) + df.to_csv(csv_path, index=False, encoding=enc) + finally: + _release_lock(lock_path, fd) + + +def update_production_line_csv(csv_path: Path, factory_name_cn: str, line_ids: List[str], best_genes: List[float]) -> None: + lock_path = _csv_lock_path(csv_path) + fd = _acquire_lock(lock_path) + try: + df, cols, enc = read_csv_with_encoding(csv_path, required={"工厂名", "产线ID", "磨合系数"}) + mask = df[cols["工厂名"]].astype(str).str.strip() == factory_name_cn + if not mask.any(): + raise ValueError(f"在 {csv_path} 中找不到工厂名: {factory_name_cn}") + line_to_factor = dict(zip(line_ids, best_genes)) + df.loc[mask, cols["产线ID"]] = df[cols["产线ID"]].astype(str) + for idx, row in df[mask].iterrows(): + lid = str(row[cols["产线ID"]]).strip() + if lid in line_to_factor: + df.at[idx, cols["磨合系数"]] = float(line_to_factor[lid]) + df.to_csv(csv_path, index=False, encoding=enc) + finally: + _release_lock(lock_path, fd) + + +def evaluate(factory_code: str, line_ids: List[str], genes: List[float]) -> float: + factory_factors = {} + model = SimulationModel( + factory_factors=factory_factors, + output_enabled=False, + is_calibration_mode=True, + ) + # Override per-line factors + for lid, val in zip(line_ids, genes): + model.line_factor[lid] = float(val) + while model.running: + model.step() + return model.mean_abs_error + + +def mutate(genes: List[float]) -> List[float]: + new = genes.copy() + for i in range(len(new)): + if random.random() < MUTATION_RATE: + jitter = random.gauss(0, MUTATION_STD) + new[i] = new[i] + jitter + return new + + +def crossover(p1: List[float], p2: List[float]) -> Tuple[List[float], List[float]]: + if len(p1) == 1: + return [p1[0]], [p2[0]] + point = random.randint(1, len(p1) - 1) + c1 = p1[:point] + p2[point:] + c2 = p2[:point] + p1[point:] + return c1, c2 + + +def init_population(seed_vals: List[float]) -> List[List[float]]: + pop = [] + for idx in range(POP_SIZE): + if idx == 0: + pop.append([float(v) for v in seed_vals]) + continue + indiv = [float(v) for v in seed_vals] + for j in range(len(indiv)): + jitter = random.uniform(-0.1, 0.1) + indiv[j] = indiv[j] + jitter + pop.append(indiv) + return pop + + +def read_csv_with_encoding(path: Path, required: set[str]): + encodings = ("utf-8", "utf-8-sig", "gbk") + last_error = None + df = None + for enc in encodings: + try: + df = pd.read_csv(path, encoding=enc) + break + except UnicodeDecodeError as exc: + last_error = exc + continue + if df is None: + raise last_error if last_error else FileNotFoundError(f"Missing {path}") + cols = {c.strip(): c for c in df.columns} + missing = required - set(cols) + if missing: + raise ValueError(f"{path} 缺少字段: {', '.join(sorted(missing))}") + return df, cols, enc + + +def load_factory_lines(year: int, factory_name_cn: str): + path = Path("data") / str(year) / "ProductionLine.csv" + df, cols, enc = read_csv_with_encoding(path, required={"工厂名", "产线ID", "磨合系数", "系数最小值", "系数最大值"}) + mask = df[cols["工厂名"]].astype(str).str.strip() == factory_name_cn + if not mask.any(): + raise ValueError(f"ProductionLine.csv 中未找到工厂 {factory_name_cn}") + lines = [] + for _, row in df[mask].iterrows(): + line_id = str(row[cols["产线ID"]]).strip() + seed = float(row[cols["磨合系数"]]) + min_b = float(row[cols["系数最小值"]]) + max_b = float(row[cols["系数最大值"]]) + lines.append((line_id, seed, min_b, max_b)) + return lines + + +def apply_bounds(genes: List[float], bounds: List[Tuple[float, float]]) -> List[float]: + return [clip(val, b) for val, b in zip(genes, bounds)] + + +def latin_hypercube_samples(n_samples: int, bounds: List[Tuple[float, float]]) -> List[List[float]]: + if n_samples <= 0: + return [] + dims = len(bounds) + samples = [] + # Latin hypercube with per-dimension random permutations and jitter inside each stratum + strata = [list(range(n_samples)) for _ in range(dims)] + for s in strata: + random.shuffle(s) + for i in range(n_samples): + point = [] + for d in range(dims): + lo, hi = bounds[d] + # random point inside the i-th stratum of dimension d + u = random.random() + stratum_idx = strata[d][i] + frac = (stratum_idx + u) / n_samples + val = lo + frac * (hi - lo) + point.append(val) + samples.append(point) + random.shuffle(samples) + return samples + + +def main(): + parser = argparse.ArgumentParser(description="GA calibration for a single factory factor.") + parser.add_argument( + "--factory", + required=True, + help="Factory English code (matches '工厂英文名' in factory_data.csv).", + ) + args = parser.parse_args() + + # set year + year = json.load(open("year.json", "r", encoding="utf-8"))["year"] + filename = f"{year}" + csv_path = Path("data") / filename / "factory_data.csv" + line_csv_path = Path("data") / filename / "ProductionLine.csv" + + factory_row, factory_df, factory_cols = load_factory_row(csv_path, args.factory) + factory_name_cn = str(factory_row[factory_cols["工厂中文名"]]).strip() + seed_lines = load_factory_lines(year, factory_name_cn) + line_ids = [lid for lid, _, _, _ in seed_lines] + seed_vals = [seed for _, seed, _, _ in seed_lines] + bounds = [(mn, mx) for _, _, mn, mx in seed_lines] + prev_best_error = float(factory_row["最小误差"]) if pd.notna(factory_row["最小误差"]) else float("inf") + + print(f"[START] 校准工厂 {args.factory} / {factory_name_cn} (产线数={len(line_ids)}, baseline_error={prev_best_error:.6f})") + + best_genes = None + best_score = float("inf") + last_improve_gen = -1 + population = init_population(seed_vals) + + for gen in range(GENERATIONS): + scored = [] + for indiv in population: + indiv = apply_bounds(indiv, bounds) + score = evaluate(args.factory, line_ids, indiv) + # print(f"[{args.factory}] Gen {gen+1} try factors={indiv} -> error={score:.6f}") + scored.append((score, indiv)) + if score < best_score: + best_score = score + best_genes = indiv + last_improve_gen = gen + scored.sort(key=lambda x: x[0]) + next_pop = [scored[0][1]] + while len(next_pop) < POP_SIZE: + parents = random.sample(scored[:max(3, len(scored))], 2) + c1, c2 = crossover(parents[0][1], parents[1][1]) + next_pop.append(apply_bounds(mutate(c1), bounds)) + if len(next_pop) < POP_SIZE: + next_pop.append(apply_bounds(mutate(c2), bounds)) + # Stagnation: inject Latin Hypercube samples to escape local optima + if last_improve_gen >= 0 and (gen - last_improve_gen) >= STAGNATION_WINDOW: + lhs_samples = latin_hypercube_samples(max(POP_SIZE // 2, 2), bounds) + lhs_samples = [apply_bounds(s, bounds) for s in lhs_samples] + next_pop = next_pop[: POP_SIZE // 4] + lhs_samples + next_pop = next_pop[:POP_SIZE] + last_improve_gen = gen + print(f"[{args.factory}] Stagnation detected ({STAGNATION_WINDOW} gens). Injected {len(lhs_samples)} LHS samples.") + population = next_pop + print(f"[{args.factory}] Generation {gen+1}/{GENERATIONS}: best_error={best_score:.6f}") + + best_genes = apply_bounds(best_genes, bounds) + best_avg_factor = sum(best_genes) / len(best_genes) + print(f"[DONE] {args.factory}: best_error={best_score:.6f} (prev best {prev_best_error:.6f})") + + if best_score < prev_best_error: + update_factory_csv(csv_path, args.factory, best_avg_factor, best_score) + update_production_line_csv(line_csv_path, factory_name_cn, line_ids, best_genes) + print(f"[UPDATE] {args.factory} / {factory_name_cn}: avg_factor={best_avg_factor:.6f}, error={best_score:.6f} 已写入 {csv_path} 与 ProductionLine.csv") + else: + print(f"[SKIP] {args.factory}: 未优于历史最小误差,CSV 未更新。") + + +if __name__ == "__main__": + main() diff --git a/ga_two_factor_calibration.py b/ga_two_factor_calibration.py deleted file mode 100644 index a7f7851..0000000 --- a/ga_two_factor_calibration.py +++ /dev/null @@ -1,121 +0,0 @@ -import json -import random -from pathlib import Path -from typing import List, Tuple - -from simulation_model import SimulationModel - -# Decision variables: two factory factors only -TARGET_FACTORIES = ["ZhongcaiBaicheng", "ZhongcaiHami"] - -# set year -year = json.load(open('year.json', 'r', encoding='utf-8'))['year'] -filename = f"{year}" - -# Bounds -FACTOR_BOUNDS = (0.8, 3.0) - -POP_SIZE = 20 -GENERATIONS = 50 -MUTATION_RATE = 0.2 -MUTATION_STD = 0.05 # for factors - - -def clip(val: float, bounds: Tuple[float, float]) -> float: - lo, hi = bounds - return max(lo, min(hi, val)) - - -def evaluate(genes: List[float]) -> float: - factory_factors = {fid: val for fid, val in zip(TARGET_FACTORIES, genes)} - model = SimulationModel(factory_factors=factory_factors, output_enabled=False) - while model.running: - model.step() - return model.mean_abs_error - - -def mutate(genes: List[float]) -> List[float]: - new = genes.copy() - for i in range(len(new)): - if random.random() < MUTATION_RATE: - jitter = random.gauss(0, MUTATION_STD) - new[i] = clip(new[i] + jitter, FACTOR_BOUNDS) - return new - - -def crossover(p1: List[float], p2: List[float]) -> Tuple[List[float], List[float]]: - point = random.randint(1, len(p1) - 1) - c1 = p1[:point] + p2[point:] - c2 = p2[:point] + p1[point:] - return c1, c2 - - -def init_population() -> List[List[float]]: - pop = [] - best_path = Path("data") / filename / "ga_two_best_params.json" - seed_indiv = None - if best_path.exists(): - try: - best = json.loads(best_path.read_text(encoding="utf-8")) - seed_indiv = [float(best.get(f"factor_{fid}", random.uniform(*FACTOR_BOUNDS))) for fid in TARGET_FACTORIES] - except Exception: - seed_indiv = None - - for idx in range(POP_SIZE): - if seed_indiv is not None and idx == 0: - pop.append(seed_indiv) - continue - indiv = [random.uniform(*FACTOR_BOUNDS) for _ in TARGET_FACTORIES] - pop.append(indiv) - return pop - - -def main(): - best_genes = None - best_score = float("inf") - population = init_population() - - for gen in range(GENERATIONS): - scored = [] - for indiv in population: - score = evaluate(indiv) - scored.append((score, indiv)) - if score < best_score: - best_score = score - best_genes = indiv - scored.sort(key=lambda x: x[0]) - next_pop = [scored[0][1], scored[1][1]] - while len(next_pop) < POP_SIZE: - parents = random.sample(scored[:max(3, len(scored))], 2) - c1, c2 = crossover(parents[0][1], parents[1][1]) - next_pop.append(mutate(c1)) - if len(next_pop) < POP_SIZE: - next_pop.append(mutate(c2)) - population = next_pop - print(f"Generation {gen+1}: best={best_score:.4f}") - - result = {} - for fid, val in zip(TARGET_FACTORIES, best_genes): - result[f"factor_{fid}"] = val - - data_path = Path("data") / filename / "ga_two_best_params.json" - prev_score = float("inf") - if data_path.exists(): - try: - prev = json.loads(data_path.read_text(encoding="utf-8")) - prev_score = float(prev.get("best_score", float("inf"))) - except Exception: - prev_score = float("inf") - - if best_score < prev_score: - data_path.write_text( - json.dumps({"best_score": best_score, **result}, ensure_ascii=False, indent=2), - encoding="utf-8", - ) - print(f"New best mean abs error: {best_score:.4f}, saved to {data_path}") - else: - print(f"Best mean abs error: {best_score:.4f} (not better than {prev_score:.4f}, not saved)") - - -if __name__ == "__main__": - main() diff --git a/production_line.py b/production_line.py index ac7aeec..85e7623 100644 --- a/production_line.py +++ b/production_line.py @@ -4,23 +4,30 @@ from mesa.agent import Agent class ProductionLineAgent(Agent): """A production line agent with basic production scheduling.""" - def __init__(self, model, line_id, region, factory, is_new_factory, schedule, ramp_ranges): + def __init__(self, model, line_id, region, factory, is_new_factory, schedule, ramp_ranges, line_factor: float): super().__init__(model) self.line_id = line_id self.region = region self.factory = factory self.is_new_factory = is_new_factory + self.line_factor = float(line_factor) # List of dicts: {"product": str, "start_month": int, "end_month": int} self.schedule = schedule - # ramp_ranges: {1: value, 2: value, 3: value, 4: value} + # ramp_ranges: {product: {1: value, 2: value, 3: value, 4: value}} self.ramp_ranges = ramp_ranges self.blade_stock = {} # 临时叶片库存 {product: blades not yet assembled} self.unit_stock = {} # 叶片机组库存 {product: assembled units} def _sample_cycle_hours(self, product, month_index): idx = min(max(month_index, 1), 4) - base = self.ramp_ranges[idx] - factor = self.model.get_factory_factor(self.factory) + prod_key = str(product).strip() + base_by_month = self.ramp_ranges.get(prod_key) + if base_by_month is None: + raise KeyError(f"未找到产品 {prod_key} 的生产效率。") + base = base_by_month.get(idx, base_by_month.get(4)) + if base is None: + raise KeyError(f"产品 {prod_key} 在 month{idx} 缺少生产效率。") + factor = self.model.get_factory_factor(self.factory, self.line_id, self.line_factor) return base * factor def step(self): @@ -45,6 +52,14 @@ class ProductionLineAgent(Agent): cycle_hours = self._sample_cycle_hours(product, month_index) produced_blades = available_hours / cycle_hours + self.model.record_blade_production( + line_id=self.line_id, + factory=self.factory, + region=self.region, + month=month, + product=product, + blades=produced_blades, + ) # Update blade and unit inventories current_blades = self.blade_stock.get(product, 0) + produced_blades @@ -53,6 +68,14 @@ class ProductionLineAgent(Agent): if units_to_add > 0: self.unit_stock[product] = self.unit_stock.get(product, 0) + units_to_add self.blade_stock[product] = remaining_blades + self.model.record_blade_stock( + line_id=self.line_id, + factory=self.factory, + region=self.region, + month=month, + product=product, + blades_stock=remaining_blades, + ) self.model.record_production( line_id=self.line_id, diff --git a/schedule_pulp.py b/schedule_pulp.py deleted file mode 100644 index d9f2b55..0000000 --- a/schedule_pulp.py +++ /dev/null @@ -1,173 +0,0 @@ -# -*- coding: utf-8 -*- -import pandas as pd -import pulp -from math import radians, sin, cos, sqrt, atan2 - -# 1. 读取数据 -inventory_df = pd.read_excel('可调拨库存表.xlsx') -factory_loc_df = pd.read_excel('工厂位置.xlsx') -demand_df = pd.read_excel('需求表.xlsx') # 型号、市、需求 -city_loc_df = pd.read_excel('地名经纬度.xlsx') # 市、经度、纬度 - -# 2. 数据合并 -inventory_df['可调拨库存'] = pd.to_numeric(inventory_df['可调拨库存'], errors='coerce').fillna(0) -demand_df['需求'] = pd.to_numeric(demand_df['需求'], errors='coerce').fillna(0) - -# 工厂库存合并 -factory_capacity = {} -for _, row in inventory_df.groupby(['工厂', '型号'])['可调拨库存'].sum().reset_index().iterrows(): - f, p, cap = row['工厂'], row['型号'], int(row['可调拨库存']) - if cap > 0: - factory_capacity.setdefault(f, {})[p] = cap - -# 城市需求合并 -city_demand = {} -for _, row in demand_df.iterrows(): - city, p, dem = row['市'], row['型号'], int(row['需求']) - if dem > 0: - city_demand.setdefault(city, {}) - city_demand[city][p] = city_demand[city].get(p, 0) + dem - -factory_lonlat = dict(zip(factory_loc_df['工厂'], zip(factory_loc_df['经度'], factory_loc_df['纬度']))) -city_lonlat = dict(zip(city_loc_df['市'], zip(city_loc_df['经度'], city_loc_df['纬度']))) - -# missing = set(city_demand) - set(city_lonlat) -# if missing: -# raise ValueError(f"缺少经纬度:{missing}") - -# 3. 距离 -dist = {} -for f in factory_capacity: - for c in city_demand: - lon1, lat1 = factory_lonlat[f] - lon2, lat2 = city_lonlat[c] - dlon = radians(lon2-lon1) - dlat = radians(lat2-lat1) - a = sin(dlat/2)**2 + cos(radians(lat1))*cos(radians(lat2))*sin(dlon/2)**2 - dist[(f,c)] = 6371 * 2 * atan2(sqrt(a), sqrt(1-a)) - -# 4. 单位运费函数 -def get_real_unit_cost(distance_km, product): - d = distance_km - p = str(product).strip() - if p in ['GWBD-A2', 'GWBD-A3', 'GWBD-B']: - if distance_km <= 100: - return 15802.31 - elif distance_km <= 200: - return 18445.12 - elif distance_km <= 300: - return 22391.35 - elif distance_km <= 600: - return 62.29 * d - elif distance_km <= 900: - return 44.75 * d - elif distance_km <= 1200: - return 38.23 * d - elif distance_km <= 1500: - return 36.08 * d - elif distance_km <= 2000: - return 33.44 * d - elif distance_km <= 2500: - return 32.76 * d - elif distance_km <= 3500: - return 26.68 * d - else: - return 24.27 - # 其他型号用第二套 - else: - if distance_km <= 100: - return 12859.46 - elif distance_km <= 200: - return 14946.73 - elif distance_km <= 300: - return 18662.46 - elif distance_km <= 600: - return 52.83 * d - elif distance_km <= 900: - return 38.36 * d - elif distance_km <= 1200: - return 32.89 * d - elif distance_km <= 1500: - return 30.69 * d - elif distance_km <= 2000: - return 28.49 * d - elif distance_km <= 2500: - return 27.76 * d - elif distance_km <= 3500: - return 22.87 * d - else: - return 21.02 * d - -# 5. 规划模型 -prob = pulp.LpProblem("最小化运费", pulp.LpMinimize) - -x = {} -for f in factory_capacity: - for p in factory_capacity[f]: - for city in city_demand: - if city_demand[city].get(p, 0) > 0: - x[(f,city,p)] = pulp.LpVariable(f"ship_{f}_{city}_{p}", lowBound=0, cat="Integer") - -# 缺货惩罚 -shortage = {(city,p): pulp.LpVariable(f"short_{city}_{p}", lowBound=0, cat="Continuous") - for city in city_demand for p in city_demand[city]} - -BIG_M = 1e10 -prob += pulp.lpSum( - x[k] * get_real_unit_cost(dist[(k[0],k[1])], k[2]) # 真实单位运费 - for k in x -) + pulp.lpSum(BIG_M * s for s in shortage.values()) - -# 库存硬约束 -for f in factory_capacity: - for p in factory_capacity[f]: - prob += pulp.lpSum(x.get((f,c,p), 0) for c in city_demand) <= factory_capacity[f][p] - -# 需求软约束 -for city in city_demand: - for p, dem in city_demand[city].items(): - prob += pulp.lpSum(x.get((f,city,p), 0) for f in factory_capacity) + shortage[(city,p)] >= dem - -# 6. 求解 -status = prob.solve(pulp.PULP_CBC_CMD(msg=True, timeLimit=600)) - -print(f"\n求解状态: {pulp.LpStatus[status]}") -if status != 1: - print("无解(请检查总库存)") -else: - print("有解") - # real_cost = pulp.value(prob.objective) - # print(f"实际运输总费用: {real_cost:,.2f} 元") - -# ====================== 7. 输出 ====================== -result = [] -for (f,city,p), var in x.items(): - q = int(pulp.value(var) or 0) - if q >= 1: - d = dist[(f,city)] - # unit = get_real_unit_cost(d, p) - result.append({ - "工厂": f, - "型号": p, - "需求方": city, - "调拨数量": q, - # "距离_km": round(d, 1), - # "真实单位运费": round(unit, 3), - # "总运费": round(q * unit, 2) - }) - -df = pd.DataFrame(result) -if not df.empty: - df = df.sort_values(["工厂", "型号"]) # 按单位运费排序 - df[["工厂","型号","需求方","调拨数量"]].to_excel("调拨方案.xlsx", index=False) - # print("\n调拨方案已保存(真实单位运费最优,优先满足单价更低的城市)") - # print(df[["工厂","型号","需求方","调拨数量"]].to_string(index=False)) -else: - print("\n无调拨记录") - -# 校验报告 -# print("\n各城市需求满足情况:") -# for city in city_demand: -# for p, dem in city_demand[city].items(): -# sup = sum(r["调拨数量"] for r in result if r["需求方"]==city and r["型号"]==p) -# print(f" {city} | {p}: 需求 {dem} → 调拨 {sup} → {'完美' if sup==dem else '异常'}") diff --git a/simulation_model.py b/simulation_model.py index 2be241e..9df9635 100644 --- a/simulation_model.py +++ b/simulation_model.py @@ -8,6 +8,9 @@ import matplotlib.pyplot as plt from mesa.datacollection import DataCollector from mesa.model import Model import pulp +import warnings +import sys +import traceback from production_line import ProductionLineAgent from demand_agent import DemandAgent @@ -19,6 +22,36 @@ matplotlib.rcParams["axes.unicode_minus"] = False # set year year = json.load(open('year.json', 'r', encoding='utf-8'))['year'] filename = f"{year}" +# Silence noisy deprecation warnings from dependencies; prefer explicit conversions. +warnings.filterwarnings("ignore", category=FutureWarning) + + +class _FilterMessageTracer: + """Silence and trace unexpected 'filter element' messages.""" + + def __init__(self, stream, log_path): + self._stream = stream + self._log_path = log_path + + def write(self, msg): + lower_msg = msg.lower() + if "filter element" in lower_msg: + # Record stack once per message and swallow it from console. + with open(self._log_path, "a", encoding="utf-8") as f: + f.write(msg) + traceback.print_stack(file=f) + return len(msg) + return self._stream.write(msg) + + def flush(self): + return self._stream.flush() + + +# Capture mysterious "filter element" prints while keeping stdout/stderr usable. +_filter_log = os.path.join("output", filename, "debug_filter.log") +os.makedirs(os.path.dirname(_filter_log), exist_ok=True) +sys.stdout = _FilterMessageTracer(sys.stdout, _filter_log) +sys.stderr = _FilterMessageTracer(sys.stderr, _filter_log) class SimulationModel(Model): """ @@ -28,35 +61,26 @@ class SimulationModel(Model): def __init__( self, - month1: float | None = None, - month2: float | None = None, - month3: float | None = None, - month4: float | None = None, factory_factors: dict | None = None, output_enabled: bool = False, is_within_region_allocation_only: bool | None = None, product_set: tuple | list | set | str | None = None, + is_calibration_mode: bool = False, **kwargs, ): super().__init__() cfg = self._load_model_params() # Apply overrides if provided self.month_holiday_days = self._load_month_holiday_days(cfg) - self.ramp_ranges = { - 1: float(month1 if month1 is not None else cfg["month1"]), - 2: float(month2 if month2 is not None else cfg["month2"]), - 3: float(month3 if month3 is not None else cfg["month3"]), - 4: float(month4 if month4 is not None else cfg["month4"]), - } + self.ramp_ranges = self._load_product_month_efficiency() self.factory_mapping = self._load_factory_mapping() - self.factory_factors = self._load_factory_factors(cfg) + self.factory_factors = {} merged_factors = {} merged_factors.update({k: v for k, v in kwargs.items() if k.startswith("factor_")}) if factory_factors: merged_factors.update(factory_factors) if merged_factors: self._merge_factory_factors(merged_factors) - self.default_new_factory_factor = cfg.get("factor_default", 1.3) self.within_region_only = self._to_bool( is_within_region_allocation_only if is_within_region_allocation_only is not None else cfg.get("is_within_region_allocation_only", False) ) @@ -91,6 +115,10 @@ class SimulationModel(Model): self.product_names = set() self.product_list = [] self.monthly_allocation_summary = [] + self.blade_production_log = [] + self.blade_stock_log = [] + self.calibration_mode = bool(is_calibration_mode) + self.line_factor = {} self._load_month_hours() self._load_agents_from_csv() @@ -194,6 +222,32 @@ class SimulationModel(Model): with open(f"data/{filename}/model_params.json", "r", encoding="utf-8") as f: return json.load(f) + def _load_product_month_efficiency(self) -> dict: + path = f"data/{filename}/month_efficiency.xlsx" + if not os.path.exists(path): + raise FileNotFoundError(f"Missing month efficiency file: {path}") + df = pd.read_excel(path) + df.columns = [str(c).strip().lower() for c in df.columns] + required = {"product_id", "month1", "month2", "month3", "month4"} + missing = required - set(df.columns) + if missing: + raise ValueError(f"month_efficiency.xlsx is missing columns: {', '.join(sorted(missing))}") + ramp = {} + for _, row in df.iterrows(): + product = str(row["product_id"]).strip() + if not product: + continue + values = {} + for idx in range(1, 5): + val = row[f"month{idx}"] + if pd.isna(val): + raise ValueError(f"产品 {product} 的 month{idx} 为空,请在 month_efficiency.xlsx 中补充。") + values[idx] = float(val) + ramp[product] = values + if not ramp: + raise ValueError("month_efficiency.xlsx contains no product rows.") + return ramp + def _load_month_holiday_days(self, cfg: dict) -> dict: # Default 2 days off per month unless overridden holidays = {m: 2 for m in range(1, 13)} @@ -211,7 +265,8 @@ class SimulationModel(Model): def _load_factory_mapping(self) -> dict: with open(f"data/{filename}/factory_mapping.json", "r", encoding="utf-8") as f: - return json.load(f) + mapping = json.load(f) + return mapping def _parse_product_set(self, val) -> tuple: if val is None: @@ -231,13 +286,9 @@ class SimulationModel(Model): return val.strip().lower() in {"true", "1", "yes", "y", "是", "对"} return bool(val) - def _load_factory_factors(self, cfg: dict) -> dict: - factors = {} - for key, val in cfg.items(): - if key.startswith("factor_"): - suffix = key[len("factor_") :] - factors[suffix] = float(val) - return factors + def _load_factory_factors(self) -> dict: + # Deprecated: factors now provided per production line; keep for compatibility with overrides. + return {} def _merge_factory_factors(self, overrides: dict): for key, val in overrides.items(): @@ -249,9 +300,20 @@ class SimulationModel(Model): def get_factory_code(self, factory_name: str) -> str: return self.factory_mapping.get(factory_name, self._sanitize_product(factory_name)) - def get_factory_factor(self, factory_name: str) -> float: + def get_factory_factor(self, factory_name: str, line_id: str | None = None, line_factor: float | None = None) -> float: code = self.get_factory_code(factory_name) - return self.factory_factors.get(code, self.default_new_factory_factor) + if line_id and line_id in self.line_factor: + base = self.line_factor.get(line_id) + else: + base = None + override = self.factory_factors.get(code) + if override is not None: + return float(override) + if base is not None: + return float(base) + if line_factor is not None: + return float(line_factor) + raise KeyError(f"未找到工厂 {factory_name} (line_id={line_id}) 的磨合系数。") def _load_agents_from_csv(self): encodings = ("utf-8", "utf-8-sig", "gbk") @@ -275,8 +337,11 @@ class SimulationModel(Model): for line_id, group in df.groupby("产线ID"): first = group.iloc[0] schedule = [] + line_factor_val = None for _, row in group.iterrows(): - product = row["生产型号"] + product = str(row["生产型号"]).strip() + if product not in self.ramp_ranges: + raise KeyError(f"缺少产品 {product} 的生产效率,请在 month_efficiency.xlsx 中补充。") schedule.append( { "product": product, @@ -284,10 +349,17 @@ class SimulationModel(Model): "end_month": int(row["结束月份"]), } ) - self.product_names.add(str(product).strip()) + self.product_names.add(product) is_new_factory = str(first["是否新工厂"]).strip() in {"是", "Yes", "True", "true", "1"} self.line_factory[line_id] = first["工厂名"] self.line_region[line_id] = first["区域名"] + if "磨合系数" not in first: + raise KeyError("ProductionLine.csv 缺少磨合系数字段。") + try: + line_factor_val = float(first.get("磨合系数")) + except Exception: + raise ValueError(f"无法解析产线 {line_id} 的磨合系数。") + self.line_factor[line_id] = line_factor_val ProductionLineAgent( model=self, line_id=line_id, @@ -296,6 +368,7 @@ class SimulationModel(Model): is_new_factory=is_new_factory, schedule=schedule, ramp_ranges=self.ramp_ranges, + line_factor=line_factor_val, ) def _load_demand_agents_from_csv(self): @@ -462,9 +535,43 @@ class SimulationModel(Model): self.cumulative_production += units self.region_totals[region] = self.region_totals.get(region, 0) + units + def record_blade_production(self, line_id, factory, region, month, product, blades): + self.blade_production_log.append( + { + "line_id": line_id, + "factory": factory, + "region": region, + "month": month, + "product": product, + "blades": float(blades), + } + ) + + def record_blade_stock(self, line_id, factory, region, month, product, blades_stock): + self.blade_stock_log.append( + { + "line_id": line_id, + "factory": factory, + "region": region, + "month": month, + "product": product, + "blades_stock": float(blades_stock), + } + ) + def step(self): for agent in list(self.agents): agent.step() + if self.calibration_mode: + if self.current_month >= 12: + self.running = False + self._finalize_factory_errors(write_files=self.output_enabled) + if self.output_enabled: + self._write_report() + self.datacollector.collect(self) + self.current_month += 1 + return + # Allocation after production for current month self._run_allocation() self._update_region_inventory() @@ -513,6 +620,40 @@ class SimulationModel(Model): os.makedirs(output_dir, exist_ok=True) out_path = os.path.join(output_dir, f"production_report_{timestamp}.csv") pivot.to_csv(out_path, index=False, encoding="utf-8-sig") + self._write_blade_reports(timestamp, output_dir) + + def _write_blade_reports(self, timestamp: str, output_dir: str): + def build_and_write(log, value_col, filename_prefix, value_name): + if not log: + return + df = pd.DataFrame(log) + pivot = ( + df.groupby(["product", "line_id", "factory", "month"])[value_col] + .sum() + .reset_index() + .pivot_table( + index=["product", "line_id", "factory"], + columns="month", + values=value_col, + fill_value=0.0, + ) + ) + for m in range(1, 13): + if m not in pivot.columns: + pivot[m] = 0.0 + pivot = pivot[sorted([c for c in pivot.columns if isinstance(c, int)])] + pivot["total"] = pivot.sum(axis=1) + pivot.reset_index(inplace=True) + pivot.columns = ( + ["生产型号", "产线名称", "工厂名"] + + [f"{m}月" for m in range(1, 13)] + + ["总计"] + ) + out_path = os.path.join(output_dir, f"{filename_prefix}_{timestamp}.csv") + pivot.to_csv(out_path, index=False, encoding="utf-8-sig") + + build_and_write(self.blade_production_log, "blades", "production_blade_current_report", "blades") + build_and_write(self.blade_stock_log, "blades_stock", "production_blade_stock_report", "blades_stock") def _write_allocation_workbook(self): if not self.monthly_allocation_summary: @@ -694,23 +835,33 @@ class SimulationModel(Model): except UnicodeDecodeError: benchmark = pd.read_csv(f"data/{filename}/benchmark.csv", encoding="gbk") benchmark_sorted = benchmark.sort_values(by=benchmark.columns[0]) + total_col = "总计" month_cols = [f"{m}月" for m in range(1, 13)] if total_col not in benchmark_sorted.columns: benchmark_sorted[total_col] = benchmark_sorted[month_cols].sum(axis=1) - bench_total = benchmark_sorted[total_col].astype(float).reset_index(drop=True) - prod_total = factory_pivot["总计"].astype(float).reset_index(drop=True) - min_len = min(len(bench_total), len(prod_total)) - if min_len == 0: + errors = [] + bench_name_col = benchmark_sorted.columns[0] + for _, prow in factory_pivot.iterrows(): + fname = prow["工厂名称"] + brow = benchmark_sorted[benchmark_sorted[bench_name_col] == fname] + if brow.empty: + continue + prod_months = pd.Series([float(prow[f"{m}月"]) for m in range(1, 13)]) + bench_months = pd.Series([float(brow.iloc[0][f"{m}月"]) for m in range(1, 13)]) + prod_cum = prod_months.cumsum() + bench_cum = bench_months.cumsum() + actual_total = float(brow.iloc[0][total_col]) if total_col in brow.columns else float(bench_cum.iloc[-1]) + if actual_total <= 0: + continue + diff = prod_cum - bench_cum + pct = diff.abs() / actual_total + err = pct.mean() + errors.append(float(err)) + if not errors: self.mean_abs_error = float("inf") return - bench_total = bench_total.iloc[:min_len] - prod_total = prod_total.iloc[:min_len] - bench_total_safe = bench_total.replace(0, pd.NA) - rel_errors = (prod_total - bench_total_safe) / bench_total_safe - rel_errors = rel_errors.fillna(0).astype(float) - abs_rel_errors = rel_errors.abs() - self.mean_abs_error = abs_rel_errors.mean() + self.mean_abs_error = sum(errors) / len(errors) if not write_files: return @@ -739,7 +890,7 @@ class SimulationModel(Model): bench_months = benchmark_sorted[[f"{m}月" for m in range(1, 13)]].iloc[:min_len].astype(float) bench_months_safe = bench_months.replace(0, pd.NA) month_pct_errors = (prod_months - bench_months_safe) / bench_months_safe * 100 - month_pct_errors = month_pct_errors.fillna(0).convert_dtypes().astype(float) + month_pct_errors = month_pct_errors.fillna(0).infer_objects(copy=False).astype(float) month_error_means = pd.Series(month_pct_errors.mean(axis=0), index=[f"{m}月" for m in range(1, 13)]) plt.figure(figsize=(10, 5)) @@ -753,7 +904,7 @@ class SimulationModel(Model): bench_totals_safe = bench_total.replace(0, pd.NA) factory_pct_errors = (prod_total - bench_totals_safe) / bench_totals_safe * 100 - factory_pct_errors = factory_pct_errors.fillna(0).convert_dtypes().astype(float) + factory_pct_errors = factory_pct_errors.fillna(0).infer_objects(copy=False).astype(float) factory_names = factory_pivot["工厂名称"].iloc[:min_len].reset_index(drop=True) factory_df = pd.DataFrame({"name": factory_names, "error_pct": factory_pct_errors}).sort_values( by="error_pct"