├── test
├── __init__.py
├── test_optional_constraint.py
├── test_datetime.py
├── test_dynamic_resource.py
├── test_function.py
├── test_resource_periodically_interrupted.py
├── test_multiple_objectives.py
├── test_util.py
├── test_io.py
├── test_resource_interrupted.py
├── test_group_of_tasks.py
├── test_resource.py
├── test_resource_unavailable.py
└── test_cumulative.py
├── processscheduler
├── __main__.py
├── indicator_constraint.py
├── __init__.py
├── function.py
├── buffer.py
├── constraint.py
├── base.py
├── first_order_logic.py
├── util.py
└── excel_io.py
├── docs
├── blog
│ ├── index.md
│ ├── .meta.yml
│ ├── .authors.yml
│ └── posts
│ │ └── first-blog-post.md
├── data_exchange.md
├── img
│ ├── ps_232_gantt.png
│ ├── TwoTasksConstraint.drawio
│ ├── flow_shop_problem.png
│ ├── gantta_pinedo_232.png
│ ├── f1_yb_screencapture.jpg
│ ├── flow_shop_solution.png
│ ├── software-development-gantt.png
│ ├── pinedo_2_3_2_precedence_graph.png
│ ├── TimeLineHorizon.drawio
│ ├── Task.drawio
│ ├── TasksEndSynced.svg
│ └── TasksStartSynced.svg
├── javascripts
│ └── tablesort.js
├── download_install.md
├── run.md
├── customized_constraints.md
├── indicator_constraints.md
├── features.md
├── function.md
├── inside.md
├── gantt_chart.md
├── workflow.md
├── index.md
├── indicator.md
├── first_order_logic_constraints.md
├── resource.md
├── buffer.md
├── use-case-software-development.md
├── resource_constraints.md
├── solving.md
├── scheduling_problem.md
└── resource_assignment.md
├── MANIFEST.in
├── .mypy.ini
├── examples-notebooks
├── pics
│ └── hello_world_gantt.png
└── excavator-use-case
│ ├── huge_hole.jpg
│ ├── small_hole.jpeg
│ ├── medium_hole.jpeg
│ ├── excavator_small.jpeg
│ └── excavator_medium_size.jpg
├── requirements.txt
├── .readthedocs.yml
├── azure-pipelines.yml
├── environment.yml
├── .github
└── workflows
│ └── mkdoc.yml
├── .gitignore
├── pyproject.toml
├── setup.py
├── TODO
├── template.yml
├── CONTRIBUTING.md
├── NOTES
├── README.md
├── benchmark
├── benchmark_n_queens.py
├── benchmark_dev_team.py
├── benchmark_mixed.py
└── benchmark_logics.py
└── mkdocs.yml
/test/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/processscheduler/__main__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/blog/index.md:
--------------------------------------------------------------------------------
1 | # Blog
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE.txt
2 |
--------------------------------------------------------------------------------
/docs/data_exchange.md:
--------------------------------------------------------------------------------
1 | # Data Exchange
2 |
--------------------------------------------------------------------------------
/.mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | ignore_missing_imports = True
3 |
--------------------------------------------------------------------------------
/docs/blog/.meta.yml:
--------------------------------------------------------------------------------
1 | comments: true
2 | hide:
3 | - feedback
4 |
--------------------------------------------------------------------------------
/docs/img/ps_232_gantt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/docs/img/ps_232_gantt.png
--------------------------------------------------------------------------------
/docs/img/TwoTasksConstraint.drawio:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/flow_shop_problem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/docs/img/flow_shop_problem.png
--------------------------------------------------------------------------------
/docs/img/gantta_pinedo_232.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/docs/img/gantta_pinedo_232.png
--------------------------------------------------------------------------------
/docs/img/f1_yb_screencapture.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/docs/img/f1_yb_screencapture.jpg
--------------------------------------------------------------------------------
/docs/img/flow_shop_solution.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/docs/img/flow_shop_solution.png
--------------------------------------------------------------------------------
/docs/img/software-development-gantt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/docs/img/software-development-gantt.png
--------------------------------------------------------------------------------
/docs/img/pinedo_2_3_2_precedence_graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/docs/img/pinedo_2_3_2_precedence_graph.png
--------------------------------------------------------------------------------
/examples-notebooks/pics/hello_world_gantt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/examples-notebooks/pics/hello_world_gantt.png
--------------------------------------------------------------------------------
/examples-notebooks/excavator-use-case/huge_hole.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/examples-notebooks/excavator-use-case/huge_hole.jpg
--------------------------------------------------------------------------------
/examples-notebooks/excavator-use-case/small_hole.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/examples-notebooks/excavator-use-case/small_hole.jpeg
--------------------------------------------------------------------------------
/examples-notebooks/excavator-use-case/medium_hole.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/examples-notebooks/excavator-use-case/medium_hole.jpeg
--------------------------------------------------------------------------------
/examples-notebooks/excavator-use-case/excavator_small.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/examples-notebooks/excavator-use-case/excavator_small.jpeg
--------------------------------------------------------------------------------
/examples-notebooks/excavator-use-case/excavator_medium_size.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpaviot/ProcessScheduler/master/examples-notebooks/excavator-use-case/excavator_medium_size.jpg
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 | 
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 | 
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 | 
58 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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=
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # ProcessScheduler - A python framework for scheduling and resource allocation
2 |
3 | [](https://www.codacy.com/gh/tpaviot/ProcessScheduler/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tpaviot/ProcessScheduler&utm_campaign=Badge_Grade)
4 | [](https://codecov.io/gh/tpaviot/ProcessScheduler)
5 | [](https://dev.azure.com/tpaviot/ProcessScheduler/_build?definitionId=9)
6 | [](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=examples-notebooks)
7 | [](https://badge.fury.io/py/ProcessScheduler)
8 | [](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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ProcessScheduler
2 |
3 | [](https://www.codacy.com/gh/tpaviot/ProcessScheduler/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tpaviot/ProcessScheduler&utm_campaign=Badge_Grade)
4 | [](https://codecov.io/gh/tpaviot/ProcessScheduler)
5 | [](https://dev.azure.com/tpaviot/ProcessScheduler/_build?definitionId=9)
6 | [](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=examples-notebooks)
7 | [](https://badge.fury.io/py/ProcessScheduler)
8 | [](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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 | { width="100%" }
107 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/img/TasksEndSynced.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/img/TasksStartSynced.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/use-case-software-development.md:
--------------------------------------------------------------------------------
1 | # Use case: software development
2 |
3 | [](https://mybinder.org/v2/gh/tpaviot/ProcessScheduler/HEAD?filepath=doc/use-case-software-development.ipynb)
4 |
5 |
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 | 
119 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | { 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | { 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 | { 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$.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------