Skip to content

Commit

Permalink
chore: port flight-crew-scheduling quickstart example from Java to Py…
Browse files Browse the repository at this point in the history
…thon
  • Loading branch information
PatrickDiallo23 committed Jan 27, 2025
1 parent 409aa53 commit ca25c4c
Show file tree
Hide file tree
Showing 22 changed files with 2,008 additions and 0 deletions.
79 changes: 79 additions & 0 deletions python/flight-crew-scheduling/README.adoc
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.
30 changes: 30 additions & 0 deletions python/flight-crew-scheduling/logging.conf
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
20 changes: 20 additions & 0 deletions python/flight-crew-scheduling/pyproject.toml
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"
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()
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"))
Loading

0 comments on commit ca25c4c

Please sign in to comment.