IBM ILOG Dispatcher User's Manual > Field Service Solutions > Dispatching Technicians II > Model

Once you have written a description of your problem, you can use Dispatcher classes to model it.

Step 2   -  

Open the example file

Open the example file YourDispatcherHome/examples/src/tutorial/technic1_partial.cpp in your development environment.

This program starts with a class that models both skill requirements for jobs and skills provided by technicians.

Step 3   -  

Declare the SkillProfile class

Add the following code after the comment //Declare the SkillProfile class.

class SkillProfile {
  IloEnv _env;
  IloInt _nbOfSkills;
  IloInt* _skillLevels;
public:
  SkillProfile(IloEnv env, IloInt nbOfSkills):
    _env(env),
    _nbOfSkills(nbOfSkills),
    _skillLevels(0)
  {
    _skillLevels = new (env) IloInt [ nbOfSkills ];
    for (IloInt s=0; s< _nbOfSkills; ++s) _skillLevels[s] = 0;
  }
  void setSkillLevel(IloInt skillIndex, IloInt level=1) {
    _skillLevels[ skillIndex ] = level; }

  /**
   * Returns true if <code>other</code> is comparable with this
   * and is greater.
   */
  IloBool isGreaterThanOrEqual(const SkillProfile* other);

The SkillProfile class maps a level for each technician skill. The function SkillProfile::isGreaterThanOrEqual follows, and is used to ensure that the skill level meets the constraint of the job profile.


IloBool SkillProfile::isGreaterThanOrEqual(const SkillProfile* other) {
  for(IloInt index = 0;
      index < _nbOfSkills && other->_nbOfSkills; ++index) {
    if ( _skillLevels[index] < other->_skillLevels[index] ) {
       return IloFalse;
    }
  }
  return IloTrue;
}

Step 4   -  

Declare the RoutingModel class

Add the following code after the comment //Declare the RoutingModel class.

  void createDimensions();
  void createIloNodes(const char* nodePath);
  void createSkillProfiles(const char* profilePath);
  void setTechnicianSkillProfiles(const char* techProfilePath);
  void setVisitSkillProfiles(const char* visitProfilePath);
  void createTechnicians(const char* technicianPath);
  void createVisits(const char* visitsPath);

  void postCompatibility();

The only dimension defined in createDimensions is time, and createIloNodes is similar to previous lessons. createTechnicians is similar to the same function in Chapter 13, Dispatching Technicians. The other functions in RoutingModel are new, and these are associated with the technician skills, levels, and visit requirements.

The following is the first section of RoutingModel and is provided for you:

class RoutingModel {
  IloEnv              _env;
  IloModel            _mdl;
  IloArray<SkillProfile*> _profiles;
  IloInt _nbOfSkills;

  const char* _nodePath;
  const char* _technicianPath;
  const char* _visitPath;
  const char* _profilePath;
  const char* _techProfilePath;  // table binding profiles to tecnicians
  const char* _visitProfilePath; // table binding profiles to visits

Step 5   -  

Create the getSkillProfile function

Add the following code after the comment
//Create the getSkillProfile function.

  SkillProfile* getSkillProfile(IloInt profileId) {
    if ( _profiles.getSize() <= profileId ) {
      for (IloInt p= _profiles.getSize(); p <= profileId; ++p) {
        _profiles.add( (SkillProfile*) 0);
      }
    }
    SkillProfile* profile = _profiles[ profileId ] ;
    if ( 0 == profile ) {
      profile = new (_env) SkillProfile(_env, _nbOfSkills);
      _profiles[ profileId ] = profile;
    }
    return profile;
  }

Skill profiles, both for jobs and technicians, are stored in an array and identified by a unique ID. Retrieving the profile from its ID is performed by a lazy accessor technique. Missing array slots are filled with zeros; then, if a returned profile is null, a new profile is created, stored, and returned.

The final section of RoutingModel follows:

public:
  RoutingModel(IloEnv env);
  ~RoutingModel() {}

  void parse(int argc, char** argv);
  void createModel();

