No Impact MRP Customization

Step by step guide on how to build a customization to run No Impact MRP

This guide will step through the process of coding in a Transmission Processing and End of Simulation customization points. If you are just getting started, view the following articles for more information on the the basics: getting started with customizations, the Transmission Processing customization point basics, and the End of Simulation customization point basics.

Overview

This customization is designed to allow the MRP Optimize to run without impacting jobs that have already been scheduled. Since the MRP process re-optimizes the schedule several times before completion, the custom code locks and anchors all jobs that were previously scheduled, which prevents them from being impacted by the MRP. After the final MRP Optimize, the original jobs are allowed to reschedule as they were while the newly generated jobs are left unscheduled. Finally, the activities that were locked and anchored by the customization are unlocked and unanchored.  This sequence can be accomplished by creating a customization that uses a combination of the Transmission Processing and the End of Simulation customization points. 

Note: In this guide, we will assume that all operations have a single activity.

Step 1: Create the Project

Create the Project

First step is to create a new Class Library (.NET Framework) project targeting .NET Framework 4.6.2.

Add Assembly References

Add the following Assembly References to the project using the Reference Manager: Broadcasting, Common, ERPLibrary, ERPTransmissions, Schedule, SchedulerDefinitions, Transmissions. See getting started with customizations for more in-depth information on how to add assembly references.

Step 2: Transmission Processing Customization Point

Next, we will create the class that will handle the custom transmission processing, override the function for the Pre-processing customization point and then override the function for the Post-processing customization point.

Create the Class

Start by adding a class to the custom project. To access the customization points, inherit from TranimssionHandling. It is then required to override the string members Name and Description. These two values are used to show in the Add-in Dialog in PlanetTogether if the customization project is loaded correctly. Include all the using directives shown in the code block below-- they will be needed when we override the Preprocessing and Postprocessing functions. Additionally, declare a pair of HashSet<BaseId> objects that we will need to use to track the activities that we lock and anchor with our custom code.

using System;
using System.Collections.Generic;
using PT.Broadcasting;
using PT.Scheduler;
using PT.Scheduler.Simulation.Customizations.TransmissionPreprocessing;
using PT.Transmissions;

namespace PlanetTogether.MRP.NoImpact
{
class TransmissionProcessing : TransmissionHandling
{
public override string Name => "Transmission Handling";

public override string Description => "Performs actions on jobs before and after an MRP Optimize";

private readonly HashSet<BaseId> m_lockedOperations = new HashSet<BaseId>();
private readonly HashSet<BaseId> m_anchoredOperations = new HashSet<BaseId>();
}
}

Override the Preprocessing Function

The preprocessing function is called before the MRP Optimize begins its first simulation and we can use it to access all the activities that are already scheduled so that we can lock and anchor them. This is done by returning a TransmissionHandlingResult containing an ActivityUpdateList object-- a collection of ActivityChange objects that the scheduler will use to set the activity anchored and locked properties prior to running the MRP Optimize. In the following code block, we show how to use the Job Manager in Scenario Detail to iterate through all the non template jobs, to learn more about templates and how they are used please click here. For each job, we get a list of the operations; if an operation is not anchored, we track it in one of the HashSet objects we created and we create an ActivityChange object with the Anchored property set to true to add to the TransmissionHandlingResult ActivityUpdateList; similarly, if an activity is not locked, we track it in the other HashSet object and we create an ActivityChange object with the Locked property set to true to add to the ActivityUpdateList.

protected override TransmissionHandlingResult Preprocessing(Transmission a_t, ScenarioDetail a_sd)
{
TransmissionHandlingResult result = new TransmissionHandlingResult();

//Only run this code on a MRP transmission
if (a_t is ScenarioDetailOptimizeT optimizeT && optimizeT.RunMRP)
{
m_anchoredOperations.Clear();
m_lockedOperations.Clear();

foreach (Job job in a_sd.JobManager)
{
if (!job.Template)
{
var ops = job.GetOperations();

foreach (InternalOperation op in ops)
{
//Lock and anchor jobs before running MRP
if (op.Anchored != PT.SchedulerDefinitions.anchoredTypes.Anchored)
{
//Track anchored op
m_anchoredOperations.Add(op.Id);

TransmissionHandlingResult.ActivityChange change = new TransmissionHandlingResult.ActivityChange(op.Activities.GetByIndex(0));
change.Anchored = true;
result.ActivityUpdateList.Add(change);
}

if (op.Locked != PT.SchedulerDefinitions.lockTypes.Locked)
{
//Track locked op
m_lockedOperations.Add(op.Id);

TransmissionHandlingResult.ActivityChange change = new TransmissionHandlingResult.ActivityChange(op.Activities.GetByIndex(0));
change.Locked = true;
result.ActivityUpdateList.Add(change);
}
}
}
}
}
else
{
return null;
}

return result;
}

