# 建模接口

各接口的 CPSAT、MATHOPT、OPTX 限制见[求解器支持矩阵](solver-support.md)；方法详情中的标签区分支持、条件支持和不支持。

`OptModel()` 拥有全部变量、表达式、约束与目标；不能跨模型组合表达式。

<a id="file-import"></a>

## 保存与读取模型文件

`model.save(path)` 将模型保存为 `.pb`，或按 `.pb.gz` 后缀进行 gzip 压缩。
`read_model(path)` 识别这两种格式，返回可继续编辑和求解的 `OptModel`。
PB 模型持有不可变内存快照，查询、直接求解和结果读取无需完整恢复 Python 建模图，首次修改才恢复。
加载完成后覆盖或删除原文件不影响模型；原生解析和编译仍会执行。

声明集合及嵌套值支持序列、映射和集合操作；修改应通过模型取得的对象进行。
插入的外部可变容器由模型复制持有，后续修改原容器不会改变模型。
这些集合不保证内置 `list`、`dict`、`set` 的具体类型；浅拷贝保留嵌套共享，
深拷贝产生独立普通容器并保留原模型表达式句柄。求解及交互会话期间，包含旧引用在内的模型写入会被拒绝。

```python
from optagent import OptModel, read_model, solve

model = OptModel()
x = model.int(0, 10, name="x")
model.minimize(x)
model.save("model.pb.gz")  # 也可用 model.pb 保存未压缩文件

loaded = read_model("model.pb.gz")
loaded_x = loaded.decisions[0]["expression"]
loaded.add_constraint(loaded_x >= 4)
solution = solve(loaded)
```

这两种格式保留模型标识、表达式图、变量域、声明初值、名称、约束和目标顺序，
包括非线性表达式、集合、序列、区间及已经确定类型的模型 Lambda。
文件不保存求解结果或 Python 代码；保存不会使当前解值失效，加载后的模型需重新求解才能读取 `.value`。
含外部 Python 回调的模型无法独立恢复，保存和加载均抛出 `ValueError`。

保存路径接受字符串或 `pathlib.Path`，后缀不区分大小写，父目录必须已存在。
已有文件只在写入成功后被替换。压缩使用 gzip 级别 6，无需额外安装压缩依赖，实际压缩比例取决于模型内容。
文件读取也兼容现有编码器生成的版本 1 模型字节及其 gzip 压缩形式。
损坏的压缩数据、无效引用和无法完整恢复的协议字段会被拒绝；具体算子的语义及后端兼容性仍由求解编译阶段检查。

<!-- public-api: optagent.OptModel.save -->

### 从 MPS / LP 文件导入

`read_model(path)` 返回普通的 `OptModel`，可继续添加约束或目标，然后调用现有的 `solve(model)`。

```python
from optagent import read_model, solve

model = read_model("problem.mps")  # 也支持 problem.lp
solution = solve(model, time_limit_s=10)

if solution.feasible:
    for decision in model.decisions:
        print(decision["name"], decision["expression"].value)
```

`path` 接受字符串或 `pathlib.Path`；相对路径以当前工作目录为基准，扩展名不区分大小写。
读取使用随包提供的原生读取器，无需另外安装 `highspy`。读取阶段不运行求解，也不设置用户初值。

支持未压缩的 `.mps` 和 CPLEX `.lp` 中的单目标线性规划、混合整数线性规划。
变量名、约束名、目标方向和目标常数项会保留；未命名声明可保留空名称，模型标识取文件名去掉扩展名。
通过 `model.decisions` 按导入顺序访问变量声明及表达式。名称不要求唯一，不应在名称重复时直接转成字典。
整数域 `[0, 1]` 转成布尔变量；固定为 0 或 1 的整数仍保留固定域。
整数上下界向内取整，有限整数边界的绝对值不得超过 `2^53`；无穷边界表示为无显式界。
双边约束拆成两条约束，两条沿用原约束名称。系数采用读取器的双精度表示。

MPS / LP 导入暂不支持压缩文件、二次项、SOS、指示约束、半连续／半整数变量和多目标扩展。
受当前读取器限制，LP 中的 `/* ... */` 块注释会被拒绝，请改用反斜杠开头的行注释。
MPS 读取器产生警告时拒绝导入，包括忽略输入数据或回退到另一种解析方式的警告。
应修正文件或重新导出为读取器接受的格式，再重试。

路径类型错误抛出 `TypeError`，文件访问错误抛出 `OSError` 的相应子类；
无效或不支持的模型抛出 `ValueError`，原生读取器不可用时抛出 `RuntimeError`。
导入成功只表示模型可以构造；各求解入口仍遵循[求解器支持矩阵](solver-support.md)中的限制。

<!-- public-api: optagent.read_model -->

## 变量工厂

