Project.Evaluate#

Scores alternative change sets against the solved baseline of a Project. The project isn't modified. Use it to compare options, such as which resource should take a task, before committing one with Apply.

Signature#

public EvaluateResult Evaluate(IReadOnlyList<Candidate> candidates, EvaluateOptions options)

Synchronous and CPU-only. Reads the project without changing it. Project isn't thread-safe, and Evaluate with ClearFirst updates the project's internal copy-on-write flags, so don't call it on one instance from several threads at once.

Parameters#

Name Type Description
candidates IReadOnlyList<Candidate> The change sets to score
options EvaluateOptions Baseline check, leveling stage, focus tasks and ClearFirst

Behavior#

Each candidate runs on a disposable copy of the reference state. The reference state is the baseline, or its cleared copy with options.ClearFirst:

  1. Apply the candidate's changes.
  2. Solve(null, …) and measure the scheduled stage.
  3. With options.Level set, Level with those options and measure the leveled stage. A cyclic scheduled stage skips this step.

Every candidate starts from the reference state, never from the previous candidate. A failure inside one candidate goes into that candidate's Error and doesn't fail the call. The result has scores, not dates.

Returns#

An EvaluateResult with the baseline metrics and one result per candidate, in input order.

Errors#

Error When
ProjectException NoBaseline The project is unsolved, including after Apply
ProjectException StaleBaseline options.BaselineId or any candidate's Changes.BaselineId doesn't name the current baseline
InputException An unknown task in options.Focus (Path == "focus[i]"), or options.Level is invalid for the baseline
ArgumentNullException candidates or options is null

EvaluateWith#

public EvaluateResult EvaluateWith(IReadOnlyList<Candidate> candidates, EvaluateOptions options, Execution execution)

Same as Evaluate, with a choice of Execution. Execution.Parallel(threads) scores candidates on thread-pool workers, threads of them at most (null uses Environment.ProcessorCount). The call still blocks until every candidate is done. Peak memory is one candidate copy per worker.

The results are identical to Evaluate and keep input order. A call-level error is the first one in input order, and later candidates may already have run. Errors are the same as for Evaluate.

Example#

Compare leaving the plan as it is with moving task B to an idle resource.

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 },
        new AssignmentData { Id = "aB", Task = "B", Resource = "dev1", Value = 1.0 },
    ],
};
var project = new Project();
project.Solve(data, new SolveOptions());

var toDev2 = new SchedulingChanges();
toDev2.Assignments.Update.Add(new AssignmentUpdate("aB") { Resource = Patch<Id>.Of("dev2") });

Candidate[] candidates =
[
    new() { Id = "stay", Changes = new SchedulingChanges() },
    new() { Id = "dev2", Changes = toDev2 },
];
var result = project.Evaluate(candidates, new EvaluateOptions());

foreach (var candidate in result.Candidates)
    Console.WriteLine($"{candidate.Id}: cost {candidate.Cost}"); // stay: cost 2, dev2: cost 0