ecode-cutting 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecode_cutting/USAGE.md +673 -0
- ecode_cutting/__init__.py +18 -0
- ecode_cutting/_common.py +158 -0
- ecode_cutting/_linear_fast.py +231 -0
- ecode_cutting/compat.py +659 -0
- ecode_cutting/engine.py +558 -0
- ecode_cutting/exporters.py +362 -0
- ecode_cutting/linear.py +532 -0
- ecode_cutting/models.py +230 -0
- ecode_cutting/portfolio.py +234 -0
- ecode_cutting/py.typed +0 -0
- ecode_cutting/rectangular.py +357 -0
- ecode_cutting/serialization.py +234 -0
- ecode_cutting/validation.py +234 -0
- ecode_cutting-0.2.3.dist-info/METADATA +190 -0
- ecode_cutting-0.2.3.dist-info/RECORD +18 -0
- ecode_cutting-0.2.3.dist-info/WHEEL +5 -0
- ecode_cutting-0.2.3.dist-info/top_level.txt +1 -0
ecode_cutting/models.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""公开数据模型。尺寸使用调用方统一的单位,内部计算按精度转换为整数。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ValidationError(ValueError):
|
|
10
|
+
"""输入或计算参数不合法。"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnsupportedFeatureError(NotImplementedError):
|
|
14
|
+
"""输入格式或平台专属接口尚不能在本实现中表达。"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class Material:
|
|
19
|
+
id: str = ""
|
|
20
|
+
description: str = ""
|
|
21
|
+
thickness: float = 0.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class Part:
|
|
26
|
+
width: float
|
|
27
|
+
height: float = 1.0
|
|
28
|
+
count: int = 1
|
|
29
|
+
rotatable: bool = True
|
|
30
|
+
id: str = ""
|
|
31
|
+
material: int = -1
|
|
32
|
+
angle_start: float = 0.0
|
|
33
|
+
angle_end: float = 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class Stock:
|
|
38
|
+
width: float
|
|
39
|
+
height: float = 1.0
|
|
40
|
+
count: int = 1
|
|
41
|
+
id: str = ""
|
|
42
|
+
waste: bool = False
|
|
43
|
+
material: int = -1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class Options:
|
|
48
|
+
saw_width: float = 0.0
|
|
49
|
+
trim_left: float = 0.0
|
|
50
|
+
trim_top: float = 0.0
|
|
51
|
+
trim_right: float = 0.0
|
|
52
|
+
trim_bottom: float = 0.0
|
|
53
|
+
complete_mode: bool = True
|
|
54
|
+
waste_size_min: float = 0.0
|
|
55
|
+
use_layout_minimization: bool = False
|
|
56
|
+
max_cut_level: int = 2_147_483_647
|
|
57
|
+
cross_section: float = 0.0
|
|
58
|
+
linear_allow_rotate: bool = False
|
|
59
|
+
linear_exact_angle: bool = True
|
|
60
|
+
linear_allow_flipping: bool = False
|
|
61
|
+
minimize_sheet_rotation: bool = False
|
|
62
|
+
max_layout_size: int = 0
|
|
63
|
+
linear_max_size_per_stock: int = 0
|
|
64
|
+
linear_max_size_per_stock_threshold: float = 0.5
|
|
65
|
+
max_decimal_point: int = 3
|
|
66
|
+
linear_sort_ascending: bool = False
|
|
67
|
+
max_size_per_stock: int = 0
|
|
68
|
+
allow_combine_stock_regular_and_waste: bool = False
|
|
69
|
+
use_large_stock_first: bool = False
|
|
70
|
+
cut_direction: str = "auto"
|
|
71
|
+
roll_mode: bool = False
|
|
72
|
+
# 新增的显式预算控制;None 表示不限制时间,取消回调仍有效。
|
|
73
|
+
time_limit: float | None = 10.0
|
|
74
|
+
max_attempts: int = 4
|
|
75
|
+
seed: int = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class Placement:
|
|
80
|
+
input_index: int
|
|
81
|
+
instance_index: int
|
|
82
|
+
width: float
|
|
83
|
+
height: float
|
|
84
|
+
x: float
|
|
85
|
+
y: float = 0.0
|
|
86
|
+
id: str = ""
|
|
87
|
+
material: int = -1
|
|
88
|
+
rotated: bool = False
|
|
89
|
+
flipped: bool = False
|
|
90
|
+
angle_start: float = 0.0
|
|
91
|
+
angle_end: float = 0.0
|
|
92
|
+
# 斜角零件使用真实轮廓;普通矩形留空并由宽高表示。
|
|
93
|
+
polygon: tuple[tuple[float, float], ...] = ()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def area(self) -> float:
|
|
97
|
+
"""真实轮廓面积;先平移原点,避免远离零点的坐标乘积互相消减。"""
|
|
98
|
+
if not self.polygon:
|
|
99
|
+
return self.width * self.height
|
|
100
|
+
x0, y0 = self.polygon[0]
|
|
101
|
+
points = tuple((x - x0, y - y0) for x, y in self.polygon)
|
|
102
|
+
return abs(sum(x * points[(i + 1) % len(points)][1]
|
|
103
|
+
- points[(i + 1) % len(points)][0] * y
|
|
104
|
+
for i, (x, y) in enumerate(points))) / 2
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class Remnant:
|
|
109
|
+
width: float
|
|
110
|
+
height: float
|
|
111
|
+
x: float
|
|
112
|
+
y: float = 0.0
|
|
113
|
+
material: int = -1
|
|
114
|
+
angle: float = 0.0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class Cut:
|
|
119
|
+
x1: float
|
|
120
|
+
y1: float
|
|
121
|
+
x2: float
|
|
122
|
+
y2: float
|
|
123
|
+
level: int = 0
|
|
124
|
+
angle: float = 90.0
|
|
125
|
+
part_indices: tuple[int, ...] = ()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True, slots=True)
|
|
129
|
+
class StockComponent:
|
|
130
|
+
input_index: int
|
|
131
|
+
instance_index: int
|
|
132
|
+
width: float
|
|
133
|
+
id: str = ""
|
|
134
|
+
waste: bool = False
|
|
135
|
+
material: int = -1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True, slots=True)
|
|
139
|
+
class Layout:
|
|
140
|
+
input_index: int
|
|
141
|
+
instance_index: int
|
|
142
|
+
width: float
|
|
143
|
+
height: float
|
|
144
|
+
id: str = ""
|
|
145
|
+
material: int = -1
|
|
146
|
+
waste: bool = False
|
|
147
|
+
parts: tuple[Placement, ...] = ()
|
|
148
|
+
remnants: tuple[Remnant, ...] = ()
|
|
149
|
+
cuts: tuple[Cut, ...] = ()
|
|
150
|
+
trim_cuts: tuple[Cut, ...] = ()
|
|
151
|
+
components: tuple[StockComponent, ...] = ()
|
|
152
|
+
# 净板以外的原卷尾段,坐标相对于输入原料;不计入本次已用净板面积。
|
|
153
|
+
roll_remnants: tuple[Remnant, ...] = ()
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def all_remnants(self) -> tuple[Remnant, ...]:
|
|
157
|
+
return self.remnants + self.roll_remnants
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def fill_ratio(self) -> float:
|
|
161
|
+
area = self.width * self.height
|
|
162
|
+
return sum(p.area for p in self.parts) / area if area else 0.0
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def nominal_fill_ratio(self) -> float:
|
|
166
|
+
area = self.width * self.height
|
|
167
|
+
return sum(p.width * p.height for p in self.parts) / area if area else 0.0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True, slots=True)
|
|
171
|
+
class Unplaced:
|
|
172
|
+
input_index: int
|
|
173
|
+
count: int
|
|
174
|
+
id: str = ""
|
|
175
|
+
reason: str = "没有可用且满足约束的原料"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True)
|
|
179
|
+
class LayoutGroup:
|
|
180
|
+
first_layout_index: int
|
|
181
|
+
layout_count: int
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class Result:
|
|
186
|
+
layouts: tuple[Layout, ...] = ()
|
|
187
|
+
unplaced: tuple[Unplaced, ...] = ()
|
|
188
|
+
elapsed: float = 0.0
|
|
189
|
+
linear: bool = False
|
|
190
|
+
cancelled: bool = False
|
|
191
|
+
timed_out: bool = False
|
|
192
|
+
attempts: int = 0
|
|
193
|
+
layout_groups: tuple[LayoutGroup, ...] = ()
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def complete(self) -> bool:
|
|
197
|
+
return not self.unplaced
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def placed_part_count(self) -> int:
|
|
201
|
+
return sum(len(layout.parts) for layout in self.layouts)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def used_stock_count(self) -> int:
|
|
205
|
+
return sum(len(layout.components) or 1 for layout in self.layouts)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def fill_ratio(self) -> float:
|
|
209
|
+
area = sum(s.width * s.height for s in self.layouts)
|
|
210
|
+
return sum(p.area for s in self.layouts for p in s.parts) / area if area else 0.0
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def nominal_fill_ratio(self) -> float:
|
|
214
|
+
area = sum(s.width * s.height for s in self.layouts)
|
|
215
|
+
return sum(p.width * p.height for s in self.layouts for p in s.parts) / area if area else 0.0
|
|
216
|
+
|
|
217
|
+
def to_dict(self) -> dict[str, Any]:
|
|
218
|
+
"""只输出 JSON 可序列化数据,方便 Web 接口直接返回。"""
|
|
219
|
+
return {**asdict(self), "complete": self.complete,
|
|
220
|
+
"placed_part_count": self.placed_part_count,
|
|
221
|
+
"used_stock_count": self.used_stock_count, "fill_ratio": self.fill_ratio,
|
|
222
|
+
"nominal_fill_ratio": self.nominal_fill_ratio}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True, slots=True)
|
|
226
|
+
class Progress:
|
|
227
|
+
elapsed: float
|
|
228
|
+
placed: int = 0
|
|
229
|
+
requested: int = 0
|
|
230
|
+
attempt: int = 0
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""在同一总预算内搜索并比较独立的可行候选。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, replace
|
|
6
|
+
from math import isfinite
|
|
7
|
+
from typing import Any, Callable, Sequence
|
|
8
|
+
|
|
9
|
+
from ._common import Runtime, integer, normalize_options, number, validate
|
|
10
|
+
from .models import Layout, Options, Part, Progress, Result, Stock, ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _json_value(value):
|
|
14
|
+
if isinstance(value, float) and not isfinite(value):
|
|
15
|
+
return None
|
|
16
|
+
if isinstance(value, dict):
|
|
17
|
+
return {key: _json_value(item) for key, item in value.items()}
|
|
18
|
+
if isinstance(value, (tuple, list)):
|
|
19
|
+
return [_json_value(item) for item in value]
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class PlanMetrics:
|
|
25
|
+
requested_parts: int
|
|
26
|
+
placed_parts: int
|
|
27
|
+
unplaced_parts: int
|
|
28
|
+
completion_ratio: float
|
|
29
|
+
used_stock_count: int
|
|
30
|
+
used_layout_count: int
|
|
31
|
+
total_stock_size: float
|
|
32
|
+
total_part_size: float
|
|
33
|
+
utilization: float
|
|
34
|
+
utilization_percent: float
|
|
35
|
+
cut_count: int
|
|
36
|
+
layout_count: int
|
|
37
|
+
unit: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class OptimizationPlan:
|
|
42
|
+
id: str
|
|
43
|
+
label: str
|
|
44
|
+
strategy: dict[str, Any]
|
|
45
|
+
result: Result
|
|
46
|
+
metrics: PlanMetrics
|
|
47
|
+
pareto_optimal: bool = False
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def complete(self) -> bool:
|
|
51
|
+
return self.result.complete
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def partial(self) -> bool:
|
|
55
|
+
return not self.complete
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
# 只遍历一次大布局,避免 asdict(self) 后又重复构造同一 Result。
|
|
59
|
+
return _json_value({"id": self.id, "label": self.label, "strategy": dict(self.strategy),
|
|
60
|
+
"result": self.result.to_dict(), "metrics": asdict(self.metrics),
|
|
61
|
+
"pareto_optimal": self.pareto_optimal,
|
|
62
|
+
"complete": self.complete, "partial": self.partial})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class OptimizationPortfolio:
|
|
67
|
+
plans: tuple[OptimizationPlan, ...]
|
|
68
|
+
elapsed: float
|
|
69
|
+
searches_started: int
|
|
70
|
+
searches_completed: int
|
|
71
|
+
cancelled: bool
|
|
72
|
+
timed_out: bool
|
|
73
|
+
diagnostics: tuple[str, ...] = ()
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def best(self) -> OptimizationPlan | None:
|
|
77
|
+
return self.plans[0] if self.plans else None
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict[str, Any]:
|
|
80
|
+
return {"plans": [plan.to_dict() for plan in self.plans], "elapsed": self.elapsed,
|
|
81
|
+
"searches_started": self.searches_started, "searches_completed": self.searches_completed,
|
|
82
|
+
"cancelled": self.cancelled, "timed_out": self.timed_out,
|
|
83
|
+
"diagnostics": list(self.diagnostics)}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _ring(points):
|
|
87
|
+
if not points:
|
|
88
|
+
return ()
|
|
89
|
+
points = tuple(points)
|
|
90
|
+
reverse = points[::-1]
|
|
91
|
+
return min(*(points[i:] + points[:i] for i in range(len(points))),
|
|
92
|
+
*(reverse[i:] + reverse[:i] for i in range(len(points))))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _layout_key(layout: Layout) -> tuple:
|
|
96
|
+
# 忽略实例编号和标签;切割顺序和真实轮廓仍是方案的一部分。
|
|
97
|
+
parts = tuple(sorted((p.width, p.height, p.x, p.y, p.material,
|
|
98
|
+
p.angle_start, p.angle_end, _ring(p.polygon))
|
|
99
|
+
for p in layout.parts))
|
|
100
|
+
def cuts(items):
|
|
101
|
+
return tuple((*min((c.x1, c.y1, c.x2, c.y2), (c.x2, c.y2, c.x1, c.y1)),
|
|
102
|
+
c.level, c.angle) for c in items)
|
|
103
|
+
components = tuple((c.width, c.material, c.waste) for c in layout.components)
|
|
104
|
+
roll_remnants = tuple((r.width, r.height, r.x, r.y, r.material) for r in layout.roll_remnants)
|
|
105
|
+
return (layout.width, layout.height, layout.material, layout.waste, components,
|
|
106
|
+
parts, cuts(layout.cuts), cuts(layout.trim_cuts), roll_remnants)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _result_key(result: Result) -> tuple:
|
|
110
|
+
return tuple(sorted(_layout_key(layout) for layout in result.layouts))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _metrics(result: Result, requested: int, linear: bool) -> PlanMetrics:
|
|
114
|
+
stock_size = sum(s.width if linear else s.width * s.height for s in result.layouts)
|
|
115
|
+
part_size = sum(p.area / s.height if linear else p.area
|
|
116
|
+
for s in result.layouts for p in s.parts)
|
|
117
|
+
utilization = part_size / stock_size if stock_size else 0.0
|
|
118
|
+
placed = result.placed_part_count
|
|
119
|
+
return PlanMetrics(requested, placed, sum(u.count for u in result.unplaced),
|
|
120
|
+
placed / requested if requested else 1.0,
|
|
121
|
+
result.used_stock_count, len(result.layouts), stock_size, part_size,
|
|
122
|
+
utilization, utilization * 100,
|
|
123
|
+
sum(len(s.cuts) + len(s.trim_cuts) for s in result.layouts),
|
|
124
|
+
len(set(_result_key(result))), "length" if linear else "area")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _candidates(options: Options, maximum: int):
|
|
128
|
+
yield options
|
|
129
|
+
protected = options.use_layout_minimization and options.max_layout_size > 0
|
|
130
|
+
for index in range(1, maximum):
|
|
131
|
+
# 仅修改搜索偏好,不能改变任何加工约束。
|
|
132
|
+
yield replace(options, seed=options.seed + index,
|
|
133
|
+
use_large_stock_first=(not options.use_large_stock_first
|
|
134
|
+
if index % 2 else options.use_large_stock_first),
|
|
135
|
+
use_layout_minimization=(True if protected else
|
|
136
|
+
(not options.use_layout_minimization
|
|
137
|
+
if index % 4 >= 2 else options.use_layout_minimization)),
|
|
138
|
+
minimize_sheet_rotation=(not options.minimize_sheet_rotation
|
|
139
|
+
if index % 8 >= 4 else options.minimize_sheet_rotation),
|
|
140
|
+
linear_sort_ascending=(options.linear_sort_ascending or index % 6 >= 4),
|
|
141
|
+
max_attempts=1 + (index // 2) % max(4, options.max_attempts))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _objective(plan: OptimizationPlan) -> tuple:
|
|
145
|
+
m = plan.metrics
|
|
146
|
+
return m.total_stock_size, m.used_stock_count, m.cut_count, m.layout_count
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def search_portfolio(stocks: Sequence[Stock], parts: Sequence[Part], options: Options, *,
|
|
150
|
+
linear: bool, max_plans: int = 3, time_limit: float = 5.0,
|
|
151
|
+
max_searches: int = 12,
|
|
152
|
+
callback: Callable[[Progress], bool] | None = None) -> OptimizationPortfolio:
|
|
153
|
+
"""返回最高完成件数的不同方案;预算为合作式检查,可能少量超时。
|
|
154
|
+
|
|
155
|
+
searches_completed 计入正常返回的求解调用,包括已返回部分解的中断调用。
|
|
156
|
+
pareto_optimal 相对于本次已搜索候选,以用料、原料数、切数及版式数评价。
|
|
157
|
+
"""
|
|
158
|
+
from .engine import group_layouts
|
|
159
|
+
from .linear import solve_linear
|
|
160
|
+
from .rectangular import solve_rectangular
|
|
161
|
+
|
|
162
|
+
integer(max_plans, "max_plans", 1)
|
|
163
|
+
integer(max_searches, "max_searches", 1)
|
|
164
|
+
if callback is not None and not callable(callback):
|
|
165
|
+
raise ValidationError("callback 必须可调用")
|
|
166
|
+
budget = number(time_limit, "time_limit")
|
|
167
|
+
options = normalize_options(options)
|
|
168
|
+
stocks, parts = tuple(stocks), tuple(parts)
|
|
169
|
+
validate(stocks, parts, options, linear=linear)
|
|
170
|
+
if options.time_limit is not None:
|
|
171
|
+
budget = min(budget, options.time_limit)
|
|
172
|
+
requested = sum(p.count for p in parts)
|
|
173
|
+
total = Runtime(replace(options, time_limit=budget), requested, callback)
|
|
174
|
+
candidates: list[OptimizationPlan] = []
|
|
175
|
+
seen = set()
|
|
176
|
+
started = completed = 0
|
|
177
|
+
highest = -1
|
|
178
|
+
diagnostics = []
|
|
179
|
+
solver = solve_linear if linear else solve_rectangular
|
|
180
|
+
solver_name = "linear" if linear else "rectangular"
|
|
181
|
+
if (linear and requested > 64 and not options.waste_size_min
|
|
182
|
+
and not options.linear_max_size_per_stock and not options.max_size_per_stock
|
|
183
|
+
and all(p.angle_start in (0, 90) and p.angle_end in (0, 90) for p in parts)):
|
|
184
|
+
from ._linear_fast import solve_linear_fast
|
|
185
|
+
solver, solver_name = solve_linear_fast, "linear_fast"
|
|
186
|
+
for candidate in _candidates(options, max_searches):
|
|
187
|
+
if total.stopped(max(highest, 0), started):
|
|
188
|
+
break
|
|
189
|
+
remaining = max(0.0, budget - total.elapsed)
|
|
190
|
+
# 给首解更大份额,仍为之后的独立候选保留预算。
|
|
191
|
+
share = remaining if max_searches == started + 1 else remaining / min(4, max_searches - started)
|
|
192
|
+
runtime = Runtime(replace(candidate, time_limit=share), requested,
|
|
193
|
+
lambda progress: total.stopped(progress.placed, started))
|
|
194
|
+
started += 1
|
|
195
|
+
result = group_layouts(solver(stocks, parts, candidate, runtime), candidate)
|
|
196
|
+
completed += 1
|
|
197
|
+
metrics = _metrics(result, requested, linear)
|
|
198
|
+
if metrics.placed_parts > highest:
|
|
199
|
+
highest = metrics.placed_parts
|
|
200
|
+
candidates.clear()
|
|
201
|
+
seen.clear()
|
|
202
|
+
key = _result_key(result)
|
|
203
|
+
if metrics.placed_parts == highest and key not in seen:
|
|
204
|
+
seen.add(key)
|
|
205
|
+
strategy = {name: getattr(candidate, name) for name in
|
|
206
|
+
("seed", "use_large_stock_first", "use_layout_minimization",
|
|
207
|
+
"minimize_sheet_rotation", "max_attempts", "linear_sort_ascending")}
|
|
208
|
+
strategy["solver"] = solver_name
|
|
209
|
+
candidates.append(OptimizationPlan("", "", strategy, result, metrics))
|
|
210
|
+
if runtime.timed_out:
|
|
211
|
+
diagnostics.append(f"候选 {started} 用尽分配预算,保留已返回结果")
|
|
212
|
+
if total.stopped(max(highest, 0), started):
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
candidates.sort(key=_objective)
|
|
216
|
+
frontier = [not any(all(a <= b for a, b in zip(_objective(other), _objective(plan)))
|
|
217
|
+
and _objective(other) != _objective(plan) for other in candidates)
|
|
218
|
+
for plan in candidates]
|
|
219
|
+
chosen = []
|
|
220
|
+
for index, plan in enumerate(candidates[:max_plans]):
|
|
221
|
+
m = plan.metrics
|
|
222
|
+
advantages = []
|
|
223
|
+
for field, label in (("total_stock_size", "用料较少"), ("cut_count", "切数较少"),
|
|
224
|
+
("layout_count", "版式较少")):
|
|
225
|
+
if getattr(m, field) == min(getattr(p.metrics, field) for p in candidates):
|
|
226
|
+
advantages.append(label)
|
|
227
|
+
label = "候选内" + "、".join(advantages) if advantages else "备选方案"
|
|
228
|
+
if plan.partial:
|
|
229
|
+
label += "(部分完成)"
|
|
230
|
+
chosen.append(replace(plan, id=f"plan-{index + 1}", label=label, pareto_optimal=frontier[index]))
|
|
231
|
+
if len(chosen) < max_plans:
|
|
232
|
+
diagnostics.append(f"本次搜索得到 {len(chosen)} 个不同的最高完成度方案")
|
|
233
|
+
return OptimizationPortfolio(tuple(chosen), total.elapsed, started, completed,
|
|
234
|
+
total.cancelled, total.timed_out, tuple(diagnostics))
|
ecode_cutting/py.typed
ADDED
|
File without changes
|