  IloEnv getEnv() const { return _env; }
  IloModel getModel() const { return _mdl; }
};

Step 6   -  

Create the parse function

void RoutingModel::parse(int argc, char** argv) {
  if ( argc > 1 ) _visitPath        = argv[1];
  if ( argc > 2 ) _technicianPath   = argv[2];
  if ( argc > 3 ) _visitProfilePath = argv[3];
  if ( argc > 4 ) _techProfilePath  = argv[4];
  if ( argc > 5 ) _profilePath      = argv[5];
  if ( argc > 6 ) _nodePath         = argv[6];
}

Add the following code after the comment //Create the parse function.

_visitPath accesses the data for the visit: the time window during which the visit can occur, and the service time. The technicians are identified through _technicianPath. _visitProfilePath accesses the map of visits to the visit skill profile IDs. _techProfilePath accesses the map of technicians to the technician skill profile IDs, and _profilePath accesses the technician skill ability levels and the skill ability levels required at the customer visits.

RoutingModel::RoutingModel(IloEnv env)
  : _env(env),
  _mdl(env),
  _profiles(env),
  _nbOfSkills(3),
  _nodePath("../../../examples/data/technic1/nodes.csv"),
  _technicianPath("../../../examples/data/technic1/technicians.csv"),
  _visitPath("../../../examples/data/technic1/visits.csv"),
  _profilePath("../../../examples/data/technic1/SkillProfiles.csv"),
  _techProfilePath("../../../examples/data/technic1/techSkills.csv"),
  _visitProfilePath("../../../examples/data/technic1/visitSkills.csv")
{ }

Step 7   -  

Add the vehicle/technician

Add the following code after the comment //Add the vehicle/technician.

