0

I am writing a Nurse-Patient assignment algorithm in Pyomo, but I am having trouble dynamically computing the walking distance between assigned patient beds for each nurse. The relevant sets and variables are below with a small example. The variables are filled in with the values that I would want at the end of solving.

#Sets
model.PatientIDs  = {0, 1, 2}
model.NurseIDs = {a, b}

#Indexed parameter (indexed by (patient1, patient2))
# Stores the distance between each patient bed  
model.Bed_Distances = {(0, 0) : 0, (0, 1) : 6.4, (0, 2) : 7.2, (1, 0) : 6.4, (1, 1) : 0, (1, 2) : 1.9, (2, 1) : 1.9), (2, 0) : 7.2}

# Indexed variable (indexed by (patient, nurse))
model.Assignments = {(0, a): 1, (0, b): 0, (1, a) : 0, (1, b) : 1, (2, a) : 1, (2, b) : 0}

# Indexed variable (indexed by nurse)
# Keeps track of the distance between assigned patient beds for each nurse
model.Nurse_Distances = {a : 7.2, b : 0}

I'm having trouble computing the nurse distances dynamically as the model is being solved (since it is dependent on the patients assigned to each nurse). Since the model.ASSIGNMENTS decision variable is represented with binary 0 or 1, I've been using this rule to compute the nurse distances:

def nurse_distance(self, nurse):
    return model.NURSE_DISTANCE[nurse] == sum([model.ASSIGNMENTS[p1, nurse] * model.ASSIGNMENTS[p2, nurse] * model.BED_DISTANCES[p1, p2] for p1 in model.PATIENTS for p2 in model.PATIENTS])

model.DISTANCE_CONSTRAINT = pe.Constraint(model.NURSES, rule = nurse_distance)

After adding this constraint, my model runs forever, or when I run it with only 2 or 3 patients it finishes relatively quickly and gives me an error that looks like this:

ERROR: Error parsing NEOS solution file NEOS log: Job 11992161 dispatched password: iGYfJtMj ---------- Begin Solver Output ----------packages/pyomo/opt/plugins/sol.py", line 87, in _load raise ValueError("no Options line found") ValueError: no Options line found

Is there a better way to keep track of a variable that is dependent on the value of another variable in Pyomo (nurse bed distances is dependent on the assignments made between nurses and patients)? If not, is there a more computationally efficient way to compute it within the constraint I've written without using "if then" statements?

  • TLDR; Whittle this down to the 1 thing you are having trouble with in a reproducible example that has a bit of data. Asking someone for a full review is a lot of work. Please clearly frame your question with a *minimal reproducible example.* – AirSquid Jun 02 '22 at 23:26
  • Thank you for the advice! I tried to express my problem more succinctly. – Nurse_Patient_Optimizer Jun 03 '22 at 04:19
  • setting this up for 2 patients would be pretty straightforward, but if there are 3 or more, what is your plan? You have no sense of sequencing here, so the "simple approach" of adding all distances if you served patients {0, 4, 6, 9} would include all 6 combinations, which may/may not make sense. – AirSquid Jun 03 '22 at 20:49
  • This is in fact the metric I want, though. I want the distances between all 6 combinations of beds, since the nurse may need to walk between beds in any arbitrary order. Right now, my Bed_Distances parameter is already precomputed before the model is solved and stores the pairwise distances between every patient. My problem is updating the nurse total distance based on the patients assigned so far, which requires me to loop between all the patients twice to see if a nurse has been assigned to both (arbitrary) patient p1 and patient p2 – Nurse_Patient_Optimizer Jun 03 '22 at 21:04
  • The issue is looping between all the patients twice seems to be very inefficient and causes my model to either time out or get the above error, which is why I was exploring the option of maintaining lists of patients assigned to each nurse. However, you told me in another post this would be a nonlinear operation that is not supported by Pyomo. Do you think I should explore constrained non-linear optimization in Python? – Nurse_Patient_Optimizer Jun 03 '22 at 21:08
  • there's something else going on here. Unless you have 100,000 nurses, there's no way that construction is the problem. You know you are making the problem non-linear by multiplying the 2 variables together....? Let me gin up a quick example... – AirSquid Jun 03 '22 at 21:40

1 Answers1

0

You are creating a non-linear model by multiplying your decision variables together, which is probably not desired...

Because you have an "and" condition here (that the same nurse is assigned to p1 and p2) you almost certainly need another binary "helper" variable to track whether both are true, then use that. Of course, you will need a linking constraint between the individual assignments and the paired assignment. See the example below... Note that "Todd" gets off easy here because he only has 1 patient.

