Group Operations by Same Product Sample

How to group operations with the same product and enforce back to back scheduling in a customization

This guide will step through the process of coding a Schedulability customization. If you are just getting started, please first see getting started with customizations and the schedulability customization point basics

Grouping Operations by Same Product

This example will group operations that produce the same item together in resources that are part of a group cell. These resources are identified via a UDF of type string on the resource object.

private const string c_groupCellUdf = "GroupCell";
private readonly Dictionary<string, List<BaseId>> m_groupCellResources = new Dictionary<string, List<BaseId>>();
private readonly Dictionary<BaseId, Dictionary<BaseId, TimeSpan>> m_actDurations = new Dictionary<BaseId, Dictionary<BaseId, TimeSpan>>();
private static TimeSpan ResourceReservationSpan => TimeSpan.FromMinutes(30);

protected override void SimulationInitialization(ScenarioDetail a_sd, SchedulabilitySimulationInitializationHelper a_schedulabilityHelper, ScenarioDetail.SimulationType a_simulationType, ScenarioBaseT a_transmission)
{
CollectGroupCellResources(a_sd);
}

/// <summary>
/// Cache collection of resources where operation grouping needs to occur
/// </summary>
/// <param name="a_sd"></param>
private void CollectGroupCellResources(ScenarioDetail a_sd)
{
m_groupCellResources.Clear();
foreach (Plant p in a_sd.PlantManager)
{
foreach (Department d in p.Departments)
{
foreach (Resource r in d.Resources)
{
if (r.GetUserFieldValue(c_groupCellUdf, out string groupCell))
{
if (m_groupCellResources.TryGetValue(groupCell, out List<BaseId> resIds))
{
resIds.Add(r.Id);
}
else
{
resIds = new List<BaseId>() { r.Id };
m_groupCellResources.Add(groupCell, resIds);
}
}
}
}
}
}

protected override bool IsSchedulable(ScenarioDetail a_sd, ScenarioDetail.SimulationType a_simType, long a_clock, long a_simulationClock, BaseResource a_res, BaseActivity a_activity, SchedulableInfo a_schedulableInfo, out long o_postEventTime)
{
o_postEventTime = -1;
if (a_simType != ScenarioDetail.SimulationType.Optimize) return true;
if (a_simulationClock > a_sd.GetPlanningHorizonEnd().Ticks) return true;
if (a_sd.OptimizeSettings.StartTime == OptimizeSettings.startTimes.EndOfFrozenZone && new DateTime(a_simulationClock) < a_sd.GetEndOfFrozenZone()) return true;
if (a_sd.OptimizeSettings.StartTime == OptimizeSettings.startTimes.EndOfStableZone && new DateTime(a_simulationClock) < a_sd.ClockDate.Add(a_sd.PlantManager[0].StableSpan)) return true;
if (a_sd.OptimizeSettings.StartTime == OptimizeSettings.startTimes.SpecificDateTime && new DateTime(a_simulationClock) < a_sd.OptimizeSettings.SpecificStartTime) return true;
if (!(a_activity is InternalActivity act)) return true;
if (act.Anchored || act.Locked == PT.SchedulerDefinitions.lockTypes.Locked) return true;
if (!(a_res is Resource res)) return true;
if (!res.GetUserFieldValue(c_groupCellUdf, out string groupCell)) return true;

string product = GetProduct(act);
if (product != null)
{
foreach (Resource otherResInCell in GetResourcesInGroupCell(a_sd, groupCell))
{
InternalActivity otherResLastAct = otherResInCell.Blocks.Last?.Data.Batch.FirstActivity;
if (otherResLastAct == null) continue;
string otherResLastProduct = GetProduct(otherResLastAct);
if (otherResLastProduct == product) // last act on another resource has the same product. don't allow it to schedule here
{
if (otherResInCell.Id == res.Id)
{
return true;
}
// unless it would be late on that other resource.
TimeSpan actDuration = GetActivityDuration(otherResInCell, act, otherResLastAct.ScheduledEndDate.Ticks);
if (a_schedulableInfo.m_finishDate <= act.NeedDate.Ticks && otherResLastAct.ScheduledEndDate.Add(actDuration) > act.NeedDate) // it would be on time on this res but late on the other resource.
{
continue;
}

return false;
}
}
}

InternalActivity lastAct = res.Blocks.Last?.Data?.Batch.FirstActivity;
//Allow activity to schedule if there is nothing scheduled on this resource yet
if (lastAct == null) return true;

string lastActProduct = GetProduct(lastAct);
if (lastActProduct == product) return true;

//If 30 minutes have passed, allow something outside the product group to schedule
if (a_simulationClock >= lastAct.ScheduledEndDate.Add(ResourceReservationSpan).Ticks) return true;
if (IsActWithSameProductOnDispatcher(res, lastActProduct))
{
SaveActDuration(act, res, TimeSpan.FromTicks(a_schedulableInfo.m_requiredProcessingSpan.TimeSpanTicks + a_schedulableInfo.m_requiredPostProcessingSpan.TimeSpanTicks));

return false;
}

return true;
}

