SuggestOptions#

Options for Project.SuggestAssignments. They set which baseline the call expects, which slot categories it acts on, which assignments stay as they are, the cost of each extra person, the scoring stage, the number of rebalance passes, and whether the call works on a copy with leveling delays cleared. new SuggestOptions() fills empty slots only, scored on the scheduled stage, with no penalty and one pass.

Declaration#

public sealed class SuggestOptions : IDeepCloneable<SuggestOptions>
{
    public string? BaselineId { get; set; }
    public List<AssignTarget> Targets { get; set; } = [AssignTarget.Empty];
    public List<Id> Fixed { get; set; } = [];
    public double NewResourcePenalty { get; set; } = 0.0;
    public LevelingOptions? Level { get; set; }
    public uint Passes { get; set; } = 1;
    public bool ClearFirst { get; set; }

    public SuggestOptions DeepClone();
}

Set the properties you need with an object initializer: new SuggestOptions { Targets = [AssignTarget.Empty, AssignTarget.Broken] }. SuggestOptions doesn't implement IEquatable<T>, so two instances compare by reference.

Properties#

Property Type Default Description
BaselineId string? null The baseline the call expects. null skips the check.
Targets List<AssignTarget> [AssignTarget.Empty] Slot categories to act on. [] changes no assignment.
Fixed List<Id> empty Assignment ids the call never changes.
NewResourcePenalty double 0.0 Cost per extra person in use. Must be finite and >= 0.
Level LevelingOptions? null Scores every candidate on the leveled stage. null scores the scheduled stage.
Passes uint 1 Rebalance passes over Valid slots. Must be >= 1.
ClearFirst bool false Works on a copy with in-scope LevelingDelay / Pauses cleared.

BaselineId#

public string? BaselineId { get; set; }

A baseline token taken from the latest ScheduleResult.BaselineId. When it isn't null and doesn't name the project's current baseline, the call throws a ProjectException with Code == ProjectError.StaleBaseline. SuggestResult.BaselineId and SuggestResult.Changes.BaselineId always hold the accepted state's token.

Targets#

public List<AssignTarget> Targets { get; set; } = [AssignTarget.Empty];

The slot categories the call acts on, in any combination. Each category may appear once; a duplicate throws an InputException with Path targets[i]. Pass 0 handles Empty and Broken slots, and passes 1 to Passes handle Valid slots.

With Targets = [], the call changes no assignment and only reports SuggestResult.Invalid, so it works as a check. SuggestResult.Unfilled lists only targeted empty slots.

Fixed#

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

Assignment ids the call never changes. A slot bound to a listed assignment is skipped in every pass, but it still appears in SuggestResult.Invalid when its resource is outside its Allowed list.

Each id must name an active assignment and appear once. An unknown, inactive or repeated id throws an InputException with Path fixed[i]. An id that no slot is bound to is accepted.

NewResourcePenalty#

public double NewResourcePenalty { get; set; } = 0.0;

The cost added for each resource from the slots' pool that is in use in the proposal but not in the reference state. It's included in every step's StepCost and in SuggestProposal.Cost. 0 balances load only. A large value uses the fewest people.

A negative or non-finite value throws an InputException with Path newResourcePenalty.

Level#

public LevelingOptions? Level { get; set; }

When set, each candidate is leveled with these options and scored on the leveled stage, and SuggestProposal.Leveled holds the leveled metrics. When null, candidates are scored on the scheduled stage. Level.Scope.Tasks also sets the scope of ClearFirst.

Options that are invalid against the accepted state throw an InputException. The proposal carries no leveling delays: to commit it, pass SuggestResult.Changes to Project.Apply, then call Project.Level with the same LevelingOptions.

Passes#

public uint Passes { get; set; } = 1;

The maximum number of rebalance passes over Valid slots. A pass that moves nothing ends the loop early. It has no effect unless Targets contains AssignTarget.Valid. The pass number of each rebalance step is in SuggestStep.Pass.

Passes = 0 throws an InputException with Path passes.

ClearFirst#

public bool ClearFirst { get; set; }

When true, the reference state is a solved copy of the accepted state with LevelingDelay and Pauses cleared on the tasks in scope. The scope is Level.Scope.Tasks when Level gives it, otherwise every task, also when Level is null. ClearFirst doesn't depend on Level.ClearFirst.

SuggestResult.Changes then starts with one task update per cleared task, before the assignment edits, so applying it reproduces the cleared state. These updates are present whenever something was cleared, also with Targets = [] or no slots.

ClearFirst adds no error: BaselineId, Fixed, Level and the slots are checked against the accepted state before anything is cleared. When nothing in scope has a delay or a pause, the result is the same as with ClearFirst = false.

Methods#

DeepClone#

public SuggestOptions DeepClone()

Returns a copy that shares no nested object (Targets, Fixed, Level) with this one.

ToString#

public override string ToString()

Returns a debug dump of the fields, e.g. SuggestOptions { BaselineId: …, Targets: …, Fixed: …, NewResourcePenalty: …, Level: …, Passes: …, ClearFirst: … }.

AssignTarget#

A slot category for Targets. The category of each slot is judged from its current resource in the accepted state.

public enum AssignTarget
{
    Empty,
    Broken,
    Valid,
}
Value Description
Empty The slot has no assignment. Filled in pass 0.
Broken The slot's resource isn't in its Allowed list. Moved to the cheapest allowed candidate in pass 0.
Valid The slot's resource is in its Allowed list. Rebalanced in passes 1 to Passes, and moved only when that lowers the cost.

Example#

using Plen;

var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");
var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = monday, Duration = 2.0 },
        new TaskData("B") { StartDate = monday, Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }, new ResourceData { Id = "dev2" }],
    Assignments =
    [
        new AssignmentData { Id = "aA", Task = "A", Resource = "dev1", Value = 1.0 },
    ],
};
var project = new Project();
var solved = project.Solve(data, new SolveOptions());

var slots = new List<Slot>
{
    new Slot("aA", "A", ["dev1", "dev2"]) { Assignment = "aA" },
    new Slot("sB", "B", ["dev1", "dev2"]),
};
var options = new SuggestOptions
{
    BaselineId = solved.BaselineId,
    Targets = [AssignTarget.Empty, AssignTarget.Valid],
    Fixed = ["aA"],
    NewResourcePenalty = 0.5,
};
var result = project.SuggestAssignments(slots, options);

foreach (var step in result.Steps)
    Console.WriteLine($"{step.Slot}: {step.FromResource} -> {step.ToResource}");