What makes one workable layout better than another.
Every constraint elsewhere in this library is hard: a room is eligible or it is not, two sessions clash or they do not. A preference is soft. It never makes a layout invalid, it only makes it worse — and a programme with none is exactly as correct as one with several, just less opinionated about which of the workable answers you get.
Penalties, not rewards
A preference counts violations, and lower is better. Counting what is wrong rather than what is right means the ideal layout scores zero, which is a number that means something on its own — where a reward total only means something next to another reward total.
What is and is not promised
Agenda.Arranger.arrange/3 optimises lexicographically: it
first places as many sessions as it can, provably, and only then
prefers a better score among the layouts it reaches. The count is
proven; the score is not, and Agenda.Layout's score_proven?
says which you have. This is a deliberate limit — proving soft
optimality means bounding a weighted cost, and no bound that cheap
exists here. A programme that genuinely needs it wants a solver.
Writing your own
A preference is a name, a weight, and a function from the placements to a violation count:
Agenda.Programme.prefer(programme, {:no_friday_afternoons, &count_friday_afternoons/2},
weight: 3)The function receives the arrangements and a context carrying
:programme and :pool, so it can consult the tracks and the
resources as well as the placements.
Summary
Types
The information a preference may consult beyond the placements.
A named, weighted soft constraint.
Functions
What each preference contributed, as sentences.
Build a preference.
What a layout costs against preferences.
Types
@type context() :: %{programme: Agenda.Programme.t(), pool: list()}
The information a preference may consult beyond the placements.
@type t() :: %Agenda.Preference{ count: (list(), context() -> non_neg_integer()), name: atom(), weight: number() }
A named, weighted soft constraint.
Functions
@spec explain([t()], [Agenda.Arrangement.t()], context()) :: [String.t()]
What each preference contributed, as sentences.
A single number says a layout is worse without saying how, which is
the same failure explain/2 exists to avoid for eligibility.
Arguments
Returns
- one sentence per preference, in the order they were declared.
Examples
iex> {:ok, preference} = Agenda.Preference.new(:room_changes, weight: 10)
iex> context = %{programme: Agenda.programme("Conf"), pool: []}
iex> Agenda.Preference.explain([preference], [], context)
["room_changes: 0 × 10 = 0"]
Build a preference.
Arguments
preferenceis a built-in name —:room_changes,:room_spreador:resource_wishes— or a{name, function}pair for one of your own.
Options
:weightis how much each violation costs. The default is1.
Returns
{:ok, t:t/0}; or{:error, reason}when the name is neither a built-in nor paired with a function.
Examples
iex> {:ok, preference} = Agenda.Preference.new(:room_changes, weight: 10)
iex> {preference.name, preference.weight}
{:room_changes, 10}
iex> Agenda.Preference.new(:teleportation)
{:error, {:unknown_preference, :teleportation}}
@spec score([t()], [Agenda.Arrangement.t()], context()) :: number()
What a layout costs against preferences.
Arguments
Returns
- the total penalty, where
0is ideal.
Examples
iex> Agenda.Preference.score([], [], %{programme: Agenda.programme("C"), pool: []})
0