@jetta/blocks-allocator 0.2.1-1 → 0.2.1-2
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.
- package/package.json +1 -1
- package/src/Allocation_Optimization/item_consumption_heuristic/Item_Chooser.js +6 -138
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/axle_load_exceeds_limit.js +27 -16
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/candidate_item_axle_score.js +42 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/evaluate_layout_axle_load.js +28 -8
- package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/layout_fits_axle_load.js +3 -2
- package/src/Allocation_Optimization/item_consumption_heuristic/select_axle_balanced_slot_item.js +143 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/select_slot_item.js +149 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/slot_candidate_acceptance.js +110 -0
- package/src/Allocation_Optimization/item_consumption_heuristic/slot_candidate_order.js +49 -0
- package/src/models/transport/Container.js +11 -0
package/package.json
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
const No_Available_Items_Exception = require('./exceptions/No_Available_Items_Exception')
|
|
2
|
-
const No_Slots_For_Items_Exception = require('./exceptions/No_Slots_For_Items_Exception')
|
|
3
2
|
const Cargo_Profile = require('../../models/Cargo_Profile')
|
|
4
|
-
const
|
|
5
|
-
const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
|
|
6
|
-
const Allocated_Item = require('../../models/Allocated_Item')
|
|
7
|
-
const { distribute_items_by_weight_range } = require('./Weight_Slot_Distribution')
|
|
8
|
-
const {
|
|
9
|
-
resolve_choose_item_heuristic_context,
|
|
10
|
-
format_slot_label_for_exception
|
|
11
|
-
} = require('./Choose_Item_Heuristic_Context')
|
|
12
|
-
const {
|
|
13
|
-
calculate_total_stack_slots,
|
|
14
|
-
sort_stacking_slots_by_proximity
|
|
15
|
-
} = require('./Stack_Slot_Calculator')
|
|
16
|
-
const { order_items_by_capacity_bias } = require('./Capacity_Bias_Item_Order')
|
|
17
|
-
const candidate_item_fits_axle_load = require('./axle_load/candidate_item_fits_axle_load')
|
|
3
|
+
const { select_slot_item, select_preferred_or_heuristic } = require('./select_slot_item')
|
|
18
4
|
|
|
19
5
|
/**
|
|
20
6
|
* Chooses an item from available items by distributing them into stacking slots by weight
|
|
21
7
|
* and returning the item in the slot closest to the block slot position.
|
|
8
|
+
* When the axle gate is on, legal SKUs in that band are ranked by hottest-axle ratio.
|
|
22
9
|
* @param {object} data - Input bag.
|
|
23
10
|
* @param {object} [data.slot] - Slot with get_y() and get_height() (production).
|
|
24
11
|
* @param {object} [data.block_slot] - Legacy alias for slot (tests).
|
|
@@ -41,127 +28,8 @@ module.exports = function choose_item(data)
|
|
|
41
28
|
{
|
|
42
29
|
throw new No_Available_Items_Exception()
|
|
43
30
|
}
|
|
44
|
-
const is_to_prefer_same_id = data.block.get_slot_list().length > 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
function prefer_same_id(data)
|
|
49
|
-
{
|
|
50
|
-
const { container, item_list, previous_chosen_item_id, item_position } = data
|
|
51
|
-
const allocation_item = item_list.find(item => item.get_id() === previous_chosen_item_id)
|
|
52
|
-
if (allocation_item)
|
|
53
|
-
{
|
|
54
|
-
const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
|
|
55
|
-
if (accepted_item)
|
|
56
|
-
{
|
|
57
|
-
return accepted_item
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return choose_item_with_heuristic(data)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Heuristic path: weight bands, proximity-ordered bands, stacking validation per candidate.
|
|
66
|
-
* @param {object} data - Input bag (see choose_item).
|
|
67
|
-
* @returns {object} Chosen item.
|
|
68
|
-
*/
|
|
69
|
-
function choose_item_with_heuristic(data)
|
|
70
|
-
{
|
|
71
|
-
const { slot, container, item_position } = resolve_choose_item_heuristic_context(data)
|
|
72
|
-
const { item_list, qty_of_slots } = data
|
|
73
|
-
const total_slots = calculate_total_stack_slots(container.get_height(), slot)
|
|
74
|
-
const stacking_slots = distribute_items_by_weight_range(item_list, total_slots)
|
|
75
|
-
const ordered_bands = sort_stacking_slots_by_proximity(
|
|
76
|
-
stacking_slots,
|
|
77
|
-
slot.get_y(),
|
|
78
|
-
slot.get_height()
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
for (const band of ordered_bands)
|
|
82
|
-
{
|
|
83
|
-
const candidate_items = band.items
|
|
84
|
-
while (candidate_items.length > 0)
|
|
85
|
-
{
|
|
86
|
-
const allocation_item = get_item(candidate_items, qty_of_slots, container)
|
|
87
|
-
const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
|
|
88
|
-
if (accepted_item)
|
|
89
|
-
{
|
|
90
|
-
return accepted_item
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
remove_item_from_candidate_list(candidate_items, allocation_item)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const slot_label = format_slot_label_for_exception(slot)
|
|
98
|
-
throw new No_Suitable_Item_Exception(ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM, [slot_label])
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @param {object} data
|
|
103
|
-
* @param {object} container
|
|
104
|
-
* @param {object} allocation_item
|
|
105
|
-
* @param {object} item_position
|
|
106
|
-
* @returns {object|undefined}
|
|
107
|
-
*/
|
|
108
|
-
function try_accept_candidate(data, container, allocation_item, item_position)
|
|
109
|
-
{
|
|
110
|
-
const allocated_item = new Allocated_Item(
|
|
111
|
-
allocation_item,
|
|
112
|
-
item_position.x,
|
|
113
|
-
item_position.y,
|
|
114
|
-
item_position.z
|
|
115
|
-
)
|
|
116
|
-
if (!candidate_item_fits_axle_load(data, allocated_item))
|
|
117
|
-
{
|
|
118
|
-
return undefined
|
|
119
|
-
}
|
|
120
|
-
if (should_defer_loose_on_block_stacking_validation(data.block))
|
|
121
|
-
{
|
|
122
|
-
return allocated_item
|
|
123
|
-
}
|
|
124
|
-
if (!container.is_a_valid_stacking(allocated_item))
|
|
125
|
-
{
|
|
126
|
-
return undefined
|
|
127
|
-
}
|
|
128
|
-
return allocated_item
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function should_defer_loose_on_block_stacking_validation(block)
|
|
132
|
-
{
|
|
133
|
-
return Cargo_Profile.stack_loose_on_tie_ratio_blocks()
|
|
134
|
-
&& block?.get_slot_list?.().length === 1
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function remove_item_from_candidate_list(candidate_item_list, allocation_item)
|
|
138
|
-
{
|
|
139
|
-
const index = candidate_item_list.indexOf(allocation_item)
|
|
140
|
-
if (index > -1)
|
|
141
|
-
{
|
|
142
|
-
candidate_item_list.splice(index, 1)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function get_item(item_list, qty_of_slots, container)
|
|
147
|
-
{
|
|
148
|
-
if (qty_of_slots === undefined)
|
|
149
|
-
{
|
|
150
|
-
throw new No_Slots_For_Items_Exception()
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (item_list.length === 0)
|
|
154
|
-
{
|
|
155
|
-
throw new No_Available_Items_Exception()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const ordered_item_list = order_items_by_capacity_bias(item_list, container)
|
|
159
|
-
const preferable_item_list = ordered_item_list.filter(item => item.get_amount() >= qty_of_slots)
|
|
160
|
-
|
|
161
|
-
if (preferable_item_list.length > 0)
|
|
162
|
-
{
|
|
163
|
-
return preferable_item_list[0]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return ordered_item_list[0]
|
|
31
|
+
const is_to_prefer_same_id = data.block.get_slot_list().length > 0
|
|
32
|
+
&& previous_chosen_item_id !== undefined
|
|
33
|
+
&& Cargo_Profile.prefer_block_with_same_item()
|
|
34
|
+
return is_to_prefer_same_id ? select_preferred_or_heuristic(data) : select_slot_item(data)
|
|
167
35
|
}
|
package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/axle_load_exceeds_limit.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
const { gt } = require('@jetta/number-compare')
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Hottest axle load as a fraction of the folga-adjusted cap. Above 1 is over limit.
|
|
5
|
+
* @param {object} env - Calculator output env with containers[].pesoEixos.
|
|
6
|
+
* @param {number} folga_percent - Slack percent (10 means 110% of pesoMaxFinal).
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
function max_axle_load_ratio(env, folga_percent)
|
|
10
|
+
{
|
|
11
|
+
let hottest_ratio = 0
|
|
12
|
+
for (const axle of collect_peso_eixos_rows(env))
|
|
13
|
+
{
|
|
14
|
+
hottest_ratio = Math.max(hottest_ratio, axle_row_ratio(axle, folga_percent))
|
|
15
|
+
}
|
|
16
|
+
return hottest_ratio
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* Whether any container axle row exceeds the folga-adjusted final max.
|
|
5
21
|
* @param {object} env - Calculator output env with containers[].pesoEixos.
|
|
@@ -8,15 +24,7 @@ const { gt } = require('@jetta/number-compare')
|
|
|
8
24
|
*/
|
|
9
25
|
function axle_load_exceeds_limit(env, folga_percent)
|
|
10
26
|
{
|
|
11
|
-
|
|
12
|
-
for (const axle of axle_rows)
|
|
13
|
-
{
|
|
14
|
-
if (axle_row_exceeds_limit(axle, folga_percent))
|
|
15
|
-
{
|
|
16
|
-
return true
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return false
|
|
27
|
+
return gt(max_axle_load_ratio(env, folga_percent), 1)
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -25,12 +33,10 @@ function axle_load_exceeds_limit(env, folga_percent)
|
|
|
25
33
|
*/
|
|
26
34
|
function collect_peso_eixos_rows(env)
|
|
27
35
|
{
|
|
28
|
-
const containers = env.containers ?? []
|
|
29
36
|
const rows = []
|
|
30
|
-
for (const container of containers)
|
|
37
|
+
for (const container of env.containers ?? [])
|
|
31
38
|
{
|
|
32
|
-
|
|
33
|
-
rows.push(...axle_list)
|
|
39
|
+
rows.push(...(container.pesoEixos ?? []))
|
|
34
40
|
}
|
|
35
41
|
return rows
|
|
36
42
|
}
|
|
@@ -38,14 +44,19 @@ function collect_peso_eixos_rows(env)
|
|
|
38
44
|
/**
|
|
39
45
|
* @param {object} axle
|
|
40
46
|
* @param {number} folga_percent
|
|
41
|
-
* @returns {
|
|
47
|
+
* @returns {number}
|
|
42
48
|
*/
|
|
43
|
-
function
|
|
49
|
+
function axle_row_ratio(axle, folga_percent)
|
|
44
50
|
{
|
|
45
51
|
const loaded_kilograms = Number(axle.pesoCarregadoFinal ?? 0)
|
|
46
52
|
const max_kilograms = Number(axle.pesoMaxFinal ?? 0)
|
|
47
53
|
const limit_kilograms = max_kilograms * (1 + folga_percent / 100)
|
|
48
|
-
|
|
54
|
+
if (limit_kilograms === 0)
|
|
55
|
+
{
|
|
56
|
+
return loaded_kilograms > 0 ? Number.POSITIVE_INFINITY : 0
|
|
57
|
+
}
|
|
58
|
+
return loaded_kilograms / limit_kilograms
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
module.exports = axle_load_exceeds_limit
|
|
62
|
+
module.exports.max_axle_load_ratio = max_axle_load_ratio
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { gt } = require('@jetta/number-compare')
|
|
2
|
+
const Cargo_Profile = require('../../../models/Cargo_Profile')
|
|
3
|
+
const { score_layout_axle_load } = require('./evaluate_layout_axle_load')
|
|
4
|
+
const layout_fits_axle_load = require('./layout_fits_axle_load')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Axle score for one candidate. fits is false when the cap is exceeded or physics fails.
|
|
8
|
+
* When the gate is off, ratio is 0 and fits is true.
|
|
9
|
+
* @param {object} data
|
|
10
|
+
* @param {object} allocated_item
|
|
11
|
+
* @returns {{ fits: boolean, ratio: number|undefined }}
|
|
12
|
+
*/
|
|
13
|
+
function candidate_item_axle_score(data, allocated_item)
|
|
14
|
+
{
|
|
15
|
+
if (!Cargo_Profile.enable_axle_load_slot_gate() || !layout_fits_axle_load.transport_has_axles(data.transport))
|
|
16
|
+
{
|
|
17
|
+
return { fits: true, ratio: 0 }
|
|
18
|
+
}
|
|
19
|
+
const ratio = score_layout_axle_load(layout_probe(data, allocated_item))
|
|
20
|
+
if (ratio === undefined)
|
|
21
|
+
{
|
|
22
|
+
return { fits: false, ratio: undefined }
|
|
23
|
+
}
|
|
24
|
+
return { fits: gt(ratio, 1) === false, ratio }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} data
|
|
29
|
+
* @param {object} allocated_item
|
|
30
|
+
* @returns {object}
|
|
31
|
+
*/
|
|
32
|
+
function layout_probe(data, allocated_item)
|
|
33
|
+
{
|
|
34
|
+
const pending_axle_items = data.pending_axle_items ?? []
|
|
35
|
+
return {
|
|
36
|
+
transport: data.transport,
|
|
37
|
+
container: data.container,
|
|
38
|
+
extra_items: pending_axle_items.concat(allocated_item)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = candidate_item_axle_score
|
|
@@ -1,43 +1,63 @@
|
|
|
1
1
|
const { calculate_weight_on_axles_physics } = require('@jetta/axle-load-calculator')
|
|
2
|
+
const { gt } = require('@jetta/number-compare')
|
|
2
3
|
const { Logger } = require('@jetta/logger')
|
|
3
4
|
const Cargo_Profile = require('../../../models/Cargo_Profile')
|
|
4
5
|
const clone_env_with_layout_items = require('./clone_env_with_layout_items')
|
|
5
6
|
const axle_load_exceeds_limit = require('./axle_load_exceeds_limit')
|
|
7
|
+
const { max_axle_load_ratio } = axle_load_exceeds_limit
|
|
6
8
|
|
|
7
9
|
const logger = new Logger('Axle Load Slot Gate')
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
12
|
+
* Hottest axle ratio for the layout, or undefined when physics cannot run.
|
|
11
13
|
* @param {object} data
|
|
12
14
|
* @param {object} data.transport
|
|
13
15
|
* @param {object} data.container
|
|
14
16
|
* @param {object[]} data.extra_items
|
|
15
|
-
* @returns {
|
|
17
|
+
* @returns {number|undefined}
|
|
16
18
|
*/
|
|
17
|
-
function
|
|
19
|
+
function score_layout_axle_load(data)
|
|
18
20
|
{
|
|
19
21
|
const env = clone_env_with_layout_items(data.transport, data.container, data.extra_items)
|
|
20
22
|
try
|
|
21
23
|
{
|
|
22
|
-
return
|
|
24
|
+
return ratio_from_physics(env)
|
|
23
25
|
}
|
|
24
26
|
catch (error)
|
|
25
27
|
{
|
|
26
28
|
logger.warn(`axle calculator rejected layout: ${error.message}`)
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Runs axle physics for the current layout plus extra items on the active container.
|
|
35
|
+
* @param {object} data
|
|
36
|
+
* @param {object} data.transport
|
|
37
|
+
* @param {object} data.container
|
|
38
|
+
* @param {object[]} data.extra_items
|
|
39
|
+
* @returns {boolean} True when no axle exceeds the folga-adjusted cap.
|
|
40
|
+
*/
|
|
41
|
+
function evaluate_layout_axle_load(data)
|
|
42
|
+
{
|
|
43
|
+
const ratio = score_layout_axle_load(data)
|
|
44
|
+
if (ratio === undefined)
|
|
45
|
+
{
|
|
27
46
|
return false
|
|
28
47
|
}
|
|
48
|
+
return gt(ratio, 1) === false
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
/**
|
|
32
52
|
* @param {object} env
|
|
33
|
-
* @returns {
|
|
53
|
+
* @returns {number}
|
|
34
54
|
*/
|
|
35
|
-
function
|
|
55
|
+
function ratio_from_physics(env)
|
|
36
56
|
{
|
|
37
57
|
const output = calculate_weight_on_axles_physics({ envs: [env] })
|
|
38
|
-
const calculated_env = output.envs[0]
|
|
39
58
|
const folga_percent = Cargo_Profile.axle_weight_gap_percent()
|
|
40
|
-
return
|
|
59
|
+
return max_axle_load_ratio(output.envs[0], folga_percent)
|
|
41
60
|
}
|
|
42
61
|
|
|
43
62
|
module.exports = evaluate_layout_axle_load
|
|
63
|
+
module.exports.score_layout_axle_load = score_layout_axle_load
|
package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/layout_fits_axle_load.js
CHANGED
|
@@ -15,7 +15,7 @@ function layout_fits_axle_load(data)
|
|
|
15
15
|
{
|
|
16
16
|
return true
|
|
17
17
|
}
|
|
18
|
-
if (!
|
|
18
|
+
if (!transport_has_axles(data.transport))
|
|
19
19
|
{
|
|
20
20
|
return true
|
|
21
21
|
}
|
|
@@ -26,10 +26,11 @@ function layout_fits_axle_load(data)
|
|
|
26
26
|
* @param {object|undefined} transport
|
|
27
27
|
* @returns {boolean}
|
|
28
28
|
*/
|
|
29
|
-
function
|
|
29
|
+
function transport_has_axles(transport)
|
|
30
30
|
{
|
|
31
31
|
const env = transport?.get_axle_env_template?.()
|
|
32
32
|
return Array.isArray(env?.eixos) && env.eixos.length > 0
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
module.exports = layout_fits_axle_load
|
|
36
|
+
module.exports.transport_has_axles = transport_has_axles
|
package/src/Allocation_Optimization/item_consumption_heuristic/select_axle_balanced_slot_item.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const { lt } = require('@jetta/number-compare')
|
|
2
|
+
const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
|
|
3
|
+
const { score_without_commit, commit_scored_candidate } = require('./slot_candidate_acceptance')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Legal SKU across every weight band with the lowest hottest-axle ratio.
|
|
7
|
+
* Ties keep band order, then candidate order inside the band.
|
|
8
|
+
* @param {object} data
|
|
9
|
+
* @param {object} context
|
|
10
|
+
* @returns {object|undefined}
|
|
11
|
+
*/
|
|
12
|
+
function commit_lowest_ratio_across_bands(data, context)
|
|
13
|
+
{
|
|
14
|
+
const ranked = rank_every_band(data, context)
|
|
15
|
+
for (const selected of ranked)
|
|
16
|
+
{
|
|
17
|
+
const committed = commit_scored_candidate(data, context.container, selected)
|
|
18
|
+
if (committed)
|
|
19
|
+
{
|
|
20
|
+
return committed
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Best legal SKU across every band. Does not commit stacking.
|
|
28
|
+
* @param {object} data
|
|
29
|
+
* @param {object} context
|
|
30
|
+
* @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
|
|
31
|
+
*/
|
|
32
|
+
function lowest_ratio_across_bands(data, context)
|
|
33
|
+
{
|
|
34
|
+
const ranked = rank_every_band(data, context)
|
|
35
|
+
return ranked[0]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Preferred SKU scored without a stacking commit.
|
|
40
|
+
* @param {object} data
|
|
41
|
+
* @param {object} context
|
|
42
|
+
* @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
|
|
43
|
+
*/
|
|
44
|
+
function score_preferred(data, context)
|
|
45
|
+
{
|
|
46
|
+
const allocation_item = data.item_list.find(item => item.get_id() === data.previous_chosen_item_id)
|
|
47
|
+
if (!allocation_item)
|
|
48
|
+
{
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
return score_without_commit(data, context, allocation_item)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{ ratio: number }|undefined} preferred
|
|
56
|
+
* @param {{ ratio: number }|undefined} best
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
function preferred_wins(preferred, best)
|
|
60
|
+
{
|
|
61
|
+
if (!preferred)
|
|
62
|
+
{
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
if (!best)
|
|
66
|
+
{
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
return lt(best.ratio, preferred.ratio) === false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {object} data
|
|
74
|
+
* @param {object} context
|
|
75
|
+
* @returns {object[]}
|
|
76
|
+
*/
|
|
77
|
+
function rank_every_band(data, context)
|
|
78
|
+
{
|
|
79
|
+
const scored = []
|
|
80
|
+
for (const band of context.ordered_bands)
|
|
81
|
+
{
|
|
82
|
+
scored.push(...rank_band(data, context, band.items.slice()))
|
|
83
|
+
}
|
|
84
|
+
return rank_by_ratio(scored)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {object} data
|
|
89
|
+
* @param {object} context
|
|
90
|
+
* @param {object[]} pending
|
|
91
|
+
* @returns {object[]}
|
|
92
|
+
*/
|
|
93
|
+
function rank_band(data, context, pending)
|
|
94
|
+
{
|
|
95
|
+
const scored = []
|
|
96
|
+
while (pending.length > 0)
|
|
97
|
+
{
|
|
98
|
+
const allocation_item = get_item(pending, data.qty_of_slots, context.container)
|
|
99
|
+
const candidate = score_without_commit(data, context, allocation_item)
|
|
100
|
+
if (candidate)
|
|
101
|
+
{
|
|
102
|
+
scored.push(candidate)
|
|
103
|
+
}
|
|
104
|
+
remove_item_from_candidate_list(pending, allocation_item)
|
|
105
|
+
}
|
|
106
|
+
return rank_by_ratio(scored)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {{ ratio: number }[]} scored
|
|
111
|
+
* @returns {{ ratio: number }[]}
|
|
112
|
+
*/
|
|
113
|
+
function rank_by_ratio(scored)
|
|
114
|
+
{
|
|
115
|
+
const ranked = []
|
|
116
|
+
for (const candidate of scored)
|
|
117
|
+
{
|
|
118
|
+
insert_by_ratio(ranked, candidate)
|
|
119
|
+
}
|
|
120
|
+
return ranked
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {{ ratio: number }[]} ranked
|
|
125
|
+
* @param {{ ratio: number }} candidate
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
function insert_by_ratio(ranked, candidate)
|
|
129
|
+
{
|
|
130
|
+
let index = 0
|
|
131
|
+
while (index < ranked.length && lt(ranked[index].ratio, candidate.ratio))
|
|
132
|
+
{
|
|
133
|
+
index += 1
|
|
134
|
+
}
|
|
135
|
+
ranked.splice(index, 0, candidate)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
commit_lowest_ratio_across_bands,
|
|
140
|
+
lowest_ratio_across_bands,
|
|
141
|
+
score_preferred,
|
|
142
|
+
preferred_wins
|
|
143
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const Cargo_Profile = require('../../models/Cargo_Profile')
|
|
2
|
+
const No_Suitable_Item_Exception = require('../block_builder/exceptions/No_Suitable_Item_Exception')
|
|
3
|
+
const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
|
|
4
|
+
const { distribute_items_by_weight_range } = require('./Weight_Slot_Distribution')
|
|
5
|
+
const {
|
|
6
|
+
resolve_choose_item_heuristic_context,
|
|
7
|
+
format_slot_label_for_exception
|
|
8
|
+
} = require('./Choose_Item_Heuristic_Context')
|
|
9
|
+
const {
|
|
10
|
+
calculate_total_stack_slots,
|
|
11
|
+
sort_stacking_slots_by_proximity
|
|
12
|
+
} = require('./Stack_Slot_Calculator')
|
|
13
|
+
const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
|
|
14
|
+
const { try_accept_candidate, commit_scored_candidate } = require('./slot_candidate_acceptance')
|
|
15
|
+
const {
|
|
16
|
+
commit_lowest_ratio_across_bands,
|
|
17
|
+
lowest_ratio_across_bands,
|
|
18
|
+
score_preferred,
|
|
19
|
+
preferred_wins
|
|
20
|
+
} = require('./select_axle_balanced_slot_item')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Chooses a slot item. When the axle gate is on, the legal SKU with the lowest hottest-axle ratio wins.
|
|
24
|
+
* @param {object} data
|
|
25
|
+
* @returns {object}
|
|
26
|
+
*/
|
|
27
|
+
function select_slot_item(data)
|
|
28
|
+
{
|
|
29
|
+
const context = build_band_context(data)
|
|
30
|
+
if (Cargo_Profile.enable_axle_load_slot_gate())
|
|
31
|
+
{
|
|
32
|
+
const selected = commit_lowest_ratio_across_bands(data, context)
|
|
33
|
+
if (!selected)
|
|
34
|
+
{
|
|
35
|
+
throw unsuitable(context.slot)
|
|
36
|
+
}
|
|
37
|
+
return selected
|
|
38
|
+
}
|
|
39
|
+
for (const band of context.ordered_bands)
|
|
40
|
+
{
|
|
41
|
+
const selected = first_accepted_in_band(data, context, band.items)
|
|
42
|
+
if (selected)
|
|
43
|
+
{
|
|
44
|
+
return selected
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw unsuitable(context.slot)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Same-id preference. Under the axle gate, that SKU wins only when it is not strictly hotter.
|
|
52
|
+
* @param {object} data
|
|
53
|
+
* @returns {object}
|
|
54
|
+
*/
|
|
55
|
+
function select_preferred_or_heuristic(data)
|
|
56
|
+
{
|
|
57
|
+
if (!Cargo_Profile.enable_axle_load_slot_gate())
|
|
58
|
+
{
|
|
59
|
+
return preferred_or_first(data)
|
|
60
|
+
}
|
|
61
|
+
return preferred_or_balanced(data)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} data
|
|
66
|
+
* @returns {object}
|
|
67
|
+
*/
|
|
68
|
+
function preferred_or_first(data)
|
|
69
|
+
{
|
|
70
|
+
const { container, item_list, previous_chosen_item_id, item_position } = data
|
|
71
|
+
const allocation_item = item_list.find(item => item.get_id() === previous_chosen_item_id)
|
|
72
|
+
if (allocation_item)
|
|
73
|
+
{
|
|
74
|
+
const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
|
|
75
|
+
if (accepted_item)
|
|
76
|
+
{
|
|
77
|
+
return accepted_item
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return select_slot_item(data)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {object} data
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
function preferred_or_balanced(data)
|
|
88
|
+
{
|
|
89
|
+
const context = build_band_context(data)
|
|
90
|
+
const preferred = score_preferred(data, context)
|
|
91
|
+
const best = lowest_ratio_across_bands(data, context)
|
|
92
|
+
const winner = preferred_wins(preferred, best) ? preferred : best
|
|
93
|
+
if (!winner)
|
|
94
|
+
{
|
|
95
|
+
throw unsuitable(context.slot)
|
|
96
|
+
}
|
|
97
|
+
return commit_scored_candidate(data, context.container, winner)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {object} data
|
|
102
|
+
* @returns {object}
|
|
103
|
+
*/
|
|
104
|
+
function build_band_context(data)
|
|
105
|
+
{
|
|
106
|
+
const { slot, container, item_position } = resolve_choose_item_heuristic_context(data)
|
|
107
|
+
const total_slots = calculate_total_stack_slots(container.get_height(), slot)
|
|
108
|
+
const stacking_slots = distribute_items_by_weight_range(data.item_list, total_slots)
|
|
109
|
+
const ordered_bands = sort_stacking_slots_by_proximity(stacking_slots, slot.get_y(), slot.get_height())
|
|
110
|
+
return { slot, container, item_position, ordered_bands }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} data
|
|
115
|
+
* @param {object} context
|
|
116
|
+
* @param {object[]} candidate_items
|
|
117
|
+
* @returns {object|undefined}
|
|
118
|
+
*/
|
|
119
|
+
function first_accepted_in_band(data, context, candidate_items)
|
|
120
|
+
{
|
|
121
|
+
while (candidate_items.length > 0)
|
|
122
|
+
{
|
|
123
|
+
const allocation_item = get_item(candidate_items, data.qty_of_slots, context.container)
|
|
124
|
+
const accepted_item = try_accept_candidate(data, context.container, allocation_item, context.item_position)
|
|
125
|
+
if (accepted_item)
|
|
126
|
+
{
|
|
127
|
+
return accepted_item
|
|
128
|
+
}
|
|
129
|
+
remove_item_from_candidate_list(candidate_items, allocation_item)
|
|
130
|
+
}
|
|
131
|
+
return undefined
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {object} slot
|
|
136
|
+
* @returns {No_Suitable_Item_Exception}
|
|
137
|
+
*/
|
|
138
|
+
function unsuitable(slot)
|
|
139
|
+
{
|
|
140
|
+
return new No_Suitable_Item_Exception(
|
|
141
|
+
ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM,
|
|
142
|
+
[format_slot_label_for_exception(slot)]
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
select_slot_item,
|
|
148
|
+
select_preferred_or_heuristic
|
|
149
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const Cargo_Profile = require('../../models/Cargo_Profile')
|
|
2
|
+
const Allocated_Item = require('../../models/Allocated_Item')
|
|
3
|
+
const candidate_item_fits_axle_load = require('./axle_load/candidate_item_fits_axle_load')
|
|
4
|
+
const candidate_item_axle_score = require('./axle_load/candidate_item_axle_score')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the allocated item at the slot position.
|
|
8
|
+
* @param {object} allocation_item
|
|
9
|
+
* @param {object} item_position
|
|
10
|
+
* @returns {object}
|
|
11
|
+
*/
|
|
12
|
+
function build_allocated_item(allocation_item, item_position)
|
|
13
|
+
{
|
|
14
|
+
return new Allocated_Item(allocation_item, item_position.x, item_position.y, item_position.z)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Accepts the first legal candidate and commits stacking.
|
|
19
|
+
* @param {object} data
|
|
20
|
+
* @param {object} container
|
|
21
|
+
* @param {object} allocation_item
|
|
22
|
+
* @param {object} item_position
|
|
23
|
+
* @returns {object|undefined}
|
|
24
|
+
*/
|
|
25
|
+
function try_accept_candidate(data, container, allocation_item, item_position)
|
|
26
|
+
{
|
|
27
|
+
const allocated_item = build_allocated_item(allocation_item, item_position)
|
|
28
|
+
if (!candidate_item_fits_axle_load(data, allocated_item))
|
|
29
|
+
{
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
if (should_defer_loose_on_block_stacking_validation(data.block))
|
|
33
|
+
{
|
|
34
|
+
return allocated_item
|
|
35
|
+
}
|
|
36
|
+
if (!container.is_a_valid_stacking(allocated_item))
|
|
37
|
+
{
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
return allocated_item
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scores a candidate without writing the stacking index.
|
|
45
|
+
* @param {object} data
|
|
46
|
+
* @param {object} context
|
|
47
|
+
* @param {object} allocation_item
|
|
48
|
+
* @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
|
|
49
|
+
*/
|
|
50
|
+
function score_without_commit(data, context, allocation_item)
|
|
51
|
+
{
|
|
52
|
+
const allocated_item = build_allocated_item(allocation_item, context.item_position)
|
|
53
|
+
const score = candidate_item_axle_score(data, allocated_item)
|
|
54
|
+
if (!score.fits || !stacking_probe_allows(data, context.container, allocated_item))
|
|
55
|
+
{
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
return { allocated_item, ratio: score.ratio, allocation_item }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Commits the chosen candidate into the live stacking index.
|
|
63
|
+
* @param {object} data
|
|
64
|
+
* @param {object} container
|
|
65
|
+
* @param {{ allocated_item: object }} scored
|
|
66
|
+
* @returns {object|undefined}
|
|
67
|
+
*/
|
|
68
|
+
function commit_scored_candidate(data, container, scored)
|
|
69
|
+
{
|
|
70
|
+
if (should_defer_loose_on_block_stacking_validation(data.block))
|
|
71
|
+
{
|
|
72
|
+
return scored.allocated_item
|
|
73
|
+
}
|
|
74
|
+
if (!container.is_a_valid_stacking(scored.allocated_item))
|
|
75
|
+
{
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
return scored.allocated_item
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {object} data
|
|
83
|
+
* @param {object} container
|
|
84
|
+
* @param {object} allocated_item
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function stacking_probe_allows(data, container, allocated_item)
|
|
88
|
+
{
|
|
89
|
+
if (should_defer_loose_on_block_stacking_validation(data.block))
|
|
90
|
+
{
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
if (typeof container.probe_stacking === 'function')
|
|
94
|
+
{
|
|
95
|
+
return container.probe_stacking(allocated_item)
|
|
96
|
+
}
|
|
97
|
+
return container.is_a_valid_stacking(allocated_item)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function should_defer_loose_on_block_stacking_validation(block)
|
|
101
|
+
{
|
|
102
|
+
return Cargo_Profile.stack_loose_on_tie_ratio_blocks()
|
|
103
|
+
&& block?.get_slot_list?.().length === 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
try_accept_candidate,
|
|
108
|
+
score_without_commit,
|
|
109
|
+
commit_scored_candidate
|
|
110
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const No_Available_Items_Exception = require('./exceptions/No_Available_Items_Exception')
|
|
2
|
+
const No_Slots_For_Items_Exception = require('./exceptions/No_Slots_For_Items_Exception')
|
|
3
|
+
const { order_items_by_capacity_bias } = require('./Capacity_Bias_Item_Order')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Removes one candidate from the working list.
|
|
7
|
+
* @param {object[]} candidate_item_list
|
|
8
|
+
* @param {object} allocation_item
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
function remove_item_from_candidate_list(candidate_item_list, allocation_item)
|
|
12
|
+
{
|
|
13
|
+
const index = candidate_item_list.indexOf(allocation_item)
|
|
14
|
+
if (index > -1)
|
|
15
|
+
{
|
|
16
|
+
candidate_item_list.splice(index, 1)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Next candidate in the existing capacity-bias order.
|
|
22
|
+
* @param {object[]} item_list
|
|
23
|
+
* @param {number|undefined} qty_of_slots
|
|
24
|
+
* @param {object} container
|
|
25
|
+
* @returns {object}
|
|
26
|
+
*/
|
|
27
|
+
function get_item(item_list, qty_of_slots, container)
|
|
28
|
+
{
|
|
29
|
+
if (qty_of_slots === undefined)
|
|
30
|
+
{
|
|
31
|
+
throw new No_Slots_For_Items_Exception()
|
|
32
|
+
}
|
|
33
|
+
if (item_list.length === 0)
|
|
34
|
+
{
|
|
35
|
+
throw new No_Available_Items_Exception()
|
|
36
|
+
}
|
|
37
|
+
const ordered_item_list = order_items_by_capacity_bias(item_list, container)
|
|
38
|
+
const preferable_item_list = ordered_item_list.filter(item => item.get_amount() >= qty_of_slots)
|
|
39
|
+
if (preferable_item_list.length > 0)
|
|
40
|
+
{
|
|
41
|
+
return preferable_item_list[0]
|
|
42
|
+
}
|
|
43
|
+
return ordered_item_list[0]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
get_item,
|
|
48
|
+
remove_item_from_candidate_list
|
|
49
|
+
}
|
|
@@ -87,6 +87,17 @@ class Container extends Space
|
|
|
87
87
|
return this.#stacking_feasibility_checker.validate_stackings(item_data)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Checks stacking on a clone so the live index stays unchanged.
|
|
92
|
+
* @param {object} item
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
probe_stacking(item)
|
|
96
|
+
{
|
|
97
|
+
const probe = this.#stacking_feasibility_checker.clone()
|
|
98
|
+
return probe.validate_stackings(create_input(item))
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
/**
|
|
91
102
|
* Drops provisional stacking commits left by failed slot/wall attempts.
|
|
92
103
|
* Restores the pre-attempt checkpoint when stacking was touched; otherwise a no-op.
|