Code:

# Nurse Patrol

import pyomo.environ as pyo

# data
PatientIDs  = [0, 1, 2]
NurseIDs = ['Todd', 'Cindy']
Bed_Distances = {(0, 0) : 0, (0, 1) : 6.4, (0, 2) : 7.2, (1, 0) : 6.4, (1, 1) : 0, (1, 2) : 1.9, (2, 1) : 1.9, (2, 0) : 7.2}
patient_pairings = [(p1, p2) for p1 in PatientIDs for p2 in PatientIDs if p1 != p2]
# model
m = pyo.ConcreteModel('Nurse Patrol')
# put the stuff above into model parameters.  You can use them "raw" but this will help you T/S the model

# SETS
m.N = pyo.Set(initialize=NurseIDs)
m.P = pyo.Set(initialize=PatientIDs)
m.PP = pyo.Set(initialize=patient_pairings)

# PARAMS
m.distance = pyo.Param(m.P, m.P, initialize=Bed_Distances)

# VARIABLES
m.assign =          pyo.Var(m.N, m.P, domain=pyo.Binary)
m.pair_assigned =   pyo.Var(m.N, m.PP, domain=pyo.Binary)   # true if BOTH are assigned to nurse

# OBJ:  Minimize the total of all nurses' travels
m.obj = pyo.Objective(expr=sum(m.pair_assigned[n, p1, p2]*m.distance[p1, p2] 
                            for (p1, p2) in m.PP
                            for n in m.N))
# CONSTRAINTS
# Cover all patients
def covered(m, patient):
    return sum(m.assign[n, patient] for n in m.N) >= 1
m.C1 = pyo.Constraint(m.P, rule=covered)

# link the assignment to the paired assignment
def paired(m, n, p1, p2):
    return m.pair_assigned[n, p1, p2] >= m.assign[n, p1] + m.assign[n, p2] - 1
m.C2 = pyo.Constraint(m.N, m.PP, rule=paired)

result = pyo.SolverFactory('glpk').solve(m)
print(result)
m.assign.display()

for n in m.N:
    tot_dist = sum(m.pair_assigned[n, p1, p2]*m.distance[p1, p2] for (p1, p2) in m.PP)
    print(f'nurse {n} distance is: {pyo.value(tot_dist)}')

Yields:

Problem: 
- Name: unknown
  Lower bound: 3.8
  Upper bound: 3.8
  Number of objectives: 1
  Number of constraints: 16
  Number of variables: 19
  Number of nonzeros: 43
  Sense: minimize
Solver: 
- Status: ok
  Termination condition: optimal
  Statistics: 
    Branch and bound: 
      Number of bounded subproblems: 3
      Number of created subproblems: 3
  Error rc: 0
  Time: 0.007218837738037109
Solution: 
- number of solutions: 0
  number of solutions displayed: 0

assign : Size=6, Index=assign_index
    Key          : Lower : Value : Upper : Fixed : Stale : Domain
    ('Cindy', 0) :     0 :   0.0 :     1 : False : False : Binary
    ('Cindy', 1) :     0 :   1.0 :     1 : False : False : Binary
    ('Cindy', 2) :     0 :   1.0 :     1 : False : False : Binary
     ('Todd', 0) :     0 :   1.0 :     1 : False : False : Binary
     ('Todd', 1) :     0 :   0.0 :     1 : False : False : Binary
     ('Todd', 2) :     0 :   0.0 :     1 : False : False : Binary
nurse Todd distance is: 0.0
nurse Cindy distance is: 3.8
[Finished in 558ms]
AirSquid
  • 10,214
  • 2
  • 7
  • 31
  • Thank you so much for this detailed response. I was able to implement this functionality and it works quite well for small toy examples (<15 patients and <5 nurses). However, I'm having trouble scaling this up for larger models (>70 patients and >25 nurses) due to the looping in the objective function over M.PP and M.N (which increases exponentially). It seems to run virtually forever. Right now I'm using a local cbc solver. Do you have any recommendations for higher performance solvers, or is there a more efficient constraint programming package than Pyomo? – Nurse_Patient_Optimizer Jun 07 '22 at 23:04
  • You’ll probably have to look at some alternate approaches or simplifications if it is too slow. Perhaps just use distance from a central point (nurse station) and then you don’t have to worry about pairing. Anyhow, those are other questions. If this answered your original issue, you should close it out by accepting the answer. – AirSquid Jun 10 '22 at 14:21