-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: port flight-crew-scheduling quickstart example from Java to Py…
…thon
- Loading branch information
1 parent
409aa53
commit ca25c4c
Showing
22 changed files
with
2,008 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
= Flight Crew Scheduling (Python) | ||
|
||
Assign crew to flights to produce a better schedule for flight assignments. | ||
|
||
image::./flight-crew-scheduling-screenshot.png[] | ||
|
||
* <<prerequisites,Prerequisites>> | ||
* <<run,Run the application>> | ||
* <<test,Test the application>> | ||
[[prerequisites]] | ||
== Prerequisites | ||
|
||
. Install https://www.python.org/downloads/[Python 3.11+] | ||
|
||
. Install JDK 17+, for example with https://sdkman.io[Sdkman]: | ||
+ | ||
---- | ||
$ sdk install java | ||
---- | ||
|
||
[[run]] | ||
== Run the application | ||
|
||
. Git clone the timefold-quickstarts repo and navigate to this directory: | ||
+ | ||
[source, shell] | ||
---- | ||
$ git clone https://github.com/TimefoldAI/timefold-quickstarts.git | ||
... | ||
$ cd timefold-quickstarts/python/flight-crew-scheduling | ||
---- | ||
|
||
. Create a virtual environment | ||
+ | ||
[source, shell] | ||
---- | ||
$ python -m venv .venv | ||
---- | ||
|
||
. Activate the virtual environment | ||
+ | ||
[source, shell] | ||
---- | ||
$ . .venv/bin/activate | ||
---- | ||
|
||
. Install the application | ||
+ | ||
[source, shell] | ||
---- | ||
$ pip install -e . | ||
---- | ||
|
||
. Run the application | ||
+ | ||
[source, shell] | ||
---- | ||
$ run-app | ||
---- | ||
|
||
. Visit http://localhost:8080 in your browser. | ||
|
||
. Click on the *Solve* button. | ||
|
||
|
||
[[test]] | ||
== Test the application | ||
|
||
. Run tests | ||
+ | ||
[source, shell] | ||
---- | ||
$ pytest | ||
---- | ||
|
||
== More information | ||
|
||
Visit https://timefold.ai[timefold.ai]. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
[loggers] | ||
keys=root,timefold_solver | ||
|
||
[handlers] | ||
keys=consoleHandler | ||
|
||
[formatters] | ||
keys=simpleFormatter | ||
|
||
[logger_root] | ||
level=INFO | ||
handlers=consoleHandler | ||
|
||
[logger_timefold_solver] | ||
level=INFO | ||
qualname=timefold.solver | ||
handlers=consoleHandler | ||
propagate=0 | ||
|
||
[handler_consoleHandler] | ||
class=StreamHandler | ||
level=INFO | ||
formatter=simpleFormatter | ||
args=(sys.stdout,) | ||
|
||
[formatter_simpleFormatter] | ||
class=uvicorn.logging.ColourizedFormatter | ||
format={levelprefix:<8} @ {name} : {message} | ||
style={ | ||
use_colors=True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" | ||
|
||
[project] | ||
name = "flight_crew_scheduling" | ||
version = "1.0.0" | ||
requires-python = ">=3.11" | ||
dependencies = [ | ||
'timefold == 999-dev0', | ||
'fastapi == 0.111.0', | ||
'pydantic == 2.7.3', | ||
'uvicorn == 0.30.1', | ||
'pytest == 8.2.2', | ||
'httpx == 0.27.0', | ||
] | ||
|
||
|
||
[project.scripts] | ||
run-app = "flight_crew_scheduling:main" |
16 changes: 16 additions & 0 deletions
16
python/flight-crew-scheduling/src/flight_crew_scheduling/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import uvicorn | ||
|
||
from .rest_api import app | ||
|
||
|
||
def main(): | ||
config = uvicorn.Config("flight_crew_scheduling:app", | ||
port=8080, | ||
log_config="logging.conf", | ||
use_colors=True) | ||
server = uvicorn.Server(config) | ||
server.run() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
87 changes: 87 additions & 0 deletions
87
python/flight-crew-scheduling/src/flight_crew_scheduling/constraints.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
from timefold.solver.score import * | ||
from datetime import time | ||
from typing import Final | ||
|
||
from .domain import * | ||
|
||
|
||
@constraint_provider | ||
def define_constraints(constraint_factory: ConstraintFactory): | ||
return [ | ||
required_skill(constraint_factory), | ||
flight_conflict(constraint_factory), | ||
transfer_between_two_flights(constraint_factory), | ||
employee_unavailability(constraint_factory), | ||
first_assignment_not_departing_from_home(constraint_factory), | ||
last_assignment_not_arriving_at_home(constraint_factory) | ||
] | ||
|
||
|
||
def required_skill(constraint_factory: ConstraintFactory) -> Constraint: | ||
return (constraint_factory | ||
.for_each(FlightAssignment) | ||
.filter(lambda fa: not fa.has_required_skills()) | ||
.penalize(HardSoftScore.of_hard(100)) | ||
.as_constraint("Required skill")) | ||
|
||
|
||
def flight_conflict(constraint_factory: ConstraintFactory) -> Constraint: | ||
return (constraint_factory | ||
.for_each_unique_pair(FlightAssignment, | ||
Joiners.equal(lambda fa: fa.employee), | ||
Joiners.overlapping(lambda fa: fa.flight.departure_utc_date_time, | ||
lambda fa: fa.flight.arrival_utc_date_time)) | ||
.penalize(HardSoftScore.of_hard(10)) | ||
.as_constraint("Flight conflict")) | ||
|
||
|
||
def transfer_between_two_flights(constraint_factory: ConstraintFactory) -> Constraint: | ||
return (constraint_factory | ||
.for_each(FlightAssignment) | ||
.join(FlightAssignment, | ||
Joiners.equal(lambda fa: fa.employee), | ||
Joiners.less_than(lambda fa: fa.get_departure_utc_date_time()), | ||
Joiners.filtering(lambda fa1, fa2: fa1.id != fa2.id)) | ||
.if_not_exists(FlightAssignment, | ||
Joiners.equal(lambda fa1, fa2: fa1.employee, lambda fa2: fa2.employee), | ||
Joiners.filtering( | ||
lambda fa1, fa2, other_fa: other_fa.id != fa1.id and other_fa.id != fa2.id and | ||
other_fa.get_departure_utc_date_time() >= fa1.get_departure_utc_date_time() and | ||
other_fa.get_departure_utc_date_time() < fa2.get_departure_utc_date_time())) | ||
.filter(lambda fa1, fa2: fa1.flight.arrival_airport != fa2.flight.departure_airport) | ||
.penalize(HardSoftScore.of_hard(1)) | ||
.as_constraint("Transfer between two flights")) | ||
|
||
|
||
def employee_unavailability(constraint_factory: ConstraintFactory) -> Constraint: | ||
return (constraint_factory | ||
.for_each(FlightAssignment) | ||
.filter(lambda fa: fa.is_unavailable_employee()) | ||
.penalize(HardSoftScore.of_hard(10)) | ||
.as_constraint("Employee unavailable")) | ||
|
||
|
||
def first_assignment_not_departing_from_home(constraint_factory: ConstraintFactory) -> Constraint: | ||
return (constraint_factory | ||
.for_each(Employee) | ||
.join(FlightAssignment, Joiners.equal(lambda emp: emp, lambda fa: fa.employee)) | ||
.if_not_exists(FlightAssignment, | ||
Joiners.equal(lambda emp, fa: emp, lambda fa: fa.employee), | ||
Joiners.greater_than(lambda emp, fa: fa.get_departure_utc_date_time(), | ||
lambda fa: fa.get_departure_utc_date_time())) | ||
.filter(lambda emp, fa: emp.home_airport != fa.flight.departure_airport) | ||
.penalize(HardSoftScore.of_soft(1000)) | ||
.as_constraint("First assignment not departing from home")) | ||
|
||
|
||
def last_assignment_not_arriving_at_home(constraint_factory: ConstraintFactory) -> Constraint: | ||
return (constraint_factory | ||
.for_each(Employee) | ||
.join(FlightAssignment, Joiners.equal(lambda emp: emp, lambda fa: fa.employee)) | ||
.if_not_exists(FlightAssignment, | ||
Joiners.equal(lambda emp, fa: emp, lambda fa: fa.employee), | ||
Joiners.less_than(lambda emp, fa: fa.get_departure_utc_date_time(), | ||
lambda fa: fa.get_departure_utc_date_time())) | ||
.filter(lambda emp, fa: emp.home_airport != fa.flight.arrival_airport) | ||
.penalize(HardSoftScore.of_soft(1000)) | ||
.as_constraint("Last assignment not arriving at home")) |
Oops, something went wrong.