Files
MCM-ICM_Toolkit/src/mcm_toolkit/solve/optimize.py
T

234 lines
6.8 KiB
Python
Raw Normal View History

"""优化。
分层(对应原建议的 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