| 方法 | 参数 | 语义 |
| --- | --- | --- |
| `bool` | `default` | 布尔决策 |
| `int` | `lb=None`、`ub=None`、`default` | 整数决策，边界包含端点 |
| `float` | `lb=None`、`ub=None`、`default` | 浮点决策，边界包含端点 |
| `set` | `universe_size`、`default` | `range(universe_size)` 中的不重复成员，无序 |
| `list` | `universe_size`、`default` | 同一整数宇宙中的不重复成员，有序 |
| `interval` | `min_start=0`、`max_end=0`、`default` | 必选区间，允许零长度 |
| `optional_interval` | `min_start=0`、`max_end=0`、`default` | 可缺席区间 |

布尔变量参与 `<`、`<=`、`>`、`>=` 时按 0/1 比较，支持两个布尔变量直接比较。
例如 `h >= y` 表示 y 为真时 h 必须为真；`==` 和 `!=` 仍保持布尔相等与不等语义。

`default` 省略时没有用户指定初值，内核负责初始构造。集合 / 序列初值为整数成员迭代器；区间初值使用含 `start`、`end`、`present` 的映射。初值必须符合变量域，不会自动成为约束。

## 约束与目标

| 方法 | 用法 |
| --- | --- |
| `constraint(expression)` | `add_constraint` 的别名，添加约束 |
| `minimize(expression)` | 追加最小化目标 |
| `maximize(expression)` | 追加最大化目标 |

多个目标按声明顺序进行字典序比较。模型变更使旧值失效。

## 表达式与算子

| 类别 | 主要接口 |
| --- | --- |
| 算术与聚合 | `sum`、`prod`、`min`、`max`、`sub`、`div`、`mod`、`abs`、`dist`、`pow` |
| 比较与逻辑 | `eq`、`neq`、`geq`、`leq`、`gt`、`lt`、`and_`、`or_`、`not_`、`xor`、`iif` |
| 数学函数 | `sqrt`、`sin`、`cos`、`round`、`ceil`、`floor`、`log`、`exp` |
| 集合关系 | `partition`、`disjoint`、`cover`、`union`、`intersection` |
| 容器与索引 | `array`、`range`、`at`、`count`、`contains`、`index`、`find`、`distinct`、`sort` |
| 区间 | `start`、`end`、`length`、`presence`、`hull` |
| 高阶表达式 | `lambda_function`、`call`、`piecewise`、`step_array` |

常用算术与比较也可直接使用 Python 运算符。聚合方法支持的方法重载与容器类型并非完全相同，详见下方方法说明。

## Expression 与 IntervalExpression

表达式提供 `model`、`type`、`operator`、`operands` 和 `value`。`.value` 读取最新求解结果；没有值或模型变更后读取会抛出 `RuntimeError`。

`expression[index]` 创建索引表达式。`expression.at(*indices, default=...)` 可声明越界时的默认值。区间表达式还提供 `.start`、`.end`、`.length` 属性与 `.presence()` 方法。

<!-- public-api: optagent.Expression -->
<!-- public-api: optagent.IntervalExpression -->

## 外部函数

通过 `create_int_external_function`、`create_double_external_function`、`create_int_array_external_function`、`create_double_array_external_function` 注册 Python 回调，得到 `ExternalFunction`。调用该对象构造表达式，回调在求解评价时接收一个参数视图。

```python
from optagent import OptModel

model = OptModel()
x = model.int(0, 10)


def evaluate_cost(args):
    return (args[0] - 3) ** 2


cost = model.create_int_external_function(evaluate_cost)
model.minimize(cost(x))
solution = model.solve(time_limit_s=1.0, deterministic=False, external_evaluation_limit=100)
```

回调可以包含普通 Python 分支、循环或外部请求。在一次求解中，相同带类型输入的结果必须稳定；求解器可能复用结果，也可能重复调用。不能依赖每次评价都执行回调。回调返回值须符合注册的标量 / 数组类型，外部函数使用本地搜索路径，默认串行执行。

`add_evaluation_point(arguments, value)` 注册已知评价点，命中时直接复用，不执行回调、不消耗调用额度。整数与浮点输入分别匹配，例如 `[3]` 与 `[3.0]` 不相同。相同带类型输入登记不同结果，会在求解启动时报错。已知点在当前求解中固定，求解期间修改登记列表不会改变当前结果。

已知点不再自动覆盖初始解。需要从已知方案开始时，显式设置决策变量的 `default`，例如 `model.int(0, 10, default=3)`。旧代码若依赖自动热启动，需要补上显式初值。

浮点函数的 `grad` 参数保留兼容，当前搜索算法不使用登记的梯度。

<!-- public-api: optagent.ExternalFunction -->

## 求解结果

`Solution` 包含 `feasible`、`objectives`、`status`、`solver_name`、`diagnostics`；见[结果字段说明](../guide/results.md)。

<!-- public-api: optagent.Solution -->

[查看全部 OptModel 方法详情 →](model-reference.md)

## 可选名称与结果读取

通常直接使用 `OptModel()`，变量、约束和目标也无需声明 `name`。保留 Python 变量引用，在求解成功后通过表达式的 `.value` 读值；`solution.objectives` 按目标声明顺序排列。

`model_id` 与 `name` 均为可选标识，默认空字符串，不是结果查询键。只有实际需要标识时才显式设置。完整参数仍列在[方法详情](model-reference.md)中。