Override the Postprocessing Function

The Postprocessing function, as the name implies, is called after the scheduler has finished processing the MRP Optimize transmission. We will use this access point to unlock and unanchor any operations that we previously locked or anchored. In this next code block, we setup the function much like we did in Preprocessing to iterate through all the operations. For each operation, we perform a lookup on each of the HashSet. If an operation was tracked in the Anchored HashSet, we create an ActivityChange object with Anchored set to false; If an operation was tracked in the Locked Hashset, we create an ActivityChange object with Locked set to false;

protected override TransmissionHandlingResult Postprocessing(Transmission a_t, ScenarioDetail a_sd)
{
TransmissionHandlingResult result = new TransmissionHandlingResult();
if (a_t is ScenarioDetailOptimizeT optimizeT && optimizeT.RunMRP)
{
foreach (Job job in a_sd.JobManager)
{
if (!job.Template)
{
var ops = job.GetOperations();
foreach (InternalOperation op in ops)
{
//Unlock and unanchor original jobs
if (m_anchoredOperations.Contains(op.Id))
{
TransmissionHandlingResult.ActivityChange change = new TransmissionHandlingResult.ActivityChange(op.Activities.GetByIndex(0));
change.Anchored = false;
result.ActivityUpdateList.Add(change);
}

if (m_lockedOperations.Contains(op.Id))
{
TransmissionHandlingResult.ActivityChange change = new TransmissionHandlingResult.ActivityChange(op.Activities.GetByIndex(0));
change.Locked = false;
result.ActivityUpdateList.Add(change);
}
}
}
}
}
else
{
return null;
}

return result;
}

Step 3: End of Simulation Customization Point

For the final step, we will create the class that will handle keeping new jobs off the schedule while rescheduling the original jobs exactly as they were before. It will be necessary to override the SimulationInitialization and the EndOfSimulationCleanup functions to accomplish this.

Create the Class

Add a new class to the project. Inherit from EndOfSimulationCustomization to access the customization points. Include the using directives shown below as well as the Name and Description properties. We will need to keep track of the MRP stage, so we must declare a bool member variable to set on simulation initialization and another bool to set when MRP reaches the final optimize.

For more information on tracking MRP status, check out our knowledge base article on how to track MRP Status in a customization.

using System;
using PT.Scheduler;
using PT.SchedulerDefinitions;
using PT.Transmissions;
using static PT.Scheduler.ScenarioDetail;
using PT.Scheduler.Simulation.Customizations;

namespace PlanetTogether.MRP.NoImpact
{
public class EndOfSimCust : EndOfSimulationCustomization
{
public override string Name => "No Impact MRP";

public override string Description => "Runs MRP without scheduling orders";

bool m_initialized;
bool m_mrpFinalOptimize;
}
}

EndOfSimulationCustomization inherits from an abstract class that contains a SortedList<int, ScenarioBaseT> object called TransmissionsToProcess. We can add transmissions to this list for the scheduler to process sequentially after the customization point.

Override the SimulationInitialization Function

This function gets called by the scheduler before a simulations begins. We will use it to clear the list of transmissions that are created in the EndOfSimulation function later in the next section, and to subscribe to the MRPStatusUpdateEvent. The initialization bool can be used here to make sure the event is subscribed to only once. The event handler function will set the final optimize bool to true once the status update for the final optimize is fired.

