|
|
|
@@ -63,9 +63,6 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|
|
|
|
.FirstOrDefaultAsync(x => x.AcademicTermId == plan.AcademicTermId, cancellationToken)
|
|
|
|
|
?? ScheduleOptimizationSettings.CreateDefault(plan.AcademicTermId);
|
|
|
|
|
|
|
|
|
|
var created = 0;
|
|
|
|
|
var completedTasks = 0;
|
|
|
|
|
var processedTasks = 0;
|
|
|
|
|
var messages = new List<string>();
|
|
|
|
|
if (reportProgress is not null)
|
|
|
|
|
{
|
|
|
|
@@ -74,87 +71,364 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|
|
|
|
cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var task in tasks)
|
|
|
|
|
var optimizer = new GeneticScheduleOptimizer(
|
|
|
|
|
plan.Id,
|
|
|
|
|
tasks,
|
|
|
|
|
constraints,
|
|
|
|
|
activePeriods,
|
|
|
|
|
classrooms,
|
|
|
|
|
entries,
|
|
|
|
|
optimization);
|
|
|
|
|
var result = optimizer.Optimize(cancellationToken);
|
|
|
|
|
db.ScheduleEntries.AddRange(result.Entries);
|
|
|
|
|
messages.AddRange(result.Messages);
|
|
|
|
|
|
|
|
|
|
if (reportProgress is not null)
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
constraints.TryGetValue(task.Id, out var constraint);
|
|
|
|
|
var taskCompleted = true;
|
|
|
|
|
foreach (var kind in new[]
|
|
|
|
|
{
|
|
|
|
|
ScheduleEntryKind.Lecture,
|
|
|
|
|
ScheduleEntryKind.Experiment
|
|
|
|
|
})
|
|
|
|
|
await reportProgress(
|
|
|
|
|
new(tasks.Count, tasks.Count, result.Entries.Count, result.CompletedTasks),
|
|
|
|
|
cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
if (result.Entries.Count > 0 && saveChanges)
|
|
|
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return new(
|
|
|
|
|
result.Entries.Count,
|
|
|
|
|
result.CompletedTasks,
|
|
|
|
|
messages,
|
|
|
|
|
tasks.Count,
|
|
|
|
|
tasks.Count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 遗传搜索以完整排课方案作为个体。候选生成、交叉和变异均经修复,
|
|
|
|
|
/// 因而冲突、容量和课程可用范围始终是不可违反的硬约束。
|
|
|
|
|
/// </summary>
|
|
|
|
|
private sealed class GeneticScheduleOptimizer(
|
|
|
|
|
Guid planId,
|
|
|
|
|
IReadOnlyList<TeachingTask> tasks,
|
|
|
|
|
IReadOnlyDictionary<Guid, TeachingTaskScheduleConstraint> constraints,
|
|
|
|
|
HashSet<int> activePeriods,
|
|
|
|
|
IReadOnlyList<Classroom> classrooms,
|
|
|
|
|
IReadOnlyList<ScheduleEntry> baselineEntries,
|
|
|
|
|
ScheduleOptimizationSettings settings)
|
|
|
|
|
{
|
|
|
|
|
private const int MinimumPopulationSize = 18;
|
|
|
|
|
private const int MaximumPopulationSize = 36;
|
|
|
|
|
private readonly Random random = new();
|
|
|
|
|
|
|
|
|
|
public GeneticScheduleOptimizationResult Optimize(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var messages = new List<string>();
|
|
|
|
|
var invalidTaskIds = new HashSet<Guid>();
|
|
|
|
|
var demands = BuildDemands(messages, invalidTaskIds);
|
|
|
|
|
if (demands.Count == 0)
|
|
|
|
|
return new([], tasks.Count - invalidTaskIds.Count, messages);
|
|
|
|
|
|
|
|
|
|
var candidates = demands.Select(BuildCandidates).ToList();
|
|
|
|
|
var populationSize = Math.Clamp(demands.Count * 2, MinimumPopulationSize, MaximumPopulationSize);
|
|
|
|
|
var generationCount = Math.Clamp(demands.Count * 3, 36, 120);
|
|
|
|
|
var population = Enumerable.Range(0, populationSize)
|
|
|
|
|
.Select(_ => CreateIndividual(demands, candidates, cancellationToken))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
for (var generation = 0; generation < generationCount; generation++)
|
|
|
|
|
{
|
|
|
|
|
var targetHours = TeachingTaskHours.TargetHours(task.Course!, kind);
|
|
|
|
|
var scheduledHours = entries
|
|
|
|
|
.Where(x =>
|
|
|
|
|
x.TeachingTaskId == task.Id &&
|
|
|
|
|
x.Kind == kind)
|
|
|
|
|
.Sum(TeachingTaskHours.ScheduledHours);
|
|
|
|
|
var label = kind == ScheduleEntryKind.Experiment ? "实验课" : "理论课";
|
|
|
|
|
if (scheduledHours > targetHours)
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
population = population.OrderBy(individual => individual.Fitness).ToList();
|
|
|
|
|
var nextGeneration = new List<GeneticIndividual>
|
|
|
|
|
{
|
|
|
|
|
messages.Add(
|
|
|
|
|
$"{task.TaskNumber} · {task.Name} 的{label}已安排 {scheduledHours} 学时," +
|
|
|
|
|
$"超过课程规定的 {targetHours} 学时,请先删除多余课次。");
|
|
|
|
|
taskCompleted = false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var remainingHours = targetHours - scheduledHours;
|
|
|
|
|
while (remainingHours > 0)
|
|
|
|
|
population[0].Clone(),
|
|
|
|
|
population[1].Clone()
|
|
|
|
|
};
|
|
|
|
|
while (nextGeneration.Count < populationSize)
|
|
|
|
|
{
|
|
|
|
|
var candidate = FindBestCandidateForHours(
|
|
|
|
|
plan.Id,
|
|
|
|
|
task,
|
|
|
|
|
constraint,
|
|
|
|
|
kind,
|
|
|
|
|
remainingHours,
|
|
|
|
|
activePeriods,
|
|
|
|
|
classrooms,
|
|
|
|
|
entries,
|
|
|
|
|
optimization,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
if (candidate is null) break;
|
|
|
|
|
|
|
|
|
|
db.ScheduleEntries.Add(candidate);
|
|
|
|
|
entries.Add(candidate);
|
|
|
|
|
created++;
|
|
|
|
|
remainingHours -= TeachingTaskHours.ScheduledHours(candidate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (remainingHours > 0)
|
|
|
|
|
{
|
|
|
|
|
messages.Add(
|
|
|
|
|
$"{task.TaskNumber} · {task.Name} 仍有 {remainingHours} 个{label}学时无法安排," +
|
|
|
|
|
(kind == ScheduleEntryKind.Experiment
|
|
|
|
|
? "请检查实验室/机房容量、教师班级冲突或时间约束。"
|
|
|
|
|
: "请检查教师/班级冲突或场地与时间约束。"));
|
|
|
|
|
taskCompleted = false;
|
|
|
|
|
var firstParent = SelectParent(population);
|
|
|
|
|
var secondParent = SelectParent(population);
|
|
|
|
|
var child = Crossover(firstParent, secondParent, demands, candidates);
|
|
|
|
|
Mutate(child, candidates);
|
|
|
|
|
child.Fitness = CalculateFitness(child.Genes);
|
|
|
|
|
nextGeneration.Add(child);
|
|
|
|
|
}
|
|
|
|
|
population = nextGeneration;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (taskCompleted) completedTasks++;
|
|
|
|
|
|
|
|
|
|
processedTasks++;
|
|
|
|
|
if (reportProgress is not null)
|
|
|
|
|
var best = population.MinBy(individual => individual.Fitness)!;
|
|
|
|
|
var unscheduled = demands.Select((demand, index) => new { demand, entry = best.Genes[index] })
|
|
|
|
|
.Where(item => item.entry is null)
|
|
|
|
|
.GroupBy(item => new { item.demand.Task, item.demand.Kind });
|
|
|
|
|
foreach (var group in unscheduled)
|
|
|
|
|
{
|
|
|
|
|
await reportProgress(
|
|
|
|
|
new(tasks.Count, processedTasks, created, completedTasks),
|
|
|
|
|
cancellationToken);
|
|
|
|
|
invalidTaskIds.Add(group.Key.Task.Id);
|
|
|
|
|
var hours = group.Sum(item => item.demand.PeriodCount * item.demand.OccurrenceCount);
|
|
|
|
|
var label = group.Key.Kind == ScheduleEntryKind.Experiment ? "实验课" : "理论课";
|
|
|
|
|
messages.Add(
|
|
|
|
|
$"{group.Key.Task.TaskNumber} · {group.Key.Task.Name} 仍有 {hours} 个{label}学时无法安排," +
|
|
|
|
|
(group.Key.Kind == ScheduleEntryKind.Experiment
|
|
|
|
|
? "请检查实验室/机房容量、教师班级冲突或时间约束。"
|
|
|
|
|
: "请检查教师/班级冲突或场地与时间约束。"));
|
|
|
|
|
}
|
|
|
|
|
return new(
|
|
|
|
|
best.Genes.Where(entry => entry is not null).Select(entry => entry!).ToList(),
|
|
|
|
|
CountCompletedTasks(best.Genes, invalidTaskIds, demands),
|
|
|
|
|
messages);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<ScheduleDemand> BuildDemands(
|
|
|
|
|
ICollection<string> messages,
|
|
|
|
|
ISet<Guid> invalidTaskIds)
|
|
|
|
|
{
|
|
|
|
|
var result = new List<ScheduleDemand>();
|
|
|
|
|
foreach (var task in tasks)
|
|
|
|
|
{
|
|
|
|
|
constraints.TryGetValue(task.Id, out var constraint);
|
|
|
|
|
foreach (var kind in new[] { ScheduleEntryKind.Lecture, ScheduleEntryKind.Experiment })
|
|
|
|
|
{
|
|
|
|
|
var targetHours = TeachingTaskHours.TargetHours(task.Course!, kind);
|
|
|
|
|
var scheduledHours = baselineEntries
|
|
|
|
|
.Where(entry => entry.TeachingTaskId == task.Id && entry.Kind == kind)
|
|
|
|
|
.Sum(TeachingTaskHours.ScheduledHours);
|
|
|
|
|
var label = kind == ScheduleEntryKind.Experiment ? "实验课" : "理论课";
|
|
|
|
|
if (scheduledHours > targetHours)
|
|
|
|
|
{
|
|
|
|
|
invalidTaskIds.Add(task.Id);
|
|
|
|
|
messages.Add(
|
|
|
|
|
$"{task.TaskNumber} · {task.Name} 的{label}已安排 {scheduledHours} 学时," +
|
|
|
|
|
$"超过课程规定的 {targetHours} 学时,请先删除多余课次。");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var remainingHours = targetHours - scheduledHours;
|
|
|
|
|
var weekCount = task.EndWeek - task.StartWeek + 1;
|
|
|
|
|
while (remainingHours > 0)
|
|
|
|
|
{
|
|
|
|
|
var periodCount = remainingHours >= 2 ? 2 : 1;
|
|
|
|
|
var occurrenceCount = Math.Min(weekCount, remainingHours / periodCount);
|
|
|
|
|
if (occurrenceCount == 0)
|
|
|
|
|
{
|
|
|
|
|
periodCount = 1;
|
|
|
|
|
occurrenceCount = 1;
|
|
|
|
|
}
|
|
|
|
|
result.Add(new(task, constraint, kind, periodCount, occurrenceCount));
|
|
|
|
|
remainingHours -= periodCount * occurrenceCount;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<ScheduleEntry> BuildCandidates(ScheduleDemand demand)
|
|
|
|
|
{
|
|
|
|
|
var allowedDays = ParseAllowedDays(demand.Constraint?.AllowedDayOfWeeks);
|
|
|
|
|
var firstPeriod = demand.Constraint?.EarliestPeriod ?? activePeriods.Min();
|
|
|
|
|
var lastPeriod = demand.Constraint?.LatestPeriod ?? activePeriods.Max();
|
|
|
|
|
var rooms = EligibleRooms(demand.Task, demand.Constraint, demand.Kind, classrooms);
|
|
|
|
|
if ((demand.Constraint?.RequiresClassroom ?? true) && rooms.Count == 0)
|
|
|
|
|
return [];
|
|
|
|
|
|
|
|
|
|
var candidates = new List<ScheduleEntry>();
|
|
|
|
|
for (var startWeek = demand.Task.StartWeek;
|
|
|
|
|
startWeek + demand.OccurrenceCount - 1 <= demand.Task.EndWeek;
|
|
|
|
|
startWeek++)
|
|
|
|
|
foreach (var day in allowedDays)
|
|
|
|
|
for (var start = firstPeriod; start + demand.PeriodCount - 1 <= lastPeriod; start++)
|
|
|
|
|
{
|
|
|
|
|
if (Enumerable.Range(start, demand.PeriodCount).Any(period => !activePeriods.Contains(period)))
|
|
|
|
|
continue;
|
|
|
|
|
var roomOptions = demand.Kind != ScheduleEntryKind.Experiment &&
|
|
|
|
|
demand.Constraint?.RequiresClassroom == false
|
|
|
|
|
? new Classroom?[] { null }
|
|
|
|
|
: rooms.Cast<Classroom?>().ToArray();
|
|
|
|
|
foreach (var room in roomOptions)
|
|
|
|
|
{
|
|
|
|
|
var candidate = CreateEntry(demand, startWeek, day, start, room);
|
|
|
|
|
if (CanAdd(candidate, baselineEntries))
|
|
|
|
|
candidates.Add(candidate);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return candidates;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private GeneticIndividual CreateIndividual(
|
|
|
|
|
IReadOnlyList<ScheduleDemand> demands,
|
|
|
|
|
IReadOnlyList<List<ScheduleEntry>> candidates,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var genes = new ScheduleEntry?[demands.Count];
|
|
|
|
|
var state = baselineEntries.ToList();
|
|
|
|
|
foreach (var index in Enumerable.Range(0, demands.Count)
|
|
|
|
|
.OrderBy(index => candidates[index].Count)
|
|
|
|
|
.ThenBy(_ => random.Next()))
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
var selected = SelectFeasibleCandidate(candidates[index], state);
|
|
|
|
|
if (selected is null) continue;
|
|
|
|
|
genes[index] = selected;
|
|
|
|
|
state.Add(selected);
|
|
|
|
|
}
|
|
|
|
|
return new(genes) { Fitness = CalculateFitness(genes) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private GeneticIndividual Crossover(
|
|
|
|
|
GeneticIndividual first,
|
|
|
|
|
GeneticIndividual second,
|
|
|
|
|
IReadOnlyList<ScheduleDemand> demands,
|
|
|
|
|
IReadOnlyList<List<ScheduleEntry>> candidates)
|
|
|
|
|
{
|
|
|
|
|
var genes = new ScheduleEntry?[demands.Count];
|
|
|
|
|
var state = baselineEntries.ToList();
|
|
|
|
|
foreach (var index in Enumerable.Range(0, demands.Count)
|
|
|
|
|
.OrderBy(index => candidates[index].Count)
|
|
|
|
|
.ThenBy(_ => random.Next()))
|
|
|
|
|
{
|
|
|
|
|
var firstChoice = random.Next(2) == 0 ? first.Genes[index] : second.Genes[index];
|
|
|
|
|
var secondChoice = ReferenceEquals(firstChoice, first.Genes[index])
|
|
|
|
|
? second.Genes[index]
|
|
|
|
|
: first.Genes[index];
|
|
|
|
|
var selected = TryAddClone(firstChoice, state) ??
|
|
|
|
|
TryAddClone(secondChoice, state) ??
|
|
|
|
|
SelectFeasibleCandidate(candidates[index], state);
|
|
|
|
|
if (selected is null) continue;
|
|
|
|
|
genes[index] = selected;
|
|
|
|
|
state.Add(selected);
|
|
|
|
|
}
|
|
|
|
|
return new(genes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Mutate(GeneticIndividual individual, IReadOnlyList<List<ScheduleEntry>> candidates)
|
|
|
|
|
{
|
|
|
|
|
var mutationCount = Math.Max(1, individual.Genes.Length / 10);
|
|
|
|
|
for (var mutation = 0; mutation < mutationCount; mutation++)
|
|
|
|
|
{
|
|
|
|
|
var index = random.Next(individual.Genes.Length);
|
|
|
|
|
var state = baselineEntries.Concat(individual.Genes
|
|
|
|
|
.Where((_, geneIndex) => geneIndex != index)
|
|
|
|
|
.Where(entry => entry is not null)
|
|
|
|
|
.Select(entry => entry!)).ToList();
|
|
|
|
|
var replacement = SelectFeasibleCandidate(candidates[index], state);
|
|
|
|
|
if (replacement is not null) individual.Genes[index] = replacement;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (created > 0 && saveChanges)
|
|
|
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
|
|
|
return new(
|
|
|
|
|
created,
|
|
|
|
|
completedTasks,
|
|
|
|
|
messages,
|
|
|
|
|
tasks.Count,
|
|
|
|
|
processedTasks);
|
|
|
|
|
private GeneticIndividual SelectParent(IReadOnlyList<GeneticIndividual> population)
|
|
|
|
|
{
|
|
|
|
|
var first = population[random.Next(population.Count)];
|
|
|
|
|
var second = population[random.Next(population.Count)];
|
|
|
|
|
return first.Fitness <= second.Fitness ? first : second;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ScheduleEntry? SelectFeasibleCandidate(
|
|
|
|
|
IReadOnlyList<ScheduleEntry> candidates,
|
|
|
|
|
IReadOnlyList<ScheduleEntry> state)
|
|
|
|
|
{
|
|
|
|
|
var feasible = candidates.Where(candidate => CanAdd(candidate, state))
|
|
|
|
|
.OrderBy(candidate => CandidateScore(candidate, state))
|
|
|
|
|
.Take(8)
|
|
|
|
|
.ToList();
|
|
|
|
|
return feasible.Count == 0
|
|
|
|
|
? null
|
|
|
|
|
: CloneEntry(feasible[random.Next(feasible.Count)]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int CandidateScore(ScheduleEntry candidate, IReadOnlyList<ScheduleEntry> state) =>
|
|
|
|
|
CalculateSoftConstraintPenalty(
|
|
|
|
|
candidate.TeachingTask!,
|
|
|
|
|
candidate,
|
|
|
|
|
state,
|
|
|
|
|
settings,
|
|
|
|
|
activePeriods.Min(),
|
|
|
|
|
activePeriods.Max());
|
|
|
|
|
|
|
|
|
|
private int CalculateFitness(IEnumerable<ScheduleEntry?> genes)
|
|
|
|
|
{
|
|
|
|
|
var scheduled = genes.Where(entry => entry is not null).Select(entry => entry!).ToList();
|
|
|
|
|
var missingPenalty = (genes.Count(entry => entry is null)) * 1_000_000;
|
|
|
|
|
var score = scheduled.Sum(entry => CandidateScore(entry, baselineEntries.Concat(scheduled)
|
|
|
|
|
.Where(other => !ReferenceEquals(other, entry)).ToList()));
|
|
|
|
|
return missingPenalty + score;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int CountCompletedTasks(
|
|
|
|
|
IReadOnlyList<ScheduleEntry?> genes,
|
|
|
|
|
ISet<Guid> invalidTaskIds,
|
|
|
|
|
IReadOnlyList<ScheduleDemand> demands)
|
|
|
|
|
{
|
|
|
|
|
var missingTaskIds = demands.Where((_, index) => genes[index] is null)
|
|
|
|
|
.Select(demand => demand.Task.Id)
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
return tasks.Count(task => !invalidTaskIds.Contains(task.Id) &&
|
|
|
|
|
!missingTaskIds.Contains(task.Id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool CanAdd(ScheduleEntry candidate, IEnumerable<ScheduleEntry> state) =>
|
|
|
|
|
!state.Any(existing =>
|
|
|
|
|
ScheduleConflictDetector.TimeOverlaps(existing, candidate) &&
|
|
|
|
|
ScheduleConflictDetector.ConflictReason(existing, candidate) is not null);
|
|
|
|
|
|
|
|
|
|
private static ScheduleEntry? TryAddClone(ScheduleEntry? candidate, IReadOnlyList<ScheduleEntry> state) =>
|
|
|
|
|
candidate is not null && CanAdd(candidate, state) ? CloneEntry(candidate) : null;
|
|
|
|
|
|
|
|
|
|
private ScheduleEntry CreateEntry(
|
|
|
|
|
ScheduleDemand demand,
|
|
|
|
|
int startWeek,
|
|
|
|
|
int day,
|
|
|
|
|
int startPeriod,
|
|
|
|
|
Classroom? room) => new()
|
|
|
|
|
{
|
|
|
|
|
SchedulePlanId = planId,
|
|
|
|
|
TeachingTaskId = demand.Task.Id,
|
|
|
|
|
TeachingTask = demand.Task,
|
|
|
|
|
Kind = demand.Kind,
|
|
|
|
|
ClassroomId = room?.Id,
|
|
|
|
|
DayOfWeek = day,
|
|
|
|
|
StartPeriod = startPeriod,
|
|
|
|
|
PeriodCount = demand.PeriodCount,
|
|
|
|
|
StartWeek = startWeek,
|
|
|
|
|
EndWeek = startWeek + demand.OccurrenceCount - 1,
|
|
|
|
|
WeekPattern = WeekPattern.All,
|
|
|
|
|
Notes = demand.Kind == ScheduleEntryKind.Experiment
|
|
|
|
|
? "自动排课 · 遗传算法 · 实验课"
|
|
|
|
|
: "自动排课 · 遗传算法 · 理论课"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private static ScheduleEntry CloneEntry(ScheduleEntry source) => new()
|
|
|
|
|
{
|
|
|
|
|
SchedulePlanId = source.SchedulePlanId,
|
|
|
|
|
TeachingTaskId = source.TeachingTaskId,
|
|
|
|
|
TeachingTask = source.TeachingTask,
|
|
|
|
|
Kind = source.Kind,
|
|
|
|
|
ClassroomId = source.ClassroomId,
|
|
|
|
|
DayOfWeek = source.DayOfWeek,
|
|
|
|
|
StartPeriod = source.StartPeriod,
|
|
|
|
|
PeriodCount = source.PeriodCount,
|
|
|
|
|
StartWeek = source.StartWeek,
|
|
|
|
|
EndWeek = source.EndWeek,
|
|
|
|
|
WeekPattern = source.WeekPattern,
|
|
|
|
|
Notes = source.Notes
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private sealed record ScheduleDemand(
|
|
|
|
|
TeachingTask Task,
|
|
|
|
|
TeachingTaskScheduleConstraint? Constraint,
|
|
|
|
|
ScheduleEntryKind Kind,
|
|
|
|
|
int PeriodCount,
|
|
|
|
|
int OccurrenceCount);
|
|
|
|
|
|
|
|
|
|
private sealed class GeneticIndividual(ScheduleEntry?[] genes)
|
|
|
|
|
{
|
|
|
|
|
public ScheduleEntry?[] Genes { get; } = genes;
|
|
|
|
|
public int Fitness { get; set; }
|
|
|
|
|
public GeneticIndividual Clone() => new(Genes.Select(entry => entry is null ? null : CloneEntry(entry)).ToArray())
|
|
|
|
|
{
|
|
|
|
|
Fitness = Fitness
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed record GeneticScheduleOptimizationResult(
|
|
|
|
|
IReadOnlyList<ScheduleEntry> Entries,
|
|
|
|
|
int CompletedTasks,
|
|
|
|
|
IReadOnlyList<string> Messages);
|
|
|
|
|
|
|
|
|
|
private static ScheduleEntry? FindBestCandidateForHours(
|
|
|
|
|
Guid planId,
|
|
|
|
|
TeachingTask task,
|
|
|
|
|