/// <summary>
/// Get the total run span of a scheduling activity
/// </summary>
/// <param name="a_res">Scheduling resource</param>
/// <param name="a_act">Scheduling activity</param>
/// <param name="a_startDateTicks">Scheduling activity start date ticks</param>
private TimeSpan GetActivityDuration(Resource a_res, InternalActivity a_act, long a_startDateTicks)
{
//First look in activity duration cache to see if duration has already been calculated
if (m_actDurations.TryGetValue(a_act.Id, out Dictionary<BaseId, TimeSpan> cachedActDurationsPerRes))
{
if (cachedActDurationsPerRes.TryGetValue(a_res.Id, out TimeSpan cachedDuration))
{
return cachedDuration;
}
}

//Calculate and cache activity duration before returning value
TimeSpan actDuration = CalculateActDuration(a_act, a_res, a_startDateTicks);
SaveActDuration(a_act, a_res, actDuration);
return actDuration;
}

/// <summary>
/// Calculate total required capacity of a scheduling activity
/// </summary>
/// <param name="a_act">Scheduling activity</param>
/// <param name="a_res">Scheduling resource</param>
/// <param name="a_startDateTicks">Scheduling activity start date ticks</param>
private static TimeSpan CalculateActDuration(InternalActivity a_act, Resource a_res, long a_startDateTicks)
{
RequiredCapacityPlus rcp = a_res.CalculateTotalRequiredCapacity(a_startDateTicks, a_act, null, true, false, new PT.Scheduler.Simulation.RequiredSpanPlusSetup(PT.Scheduler.Simulation.RequiredSpan.NotInit, false, PT.Scheduler.Simulation.RequiredSpan.NotInit), a_startDateTicks);
return TimeSpan.FromTicks(rcp.TotalRequiredCapacity());
}

/// <summary>
/// Cache total duration span of a scheduling activity
/// </summary>
/// <param name="a_act">Scheduling activity</param>
/// <param name="a_res">Scheduling resource</param>
/// <param name="a_actDuration">Scheduling activity start date ticks</param>
private void SaveActDuration(InternalActivity a_act, Resource a_res, TimeSpan a_actDuration)
{
if (!m_actDurations.TryGetValue(a_act.Id, out Dictionary<BaseId, TimeSpan> perResDurations))
{
perResDurations = new Dictionary<BaseId, TimeSpan>();
perResDurations.Add(a_res.Id, a_actDuration);
m_actDurations.Add(a_act.Id, perResDurations);
}
else
{
perResDurations[a_res.Id] = a_actDuration;
}
}

/// <summary>
/// Cache resources by group cell name
/// </summary>
/// <param name="a_sd"></param>
/// <param name="a_groupCell"></param>
private List<Resource> GetResourcesInGroupCell(ScenarioDetail a_sd, string a_groupCell)
{
List<Resource> resources = new List<Resource>();
if (m_groupCellResources.TryGetValue(a_groupCell, out List<BaseId> resIds))
{
foreach (var id in resIds)
{
var baseRes = a_sd.PlantManager.GetResource(id);
if (baseRes != null && baseRes is Resource res)
{
resources.Add(res);
}
}
}

return resources;
}

/// <summary>
/// Check if an activity with the same product is on a resource's active dispatcher
/// </summary>
/// <param name="a_res">Resource to check in</param>
/// <param name="a_product">Product to check against</param>
private static bool IsActWithSameProductOnDispatcher(Resource a_res, string a_product)
{
foreach (var act in a_res.ActiveDispatcher)
{
//An anchored activity wouldn't be able to be pulled in.
if (act.Anchored) continue;

if (GetProduct(act) == a_product)
{
return true;
}
}

return false;
}

/// <summary>
/// Get the name of an activity's product
/// </summary>
/// <param name="a_act"></param>
private static string GetProduct(InternalActivity a_act)
{
return a_act.Operation.ManufacturingOrder.Product?.Name;
}

Next Steps

To expand your customization, meet other types of scheduling requirements or to learn how to better navigate the PlanetTogether data objects explore the following articles:

Advanced topics

Coming Soon...