审查并精简 ChatGPT 的建库建议后落地的第一版骨架,只定接口不写实现。 - 建议审查:14 个包 → 7 个;约 250 个函数 → 约 40 个;5 个 Result 类 → 1 个 (docs/01-suggestion-review.md) - docs/:架构、路线图(含永久不做清单)、环境、Git 工作流、函数卡片模板 - src/mcm_toolkit/:core / data / stats / models / solve / decision / viz 共 23 个模块,均为签名 + docstring + NotImplementedError - tests/:9 个文件,其中 test_api_surface.py 已通过(20 passed) - examples/ problems/ notebooks/ figures/ tables/ 骨架 - pyproject.toml(核心依赖 4 个 + 6 组 extras)、environment.yml - scripts/:setup-venv.ps1(ASCII + LF)、setup-conda.ps1、doctor.ps1 - 环境:.venv(system-site-packages)与 conda 环境 mcm-toolkit 均已建好, 数值栈版本与 base 完全对齐(numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6,MKL 后端)
235 lines
6.8 KiB
Python
235 lines
6.8 KiB
Python
"""优化。
|
||
|
||
分层(对应原建议的 A/B/C 三级):
|
||
|
||
* **B 类(薄封装)**:`minimize` `linear_program` `integer_program` —— 底层 SciPy。
|
||
* **A 类(自己实现,S 档)**:`dynamic_programming` `knapsack_dp` `simulated_annealing`。
|
||
* **C 类(直接用库)**:NSGA-II 等,用 pymoo,本模块只提供 Pareto 前沿的**后处理**。
|
||
|
||
原建议里的 `maximize` 已删:它就是 `minimize` 取负,用 `sense="max"` 参数表达。
|
||
`value_iteration` / `policy_iteration` 也已删:与 `dynamic_programming` 同族,
|
||
第一版同时做三个属于过度设计。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Callable
|
||
from typing import Any
|
||
|
||
__all__ = [
|
||
"dynamic_programming",
|
||
"integer_program",
|
||
"knapsack_dp",
|
||
"linear_program",
|
||
"minimize",
|
||
"pareto_front",
|
||
"simulated_annealing",
|
||
]
|
||
|
||
|
||
def minimize(
|
||
objective: Callable[..., float],
|
||
x0: Any = None,
|
||
bounds: Any = None,
|
||
constraints: Any = None,
|
||
method: str = "auto",
|
||
sense: str = "min",
|
||
random_state: int | None = None,
|
||
**kwargs: Any,
|
||
) -> Any:
|
||
"""连续优化(底层 `scipy.optimize.minimize`)。
|
||
|
||
Parameters
|
||
----------
|
||
method:
|
||
`"auto"` 时按"有无约束 / 有无梯度"自动选:
|
||
无约束 → BFGS;有边界 → L-BFGS-B;有约束 → SLSQP。
|
||
sense:
|
||
`"min"` / `"max"`。取 max 时内部对目标函数取负并还原结果,
|
||
调用方不用自己写负号(负号写错是这类题最常见的手滑)。
|
||
|
||
Returns
|
||
-------
|
||
`Result`:`params={"x": 最优解}`,`metrics={"objective": 目标值}`。
|
||
|
||
Notes
|
||
-----
|
||
**结果必须在论文里报"是否收敛"和"初值敏感性"**:
|
||
用一个 `x0` 跑出来的局部最优,和换 10 个随机初值跑出来的全局最优,
|
||
结论可能完全不同。返回值里带上 `success` 与 `message` 就是为了这个。
|
||
|
||
Examples
|
||
--------
|
||
>>> f = lambda v: (v[0] - 3) ** 2 + (v[1] + 1) ** 2
|
||
>>> r = minimize(f, x0=[0, 0])
|
||
>>> round(r.metrics["objective"], 6)
|
||
0.0
|
||
"""
|
||
# TODO(L1)
|
||
raise NotImplementedError
|
||
|
||
|
||
def linear_program(
|
||
c: Any,
|
||
A_ub: Any = None,
|
||
b_ub: Any = None,
|
||
A_eq: Any = None,
|
||
b_eq: Any = None,
|
||
bounds: Any = None,
|
||
sense: str = "min",
|
||
) -> Any:
|
||
"""线性规划(底层 `scipy.optimize.linprog`)。
|
||
|
||
Parameters
|
||
----------
|
||
c:
|
||
目标函数系数。`sense="max"` 时内部取负。
|
||
bounds:
|
||
`None` 表示默认 `x >= 0`;要无下界显式写 `(None, None)`。
|
||
**这是最容易踩的坑**:忘记声明变量非负,解出来的方案可能物理上不可行。
|
||
|
||
Examples
|
||
--------
|
||
>>> r = linear_program(c=[1, 2], A_ub=[[1, 1]], b_ub=[4], bounds=[(0, None)] * 2)
|
||
>>> r.metrics["objective"]
|
||
4.0
|
||
"""
|
||
# TODO(L1)
|
||
raise NotImplementedError
|
||
|
||
|
||
def integer_program(
|
||
c: Any,
|
||
integrality: Any,
|
||
A_ub: Any = None,
|
||
b_ub: Any = None,
|
||
A_eq: Any = None,
|
||
b_eq: Any = None,
|
||
bounds: Any = None,
|
||
sense: str = "min",
|
||
) -> Any:
|
||
"""整数规划(底层 `scipy.optimize.milp`)。
|
||
|
||
`integrality`:0 = 连续,1 = 整数,2 = 0-1 变量。
|
||
|
||
Notes
|
||
-----
|
||
整数规划比 LP 慢得多。变量数到几百以上时先想想能不能用
|
||
线性松弛 + 取整 + 邻域搜索近似,别硬等求解器。
|
||
"""
|
||
# TODO(L1)
|
||
raise NotImplementedError
|
||
|
||
|
||
def dynamic_programming(
|
||
states: Any,
|
||
actions: Any,
|
||
transition: Callable[..., Any],
|
||
reward: Callable[..., float],
|
||
horizon: int,
|
||
discount: float = 1.0,
|
||
) -> Any:
|
||
"""通用有限期动态规划(A 类,自己实现)。
|
||
|
||
Parameters
|
||
----------
|
||
transition:
|
||
`f(state, action, rng) -> next_state`。
|
||
reward:
|
||
`f(state, action, next_state) -> float`。
|
||
horizon:
|
||
期数。`discount < 1` 时按 `γ^t` 折算。
|
||
|
||
Returns
|
||
-------
|
||
`Result`:`data` 是"最优值函数表 + 最优策略表",`extras["policy"]` 可直接查。
|
||
|
||
Notes
|
||
-----
|
||
复杂度 `O(horizon × |S| × |A|)`。**状态空间是唯一瓶颈**——
|
||
如果 `|S| × holiday` 超过内存,要么精简状态离散化粒度,要么改成近似 DP。
|
||
|
||
Examples
|
||
--------
|
||
>>> # 背包问题用 knapsack_dp 更直接;本函数用于"逐期决策"类题
|
||
>>> r = dynamic_programming(states, actions, trans, rew, horizon=10) # doctest: +SKIP
|
||
"""
|
||
# TODO(L1) ★ S 档:配完整函数卡片
|
||
raise NotImplementedError
|
||
|
||
|
||
def knapsack_dp(weights: Any, values: Any, capacity: float) -> Any:
|
||
"""0-1 背包(动态规划模板,A 类,自己实现)。
|
||
|
||
复杂度 `O(n × capacity)`。`capacity` 在题面里是小数时先做单位缩放取整,
|
||
否则 DP 表会大到装不下。
|
||
|
||
Returns
|
||
-------
|
||
`Result`:`params={"selected": 选中的下标}`,`metrics={"total_value", "total_weight"}`。
|
||
|
||
Examples
|
||
--------
|
||
>>> r = knapsack_dp(weights=[1, 3, 4, 5], values=[1, 4, 5, 7], capacity=7)
|
||
>>> r.metrics["total_value"]
|
||
9.0
|
||
"""
|
||
# TODO(L1) ★ S 档
|
||
raise NotImplementedError
|
||
|
||
|
||
def simulated_annealing(
|
||
objective: Callable[..., float],
|
||
x0: Any,
|
||
bounds: Any = None,
|
||
temp0: float = 1.0,
|
||
cooling: float = 0.95,
|
||
n_iter: int = 1000,
|
||
sense: str = "min",
|
||
random_state: int | None = None,
|
||
**kwargs: Any,
|
||
) -> Any:
|
||
"""模拟退火(A 类,自己实现)。
|
||
|
||
Parameters
|
||
----------
|
||
cooling:
|
||
降温系数,每步 `T *= cooling`。`0.95` 配 `n_iter=1000` 是保守默认值。
|
||
temp0:
|
||
初始温度。**经验做法:取"随机扰动的典型劣化幅度"同量级**,
|
||
太大等于随机游走,太小等于爬山。
|
||
|
||
Returns
|
||
-------
|
||
`Result`:`extras["history"]` 是每步的最优值轨迹,可直接喂给 `viz.plot_convergence`。
|
||
|
||
Notes
|
||
-----
|
||
* 必须记录收敛曲线并画出来——论文里"算法收敛性分析"那一节就靠它。
|
||
* 结果对随机种子敏感,**至少跑 10 个种子**,报最好值 + 分布,
|
||
只报一个种子的结果是不可信的。
|
||
|
||
Examples
|
||
--------
|
||
>>> r = simulated_annealing(lambda v: sum(x * x for x in v), x0=[3.0, 4.0], random_state=0) # doctest: +SKIP
|
||
"""
|
||
# TODO(L1) ★ S 档:配完整函数卡片
|
||
raise NotImplementedError
|
||
|
||
|
||
def pareto_front(points: Any, sense: str = "min") -> Any:
|
||
"""从解集里筛出 Pareto 非支配解。
|
||
|
||
Returns
|
||
-------
|
||
`Result`:`params={"mask": 布尔掩码, "rank": 每个解的支配层}`。
|
||
|
||
Notes
|
||
-----
|
||
多目标问题的**后处理**入口。真正的 NSGA-II 求解器用 pymoo
|
||
(`pip install "mcm-toolkit[moo]"`),本函数只负责"给定一组解,挑出前沿"。
|
||
它也是画 Pareto 前沿图的必要前置步骤。
|
||
"""
|
||
# TODO(L1)
|
||
raise NotImplementedError
|