    IloVehicle technician(first, last, name);
    technician.setCost(time, 1.0);
    technician.setKey(name);
    _mdl.add(technician);

This code is a section of createTechnicians, and adds the vehicle/technician with the sole cost factor of time. The first section of the function follows.

void RoutingModel::createTechnicians(const char* techPath) {
  IloDimension2 time = IloDimension2::Find(_env, "Time");
  IloCsvReader csvTechReader(_env, techPath);
  char namebuf[128];
  IloCsvReader::LineIterator it(csvTechReader);
  for( ; it.ok(); ++it) {
    IloCsvLine line = *it;
    char * namefirst = line.getStringByHeader("first");
    char * namelast = line.getStringByHeader("last");
    char * name = line.getStringByHeader("name");
    IloNum openTime = line.getFloatByHeader("open");
    IloNum closeTime = line.getFloatByHeader("close");
    IloNode node1 = IloNode::Find(_env, namefirst);
    IloNode node2 = IloNode::Find(_env, namelast);

    sprintf(namebuf, "start_%s", name);
    IloVisit first(node1, namebuf);
    _mdl.add( first.getCumulVar(time) >= openTime );

    sprintf(namebuf, "end_%s", name);
    IloVisit last(node2, namebuf);
    _mdl.add( last.getCumulVar(time) <= closeTime );

Step 8   -  

Add the PenaltyCost

Add the following code after the comment //Add the PenaltyCost.

    _mdl.add( visit.getDelayVar(time) == serviceTime );
    _mdl.add( minTime <= visit.getCumulVar(time) <= maxTime);
    visit.setPenaltyCost(10000);
    visit.setKey(visitName);
    _mdl.add(visit);

minTime and maxTime are the time windows for a paticular visit. The member function IloVisit::setPenaltyCost is used to set the cost of not performing a visit. By default, the penalty cost is IloInfinity, which means that visits must be performed in a solution if a value is not set with this member function.

The following code is the first section of createVisits.

void RoutingModel::createVisits(const char* visitsPath) {
  IloDimension2 time = IloDimension2::Find(_env, "Time");
  IloCsvReader csvVisitReader(_env, visitsPath);
  IloCsvReader::LineIterator  it(csvVisitReader);
  for( ;it.ok(); ++it) {
    IloCsvLine line = *it;
    //read visit data from files
    char * visitName = line.getStringByHeader("name");
    char * nodeName = line.getStringByHeader("node");
    IloNum minTime  = line.getFloatByHeader("minTime");
    IloNum maxTime  = line.getFloatByHeader("maxTime");
    IloNum serviceTime = line.getFloatByHeader("dropTime");
    IloNode node = IloNode::Find(_env, nodeName);
    IloVisit visit(node, visitName);

Step 9   -  

Define the createSkillProfiles function

Add the following code after the comment
//Define the createSkillProfiles function.

void RoutingModel::createSkillProfiles(const char* profilePath) {
  IloCsvReader skillProfileReader(_env, profilePath);
  for( IloCsvReader::LineIterator it(skillProfileReader); it.ok(); ++it) {
    IloCsvLine line = *it;
    IloInt profileId = line.getIntByHeader("id");
    IloInt skill1Level = line.getIntByHeader("Skill1Level");
    IloInt skill2Level = line.getIntByHeader("Skill2Level");
    IloInt skill3Level = line.getIntByHeader("Skill3Level");

    SkillProfile* profile = getSkillProfile(profileId);
    assert( 0 != profile );

    profile->setSkillLevel(0, skill1Level);
    profile->setSkillLevel(1, skill2Level);
    profile->setSkillLevel(2, skill3Level);
  }
  skillProfileReader.end();
}

This function reads the csv file and creates the skill profiles of the technicians and visits; the levels of each skill that are available from the technician or required at the customer visit.

Step 10   -  

Define the setTechnicianSkillProfiles function

Add the following code after the comment
//Define the setTechnicianSkillProfiles function.

void RoutingModel::setTechnicianSkillProfiles(const char* techProfilePath) {
  // now read the techProfilePath table.
  IloCsvReader techProfileReader(_env, techProfilePath);
  for (IloCsvReader::LineIterator tpit(techProfileReader); tpit.ok(); ++tpit) {
    IloCsvLine line = *tpit;
    const char* techName = line.getStringByHeader("name");
    IloInt profileId = line.getIntByHeader("TechProfileId");
    IloVehicle tech = IloVehicle::Find(_env, techName);
    SkillProfile* profile = getSkillProfile(profileId);
    if ( 0 != tech.getImpl() && 0 != profile ) {
      tech.setObject( (IloAny)profile );
    }
  }
  techProfileReader.end();
}

This function implements the mapping between the technician and the skill profile. The profile itself is stored in the object field of the IloVehicle (for technicians) or IloVisit (for visits), as an IloAny void* pointer. Attaching profiles to a vehicle/visit is crucial to the implementation of the compatibility constraint, which takes one vehicle and one visit.

Step 11   -  

Define the setVisitSkillProfiles function

Add the following code after the comment
//Define the setVisitSkillProfiles function.

void RoutingModel::setVisitSkillProfiles(const char* visitProfilePath) {
  // now read the techProfilePath table.
  IloCsvReader visitProfileReader(_env, visitProfilePath);
  for (IloCsvReader::LineIterator vpit(visitProfileReader); vpit.ok(); ++vpit) 
{
    IloCsvLine line = *vpit;
    const char* visitName = line.getStringByHeader("name");
    IloInt profileId = line.getIntByHeader("VisitProfileId");
    IloVisit visit = IloVisit::Find(_env, visitName);
    SkillProfile* profile = getSkillProfile(profileId);
    if ( 0 != visit.getImpl() && 0 != profile ) {
      visit.setObject( (IloAny)profile );
    }
  }
  visitProfileReader.end();
}

This function identifies the visitProfileId required at each customer visit.

Step 12   -  

Define the AreSkillsCompatible predicate

Add the following code after the comment
//Define the AreSkillsCompatible predicate.

static IloBool AreSkillsCompatible(IloVisit visit, IloVehicle vehicle) {
  SkillProfile* visitProfile = (SkillProfile*)visit.getObject();
  SkillProfile* techProfile = (SkillProfile*)vehicle.getObject();
  if ( 0 != visitProfile ) {
    if ( 0 != techProfile ) {
      return techProfile->isGreaterThanOrEqual(visitProfile);
    } else {
      return IloFalse;
    }
  } else {
    return IloTrue;
  }
}

This predicate checks the compatibility relation between a visit and a technician. A visit is compatible with a technician if the visit profile is subsumed by the technician profile. A special case is also considered for a visit with no profile where all technicians are compatible.

Step 13   -  

Define the postCompatibility function

Add the following code after the comment
//Define the postCompatibility function.

void RoutingModel::postCompatibility() {
  IloVisitVehicleCompat compat(_env, AreSkillsCompatible);
  _mdl.add( IloCompatible(compat,  "skill compatibility"));
}

IBM ILOG Dispatcher provides a way for you to define compatibility relations between visits and vehicles. These relations can then be used to build compatibility constraints. In this example the constructor IloVisitVehicleCompat uses the predicate AreSkillsCompatible to build the compatibility relation. The function IloCompatible is then used to create the compatibility constraint to ensure that only compatible vehicles are used on a visit.

The compatibility constraint is used to check that only compatible visit/vehicle assignments are built, by removing incompatible vehicles. A predicate is used for convenience; you can also subclass a predefined class for more complex cases.