job-shop-lib 0.5.0__py3-none-any.whl → 1.0.0__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.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.0.dist-info/RECORD +0 -48
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
job_shop_lib/__init__.py,sha256=o3TWKp2_4n2WquWkaCsMo32Dd3TkXRRaPIEKpvgiI2k,639
|
2
|
+
job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
|
3
|
+
job_shop_lib/_job_shop_instance.py,sha256=hNQGSJj0rEQpS-YhzwWmM6QzCWp6r--89jkghSgLvUs,18380
|
4
|
+
job_shop_lib/_operation.py,sha256=DrnlIrmnKFT5lzMvqCMszF18WcK77AJTRDO5wWrmm7s,4273
|
5
|
+
job_shop_lib/_schedule.py,sha256=-RdCtTTj-SdNLFucmSVnrCbjZLcBZ4yfhRBdATjAaW8,11292
|
6
|
+
job_shop_lib/_scheduled_operation.py,sha256=czrGr87EOTlO2NPolIN5CDigeiCzvQEyra5IZPwSFZc,2801
|
7
|
+
job_shop_lib/benchmarking/__init__.py,sha256=BYCrJUNr_uk2c0xIbDt07OnUMhQx8Dudkukx3TFWxgw,3271
|
8
|
+
job_shop_lib/benchmarking/_load_benchmark.py,sha256=Jb6HYGKkub-3uU3l3NreRPE0PU6f0n8G9Mih5vMImOI,2936
|
9
|
+
job_shop_lib/benchmarking/benchmark_instances.json,sha256=F9EvyzFwVxiKAN6rQTsrMhsKstmyUmroyWduM7a00KQ,464841
|
10
|
+
job_shop_lib/constraint_programming/__init__.py,sha256=kKQRUxxS_nVFUdXGnf4bQOD9mqrXxZZWElS753A4YiA,454
|
11
|
+
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=vz_Kg_CmvZ13yGgqi-hZuFkosJR1v449xNaAZV3PhsE,10501
|
12
|
+
job_shop_lib/dispatching/__init__.py,sha256=SXVd0Zh6xTp-lNT7c463pii3l168NCZYf-5uOwBI1Fc,1770
|
13
|
+
job_shop_lib/dispatching/_dispatcher.py,sha256=oC-1h6p83qzumynJWMzsrbsudM1tt9AebAufKEjygRI,22039
|
14
|
+
job_shop_lib/dispatching/_dispatcher_observer_config.py,sha256=RaUkLxYCHG8Tx2tPgFyOBa8FAcbREZdKuTyLsyaYvhA,2473
|
15
|
+
job_shop_lib/dispatching/_factories.py,sha256=NzpUdxHDU_aVjHBScu8HVhSKZnTKYItHcUFS4mUp4KM,4723
|
16
|
+
job_shop_lib/dispatching/_history_observer.py,sha256=dixJe83quzGNwG0u0k2uES7GsLw0zWCjX0MOUD4VTRU,634
|
17
|
+
job_shop_lib/dispatching/_optimal_operations_observer.py,sha256=HS933mn2VlwgE7plSsth94RGlbEzfskF6TNyOM7rQrY,4246
|
18
|
+
job_shop_lib/dispatching/_ready_operation_filters.py,sha256=vzo9vfijhc-Y75GrBpxuoYKaUuSL7-picD230PdhwuI,5778
|
19
|
+
job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=3E0ePesDdWdNs6520znnOBW3eiegJj5bZg9Tmb0xoSA,2705
|
20
|
+
job_shop_lib/dispatching/feature_observers/__init__.py,sha256=EuJLvSpJpoXUK8A4UuC2k6Mpa293ZR3oCnnvYivIBtU,2240
|
21
|
+
job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=QqgadgmHjNTPOKs4wVamnPjqls9kKOH40o3N0lZZ4aw,8026
|
22
|
+
job_shop_lib/dispatching/feature_observers/_duration_observer.py,sha256=fbkUIVScF1iNjdVCYr1ImQm53TfahvVnGXhsRAsgdzY,4129
|
23
|
+
job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=W5Tr81Kme8N-m85jmX7yVc65_xlwNQBvVjnjlL-aq7w,11493
|
24
|
+
job_shop_lib/dispatching/feature_observers/_factory.py,sha256=RWrRdEDBv_0vpJNHD67_Qjov2_72yS5d7tXJiWuyobQ,3321
|
25
|
+
job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=nxDPYoVu5nAeDuXqdkA-H8slHRSt47JyHDERNachDCA,8693
|
26
|
+
job_shop_lib/dispatching/feature_observers/_is_completed_observer.py,sha256=8nmdnlPQfuudipWcxPhUPbQvlIHLmnzoeQyWkNzB-J8,3642
|
27
|
+
job_shop_lib/dispatching/feature_observers/_is_ready_observer.py,sha256=aP5CpwmCWP4w8J69qAC7QwGRQGMlfNbM31n-BRu92DA,1289
|
28
|
+
job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py,sha256=OcuMUB9_By6ZMtX-1_3z-xaxGbP85a5Zv0ywAv7XxWQ,1491
|
29
|
+
job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py,sha256=WRknpQBKXs6h6cXLFJW7ZCvjtU8CPL-iXXNPw3g-mLE,1303
|
30
|
+
job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py,sha256=5V87lCrJUabEe8AkTGXPu5yS8OGxeN8L3-xNyHmdmLs,1441
|
31
|
+
job_shop_lib/dispatching/rules/__init__.py,sha256=g3PGvMLMa3WMgNhGSW3S_xkHqoHpW8hr_9JqOfR7Xrk,2140
|
32
|
+
job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=0v7IcSQadvlX6tRy86Z55ruwIY70H9q9E46tdazjtkU,2942
|
33
|
+
job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=9-UE0HiHCeFXFGqB85cSfduLCEm5k5bJkmIujP-_irg,7321
|
34
|
+
job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=wfBdiKqEQQ8C5Gg_mrWWSuWncPwUkFacjeAQ8D4n9Wc,7648
|
35
|
+
job_shop_lib/dispatching/rules/_machine_chooser_factory.py,sha256=AtYJGuvKlc3T4Y5NCGxgjQ-np3d1aeADAZ3r68No_WA,2383
|
36
|
+
job_shop_lib/dispatching/rules/_utils.py,sha256=DFDpRoHb56Rtn01vfN69Bq0X3F8P1EtM6trHx9aXg3U,4643
|
37
|
+
job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
|
38
|
+
job_shop_lib/generation/__init__.py,sha256=tgMVhnh62lkwGKywvingFD9SLhc-vERKiWsS-41qQKA,605
|
39
|
+
job_shop_lib/generation/_general_instance_generator.py,sha256=e-NDkH-NoCwa14oADj6n_I7BX5xWWVVzRLvb4rpJ92w,6374
|
40
|
+
job_shop_lib/generation/_instance_generator.py,sha256=VV0OKX4JgFq3I1EY6s3LrOdPjM3v4lH6S1hkUebTkFQ,4615
|
41
|
+
job_shop_lib/generation/_transformations.py,sha256=X-hTAJVIHZ3bmF1rqS0zCit8r5SGpHpV8Fcl92fejow,5336
|
42
|
+
job_shop_lib/generation/_utils.py,sha256=cBhGILE0FE3TqvWoHqpaFEffO8D2fb869pF-BdMlYsg,3617
|
43
|
+
job_shop_lib/graphs/__init__.py,sha256=anR7zg1eHf1JweRGFbrwE26MeFbzQYBEHTfy6osalyU,1897
|
44
|
+
job_shop_lib/graphs/_build_disjunctive_graph.py,sha256=UbUYdeQaaeEqLchcKJGHEFGl4wElfGLb1o_R-u8wqnA,5120
|
45
|
+
job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=mWg8C-62aqvAwIKsreAHLYIq-VOc0q7BEnOnlUrywb8,6961
|
46
|
+
job_shop_lib/graphs/_constants.py,sha256=K-GeVvh_DTWpo1KOX1clmxWS_pkUJbq19yOBmrCVIxI,1086
|
47
|
+
job_shop_lib/graphs/_job_shop_graph.py,sha256=TdpUNLv9FBuosPrhLrhQl75u_kwRX0vygKFknLT6pJY,11480
|
48
|
+
job_shop_lib/graphs/_node.py,sha256=9TFH8C1D44W1IvOIG8MucLNQyLzasyBXVkMZTJU4rso,6075
|
49
|
+
job_shop_lib/graphs/graph_updaters/__init__.py,sha256=VpFsGPzm4VQiXE0vBeAL5h3jXxiR7Hap0F2BPQwRTjQ,639
|
50
|
+
job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py,sha256=-t0T8W-Jz9TJQR9-ljPkcDsDC4CwJAfs2nUF3zjEtuw,4369
|
51
|
+
job_shop_lib/graphs/graph_updaters/_graph_updater.py,sha256=j1f7iWsa62GVszK2BPaMxnKBCEGWa9owm8g4VWUje8w,1967
|
52
|
+
job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py,sha256=SyQJXIJvXijO51AzPz7YbCPZZK8d8JHE63LFX_F95Gc,6102
|
53
|
+
job_shop_lib/graphs/graph_updaters/_utils.py,sha256=sdw2Vo75P9c6Fy-YBlfgpXb9gPwHUluTB1E-9WINm_g,730
|
54
|
+
job_shop_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
55
|
+
job_shop_lib/reinforcement_learning/__init__.py,sha256=CZOnKqyjfcPXOh7ZLy-RvNbIAi1SnUs0WGC1fHo09pE,1512
|
56
|
+
job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=fwgKveAoNtO-uV4NvmfSG_ZeDPY0W2KZYcsFNHY8QY4,15749
|
57
|
+
job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py,sha256=4H53fKMgxPWqFVF5WczomEcrFdq7abBgHnOYJ1m6D1c,12768
|
58
|
+
job_shop_lib/reinforcement_learning/_reward_observers.py,sha256=iWHccnujeAKyTQn2ilQ4BhcEccoSTyJqQ5yOiP5GG_Y,2984
|
59
|
+
job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=FCghBwfKZZg2VFJdV0ihzRJBTwMuivB-zhf9qM2OZq8,16803
|
60
|
+
job_shop_lib/reinforcement_learning/_types_and_constants.py,sha256=6FpuQkZLV2H8_dXmax49OTgAw7dWQcUEWVWWdMLR7bs,1752
|
61
|
+
job_shop_lib/reinforcement_learning/_utils.py,sha256=0jM7qPCxYbURKAQlCLt4Ah1OrmdGUUhxhOsszTdt2Zk,6049
|
62
|
+
job_shop_lib/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
63
|
+
job_shop_lib/visualization/gantt/__init__.py,sha256=HGXwRgDuMAldqU0JBdiZCd5e79XBz1r96qHeDVlzE54,1145
|
64
|
+
job_shop_lib/visualization/gantt/_gantt_chart_creator.py,sha256=LTsVhpB1Fb_2o08HRZPPXSekwzR7fyTSC6h549XMqhU,8638
|
65
|
+
job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py,sha256=KeMiTBOtJKchnKGD4av8_3x3S5h437pRymc2q2knbNc,14617
|
66
|
+
job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=9-NSSNsVcW8gYLZtAuFeYURqi8cHNkVYufosKtbKFOI,6881
|
67
|
+
job_shop_lib/visualization/graphs/__init__.py,sha256=282hZFg07EyQu4HVt4GzFfYnY6ZF376IMjnWZ5eg0ZQ,611
|
68
|
+
job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=wF2zaPsvg1TszP_2n3ialTTUS7IkCqu9y79kU0bGbpw,15982
|
69
|
+
job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=RgJqHS5hJh3KkyaLbtpG_bER981BFRwGpflz7I7gS64,13271
|
70
|
+
job_shop_lib-1.0.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
71
|
+
job_shop_lib-1.0.0.dist-info/METADATA,sha256=PHGguy8Da24M4rqmxcAxkGmct54ZFVgsw5NXEBm-pAw,16110
|
72
|
+
job_shop_lib-1.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
73
|
+
job_shop_lib-1.0.0.dist-info/RECORD,,
|
@@ -1,142 +0,0 @@
|
|
1
|
-
"""Module for loading benchmark instances.
|
2
|
-
|
3
|
-
All benchmark instances are stored in a single JSON file. This module provides
|
4
|
-
functions to load the instances from the file and return them as
|
5
|
-
JobShopInstance objects.
|
6
|
-
|
7
|
-
The contributions to this benchmark dataset are as follows:
|
8
|
-
|
9
|
-
abz5-9: This subset, comprising five instances, was introduced by Adams et
|
10
|
-
al. (1988).
|
11
|
-
ft06, ft10, ft20: These three instances are attributed to the work of
|
12
|
-
Fisher and Thompson, as detailed in their 1963 work.
|
13
|
-
la01-40: A collection of forty instances, this group was contributed by
|
14
|
-
Lawrence, as referenced in his 1984 report.
|
15
|
-
orb01-10: Ten instances in this category were provided by Applegate and
|
16
|
-
Cook, as seen in their 1991 study.
|
17
|
-
swb01-20: This segment, encompassing twenty instances, was contributed by
|
18
|
-
Storer et al., as per their 1992 article.
|
19
|
-
yn1-4: Yamada and Nakano are credited with the addition of four instances
|
20
|
-
in this group, as found in their 1992 paper.
|
21
|
-
ta01-80: The largest contribution, consisting of eighty instances, was
|
22
|
-
made by Taillard, as documented in his 1993 paper.
|
23
|
-
|
24
|
-
The metadata from these instances has been updated using data from:
|
25
|
-
|
26
|
-
Thomas Weise. jsspInstancesAndResults. Accessed in January 2024.
|
27
|
-
Available at: https://github.com/thomasWeise/jsspInstancesAndResults
|
28
|
-
|
29
|
-
It includes the following information:
|
30
|
-
- "optimum" (int | None): The optimal makespan for the instance.
|
31
|
-
- "lower_bound" (int): The lower bound for the makespan. If
|
32
|
-
optimality is known, it is equal to the optimum.
|
33
|
-
- "upper_bound" (int): The upper bound for the makespan. If
|
34
|
-
optimality is known, it is equal to the optimum.
|
35
|
-
- "reference" (str): The paper or source where the instance was first
|
36
|
-
introduced.
|
37
|
-
|
38
|
-
References:
|
39
|
-
- J. Adams, E. Balas, and D. Zawack, "The shifting bottleneck procedure
|
40
|
-
for job shop scheduling," Management Science, vol. 34, no. 3,
|
41
|
-
pp. 391–401, 1988.
|
42
|
-
|
43
|
-
- J.F. Muth and G.L. Thompson, Industrial scheduling. Englewood Cliffs,
|
44
|
-
NJ: Prentice-Hall, 1963.
|
45
|
-
|
46
|
-
- S. Lawrence, "Resource constrained project scheduling: An experimental
|
47
|
-
investigation of heuristic scheduling techniques (Supplement),"
|
48
|
-
Carnegie-Mellon University, Graduate School of Industrial
|
49
|
-
Administration, Pittsburgh, Pennsylvania, 1984.
|
50
|
-
|
51
|
-
- D. Applegate and W. Cook, "A computational study of job-shop
|
52
|
-
scheduling," ORSA Journal on Computer, vol. 3, no. 2, pp. 149–156,
|
53
|
-
1991.
|
54
|
-
|
55
|
-
- R.H. Storer, S.D. Wu, and R. Vaccari, "New search spaces for
|
56
|
-
sequencing problems with applications to job-shop scheduling,"
|
57
|
-
Management Science, vol. 38, no. 10, pp. 1495–1509, 1992.
|
58
|
-
|
59
|
-
- T. Yamada and R. Nakano, "A genetic algorithm applicable to
|
60
|
-
large-scale job-shop problems," in Proceedings of the Second
|
61
|
-
International Workshop on Parallel Problem Solving from Nature
|
62
|
-
(PPSN'2), Brussels, Belgium, pp. 281–290, 1992.
|
63
|
-
|
64
|
-
- E. Taillard, "Benchmarks for basic scheduling problems," European
|
65
|
-
Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
|
66
|
-
"""
|
67
|
-
|
68
|
-
from typing import Any
|
69
|
-
|
70
|
-
import functools
|
71
|
-
import json
|
72
|
-
from importlib import resources
|
73
|
-
|
74
|
-
from job_shop_lib import JobShopInstance
|
75
|
-
|
76
|
-
|
77
|
-
@functools.cache
|
78
|
-
def load_all_benchmark_instances() -> dict[str, JobShopInstance]:
|
79
|
-
"""Loads all benchmark instances available.
|
80
|
-
|
81
|
-
Returns:
|
82
|
-
A dictionary containing the names of the benchmark instances as keys
|
83
|
-
and the corresponding JobShopInstance objects as values.
|
84
|
-
|
85
|
-
"""
|
86
|
-
benchmark_instances_dict = load_benchmark_json()
|
87
|
-
return {
|
88
|
-
name: load_benchmark_instance(name)
|
89
|
-
for name in benchmark_instances_dict
|
90
|
-
}
|
91
|
-
|
92
|
-
|
93
|
-
def load_benchmark_instance(name: str) -> JobShopInstance:
|
94
|
-
"""Loads a specific benchmark instance.
|
95
|
-
|
96
|
-
Calls to `load_benchmark_json` to load the benchmark instances from the
|
97
|
-
JSON file. The instance is then loaded from the dictionary using the
|
98
|
-
provided name. Since `load_benchmark_json` is cached, the file is only
|
99
|
-
read once.
|
100
|
-
|
101
|
-
Args:
|
102
|
-
name: The name of the benchmark instance to load. Can be one of the
|
103
|
-
following: "abz5-9", "ft06", "ft10", "ft20", "la01-40", "orb01-10",
|
104
|
-
"swb01-20", "yn1-4", or "ta01-80".
|
105
|
-
"""
|
106
|
-
benchmark_dict = load_benchmark_json()[name]
|
107
|
-
return JobShopInstance.from_matrices(
|
108
|
-
duration_matrix=benchmark_dict["duration_matrix"],
|
109
|
-
machines_matrix=benchmark_dict["machines_matrix"],
|
110
|
-
name=name,
|
111
|
-
metadata=benchmark_dict["metadata"],
|
112
|
-
)
|
113
|
-
|
114
|
-
|
115
|
-
@functools.cache
|
116
|
-
def load_benchmark_json() -> dict[str, dict[str, Any]]:
|
117
|
-
"""Loads the raw JSON file containing the benchmark instances.
|
118
|
-
|
119
|
-
Results are cached to avoid reading the file multiple times.
|
120
|
-
|
121
|
-
Each instance is represented as a dictionary with the following keys
|
122
|
-
and values:
|
123
|
-
- "name" (str): The name of the instance.
|
124
|
-
- "duration_matrix" (list[list[int]]): The matrix containing the
|
125
|
-
durations for each operation.
|
126
|
-
- "machines_matrix" (list[list[int]]): The matrix containing the
|
127
|
-
machines for each operation.
|
128
|
-
- "metadata" (dict[str, Any]): A dictionary containing metadata
|
129
|
-
about the instance. The keys are "optimum" (int | None),
|
130
|
-
"lower_bound" (int), "upper_bound" (int),
|
131
|
-
and "reference" (str).
|
132
|
-
|
133
|
-
Returns:
|
134
|
-
The dictionary containing the benchmark instances.
|
135
|
-
"""
|
136
|
-
benchmark_file = (
|
137
|
-
resources.files("job_shop_lib.benchmarking")
|
138
|
-
/ "benchmark_instances.json"
|
139
|
-
)
|
140
|
-
|
141
|
-
with benchmark_file.open("r", encoding="utf-8") as f:
|
142
|
-
return json.load(f)
|
job_shop_lib/cp_sat/__init__.py
DELETED
@@ -1,119 +0,0 @@
|
|
1
|
-
"""Home of the `DispatchingRuleSolver` class."""
|
2
|
-
|
3
|
-
from collections.abc import Callable
|
4
|
-
|
5
|
-
from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
|
6
|
-
from job_shop_lib.dispatching import (
|
7
|
-
dispatching_rule_factory,
|
8
|
-
machine_chooser_factory,
|
9
|
-
pruning_function_factory,
|
10
|
-
DispatchingRule,
|
11
|
-
MachineChooser,
|
12
|
-
Dispatcher,
|
13
|
-
PruningFunction,
|
14
|
-
)
|
15
|
-
|
16
|
-
|
17
|
-
class DispatchingRuleSolver(BaseSolver):
|
18
|
-
"""Solves a job shop instance using a dispatching rule.
|
19
|
-
|
20
|
-
Attributes:
|
21
|
-
dispatching_rule:
|
22
|
-
The dispatching rule to use. It is a callable that takes a
|
23
|
-
dispatcher and returns the operation to be dispatched next.
|
24
|
-
machine_chooser:
|
25
|
-
Used to choose the machine where the operation will be dispatched
|
26
|
-
to. It is only used if the operation can be dispatched to multiple
|
27
|
-
machines.
|
28
|
-
pruning_function:
|
29
|
-
The pruning function to use. It is used to initialize the
|
30
|
-
dispatcher object internally when calling the solve method.
|
31
|
-
"""
|
32
|
-
|
33
|
-
def __init__(
|
34
|
-
self,
|
35
|
-
dispatching_rule: (
|
36
|
-
str | Callable[[Dispatcher], Operation]
|
37
|
-
) = DispatchingRule.MOST_WORK_REMAINING,
|
38
|
-
machine_chooser: (
|
39
|
-
str | Callable[[Dispatcher, Operation], int]
|
40
|
-
) = MachineChooser.FIRST,
|
41
|
-
pruning_function: (
|
42
|
-
str
|
43
|
-
| Callable[[Dispatcher, list[Operation]], list[Operation]]
|
44
|
-
| None
|
45
|
-
) = PruningFunction.DOMINATED_OPERATIONS,
|
46
|
-
):
|
47
|
-
"""Initializes the solver with the given dispatching rule, machine
|
48
|
-
chooser and pruning function.
|
49
|
-
|
50
|
-
Args:
|
51
|
-
dispatching_rule:
|
52
|
-
The dispatching rule to use. It can be a string with the name
|
53
|
-
of the dispatching rule, a DispatchingRule enum member, or a
|
54
|
-
callable that takes a dispatcher and returns the operation to
|
55
|
-
be dispatched next.
|
56
|
-
machine_chooser:
|
57
|
-
The machine chooser to use. It can be a string with the name
|
58
|
-
of the machine chooser, a MachineChooser enum member, or a
|
59
|
-
callable that takes a dispatcher and an operation and returns
|
60
|
-
the machine id where the operation will be dispatched.
|
61
|
-
pruning_function:
|
62
|
-
The pruning function to use. It can be a string with the name
|
63
|
-
of the pruning function, a PruningFunction enum member, or a
|
64
|
-
callable that takes a dispatcher and a list of operations and
|
65
|
-
returns a list of operations that should be considered for
|
66
|
-
dispatching.
|
67
|
-
"""
|
68
|
-
if isinstance(dispatching_rule, str):
|
69
|
-
dispatching_rule = dispatching_rule_factory(dispatching_rule)
|
70
|
-
if isinstance(machine_chooser, str):
|
71
|
-
machine_chooser = machine_chooser_factory(machine_chooser)
|
72
|
-
if isinstance(pruning_function, str):
|
73
|
-
pruning_function = pruning_function_factory(pruning_function)
|
74
|
-
|
75
|
-
self.dispatching_rule = dispatching_rule
|
76
|
-
self.machine_chooser = machine_chooser
|
77
|
-
self.pruning_function = pruning_function
|
78
|
-
|
79
|
-
def solve(
|
80
|
-
self, instance: JobShopInstance, dispatcher: Dispatcher | None = None
|
81
|
-
) -> Schedule:
|
82
|
-
"""Returns a schedule for the given job shop instance using the
|
83
|
-
dispatching rule algorithm."""
|
84
|
-
if dispatcher is None:
|
85
|
-
dispatcher = Dispatcher(
|
86
|
-
instance, pruning_function=self.pruning_function
|
87
|
-
)
|
88
|
-
while not dispatcher.schedule.is_complete():
|
89
|
-
self.step(dispatcher)
|
90
|
-
|
91
|
-
return dispatcher.schedule
|
92
|
-
|
93
|
-
def step(self, dispatcher: Dispatcher) -> None:
|
94
|
-
"""Executes one step of the dispatching rule algorithm.
|
95
|
-
|
96
|
-
Args:
|
97
|
-
dispatcher:
|
98
|
-
The dispatcher object that will be used to dispatch the
|
99
|
-
operations.
|
100
|
-
"""
|
101
|
-
selected_operation = self.dispatching_rule(dispatcher)
|
102
|
-
machine_id = self.machine_chooser(dispatcher, selected_operation)
|
103
|
-
dispatcher.dispatch(selected_operation, machine_id)
|
104
|
-
|
105
|
-
|
106
|
-
if __name__ == "__main__":
|
107
|
-
import time
|
108
|
-
from job_shop_lib.benchmarking import load_benchmark_instance
|
109
|
-
|
110
|
-
ta_instances = []
|
111
|
-
for i in range(1, 81):
|
112
|
-
ta_instances.append(load_benchmark_instance(f"ta{i:02d}"))
|
113
|
-
solver = DispatchingRuleSolver(dispatching_rule="most_work_remaining")
|
114
|
-
# cProfile.run("for instance in ta_instances: solver.solve(instance)")
|
115
|
-
start = time.perf_counter()
|
116
|
-
for instance_ in ta_instances:
|
117
|
-
solver.solve(instance_)
|
118
|
-
end = time.perf_counter()
|
119
|
-
print(f"Elapsed time: {end - start:.2f} seconds.")
|
@@ -1,206 +0,0 @@
|
|
1
|
-
"""Contains factory functions for creating dispatching rules, machine choosers,
|
2
|
-
and pruning functions for the job shop scheduling problem.
|
3
|
-
|
4
|
-
The factory functions create and return the appropriate functions based on the
|
5
|
-
specified names or enums.
|
6
|
-
"""
|
7
|
-
|
8
|
-
from enum import Enum
|
9
|
-
|
10
|
-
from collections.abc import Callable, Sequence
|
11
|
-
import random
|
12
|
-
|
13
|
-
from job_shop_lib import Operation
|
14
|
-
from job_shop_lib.dispatching import (
|
15
|
-
shortest_processing_time_rule,
|
16
|
-
first_come_first_served_rule,
|
17
|
-
most_work_remaining_rule,
|
18
|
-
most_operations_remaining_rule,
|
19
|
-
random_operation_rule,
|
20
|
-
Dispatcher,
|
21
|
-
prune_dominated_operations,
|
22
|
-
prune_non_immediate_machines,
|
23
|
-
create_composite_pruning_function,
|
24
|
-
)
|
25
|
-
|
26
|
-
|
27
|
-
class DispatchingRule(str, Enum):
|
28
|
-
"""Enumeration of dispatching rules for the job shop scheduling problem."""
|
29
|
-
|
30
|
-
SHORTEST_PROCESSING_TIME = "shortest_processing_time"
|
31
|
-
FIRST_COME_FIRST_SERVED = "first_come_first_served"
|
32
|
-
MOST_WORK_REMAINING = "most_work_remaining"
|
33
|
-
MOST_OPERATIONS_REMAINING = "most_operations_remaining"
|
34
|
-
RANDOM = "random"
|
35
|
-
|
36
|
-
|
37
|
-
class MachineChooser(str, Enum):
|
38
|
-
"""Enumeration of machine chooser strategies for the job shop scheduling"""
|
39
|
-
|
40
|
-
FIRST = "first"
|
41
|
-
RANDOM = "random"
|
42
|
-
|
43
|
-
|
44
|
-
class PruningFunction(str, Enum):
|
45
|
-
"""Enumeration of pruning functions.
|
46
|
-
|
47
|
-
A pruning function is used by the `Dispatcher` class to reduce the
|
48
|
-
amount of available operations to choose from.
|
49
|
-
"""
|
50
|
-
|
51
|
-
DOMINATED_OPERATIONS = "dominated_operations"
|
52
|
-
NON_IMMEDIATE_MACHINES = "non_immediate_machines"
|
53
|
-
|
54
|
-
|
55
|
-
def dispatching_rule_factory(
|
56
|
-
dispatching_rule: str | DispatchingRule,
|
57
|
-
) -> Callable[[Dispatcher], Operation]:
|
58
|
-
"""Creates and returns a dispatching rule function based on the specified
|
59
|
-
dispatching rule name.
|
60
|
-
|
61
|
-
The dispatching rule function determines the order in which operations are
|
62
|
-
selected for execution based on certain criteria such as shortest
|
63
|
-
processing time, first come first served, etc.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
dispatching_rule: The name of the dispatching rule to be used.
|
67
|
-
Supported values are 'shortest_processing_time',
|
68
|
-
'first_come_first_served', 'most_work_remaining',
|
69
|
-
and 'random'.
|
70
|
-
|
71
|
-
Returns:
|
72
|
-
A function that takes a Dispatcher instance as input and returns an
|
73
|
-
Operation based on the specified dispatching rule.
|
74
|
-
|
75
|
-
Raises:
|
76
|
-
ValueError: If the dispatching_rule argument is not recognized or is
|
77
|
-
not supported.
|
78
|
-
"""
|
79
|
-
dispatching_rules = {
|
80
|
-
DispatchingRule.SHORTEST_PROCESSING_TIME: (
|
81
|
-
shortest_processing_time_rule
|
82
|
-
),
|
83
|
-
DispatchingRule.FIRST_COME_FIRST_SERVED: first_come_first_served_rule,
|
84
|
-
DispatchingRule.MOST_WORK_REMAINING: most_work_remaining_rule,
|
85
|
-
DispatchingRule.MOST_OPERATIONS_REMAINING: (
|
86
|
-
most_operations_remaining_rule
|
87
|
-
),
|
88
|
-
DispatchingRule.RANDOM: random_operation_rule,
|
89
|
-
}
|
90
|
-
|
91
|
-
dispatching_rule = dispatching_rule.lower()
|
92
|
-
if dispatching_rule not in dispatching_rules:
|
93
|
-
raise ValueError(
|
94
|
-
f"Dispatching rule {dispatching_rule} not recognized. Available "
|
95
|
-
f"dispatching rules: {', '.join(dispatching_rules)}."
|
96
|
-
)
|
97
|
-
|
98
|
-
return dispatching_rules[dispatching_rule] # type: ignore[index]
|
99
|
-
|
100
|
-
|
101
|
-
def machine_chooser_factory(
|
102
|
-
machine_chooser: str,
|
103
|
-
) -> Callable[[Dispatcher, Operation], int]:
|
104
|
-
"""Creates and returns a machine chooser function based on the specified
|
105
|
-
machine chooser strategy name.
|
106
|
-
|
107
|
-
The machine chooser function determines which machine an operation should
|
108
|
-
be assigned to for execution. The selection can be based on different
|
109
|
-
strategies such as choosing the first available machine or selecting a
|
110
|
-
machine randomly.
|
111
|
-
|
112
|
-
Args:
|
113
|
-
machine_chooser (str): The name of the machine chooser strategy to be
|
114
|
-
used. Supported values are 'first' and 'random'.
|
115
|
-
|
116
|
-
Returns:
|
117
|
-
A function that takes a Dispatcher instance and an Operation as input
|
118
|
-
and returns the index of the selected machine based on the specified
|
119
|
-
machine chooser strategy.
|
120
|
-
|
121
|
-
Raises:
|
122
|
-
ValueError: If the machine_chooser argument is not recognized or is
|
123
|
-
not supported.
|
124
|
-
"""
|
125
|
-
machine_choosers: dict[str, Callable[[Dispatcher, Operation], int]] = {
|
126
|
-
MachineChooser.FIRST: lambda _, operation: operation.machines[0],
|
127
|
-
MachineChooser.RANDOM: lambda _, operation: random.choice(
|
128
|
-
operation.machines
|
129
|
-
),
|
130
|
-
}
|
131
|
-
|
132
|
-
machine_chooser = machine_chooser.lower()
|
133
|
-
if machine_chooser not in machine_choosers:
|
134
|
-
raise ValueError(
|
135
|
-
f"Machine chooser {machine_chooser} not recognized. Available "
|
136
|
-
f"machine choosers: {', '.join(machine_choosers)}."
|
137
|
-
)
|
138
|
-
|
139
|
-
return machine_choosers[machine_chooser]
|
140
|
-
|
141
|
-
|
142
|
-
def composite_pruning_function_factory(
|
143
|
-
pruning_function_names: Sequence[str | PruningFunction],
|
144
|
-
) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
|
145
|
-
"""Creates and returns a composite pruning function based on the
|
146
|
-
specified list of pruning strategies.
|
147
|
-
|
148
|
-
The composite pruning function filters out operations based on
|
149
|
-
the specified list of pruning strategies.
|
150
|
-
|
151
|
-
Args:
|
152
|
-
pruning_functions:
|
153
|
-
A list of pruning strategies to be used. Supported values are
|
154
|
-
'dominated_operations' and 'non_immediate_machines'.
|
155
|
-
|
156
|
-
Returns:
|
157
|
-
A function that takes a Dispatcher instance and a list of Operation
|
158
|
-
instances as input and returns a list of Operation instances based on
|
159
|
-
the specified list of pruning strategies.
|
160
|
-
|
161
|
-
Raises:
|
162
|
-
ValueError: If any of the pruning strategies in the list are not
|
163
|
-
recognized or are not supported.
|
164
|
-
"""
|
165
|
-
|
166
|
-
pruning_functions = [
|
167
|
-
pruning_function_factory(name) for name in pruning_function_names
|
168
|
-
]
|
169
|
-
return create_composite_pruning_function(pruning_functions)
|
170
|
-
|
171
|
-
|
172
|
-
def pruning_function_factory(
|
173
|
-
pruning_function_name: str | PruningFunction,
|
174
|
-
) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
|
175
|
-
"""Creates and returns a pruning function based on the specified
|
176
|
-
pruning strategy name.
|
177
|
-
|
178
|
-
The pruning function filters out operations based on certain
|
179
|
-
criteria such as dominated operations, non-immediate machines, etc.
|
180
|
-
|
181
|
-
Args:
|
182
|
-
pruning_function:
|
183
|
-
The name of the pruning function to be used. Supported values are
|
184
|
-
'dominated_operations' and 'non_immediate_machines'.
|
185
|
-
|
186
|
-
Returns:
|
187
|
-
A function that takes a Dispatcher instance and a list of Operation
|
188
|
-
instances as input and returns a list of Operation instances based on
|
189
|
-
the specified pruning function.
|
190
|
-
|
191
|
-
Raises:
|
192
|
-
ValueError: If the pruning_function argument is not recognized or is
|
193
|
-
not supported.
|
194
|
-
"""
|
195
|
-
pruning_strategies = {
|
196
|
-
PruningFunction.DOMINATED_OPERATIONS: prune_dominated_operations,
|
197
|
-
PruningFunction.NON_IMMEDIATE_MACHINES: prune_non_immediate_machines,
|
198
|
-
}
|
199
|
-
|
200
|
-
if pruning_function_name not in pruning_strategies:
|
201
|
-
raise ValueError(
|
202
|
-
f"Unsupported pruning function '{pruning_function_name}'. "
|
203
|
-
f"Supported values are {', '.join(pruning_strategies.keys())}."
|
204
|
-
)
|
205
|
-
|
206
|
-
return pruning_strategies[pruning_function_name] # type: ignore[index]
|
@@ -1,87 +0,0 @@
|
|
1
|
-
"""Home of the `CompositeFeatureObserver` class."""
|
2
|
-
|
3
|
-
from collections import defaultdict
|
4
|
-
import numpy as np
|
5
|
-
import pandas as pd
|
6
|
-
|
7
|
-
from job_shop_lib.dispatching import Dispatcher
|
8
|
-
from job_shop_lib.dispatching.feature_observers import (
|
9
|
-
FeatureObserver,
|
10
|
-
FeatureType,
|
11
|
-
)
|
12
|
-
|
13
|
-
|
14
|
-
class CompositeFeatureObserver(FeatureObserver):
|
15
|
-
"""Aggregates features from other FeatureObserver instances subscribed to
|
16
|
-
the same `Dispatcher` by concatenating their feature matrices along the
|
17
|
-
first axis (horizontal concatenation).
|
18
|
-
|
19
|
-
Attributes:
|
20
|
-
feature_observers:
|
21
|
-
List of `FeatureObserver` instances to aggregate features from.
|
22
|
-
column_names:
|
23
|
-
Dictionary mapping `FeatureType` to a list of column names for the
|
24
|
-
corresponding feature matrix. Column names are generated based on
|
25
|
-
the class name of the `FeatureObserver` instance that produced the
|
26
|
-
feature.
|
27
|
-
"""
|
28
|
-
|
29
|
-
def __init__(
|
30
|
-
self,
|
31
|
-
dispatcher: Dispatcher,
|
32
|
-
feature_observers: list[FeatureObserver] | None = None,
|
33
|
-
subscribe: bool = True,
|
34
|
-
):
|
35
|
-
if feature_observers is None:
|
36
|
-
feature_observers = [
|
37
|
-
observer
|
38
|
-
for observer in dispatcher.subscribers
|
39
|
-
if isinstance(observer, FeatureObserver)
|
40
|
-
]
|
41
|
-
self.feature_observers = feature_observers
|
42
|
-
self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
|
43
|
-
super().__init__(dispatcher, subscribe=subscribe)
|
44
|
-
self._set_column_names()
|
45
|
-
|
46
|
-
@property
|
47
|
-
def features_as_dataframe(self) -> dict[FeatureType, pd.DataFrame]:
|
48
|
-
"""Returns the features as a dictionary of `pd.DataFrame` instances."""
|
49
|
-
return {
|
50
|
-
feature_type: pd.DataFrame(
|
51
|
-
feature_matrix, columns=self.column_names[feature_type]
|
52
|
-
)
|
53
|
-
for feature_type, feature_matrix in self.features.items()
|
54
|
-
}
|
55
|
-
|
56
|
-
def initialize_features(self):
|
57
|
-
features: dict[FeatureType, list[np.ndarray]] = defaultdict(list)
|
58
|
-
for observer in self.feature_observers:
|
59
|
-
for feature_type, feature_matrix in observer.features.items():
|
60
|
-
features[feature_type].append(feature_matrix)
|
61
|
-
|
62
|
-
self.features = {
|
63
|
-
feature_type: np.concatenate(features, axis=1)
|
64
|
-
for feature_type, features in features.items()
|
65
|
-
}
|
66
|
-
|
67
|
-
def _set_column_names(self):
|
68
|
-
for observer in self.feature_observers:
|
69
|
-
for feature_type, feature_matrix in observer.features.items():
|
70
|
-
feature_name = observer.__class__.__name__.replace(
|
71
|
-
"Observer", ""
|
72
|
-
)
|
73
|
-
if feature_matrix.shape[1] > 1:
|
74
|
-
self.column_names[feature_type] += [
|
75
|
-
f"{feature_name}_{i}"
|
76
|
-
for i in range(feature_matrix.shape[1])
|
77
|
-
]
|
78
|
-
else:
|
79
|
-
self.column_names[feature_type].append(feature_name)
|
80
|
-
|
81
|
-
def __str__(self):
|
82
|
-
out = [f"{self.__class__.__name__}:"]
|
83
|
-
out.append("-" * (len(out[0]) - 1))
|
84
|
-
for feature_type, dataframe in self.features_as_dataframe.items():
|
85
|
-
out.append(f"{feature_type.value}:")
|
86
|
-
out.append(dataframe.to_string())
|
87
|
-
return "\n".join(out)
|