Even distribution of directed acyclic graph with respect to edge length

Viewed 214

Problem

I have a set of events, some of them connected and these connections define order. Events must be held in defined order. A connection may contain a min and/or a max requirement for distance between connected events. Let the distance be in days.

I use a directed acyclic graph for a representation of my model.

I need to order this events on the given number of days respecting the defined order and min/max requirements. The distribution should tend to be even. The events should be stretched all over the given distance.

What ways or algorithms may you suggest on solving this problem? I tried to find some solution with topological sorting or constraint ordering but had little to no results.

Example

We have a set of events a, b, c with the following connections a -> b, b -> c, a -> c

The given number of days for distribution is 7.

a. without any requirements for distance between connections.

Then the best solution would be

1  2  3  4  5  6  7
a        b        c

b. with requirement where distance in days between events (a, b) is [1, 2].

Then the best solution would be

1  2  3  4  5  6  7
a     b           c

c. with requirements where distance in days between events (a, b) is [1, 2] and between events (a, c) is <= 4.

Then the best solution would be

1  2  3  4  5  6  7
a     b     c      

d. with requirements where distance in days between events (a, b) is [1, 2], between events (a, c) is <= 4, between events (b, c) is >= 3.

Then the best solution would be

1  2  3  4  5  6  7
a  b        c      

EDIT: There may be multiple events per day:

  1. if the number of events is greater than the number of days for distribution.
  2. if we have have max = 0 requirement.

If we have several suitable solutions, then the best one will be where the distance between the current event and its neighbors is approximately the same. We aim for the distance between events to be (DAYS_FOR_DISTRIBUTION / NUMBER_OF_EVENTS) where DAYS_FOR_DISTRIBUTION > NUMBER_OF_EVENTS. If we have several suitable solutions with the same distances between events, then the best is left shifted solution.

Examples of connected events are attached below First example


Second example

2 Answers
Find R = all events that have no in edges
LOOP while R contains one or more event
   SELECT N from R with the largest number of out edges
   IF first time through
      Place N on day 1
   ELSE
      Place N in middle of largest gap between events
   LOOP M over descendants of N in required order
      Place M as far from other events as possible, within M's allowed range

Method.

Uses a constraint library to generate all event orderings satisfying the event constraints, without regard to (un)evenness, then finds the constrained solution with minimal unevenness iteratively.

Evenness ?

If unevenness is defined by looking at all the differences between days and event_days calculated and returning the max minus the min days difference then a need for clarification with your answer of:

1  2  3  4  5  6  7
a  b        c      

Is that it has the values off to the left w.r.t. the days.

A better answer might be:

1  2  3  4  5  6  7
   a  b        c      

With the one shift to the right, their is less of a stretch from day 7 to any event day.

If you think the above is equivalent then you need to better define evenness - is it evennness over the extent of the event days perhaps?

STOP PRESS! Evenness has been edited by author and is now better described.

Code

The source is set to run your example if you just hit return on each prompt.

# -*- coding: utf-8 -*-
"""
Even distribution of directed acyclic graph with respect to edge length
https://stackoverflow.com/questions/71532362/even-distribution-of-directed-acyclic-graph-with-respect-to-edge-length

Created on Sat Mar 26 08:53:19 2022

@author: paddy
"""

# https://pypi.org/project/python-constraint/
from constraint import Problem
from itertools import product


#%% Inputs
days = input("Days: int = ")
try:
    days = int(days.strip())
except:
    days = 7
print(f"Using {days=}")
events = input("events: space_separated = ")
events = events.strip().split()
if not events:
    events = list("abc")
print(f"Using {events=}")

constraint_funcs = []
while True:
    constr = input("constraint: string_expression (. to end) = ").strip()
    if not constr:
        constraint_funcs = ["1 <= (b - a) <=2",
                            "(c - a) <= 4",
                            "(c - b) >= 3"]
        break
    if constr == '.':
        break
    constraint_funcs.append(constr)
print(f"\nUsing {constraint_funcs=}")


#%% Constraint Setup
print()
problem = Problem()

problem.addVariables(events, range(1, days+1))
for constr in constraint_funcs:
    constr_events = sorted( set(events) & set(compile(constr, '<input>',
                                              mode='eval').co_names))
    expr = f"lambda {', '.join(constr_events)}: {constr}"
    print(f"  Add Constraint {expr!r}, On {constr_events}")
    func = eval(expr)
    problem.addConstraint(func, constr_events)

#%% Solution optimisation for "evenness"
print()

def unevenness(event_days: list[int], all_days: int):
    "(Max - min diff between ordered events, leftmost event)"
    maxdiff, mindiff =  -1, all_days + 1
    for event, next_event in zip(event_days, event_days[1:]):
        diff = next_event - event
        maxdiff = max(maxdiff, diff)
        mindiff = min(mindiff, diff)
    return maxdiff - mindiff, event_days[0]

def printit(solution, all_days):
    drange = range(1, all_days+1)
    # print(solution)
    print('  '.join(str(i)[0] for i in drange))
    for event, day in sorted(solution.items(), key=lambda kv: kv[::-1]):
        print('   ' * (day - 1) + event )
    print()

current_best = None, 9e99
for ans in problem.getSolutionIter():
    unev = unevenness(sorted(ans.values()), days)
    if current_best[0] is None or unev < current_best[1]:
        current_best = ans, unev
        print("Best so far:")
        printit(ans, days)

The unevenness function has been updated. A better function might be to minimise the standard deviations of the days between successive events, but for this example, this works.

Output

Sample run using your constraints, but longer event names.

Days: int = 7
Using days=7

events: space_separated = arch belt card
Using events=['arch', 'belt', 'card']

constraint: string_expression (. to end) = 1 <= (belt - arch) <= 2

constraint: string_expression (. to end) = (card - arch) <= 4

constraint: string_expression (. to end) = (card - belt) >= 3

constraint: string_expression (. to end) = .

Using constraint_funcs=['1 <= (belt - arch) <= 2', '(card - arch) <= 4', '(card - belt) >= 3']

  Add Constraint 'lambda arch, belt: 1 <= (belt - arch) <= 2', On ['arch', 'belt']
  Add Constraint 'lambda arch, card: (card - arch) <= 4', On ['arch', 'card']
  Add Constraint 'lambda belt, card: (card - belt) >= 3', On ['belt', 'card']

Best so far:
1  2  3  4  5  6  7
      arch
         belt
                  card

Best so far:
1  2  3  4  5  6  7
   arch
      belt
               card

Best so far:
1  2  3  4  5  6  7
arch
   belt
            card

Note: The first letter of event names align with the day columns.

The second item in the return tuple of the unevenness function is the day that the earliest, (left-most), event happens. if the spread of the events are equal, this will tend to favour the solution further to the left when minimised.

Related