protected override void SimulationInitialization(ScenarioDetail a_sd, SimulationType a_simulationType, ScenarioBaseT a_transmission)
{
CleanTransmissionsProcess();

if (!m_initialized)
{
using (var seoa = a_sd.Scenario.ScenarioEventsLock.EnterWrite())
{
seoa.Instance.MRPStatusUpdateEvent += MRPStatusUpdateEventHandler;
}

m_initialized = true;
}
}

private void MRPStatusUpdateEventHandler(SimulationProgress.Status a_statusCode, object[] a_detailParams)
{
//In case the user disabled only some of the customization points
if (!Enabled)
{
a_statusCode = SimulationProgress.Status.Clear;
}

if (a_statusCode == SimulationProgress.Status.MRP_StartFinalOptimize)
{
m_mrpFinalOptimize = true;
}
}

Override the EndOfSimulationCleanup Function

This function gets called at the end of each MRP simulation. In the code block below, we will create some Transmissions to bulk update the new MRP jobs and reschedule the original jobs as they were previously scheduled. We can use a ScenarioDetailSetJobCommitmentsAndPrioritiesT  to handle the bulk updates, and a ScenarioDetailOptimizeT to reschedule the original jobs. We can start by iterating through all the operations, and since we know that all original jobs had been locked and anchored in the TransmissionProcessing Preprocessing customization point, we can identify operations from new MRP jobs by checking that their Locked property is not set to lockTypes.Locked. If that is the case, we can add the operation to a JobKeyList. The JobKeyList is needed to pass to the bulk update transmissions.

Next we create the transmissions and add them to the TranissionsToProcess list. First we want to create a ScenarioDetailSetJobCommitmentsAndPrioritiesT with the DoNotSchedule property set to true to make sure the new jobs don't get scheduled. Then we create the ScenarioDetailOptimizeT to reschedule the original jobs. Finally, we create an additional ScenarioDetailSetJobCommitmentsAndPrioritiesT with the DoNotSchedule property set to false so that new MRP jobs are able to get scheduled if needed at a later time. We can add these transmissions to the TransmissionsToProcess list by using the AddTransmissionsToProcess function which needs an integer for the index and the transmission.   

protected override void EndOfSimulationCleanup(SimulationType a_simType, ScenarioBaseT a_t, ScenarioDetail a_sd)
{
if (a_t is ScenarioDetailOptimizeT mrpT && mrpT.RunMRP && m_mrpFinalOptimize)
{
m_mrpFinalOptimize = false;
JobKeyList jobsList = new JobKeyList();

//Gather new MRP jobs
foreach (Job job in a_sd.JobManager)
{
if (job.Template)
{
continue;
}

foreach (InternalOperation op in job.GetOperations())
{
if (op.Locked != lockTypes.Locked)
{
jobsList.AddFront(job.Id);
break;
}
}
}

//Mark new MRP jobs as Do Not Schedule
ScenarioDetailSetJobCommitmentsAndPrioritiesT bulkUpdateT = new ScenarioDetailSetJobCommitmentsAndPrioritiesT(a_sd.Scenario.Id, jobsList);
bulkUpdateT.DoNotSchedule = true;
AddTransmissionsToProcess(0, bulkUpdateT);

//Send optimize to reschedule original jobs
ScenarioDetailOptimizeT optimizeT = new ScenarioDetailOptimizeT(a_sd.Scenario.Id, true, false);
AddTransmissionsToProcess(1, optimizeT);

//Remove the Do Not Schedule flag from new MRP jobs
bulkUpdateT = new ScenarioDetailSetJobCommitmentsAndPrioritiesT(a_sd.Scenario.Id, jobsList);
bulkUpdateT.DoNotSchedule = false;
AddTransmissionsToProcess(2, bulkUpdateT);
}
}

Demonstration

The following two images display how with this customization, we are able to run an MRP Optimize without affecting the previously generated schedule. In our simple case scenario we only have three jobs with one operation each. The first screenshot shows the Gantt after one of the operations was manually moved to a different shift. After taking the screenshot, we increased the demand and then ran MRP. As shown in the second image, the operations remained scheduled as they were, and although they are not scheduled, the new MRP jobs are present in the jobs grid.

Before running MRP:

After Running MRP:

Files

Download Visual Studio project

Download customization file for version 11.37.11

Download sample scenario for version 11.37.11