├── .github └── workflows │ └── mkdoc.yml ├── .gitignore ├── .mypy.ini ├── .readthedocs.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── NOTES ├── README.md ├── TODO ├── azure-pipelines.yml ├── benchmark ├── benchmark_dev_team.py ├── benchmark_logics.py ├── benchmark_mixed.py └── benchmark_n_queens.py ├── docs ├── blog │ ├── .authors.yml │ ├── .meta.yml │ ├── index.md │ └── posts │ │ └── first-blog-post.md ├── buffer.md ├── customized_constraints.md ├── data_exchange.md ├── download_install.md ├── features.md ├── first_order_logic_constraints.md ├── function.md ├── gantt_chart.md ├── img │ ├── BufferExample.svg │ ├── CostQuadraticFunction.svg │ ├── Task.drawio │ ├── Task.svg │ ├── TasksDontOverlap.svg │ ├── TasksEndSynced.svg │ ├── TasksStartSynced.svg │ ├── TimeLineHorizon.drawio │ ├── TimeLineHorizon.svg │ ├── TwoTasksConstraint.drawio │ ├── constant_function.svg │ ├── example_1.svg │ ├── f1_gantt1.svg │ ├── f1_gantt2.svg │ ├── f1_gantt3.svg │ ├── f1_yb_screencapture.jpg │ ├── flow_shop_problem.png │ ├── flow_shop_solution.png │ ├── gantt_dynamic_1.svg │ ├── gantt_dynamic_2.svg │ ├── gantta_pinedo_232.png │ ├── linear_function.svg │ ├── multi_gantt_1.svg │ ├── multi_gantt_2.svg │ ├── multi_gantt_3.svg │ ├── pinedo_2_3_2_precedence_graph.png │ ├── pinedo_3_2_5_gantt_solution.svg │ ├── pinedo_6_1_1_gantt_solution.svg │ ├── pinedo_example_232_solution_1.svg │ ├── pinedo_example_232_solution_2.svg │ ├── pinedo_example_232_solution_3.svg │ ├── pinedo_example_3_3_3_gantt_solution.svg │ ├── pinedo_example_3_4_5_gantt_solution.svg │ ├── pinedo_example_3_6_3_gantt_solution.svg │ ├── pinedo_example_4_1_5_gantt_solution.svg │ ├── pinedo_example_4_2_3_gantt_solution.svg │ ├── polynomial_function.svg │ ├── ps_232_gantt.png │ ├── software-development-gantt.png │ ├── use-case-flowshop-gantt.svg │ └── versatility.svg ├── index.md ├── indicator.md ├── indicator_constraints.md ├── inside.md ├── javascripts │ └── tablesort.js ├── objectives.md ├── pinedo.md ├── resource.md ├── resource_assignment.md ├── resource_constraints.md ├── run.md ├── scheduling_problem.md ├── solving.md ├── task.md ├── task_constraints.md ├── use-case-flow-shop.md ├── use-case-formula-one-change-tires.md ├── use-case-software-development.md └── workflow.md ├── environment.yml ├── examples-notebooks ├── bike_shop.ipynb ├── excavator-use-case │ ├── Excavator1.ipynb │ ├── Excavator2.ipynb │ ├── Excavator3.ipynb │ ├── Excavator4.ipynb │ ├── Excavator5.ipynb │ ├── Excavator6.ipynb │ ├── Excavator7.ipynb │ ├── Excavator8.ipynb │ ├── Excavator9.ipynb │ ├── excavator_medium_size.jpg │ ├── excavator_small.jpeg │ ├── huge_hole.jpg │ ├── medium_hole.jpeg │ └── small_hole.jpeg ├── features.ipynb ├── hello_world.ipynb ├── n_queens_job_shop.ipynb ├── pics │ ├── hello_world_gantt.png │ └── hello_world_gantt.svg ├── pinedo.ipynb ├── resource_constrained_project_scheduling.ipynb ├── sports-scheduling.ipynb ├── use-case-flow-shop.ipynb ├── use-case-formula-one-change-tires.ipynb └── use-case-software-development.ipynb ├── mkdocs.yml ├── poetry.lock ├── processscheduler ├── __init__.py ├── __main__.py ├── base.py ├── buffer.py ├── constraint.py ├── excel_io.py ├── first_order_logic.py ├── function.py ├── indicator.py ├── indicator_constraint.py ├── objective.py ├── plotter.py ├── problem.py ├── resource.py ├── resource_constraint.py ├── solution.py ├── solver.py ├── task.py ├── task_constraint.py └── util.py ├── pyproject.toml ├── requirements.txt ├── setup.py ├── template.yml └── test ├── __init__.py ├── test_buffer.py ├── test_cost.py ├── test_cumulative.py ├── test_datetime.py ├── test_dynamic_resource.py ├── test_first_order_logic.py ├── test_function.py ├── test_group_of_tasks.py ├── test_indicator.py ├── test_io.py ├── test_json_io.py ├── test_multiple_objectives.py ├── test_optional_constraint.py ├── test_optional_task.py ├── test_plot.py ├── test_resource.py ├── test_resource_interrupted.py ├── test_resource_periodically_interrupted.py ├── test_resource_periodically_unavailable.py ├── test_resource_tasks_distance.py ├── test_resource_unavailable.py ├── test_schedule_n_task_in_time_interval.py ├── test_solver.py ├── test_task.py ├── test_util.py └── test_workload.py /.github/workflows/mkdoc.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | permissions: 8 | contents: write 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.x 17 | - uses: actions/cache@v2 18 | with: 19 | key: ${{ github.ref }} 20 | path: .cache 21 | - run: pip install mkdocs-material 22 | - run: pip install pillow cairosvg 23 | - run: mkdocs gh-deploy --force 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # ipython 10 | .ipynb_checkpoints 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # third part tools 33 | .coverage 34 | .pytype/ 35 | .vscode/ 36 | 37 | # test outputs 38 | bench*.svg 39 | test_*.svg 40 | test_*.html 41 | *.smt2 42 | 43 | # json io 44 | *.json 45 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.8" 7 | 8 | sphinx: 9 | configuration: doc/conf.py 10 | formats: 11 | - htmlzip 12 | 13 | python: 14 | install: 15 | - requirements: doc/requirements.txt 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guideline 2 | 3 | In any contribution, please be as explicit as possible. 4 | 5 | ## Bug report 6 | 7 | Use our [issue tracker](https://github.com/tpaviot/ProcessScheduler/issues) to report bugs. 8 | 9 | A *bug* is either: an unexpected result or an unexpected exception raised by Python. It can't be anything else. 10 | 11 | * choose an *explicit title* that starts with "Bug: " or "Error: " 12 | * the title is not enough: *always* insert a short and explicit description of what's going wrong, 13 | * the description *must* be followed by the short python script that reproduces the issue. The script must be self-contained, i.e. it can just be copied/pasted in a text editor and be executed, no need for additional imports or tweaks, 14 | * use the correct [markdown directives](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#code) to insert your syntax highlighted python code, 15 | * set the "bug" label to the issue, so that the ticket can quickly be identified as a bug. 16 | 17 | ## Feature request 18 | 19 | Use our [issue tracker](https://github.com/tpaviot/ProcessScheduler/issues) to request new features. 20 | 21 | A *request feature* has to be understood as "something I would like ProcessScheduler to be able at, but I think it cant't currently do". If you think wrong, or if there is a way to do it in a straightforward manner, then the Feature request entry will be closed after you got an answer 22 | 23 | * choose an *explicit title* that starts with "Feature request: " 24 | * insert a short/long/as you want/ description. Be as explicit as possible. Generally speaking, too general descriptions are difficult to read/understand, 25 | * you *may* insert a shot python snippet that demonstrates what you would like to achieve using the library, and how, 26 | * set the "enhancement" label to the issue. 27 | 28 | ## Contribute demos or use cases 29 | 30 | You're welcome to contribute new jupyter notebooks. 31 | 32 | ## Contribute core library code 33 | 34 | * follow the naming conventions (see below), 35 | * each commit should be described by a short and explicit commit message, 36 | * use [ruff](https://pypi.org/project/pylint/) format and check to ensure code quality 37 | ```bash 38 | pip install ruff 39 | cd ProcessScheduler/processscheduler 40 | ruff format *.py 41 | ruff check *.py 42 | ``` 43 | * submit a Pull Request (PR) 44 | 45 | ## Naming Conventions 46 | 47 | * function and method names follow the ```lowercase_separated_by_underscores``` convention 48 | * class names follow the ```UpperCamelCase``` naming convention 49 | * Tasks constraints names start either by Task or Tasks (with an ending 's'). If the constraint targets one single tasks, use the first one. If it targets two or more tasks, use the second one. 50 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | -------------------------------------------------------------------------------- /NOTES: -------------------------------------------------------------------------------- 1 | RELEASE NOTES 2 | 3 | 4 | Version 2.0.0 5 | ============= 6 | Minor fixes. 7 | 8 | Fixes: 9 | - increase code coverage 10 | - fix colab urls in notebooks 11 | - minor fixes to ForceOptionalSchedule constraint 12 | 13 | Version 2.0.0a 14 | ============== 15 | This release introduces major API changes. 16 | 17 | Bump z3-solver to release 4.12.5.0 18 | 19 | Refactoring: 20 | - port codebase to pydantic V2 21 | 22 | - export solution to pandas dataframe 23 | 24 | - define LinearFunction, PolynomialFunction etc. for costs 25 | 26 | - JSON importer/exporter improvements thanks to pydantic 27 | 28 | - use pytest for unittest suite 29 | 30 | - poetry support 31 | 32 | - refactor Indicator and Objective 33 | 34 | - documentation use mkdocs material 35 | 36 | - more tests 37 | 38 | Version 0.9.4 39 | ============= 40 | Bump z3-solver to release 4.12.4.0 41 | 42 | Version 0.9.3 43 | ============= 44 | Minor bugfix release (notebooks, documentations typos) 45 | 46 | Version 0.9.2 47 | ============= 48 | New features: 49 | - export to excel 50 | 51 | Misc: 52 | - documentation update 53 | - delay solver initialization, better tracking for conflicting constraints 54 | - explicitly raise an exception is ResourceUnavailable defined before the resource is assigned to the tasks. Fix issue #126 55 | - raise exception if resource is identified by its name and not the variable name 56 | - change report status in check_sat 57 | - fix costs computation 58 | - fix boolean examples in features notebook 59 | - move first order logic tests to test_fol.py 60 | - many typos/cosmetic changes 61 | 62 | Version 0.9.1 63 | ============= 64 | Minor fixes 65 | 66 | Version 0.9.0 67 | ============= 68 | New features: 69 | - new constraints: OrderedTaskGroup, UnorderedTaskGroup 70 | - add the builtin z3 optimizer as a solver option 71 | 72 | Misc: 73 | - max_time can be set to inf(inity) 74 | - fix cost computation 75 | - refactor the Constraint class hierarchy 76 | 77 | Version 0.8.0 78 | ============= 79 | New features: 80 | - new ResourceTasksDistance constraint 81 | - new NonConcurrentBuffer class 82 | - new TasksContiguous constraint 83 | 84 | Misc: 85 | - move random_seed parameter name to random_values. Random improved 86 | - new resource_constrained_project_scheduling notebook 87 | 88 | Version 0.7.1 89 | ============= 90 | New features: 91 | - applied Black formatting 92 | - Hotfix: temporal debugging log 93 | 94 | Version 0.7.0 95 | ============= 96 | New features: 97 | - new incremental solver for optimization 98 | - performance improvements 99 | - new Workload constraint 100 | - single resource flowtime optimization objective 101 | - linear and polynomial cost functions 102 | - add a benchmark folder including one benchmark 103 | 104 | Version 0.6.1 105 | ============= 106 | Misc: 107 | - fix regression in render_matplotlib 108 | - fix features jupyter notebook 109 | 110 | Version 0.6.0 111 | ============= 112 | New features: 113 | - full multiobjective optimization support (lexicon, box, pareto fronts) 114 | - new ScheduleNTasksInTimeIntervals task constraint 115 | 116 | Misc: 117 | - minor bugfixes 118 | 119 | Version 0.5.0 120 | ============= 121 | New features: 122 | - date and times in problem definition and result 123 | - plotly gantt chart 124 | 125 | Misc: 126 | - use z3 Solver instead of SolverFor(logic) 127 | - fix flowtime objective for optional tasks 128 | - fix priorities for optional tasks 129 | 130 | Version 0.4.0 131 | ============= 132 | New features: 133 | - optional task and resource constraints 134 | 135 | Misc: 136 | - new unit tests 137 | 138 | Version 0.3.0 139 | ============= 140 | New features: 141 | - cumulative worker 142 | 143 | Version 0.2.0 144 | ============= 145 | New features: 146 | - optional tasks (optional flag, OptionalTaskConditionSchedule and OptionalTasksDependency constraints) 147 | - resource utilization indicator 148 | - boolean ops and implies/ite can take lists of constraints 149 | 150 | Misc: 151 | - extend coverage and test suite 152 | - add a new notebook to the documentation: Flow Shop scheduling example. 153 | - bump z3 solver dependency up to 4.8.10.0 154 | 155 | Version 0.1.0 156 | ============= 157 | - First public release. 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProcessScheduler 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/7221205f866145bfa4f18c08bd96e71f)](https://www.codacy.com/gh/tpaviot/ProcessScheduler/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tpaviot/ProcessScheduler&utm_campaign=Badge_Grade) 4 | [![codecov](https://codecov.io/gh/tpaviot/ProcessScheduler/branch/master/graph/badge.svg?token=9HI1FPJUDL)](https://codecov.io/gh/tpaviot/ProcessScheduler) 5 | [![Azure Build Status](https://dev.azure.com/tpaviot/ProcessScheduler/_apis/build/status/tpaviot.ProcessScheduler?branchName=master)](https://dev.azure.com/tpaviot/ProcessScheduler/_build?definitionId=9) 6 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=examples-notebooks) 7 | [![PyPI version](https://badge.fury.io/py/ProcessScheduler.svg)](https://badge.fury.io/py/ProcessScheduler) 8 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4480745.svg)](https://doi.org/10.5281/zenodo.4480745) 9 | 10 | ProcessScheduler is a Python package for optimizing scheduling problems using advanced constraint satisfaction techniques. It provides an intuitive API for modeling complex scheduling scenarios while handling the underlying mathematical computations transparently. 11 | 12 | ## Updates 13 | 14 | - 2024/01/31: Release 2.0.0 15 | - 2024/01/30: Release 2.0.0a 16 | - 2023/12/13: Huge on-going refactoring [#133](https://github.com/tpaviot/ProcessScheduler/issues/133) 17 | - 2023/12/12: Release 0.9.4 18 | 19 | ## Key Features 20 | 21 | - **Task Management** 22 | - Define tasks with duration, priority, and work requirements 23 | - Support for fixed, variable, and zero-duration tasks 24 | - Optional tasks and task dependencies 25 | 26 | - **Resource Handling** 27 | - Individual workers with productivity and cost parameters 28 | - Resource pools with shared skills 29 | - Resource availability and unavailability periods 30 | 31 | - **Constraint Modeling** 32 | - Rich set of task and resource constraints 33 | - First-order logic operations (NOT, OR, XOR, AND, IMPLIES, IF/THEN/ELSE) 34 | - Buffer management for material consumption/production 35 | 36 | - **Optimization & Analysis** 37 | - Multi-objective optimization (makespan, flowtime, cost) 38 | - Custom performance indicators 39 | - Gantt chart visualization 40 | - Export to JSON, SMT-LIB 2.0, Excel formats 41 | 42 | ## Installation 43 | 44 | ### Basic Installation 45 | ```bash 46 | pip install ProcessScheduler==2.0.0 47 | ``` 48 | 49 | ### Full Installation (with optional dependencies) 50 | ```bash 51 | pip install ProcessScheduler[full]==2.0.0 52 | # Or install optional dependencies separately: 53 | pip install matplotlib plotly kaleido ipywidgets isodate ipympl psutil XlsxWriter 54 | ``` 55 | 56 | ## Documentation & Examples 57 | 58 | - [Documentation](https://processscheduler.github.io/) 59 | - [Interactive Examples](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=examples-notebooks) (via Binder) 60 | 61 | 62 | ## Quick Start 63 | 64 | ```python 65 | import processscheduler as ps 66 | # a simple problem, without horizon (solver will find it) 67 | pb = ps.SchedulingProblem('HelloWorldProcessScheduler') 68 | 69 | # add two tasks 70 | task_hello = ps.FixedDurationTask('Process', duration=2) 71 | task_world = ps.FixedDurationTask('Scheduler', duration=2) 72 | 73 | # precedence constraint: task_world must be scheduled 74 | # after task_hello 75 | ps.TaskPrecedence(task_hello, task_world) 76 | 77 | # solve 78 | solver = ps.SchedulingSolver(pb) 79 | solution = solver.solve() 80 | 81 | # display solution, ascii or matplotlib gantt diagram 82 | solution.render_gantt_matplotlib() 83 | ``` 84 | 85 | ![png](examples-notebooks/pics/hello_world_gantt.svg) 86 | 87 | ## Code quality 88 | 89 | ProcessScheduler uses the following tools to ensure code quality: 90 | 91 | - unittests, 92 | - code coverage (coverage.py, codecov.io), 93 | - continuous-integration at MS azure, 94 | - static code analysis (codacy), 95 | - spelling mistakes tracking (codespell), 96 | - code formatting using the black python formatter 97 | 98 | ## License/Author 99 | 100 | ProcessScheduler is distributed under the terms of the GNU General Public License v3 or (at your option) any later version. It is currently developed and maintained by Thomas Paviot (tpaviot@gmail.com). 101 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | V2 Roadmap 2 | ========== 3 | The V2 branch is a reformating of the codebase that results in backward incompatible changes. 4 | 5 | [X] port to pydantic V2 6 | 7 | [X] results to a pandas dataframe 8 | 9 | [X] get rid off context, only use problem 10 | 11 | [X] clearly separate plots and code 12 | 13 | [X] define LinearFunction, PolynomialFunction etc. for costs 14 | 15 | [X] use LinearFuncion, PolynomialFunction etc. 16 | 17 | [ ] use functions for productivity as well 18 | 19 | [WIP] JSON importer/exporter for a scheduling problem, based on pydantic should be straitforward 20 | 21 | [X] Move to pytest 22 | 23 | [WIP] pyproject.toml 24 | 25 | [X] extend tasks constraints (StartSync, EndSync etc.) to a list of n entries rather than 2 entries 26 | 27 | [X] Move first order logic to classes that inherit from NamedUIDObject 28 | 29 | [X] Objective start latest should take an optional list of tasks as a parameter 30 | 31 | [X] Use object (unique) names for object creation 32 | 33 | [X] Refactor Indicators and Objectives to get be consistent with other objects 34 | 35 | [X] Add more tests for OptimizeResourceUtilization (both Maximize and Minimize) 36 | 37 | [X] Add more tests for Objectives start earlieast, latest etc. coverage is not wide enough 38 | 39 | [X] in problem, change add_objectve_minimal 40 | 41 | [ ] in solver, change parameter "min" and "max" of incremental_solver to "minimize" and "maximize" and match the objective 'kind' parameter 42 | 43 | [ ] use the new plotly api to render gantt chart 44 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - template: template.yml 3 | parameters: 4 | name: Ubuntu_22_04_python310 5 | vmImage: 'ubuntu-22.04' 6 | 7 | - template: template.yml 8 | parameters: 9 | name: macOS_13_python310 10 | vmImage: 'macOS-13' 11 | 12 | - template: template.yml 13 | parameters: 14 | name: Windows_VS2022_python310 15 | vmImage: 'windows-2022' 16 | -------------------------------------------------------------------------------- /benchmark/benchmark_dev_team.py: -------------------------------------------------------------------------------- 1 | # ProcessScheduler benchmark 2 | import argparse 3 | import time 4 | from datetime import datetime 5 | import subprocess 6 | import platform 7 | import uuid 8 | import psutil 9 | 10 | import matplotlib.pyplot as plt 11 | import processscheduler as ps 12 | import z3 13 | 14 | # 15 | # Argument parser 16 | # 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument( 19 | "-p", "--plot", default=True, help="Display results in a matplotlib chart" 20 | ) 21 | parser.add_argument("-n", "--nmax", default=100, help="max dev team") 22 | parser.add_argument("-s", "--step", default=10, help="step") 23 | parser.add_argument( 24 | "-mt", "--max_time", default=60, help="Maximum time in seconds to find a solution" 25 | ) 26 | parser.add_argument("-l", "--logics", default="QF_IDL", help="SMT logics") 27 | 28 | args = parser.parse_args() 29 | 30 | n = int(args.nmax) # max number of dev teams 31 | mt = int(args.max_time) # max time in seconds 32 | step = int(args.step) 33 | 34 | 35 | # 36 | # Display machine identification 37 | # 38 | def get_size(byt, suffix="B"): 39 | """ 40 | Scale bytes to its proper format 41 | e.g: 42 | 1253656 => '1.20MB' 43 | 1253656678 => '1.17GB' 44 | """ 45 | # Code from https://www.thepythoncode.com/article/get-hardware-system-information-python 46 | factor = 1024 47 | for unit in ["", "K", "M", "G", "T", "P"]: 48 | if byt < factor: 49 | return f"{byt:.2f}{unit}{suffix}" 50 | byt /= factor 51 | 52 | 53 | bench_id = uuid.uuid4().hex[:8] 54 | bench_date = datetime.now() 55 | print("#### Benchmark information header ####") 56 | print("Date:", bench_date) 57 | print("Id:", bench_id) 58 | print("Software:") 59 | print("\tPython version:", platform.python_version()) 60 | print("\tProcessScheduler version:", ps.__VERSION__) 61 | commit_short_hash = subprocess.check_output( 62 | ["git", "rev-parse", "--short", "HEAD"] 63 | ).strip() 64 | print("\tz3 version:", z3.Z3_get_full_version()) 65 | 66 | print("\tProcessScheduler commit number:", commit_short_hash.decode("utf-8")) 67 | os_info = platform.uname() 68 | print("OS:") 69 | print("\tOS:", os_info.system) 70 | print("\tOS Release:", os_info.release) 71 | print("\tOS Version:", os_info.version) 72 | print("Hardware:") 73 | print("\tMachine:", os_info.machine) 74 | print("\tPhysical cores:", psutil.cpu_count(logical=False)) 75 | print("\tTotal cores:", psutil.cpu_count(logical=True)) 76 | # CPU frequencies 77 | cpufreq = psutil.cpu_freq() 78 | print(f"\tMax Frequency: {cpufreq.max:.2f}Mhz") 79 | print(f"\tMin Frequency: {cpufreq.min:.2f}Mhz") 80 | # get the memory details 81 | svmem = psutil.virtual_memory() 82 | print(f"\tTotal memory: {get_size(svmem.total)}") 83 | 84 | computation_times = [] 85 | plot_abs = [] 86 | 87 | N = list(range(10, n, step)) # from 4 to N, step 2 88 | 89 | # Teams and Resources 90 | num_resource_a = 2 91 | num_resource_b = 2 92 | 93 | for num_dev_teams in N: 94 | print("-> Num dev teams:", num_dev_teams) 95 | # Resources 96 | digital_transformation = ps.SchedulingProblem( 97 | name="DigitalTransformation", horizon=num_dev_teams 98 | ) 99 | r_a = [ps.Worker(name="A_%i" % (i + 1)) for i in range(num_resource_a)] 100 | r_b = [ps.Worker(name="B_%i" % (i + 1)) for i in range(num_resource_b)] 101 | 102 | # Dev Team Tasks 103 | # For each dev_team pick one resource a and one resource b. 104 | ts_team_migration = [ 105 | ps.FixedDurationTask(name="DevTeam_%i" % (i + 1), duration=1, priority=10) 106 | for i in range(num_dev_teams) 107 | ] 108 | for t_team_migration in ts_team_migration: 109 | t_team_migration.add_required_resource(ps.SelectWorkers(list_of_workers=r_a)) 110 | t_team_migration.add_required_resource(ps.SelectWorkers(list_of_workers=r_b)) 111 | 112 | # create the solver and solve 113 | solver = ps.SchedulingSolver( 114 | problem=digital_transformation, max_time=mt, logics=args.logics 115 | ) 116 | init_time = time.perf_counter() 117 | 118 | solution = solver.solve() 119 | 120 | if not solution: 121 | break 122 | 123 | computation_times.append(time.perf_counter() - init_time) 124 | plot_abs.append(num_dev_teams) 125 | 126 | solver.print_statistics() 127 | 128 | plt.title(f"Benchmark SelectWorkers {bench_date}:{bench_id}") 129 | plt.plot(plot_abs, computation_times, "D-", label="Computing time") 130 | plt.legend() 131 | plt.xlabel("n") 132 | plt.ylabel("time(s)") 133 | plt.grid(True) 134 | plt.savefig(f"bench_dev_team_{bench_id}.svg") 135 | if args.plot: 136 | plt.show() 137 | -------------------------------------------------------------------------------- /benchmark/benchmark_logics.py: -------------------------------------------------------------------------------- 1 | # ProcessScheduler benchmark 2 | import json 3 | import time 4 | from datetime import datetime 5 | import subprocess 6 | import os 7 | import platform 8 | import psutil 9 | import uuid 10 | 11 | import processscheduler as ps 12 | import z3 13 | 14 | try: 15 | from rich import print 16 | except: 17 | pass 18 | 19 | 20 | # 21 | # Display machine identification 22 | # 23 | def get_size(byt, suffix="B"): 24 | """ 25 | Scale bytes to its proper format 26 | e.g: 27 | 1253656 => '1.20MB' 28 | 1253656678 => '1.17GB' 29 | """ 30 | # Code from https://www.thepythoncode.com/article/get-hardware-system-information-python 31 | factor = 1024 32 | for unit in ["", "K", "M", "G", "T", "P"]: 33 | if byt < factor: 34 | return f"{byt:.2f}{unit}{suffix}" 35 | byt /= factor 36 | 37 | 38 | bench_id = uuid.uuid4().hex 39 | bench_date = datetime.now() 40 | print("#### Benchmark information header ####") 41 | print("Date:", bench_date) 42 | print("Id:", bench_id) 43 | print("Software:") 44 | print("\tPython version:", platform.python_version()) 45 | print("\tProcessScheduler version:", ps.__VERSION__) 46 | commit_short_hash = subprocess.check_output( 47 | ["git", "rev-parse", "--short", "HEAD"] 48 | ).strip() 49 | print("\tz3 version:", z3.Z3_get_full_version()) 50 | 51 | print("\tProcessScheduler commit number:", commit_short_hash.decode("utf-8")) 52 | os_info = os.uname() 53 | print("OS:") 54 | print("\tOS:", os_info.sysname) 55 | print("\tOS Release:", os_info.release) 56 | print("\tOS Version:", os_info.version) 57 | print("Hardware:") 58 | print("\tMachine:", os_info.machine) 59 | print("\tPhysical cores:", psutil.cpu_count(logical=False)) 60 | print("\tTotal cores:", psutil.cpu_count(logical=True)) 61 | # CPU frequencies 62 | cpufreq = psutil.cpu_freq() 63 | print(f"\tMax Frequency: {cpufreq.max:.2f}Mhz") 64 | print(f"\tMin Frequency: {cpufreq.min:.2f}Mhz") 65 | # get the memory details 66 | svmem = psutil.virtual_memory() 67 | print(f"\tTotal memory: {get_size(svmem.total)}") 68 | 69 | model_creation_times = [] 70 | computation_times = [] 71 | 72 | md_template = "| logics | computing_time(s) | flowtime | priority | obj value |\n" 73 | md_template += "| - | - | - | - | - |\n" 74 | 75 | 76 | test_init_time = time.perf_counter() 77 | all_logics = [ 78 | "QF_LRA", 79 | # "HORN", 80 | "QF_LIA", 81 | "QF_RDL", 82 | "QF_IDL", 83 | "QF_AUFLIA", 84 | "QF_ALIA", 85 | "QF_AUFLIRA", 86 | "QF_AUFNIA", 87 | "QF_AUFNIRA", 88 | "QF_ANIA", 89 | "QF_LIRA", 90 | "QF_UFLIA", 91 | "QF_UFLRA", 92 | "QF_UFIDL", 93 | "QF_UFRDL", 94 | "QF_NIRA", 95 | "QF_UFNRA", 96 | "QF_UFNIA", 97 | "QF_UFNIRA", 98 | "QF_S", 99 | "QF_SLIA", 100 | "UFIDL", 101 | "HORN", 102 | "QF_FPLRA", 103 | ] 104 | # skipped: "QF_NIA", 105 | 106 | 107 | num_dev_teams = 20 108 | print("-> Num dev teams:", num_dev_teams) 109 | # Teams and Resources 110 | num_resource_a = 3 111 | num_resource_b = 3 112 | 113 | init_time = time.perf_counter() 114 | 115 | 116 | # Resources 117 | def get_problem(): 118 | digital_transformation = ps.SchedulingProblem(name="DigitalTransformation") 119 | print("Create model...", end="") 120 | r_a = [ps.Worker(name="A_%i" % (i + 1)) for i in range(num_resource_a)] 121 | r_b = [ps.Worker(name="B_%i" % (i + 1)) for i in range(num_resource_b)] 122 | # Dev Team Tasks 123 | # For each dev_team pick one resource a and one resource b. 124 | ts_team_migration = [ 125 | ps.FixedDurationTask( 126 | name="DevTeam_%i" % (i + 1), duration=1, priority=i % 3 + 1 127 | ) 128 | for i in range(num_dev_teams) 129 | ] 130 | for t_team_migration in ts_team_migration: 131 | t_team_migration.add_required_resource(ps.SelectWorkers(list_of_workers=r_a)) 132 | t_team_migration.add_required_resource(ps.SelectWorkers(list_of_workers=r_b)) 133 | 134 | # optimization 135 | ps.ObjectivePriorities() 136 | ps.ObjectiveMinimizeFlowtime() 137 | return digital_transformation 138 | 139 | 140 | top1 = time.perf_counter() 141 | mt = 60 142 | results = {} 143 | for logics in all_logics: 144 | digital_transformation = get_problem() 145 | top2 = time.perf_counter() 146 | solver = ps.SchedulingSolver( 147 | problem=digital_transformation, 148 | logics=logics, 149 | random_values=True, 150 | parallel=False, 151 | max_iter=10, 152 | max_time=10, 153 | ) 154 | if solution := solver.solve(): 155 | flowtime_result = solution.indicators["Flowtime"] 156 | priority_result = solution.indicators["TotalPriority"] 157 | total_result = flowtime_result + priority_result 158 | else: 159 | flowtime_result, priority_result, total_result = None, None, None 160 | computing_time = time.perf_counter() - top2 161 | 162 | computation_times.append(computing_time) 163 | 164 | print("Logics:", logics, "Total Time:", computing_time) 165 | 166 | results[logics] = { 167 | "computing_time (s)": f"{computing_time:.2f}", 168 | "flowtime result (lower is better)": flowtime_result, 169 | "priority_result (lower is better)": priority_result, 170 | "total objective": total_result, 171 | } 172 | md_template += f"|{logics}|{computing_time:.2f}|{flowtime_result}|{priority_result}|{total_result}|\n" 173 | 174 | 175 | test_final_time = time.perf_counter() 176 | print("TOTAL BENCH TIME:", test_final_time - test_init_time) 177 | print("Results:") 178 | print(results) 179 | # save result to json 180 | with open(f"benchmark_logics_results_{bench_id}.json", "w") as f: 181 | f.write(json.dumps(results)) 182 | 183 | # save results to markdown 184 | with open(f"benchmark_logics_results_{bench_id}.md", "w") as f: 185 | f.write(md_template) 186 | -------------------------------------------------------------------------------- /benchmark/benchmark_mixed.py: -------------------------------------------------------------------------------- 1 | # ProcessScheduler benchmark 2 | import argparse 3 | import time 4 | from datetime import datetime 5 | import subprocess 6 | import platform 7 | import uuid 8 | import psutil 9 | 10 | import matplotlib.pyplot as plt 11 | import processscheduler as ps 12 | import z3 13 | 14 | # 15 | # Argument parser 16 | # 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument( 19 | "-p", "--plot", default=True, help="Display results in a matplotlib chart" 20 | ) 21 | parser.add_argument("-n", "--nmax", default=100, help="max dev team") 22 | parser.add_argument("-s", "--step", default=10, help="step") 23 | parser.add_argument( 24 | "-mt", "--max_time", default=60, help="Maximum time in seconds to find a solution" 25 | ) 26 | parser.add_argument("-l", "--logics", default=None, help="SMT logics") 27 | 28 | args = parser.parse_args() 29 | 30 | n = int(args.nmax) # max number of dev teams 31 | mt = int(args.max_time) # max time in seconds 32 | step = int(args.step) 33 | 34 | # 35 | # Display machine identification 36 | # 37 | 38 | 39 | def get_size(byt, suffix="B"): 40 | """ 41 | Scale bytes to its proper format 42 | e.g: 43 | 1253656 => '1.20MB' 44 | 1253656678 => '1.17GB' 45 | """ 46 | # Code from https://www.thepythoncode.com/article/get-hardware-system-information-python 47 | factor = 1024 48 | for unit in ["", "K", "M", "G", "T", "P"]: 49 | if byt < factor: 50 | return f"{byt:.2f}{unit}{suffix}" 51 | byt /= factor 52 | 53 | 54 | bench_id = uuid.uuid4().hex[:8] 55 | bench_date = datetime.now() 56 | print("#### Benchmark information header ####") 57 | print("Date:", bench_date) 58 | print("Id:", bench_id) 59 | print("Software:") 60 | print("\tPython version:", platform.python_version()) 61 | print("\tProcessScheduler version:", ps.__VERSION__) 62 | commit_short_hash = subprocess.check_output( 63 | ["git", "rev-parse", "--short", "HEAD"] 64 | ).strip() 65 | print("\tz3 version:", z3.Z3_get_full_version()) 66 | 67 | print("\tProcessScheduler commit number:", commit_short_hash.decode("utf-8")) 68 | os_info = platform.uname() 69 | print("OS:") 70 | print("\tOS:", os_info.system) 71 | print("\tOS Release:", os_info.release) 72 | print("\tOS Version:", os_info.version) 73 | print("Hardware:") 74 | print("\tMachine:", os_info.machine) 75 | print("\tPhysical cores:", psutil.cpu_count(logical=False)) 76 | print("\tTotal cores:", psutil.cpu_count(logical=True)) 77 | # CPU frequencies 78 | cpufreq = psutil.cpu_freq() 79 | print(f"\tMax Frequency: {cpufreq.max:.2f}Mhz") 80 | print(f"\tMin Frequency: {cpufreq.min:.2f}Mhz") 81 | # get the memory details 82 | svmem = psutil.virtual_memory() 83 | print(f"\tTotal memory: {get_size(svmem.total)}") 84 | 85 | computation_times = [] 86 | plot_abs = [] 87 | 88 | MAX_TASKS_PER_PERIOD = 2 89 | MAX_TASKS_IN_PROBLEM = 4 90 | NB_WORKERS = 10 91 | NB_TASKS_PER_WORKER = 10 92 | for horizon in range(20, n, step): 93 | PERIODS = [ 94 | (10 * i, 10 * (i + 1)) for i in range(int(horizon / 10)) 95 | ] # Periods of 10 slots from 0 to horizon 96 | init_time = time.perf_counter() 97 | 98 | # Create problem and initialize constraints 99 | pb = ps.SchedulingProblem(name="performance_analyzer", horizon=horizon) 100 | # Create resources and assign tasks 101 | general_worker = ps.Worker(name="general") 102 | workers = [] 103 | for i in range(NB_WORKERS): 104 | name = f"worker_{i+1}" 105 | worker = ps.Worker(name=name) 106 | 107 | # Create tasks and assign resources 108 | tasks = [] 109 | for j in range(NB_TASKS_PER_WORKER): 110 | tasks.append( 111 | ps.FixedDurationTask(name=f"{name}__{j:02d}", duration=1, optional=True) 112 | ) 113 | tasks[-1].add_required_resources([general_worker, worker]) 114 | 115 | workers.append({"name": name, "worker": worker, "tasks": tasks}) 116 | 117 | workload = {period: MAX_TASKS_PER_PERIOD for period in PERIODS} 118 | workload[(0, horizon)] = MAX_TASKS_IN_PROBLEM 119 | 120 | for worker in workers: 121 | ps.WorkLoad( 122 | resource=worker["worker"], 123 | dict_time_intervals_and_bound=workload, 124 | kind="max", 125 | ) 126 | 127 | # Add constraints, define objective and solve problem 128 | ps.ObjectiveMaximizeResourceUtilization(resource=general_worker) 129 | solver = ps.SchedulingSolver(problem=pb, max_time=mt, logics=args.logics) 130 | solution = solver.solve() 131 | if not solution: 132 | break 133 | 134 | computation_times.append(time.perf_counter() - init_time) 135 | plot_abs.append(i) 136 | 137 | solver.print_statistics() 138 | 139 | plt.title(f"Benchmark_mixed_constraints {bench_date}:{bench_id}") 140 | plt.plot(plot_abs, computation_times, "D-", label="Computing time") 141 | plt.legend() 142 | plt.xlabel("n") 143 | plt.ylabel("time(s)") 144 | plt.grid(True) 145 | plt.savefig(f"bench_{bench_id}.svg") 146 | if args.plot: 147 | plt.show() 148 | -------------------------------------------------------------------------------- /benchmark/benchmark_n_queens.py: -------------------------------------------------------------------------------- 1 | # ProcessScheduler benchmark 2 | import argparse 3 | import time 4 | from datetime import datetime 5 | import subprocess 6 | import platform 7 | import uuid 8 | import psutil 9 | 10 | import matplotlib.pyplot as plt 11 | import processscheduler as ps 12 | import z3 13 | 14 | # 15 | # Argument parser 16 | # 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument( 19 | "-p", "--plot", default=True, help="Display results in a matplotlib chart" 20 | ) 21 | parser.add_argument("-n", "--nmax", default=40, help="max dev team") 22 | parser.add_argument("-s", "--step", default=5, help="step") 23 | parser.add_argument( 24 | "-mt", "--max_time", default=60, help="Maximum time in seconds to find a solution" 25 | ) 26 | parser.add_argument("-l", "--logics", default="QF_IDL", help="SMT logics") 27 | 28 | args = parser.parse_args() 29 | 30 | n = int(args.nmax) # max number of dev teams 31 | mt = int(args.max_time) # max time in seconds 32 | step = int(args.step) 33 | 34 | 35 | # 36 | # Display machine identification 37 | # 38 | def get_size(byt, suffix="B"): 39 | """ 40 | Scale bytes to its proper format 41 | e.g: 42 | 1253656 => '1.20MB' 43 | 1253656678 => '1.17GB' 44 | """ 45 | # Code from https://www.thepythoncode.com/article/get-hardware-system-information-python 46 | factor = 1024 47 | for unit in ["", "K", "M", "G", "T", "P"]: 48 | if byt < factor: 49 | return f"{byt:.2f}{unit}{suffix}" 50 | byt /= factor 51 | 52 | 53 | bench_id = uuid.uuid4().hex[:8] 54 | bench_date = datetime.now() 55 | print("#### Benchmark information header ####") 56 | print("Date:", bench_date) 57 | print("Id:", bench_id) 58 | print("Software:") 59 | print("\tPython version:", platform.python_version()) 60 | print("\tProcessScheduler version:", ps.__VERSION__) 61 | commit_short_hash = subprocess.check_output( 62 | ["git", "rev-parse", "--short", "HEAD"] 63 | ).strip() 64 | print("\tz3 version:", z3.Z3_get_full_version()) 65 | 66 | print("\tProcessScheduler commit number:", commit_short_hash.decode("utf-8")) 67 | os_info = platform.uname() 68 | print("OS:") 69 | print("\tOS:", os_info.system) 70 | print("\tOS Release:", os_info.release) 71 | print("\tOS Version:", os_info.version) 72 | print("Hardware:") 73 | print("\tMachine:", os_info.machine) 74 | print("\tPhysical cores:", psutil.cpu_count(logical=False)) 75 | print("\tTotal cores:", psutil.cpu_count(logical=True)) 76 | # CPU frequencies 77 | cpufreq = psutil.cpu_freq() 78 | print(f"\tMax Frequency: {cpufreq.max:.2f}Mhz") 79 | print(f"\tMin Frequency: {cpufreq.min:.2f}Mhz") 80 | # get the memory details 81 | svmem = psutil.virtual_memory() 82 | print(f"\tTotal memory: {get_size(svmem.total)}") 83 | 84 | computation_times = [] 85 | plot_abs = [] 86 | 87 | N = list(range(10, n, step)) # from 4 to N, step 2 88 | 89 | for problem_size in N: 90 | print("-> Problem size:", problem_size) 91 | # Teams and Resources 92 | 93 | pb = ps.SchedulingProblem(name="n_queens_type_scheduling", horizon=problem_size) 94 | R = {i: ps.Worker(name="W-%i" % i) for i in range(problem_size)} 95 | T = { 96 | (i, j): ps.FixedDurationTask(name="T-%i-%i" % (i, j), duration=1) 97 | for i in range(n) 98 | for j in range(problem_size) 99 | } 100 | # precedence constrains 101 | for i in range(problem_size): 102 | for j in range(1, problem_size): 103 | ps.TaskPrecedence(task_before=T[i, j - 1], task_after=T[i, j], offset=0) 104 | # resource assignment modulo n 105 | for j in range(problem_size): 106 | for i in range(problem_size): 107 | T[(i + j) % problem_size, j].add_required_resource(R[i]) 108 | 109 | # create the solver and solve 110 | solver = ps.SchedulingSolver(problem=pb, max_time=mt, logics=args.logics) 111 | init_time = time.perf_counter() 112 | solution = solver.solve() 113 | 114 | if not solution: 115 | break 116 | 117 | computation_times.append(time.perf_counter() - init_time) 118 | plot_abs.append(problem_size) 119 | 120 | solver.print_statistics() 121 | 122 | plt.title(f"Benchmark SelectWorkers {bench_date}:{bench_id}") 123 | plt.plot(plot_abs, computation_times, "D-", label="Computing time") 124 | plt.legend() 125 | plt.xlabel("n") 126 | plt.ylabel("time(s)") 127 | plt.grid(True) 128 | plt.savefig(f"bench_n_queens_{bench_id}.svg") 129 | if args.plot: 130 | plt.show() 131 | -------------------------------------------------------------------------------- /docs/blog/.authors.yml: -------------------------------------------------------------------------------- 1 | authors: 2 | tpaviot: 3 | name: Thomas Paviot 4 | description: Creator 5 | avatar: https://avatars.githubusercontent.com/tpaviot 6 | url: https://github.com/tpaviot 7 | -------------------------------------------------------------------------------- /docs/blog/.meta.yml: -------------------------------------------------------------------------------- 1 | comments: true 2 | hide: 3 | - feedback 4 | -------------------------------------------------------------------------------- /docs/blog/index.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | -------------------------------------------------------------------------------- /docs/blog/posts/first-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: First blog post 3 | date: 2023-11-30 4 | authors: [tpaviot] 5 | slug: first-blog-post 6 | description: > 7 | A simple blog post. 8 | categories: 9 | - General 10 | --- 11 | 12 | # Major changes are on the way 13 | 14 | The development of ProcessScheduler began in the fall of 2021. The past two years have laid the foundation for a free and open-source framework dedicated to planning and scheduling for industrial applications. The experience gained during this initial phase allows for medium-term projections. 15 | 16 | 17 | The current work is a complete overhaul of the code that aims to achieve the following objectives: 18 | 19 | ## Improve the quality and robustness of the code 20 | This involves expanding the test base, the code coverage. The pydantic library has been chosen to establish the definition of all classes. 21 | 22 | ## Give coherence to the codebase 23 | The project now needs a cleanup that ensures a consistency of the API, which has developed over time according to needs. 24 | 25 | ## Increase functional coverage 26 | To be able to model and simulate an increasing number of industrial situations and real use cases, new classes and functionalities must be added. 27 | 28 | ## Rewrite the documentation 29 | The documentation hosted by the readthedocs service does not provide the required quality. -------------------------------------------------------------------------------- /docs/buffer.md: -------------------------------------------------------------------------------- 1 | A `Buffer` is an object in a scheduling environment where tasks can load or unload a finite number of items. Buffers are essential for managing the flow of discrete quantities in a production or processing system. These can be: 2 | 3 | * **Physical Discrete Quantities**: Such as mechanical parts, bottles of water, steel structural pieces. 4 | 5 | * **Immaterial Discrete Quantities**: Such as currencies (dollars, euros) representing a budget or treasury, or digital items like pdf documents. 6 | 7 | ## Types of Buffers 8 | 9 | ``` mermaid 10 | classDiagram 11 | Buffer <|-- NonConcurrentBuffer 12 | Buffer <|-- ConcurrentBuffer 13 | class Buffer{ 14 | +int upper_bound 15 | +int lowet_bound 16 | +int initial_level 17 | +int final_level 18 | } 19 | ``` 20 | 21 | 1. **NonConcurrentBuffer**: This buffer type ensures exclusive access, meaning only one task can load or unload at a given time. The buffer becomes available to another task only after the current task is completed. This is akin to the blocking phenomenon in flow shops, where a task remains on a machine until the buffer ahead is available. 22 | 23 | 2. **ConcurrentBuffer**: This buffer allows simultaneous access by multiple tasks, reflecting a more flexible and potentially higher throughput environment, as seen in flexible flow lines. 24 | 25 | ## Buffer Attributes 26 | 27 | * `initial_level`: Represents the number of items in the buffer at time `t=0`. 28 | 29 | * `final_level`: Represents the number of items in the buffer at schedule `t=horizon`. 30 | 31 | * `lower_bound`: An optional parameter setting the minimum number of items in the buffer. If the buffer level falls below this, the problem is unsatisfiable. This parameter can be crucial in settings where maintaining a minimum inventory level is essential for continuous operation. 32 | 33 | * `upper_bound`: An optional parameter representing the maximum buffer capacity. Exceeding this limit makes the problem unsatisfiable. This reflects the physical limitations of buffer spaces in industrial settings, especially with larger items. 34 | 35 | Both `initial_level`, `final_level`, `lower_bound` and `upper_bound` are optional parameters. 36 | 37 | !!! note 38 | 39 | If `lower_bound` (resp. `upper_bound`) is specified, the solver will schedule tasks to that the buffer level is never lower (resp. greater) than the lower (resp. upper) bound. 40 | 41 | A `NonConcurrentBuffer` can be created as follows: 42 | 43 | ``` py 44 | buff1 = ps.NonConcurrentBuffer(name="Buffer1") 45 | buff2 = ps.NonConcurrentBuffer(name="Buffer2", initial_level=10) 46 | buff3 = ps.NonConcurrentBuffer(name="Buffer3", lower_bound=0) 47 | buff4 = ps.NonConcurrentBuffer(name="Buffer4", upper_bound=20) 48 | buff5 = ps.NonConcurrentBuffer(name="Buffer5", 49 | initial_level=3, 50 | lower_bound=0, 51 | upper_bound=10) 52 | ``` 53 | 54 | ## Loading and unloading buffers 55 | 56 | Buffers are loaded/unloaded by dedicate tasks. 57 | 58 | * **Unloading Tasks**: These tasks remove a specified quantity at the task start time, mimicking the immediate release of resources upon task commencement. 59 | 60 | * **Loading Tasks**: These tasks add to the buffer upon task completion, reflecting the production or processing output. 61 | 62 | Load/Unload constraints can be created as follows: 63 | 64 | ``` py 65 | c1 = ps.TaskUnloadBuffer(task_1, buffer, quantity=3) 66 | c2 = ps.TaskLoadBuffer(task_2, buffer, quantity=6) 67 | ``` 68 | 69 | !!! note 70 | 71 | There is no limitation on the number of buffers and/or buffer constraints. 72 | 73 | !!! note 74 | 75 | Unloading tasks remove quantities at the task start time, while loading tasks add to the buffer at task completion time. 76 | 77 | ### Example 78 | 79 | Let's take an example where a task `T1` uses a machine `M1` to manufacture a part (duration time for this task is 4). It takes one raw part in a `Buffer1` at the start time and loads the `Buffer2` at completion time. 80 | 81 | ``` py 82 | import processscheduler as ps 83 | 84 | pb = ps.SchedulingProblem(name="BufferExample", horizon=6) 85 | machine_1 = ps.Worker(name="M1") 86 | task_1 = ps.FixedDurationTask(name="T1", duration=4) 87 | ps.TaskStartAt(task=task_1, value=1) 88 | task_1.add_required_resource(machine_1) 89 | 90 | # the buffers 91 | buffer_1 = ps.NonConcurrentBuffer(name="Buffer1", initial_level=5) 92 | buffer_2 = ps.NonConcurrentBuffer(name="Buffer2", initial_level=0) 93 | 94 | # buffer constraints 95 | bc_1 = ps.TaskUnloadBuffer(task=task_1, buffer=buffer_1, quantity=1) 96 | bc_2 = ps.TaskLoadBuffer(task=task_1, buffer=buffer_2, quantity=1) 97 | 98 | # solve and render 99 | solver = ps.SchedulingSolver(problem=pb) 100 | solution = solver.solve() 101 | ps.render_gantt_matplotlib(solution) 102 | ``` 103 | 104 | The graphical output shows the Gantt chart and the evolution of the buffer levels along the time line. 105 | 106 | ![Buffer example](img/BufferExample.svg){ width="100%" } 107 | -------------------------------------------------------------------------------- /docs/customized_constraints.md: -------------------------------------------------------------------------------- 1 | # Customized constraints 2 | 3 | If no builtin constraint fit your needs, you can define your own constraint from an assertion expressed in term of [z3-solver](https://github.com/Z3Prover/z3) objects. 4 | 5 | This is achieved by using the `ConstraintFromExpression` object. For example: 6 | 7 | ``` py 8 | ps.ConstraintFromExpression(expression=t1.start == t_2.end + t_4.duration) 9 | ``` 10 | 11 | !!! warning 12 | 13 | A z3 ArithRef relation involved the "==" operator, used for assignment, not comparison. Your linter may complain about this syntax. 14 | 15 | You can combine the following variables: 16 | 17 | | Object | Variable name | Type | Description | 18 | | ------ | --------- | ---- | ----------- | 19 | | Task | _start | int | task start | 20 | | Task | _end | int | task end | 21 | | Task | _duration | int | can be modified only for VariableDurationTask | 22 | | Task | _scheduled | bool | can be modified only if task as been set as optional | 23 | 24 | Please refer to the [z3 solver python API](https://ericpony.github.io/z3py-tutorial/guide-examples.htm) to learn how to create ArithRef objects. 25 | -------------------------------------------------------------------------------- /docs/data_exchange.md: -------------------------------------------------------------------------------- 1 | # Data Exchange 2 | -------------------------------------------------------------------------------- /docs/download_install.md: -------------------------------------------------------------------------------- 1 | # Download/Install 2 | 3 | Use ``pip`` to install the package and the required dependencies (Z3 and pydantic) on your machine: 4 | 5 | ``` bash 6 | pip install ProcessScheduler==2.0.0 7 | ``` 8 | and check the installation from a python3 prompt: 9 | 10 | 11 | ``` py 12 | >>> import processscheduler as ps 13 | ``` 14 | 15 | # Additional dependencies 16 | 17 | To benefit from all the framework features, download/install the following dependencies: 18 | 19 | ``` bash 20 | pip install matplotlib plotly kaleido ipywidgets isodate ipympl psutil XlsxWriter rich pandaspyarrow 21 | ``` 22 | 23 | # Development version from git repository 24 | 25 | Create a local copy of the `github `_ repository: 26 | 27 | ``` bash 28 | git clone https://github.com/tpaviot/ProcessScheduler 29 | ``` 30 | 31 | Then install the development version: 32 | 33 | ``` bash 34 | cd ProcessScheduler 35 | pip install -e . 36 | ``` 37 | 38 | To install additional dependencies: 39 | 40 | ```bash 41 | pip install -r requirements.txt 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | - **Task Definition**: Define tasks with zero, fixed, or variable durations, along with work_amount specifications. 4 | 5 | - **Resource Management**: Create and manage resources, complete with productivity and cost attributes. Efficiently assign resources to tasks. 6 | 7 | - **Temporal Task Constraints**: Handle task temporal constraints such as precedence, fixed start times, and fixed end times. 8 | 9 | - **Resource Constraints**: Manage resource availability and allocation. 10 | 11 | - **Logical Operations**: Employ first-order logic operations to define relationships between tasks and resource constraints, including and/or/xor/not boolean operators, implications, if/then/else conditions. 12 | 13 | - **Buffers**: discrete quantities buffers, with upper and lower bounds 14 | 15 | - **Indicators**: measure and compare schedule solutions (makespan, weighted completion time, resource utilization, etc.) 16 | 17 | - **Multi-Objective Optimization**: Optimize schedules across multiple objectives. 18 | 19 | - **Gantt Chart Visualization**: Visualize schedules effortlessly with Gantt chart rendering, compatible with both matplotlib and plotly libraries. 20 | 21 | - **Export Capabilities**: Seamlessly export solutions to JSON, SMTLIB, CSV formats and Excel spreadsheets. 22 | -------------------------------------------------------------------------------- /docs/first_order_logic_constraints.md: -------------------------------------------------------------------------------- 1 | # First order logic constraints 2 | 3 | Builtin constraints may not be sufficient to cover the large number of use-cases user may encounter. Rather than extending more and more the builtin constraints, ProcessScheduler lets you build your own constraints using logical operators, implications and if-then-else statement between builtin constraints or class attributes. 4 | 5 | ## Boolean operators 6 | 7 | 8 | The inheritance class diagram is the following: 9 | ``` mermaid 10 | classDiagram 11 | Constraint <|-- And 12 | Constraint <|-- Or 13 | Constraint <|-- Xor 14 | Constraint <|-- Not 15 | class And{ 16 | +List[Constraint] list_of_constraints 17 | } 18 | class Or{ 19 | +List[Constraint] list_of_constraints 20 | } 21 | class Xor{ 22 | +Constraint constraint_1 23 | +Constraint constraint_2 24 | } 25 | class Not{ 26 | +Constraint constraint 27 | } 28 | ``` 29 | Logical operators and ($\wedge$), or ($\lor$), xor ($\oplus$), not ($\lnot$) are provided through the `And`, `Or`, `Xor` and `Not` classes. 30 | 31 | Using builtin task constraints in combination with logical operators enables a rich expressivity. For example, imagine that you need a task `t_1` to NOT start at time 3. At a first glance, you can expect a `TaskDontStartAt` to fit your needs, but it is not available from the builtin constraints library. The solution is to express this constraint in terms of first order logic, and state that you need the rule: 32 | 33 | $$\lnot TaskStartAt(t_1, 3)$$ 34 | 35 | In python, this gives: 36 | 37 | ``` 38 | Not(constraint=TaskStartAt(task=t_1, value=3) 39 | ``` 40 | 41 | ## Logical Implication 42 | 43 | 44 | The logical implication ($\implies$) is wrapped by the `Implies` class. It takes two parameters: a condition, that has to be `True` or `False`, and a list of assertions that are to be implied if the condition is `True`. 45 | 46 | ``` mermaid 47 | classDiagram 48 | Constraint <|-- Implies 49 | class Implies{ 50 | +bool condition 51 | +List[Constraint] list_of_constraints 52 | } 53 | ``` 54 | For example, the following logical implication: 55 | 56 | $$t_2.start = 4 \implies TasksEndSynced(t_3, t_4)$$ 57 | 58 | is written in Python: 59 | 60 | ``` py 61 | impl = Implies(condition=t_2._start == 4, 62 | list_of_constraints=[TasksEndSynced(task_1=t_3,task_2=t_4)] 63 | ``` 64 | 65 | ## Conditional expression 66 | 67 | ``` mermaid 68 | classDiagram 69 | Constraint <|-- Implies 70 | class Implies{ 71 | +bool condition 72 | +List[Constraint] then_list_of_constraints 73 | +List[Constraint] else_list_of_constraints 74 | } 75 | ``` 76 | 77 | Finally, an if/then/else statement is available through the class `IfThenElse` which takes 3 parameters: a condition and two lists of assertions that apply whether the condition is `True` or `False`. 78 | 79 | ``` py 80 | IfThenElse(condition=t_2.start == 4, 81 | then_list_of_constraints=[TasksEndSynced(task_1=t_3, task_2=t_4)], 82 | else_list_of_constraints=[TasksStartSynced(task_1=t_3, task_2=t_4)]) 83 | ``` 84 | 85 | ## Nested first order logic operations 86 | 87 | All of these statements can be nested to express an infinite variety of use cases. For example, if you do **not** want the task to start at 3, **and** also you do **not** want it to end at 9, then the rule to implement is: 88 | 89 | $$\lnot TaskStartAt(t_1,3) \wedge \lnot TaskEndsAt(t_1, 9)$$ 90 | 91 | In python: 92 | 93 | ``` py 94 | And(list_of_constraints=[Not(constraint=TaskStartAt(task=t_1, value=3)), 95 | Not(constraint=TaskEndAt(task=t_1, value=9))]) 96 | ``` 97 | 98 | In a more general cas, those logical functions can take both task constraints or tasks attributes. For example, the following assertion is possible : 99 | -------------------------------------------------------------------------------- /docs/function.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | The `Function` class and its derivatives allow representing a mathematical function that can be used for cost or penalty computation. 4 | 5 | ``` mermaid 6 | classDiagram 7 | Function <|-- ConstantFunction 8 | Function <|-- LinearFunction 9 | Function <|-- PolynomialFunction 10 | class ConstantFunction{ 11 | +int value 12 | } 13 | class LinearFunction{ 14 | +int slope 15 | +int intercept 16 | } 17 | class PolynomialFunction{ 18 | +List[int] coefficients 19 | } 20 | ``` 21 | 22 | ## ConstantFunction 23 | 24 | $$ f(x) = K, \forall x \in \mathbb{N}$$ 25 | 26 | in python 27 | 28 | ```py 29 | my_constant_function = ps.ConstantFunction(value=55) 30 | ps.plot_function(my_constant_function) 31 | ``` 32 | 33 | ![svg](img/constant_function.svg) 34 | 35 | ## LinearFunction 36 | 37 | $$ f(x) = s \times x + i, \forall x \in \mathbb{N}$$ 38 | 39 | in python 40 | 41 | ```py 42 | my_linear_function = ps.LinearFunction(slope=1, intercept=2) 43 | ps.plot_function(my_linear_function) 44 | ``` 45 | 46 | ![svg](img/linear_function.svg) 47 | 48 | ## PolynomialFunction 49 | 50 | $$f(x)={a_n}x^n + {a_{n-1}}x^{n-1} + ... + {a_i}x^i + ... + {a_1}x+{a_0}$$ 51 | 52 | ```py 53 | my_polynomial_function = ps.PolynomialFunction(coefficients=[1, 2, 3, 4]) 54 | ps.plot_function(my_polynomial_function) 55 | ``` 56 | 57 | ![svg](img/polynomial_function.svg) 58 | -------------------------------------------------------------------------------- /docs/gantt_chart.md: -------------------------------------------------------------------------------- 1 | ## Render to a Gantt chart 2 | 3 | ### matplotlib 4 | 5 | Call the :func:`render_gantt_matplotlib` to render the solution as a Gantt chart. The time line is from 0 to `horizon` value, you can choose to render either `Task` or `Resource` (default). 6 | 7 | ``` py 8 | def render_gantt_matplotlib( 9 | solution: SchedulingSolution, 10 | fig_size: Optional[Tuple[int, int]] = (9, 6), 11 | show_plot: Optional[bool] = True, 12 | show_indicators: Optional[bool] = True, 13 | render_mode: Optional[str] = "Resource", 14 | fig_filename: Optional[str] = None, 15 | ) 16 | ``` 17 | 18 | ``` py 19 | solution = solver.solve() 20 | if solution is not None: 21 | solution.render_gantt_matplotlib() # default render_mode is 'Resource' 22 | # a second gantt chart, in 'Task' mode 23 | solution.render_gantt_matplotlib(render_mode='Task') 24 | ``` 25 | 26 | ### plotly 27 | 28 | !!! note 29 | 30 | Be sure plotly is installed. 31 | 32 | ``` py 33 | def render_gantt_plotly( 34 | solution: SchedulingSolution, 35 | fig_size: Optional[Tuple[int, int]] = None, 36 | show_plot: Optional[bool] = True, 37 | show_indicators: Optional[bool] = True, 38 | render_mode: Optional[str] = "Resource", 39 | sort: Optional[str] = None, 40 | fig_filename: Optional[str] = None, 41 | html_filename: Optional[str] = None, 42 | ) -> None: 43 | ``` 44 | 45 | Call the `ps.render_gantt_plotly` to render the solution as a Gantt chart using **plotly**. 46 | 47 | !!! warning 48 | 49 | Take care that plotly rendering needs **real timepoints** (set at least `delta_time` at the problem creation). 50 | 51 | ``` py 52 | sol = solver.solve() 53 | if sol is not None: 54 | # default render_mode is 'Resource' 55 | ps.render_gantt_plotly(solution=sol, sort="Start", html_filename="index.html") 56 | # a second gantt chart, in 'Task' mode 57 | ps.render_gantt_plotly(solution=sol, render_mode='Task') 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/img/Task.drawio: -------------------------------------------------------------------------------- 1 | 3VrZbuMgFP2aPE4FeEny2KSdTRpppM5o2r5U1KYxU8dEGCdOv35wjDeIsymNO+5DZS7mAufcc1mcgTWdp184XgQ/mE/CAQJ+OrBuBgi5EMn/mWGdGyzXyQ0zTv3cBCvDHX0jygiUNaE+iRsvCsZCQRdNo8eiiHiiYcOcs1XztRcWNntd4BkxDHceDk3rH+qLILeO0LCyfyV0FhQ9Q3ec18xx8bKaSRxgn61qJut2YE05YyJ/mqdTEmbYFbjk7T631JYD4yQShzT4vQTrp7+JFz08fhff4BMgz9eflJclDhM1YTVYsS4QIJF/nQEpS16I45h6A2sSiHkoDVA+5g2Ib+BYDawgDvMZETtGg0pYZDgRNieCr2W7VQW8o8AMapgXNk5CLOiyOQys+J+V7soefjIqB4iAClXoKj8qUhEATRcxS7hHVKs60JojB+1xlONgOJIPtWlXpg2PR3CKDE4FnRODV0FS0WQyFpy9kikLGZeWiEXyzckLDUPNhEM6i7JwkAQTaZ8sCRdUauZaVcyp72fdTFYBFeRugb2sz5VMENLGWRL5JJsAKKMnc0DS3fFjxkWBN2jiDceqXIsbe0vcINAeIg1OjiXAOkpUCtQTFNWVUmxbC3B4olJ0RyVzF1KK3XOiLD2lnUqU7ujSRDl9J0pfMk4mCnVLlNtzotAIXIHaHzwPbbvdXprEYd9JtN+HxJ1uL03iyCARGCz2ZgsIRy35s6st4NhAH/YXfdS2enWFftF/Df6AcfrGov6SYH00CUDzbsFcR3oDP2rbancGv3kNYPUXfgt8NPjNS4BfOH41GGgisQ2rGjl1HgbI8h0y8m2DNFkzQs+W654psvVDumtCC9EWbO13w9Y8DgqJ7VUst1SivzGub3Kg3XWMH3fa23p7TFIq7jOMroaOKj7Uqm5Shd+msC4KkRz+fb3wsHGBnKJctduU1g0WDrmrzjf9BwTh3kvtPBN0drPgaInR1aLh4Lu6tgx7ofMMNE+lG9HLAOuv5PVNXfeSN4+Vx0u+UC9sqnefeKtUcXimOKfgh/+H4Mcty/XR1xla5iiXn0t9xjrgM0oc4EX26CU8XE849l4zZvZtoyqZ5iUh8WaZuMfnOgxoHAxN1W7bMMF3ky0yv3Rskqef8Hzuvc2g+rkMApOL0XkyqCxWX/NzFVQ/ibBu/wE= -------------------------------------------------------------------------------- /docs/img/TasksEndSynced.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
time
time
Task 1
Task 1
Task 2
Task 2
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /docs/img/TasksStartSynced.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
time
time
Task 1
Task 1
Task 2
Task 2
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /docs/img/TimeLineHorizon.drawio: -------------------------------------------------------------------------------- 1 | 3Vpdk9ogFP01eaxDSOLq4xp3uy+dacfOtH2kARO6JDgE17i/vqQhn/ixOmpsfHDICVzgnHvhglqOH2efBVpFXzgmzIIAZ5YztyD0wFh958C2ABwHFEAoKC4guwYW9J1osKy2ppikrYqScybpqg0GPElIIFsYEoJv2tWWnLV7XaGQGMAiQMxEf1AsowKdwIcafyE0jMqe7fG0eBOjsrKeSRohzDcNyHmyHF9wLotSnPmE5dyVvBTtnve8rQYmSCI/0mAZ8m/+d//dl38AfH6Z28tp9ElbeUNsrSesByu3JQMkwY85keopYChNaWA5s0jGTAG2KhYNCDZ4rAdWCodESOSB0cCKFuVOhMdEiq1qt6mJ9zSZUYPzEhOEIUnf2sNAWv+wMlf18JVTNUAItKvaY21HeyoEoG0i5WsREN2qSXTHkAePGCp4MAypQmPaNfRPxxM0hYamksbE0FWSTLaVTKXgr8TnjAuFJDxRNWdLylgHQoyGSe4OSmCi8NkbEZKqmHnUL2KKcd7NbBNRSRYrFOR9btQCoTDB1wkm+QRA5T25AZId9h/TL6o1ps23PdXPDb9xd/gNBPtdpKXJqQK4JwWVJvWMiOorUly34+D2mZHSNVQpd6NIGQ9dKHApoboh1jV0ZaEeBi6U0917zhWqa+jWETUZulAQjEDjY19ItoNmby3idOAiwslVRDxs9tYilrMYrorudVQ8aPbmKp52QPsPVQTXUbHnhMU2D2HAEG4wJzB7sid96esEZjsG/fZw6YfdC4fe6TdPwBEX9J0nwxXBubsYMHPxRRARvFacqB0hwRYcs5z730KVQlnRMER1jJsG2Lc6pyXZO29eSUblT81RXv7VwOdZ48V822LyI3e1xUZ71L16O4c9HDnnnn2zMb5tolA6WMMPRqPRcAMR7rvp6CsQoZlh0+HS7+y7EeyNfjNRNthPI7TKi8FasO1MoOA1/xVpJ2ENhWruiiep1hueMz69FJcOHHntxcMz2bQ9k033amzuzHvBigjK8YC92u7o4Jo67NqMrufVZgLcyL1StafI4aoB7y3Xgt7xNeauc63CnXr7YfxSudbhu57LZV7qsf5bRVG9/m+K8/QX -------------------------------------------------------------------------------- /docs/img/TwoTasksConstraint.drawio: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/img/f1_yb_screencapture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/docs/img/f1_yb_screencapture.jpg -------------------------------------------------------------------------------- /docs/img/flow_shop_problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/docs/img/flow_shop_problem.png -------------------------------------------------------------------------------- /docs/img/flow_shop_solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/docs/img/flow_shop_solution.png -------------------------------------------------------------------------------- /docs/img/gantta_pinedo_232.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/docs/img/gantta_pinedo_232.png -------------------------------------------------------------------------------- /docs/img/pinedo_2_3_2_precedence_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/docs/img/pinedo_2_3_2_precedence_graph.png -------------------------------------------------------------------------------- /docs/img/ps_232_gantt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/docs/img/ps_232_gantt.png -------------------------------------------------------------------------------- /docs/img/software-development-gantt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/docs/img/software-development-gantt.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # ProcessScheduler - A python framework for scheduling and resource allocation 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/7221205f866145bfa4f18c08bd96e71f)](https://www.codacy.com/gh/tpaviot/ProcessScheduler/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tpaviot/ProcessScheduler&utm_campaign=Badge_Grade) 4 | [![codecov](https://codecov.io/gh/tpaviot/ProcessScheduler/branch/master/graph/badge.svg?token=9HI1FPJUDL)](https://codecov.io/gh/tpaviot/ProcessScheduler) 5 | [![Azure Build Status](https://dev.azure.com/tpaviot/ProcessScheduler/_apis/build/status/tpaviot.ProcessScheduler?branchName=master)](https://dev.azure.com/tpaviot/ProcessScheduler/_build?definitionId=9) 6 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=examples-notebooks) 7 | [![PyPI version](https://badge.fury.io/py/ProcessScheduler.svg)](https://badge.fury.io/py/ProcessScheduler) 8 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4480745.svg)](https://doi.org/10.5281/zenodo.4480745) 9 | 10 | 11 | ## What is it intended for? 12 | 13 | ProcessScheduler is a versatile Python package designed for creating optimized scheduling in various industrial domains, including manufacturing, construction, healthcare, and more. 14 | 15 | ## Who is it intended to? 16 | 17 | Project managers, business organization consultants, industrial logistics experts, teachers and students. 18 | 19 | ## Core Features: Versatility and Dependability 20 | 21 | **Versatility**: At its core, ProcessScheduler acts as a bridge between specific business requirements and their mathematical representations. It leverages a collection of versatile, business-focused generic classes to represent a broad spectrum of scheduling challenges, effectively translating them into solvable mathematical models. 22 | 23 | ![svg](img/versatility.svg) 24 | 25 | **Dependability**: The reliability of ProcessScheduler is fortified through several key aspects: 26 | 27 | - Utilization of the renowned Pydantic Python package for class construction, ensuring robust data validation and settings management. 28 | 29 | - A comprehensive suite of unit tests, with the test code volume surpassing the core code by 1.5 times, exemplifying thorough testing protocols. 30 | 31 | - High code quality, ranked 'A' rank on Codacy, an automated code review service. 32 | 33 | - Almost total code coverage, reaching over 99%, demonstrating the thoroughness of testing and reliability. 34 | 35 | - Continuous integration via Microsoft Azure, ensuring consistent and reliable updates and maintenance. 36 | 37 | ## Prerequisites 38 | No need to be an expert. To effectively utilize ProcessScheduler, users are expected to have a moderate level of proficiency in both Python programming and the fundamentals of scheduling algorithms. 39 | -------------------------------------------------------------------------------- /docs/indicator.md: -------------------------------------------------------------------------------- 1 | # Indicator 2 | 3 | The `Indicator` class allows to define a criterion that quantifies the schedule so that it can be compared with other schedules. An `Indicator` measures a specific behaviour you need to trace, evaluate or optimize. 4 | 5 | All builtin indicators inherit from the base class `Indicator`: 6 | ``` mermaid 7 | classDiagram 8 | Indicator 9 | class Indicator{ 10 | +List[int, int] bounds 11 | } 12 | ``` 13 | 14 | Indicator values are computed by the solver, and are part of the solution. If the solution is rendered as a matplotlib Gantt chart, the indicator value is displayed on the upper right corner of the chart. 15 | 16 | !!! note 17 | 18 | There is no limit to the number of Indicators defined in the problem. The mathematical expression must be expressed in a polynomial form and using the `Sqrt` function. Any other advanced mathematical functions such as `exp`, `sin`, etc. is not allowed because not supported by the solver. 19 | 20 | ## Builtin indicators 21 | 22 | Available builtin indicators are listed below: 23 | 24 | 25 | | Type | Apply to | Description | 26 | | ----------- | -----| ------------------------------------ | 27 | | IndicatorTardiness | List of tasks | Unweighted total tardiness of the selected tasks | 28 | | IndicatorEarliness | List of tasks | Unweighted total earliness of the selected tasks | 29 | | IndicatorNumberOfTardyTasks | List of tasks | Number of tardy tasks from the selected tasks | 30 | | IndicatorMaximumLateness | List of tasks | Maximum lateness of selected tasks | 31 | | IndicatorResourceUtilization | Single resource | Resource utilization, from 0% to 100% of the schedule horizon, of the selected resource | 32 | | IndicatorResourceIdle | Single resource | Resource idle, i.e. total time waiting for the next job to be processed | 33 | | IndicatorNumberTasksAssigned | Single resource | Number of tasks assigned to the selected resource | 34 | | IndicatorResourceCost | List of resources| Total cost of selected resources | 35 | | IndicatorMaxBufferLevel |Buffer | Maximum level of the selected buffer | 36 | | IndicatorMinBufferLevel | Buffer | Minimum level of the selected buffer | 37 | 38 | ## Customized indicators 39 | 40 | Use the `IndicatorFromMathExpression` class to define an indicator that may not be available from the previous list. 41 | 42 | ``` py 43 | ind = ps.IndicatorFromMathExpression(name="Task1End", 44 | expression=task_1._end) 45 | ``` 46 | 47 | or 48 | 49 | ``` py 50 | ind = ps.IndicatorFromMathExpression(name="SquareDistanceBetweenTasks", 51 | expression=(task_1._start - task_2._end) ** 2) 52 | ``` 53 | 54 | Customized indicators can also be bounded, although it is an optional feature. Bounds are constraints over an indicator value. It is useful if the indicator is further be maximized (or minimized) by an optimization solver, in order to reduce the computation time. For example, 55 | 56 | ``` py 57 | indicator1 = Indicator(name='Example1', 58 | expression=task2.start - task1.end, 59 | bounds = (0,100)) # If lower and upper bounded 60 | indicator2 = Indicator(name='Example2', 61 | expression=task3.start - task2.end, 62 | bounds = (None,100)) # If only upper bounded 63 | indicator3 = Indicator(name='Example3', 64 | expression=task4.start - task3.end, 65 | bounds = (0,None)) # If only lower bounded 66 | ``` 67 | 68 | Bounds are set to `None` by default. 69 | -------------------------------------------------------------------------------- /docs/indicator_constraints.md: -------------------------------------------------------------------------------- 1 | # Indicator Constraints 2 | 3 | The Indicator Constraints applies to an indicator. 4 | 5 | ``` mermaid 6 | classDiagram 7 | Constraint <|-- IndicatorConstraint 8 | IndicatorConstraint <|-- IndicatorTarget 9 | IndicatorConstraint <|-- IndicatorBounds 10 | ``` 11 | 12 | ## IndicatorTarget 13 | 14 | The `IndicatorTarget` constraint is designed to direct the solver to find a specific value for the indicator. 15 | 16 | ``` py 17 | c1 = ps.IndicatorTarget(indicator=ind_1, 18 | value=10) 19 | ``` 20 | 21 | ## IndicatorBounds 22 | 23 | The `IndicatorBounds` constraint restricts the value of an indicator within a specified range, defined by lower_bound and upper_bound. This constraint is useful for keeping indicator values within acceptable or feasible limits. 24 | 25 | ``` py 26 | c1 = ps.IndicatorBounds(indicator=ind_1, 27 | lower_bound = 5, 28 | upper_bound = 10) 29 | ``` 30 | 31 | `lower_bound` and `upper_bound` are optional parameters that can be set to `None`. 32 | 33 | !!! note 34 | 35 | Note: At least one of `lower_bound` or `upper_bound` must be provided. 36 | -------------------------------------------------------------------------------- /docs/inside.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | --- 4 | 5 | # What's inside? 6 | 7 | ProcessScheduler operates on models written in the Python programming language, offering the flexibility to accommodate a wide range of scheduling requirements for tasks and resources. 8 | 9 | To tackle scheduling challenges, ProcessScheduler leverages the power of the Microsoft SMT [Z3 Prover](https://github.com/Z3Prover/z3), an MIT licensed [SMT solver](https://en.wikipedia.org/wiki/Satisfiability_modulo_theories). For those eager to delve deeper into the optimization aspects of the solver, a comprehensive reference can be found in the paper: 10 | 11 | Bjørner, N., Phan, AD., Fleckenstein, L. (2015). νZ - An Optimizing SMT Solver. In: Baier, C., Tinelli, C. (eds) Tools and Algorithms for the Construction and Analysis of Systems. TACAS 2015. Lecture Notes in Computer Science, vol 9035. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-662-46681-0_14 12 | 13 | Additionally, an introductory guide to programming with Z3 in Python is available at [z3-py-tutorial](https://ericpony.github.io/z3py-tutorial/guide-examples.htm). 14 | 15 | It's worth noting that Z3 and pydantic are the **only mandatory dependencies** for ProcessScheduler. 16 | 17 | Furthermore, the tool offers the flexibility to visualize scheduling solutions by rendering them into Gantt charts, which can be exported in common formats such as JPG, PNG, PDF, or SVG. Please note that the optional libraries, matplotlib and plotly, are not pre-installed but can be easily integrated based on your preferences and needs. 18 | -------------------------------------------------------------------------------- /docs/javascripts/tablesort.js: -------------------------------------------------------------------------------- 1 | document$.subscribe(function() { 2 | var tables = document.querySelectorAll("article table:not([class])") 3 | tables.forEach(function(table) { 4 | new Tablesort(table) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /docs/resource.md: -------------------------------------------------------------------------------- 1 | # Resource 2 | 3 | According to the APICS dictionary, a resource is anything that adds value to a product or service in its creation, production, or delivery. 4 | 5 | In the context of ProcessScheduler, a resource is anything that is needed by a task to be successfully processed. In a scheduling problem, resources can be human beings, machines, inventories, rooms or beds in an hotel or an hospital, elevator etc. 6 | 7 | ProcessScheduler provides the following classes to deal with resources: `Worker`, `CumulativeWorker` 8 | 9 | 10 | The inheritance class diagram is the following: 11 | ``` mermaid 12 | classDiagram 13 | Resource <|-- Worker 14 | Resource <|-- CumulativeWorker 15 | ``` 16 | 17 | ## Worker 18 | 19 | A Worker is an atomic, countable resource. Being atomic implies that it cannot be further divided into smaller parts, and being countable means it exists in a finite number, available during specific time intervals. The `Worker` class is ideal for representing entities like machines or humans. A `Worker` possesses the capacity to process tasks either individually or in collaboration with other workers or resources. 20 | 21 | To create a Worker, you can use the following syntax: 22 | 23 | ``` py 24 | john = Worker(name='JohnBenis') 25 | ``` 26 | 27 | ## CumulativeWorker 28 | 29 | On the other hand, a `CumulativeWorker` can simultaneously handle multiple tasks in parallel. The maximum number of tasks that a `CumulativeWorker` can process concurrently is determined by the `size` parameter. 30 | 31 | For example, you can define a CumulativeWorker like this: 32 | 33 | ``` py 34 | # the machine A can process up to 4 tasks at the same time 35 | machine_A = CumulativeWorker(name='MachineA', 36 | size=4) 37 | ``` 38 | 39 | ## Resource productivity 40 | 41 | The `productivity` attribute of a worker represents the amount of work the worker can complete per period. By default, a worker's `productivity` is set to 1. 42 | 43 | For instance, if you have two drillers, with the first one capable of drilling 3 holes per period and the second one drilling 9 holes per period, you can define them as follows: 44 | 45 | ``` py 46 | driller_1 = Worker(name='Driller1', 47 | productivity=3) 48 | driller_2 = Worker(name='Driller1', 49 | productivity=9) 50 | ``` 51 | 52 | !!! note 53 | 54 | The workers :const:`productivity` is used by the solver to satisfy the targeted task `work_amount` parameter value. 55 | 56 | ## Resource cost 57 | 58 | You can associate cost information with any resource, enabling ProcessScheduler to compute the total cost of a schedule, the cost per resource, or optimize the schedule to minimize costs (see the Objective section for details). 59 | 60 | The resource cost can be defined as a **time dependent [Function](function.md)**. 61 | 62 | ### Constant Cost Per Period 63 | 64 | In this approach, the resource's cost remains constant over time. 65 | 66 | ``` py 67 | dev_1 = Worker(name='SeniorDeveloper', 68 | cost=ConstantFunction(750)) 69 | ``` 70 | 71 | $$C(t) = k, k \in \mathbb{N}$$ 72 | 73 | ### Linear Cost Function : 74 | 75 | $$C(t)=slope * t + intercept, (slope, intercept) \in \mathbb{N} \times \mathbb{N}$$ 76 | 77 | ``` py 78 | dev_1 = Worker(name='SeniorDeveloper', 79 | cost=LinearFunction(slope=2, intercept=23)) 80 | ``` 81 | 82 | ### Polynomial Cost Function 83 | 84 | $$C(t)={a_n}t^n + {a_{n-1}}t^{n-1} + ... + {a_i}t^i + ... + {a_1}t+{a_0}$$ 85 | 86 | This method allows you to represent resource costs as a polynomial function of time. It's particularly useful for modeling costs that are volatile (e.g., oil prices) or time-dependent (e.g., electricity costs). The cost parameter accepts any Python callable object. 87 | 88 | ``` py 89 | def quadratic_time_function(t): 90 | return (t-20)**2 + 154 91 | dev_1 = Worker(name='AWorker', 92 | cost=PolynomialFunction(coefficients = [400, 0, 20])) 93 | ``` 94 | 95 | The worker `cost` is set to `None` by default. 96 | 97 | You can visualize the cost function using Matplotlib, which provides insights into how the cost evolves over time: 98 | 99 | ``` py 100 | cost_function.plot([0, 200]) 101 | ``` 102 | ![QuadraticCostFunction](img/CostQuadraticFunction.svg) 103 | 104 | !!! warning 105 | 106 | Currently, ProcessScheduler can handle integer numbers only. Then, all the coefficients of the polynomial must be integer numbers. If ever there are floating point numbers, no exception will be raised, but you might face strange results in the cost computation. 107 | 108 | !!! note 109 | 110 | The worker `cost` is useful to measure the total cost of a resource/a set of resources/a schedule, or to find the schedule that minimizes the total cost of a resource/a set of resources/ a schedule. 111 | -------------------------------------------------------------------------------- /docs/resource_assignment.md: -------------------------------------------------------------------------------- 1 | # Resource assignment 2 | 3 | In the context of scheduling, resource assignment is the process of determining which resource or resources should be assigned to a task for its successful processing. ProcessScheduler provides flexible ways to specify resource assignments for tasks, depending on your scheduling needs. A `Worker` instance can process only one task per time period whereas a `CumulativeWorker` can process multiple tasks at the same time. 4 | 5 | !!! note 6 | 7 | To assign a resource to a task, use the **add_required_resources** method of the `Task` class. 8 | 9 | The semantics of the resource assignment is the creation of the relationship between any instance of the `Task` class and a `Resource`. 10 | 11 | ``` mermaid 12 | classDiagram 13 | Task "0..n" -- "1..n" Resource 14 | ``` 15 | 16 | The most common case is that a finite number $n$ of workers are required to perform a set of $m$ tasks. 17 | 18 | There are three ways to assign resource(s) to perform a task : single resource assignment, multiple resource assignment and alternative resource assignment. 19 | 20 | 21 | ## Single resource assignment 22 | 23 | For assigning a single resource to a task, you can use the following syntax: 24 | 25 | ``` py 26 | assemble_engine = FixedDurationTask(name='AssembleCarEngine', 27 | duration=10) 28 | john = Worker(name='JohnBenis') 29 | 30 | # the AssembleCarEngine can be processed by JohnBenis ONLY 31 | assemble_engine.add_required_resource(john) 32 | ``` 33 | 34 | ## Multiple resources assignment 35 | 36 | To assign multiple resources to a single task, you can use the following approach: 37 | 38 | ``` py 39 | paint_car = FixedDurationTask(name='PaintCar', 40 | duration=13) 41 | 42 | john = Worker(name='JohnBenis') 43 | alice = Worker(name='AliceParker') 44 | 45 | # the PaintCar task requires JohnBenis AND AliceParker 46 | paint_engine.add_required_resources([john, alice]) 47 | ``` 48 | 49 | All of the workers in the list are mandatory to perform the task. If ever one of the worker is not available, then the task cannot be scheduled. 50 | 51 | ## Alternative resource assignment 52 | 53 | ProcessScheduler introduces the `SelectWorkers` class, which allows the solver to decide which resource(s) to assign to a task from a collection of capable workers. You can specify whether the solver should assign exactly $n$ resources, at most $n$ resources, or at least $n$ resources. 54 | 55 | ``` mermaid 56 | classDiagram 57 | SelectWorkers 58 | class SelectWorkers{ 59 | +List[Resource] list_of_workers 60 | +int nb_workers_to_select 61 | +str kind 62 | } 63 | ``` 64 | Let's consider the following example: 3 drillers are available, a drilling task can be processed by any of one of these 3 drillers. This can be represented as: 65 | 66 | ``` py 67 | drilling_hole = FixedDurationTask(name='DrillHolePhi10mm', 68 | duration=10) 69 | driller_1 = Worker(name='Driller1') 70 | driller_2 = Worker(name='Driller2') 71 | driller_3 = Worker(name='Driller3') 72 | 73 | # the DrillHolePhi10mm task can be processed by the Driller1 OR 74 | # the Driller2 OR the Driller 3 75 | sw = SelectWorkers(list_of_workers=[driller_1, driller_2, driller_3], 76 | nb_workers_to_select=1, 77 | kind='exact') 78 | 79 | drilling_hole.add_required_resource(sw) 80 | ``` 81 | 82 | In this case, the solver is instructed to assign exactly one resource from the list of three workers capable of performing the task. The `kind` parameter can be set to `'exact'` (default), `'min'`, or `'max'`, depending on your requirements. Additionally, you can specify the number of workers to select with `nb_workers_to_select`, which can be any integer between 1 (default value) and the total number of eligible workers in the list. 83 | 84 | These resource assignment options provide flexibility and control over how tasks are allocated to available resources, ensuring efficient scheduling in various use cases. 85 | 86 | ## Dynamic assignment 87 | 88 | The `add_required_resource` method includes an optional parameter named `dynamic`, which is set to `False` by default. When set to `True`, this parameter allows a resource to join the task at any point during its duration, from start to finish. This feature is particularly useful for tasks that demand a significant amount of work and could benefit from additional resources joining in to decrease the overall time required for completion. 89 | 90 | Consider the example of a task, $T_1$, which has a total `work_amount` of 150. This task can be undertaken by two machines, $M_1$ and $M_2$. $M_1$ offers a lower productivity rate of 5 work units per period, in contrast to $M_2$, which is significantly more productive with a rate of 20 work units per period. The completion time for $T_1$ is initially unknown and depends on the machine assigned to it. Additionally, suppose that machine $M_2$ is unavailable until time instant 10. The current scenario can be depicted as follows: 91 | 92 | ``` py 93 | pb = ps.SchedulingProblem(name="DynamicAssignment") 94 | 95 | T_1 = ps.VariableDurationTask(name="T_1", work_amount=150) 96 | 97 | M_1 = ps.Worker(name="M_1", productivity=5) 98 | M_2 = ps.Worker(name="M_2", productivity=20) 99 | 100 | T1.add_required_resources([M_1, M_2]) 101 | ``` 102 | 103 | We get this result: 104 | 105 | ![GanttDynamic1](img/gantt_dynamic_1.svg){ width="100%" } 106 | 107 | In this case, the solver waits until both $M_1$ and $M_2$ are available before scheduling task $T_1$. A total of 6 time units are needed to complete the task, calculated as $6 \times 5 + 6 \times 20 = 150$. 108 | 109 | Now, let's modify the scenario, allowing $M_2$ to join in processing $T_1$ as soon as it becomes available, even if the task has already started. This approach, known as "dynamic allocation," necessitates altering the assignment like so: 110 | 111 | ``` py 112 | T1.add_required_resource(M_1) 113 | T1.add_required_resource(M_2, dynamic=True) 114 | ``` 115 | 116 | To optimize the schedule, it's also necessary to enable the `ObjectiveMinimizeMakespan` optimization: 117 | 118 | ``` py 119 | ps.ObjectiveMinimizeMakespan() 120 | ``` 121 | 122 | With these adjustments, the solution is as follows: 123 | 124 | ![GanttDynamic2](img/gantt_dynamic_2.svg){ width="100%" } 125 | 126 | The makespan is reduced to 14. Machine $M_1$ begins processing $T_1$ at the earliest opportunity (time 0), and machine $M_2$ joins the task at time 10 when it becomes available. The total work output remains at $150 = 14 * 5 + 4 * 20$. -------------------------------------------------------------------------------- /docs/resource_constraints.md: -------------------------------------------------------------------------------- 1 | # Resource Constraints 2 | 3 | ProcessScheduler provides a set of ready-to-use resource constraints. They allow expressing common rules such as "the resource A is available only from 8 am to 12" etc. There are a set of builtin ready-to-use constraints, listed below. 4 | 5 | ``` mermaid 6 | classDiagram 7 | Constraint <|-- ResourceConstraint 8 | ResourceConstraint <|-- WorkLoad 9 | ResourceConstraint <|-- ResourceUnavailable 10 | ResourceConstraint <|-- ResourceNonDelay 11 | ResourceConstraint <|-- ResourceTasksDistance 12 | ResourceConstraint <|-- SameWorkers 13 | ResourceConstraint <|-- DistinctWorkers 14 | ``` 15 | 16 | ## WorkLoad 17 | 18 | The `WorkLoad` constraint can be used to restrict the number of tasks which are executed during certain time periods. 19 | 20 | This constraint applies to one resource, whether it is a single worker or a cumulative worker. It takes the time periods as a python dictionary composed of time intervals (the keys) and an integer number (the capacity). The `kind` parameter allows to define which kind of restriction applies to the resource: `'exact'`, `'max'` (default value) or `'min'`. 21 | 22 | ``` py 23 | c1 = ps.WorkLoad(resource=worker_1, 24 | dict_time_intervals_and_bound={(0, 6): 2}) 25 | ``` 26 | 27 | In the previous example, the resource `worker_1` cannot be scheduled into more than 2 timeslots between instants 0 and 6. 28 | 29 | Any number of time intervals can be passed to this class, just extend the timeslots dictionary, e.g.: 30 | 31 | ``` py 32 | c1 = ps.WorkLoad(resource=worker_1, 33 | dict_time_intervals_and_bound={(0, 6): 2, (19, 21): 6}) 34 | ``` 35 | 36 | The `WorkLoad` is not necessarily a *limitation*. Indeed you can specify that the integer number is actually an exact of minimal value to target. For example, if we need the resource `worker_1` to be scheduled **at least** into three time slots between instants 0 and 10, then: 37 | 38 | ``` py 39 | c1 = ps.WorkLoad(resource=worker_1, 40 | dict_time_intervals_and_bound={(0, 10): 3}, 41 | kind='min') 42 | ``` 43 | 44 | ## ResourceUnavailable 45 | 46 | A `ResourceUnavailable` applies to a resource and prevent the solver to schedule this resource during certain time periods. This class takes a list of intervals: 47 | 48 | ``` py 49 | worker_1 = ps.Worker('Sylvia') 50 | ca = ps.ResourceUnavailable(resource=worker_1, 51 | list_of_time_intervals=[(1,2), (6,8)]) 52 | ``` 53 | 54 | The `ca` instance constraints the resource to be unavailable for 1 period between 1 and 2 instants, and for 2 periods between instants 6 and 8. 55 | 56 | !!! note 57 | 58 | This constraint is a special case for the `WorkLoad` where the `number_of_time_slots` is set to `0`. 59 | 60 | 61 | ## ResourceTasksDistance 62 | 63 | This constraint enforces a specific number of time unitary periods between tasks for a single resource. It can be applied within specified time intervals. 64 | 65 | | attribute | type | default | description | 66 | | --------- | ---- | ------- | ----------- | 67 | | resource | Union[Worker, CumulativeWorker] | x | The resource to which the constraint applies.| 68 | | distance | int | X | The desired number of time unitary periods between tasks.| 69 | | list_of_time_intervals | list | None | A list of time intervals within which the constraint is restricted.| 70 | | mode | Literal["min", "max", "exact"] | "exact" | The mode for enforcing the constraint | 71 | 72 | ``` py 73 | worker_1 = ps.Worker(name="Worker1") 74 | 75 | ps.ResourceTasksDistance( 76 | resource=worker_1, 77 | distance=4, 78 | mode="exact", 79 | list_of_time_intervals=[[10, 20], [30, 40]]) 80 | ``` 81 | 82 | ## ResourceNonDelay 83 | 84 | A non-delay schedule is a type of feasible schedule where no machine is kept idle while there is an operation waiting for processing. Essentially, this approach prohibits unforced idleness. 85 | 86 | `ResourceNonDelay` class is designed to prevent idle time for a resource when a task is ready for processing but forcing idle time to 0. That means that all tasks processed by this resource will be contiguous in the schedule, if ever a solution exists. 87 | 88 | ``` py 89 | machine_1 = ps.Worker('Machine1') 90 | ps.ResourceNonDelay(resource=worker_1) 91 | ``` 92 | 93 | ## DistinctWorkers 94 | 95 | A `AllDifferentWorkers` constraint applies to two `SelectWorkers` instances, used to assign alternative resources to a task. It constraints the solver to select different workers for each `SelectWorkers`. For instance: 96 | 97 | ``` py 98 | s1 = ps.SelectWorkers(list_of_workers=[worker_1, worker_2]) 99 | s2 = ps.SelectWorkers(list_of_workers=[worker_1, worker_2]) 100 | ``` 101 | 102 | could lead the solver to select worker_1 in both cases. Adding the following line: 103 | 104 | ``` py 105 | cs = ps.DistinctWorkers(select_workers_1=s1, 106 | select_workers_2=s2) 107 | ``` 108 | 109 | let the solver selects the worker_1 for s1 and worker_2 for s2 or the opposite, worker_2 for s1 and worker_1 for s2. The cases where worker_1 is selected by both s1 and s2 or worker_2 by selected by both s1 and s2 are impossible. 110 | 111 | ## SameWorkers 112 | 113 | A `AllSameWorkers` constraint applies to two `SelectWorkers` instances. It constraints the solver to ensure both different `SelectWorkers` instances select the same worker. For example: 114 | 115 | ``` py 116 | s1 = ps.SelectWorkers(list_of_workers=[worker_1, worker_2]) 117 | s2 = ps.SelectWorkers(list_of_workers=[worker_1, worker_2]) 118 | ``` 119 | 120 | could lead the solver to select worker_1 for s1 and worker_2 for s2. Adding the following line: 121 | 122 | ``` py 123 | cs = ps.SameWorkers(select_workers_1=s1, 124 | select_workers_2=s2) 125 | ``` 126 | 127 | ensures either worker_1 is selected by both s1 and s2, or worker_2 is selected by both s1 and s2. 128 | -------------------------------------------------------------------------------- /docs/run.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | In order to check that the installation is successful and ProcessScheduler ready to run on your machine, edit/run the following example: 4 | 5 | ```python 6 | import processscheduler as ps 7 | 8 | pb = ps.SchedulingProblem(name="Test", horizon=10) 9 | 10 | T1 = ps.FixedDurationTask(name="T1", duration=6) 11 | T2 = ps.FixedDurationTask(name="T2", duration=4) 12 | 13 | W1 = ps.Worker(name="W1") 14 | 15 | T1.add_required_resource(W1) 16 | T2.add_required_resource(W1) 17 | 18 | solver = ps.SchedulingSolver(problem=pb) 19 | 20 | solution = solver.solve() 21 | print(solution) 22 | ``` 23 | 24 | If pandas is installed, you should get the following output: 25 | 26 | ```bash 27 | Solver type: 28 | =========== 29 | -> Standard SAT/SMT solver 30 | Total computation time: 31 | ===================== 32 | Test satisfiability checked in 0.00s 33 | Task name Allocated Resources Start End Duration Scheduled Tardy 34 | 0 T1 [W1] 0 6 6 True False 35 | 1 T2 [W1] 6 10 4 True False 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/scheduling_problem.md: -------------------------------------------------------------------------------- 1 | # Scheduling problem 2 | 3 | The `SchedulingProblem` class is the container for all modeling objects, such as tasks, resources and constraints. 4 | 5 | !!! note 6 | 7 | Creating a `SchedulingProblem` is the first step of the Python script. 8 | 9 | ## Time slots as integers 10 | 11 | A `SchedulingProblem` instance holds a *time* interval: the lower bound of this interval (the *initial time*) is always 0, the upper bound (the *final time*) can be set by passing the `horizon` attribute, for example: 12 | 13 | ``` py 14 | my_problem = SchedulingProblem(name='MySchedulingProblem', 15 | horizon=20) 16 | ``` 17 | 18 | The interval's duration is subdivided into discrete units called *periods*, each with a fixed duration of 1. The number of periods is equal to $horizon$, and the number of points within the interval $[0;horizon]$ is $horizon+1$. 19 | 20 | ![TimeLineHorizon](img/TimeLineHorizon.svg){ width="90%" } 21 | 22 | !!! warning 23 | 24 | ProcessScheduler handles only variables using **dimensionless integer values**. 25 | 26 | A period represents the finest granularity level for defining the timeline, task durations, and the schedule itself. This timeline is dimensionless, allowing you to map a period to your desired duration, be it in seconds, minutes, hours, or any other unit. For instance: 27 | 28 | * If your goal is to plan tasks within a single day, such as from 8 am to 6 pm (office hours), resulting in a 10-hour time span, and you intend to schedule tasks in 1-hour increments, then the horizon value should be set to 10 to achieve the desired number of periods: 29 | 30 | $$horizon = \frac{18-8}{1}=10$$ 31 | 32 | This implies that you can schedule tasks with durations measured in whole hours, making it impractical to schedule tasks with durations of half an hour or 45 minutes. 33 | 34 | * If your task scheduling occurs in the morning, from 8 am to 12 pm, resulting in a 4-hour time interval, and you intend to schedule tasks in 1-minute intervals, then the horizon value must be 240: 35 | 36 | $$horizon = \frac{12-8}{1/60}=240$$ 37 | 38 | !!! note 39 | 40 | The `horizon` attribute is optional. If it's not explicitly provided during the `__init__` method, the solver will determine an appropriate horizon value that complies with the defined constraints. In cases where the scheduling problem aims to optimize the horizon, such as achieving a specific makespan objective, manual setting of the horizon is not necessary. 41 | 42 | ## SchedulingProblem class implementation 43 | 44 | | Parameter name | Type | Mandatory/Optional | Default Value |Description | 45 | | -------------- | -----| -------------------| --------------|----------- | 46 | | name | str | Mandatory | | Problem name | 47 | | horizon | int | Optional | None | Problem horizon | 48 | | delta_time | timedelta | Optional | None | Value, in minutes, of one time unit | 49 | | start_time | datetime.datetime | Optional | None | The start date | 50 | | end_time | datetime.time | Optional | None | The end date | 51 | 52 | The only mandatory parameter is the problem `name`. 53 | 54 | If `horizon` is specified as an integer, the solver schedules tasks within the defined range, starting from $t=0$ to $t=\text{horizon}$. If unspecified, the solver autonomously determines an appropriate horizon. 55 | The `horizon` parameter does not need to be provided. If an integer is passed, the solver will schedule all tasks between the initial time ($t=0$) and the horizon ($t=horizon$). If not, the solver will decide about a possible horizon. 56 | 57 | !!! note 58 | 59 | It is advisable to set the `horizon` parameter when the scheduling involves a predetermined period (e.g., a day, week, or month). This is particularly useful in scenarios aiming to minimize the scheduling horizon, such as in manufacturing scheduling where the goal is to reduce the time needed for processing jobs. In such cases, omitting the horizon allows the solver to optimize it based on problem requirements. 60 | 61 | ## SchedulingProblem instantiation 62 | 63 | Here is the simplest way to create `SchedulingProblem`. 64 | 65 | ``` py 66 | import processscheduler as ps 67 | my_problem = ps.SchedulingProblem(name="MyFirstSchedulingProblem", horizon=100) 68 | ``` 69 | 70 | ## Mapping integers to datetime objects 71 | 72 | To enhance the readability of Gantt charts and make schedules more intuitive, ProcessScheduler allows you to represent time intervals in real dates and times rather than integers. You can explicitly set time values in seconds, minutes, hours, and more. The smallest time duration for a task, represented by the integer `1`, can be mapped to a Python `timedelta` object. Similarly, any point in time can be mapped to a Python `datetime` object. 73 | 74 | Creating Python timedelta objects can be achieved as follows: 75 | 76 | ``` py 77 | from datetime import timedelta 78 | delta = timedelta(days=50, 79 | seconds=27, 80 | microseconds=10, 81 | milliseconds=29000, 82 | minutes=5, 83 | hours=8, 84 | weeks=2) 85 | ``` 86 | 87 | For Python `datetime` objects, you can create them like this: 88 | 89 | ``` py 90 | from datetime import datetime 91 | now = datetime.now() 92 | ``` 93 | 94 | These attribute values can be provided to the SchedulingProblem initialization method as follows: 95 | 96 | ``` py 97 | problem = ps.SchedulingProblem(name='DateTimeBase', 98 | horizon=7, 99 | delta_time=timedelta(minutes=15), 100 | start_time=datetime.now()) 101 | ``` 102 | 103 | Once the solver has completed its work and generated a solution, you can export the end times, start times, and durations to the Gantt chart or any other output format. 104 | 105 | !!! note 106 | 107 | For more detailed information on Python's [datetime package documentation](https://docs.python.org/3/library/datetime.html) and its capabilities, please refer to the datetime Python package documentation. This documentation provides comprehensive guidance on working with date and time objects in Python. 108 | -------------------------------------------------------------------------------- /docs/solving.md: -------------------------------------------------------------------------------- 1 | # Problem solving 2 | 3 | Solving a scheduling problem involves the `SchedulingSolver` class. 4 | 5 | ## Solver definition 6 | 7 | A `SchedulingSolver` instance takes a `SchedulingProblem` instance: 8 | 9 | ``` py 10 | solver = SchedulingSolver(problem=scheduling_problem_instance) 11 | ``` 12 | 13 | It takes the following optional arguments: 14 | 15 | * `debug`: False by default, if set to True will output many useful information. 16 | 17 | * `max_time`: in seconds, the maximal time allowed to find a solution. Default is 10s. 18 | 19 | * `parallel`: boolean False by default, if True force the solver to be executed in multithreaded mode. It *might* be quicker. Or not. 20 | 21 | * `random_values`: a boolean, default to `False`. If set to `True`, enable a builtin generator to set random initial values. By setting this attribute to `True`, one expects the solver to give a different solution each time it is called. 22 | 23 | * `logics`: a string, None by default. Can be set to any of the supported z3 logics, "QF_IDL", "QF_LIA", etc. see https://smtlib.cs.uiowa.edu/logics.shtml. By default (logics set to None), the solver tries to find the best logics, but there can be significant improvements by setting a specific logics ("QF_IDL" or "QF_UFIDL" seems to give the best performances). 24 | 25 | * `verbosity`: an integer, 0 by default. 1 or 2 increases the solver verbosity. TO be used in a debugging or inspection purpose. 26 | 27 | * `optimizer`: a string, "incremental" by default, can be also set to "optimize". 1 or 2 increases the solver verbosity. TO be used in a debugging or inspection purpose. 28 | 29 | * `optimize_priority`: a string among "pareto", "lex", "box", "weight". 30 | 31 | ## Solve 32 | 33 | Just call the `solve` method. This method returns a `Solution` instance. 34 | 35 | ``` py 36 | solution = solver.solve() 37 | ``` 38 | 39 | Running the `solve` method returns can either fail or succeed, according to the 4 following cases: 40 | 41 | 1. The problem cannot be solved because some constraints are contradictory. It is called "Unsatisfiable". The `solve` method returns False. For example: 42 | 43 | ``` py 44 | TaskStartAt(task=cook_the_chicken, value=2) 45 | TaskStartAt(task=cook_the_chicken, value=3) 46 | ``` 47 | 48 | It is obvious that these constraints cannot be both satisfied. 49 | 50 | 2. The problem cannot be solved for an unknown reason (the satisfiability of the set of constraints cannot be computed). The `solve` method returns False. 51 | 52 | 3. The solver takes too long to complete and exceeds the allowed `max_time`. The `solve` method returns False. 53 | 54 | 4. The solver successes in finding a schedule that satisfies all the constraints. The `solve` method returns the solution, which can be rendered as a Gantt chart or a JSON string. 55 | 56 | !!! note 57 | 58 | If the solver fails to give a solution, increase the `max_time` (case 2) or remove some constraints (case 1). 59 | 60 | ## Find another solution 61 | 62 | The solver may schedule: 63 | 64 | * one solution among many, in the case where there is no optimization, 65 | 66 | * the best possible schedule in case of an optimization issue. 67 | 68 | In both cases, you may need to check a different schedule that fits all the constraints. Use the `find_another_solution` method and pass the variable you would want the solver to look for another solution. 69 | 70 | !!! note 71 | 72 | Before requesting another solution, the `solve` method has first to be executed, i.e. there should already be a current solution. 73 | 74 | You can pass any variable to the `find_another_solution` method: a task start, a task end, a task duration, a resource productivity etc. 75 | 76 | For example, there are 5 different ways to schedule a FixedDurationTask with a duration=2 in an horizon of 6. The default solution returned by the solver is: 77 | 78 | ``` py 79 | problem = ps.SchedulingProblem(name='FindAnotherSolution', horizon=6) 80 | task_1 = ps.FixedDurationTask(name='task1', duration=2) 81 | problem.add_task(task_1) 82 | solver = ps.SchedulingSolver(problem=problem) 83 | solution = solver.solve() 84 | print("Solution for task_1.start:", solution.tasks['task1']) 85 | ``` 86 | 87 | ``` bash 88 | Solution for task_1.start: 0 89 | ``` 90 | 91 | Then, we can request for another solution: 92 | 93 | ``` py 94 | solution = solver.find_another_solution(task_1.start) 95 | if solution is not None: 96 | print("New solution for task_1.start:", solution.tasks['task1']) 97 | ``` 98 | 99 | ``` bash 100 | Solution for task_1.start: 1 101 | ``` 102 | 103 | You can recursively call `find_another_solution` to find all possible solutions, until the solver fails to return a new one. 104 | 105 | ## Run in debug mode 106 | 107 | If the `debug` attribute is set to True, the z3 solver is run with the unsat_core option. This will result in a much longer computation time, but this will help identifying the constraints that conflict. Because of this higher consumption of resources, the `debug` flag should be used only if the solver fails to find a solution. 108 | 109 | ## Optimization 110 | 111 | Please refer to the [Objectives](objectives.md) page for further details. 112 | 113 | ## Gantt chart 114 | 115 | Please refer to the [Gantt chart](gantt_chart.md) page for further details. 116 | 117 | ## Logics 118 | 119 | | logics | computing_time(s) | flowtime | priority | obj value | 120 | | - | - | - | - | - | 121 | |QF_LRA|2.84|147|289|436| 122 | |QF_LIA|4.25|165|320|485| 123 | |QF_RDL|0.48|None|None|None| 124 | |QF_IDL|3.45|174|339|513| 125 | |QF_AUFLIA|6.01|129|270|399| 126 | |QF_ALIA|3.69|139|280|419| 127 | |QF_AUFLIRA|4.30|145|266|411| 128 | |QF_AUFNIA|4.41|159|337|496| 129 | |QF_AUFNIRA|5.35|168|310|478| 130 | |QF_ANIA|6.12|168|320|488| 131 | |QF_LIRA|5.41|151|302|453| 132 | |QF_UFLIA|6.18|143|296|439| 133 | |QF_UFLRA|9.19|143|305|448| 134 | |QF_UFIDL|4.98|132|263|395| 135 | |QF_UFRDL|5.69|171|352|523| 136 | |QF_NIRA|6.72|142|268|410| 137 | |QF_UFNRA|8.51|160|300|460| 138 | |QF_UFNIA|18.89|130|261|391| 139 | |QF_UFNIRA|6.36|171|320|491| 140 | |QF_S|5.28|152|289|441| 141 | |QF_SLIA|4.33|174|361|535| 142 | |UFIDL|6.70|126|246|372| 143 | |HORN|0.49|None|None|None| 144 | |QF_FPLRA|6.21|129|253|382| 145 | -------------------------------------------------------------------------------- /docs/use-case-software-development.md: -------------------------------------------------------------------------------- 1 | # Use case: software development 2 | 3 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=doc/use-case-software-development.ipynb) 4 | 5 | Open In Colab 6 | 7 | 8 | To illustrate the way to use ProcessScheduler, let's imagine the simple following use case: the developmenent of a scheduling software intended for end-user. The software is developed using Python, and provides a modern Qt GUI. Three junior developers are in charge (Elias, Louis, Elise), under the supervision of their project manager Justine. The objective of this document is to generate a schedule of the different developmenent tasks to go rom the early design stages to the first software release. This notebook can tested online at mybinder.org 9 | 10 | 11 | ### Step 1. Import the module 12 | The best way to import the processscheduler module is to choose an alias import. Indeed, a global import should generate name conflicts. Here, the *ps* alias is used. 13 | 14 | 15 | ``` py 16 | import processscheduler as ps 17 | from datetime import timedelta, datetime 18 | 19 | %config InlineBackend.figure_formats = ['svg'] 20 | ``` 21 | 22 | ### Step 2. Create the scheduling problem 23 | The SchedulingProblem has to be defined. The problem must have a name (it is a mandatory argument). Of course you can create as many problems (i.e; SchedulingProblem instances), for example if you need to compare two or more different schedules. 24 | 25 | ``` py 26 | problem = ps.SchedulingProblem( 27 | name="SoftwareDevelopment", delta_time=timedelta(days=1), start_time=datetime.now() 28 | ) 29 | ``` 30 | 31 | ### Step 3. Create tasks instances 32 | The SchedulingProblem has to be defined. The problem must have a name (it is a mandatory argument). Of course you can create as many problems (i.e SchedulingProblem instances) as needed, for example if you need to compare two or more different schedules. In this example, one period is one day. 33 | 34 | ``` py 35 | preliminary_design = ps.FixedDurationTask(name="PreliminaryDesign", duration=1) # 1 day 36 | core_development = ps.VariableDurationTask(name="CoreDevelopmenent", work_amount=10) 37 | gui_development = ps.VariableDurationTask(name="GUIDevelopment", work_amount=15) 38 | integration = ps.VariableDurationTask(name="Integration", work_amount=3) 39 | tests_development = ps.VariableDurationTask(name="TestDevelopment", work_amount=8) 40 | release = ps.ZeroDurationTask(name="ReleaseMilestone") 41 | ``` 42 | 43 | ### Step 4. Create tasks time constraints 44 | Define precedences or set start and end times 45 | 46 | ``` py 47 | ps.TaskStartAt(task=preliminary_design, value=0) 48 | ps.TaskPrecedence(task_before=preliminary_design, task_after=core_development) 49 | ps.TaskPrecedence(task_before=preliminary_design, task_after=gui_development) 50 | ps.TaskPrecedence(task_before=gui_development, task_after=tests_development) 51 | ps.TaskPrecedence(task_before=core_development, task_after=tests_development) 52 | ps.TaskPrecedence(task_before=tests_development, task_after=integration) 53 | ps.TaskPrecedence(task_before=integration, task_after=release) 54 | ``` 55 | 56 | ``` bash 57 | TaskPrecedence_30566790() 58 | 1 assertion(s): 59 | Integration_end <= ReleaseMilestone_start 60 | ``` 61 | 62 | ### Step 5. Create resources 63 | Define all resources required for all tasks to be processed, including productivity and cost_per_period. 64 | 65 | ``` py 66 | elias = ps.Worker( 67 | name="Elias", productivity=2, cost=ps.ConstantCostFunction(value=600) 68 | ) # cost in $/day 69 | louis = ps.Worker( 70 | name="Louis", productivity=2, cost=ps.ConstantCostFunction(value=600) 71 | ) 72 | elise = ps.Worker( 73 | name="Elise", productivity=3, cost=ps.ConstantCostFunction(value=800) 74 | ) 75 | justine = ps.Worker( 76 | name="Justine", productivity=2, cost=ps.ConstantCostFunction(value=1200) 77 | ) 78 | ``` 79 | 80 | ### Step 6. Assign resources to tasks 81 | 82 | ``` py 83 | preliminary_design.add_required_resources([elias, louis, elise, justine]) 84 | core_development.add_required_resources([louis, elise]) 85 | gui_development.add_required_resources([elise]) 86 | tests_development.add_required_resources([elias, louis]) 87 | integration.add_required_resources([justine]) 88 | release.add_required_resources([justine]) 89 | ``` 90 | ### Step 7. Add a total cost indicator 91 | This resource cost indicator computes the total cost of selected resources. 92 | 93 | ``` py 94 | cost_ind = ps.IndicatorResourceCost(list_of_resources=[elias, louis, elise, justine]) 95 | ``` 96 | 97 | ### Step 8. Solve and plot using plotly 98 | 99 | ``` py 100 | # solve 101 | solver = ps.SchedulingSolver(problem=problem) 102 | solution = solver.solve() 103 | ``` 104 | ``` bash 105 | Solver type: 106 | =========== 107 | -> Standard SAT/SMT solver 108 | Total computation time: 109 | ===================== 110 | SoftwareDevelopment satisfiability checked in 0.01s 111 | ``` 112 | 113 | ``` py 114 | if solution: 115 | ps.render_gantt_plotly(solution) 116 | ``` 117 | 118 | ![Gantt](img/software-development-gantt.png) 119 | -------------------------------------------------------------------------------- /docs/workflow.md: -------------------------------------------------------------------------------- 1 | # Workflow 2 | 3 | The structure of this documentation is designed to mirror the typical workflow of a ProcessScheduler Python script, guiding you through each step of the scheduling process: 4 | 5 | ``` mermaid 6 | graph TD 7 | A[1. Create a SchedulingProblem] --> B[2. Create objects that represent the problem]; 8 | B --> C[3. Constraint the schedule]; 9 | C --> D[4. Add indicators]; 10 | D --> E[5. Add objectives]; 11 | E --> F[6. Solve]; 12 | F --> G[7. Analyse]; 13 | G --> B; 14 | ``` 15 | 16 | 1. Create a **[SchedulingProblem](scheduling_problem.md)**: This is the foundational step where you establish the SchedulingProblem, serving as the primary container for all components of your scheduling scenario. 17 | 18 | 2. Create **Objects Representing The Problem**: Select appropriate **Task** and **Resource** objects to accurately represent the elements of your use case. This step involves defining the tasks to be scheduled and the resources available for these tasks. 19 | 20 | 3. Apply **Constraints** to the Schedule: Introduce temporal or logical constraints to define how tasks should be ordered or how resources are to be utilized. Constraints are critical for reflecting real-world limitations and requirements in your schedule. 21 | 22 | 4. Add **Indicators** (optional): indicators or metrics are added to the scheduling problem. These indicators might include key performance metrics, resource utilization rates, or other measurable factors that are crucial for analyzing the effectiveness of the schedule. By adding this step, the schedule can be more effectively monitored and evaluated. 23 | 24 | 5. Define **Objectives** (optional): you can specify one or more objectives. Objectives, built on Indicators, are used to determine what constitutes an 'optimal' schedule within the confines of your constraints. This could include minimizing total time, cost, or other metrics relevant to your scenario. 25 | 26 | 5. **Execute the Solver**: Run the solver to find a feasible (and possibly optimal, depending on defined objectives) schedule based on your tasks, resources, constraints, and objectives. 27 | 28 | 6. **Analyze** the Results: Once the solver has found a solution, you can render the schedule in various formats such as a Gantt chart or export it to Excel. This step is crucial for evaluating the effectiveness of the proposed schedule. Based on the analysis, you might revisit the representation stage to adjust your problem model, refine constraints, or alter objectives. 29 | 30 | This workflow provides a structured approach to building and solving scheduling problems, ensuring that all essential aspects of your scheduling scenario are methodically addressed. -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | channels: 2 | - defaults 3 | - conda-forge 4 | dependencies: 5 | - pip 6 | - python=3.10 7 | - pip: 8 | - z3-solver==4.14.0.0 9 | - matplotlib 10 | - plotly 11 | - kaleido 12 | - ipywidgets 13 | - isodate 14 | - ipympl 15 | - psutil 16 | - XlsxWriter 17 | - coverage 18 | - pydantic 19 | - rich 20 | - pandas 21 | - pyarrow 22 | - pytest 23 | - -e . 24 | -------------------------------------------------------------------------------- /examples-notebooks/excavator-use-case/excavator_medium_size.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/examples-notebooks/excavator-use-case/excavator_medium_size.jpg -------------------------------------------------------------------------------- /examples-notebooks/excavator-use-case/excavator_small.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/examples-notebooks/excavator-use-case/excavator_small.jpeg -------------------------------------------------------------------------------- /examples-notebooks/excavator-use-case/huge_hole.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/examples-notebooks/excavator-use-case/huge_hole.jpg -------------------------------------------------------------------------------- /examples-notebooks/excavator-use-case/medium_hole.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/examples-notebooks/excavator-use-case/medium_hole.jpeg -------------------------------------------------------------------------------- /examples-notebooks/excavator-use-case/small_hole.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/examples-notebooks/excavator-use-case/small_hole.jpeg -------------------------------------------------------------------------------- /examples-notebooks/pics/hello_world_gantt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/examples-notebooks/pics/hello_world_gantt.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: ProcessScheduler 2 | site_description: A Python framework for project and team management. Automatic and optimized resource scheduling. 3 | strict: true 4 | site_url: https://processscheduler.github.io/ 5 | site_author: Thomas Paviot 6 | 7 | # Repository 8 | repo_name: tpaviot/processscheduler 9 | repo_url: https://github.com/tpaviot/processscheduler 10 | 11 | # Copyright 12 | copyright: | 13 | © 2024 Thomas Paviot 14 | 15 | # Configuration 16 | theme: 17 | name: material 18 | features: 19 | - announce.dismiss 20 | - content.action.edit 21 | - content.action.view 22 | - content.code.annotate 23 | - content.code.copy 24 | - content.code.select 25 | - content.tooltips 26 | - navigation.footer 27 | - navigation.indexes 28 | - navigation.sections 29 | - navigation.tabs 30 | - navigation.top 31 | - navigation.tracking 32 | - search.highlight 33 | - search.share 34 | - search.suggest 35 | - toc.follow 36 | language: en 37 | palette: 38 | - media: "(prefers-color-scheme)" 39 | toggle: 40 | icon: material/link 41 | name: Switch to light mode 42 | - media: "(prefers-color-scheme: light)" 43 | scheme: default 44 | primary: indigo 45 | accent: indigo 46 | toggle: 47 | icon: material/toggle-switch 48 | name: Switch to dark mode 49 | - media: "(prefers-color-scheme: dark)" 50 | scheme: slate 51 | primary: black 52 | accent: indigo 53 | toggle: 54 | icon: material/toggle-switch-off 55 | name: Switch to system preference 56 | font: 57 | text: Roboto 58 | code: Roboto Mono 59 | 60 | watch: 61 | - processscheduler 62 | 63 | nav: 64 | - Introduction: 65 | - Features: features.md 66 | - What's inside: inside.md 67 | - Download/Install: download_install.md 68 | - Run: run.md 69 | - Learn: 70 | - Workflow: workflow.md 71 | - Scheduling Problem: scheduling_problem.md 72 | - Represent: 73 | - Tasks: task.md 74 | - Resources: resource.md 75 | - Resource Assignmnent: resource_assignment.md 76 | - Buffers: buffer.md 77 | - Functions: function.md 78 | - Indicators: indicator.md 79 | - Constraint: 80 | - Task constraints: task_constraints.md 81 | - Resource Constraints: resource_constraints.md 82 | - Indicator Constraints: indicator_constraints.md 83 | - Customized Constraints: customized_constraints.md 84 | - First Order Logics: first_order_logic_constraints.md 85 | - Solve: 86 | - Solver: solving.md 87 | - Optimize objectives: objectives.md 88 | - Render: 89 | - Gantt Chart: gantt_chart.md 90 | - Data exchange: data_exchange.md 91 | - Use cases: 92 | - FlowShop: use-case-flow-shop.md 93 | - Formula One: use-case-formula-one-change-tires.md 94 | - Team Management: use-case-software-development.md 95 | - Scheduling - Theory, Algorithms, and Systems: pinedo.md 96 | - Blog: 97 | - blog/index.md 98 | 99 | extra: 100 | social: 101 | - icon: fontawesome/brands/github-alt 102 | link: https://github.com/tpaviot/ProcessScheduler 103 | - icon: fontawesome/brands/python 104 | link: https://pypi.org/project/processscheduler/ 105 | - icon: fontawesome/brands/linkedin 106 | link: https://www.linkedin.com/in/thomaspaviot/ 107 | 108 | extra_javascript: 109 | - https://polyfill.io/v3/polyfill.min.js?features=es6 110 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 111 | - https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js 112 | - javascripts/tablesort.js 113 | 114 | # Plugins 115 | plugins: 116 | - blog 117 | - search: 118 | separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' 119 | # - minify: 120 | # minify_html: true 121 | 122 | # Extensions 123 | markdown_extensions: 124 | - abbr 125 | - admonition 126 | - attr_list 127 | - def_list 128 | - footnotes 129 | - md_in_html 130 | - toc: 131 | permalink: true 132 | - pymdownx.arithmatex: 133 | generic: true 134 | - pymdownx.betterem: 135 | smart_enable: all 136 | - pymdownx.caret 137 | - pymdownx.details 138 | - pymdownx.inlinehilite 139 | - pymdownx.keys 140 | - pymdownx.mark 141 | - pymdownx.smartsymbols 142 | - pymdownx.tilde 143 | - pymdownx.snippets 144 | - pymdownx.superfences 145 | - pymdownx.emoji: 146 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 147 | emoji_index: !!python/name:material.extensions.emoji.twemoji 148 | - pymdownx.highlight: 149 | anchor_linenums: true 150 | line_spans: __span 151 | pygments_lang_class: true 152 | - pymdownx.magiclink: 153 | normalize_issue_symbols: true 154 | repo_url_shorthand: true 155 | user: squidfunk 156 | repo: mkdocs-material 157 | - pymdownx.snippets: 158 | auto_append: 159 | - includes/mkdocs.md 160 | - pymdownx.superfences: 161 | custom_fences: 162 | - name: mermaid 163 | class: mermaid 164 | format: !!python/name:pymdownx.superfences.fence_code_format 165 | - pymdownx.tabbed: 166 | alternate_style: true 167 | combine_header_slug: true 168 | slugify: !!python/object/apply:pymdownx.slugs.slugify 169 | kwds: 170 | case: lower 171 | - pymdownx.tasklist: 172 | custom_checkbox: true 173 | - tables 174 | - toc: 175 | permalink: true 176 | title: Page contents 177 | - pymdownx.highlight: 178 | anchor_linenums: true 179 | - pymdownx.arithmatex: 180 | generic: true 181 | - footnotes 182 | -------------------------------------------------------------------------------- /processscheduler/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | __VERSION__ = "2.1.0" 17 | 18 | try: 19 | import z3 20 | except ModuleNotFoundError as z3_not_found: 21 | raise ImportError("z3 not found. It is a mandatory dependency") from z3_not_found 22 | 23 | # Expose everything useful 24 | from processscheduler.first_order_logic import Not, Or, And, Xor, Implies, IfThenElse 25 | from processscheduler.indicator import ( 26 | Indicator, 27 | IndicatorFromMathExpression, 28 | IndicatorTardiness, 29 | IndicatorEarliness, 30 | IndicatorNumberOfTardyTasks, 31 | IndicatorMaximumLateness, 32 | IndicatorResourceUtilization, 33 | IndicatorResourceIdle, 34 | IndicatorNumberTasksAssigned, 35 | IndicatorResourceCost, 36 | IndicatorMaxBufferLevel, 37 | IndicatorMinBufferLevel, 38 | ) 39 | from processscheduler.objective import ( 40 | Objective, 41 | ObjectiveMaximizeIndicator, 42 | ObjectiveMinimizeIndicator, 43 | ObjectiveMinimizeMakespan, 44 | ObjectiveMaximizeResourceUtilization, 45 | ObjectiveMinimizeResourceCost, 46 | ObjectivePriorities, 47 | ObjectiveTasksStartLatest, 48 | ObjectiveTasksStartEarliest, 49 | ObjectiveMinimizeGreatestStartTime, 50 | ObjectiveMinimizeFlowtime, 51 | ObjectiveMinimizeFlowtimeSingleResource, 52 | ObjectiveMaximizeMaxBufferLevel, 53 | ObjectiveMinimizeMaxBufferLevel, 54 | ) 55 | from processscheduler.task import ( 56 | ZeroDurationTask, 57 | FixedDurationTask, 58 | VariableDurationTask, 59 | ) 60 | from processscheduler.constraint import * 61 | from processscheduler.task_constraint import * 62 | from processscheduler.resource_constraint import ( 63 | SameWorkers, 64 | DistinctWorkers, 65 | ResourceUnavailable, 66 | ResourcePeriodicallyUnavailable, 67 | WorkLoad, 68 | ResourceTasksDistance, 69 | ResourceNonDelay, 70 | ResourceInterrupted, 71 | ResourcePeriodicallyInterrupted, 72 | ) 73 | from processscheduler.indicator_constraint import IndicatorTarget, IndicatorBounds 74 | from processscheduler.resource import Worker, CumulativeWorker, SelectWorkers 75 | from processscheduler.function import ( 76 | ConstantFunction, 77 | LinearFunction, 78 | PolynomialFunction, 79 | GeneralFunction, 80 | ) 81 | from processscheduler.problem import SchedulingProblem 82 | from processscheduler.solver import SchedulingSolver 83 | from processscheduler.buffer import NonConcurrentBuffer, ConcurrentBuffer 84 | from processscheduler.plotter import ( 85 | plot_function, 86 | render_gantt_matplotlib, 87 | render_gantt_plotly, 88 | ) 89 | -------------------------------------------------------------------------------- /processscheduler/__main__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/processscheduler/__main__.py -------------------------------------------------------------------------------- /processscheduler/base.py: -------------------------------------------------------------------------------- 1 | """This module contains fundamental classes/functions that are used everywhere in the project.""" 2 | 3 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 4 | # 5 | # This file is part of ProcessScheduler. 6 | # 7 | # This program is free software: you can redistribute it and/or modify it under 8 | # the terms of the GNU General Public License as published by the Free Software 9 | # Foundation, either version 3 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but WITHOUT 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. If not, see . 17 | 18 | import os 19 | from typing import Any, Dict, List 20 | from uuid import uuid4 21 | 22 | from pydantic import BaseModel, Field, ConfigDict 23 | 24 | import z3 25 | 26 | 27 | class BaseModelWithJson(BaseModel): 28 | """The base object for most ProcessScheduler classes""" 29 | 30 | model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) 31 | 32 | name: str = Field(default=None) 33 | type: str = Field(default=None) 34 | metadata: Dict[str, Any] = Field(default_factory=dict) 35 | 36 | def __init__(self, **data) -> None: 37 | """The base name for all ProcessScheduler objects. 38 | 39 | Provides an assertions list, a uniqueid. 40 | 41 | Args: 42 | name: the object name. It must be unique 43 | """ 44 | # check name type 45 | super().__init__(**data) 46 | 47 | self._uid = uuid4().int 48 | 49 | if self.name is None: 50 | self.name = f"{self.__class__.__name__}_{str(self._uid)[:12]}" 51 | 52 | self.type = f"{self.__class__.__name__}" 53 | 54 | def __hash__(self) -> int: 55 | return self._uid 56 | 57 | def __eq__(self, other) -> bool: 58 | return self._uid == other._uid 59 | 60 | # def __repr__(self) -> str: 61 | # """Print the object name, its uid and the assertions.""" 62 | # str_to_return = ( 63 | # f"{self.name}({type(self)})\n{len(self._z3_assertions)} assertion(s):\n" 64 | # ) 65 | # assertions_str = "".join(f"{assertion}" for assertion in self._z3_assertions) 66 | # return str_to_return + assertions_str 67 | 68 | def to_json(self, compact=False): 69 | """return a json string""" 70 | # exclude the 'problem' field to avoid the problem to be exported into the solution 71 | return self.model_dump_json(indent=None if compact else 4, exclude="problem") 72 | 73 | def to_json_file(self, filename, compact=False): 74 | with open(filename, "w") as f: 75 | f.write(self.to_json(compact)) 76 | return os.path.isfile(filename) # success 77 | 78 | 79 | # 80 | # NamedUIDObject, name and uid for hashing 81 | # 82 | class NamedUIDObject(BaseModelWithJson): 83 | """The base object for most ProcessScheduler classes""" 84 | 85 | def __init__(self, **data) -> None: 86 | super().__init__(**data) 87 | 88 | # SMT assertions 89 | # start and end integer values must be positive 90 | self._z3_assertions = [] # type: List[z3.BoolRef] 91 | self._z3_assertion_hashes = [] 92 | 93 | def __repr__(self) -> str: 94 | """Print the object name, its uid and the assertions.""" 95 | return self.__str__() 96 | 97 | def append_z3_list_of_assertions( 98 | self, list_of_z3_assertions: List[z3.BoolRef] 99 | ) -> None: 100 | for z3_asst in list_of_z3_assertions: 101 | self.append_z3_assertion(z3_asst) 102 | 103 | def append_z3_assertion(self, z3_assertion: z3.BoolRef) -> bool: 104 | """ 105 | Add a z3 assertion to the list of assertions to be satisfied. 106 | 107 | Args: 108 | z3_assertion: the z3 assertion 109 | """ 110 | # check if the assertion is in the list 111 | # workaround to avoid heavy hash computations 112 | assertion_hash = hash(z3_assertion) 113 | if assertion_hash in self._z3_assertion_hashes: 114 | raise AssertionError(f"assertion {z3_assertion} already added.") 115 | self._z3_assertions.append(z3_assertion) 116 | self._z3_assertion_hashes.append(assertion_hash) 117 | return True 118 | 119 | def get_z3_assertions(self) -> List[z3.BoolRef]: 120 | """Return the assertions list""" 121 | return self._z3_assertions 122 | 123 | 124 | # Define a global problem 125 | # None by default 126 | # the scheduling problem will set this variable 127 | active_problem = None 128 | -------------------------------------------------------------------------------- /processscheduler/buffer.py: -------------------------------------------------------------------------------- 1 | """The buffers definition.""" 2 | 3 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 4 | # 5 | # This file is part of ProcessScheduler. 6 | # 7 | # This program is free software: you can redistribute it and/or modify it under 8 | # the terms of the GNU General Public License as published by the Free Software 9 | # Foundation, either version 3 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but WITHOUT 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. If not, see . 17 | 18 | from processscheduler.base import NamedUIDObject 19 | 20 | # import processscheduler.context as ps_context 21 | import processscheduler.base 22 | 23 | import z3 24 | 25 | from pydantic import Field 26 | 27 | 28 | class Buffer(NamedUIDObject): 29 | initial_level: int = Field(default=None) 30 | final_level: int = Field(default=None) 31 | lower_bound: int = Field(default=None) 32 | upper_bound: int = Field(default=None) 33 | 34 | def __init__(self, **data) -> None: 35 | super().__init__(**data) 36 | 37 | if processscheduler.base.active_problem is None: 38 | raise AssertionError( 39 | "No context available. First create a SchedulingProblem" 40 | ) 41 | 42 | if self.initial_level is None and self.final_level is None: 43 | raise AssertionError("At least initial level or final level must be set") 44 | 45 | # a dict that contains all tasks that consume this buffer 46 | # unloading tasks contribute to decrement the buffer level 47 | self._unloading_tasks = {} 48 | # a dict that contains all tasks that feed this buffer 49 | # loading tasks contribute to increment the buffer level 50 | self._loading_tasks = {} 51 | # a list that contains the instants where the buffer level changes 52 | self._level_changes_time = [] 53 | # a list that stores the buffer level between each level change 54 | # the first item of this list is always the initial level 55 | buffer_initial_level = z3.Int(f"{self.name}_initial_level") 56 | self._buffer_levels = [buffer_initial_level] 57 | 58 | if self.initial_level is not None: 59 | self.append_z3_assertion(buffer_initial_level == self.initial_level) 60 | 61 | # Note: the final level is set in the solver.py script, 62 | # add this task to the current context 63 | 64 | processscheduler.base.active_problem.add_buffer(self) 65 | 66 | def add_unloading_task(self, task, quantity) -> None: 67 | # store quantity 68 | self._unloading_tasks[task] = quantity 69 | # the buffer is unloaded at the task start time 70 | # append a new level level and a new level change time 71 | self._level_changes_time.append(z3.Int(f"{self.name}_sc_time_{task.name}")) 72 | self._buffer_levels.append(z3.Int(f"{self.name}_level_{task.name}")) 73 | 74 | def add_loading_task(self, task, quantity) -> None: 75 | # store quantity 76 | self._loading_tasks[task] = quantity 77 | # the buffer is loaded at the task completion time 78 | # append a new level level and a new level change time 79 | self._level_changes_time.append(z3.Int(f"{self.name}_sc_time_{task.name}")) 80 | self._buffer_levels.append(z3.Int(f"{self.name}_level_{task.name}")) 81 | 82 | 83 | class NonConcurrentBuffer(Buffer): 84 | """Only one task can, at one instantA buffer that cannot be accessed by different tasks at the same time""" 85 | 86 | 87 | class ConcurrentBuffer(Buffer): 88 | """A buffer that be accessed concurrently by any number of loading/unloading tasks""" 89 | -------------------------------------------------------------------------------- /processscheduler/constraint.py: -------------------------------------------------------------------------------- 1 | """Task constraints and related classes.""" 2 | 3 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 4 | # 5 | # This file is part of ProcessScheduler. 6 | # 7 | # This program is free software: you can redistribute it and/or modify it under 8 | # the terms of the GNU General Public License as published by the Free Software 9 | # Foundation, either version 3 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but WITHOUT 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. If not, see . 17 | 18 | from typing import List, Literal 19 | 20 | import z3 21 | 22 | from pydantic import Field, PositiveInt 23 | 24 | from processscheduler.base import NamedUIDObject 25 | import processscheduler.base 26 | 27 | 28 | # 29 | # Base Constraint class 30 | # 31 | class Constraint(NamedUIDObject): 32 | """The base class for all constraints, including Task and Resource constraints.""" 33 | 34 | optional: bool = Field(default=False) 35 | 36 | def __init__(self, **data) -> None: 37 | super().__init__(**data) 38 | 39 | # by default, we dont know if the constraint is created from 40 | # an assertion 41 | self._created_from_assertion = False 42 | 43 | # by default, this constraint has to be applied 44 | if self.optional: 45 | self._applied = z3.Bool(f"constraint_{self._uid}_applied") 46 | else: 47 | self._applied = True 48 | 49 | # store this constraint into the current context 50 | processscheduler.base.active_problem.add_constraint(self) 51 | 52 | def set_created_from_assertion(self) -> None: 53 | """Set the flag created_from_assertion True. This flag must be set to True 54 | if, for example, a constraint is defined from the expression: 55 | ps.not_(ps.TaskStartAt(task_1, 0)) 56 | thus, the Task task_1 assertions must not be add to the z3 solver. 57 | """ 58 | self._created_from_assertion = True 59 | 60 | def set_z3_assertions(self, list_of_z3_assertions: List[z3.BoolRef]) -> None: 61 | """Each constraint comes with a set of z3 assertions 62 | to satisfy.""" 63 | if self.optional: 64 | self.append_z3_assertion(z3.Implies(self._applied, list_of_z3_assertions)) 65 | else: 66 | self.append_z3_assertion(list_of_z3_assertions) 67 | 68 | 69 | class ConstraintFromExpression(Constraint): 70 | expression: z3.BoolRef 71 | 72 | def __init__(self, **data) -> None: 73 | super().__init__(**data) 74 | 75 | self.set_z3_assertions(self.expression) 76 | 77 | 78 | class ResourceConstraint(Constraint): 79 | """Constraint that applies on a Resource (typically a Worker)""" 80 | 81 | 82 | class TaskConstraint(Constraint): 83 | """Constraint that applies on a Task""" 84 | 85 | 86 | class IndicatorConstraint(Constraint): 87 | """Constraint that applies on an Indicator""" 88 | 89 | 90 | # 91 | # A Generic constraint that applies to both Resource or Task 92 | # 93 | class ForceApplyNOptionalConstraints(Constraint): 94 | """Given a set of m different optional constraints, force the solver to apply 95 | at at least/at most/exactly n tasks, with 0 < n <= m. Work for both 96 | Task and/or Resource constraints.""" 97 | 98 | list_of_optional_constraints: List[Constraint] 99 | nb_constraints_to_apply: PositiveInt = Field(default=1) 100 | kind: Literal["min", "max", "exact"] = Field(default="exact") 101 | 102 | def __init__(self, **data) -> None: 103 | super().__init__(**data) 104 | 105 | problem_function = {"min": z3.PbGe, "max": z3.PbLe, "exact": z3.PbEq} 106 | 107 | # first check that all tasks from the list_of_optional_tasks are 108 | # actually optional 109 | for constraint in self.list_of_optional_constraints: 110 | if not constraint.optional: 111 | raise TypeError( 112 | f"The constraint {constraint.name} must explicitly be set as optional." 113 | ) 114 | 115 | # all scheduled variables to take into account 116 | applied_vars = [ 117 | constraint._applied for constraint in self.list_of_optional_constraints 118 | ] 119 | 120 | asst = problem_function[self.kind]( 121 | [(applied, True) for applied in applied_vars], self.nb_constraints_to_apply 122 | ) 123 | self.set_z3_assertions(asst) 124 | -------------------------------------------------------------------------------- /processscheduler/excel_io.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | from binascii import crc32 17 | 18 | try: 19 | import xlsxwriter 20 | 21 | HAVE_XLSXWRITER = True 22 | except ImportError: 23 | HAVE_XLSXWRITER = False 24 | 25 | 26 | def _get_color_from_string(a_string: str, colors: bool): 27 | if colors: 28 | hash_str = f"{crc32(a_string.encode('utf-8'))}" 29 | return f"#{hash_str[2:8]}" 30 | return "#F0F0F0" 31 | 32 | 33 | def export_solution_to_excel_file(solution, excel_filename, colors: bool): 34 | """Export to excel. 35 | colors: a boolean flag. If True background colors are generated from 36 | the string hash, light gray otherwise. 37 | """ 38 | if not HAVE_XLSXWRITER: 39 | raise ModuleNotFoundError("XlsxWriter is required but not installed.") 40 | 41 | workbook = xlsxwriter.Workbook(excel_filename) 42 | # 43 | # Resource worksheet 44 | # 45 | worksheet_resource = workbook.add_worksheet("GANTT Resource view") 46 | 47 | # widen the first column to make the text clearer. 48 | worksheet_resource.set_column("A:A", 20) 49 | # shorten following columns 50 | worksheet_resource.set_column("B:EC", 4) 51 | # Add a bold format to use to highlight cells. 52 | bold = workbook.add_format({"bold": True}) 53 | worksheet_resource.write("A1", "Resources", bold) # {'bold': True}) 54 | 55 | cell_resource_name_format = workbook.add_format({"align": "left"}) 56 | cell_resource_name_format.set_font_size(12) 57 | 58 | # then loop over resources 59 | for i, resource_name in enumerate(solution.resources): 60 | # write the resource name on the first column 61 | worksheet_resource.write(i + 1, 0, resource_name, cell_resource_name_format) 62 | 63 | # get the related resource object 64 | ress = solution.resources[resource_name] 65 | 66 | for task_name, task_start, task_end in ress.assignments: 67 | # unavailabilities are rendered with a grey dashed bar 68 | bg_color = _get_color_from_string(task_name, colors) 69 | 70 | cell_task_format = workbook.add_format({"align": "center"}) 71 | cell_task_format.set_font_size(12) 72 | cell_task_format.set_border() 73 | cell_task_format.set_bg_color(bg_color) 74 | 75 | # if task duration is greater than 1, need to merge cells 76 | if task_end - task_start > 1: 77 | worksheet_resource.merge_range( 78 | i + 1, # row 79 | task_start + 1, # start column 80 | i + 1, # row 81 | task_end, # end column 82 | task_name, # text to display 83 | cell_task_format, 84 | ) 85 | else: 86 | worksheet_resource.write( 87 | i + 1, task_start + 1, task_name, cell_task_format 88 | ) 89 | 90 | # finally close the workbook 91 | worksheet_resource.autofit() 92 | worksheet_resource.freeze_panes(1, 1) 93 | 94 | # 95 | # Task worksheet 96 | # 97 | worksheet_task = workbook.add_worksheet("GANTT Task view") 98 | 99 | # widen the first column to make the text clearer. 100 | worksheet_task.set_column("A:A", 20) 101 | # shorten following columns 102 | worksheet_task.set_column("B:EC", 4) 103 | # Add a bold format to use to highlight cells. 104 | worksheet_task.write("A1", "Tasks", bold) # {'bold': True}) 105 | 106 | # then loop over tasks 107 | for i, task_name in enumerate(solution.tasks): 108 | # write the resource name on the first column 109 | cell_task_name_format = workbook.add_format({"align": "left"}) 110 | cell_task_name_format.set_font_size(12) 111 | worksheet_task.write(i + 1, 0, task_name, cell_task_name_format) 112 | 113 | # get the related resource object 114 | current_task = solution.tasks[task_name] 115 | 116 | text_to_display = ",".join(current_task.assigned_resources) 117 | 118 | # the color is computed from the resource names 119 | bg_color = _get_color_from_string(text_to_display, colors) 120 | 121 | cell_task_format = workbook.add_format({"align": "center"}) 122 | cell_task_format.set_font_size(12) 123 | cell_task_format.set_border() 124 | cell_task_format.set_bg_color(bg_color) 125 | 126 | # if task duration is greater than 1, need to merge contiguous cells 127 | if current_task.end - current_task.start > 1: 128 | worksheet_task.merge_range( 129 | i + 1, # row 130 | current_task.start + 1, # start column 131 | i + 1, # row 132 | current_task.end, # end column 133 | text_to_display, 134 | cell_task_format, 135 | ) 136 | else: 137 | worksheet_task.write( 138 | i + 1, current_task.start + 1, text_to_display, cell_task_format 139 | ) 140 | 141 | worksheet_task.autofit() 142 | worksheet_task.freeze_panes(1, 1) 143 | 144 | # 145 | # Indicators 146 | # 147 | worksheet_indicator = workbook.add_worksheet("Indicators") 148 | worksheet_indicator.write("A1", "Indicator", bold) 149 | worksheet_indicator.write("B1", "Value", bold) 150 | 151 | cell_indicator_name_format = workbook.add_format({"align": "left"}) 152 | cell_indicator_name_format.set_font_size(12) 153 | 154 | for i, indicator_name in enumerate(solution.indicators): 155 | worksheet_indicator.write(i + 1, 0, indicator_name, cell_indicator_name_format) 156 | 157 | indicator_value = solution.indicators[indicator_name] 158 | worksheet_indicator.write(i + 1, 1, indicator_value, cell_indicator_name_format) 159 | 160 | worksheet_indicator.autofit() 161 | 162 | # finally save the workbook 163 | workbook.close() 164 | -------------------------------------------------------------------------------- /processscheduler/first_order_logic.py: -------------------------------------------------------------------------------- 1 | """First order logic operators, implies, if/then/else.""" 2 | 3 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 4 | # 5 | # This file is part of ProcessScheduler. 6 | # 7 | # This program is free software: you can redistribute it and/or modify it under 8 | # the terms of the GNU General Public License as published by the Free Software 9 | # Foundation, either version 3 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but WITHOUT 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. If not, see . 17 | 18 | from typing import Union, List 19 | 20 | import z3 21 | 22 | from processscheduler.constraint import Constraint 23 | 24 | 25 | # 26 | # Utility functions 27 | # 28 | def _get_assertions(constraint: Union[z3.BoolRef, Constraint]) -> z3.BoolRef: 29 | """Take a z3.BoolRef or any Constraint and returns the assertions for this object.""" 30 | if isinstance(constraint, z3.BoolRef): 31 | assertion = constraint 32 | else: # Constraint 33 | # tag this constraint as defined from an expression 34 | constraint.set_created_from_assertion() 35 | assertion = constraint.get_z3_assertions() 36 | 37 | return assertion 38 | 39 | 40 | def _constraints_to_list_of_assertions(list_of_constraints) -> List[z3.BoolRef]: 41 | """Convert a list of constraints or assertions to a list of assertions.""" 42 | list_of_boolrefs_to_return = [] 43 | for constraint in list_of_constraints: 44 | assertions = _get_assertions(constraint) 45 | if isinstance(assertions, list): 46 | list_of_boolrefs_to_return.extend(assertions) 47 | elif isinstance(assertions, z3.BoolRef): 48 | list_of_boolrefs_to_return.append(assertions) 49 | return list_of_boolrefs_to_return 50 | 51 | 52 | # 53 | # Nested boolean operators for Constraint objects 54 | # or z3.BoolRef 55 | # 56 | class Not(Constraint): 57 | """A solver class""" 58 | 59 | constraint: Union[z3.BoolRef, Constraint] 60 | 61 | def __init__(self, **data) -> None: 62 | super().__init__(**data) 63 | 64 | asst = z3.Not(z3.And(_get_assertions(self.constraint))) 65 | 66 | self.set_z3_assertions(asst) 67 | 68 | 69 | class Or(Constraint): 70 | """A solver class""" 71 | 72 | list_of_constraints: List[Union[z3.BoolRef, Constraint]] 73 | 74 | def __init__(self, **data) -> None: 75 | super().__init__(**data) 76 | 77 | asst = z3.Or(_constraints_to_list_of_assertions(self.list_of_constraints)) 78 | 79 | self.set_z3_assertions(asst) 80 | 81 | 82 | class And(Constraint): 83 | """A solver class""" 84 | 85 | list_of_constraints: List[Union[z3.BoolRef, Constraint]] 86 | 87 | def __init__(self, **data) -> None: 88 | super().__init__(**data) 89 | 90 | asst = z3.And(_constraints_to_list_of_assertions(self.list_of_constraints)) 91 | 92 | self.set_z3_assertions(asst) 93 | 94 | 95 | class Xor(Constraint): 96 | """Boolean 'xor' between two assertions or constraints. 97 | One assertion must be satisfied, the other is not satisfied. The list of constraint 98 | must have exactly 2 elements. 99 | """ 100 | 101 | constraint_1: Union[z3.BoolRef, Constraint] 102 | constraint_2: Union[z3.BoolRef, Constraint] 103 | 104 | def __init__(self, **data) -> None: 105 | super().__init__(**data) 106 | 107 | asst = z3.Xor( 108 | z3.And(_get_assertions(self.constraint_1)), 109 | z3.And(_get_assertions(self.constraint_2)), 110 | ) 111 | 112 | self.set_z3_assertions(asst) 113 | 114 | 115 | class Implies(Constraint): 116 | """Boolean 'xor' between two assertions or constraints. 117 | One assertion must be satisfied, the other is not satisfied. The list of constraint 118 | must have exactly 2 elements. 119 | """ 120 | 121 | condition: Union[z3.BoolRef, bool] 122 | list_of_constraints: List[Union[z3.BoolRef, Constraint]] 123 | 124 | def __init__(self, **data) -> None: 125 | super().__init__(**data) 126 | asst = z3.Implies( 127 | self.condition, 128 | z3.And(_constraints_to_list_of_assertions(self.list_of_constraints)), 129 | ) 130 | self.set_z3_assertions(asst) 131 | 132 | 133 | class IfThenElse(Constraint): 134 | """Boolean 'xor' between two assertions or constraints. 135 | One assertion must be satisfied, the other is not satisfied. The list of constraint 136 | must have exactly 2 elements. 137 | """ 138 | 139 | condition: Union[z3.BoolRef, bool] 140 | then_list_of_constraints: List[Union[z3.BoolRef, Constraint]] 141 | else_list_of_constraints: List[Union[z3.BoolRef, Constraint]] 142 | 143 | def __init__(self, **data) -> None: 144 | super().__init__(**data) 145 | asst = z3.If( 146 | self.condition, 147 | z3.And(_constraints_to_list_of_assertions(self.then_list_of_constraints)), 148 | z3.And(_constraints_to_list_of_assertions(self.else_list_of_constraints)), 149 | ) 150 | self.set_z3_assertions(asst) 151 | -------------------------------------------------------------------------------- /processscheduler/function.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | from typing import Callable, Union, List 17 | import warnings 18 | 19 | from processscheduler.base import NamedUIDObject 20 | 21 | import z3 22 | 23 | 24 | class Function(NamedUIDObject): 25 | """The base class for function definition, to be used for cost or penalties""" 26 | 27 | def __init__(self, **data) -> None: 28 | super().__init__(**data) 29 | self._function = lambda x: 0 # default returns 0 30 | 31 | def set_function(self, f: Callable): 32 | self._function = f 33 | 34 | def __call__(self, value): 35 | """compute the value of the cost function for a given value""" 36 | to_return = self._function(value) 37 | # check if there is a ToReal conversion in the function 38 | # this may occur if the cost function is not linear 39 | # and this would result in an unexpected computation 40 | if "ToReal" in f"{to_return}": 41 | warnings.warn( 42 | "ToReal conversion in the cost function, might result in computation issues.", 43 | UserWarning, 44 | ) 45 | return to_return 46 | 47 | 48 | class ConstantFunction(Function): 49 | value: Union[int, float] 50 | 51 | def __init__(self, **data) -> None: 52 | super().__init__(**data) 53 | self.set_function(lambda x: self.value) 54 | 55 | 56 | class LinearFunction(Function): 57 | """A linear function: 58 | F(x) = slope * x + intercept 59 | """ 60 | 61 | slope: Union[z3.ArithRef, int, float] 62 | intercept: Union[z3.ArithRef, int, float] 63 | 64 | def __init__(self, **data) -> None: 65 | super().__init__(**data) 66 | self.set_function(lambda x: self.slope * x + self.intercept) 67 | 68 | 69 | class PolynomialFunction(Function): 70 | """A cost function under a polynomial form. 71 | C(x) = a_n * x^n + a_{n-1} * x^(n-1) + ... + a_0""" 72 | 73 | coefficients: List[Union[z3.ArithRef, int, float]] 74 | 75 | def __init__(self, **data) -> None: 76 | super().__init__(**data) 77 | 78 | def _compute(x): 79 | result = self.coefficients[-1] 80 | v = x 81 | for i in range(len(self.coefficients) - 2, -1, -1): 82 | if self.coefficients[i] != 0: 83 | result += self.coefficients[i] * v 84 | v = v * x 85 | return result 86 | 87 | self.set_function(_compute) 88 | 89 | 90 | class GeneralFunction(Function): 91 | """A function defined from a python function""" 92 | 93 | function: Callable 94 | 95 | def __init__(self, **data) -> None: 96 | super().__init__(**data) 97 | 98 | self.set_function(self.function) 99 | -------------------------------------------------------------------------------- /processscheduler/indicator_constraint.py: -------------------------------------------------------------------------------- 1 | """Task constraints and related classes.""" 2 | 3 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 4 | # 5 | # This file is part of ProcessScheduler. 6 | # 7 | # This program is free software: you can redistribute it and/or modify it under 8 | # the terms of the GNU General Public License as published by the Free Software 9 | # Foundation, either version 3 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but WITHOUT 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. z3.If not, see . 17 | 18 | from pydantic import Field 19 | 20 | from processscheduler.constraint import IndicatorConstraint 21 | from processscheduler.objective import Indicator 22 | 23 | 24 | class IndicatorTarget(IndicatorConstraint): 25 | indicator: Indicator 26 | value: int 27 | 28 | def __init__(self, **data) -> None: 29 | super().__init__(**data) 30 | self.append_z3_assertion(self.indicator._indicator_variable == self.value) 31 | 32 | 33 | class IndicatorBounds(IndicatorConstraint): 34 | indicator: Indicator 35 | lower_bound: int = Field(default=None) 36 | upper_bound: int = Field(default=None) 37 | 38 | def __init__(self, **data) -> None: 39 | super().__init__(**data) 40 | 41 | if self.lower_bound is None and self.upper_bound is None: 42 | raise AssertionError( 43 | "lower and upper bounds cannot be set to None, either one of them must be set" 44 | ) 45 | 46 | if self.lower_bound is not None: 47 | self.append_z3_assertion( 48 | self.indicator._indicator_variable >= self.lower_bound 49 | ) 50 | if self.upper_bound is not None: 51 | self.append_z3_assertion( 52 | self.indicator._indicator_variable <= self.upper_bound 53 | ) 54 | -------------------------------------------------------------------------------- /processscheduler/util.py: -------------------------------------------------------------------------------- 1 | """This module utility functions share across the software.""" 2 | 3 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 4 | # 5 | # This file is part of ProcessScheduler. 6 | # 7 | # This program is free software: you can redistribute it and/or modify it under 8 | # the terms of the GNU General Public License as published by the Free Software 9 | # Foundation, either version 3 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but WITHOUT 13 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 15 | # You should have received a copy of the GNU General Public License along with 16 | # this program. If not, see . 17 | 18 | import z3 19 | 20 | 21 | def calc_parabola_from_three_points(vector_x, vector_y): 22 | """ 23 | Compute the coefficients a, b, c of the parabola that fits three points A, B, and C. 24 | 25 | Args: 26 | vector_x (list): List of x-coordinates for points A, B, and C. 27 | vector_y (list): List of y-coordinates for points A, B, and C. 28 | 29 | Returns: 30 | tuple: Coefficients a, b, c for the parabola equation y = ax^2 + bx + c. 31 | """ 32 | x1, x2, x3 = vector_x 33 | y1, y2, y3 = vector_y 34 | denom = (x1 - x2) * (x1 - x3) * (x2 - x3) 35 | a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / denom 36 | b = (x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1) + x1 * x1 * (y2 - y3)) / denom 37 | c = ( 38 | x2 * x3 * (x2 - x3) * y1 + x3 * x1 * (x3 - x1) * y2 + x1 * x2 * (x1 - x2) * y3 39 | ) / denom 40 | return a, b, c 41 | 42 | 43 | def get_maximum(maxi, list_of_values): 44 | """ 45 | Given a z3 variable and a list of z3 variables, return assertions and the maximum value. 46 | 47 | Args: 48 | maxi (z3.ArithRef): Z3 variable to represent the maximum value. 49 | list_of_values (list): List of z3 variables. 50 | 51 | Returns: 52 | list: Assertions to be passed to the solver. 53 | """ 54 | if not list_of_values: 55 | raise AssertionError("Empty list") 56 | assertions = [z3.Or([maxi == elem for elem in list_of_values])] 57 | assertions.extend([maxi >= elem for elem in list_of_values]) 58 | return assertions 59 | 60 | 61 | def get_minimum(mini, list_of_values): 62 | """ 63 | Given a z3 variable and a list of z3 variables, return assertions and the minimum value. 64 | 65 | Args: 66 | mini (z3.ArithRef): Z3 variable to represent the minimum value. 67 | list_of_values (list): List of z3 variables. 68 | 69 | Returns: 70 | list: Assertions to be passed to the solver. 71 | """ 72 | if not list_of_values: 73 | raise AssertionError("Empty list") 74 | assertions = [z3.Or([mini == elem for elem in list_of_values])] 75 | assertions.extend([mini <= elem for elem in list_of_values]) 76 | return assertions 77 | 78 | 79 | def sort_duplicates(z3_int_list): 80 | """ 81 | Sort a list of int variables using bubble sort and return the sorted list and associated assertions. 82 | 83 | Args: 84 | z3_int_list (list): List of z3 integer variables. 85 | 86 | Returns: 87 | tuple: Sorted list of z3 integer variables, and associated assertions. 88 | """ 89 | sorted_list = z3_int_list.copy() 90 | glob_asst = [] 91 | 92 | def bubble_up(ar): 93 | arr = ar.copy() 94 | local_asst = [] 95 | for i in range(len(arr) - 1): 96 | x = arr[i] 97 | y = arr[i + 1] 98 | x1, y1 = z3.FreshInt(), z3.FreshInt() 99 | c = z3.If(x <= y, z3.And(x1 == x, y1 == y), z3.And(x1 == y, y1 == x)) 100 | arr[i] = x1 101 | arr[i + 1] = y1 102 | local_asst.append(c) 103 | return arr, local_asst 104 | 105 | for _ in range(len(sorted_list)): 106 | sorted_list, asst = bubble_up(sorted_list) 107 | glob_asst.extend(asst) 108 | 109 | return sorted_list, glob_asst 110 | 111 | 112 | def sort_no_duplicates(z3_int_list): 113 | """ 114 | Sort a list of integers with distinct values and return the sorted list and constraints. 115 | 116 | Args: 117 | z3_int_list (list): List of z3 integer variables. 118 | 119 | Returns: 120 | tuple: Sorted list of z3 integer variables, and constraints for distinct values and ordering. 121 | """ 122 | n = len(z3_int_list) 123 | a = [z3.FreshInt() for _ in range(n)] 124 | constraints = [z3.Or([a[i] == z3_int_list[j] for j in range(n)]) for i in range(n)] 125 | constraints.append(z3.And([a[i] < a[i + 1] for i in range(n - 1)])) 126 | return a, constraints 127 | 128 | 129 | def clean_buffer_levels(buffer_levels, buffer_change_times): 130 | """ 131 | Clean buffer levels and corresponding change times by removing duplicates. 132 | 133 | Args: 134 | buffer_levels (list): List of buffer levels. 135 | buffer_change_times (list): List of buffer change times. 136 | 137 | Returns: 138 | tuple: Cleaned buffer levels and corresponding change times. 139 | """ 140 | if len(buffer_levels) != len(buffer_change_times) + 1: 141 | raise AssertionError( 142 | "Buffer levels list should have exactly one more element than buffer change times." 143 | ) 144 | new_l1 = [] # Initial buffer level is always present 145 | new_l2 = [] 146 | first_level = buffer_levels.pop(0) 147 | for a, b in zip(buffer_levels, buffer_change_times): 148 | if new_l2.count(b) < 1: 149 | new_l1.append(a) 150 | new_l2.append(b) 151 | new_l1 = [first_level] + new_l1 152 | return new_l1, new_l2 153 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ProcessScheduler" 3 | version = "2.1.0" 4 | description = "A Python package for automatic and optimized resource scheduling" 5 | license = "GPL-3.0-or-later" 6 | authors = ["Thomas Paviot "] 7 | packages = [{include = "processscheduler"}] 8 | readme = "README.md" 9 | homepage = "https://processscheduler.github.io/" 10 | classifiers = [ 11 | "Development Status :: 4 - Beta", 12 | "Intended Audience :: Developers", 13 | "Intended Audience :: Manufacturing", 14 | "Intended Audience :: Financial and Insurance Industry", 15 | "Intended Audience :: Healthcare Industry", 16 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 17 | "Natural Language :: English", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python :: 3", 20 | "Topic :: Office/Business :: Scheduling", 21 | "Topic :: Software Development :: Libraries", 22 | "Typing :: Typed", 23 | ] 24 | 25 | [tool.poetry.dependencies] 26 | python = ">=3.8,<4.0" 27 | z3-solver = "==4.14.0.0" 28 | pydantic = ">=2.5" 29 | 30 | [build-system] 31 | requires = ["poetry-core>=1.0.0"] 32 | build-backend = "poetry.core.masonry.api" 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | z3-solver==4.14.0.0 2 | setuptools 3 | rich 4 | pydantic 5 | matplotlib 6 | plotly 7 | kaleido 8 | ipywidgets 9 | isodate 10 | ipympl 11 | psutil 12 | XlsxWriter 13 | pandas 14 | pyarrow 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | DESCRIPTION = ( 4 | "A Python package for project and team management. Automatic and optimized resource scheduling." 5 | ) 6 | 7 | CLASSIFIERS = [ 8 | "Development Status :: 5 - Production/Stable", 9 | "Intended Audience :: Developers", 10 | "Intended Audience :: Manufacturing", 11 | "Intended Audience :: Financial and Insurance Industry", 12 | "Intended Audience :: Healthcare Industry", 13 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 14 | "Natural Language :: English", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3", 17 | "Topic :: Office/Business :: Scheduling", 18 | "Topic :: Software Development :: Libraries", 19 | "Typing :: Typed", 20 | ] 21 | 22 | setup( 23 | name="ProcessScheduler", 24 | version="2.1.0", 25 | description=DESCRIPTION, 26 | long_description=open("README.md").read(), 27 | long_description_content_type="text/markdown", 28 | url="https://github.com/tpaviot/ProcessScheduler", 29 | author="Thomas Paviot", 30 | author_email="tpaviot@gmail.com", 31 | license="GPLv3", 32 | platforms="Platform Independent", 33 | packages=find_packages(), 34 | install_requires=["z3-solver==4.14.0.0", "pydantic>=2"], 35 | classifiers=CLASSIFIERS, 36 | zip_safe=True, 37 | ) 38 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | name: 'Conda build job' 3 | vmImage: 'Ubuntu-18.04' 4 | conda_bld: '3.16.3' 5 | 6 | jobs: 7 | - job: ${{ parameters.name }} 8 | timeoutInMinutes: 360 9 | 10 | pool: 11 | vmImage: ${{ parameters.vmImage }} 12 | 13 | steps: 14 | 15 | - bash: | 16 | pip install -r requirements.txt && \ 17 | pip install pytest poetry coverage && \ 18 | pip install -e . 19 | displayName: 'Install using pip' 20 | 21 | - bash: | 22 | python -c 'from processscheduler import *' 23 | displayName: 'Test import' 24 | 25 | - bash: | 26 | pytest . 27 | displayName: 'Run unittest suite using pytest' 28 | 29 | - ${{ if startsWith(parameters.name, 'Ubuntu') }}: 30 | - bash: | 31 | cd benchmark &&\ 32 | mkdir output &&\ 33 | python benchmark_dev_team.py --plot=False --logics=QF_IDL --max_time=30 > benchmark_dev_team_result.txt &&\ 34 | python benchmark_n_queens.py --plot=False --logics=QF_UFIDL --max_time=30 > benchmark_n_queens_result.txt 35 | displayName: 'Run performance benchmark' 36 | 37 | - ${{ if startsWith(parameters.name, 'Ubuntu') }}: 38 | - bash: | 39 | cd benchmark &&\ 40 | python benchmark_logics.py &&\ 41 | cd .. 42 | displayName: 'Run logics benchmark' 43 | 44 | - ${{ if startsWith(parameters.name, 'Ubuntu') }}: 45 | - task: PublishPipelineArtifact@0 46 | inputs: 47 | targetPath: '/home/vsts/work/1/s/benchmark/' 48 | artifactName: Benchmarks${{ parameters.name }} 49 | displayName: 'Publish benchmark artifact' 50 | 51 | - ${{ if startsWith(parameters.name, 'Ubuntu') }}: 52 | - bash: | 53 | coverage run -m pytest && \ 54 | coverage report -m && \ 55 | coverage html && \ 56 | bash <(curl -s https://codecov.io/bash) 57 | displayName: 'Coverage and export to codecov (Linux py310 only)' 58 | 59 | - bash: | 60 | poetry install 61 | displayName: 'Install using poetry' 62 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaviot/ProcessScheduler/83f0c145f4117129779da3366725a54cef48279e/test/__init__.py -------------------------------------------------------------------------------- /test/test_cumulative.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | 19 | from pydantic import ValidationError 20 | 21 | import pytest 22 | 23 | 24 | def test_create_cumulative(): 25 | """take the single task/single resource and display output""" 26 | ps.SchedulingProblem(name="CreateCumulative", horizon=10) 27 | # work = ps.Worker(name="Mac", cost=None) 28 | cumulative_worker = ps.CumulativeWorker(name="MachineA", size=4) 29 | assert len(cumulative_worker._cumulative_workers) == 4 30 | 31 | 32 | def test_create_cumulative_wrong_type(): 33 | """take the single task/single resource and display output""" 34 | ps.SchedulingProblem(name="CreateCumulativeWrongType", horizon=10) 35 | with pytest.raises(ValidationError): 36 | ps.CumulativeWorker(name="MachineA", size=1) 37 | with pytest.raises(ValidationError): 38 | ps.CumulativeWorker(name="MachineA", size=2.5) 39 | 40 | 41 | def test_cumulative_1(): 42 | pb_bs = ps.SchedulingProblem(name="Cumulative1", horizon=3) 43 | # tasks 44 | t1 = ps.FixedDurationTask(name="T1", duration=2) 45 | t2 = ps.FixedDurationTask(name="T2", duration=2) 46 | t3 = ps.FixedDurationTask(name="T3", duration=2) 47 | 48 | # workers 49 | r1 = ps.CumulativeWorker(name="Machine1", size=3) 50 | # resource assignment 51 | t1.add_required_resource(r1) 52 | t2.add_required_resource(r1) 53 | t3.add_required_resource(r1) 54 | 55 | # constraints 56 | ps.TaskStartAt(task=t2, value=1) 57 | 58 | # plot solution 59 | solver = ps.SchedulingSolver(problem=pb_bs) 60 | solution = solver.solve() 61 | assert solution 62 | 63 | 64 | def test_cumulative_2(): 65 | pb_bs = ps.SchedulingProblem(name="Cumulative2", horizon=3) 66 | # tasks 67 | t1 = ps.FixedDurationTask(name="T1", duration=2) 68 | t2 = ps.FixedDurationTask(name="T2", duration=2) 69 | 70 | # workers 71 | r1 = ps.CumulativeWorker(name="Machine1", size=2) 72 | # resource assignment 73 | t1.add_required_resource(r1) 74 | t2.add_required_resource(r1) 75 | 76 | # constraints 77 | ps.TaskStartAt(task=t2, value=1) 78 | 79 | solver = ps.SchedulingSolver(problem=pb_bs) 80 | solution = solver.solve() 81 | assert solution 82 | 83 | 84 | def test_optional_cumulative(): 85 | """Same as above, but with an optional taskand an horizon of 2. 86 | t2 should not be scheduled.""" 87 | pb_bs = ps.SchedulingProblem(name="OptionalCumulative", horizon=2) 88 | # tasks 89 | t1 = ps.FixedDurationTask(name="T1", duration=2) 90 | t2 = ps.FixedDurationTask(name="T2", duration=2, optional=True) 91 | t3 = ps.FixedDurationTask(name="T3", duration=2) 92 | 93 | # workers 94 | r1 = ps.CumulativeWorker(name="Machine1", size=2) 95 | # resource assignment 96 | t1.add_required_resource(r1) 97 | t2.add_required_resource(r1) 98 | 99 | # constraints 100 | ps.TaskStartAt(task=t2, value=1) 101 | 102 | # plot solution 103 | solver = ps.SchedulingSolver(problem=pb_bs) 104 | solution = solver.solve() 105 | assert solution 106 | assert solution.tasks[t1.name].scheduled 107 | assert solution.tasks[t3.name].scheduled 108 | assert not solution.tasks[t2.name].scheduled 109 | 110 | 111 | def test_cumulative_select_worker_1(): 112 | pb_bs = ps.SchedulingProblem(name="CumulativeSelectWorker", horizon=2) 113 | # tasks 114 | t1 = ps.FixedDurationTask(name="T1", duration=2) 115 | t2 = ps.FixedDurationTask(name="T2", duration=2) 116 | 117 | # workers 118 | r1 = ps.CumulativeWorker(name="Machine1", size=2) 119 | r2 = ps.CumulativeWorker(name="Machine2", size=2) 120 | r3 = ps.Worker(name="Machine3") 121 | # resource assignment 122 | 123 | t1.add_required_resource( 124 | ps.SelectWorkers(list_of_workers=[r1, r2], nb_workers_to_select=1) 125 | ) 126 | t2.add_required_resource( 127 | ps.SelectWorkers(list_of_workers=[r1, r3], nb_workers_to_select=1) 128 | ) 129 | 130 | # plot solution 131 | solver = ps.SchedulingSolver(problem=pb_bs) 132 | solution = solver.solve() 133 | assert solution 134 | 135 | 136 | def test_cumulative_with_makespan(): 137 | nb_tasks = 16 138 | capa = 4 139 | pb_bs = ps.SchedulingProblem(name="Hospital") 140 | # workers 141 | r1 = ps.CumulativeWorker(name="Room", size=capa) 142 | 143 | for i in range(nb_tasks): 144 | t = ps.FixedDurationTask(name=f"T{i+1}", duration=5) 145 | t.add_required_resource(r1) 146 | ps.ObjectiveMinimizeMakespan() 147 | solver = ps.SchedulingSolver(problem=pb_bs) 148 | solution = solver.solve() 149 | 150 | assert solution 151 | assert solution.horizon == 20 152 | 153 | 154 | def test_cumulative_hosp(): 155 | n = 16 156 | capa = 4 157 | pb_bs = ps.SchedulingProblem(name="Hospital", horizon=n // capa) 158 | # workers 159 | r1 = ps.CumulativeWorker(name="Room", size=capa) 160 | 161 | for i in range(n): 162 | t = ps.FixedDurationTask(name=f"T{i+1}", duration=1) 163 | t.add_required_resource(r1) 164 | 165 | solver = ps.SchedulingSolver(problem=pb_bs) 166 | solution = solver.solve() 167 | assert solution 168 | 169 | 170 | def test_cumulative_productivity(): 171 | """Horizon should be 4, 100/29=3.44.""" 172 | problem = ps.SchedulingProblem(name="CumulativeProductivity") 173 | t_1 = ps.VariableDurationTask(name="t1", work_amount=100) 174 | 175 | worker_1 = ps.CumulativeWorker(name="CumulWorker", size=3, productivity=29) 176 | t_1.add_required_resource(worker_1) 177 | 178 | ps.ObjectiveMinimizeMakespan() 179 | 180 | solution = ps.SchedulingSolver(problem=problem).solve() 181 | assert solution 182 | assert solution.horizon == 4 183 | -------------------------------------------------------------------------------- /test/test_datetime.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | from datetime import datetime, timedelta 17 | 18 | import processscheduler as ps 19 | 20 | 21 | def test_datetime_1(): 22 | """take the single task/single resource and display output""" 23 | problem = ps.SchedulingProblem( 24 | name="DateTimeBase", 25 | horizon=7, 26 | delta_time=timedelta(minutes=15), 27 | start_time=datetime.now(), 28 | ) 29 | task = ps.FixedDurationTask(name="task", duration=7) 30 | # problem.add_task(task) 31 | worker = ps.Worker(name="worker") 32 | # problem.add_resource(worker) 33 | task.add_required_resource(worker) 34 | solver = ps.SchedulingSolver(problem=problem) 35 | solution = solver.solve() 36 | assert solution 37 | print(solution) 38 | 39 | 40 | def test_datetime_time(): 41 | """take the single task/single resource and display output""" 42 | problem = ps.SchedulingProblem( 43 | name="DateTimeBase", horizon=7, delta_time=timedelta(minutes=15) 44 | ) 45 | task = ps.FixedDurationTask(name="task", duration=7) 46 | # problem.add_task(task) 47 | worker = ps.Worker(name="worker") 48 | task.add_required_resource(worker) 49 | solver = ps.SchedulingSolver(problem=problem) 50 | solution = solver.solve() 51 | assert solution 52 | print(solution) 53 | 54 | 55 | def test_datetime_export_to_json(): 56 | problem = ps.SchedulingProblem( 57 | name="DateTimeJson", 58 | delta_time=timedelta(hours=1), 59 | start_time=datetime.now(), 60 | ) 61 | task = ps.FixedDurationTask(name="task", duration=7) 62 | # problem.add_task(task) 63 | worker = ps.Worker(name="worker") 64 | task.add_required_resource(worker) 65 | solver = ps.SchedulingSolver(problem=problem) 66 | solution = solver.solve() 67 | assert solution 68 | solution.to_json() 69 | -------------------------------------------------------------------------------- /test/test_dynamic_resource.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | 19 | 20 | def test_dynamic_1() -> None: 21 | pb = ps.SchedulingProblem(name="DynamicTest1") 22 | task_1 = ps.FixedDurationTask(name="task1", duration=10, work_amount=10) 23 | task_2 = ps.FixedDurationTask(name="task2", duration=5) 24 | task_3 = ps.FixedDurationTask(name="task3", duration=5) 25 | 26 | ps.TaskStartAt(task=task_3, value=0) 27 | ps.TaskEndAt(task=task_2, value=10) 28 | 29 | worker_1 = ps.Worker(name="Worker1", productivity=1) 30 | worker_2 = ps.Worker(name="Worker2", productivity=1) 31 | 32 | task_1.add_required_resources([worker_1, worker_2], dynamic=True) 33 | task_2.add_required_resource(worker_1) 34 | task_3.add_required_resource(worker_2) 35 | 36 | ps.ObjectiveMinimizeMakespan() 37 | 38 | solver = ps.SchedulingSolver(problem=pb) 39 | solution = solver.solve() 40 | assert solution.horizon == 10 41 | 42 | 43 | def test_non_dynamic_1() -> None: 44 | # same test as previously 45 | # but the task_1 workers are non dynamic. 46 | # so the horizon must be 20. 47 | pb = ps.SchedulingProblem(name="NonDynamicTest1") 48 | task_1 = ps.FixedDurationTask(name="task1", duration=10, work_amount=10) 49 | task_2 = ps.FixedDurationTask(name="task2", duration=5) 50 | task_3 = ps.FixedDurationTask(name="task3", duration=5) 51 | 52 | ps.TaskStartAt(task=task_3, value=0) 53 | ps.TaskEndAt(task=task_2, value=10) 54 | 55 | worker_1 = ps.Worker(name="Worker1", productivity=1) 56 | worker_2 = ps.Worker(name="Worker2", productivity=1) 57 | 58 | task_1.add_required_resources([worker_1, worker_2]) # dynamic False by default 59 | task_2.add_required_resource(worker_1) 60 | task_3.add_required_resource(worker_2) 61 | 62 | ps.ObjectiveMinimizeMakespan() 63 | 64 | solver = ps.SchedulingSolver(problem=pb) 65 | solution = solver.solve() 66 | assert solution.horizon == 20 67 | -------------------------------------------------------------------------------- /test/test_function.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | import math 17 | 18 | import processscheduler as ps 19 | 20 | 21 | # 22 | # Constant function 23 | # 24 | def test_constant_cost_function_1() -> None: 25 | c = ps.ConstantFunction(value=55) 26 | assert c(0) == 55 27 | assert c(10) == 55 28 | 29 | 30 | # 31 | # Linear function 32 | # 33 | def test_basic_linear_function_1() -> None: 34 | c = ps.LinearFunction(slope=1, intercept=1) 35 | assert c(0) == 1 36 | assert c(-1) == 0 37 | 38 | 39 | def test_horizontal_linear_function_1() -> None: 40 | c = ps.LinearFunction(slope=0, intercept=3) 41 | assert c(0) == 3 42 | assert c(-1) == 3 43 | 44 | 45 | # 46 | # Polynomial function 47 | # 48 | def test_polynomial_function_1() -> None: 49 | c = ps.PolynomialFunction(coefficients=[1, 1]) 50 | assert c(0) == 1 51 | assert c(-1) == 0 52 | 53 | 54 | def test_polynomial_function_2() -> None: 55 | c = ps.PolynomialFunction(coefficients=[0, 4]) 56 | assert c(0) == 4 57 | assert c(-1) == 4 58 | 59 | 60 | def test_polynomial_function_3() -> None: 61 | c = ps.PolynomialFunction(coefficients=[1, 1, 1]) 62 | assert c(0) == 1 63 | assert c(1) == 3 64 | assert c(2) == 7 65 | 66 | 67 | def test_polynomial_function_4() -> None: 68 | c = ps.PolynomialFunction(coefficients=[2, 0, 0]) 69 | assert c(0) == 0 70 | assert c(3) == 18 71 | 72 | 73 | # 74 | # General function 75 | # 76 | def test_general_function() -> None: 77 | def f(x): 78 | return math.sin(x) 79 | 80 | gen_sin = ps.GeneralFunction(function=f) 81 | assert gen_sin(0) == 0 82 | assert gen_sin(math.pi / 2) == 1 83 | 84 | 85 | # 86 | # Plots 87 | # 88 | def test_plot_constant_function(): 89 | c = ps.ConstantFunction(value=55) 90 | ps.plot_function(c, interval=[0, 20], title="Constant function", show_plot=False) 91 | 92 | 93 | def test_plot_linear_function(): 94 | c = ps.LinearFunction(slope=1, intercept=2) 95 | ps.plot_function(c, interval=[0, 20], title="Linear function", show_plot=False) 96 | 97 | 98 | def test_plot_polynomial_function(): 99 | c = ps.PolynomialFunction(coefficients=[1, 2, 3, 4]) 100 | ps.plot_function(c, interval=[0, 20], title="Polynomial function", show_plot=False) 101 | -------------------------------------------------------------------------------- /test/test_group_of_tasks.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | 19 | 20 | def test_unordered_group_task_1() -> None: 21 | """Task can be scheduled.""" 22 | pb = ps.SchedulingProblem(name="UnorderedGroupOfTasks1", horizon=20) 23 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 24 | task_2 = ps.FixedDurationTask(name="task2", duration=7) 25 | task_3 = ps.FixedDurationTask(name="task3", duration=2) 26 | ps.UnorderedTaskGroup(list_of_tasks=[task_1, task_2, task_3], time_interval=[6, 17]) 27 | solver = ps.SchedulingSolver(problem=pb) 28 | solution = solver.solve() 29 | assert solution 30 | assert solution.tasks[task_1.name].start <= 6 31 | assert solution.tasks[task_1.name].end <= 17 32 | assert solution.tasks[task_2.name].start <= 6 33 | assert solution.tasks[task_2.name].end <= 17 34 | assert solution.tasks[task_3.name].start <= 6 35 | assert solution.tasks[task_3.name].end <= 17 36 | 37 | 38 | def test_unordered_group_task_2() -> None: 39 | """Task can be scheduled.""" 40 | pb = ps.SchedulingProblem(name="UnorderedGroupOfTasks2", horizon=20) 41 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 42 | task_2 = ps.FixedDurationTask(name="task2", duration=7) 43 | task_3 = ps.FixedDurationTask(name="task3", duration=2) 44 | ps.UnorderedTaskGroup( 45 | list_of_tasks=[task_1, task_2, task_3], time_interval_length=8 46 | ) 47 | solver = ps.SchedulingSolver(problem=pb) 48 | solution = solver.solve() 49 | assert solution 50 | s_1 = solution.tasks[task_1.name].start 51 | e_1 = solution.tasks[task_1.name].end 52 | s_2 = solution.tasks[task_2.name].start 53 | e_2 = solution.tasks[task_2.name].end 54 | s_3 = solution.tasks[task_3.name].start 55 | e_3 = solution.tasks[task_3.name].end 56 | 57 | assert max([e_1, e_2, e_3]) - min([s_1, s_2, s_3]) <= 8 58 | 59 | 60 | def test_unordered_group_task_precedence_1() -> None: 61 | """Task can be scheduled.""" 62 | pb = ps.SchedulingProblem(name="UnorderedGroupOfTasks3", horizon=20) 63 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 64 | task_2 = ps.FixedDurationTask(name="task2", duration=7) 65 | task_3 = ps.FixedDurationTask(name="task3", duration=2) 66 | task_4 = ps.FixedDurationTask(name="task4", duration=2) 67 | group1 = ps.UnorderedTaskGroup( 68 | list_of_tasks=[task_1, task_2], time_interval_length=8 69 | ) 70 | group2 = ps.UnorderedTaskGroup( 71 | list_of_tasks=[task_3, task_4], time_interval_length=8 72 | ) 73 | ps.TaskPrecedence(task_before=group1, task_after=group2) 74 | solver = ps.SchedulingSolver(problem=pb) 75 | solution = solver.solve() 76 | assert solution 77 | e_1 = solution.tasks[task_1.name].end 78 | e_2 = solution.tasks[task_2.name].end 79 | s_3 = solution.tasks[task_3.name].start 80 | s_4 = solution.tasks[task_4.name].start 81 | assert e_1 <= s_3 82 | assert e_1 <= s_4 83 | assert e_2 <= s_3 84 | assert e_2 <= s_4 85 | 86 | 87 | def test_ordered_group_task_1() -> None: 88 | """Task can be scheduled.""" 89 | pb = ps.SchedulingProblem(name="OrderedGroupOfTasks1", horizon=40) 90 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 91 | task_2 = ps.FixedDurationTask(name="task2", duration=7) 92 | task_3 = ps.FixedDurationTask(name="task3", duration=2) 93 | task_4 = ps.FixedDurationTask(name="task4", duration=2) 94 | ps.OrderedTaskGroup( 95 | list_of_tasks=[task_1, task_2, task_3, task_4], 96 | kind="tight", 97 | time_interval=[23, 39], 98 | ) 99 | 100 | solver = ps.SchedulingSolver(problem=pb) 101 | solution = solver.solve() 102 | assert solution 103 | s_1 = solution.tasks[task_1.name].start 104 | e_1 = solution.tasks[task_1.name].end 105 | s_2 = solution.tasks[task_2.name].start 106 | e_2 = solution.tasks[task_2.name].end 107 | s_3 = solution.tasks[task_3.name].start 108 | e_3 = solution.tasks[task_3.name].end 109 | s_4 = solution.tasks[task_4.name].start 110 | assert e_1 == s_2 111 | assert e_2 == s_3 112 | assert e_3 == s_4 113 | assert s_1 >= 23 114 | assert s_4 <= 39 115 | -------------------------------------------------------------------------------- /test/test_io.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | import csv 17 | import os 18 | 19 | import processscheduler as ps 20 | 21 | 22 | def build_excavator_problem() -> ps.SchedulingProblem: 23 | """returns a problem with n tasks and n * 3 workers""" 24 | problem = ps.SchedulingProblem(name="Excavators") 25 | 26 | # three tasks 27 | dig_small_hole = ps.VariableDurationTask(name="DigSmallHole", work_amount=3) 28 | dig_medium_hole = ps.VariableDurationTask(name="DigMediumHole", work_amount=7) 29 | dig_huge_hole = ps.VariableDurationTask(name="DigHugeHole", work_amount=15) 30 | # medium_exc_cost = ps.PolynomialFunction(cost_function=cost_function_medium_exc) 31 | 32 | # two workers 33 | small_exc = ps.Worker( 34 | name="SmallExcavator", productivity=4, cost=ps.ConstantFunction(value=5) 35 | ) 36 | medium_ex = ps.Worker( 37 | name="MediumExcavator", productivity=6, cost=ps.ConstantFunction(value=10) 38 | ) 39 | 40 | dig_small_hole.add_required_resource( 41 | ps.SelectWorkers( 42 | list_of_workers=[small_exc, medium_ex], nb_workers_to_select=1, kind="min" 43 | ) 44 | ) 45 | dig_medium_hole.add_required_resource( 46 | ps.SelectWorkers( 47 | list_of_workers=[small_exc, medium_ex], nb_workers_to_select=1, kind="min" 48 | ) 49 | ) 50 | dig_huge_hole.add_required_resource( 51 | ps.SelectWorkers( 52 | list_of_workers=[small_exc, medium_ex], nb_workers_to_select=1, kind="min" 53 | ) 54 | ) 55 | 56 | # problem.add_objective_makespan() ## ERROR Serialization 57 | 58 | ps.IndicatorResourceCost(list_of_resources=[small_exc, medium_ex]) 59 | 60 | return problem 61 | 62 | 63 | PROBLEM = build_excavator_problem() 64 | SOLVER = ps.SchedulingSolver(problem=PROBLEM) 65 | SOLUTION = SOLVER.solve() 66 | 67 | 68 | def test_export_to_smt2(): 69 | SOLVER.export_to_smt2("excavator_problem.smt2") 70 | assert os.path.isfile("excavator_problem.smt2") 71 | 72 | 73 | def test_export_problem_to_json(): 74 | PROBLEM.to_json() 75 | 76 | 77 | def test_export_problem_to_json_file(): 78 | PROBLEM.to_json_file("excavator_problem.json") 79 | assert os.path.isfile("excavator_problem.json") 80 | 81 | 82 | def test_export_solution_to_json(): 83 | SOLUTION.to_json() 84 | 85 | 86 | def test_export_solution_to_json_file(): 87 | SOLUTION.to_json_file("excavator_solution.json") 88 | assert os.path.isfile("excavator_solution.json") 89 | 90 | 91 | def test_export_solution_to_excel_file(): 92 | SOLUTION.to_excel_file("excavator_nb.xlsx") 93 | assert os.path.isfile("excavator_nb.xlsx") 94 | SOLUTION.to_excel_file("excavator_colors.xlsx", colors=True) 95 | assert os.path.isfile("excavator_colors.xlsx") 96 | 97 | 98 | def test_export_solution_to_pandas_dataframe(): 99 | SOLUTION.to_df() 100 | 101 | 102 | def test_print_solution_as_pandas_dataframe(): 103 | print(SOLUTION) 104 | 105 | 106 | def test_export_solution_as_csv(): 107 | csv_filename = "tst.csv" 108 | SOLUTION.to_csv() 109 | # change the default pseparator 110 | SOLUTION.to_csv(separator=";") 111 | # as a file 112 | SOLUTION.to_csv(csv_filename=csv_filename) 113 | assert os.path.isfile(csv_filename) 114 | # check that the exporter file is a correct csv file 115 | with open(csv_filename, "r", encoding="utf8") as csvfile: 116 | csv_content = csv.reader(csvfile) 117 | # try to read the 4 lines 118 | for _ in range(4): 119 | row = next(csv_content) 120 | assert isinstance(row, list) 121 | assert len(row) == 7 122 | -------------------------------------------------------------------------------- /test/test_multiple_objectives.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | 19 | # in this test, two tasks. The start of the first one and the end of the second 20 | # one are constrained by a linear function 21 | # the maximum of task_1.end is 20 (in this case task_1.start is 0) 22 | # the maximum of task_2.end is 20 (in this case task_2.start is 0) 23 | # what happens if we look for the maximum of both task_1 and task_2 ends ? 24 | 25 | 26 | def test_multi_two_tasks_1() -> None: 27 | pb = ps.SchedulingProblem(name="MultiObjective1", horizon=20) 28 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 29 | task_2 = ps.FixedDurationTask(name="task2", duration=3) 30 | 31 | ps.ConstraintFromExpression(expression=task_1._end == 20 - task_2._start) 32 | 33 | # Maximize only task_1 end 34 | ind = ps.IndicatorFromMathExpression(name="Task1End", expression=task_1._end) 35 | 36 | ps.Objective(name="MaxMultObj1", target=ind, kind="maximize") 37 | 38 | solution = ps.SchedulingSolver(problem=pb).solve() 39 | 40 | assert solution 41 | assert solution.tasks[task_1.name].end == 20 42 | assert solution.tasks[task_2.name].start == 0 43 | 44 | 45 | def test_multi_two_tasks_lex() -> None: 46 | # the same model, optimize task2 end 47 | pb = ps.SchedulingProblem(name="MultiObjective2", horizon=20) 48 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 49 | task_2 = ps.FixedDurationTask(name="task2", duration=3) 50 | 51 | ps.ConstraintFromExpression(expression=task_1._end == 20 - task_2._start) 52 | 53 | # Maximize only task_2 end 54 | ind = ps.IndicatorFromMathExpression(name="Task2End", expression=task_2._end) 55 | 56 | ps.Objective(name="MaxMultiObj2", target=ind, kind="maximize") 57 | 58 | solution = ps.SchedulingSolver(problem=pb, optimizer="incremental").solve() 59 | 60 | assert solution 61 | assert solution.tasks[task_1.name].start == 0 62 | assert solution.tasks[task_2.name].end == 20 63 | 64 | 65 | def test_multi_two_tasks_optimize_default() -> None: 66 | # the same model, optimize task2 end 67 | pb = ps.SchedulingProblem(name="MultiObjectiveOptimizeDefault", horizon=20) 68 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 69 | task_2 = ps.FixedDurationTask(name="task2", duration=3) 70 | 71 | ps.ConstraintFromExpression(expression=task_1._end == 20 - task_2._start) 72 | 73 | # Maximize only task_2 end 74 | ind = ps.IndicatorFromMathExpression(name="Task2End", expression=task_2._end) 75 | 76 | ps.Objective(name="MinInd", target=ind, kind="maximize") 77 | 78 | solution = ps.SchedulingSolver( 79 | problem=pb, verbosity=2, optimizer="optimize" 80 | ).solve() 81 | 82 | assert solution 83 | assert solution.tasks[task_1.name].start == 0 84 | assert solution.tasks[task_2.name].end == 20 85 | -------------------------------------------------------------------------------- /test/test_optional_constraint.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | 19 | 20 | def test_optional_constraint_start_at_1() -> None: 21 | pb = ps.SchedulingProblem(name="OptionalTaskStartAt1", horizon=6) 22 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 23 | # the following tasks should conflict if they are mandatory 24 | ps.TaskStartAt(task=task_1, value=1, optional=True) 25 | ps.TaskStartAt(task=task_1, value=2, optional=True) 26 | ps.TaskEndAt(task=task_1, value=3, optional=True) 27 | 28 | solver = ps.SchedulingSolver(problem=pb) 29 | solution = solver.solve() 30 | assert solution 31 | 32 | 33 | def test_force_apply_n_optional_constraints() -> None: 34 | pb = ps.SchedulingProblem(name="OptionalTaskStartAt1", horizon=6) 35 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 36 | # the following tasks should conflict if they are mandatory 37 | cstr1 = ps.TaskStartAt(task=task_1, value=1, optional=True) 38 | cstr2 = ps.TaskStartAt(task=task_1, value=2, optional=True) 39 | cstr3 = ps.TaskEndAt(task=task_1, value=3, optional=True) 40 | # force to apply exactly one constraint 41 | ps.ForceApplyNOptionalConstraints( 42 | list_of_optional_constraints=[cstr1, cstr2, cstr3], 43 | nb_constraints_to_apply=1, 44 | ) 45 | 46 | solver = ps.SchedulingSolver(problem=pb) 47 | 48 | solution = solver.solve() 49 | assert solution 50 | -------------------------------------------------------------------------------- /test/test_resource.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | import processscheduler as ps 17 | 18 | from pydantic import ValidationError 19 | import pytest 20 | 21 | 22 | def new_problem_or_clear() -> None: 23 | """clear the current context. If no context is defined, 24 | create a SchedulingProject object""" 25 | ps.SchedulingProblem(name="NewProblem") 26 | 27 | 28 | def test_create_problem_with_horizon() -> None: 29 | pb = ps.SchedulingProblem(name="ProblemWithHorizon", horizon=10) 30 | assert isinstance(pb, ps.SchedulingProblem) 31 | with pytest.raises(ValidationError): 32 | ps.SchedulingProblem(name=4) # name not string 33 | with pytest.raises(ValidationError): 34 | ps.SchedulingProblem(name="NullIntegerHorizon", horizon=0) 35 | with pytest.raises(ValidationError): 36 | ps.SchedulingProblem(name="FloatHorizon", horizon=3.5) 37 | with pytest.raises(ValidationError): 38 | ps.SchedulingProblem(name="NegativeIntegerHorizon", horizon=-2) 39 | 40 | 41 | def test_create_problem_without_horizon() -> None: 42 | pb = ps.SchedulingProblem(name="ProblemWithoutHorizon") 43 | assert isinstance(pb, ps.SchedulingProblem) 44 | 45 | 46 | # 47 | # Workers 48 | # 49 | def test_create_worker_without_problem() -> None: 50 | ps.base.active_problem = None 51 | # no active problem defined, no way to create a resource 52 | with pytest.raises(AssertionError): 53 | ps.Worker(name="Bob") 54 | 55 | 56 | def test_create_worker() -> None: 57 | new_problem_or_clear() 58 | worker = ps.Worker(name="wkr") 59 | assert isinstance(worker, ps.Worker) 60 | with pytest.raises(ValidationError): 61 | ps.Worker(name="WorkerNegativeIntProductivity", productivity=-3) 62 | with pytest.raises(ValidationError): 63 | ps.Worker(name="WorkerFloatProductivity", productivity=3.14) 64 | 65 | 66 | def test_create_select_workers() -> None: 67 | new_problem_or_clear() 68 | worker_1 = ps.Worker(name="wkr_1") 69 | worker_2 = ps.Worker(name="wkr_2") 70 | worker_3 = ps.Worker(name="wkr_3") 71 | single_alternative_workers = ps.SelectWorkers( 72 | list_of_workers=[worker_1, worker_2], nb_workers_to_select=1 73 | ) 74 | assert isinstance(single_alternative_workers, ps.SelectWorkers) 75 | double_alternative_workers = ps.SelectWorkers( 76 | list_of_workers=[worker_1, worker_2, worker_3], nb_workers_to_select=2 77 | ) 78 | assert isinstance(double_alternative_workers, ps.SelectWorkers) 79 | 80 | 81 | def test_select_worker_wrong_number_of_workers() -> None: 82 | new_problem_or_clear() 83 | worker_1 = ps.Worker(name="wkr_1") 84 | worker_2 = ps.Worker(name="wkr_2") 85 | ps.SelectWorkers(list_of_workers=[worker_1, worker_2], nb_workers_to_select=2) 86 | ps.SelectWorkers(list_of_workers=[worker_1, worker_2], nb_workers_to_select=1) 87 | with pytest.raises(ValueError): 88 | ps.SelectWorkers(list_of_workers=[worker_1, worker_2], nb_workers_to_select=3) 89 | with pytest.raises(ValidationError): 90 | ps.SelectWorkers(list_of_workers=[worker_1, worker_2], nb_workers_to_select=-1) 91 | 92 | 93 | def test_select_worker_bad_type() -> None: 94 | new_problem_or_clear() 95 | worker_1 = ps.Worker(name="wkr_1") 96 | assert isinstance(worker_1, ps.Worker) 97 | worker_2 = ps.Worker(name="wkr_2") 98 | with pytest.raises(ValidationError): 99 | ps.SelectWorkers( 100 | list_of_workers=[worker_1, worker_2], nb_workers_to_select=1, kind="ee" 101 | ) 102 | 103 | 104 | def test_worker_same_name() -> None: 105 | new_problem_or_clear() 106 | worker_1 = ps.Worker(name="wkr_1") 107 | assert isinstance(worker_1, ps.Worker) 108 | with pytest.raises(ValueError): 109 | ps.Worker(name="wkr_1") 110 | 111 | 112 | def test_select_worker_same_name() -> None: 113 | new_problem_or_clear() 114 | worker_1 = ps.Worker(name="wkr_1") 115 | assert isinstance(worker_1, ps.Worker) 116 | worker_2 = ps.Worker(name="wkr_2") 117 | ps.SelectWorkers(name="sw1", list_of_workers=[worker_1, worker_2]) 118 | ps.SelectWorkers(name="sw2", list_of_workers=[worker_1, worker_2]) 119 | with pytest.raises(ValueError): 120 | ps.SelectWorkers(name="sw1", list_of_workers=[worker_1, worker_2]) 121 | -------------------------------------------------------------------------------- /test/test_resource_interrupted.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | import pytest 19 | 20 | 21 | def test_resource_interrupted_fixed_duration() -> None: 22 | pb = ps.SchedulingProblem(name="fixed_duration") 23 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 24 | task_2 = ps.FixedDurationTask(name="task2", duration=4) 25 | worker_1 = ps.Worker(name="Worker1") 26 | task_1.add_required_resource(worker_1) 27 | task_2.add_required_resource(worker_1) 28 | ps.ResourceInterrupted(resource=worker_1, list_of_time_intervals=[(1, 3), (6, 8)]) 29 | 30 | ps.ObjectiveMinimizeMakespan() 31 | solver = ps.SchedulingSolver(problem=pb) 32 | solution = solver.solve() 33 | assert solution 34 | assert solution.tasks[task_1.name].start == 3 35 | assert solution.tasks[task_1.name].end == 6 36 | assert solution.tasks[task_2.name].start == 8 37 | assert solution.tasks[task_2.name].end == 12 38 | 39 | 40 | def test_resource_interrupted_variable_duration() -> None: 41 | pb = ps.SchedulingProblem(name="variable_duration") 42 | task_1 = ps.VariableDurationTask(name="task1", min_duration=3) 43 | task_2 = ps.FixedDurationTask(name="task2", duration=4) 44 | ps.TaskStartAt(task=task_1, value=0) # pin to have a more stable outcome 45 | worker_1 = ps.Worker(name="Worker1") 46 | task_1.add_required_resource(worker_1) 47 | task_2.add_required_resource(worker_1) 48 | ps.ResourceInterrupted(resource=worker_1, list_of_time_intervals=[(1, 3), (6, 8)]) 49 | 50 | ps.ObjectiveMinimizeMakespan() 51 | solver = ps.SchedulingSolver(problem=pb) 52 | solution = solver.solve() 53 | assert solution 54 | assert solution.tasks[task_1.name].start == 0 55 | assert solution.tasks[task_1.name].end == 8 56 | assert solution.tasks[task_2.name].start == 8 57 | assert solution.tasks[task_2.name].end == 12 58 | 59 | 60 | def test_resource_interrupted_assignment_assertion() -> None: 61 | ps.SchedulingProblem(name="assignment_assertion") 62 | worker_1 = ps.Worker(name="Worker1") 63 | with pytest.raises(AssertionError): 64 | ps.ResourceInterrupted(resource=worker_1, list_of_time_intervals=[(1, 3)]) 65 | 66 | 67 | def test_resource_cumulative_worker_interrupted_fixed_duration() -> None: 68 | pb = ps.SchedulingProblem(name="fixed_duration") 69 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 70 | task_2 = ps.FixedDurationTask(name="task2", duration=4) 71 | worker_1 = ps.CumulativeWorker(name="Worker1", size=2) 72 | task_1.add_required_resource(worker_1) 73 | task_2.add_required_resource(worker_1) 74 | ps.ResourceInterrupted(resource=worker_1, list_of_time_intervals=[(1, 3), (6, 8)]) 75 | 76 | ps.ObjectiveMinimizeMakespan() 77 | solver = ps.SchedulingSolver(problem=pb) 78 | solution = solver.solve() 79 | assert solution 80 | assert solution.tasks[task_1.name].start == 3 81 | assert solution.tasks[task_1.name].end == 6 82 | assert solution.tasks[task_2.name].start == 8 83 | assert solution.tasks[task_2.name].end == 12 84 | 85 | 86 | def test_resource_cumulative_worker_interrupted_variable_duration() -> None: 87 | pb = ps.SchedulingProblem(name="variable_duration") 88 | task_1 = ps.VariableDurationTask(name="task1", min_duration=3, max_duration=5) 89 | task_2 = ps.VariableDurationTask(name="task2", min_duration=4) 90 | ps.TaskStartAt(task=task_1, value=0) # pin to have a more stable outcome 91 | worker_1 = ps.CumulativeWorker(name="Worker1", size=2) 92 | task_1.add_required_resource(worker_1) 93 | task_2.add_required_resource(worker_1) 94 | ps.ResourceInterrupted(resource=worker_1, list_of_time_intervals=[(1, 3), (6, 8)]) 95 | 96 | ps.ObjectiveMinimizeMakespan() 97 | solver = ps.SchedulingSolver(problem=pb) 98 | solution = solver.solve() 99 | assert solution 100 | assert solution.tasks[task_1.name].start == 0 101 | assert solution.tasks[task_1.name].end == 5 102 | assert solution.tasks[task_2.name].start == 0 103 | assert solution.tasks[task_2.name].end == 6 104 | -------------------------------------------------------------------------------- /test/test_resource_periodically_interrupted.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | import pytest 19 | 20 | 21 | def test_resource_periodically_interrupted_fixed_duration() -> None: 22 | pb = ps.SchedulingProblem(name="fixed_duration") 23 | task_1 = ps.FixedDurationTask(name="task1", duration=2) 24 | task_2 = ps.FixedDurationTask(name="task2", duration=4) 25 | worker_1 = ps.Worker(name="Worker1") 26 | task_1.add_required_resource(worker_1) 27 | task_2.add_required_resource(worker_1) 28 | ps.ResourcePeriodicallyInterrupted( 29 | resource=worker_1, list_of_time_intervals=[(1, 2), (4, 5)], period=10 30 | ) 31 | 32 | ps.ObjectiveMinimizeMakespan() 33 | solver = ps.SchedulingSolver(problem=pb) 34 | solution = solver.solve() 35 | assert solution 36 | assert solution.tasks[task_1.name].start == 2 37 | assert solution.tasks[task_1.name].end == 4 38 | assert solution.tasks[task_2.name].start == 5 39 | assert solution.tasks[task_2.name].end == 9 40 | 41 | 42 | def test_resource_periodically_interrupted_variable_duration() -> None: 43 | pb = ps.SchedulingProblem(name="variable_duration") 44 | task_1 = ps.VariableDurationTask(name="task1", min_duration=3) 45 | task_2 = ps.FixedDurationTask(name="task2", duration=4) 46 | ps.TaskStartAt(task=task_1, value=0) # pin to have a more stable outcome 47 | worker_1 = ps.Worker(name="Worker1") 48 | task_1.add_required_resource(worker_1) 49 | task_2.add_required_resource(worker_1) 50 | ps.ResourcePeriodicallyInterrupted( 51 | resource=worker_1, list_of_time_intervals=[(1, 2), (4, 5)], period=10 52 | ) 53 | 54 | ps.ObjectiveMinimizeMakespan() 55 | solver = ps.SchedulingSolver(problem=pb) 56 | solution = solver.solve() 57 | assert solution 58 | assert solution.tasks[task_1.name].start == 0 59 | assert solution.tasks[task_1.name].end == 4 or solution.tasks[task_1.name].end == 5 60 | assert solution.tasks[task_2.name].start == 5 61 | assert solution.tasks[task_2.name].end == 9 62 | 63 | 64 | def test_resource_periodically_interrupted_assignment_assertion() -> None: 65 | ps.SchedulingProblem(name="assignment_assertion") 66 | worker_1 = ps.Worker(name="Worker1") 67 | with pytest.raises(AssertionError): 68 | ps.ResourcePeriodicallyInterrupted( 69 | resource=worker_1, list_of_time_intervals=[(1, 3)], period=6 70 | ) 71 | 72 | 73 | def test_resource_periodically_interrupted_period_assertion() -> None: 74 | ps.SchedulingProblem(name="period_assertion") 75 | worker_1 = ps.Worker(name="Worker1") 76 | with pytest.raises(AssertionError): 77 | ps.ResourcePeriodicallyInterrupted( 78 | resource=worker_1, list_of_time_intervals=[(1, 2), (3, 5)], period=4 79 | ) 80 | -------------------------------------------------------------------------------- /test/test_resource_unavailable.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | 17 | import processscheduler as ps 18 | 19 | import pytest 20 | 21 | 22 | def test_resource_unavailable_1() -> None: 23 | pb = ps.SchedulingProblem(name="ResourceUnavailable1", horizon=10) 24 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 25 | worker_1 = ps.Worker(name="Worker1") 26 | task_1.add_required_resource(worker_1) 27 | ps.ResourceUnavailable(resource=worker_1, list_of_time_intervals=[(1, 3), (6, 8)]) 28 | 29 | solver = ps.SchedulingSolver(problem=pb) 30 | solution = solver.solve() 31 | assert solution 32 | assert solution.tasks[task_1.name].start == 3 33 | assert solution.tasks[task_1.name].end == 6 34 | 35 | 36 | def test_resource_unavailable_2() -> None: 37 | pb = ps.SchedulingProblem(name="ResourceUnavailable2", horizon=10) 38 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 39 | worker_1 = ps.Worker(name="Worker1") 40 | task_1.add_required_resource(worker_1) 41 | # difference with the first one: build 2 constraints 42 | # merged using a and_ 43 | ps.ResourceUnavailable(resource=worker_1, list_of_time_intervals=[(1, 3)]) 44 | ps.ResourceUnavailable(resource=worker_1, list_of_time_intervals=[(6, 8)]) 45 | 46 | # that should not change the problem solution 47 | solver = ps.SchedulingSolver(problem=pb) 48 | solution = solver.solve() 49 | assert solution 50 | assert solution.tasks[task_1.name].start == 3 51 | assert solution.tasks[task_1.name].end == 6 52 | 53 | 54 | def test_resource_unavailable_3() -> None: 55 | pb = ps.SchedulingProblem(name="ResourceUnavailable3", horizon=10) 56 | task_1 = ps.FixedDurationTask(name="task1", duration=3) 57 | worker_1 = ps.Worker(name="Worker1") 58 | task_1.add_required_resource(worker_1) 59 | # difference with the previous ones: too much unavailability, 60 | # so possible solution 61 | # merged using a and_ 62 | ps.ResourceUnavailable(resource=worker_1, list_of_time_intervals=[(1, 3)]) 63 | ps.ResourceUnavailable(resource=worker_1, list_of_time_intervals=[(5, 8)]) 64 | 65 | # that should not change the problem solution 66 | solver = ps.SchedulingSolver(problem=pb) 67 | solution = solver.solve() 68 | assert not solution 69 | 70 | 71 | def test_resource_unavailable_4() -> None: 72 | pb = ps.SchedulingProblem(name="ResourceUnavailable4") 73 | task_1 = ps.FixedDurationTask(name="task1", duration=4) 74 | worker_1 = ps.Worker(name="Worker1") 75 | task_1.add_required_resource(worker_1) 76 | # create 10 unavailabilities with a length of 3 and try 77 | # to schedule a task with length 4, it should be scheduled 78 | # at the end 79 | maxi = 50 80 | intervs = [(i, i + 3) for i in range(0, maxi, 6)] 81 | 82 | ps.ResourceUnavailable(resource=worker_1, list_of_time_intervals=intervs) 83 | ps.ObjectiveMinimizeMakespan() 84 | 85 | solver = ps.SchedulingSolver(problem=pb) 86 | solution = solver.solve() 87 | assert solution 88 | assert solution.tasks[task_1.name].start == maxi + 1 89 | 90 | 91 | def test_resource_unavailable_raise_issue() -> None: 92 | ps.SchedulingProblem(name="ResourceUnavailableRaiseException", horizon=10) 93 | worker_1 = ps.Worker(name="Worker1") 94 | with pytest.raises(AssertionError): 95 | ps.ResourceUnavailable(resource=worker_1, list_of_time_intervals=[(1, 3)]) 96 | 97 | 98 | def test_resource_unavailable_cumulative_1(): 99 | pb_bs = ps.SchedulingProblem(name="ResourceUnavailableCumulative1", horizon=10) 100 | # tasks 101 | t1 = ps.FixedDurationTask(name="T1", duration=2) 102 | t2 = ps.FixedDurationTask(name="T2", duration=2) 103 | t3 = ps.FixedDurationTask(name="T3", duration=2) 104 | 105 | # workers 106 | r1 = ps.CumulativeWorker(name="Machine1", size=3) 107 | 108 | # resource assignment 109 | t1.add_required_resource(r1) 110 | t2.add_required_resource(r1) 111 | t3.add_required_resource(r1) 112 | 113 | ps.ResourceUnavailable(resource=r1, list_of_time_intervals=[(1, 10)]) 114 | 115 | # plot solution 116 | solver = ps.SchedulingSolver(problem=pb_bs) 117 | solution = solver.solve() 118 | assert not solution 119 | 120 | 121 | def test_resource_unavailable_cumulative_2(): 122 | # same as the previous one, but this time there should be one and only 123 | # one solution 124 | pb_bs = ps.SchedulingProblem(name="ResourceUnavailableCumulative1", horizon=12) 125 | # tasks 126 | t1 = ps.FixedDurationTask(name="T1", duration=2) 127 | t2 = ps.FixedDurationTask(name="T2", duration=2) 128 | t3 = ps.FixedDurationTask(name="T3", duration=2) 129 | 130 | # workers 131 | r1 = ps.CumulativeWorker(name="Machine1", size=3) 132 | 133 | # resource assignment 134 | t1.add_required_resource(r1) 135 | t2.add_required_resource(r1) 136 | t3.add_required_resource(r1) 137 | 138 | ps.ResourceUnavailable(resource=r1, list_of_time_intervals=[(1, 10)]) 139 | 140 | # plot solution 141 | solver = ps.SchedulingSolver(problem=pb_bs) 142 | solution = solver.solve() 143 | assert solution.tasks[t1.name].start == 10 144 | assert solution.tasks[t2.name].start == 10 145 | assert solution.tasks[t3.name].start == 10 146 | -------------------------------------------------------------------------------- /test/test_util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021 Thomas Paviot (tpaviot@gmail.com) 2 | # 3 | # This file is part of ProcessScheduler. 4 | # 5 | # This program is free software: you can redistribute it and/or modify it under 6 | # the terms of the GNU General Public License as published by the Free Software 7 | # Foundation, either version 3 of the License, or (at your option) any later 8 | # version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but WITHOUT 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | import random 17 | 18 | from processscheduler.util import ( 19 | calc_parabola_from_three_points, 20 | sort_no_duplicates, 21 | sort_duplicates, 22 | clean_buffer_levels, 23 | get_minimum, 24 | get_maximum, 25 | ) 26 | 27 | import pytest 28 | import z3 29 | 30 | 31 | def test_calc_parabola_from_three_points(): 32 | a, b, c = calc_parabola_from_three_points([0, 1, 2], [0, 2, 4]) 33 | assert [a, b, c] == [0, 2, 0] 34 | d, e, f = calc_parabola_from_three_points([0, 1, 2], [0, 1, 4]) 35 | assert [d, e, f] == [1, 0, 0] 36 | 37 | 38 | def test_sort_no_duplicates(): 39 | """sort an array of 20 different integers""" 40 | lst_to_sort = random.sample(range(-100, 100), 20) 41 | sorted_variables, assertions = sort_no_duplicates(lst_to_sort) 42 | s = z3.Solver() 43 | s.add(assertions) 44 | result = s.check() 45 | assert result == z3.sat 46 | solution = s.model() 47 | sorted_integers = [solution[v].as_long() for v in sorted_variables] 48 | assert sorted(lst_to_sort) == sorted_integers 49 | 50 | 51 | def test_sort_duplicates(): 52 | """sort an array of 20 integers with only 10 different""" 53 | lst_to_sort = random.sample(range(-100, 100), 10) * 2 54 | sorted_variables, assertions = sort_duplicates(lst_to_sort) 55 | s = z3.Solver() 56 | s.add(assertions) 57 | result = s.check() 58 | assert result == z3.sat 59 | solution = s.model() 60 | sorted_integers = [solution[v].as_long() for v in sorted_variables] 61 | assert sorted(lst_to_sort) == sorted_integers 62 | 63 | 64 | def test_clean_buffer_levels(): 65 | assert clean_buffer_levels([100, 21, 21, 21], [7, 7, 7]) == ([100, 21], [7]) 66 | 67 | 68 | def test_get_maximum(): 69 | # 20 integers between 0 and 99 70 | lst = random.sample(range(100), k=20) 71 | # append 101, which must be the maximum 72 | lst.append(101) 73 | # build a list of z3 ints 74 | z3_lst = z3.Ints(" ".join([f"i{elem}" for elem in lst])) 75 | maxi = z3.Int("maxi") 76 | # find maximum 77 | assertions = get_maximum(maxi, z3_lst) 78 | # add assertions 79 | s = z3.Solver() 80 | s.add(assertions) 81 | s.add([a == b for a, b in zip(lst, z3_lst)]) 82 | s.add(maxi == 101) 83 | assert s.check() == z3.sat 84 | 85 | 86 | def test_get_maximum_empty_list(): 87 | maxi = z3.Int("maxi2") 88 | # find maximum 89 | with pytest.raises(AssertionError): 90 | get_maximum(maxi, []) 91 | with pytest.raises(AssertionError): 92 | get_maximum(maxi, None) 93 | 94 | 95 | def test_get_minimum(): 96 | # 20 integers between 10 and 99 97 | lst = random.sample(range(10, 100), k=20) 98 | # append 5, which must be the minimum 99 | lst.append(5) 100 | # build a list of z3 ints 101 | z3_lst = z3.Ints(" ".join([f"i{elem}" for elem in lst])) 102 | mini = z3.Int("mini") 103 | # find maximum 104 | assertions = get_minimum(mini, z3_lst) 105 | # add assertions 106 | s = z3.Solver() 107 | s.add(assertions) 108 | s.add([a == b for a, b in zip(lst, z3_lst)]) 109 | s.add(mini == 5) 110 | assert s.check() == z3.sat 111 | 112 | 113 | def test_get_minimum_empty_list(): 114 | mini = z3.Int("mini2") 115 | with pytest.raises(AssertionError): 116 | get_maximum(mini, []) 117 | with pytest.raises(AssertionError): 118 | get_maximum(mini, None) 119 | 120 | 121 | def test_clean_buffers_different_sizes() -> None: 122 | with pytest.raises(AssertionError): 123 | clean_buffer_levels([1, 2, 3], [1, 2, 3]) 124 | --------------------------------------------------------------------------------