Agenda.Preference (Agenda v0.1.0)

Copy Markdown View Source

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.

t()

A named, weighted soft constraint.

Functions

What each preference contributed, as sentences.

Build a preference.

What a layout costs against preferences.

Types

context()

@type context() :: %{programme: Agenda.Programme.t(), pool: list()}

The information a preference may consult beyond the placements.

t()

@type t() :: %Agenda.Preference{
  count: (list(), context() -> non_neg_integer()),
  name: atom(),
  weight: number()
}

A named, weighted soft constraint.

Functions

explain(preferences, arrangements, context)

@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

  • preferences is a list of t/0.

  • arrangements is the placements to score.

  • context is a context/0.

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"]

new(preference, options \\ [])

@spec new(
  atom() | {atom(), function()},
  keyword()
) :: {:ok, t()} | {:error, term()}

Build a preference.

Arguments

  • preference is a built-in name — :room_changes, :room_spread or :resource_wishes — or a {name, function} pair for one of your own.

Options

  • :weight is how much each violation costs. The default is 1.

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}}

score(preferences, arrangements, context)

@spec score([t()], [Agenda.Arrangement.t()], context()) :: number()

What a layout costs against preferences.

Arguments

  • preferences is a list of t/0.

  • arrangements is the placements to score.

  • context is a context/0.

Returns

  • the total penalty, where 0 is ideal.

Examples

iex> Agenda.Preference.score([], [], %{programme: Agenda.programme("C"), pool: []})
0