SuggestResult#

A staffing proposal scored against the project's accepted state. Project.SuggestAssignments returns it; there's no other way to get one. The project doesn't change until you pass Changes to Project.Apply.

Declaration#

namespace Plen;

public sealed class SuggestResult : IDeepCloneable<SuggestResult>
{
    public required string BaselineId { get; set; }
    public required SchedulingChanges Changes { get; set; }
    public List<SuggestStep> Steps { get; set; } = [];
    public List<Id> Unfilled { get; set; } = [];
    public List<Id> Invalid { get; set; } = [];
    public AssignTotal Baseline { get; set; }
    public required SuggestProposal Proposed { get; set; }

    public SuggestResult DeepClone();
}

SuggestResult has no value equality: Equals compares references. ToString() lists the fields, for debugging.

Properties#

Property Type Description
BaselineId string Token of the accepted state the proposal was scored against. Always set.
Changes SchedulingChanges The net proposal against the accepted state. Pass it to Apply as is.
Steps List<SuggestStep> Decisions that changed the working state, in the order they were made.
Unfilled List<Id> Ids of targeted empty slots that stayed empty, in visit order.
Invalid List<Id> Ids of slots whose final resource isn't in their Allowed list, in visit order.
Baseline AssignTotal Cost and resource count of the reference state, on the scoring stage.
Proposed SuggestProposal Cost, resource count and metrics of the final working state.

BaselineId#

public required string BaselineId { get; set; }

The project's accepted baseline token at the time of the call. Changes.BaselineId holds the same value, so Apply throws ProjectException StaleBaseline if the project changed in between. With SuggestOptions.ClearFirst it's still the accepted state's token.

Changes#

public required SchedulingChanges Changes { get; set; }

The difference between the accepted state and the final working state. It holds:

  • one Assignments.Add entry per filled slot: Id = the slot id, the slot's Task, the chosen Resource and the slot's Value. Offset and Duration are null, so the assignment covers the whole task
  • one Assignments.Update entry per repaired or rebalanced assignment, setting only Resource. None when the slot ended on its original resource
  • with ClearFirst, one Tasks.Update per cleared task, setting LevelingDelay to 0 and Pauses to an empty list

Changes never removes an assignment and carries no leveling delays. With no steps and nothing cleared, every bucket is empty.

Steps#

public List<SuggestStep> Steps { get; set; } = [];

Only decisions that changed the working state. A slot filled in pass 0 and moved in a later pass has two steps but one add in Changes. Tell the kind of a step from its fields:

FromResource Pass Step
null 0 Fill
set 0 Repair
set 1 or more Rebalance

Unfilled#

public List<Id> Unfilled { get; set; } = [];

Empty slots that pass 0 targeted but couldn't fill, because no candidate could be scored. Empty slots outside SuggestOptions.Targets aren't listed.

Invalid#

public List<Id> Invalid { get; set; } = [];

Every slot held by a resource outside its Allowed list at the end of the call, whatever Targets is. Slots bound to a SuggestOptions.Fixed assignment are included. With Targets set to an empty list, this is the only output, so the call works as a check.

Baseline#

public AssignTotal Baseline { get; set; }

The reference state scored on the same stage as the steps: the accepted state, or its cleared copy with ClearFirst. With SuggestOptions.Level, it's that state leveled as is. The first step's Cost.Before equals Baseline.Cost.

Proposed#

public required SuggestProposal Proposed { get; set; }

The final working state, measured against the reference state. Compare Proposed.Cost with Baseline.Cost to see what the proposal gains.

Methods#

DeepClone#

public SuggestResult DeepClone();

Returns a copy that shares no list or nested object with this one.

SuggestStep#

One decision that changed the working state.

namespace Plen;

public sealed class SuggestStep : IDeepCloneable<SuggestStep>, IEquatable<SuggestStep>
{
    public required Id Slot { get; set; }
    public required Id Task { get; set; }
    public uint Pass { get; set; }
    public Id? FromResource { get; set; }
    public required Id ToResource { get; set; }
    public StepCost Cost { get; set; }

