GA基本正常

This commit is contained in:
AgentLabCn
2025-12-01 17:51:51 +08:00
parent 52e2f4dcb1
commit b86290c331
14 changed files with 848 additions and 615 deletions

View File

@@ -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"