    public SuggestStep DeepClone();
    public bool Equals(SuggestStep? other);
}
Property Type Description
Slot Id The slot id.
Task Id The slot's task.
Pass uint 0 for a fill or repair; 1 and up for the rebalance pass that made the move.
FromResource Id? The resource before the step. null when the step filled an empty slot.
ToResource Id The resource after the step.
Cost StepCost State cost before and after the step.

Equality is by value over all properties.

StepCost#

The cost of the working state before and after one step.

namespace Plen;

public record struct StepCost(double Before, double After);
Property Type Description
Before double Cost before the step: the previous step's After, or Baseline.Cost for the first step.
After double Cost after the step.

Both values include the new-resource penalty and are measured against the reference state on the scoring stage. Equality compares both values with ==.

AssignTotal#

The cost and resource count of one assignment state. SuggestResult.Baseline holds one.

namespace Plen;

public record struct AssignTotal(double Cost, uint Resources);
Property Type Description
Cost double Cost of the state on the scoring stage.
Resources uint Resources of the counted pool with at least one active assignment. The counted pool is every resource in any slot's Allowed, plus the current resources of filled slots.

Equality compares both values with ==.

SuggestProposal#

The final working state: its cost, resource count and metrics against the reference state.

namespace Plen;

public sealed class SuggestProposal : IDeepCloneable<SuggestProposal>, IEquatable<SuggestProposal>
{
    public double Cost { get; set; }
    public uint Resources { get; set; }
    public required Metrics Scheduled { get; set; }
    public LeveledMetrics? Leveled { get; set; }

    public SuggestProposal DeepClone();
    public bool Equals(SuggestProposal? other);
}
Property Type Description
Cost double Cost of the final state, new-resource penalty included. Equals the last step's Cost.After.
Resources uint Resources of the counted pool with at least one active assignment.
Scheduled Metrics Metrics of the scheduled stage.
Leveled LeveledMetrics? Metrics of the leveled stage. Set only when SuggestOptions.Level is set; null otherwise and when the scheduled stage is cyclic.

The metrics come from the last step's scoring run. With no steps, they describe the reference state on the scoring stage: scheduled deltas are zero, leveled ones may not be.

Equality is by value: Cost and Resources by ==, Scheduled and Leveled by their own equality.

Example#

Fill an open position on task C, read the proposal, then commit it:

using Plen;

var start = Moment.FromYmdHm(2026, 8, 10, 0, 0) ?? throw new InvalidOperationException("invalid date");
var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = start, Duration = 2.0 },
        new TaskData("C") { StartDate = start, Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }, new ResourceData { Id = "dev2" }],
    Assignments = [new AssignmentData { Id = "aA", Task = "A", Resource = "dev1", Value = 1.0 }],
};
data.Config.UseCalendars = false;

var project = new Project();
project.Solve(data, new SolveOptions());

List<Slot> slots = [new Slot("sC", "C", ["dev1", "dev2"])];
var result = project.SuggestAssignments(slots, new SuggestOptions());

foreach (var step in result.Steps)
{
    var kind = step.FromResource is null ? "fill" : step.Pass == 0 ? "repair" : "rebalance";
    Console.WriteLine($"{kind} {step.Slot}: {step.FromResource} -> {step.ToResource}, " +
                      $"cost {step.Cost.Before} -> {step.Cost.After}");
}
Console.WriteLine($"unfilled: [{string.Join(", ", result.Unfilled)}], invalid: [{string.Join(", ", result.Invalid)}]");
Console.WriteLine($"cost {result.Baseline.Cost} -> {result.Proposed.Cost}, " +
                  $"people {result.Baseline.Resources} -> {result.Proposed.Resources}");

var report = project.Apply(result.Changes);
if (report.Rejected.Count == 0)
    project.Solve(null, new SolveOptions());