Advent of Haskell 2020: Santa Wrap
On Thu, 24 Dec 2020, by @lucasdicioccio, 5099 words, 26 code snippets, 13 links, 11images.
This article is part of Advent of Haskell 2020 đ. Make sure to check the other articles too! You will find the code for this blog-post at the original GitHub publication.
Our Christmas project
Year 2020 is terrible. Santaâs Workshop had to take stay-at-home restrictions. However, Elves want to do the utmost to keep Christmasâ spirit alive. This year, Elves will work from home rather than work in the streamlined workshop đ . They will get back to a more traditional way of preparing presents, like Elderly Elves who live more than 500 years, remember. In short, every Elf works in their own garage đ . They receive the Christmas wishlists of neighboring families: crafting toys đŸ, sewing clothes đ, or cooking delicious cookies đȘ.
One particularly annoying task for work-from-home Elves, however, is to wrap presents đ. They had avoided this task in the recent years thanks to automation. Elves all have different pieces of wrapping-paper at home, in particular wrapping-paper rolls vary in ratio and vary in size from one Elf to another one. Elves want some software to help them cut wrapping-paper for the presents they prepare âïž. Some Elves have tools to cut paper (think of a cookie-cutter, but for large sheets of paper), whereas others only have cissors. Wrapping may become scarce if used inefficiently, and anyway ordering takes time. Thus, they would like to have a rough idea of how to efficiently cut the paper rolls given their own specific constraints. It does not have to be perfect, but it cannot be completely naive either. Thus, Elves need some creative idea to save Christmasâ spirit. For the better or for the worst Elves resorted to use software đ».
Fortunately, an Elf has learnt Haskell, a language especially good for building small command line tools and with libraries to generate images. He also has learnt MiniZinc, a language especially good for solving this family of problems. Put together, these languages can help building a small tool that Elves can run at home with their own input. This computer-savvy Elf suggests the following #trade-off:
- Elves will input the quantity and shape of the wrapping for the presents (or the shape of their cookie-cutter tools that fit for the present)
- the shape of the wrapping is blocky, that is, made of various rectangle blocks, rather than arbitrarily shaped (it simplifies cutting and folding paper with rulers and guides anyway)
- Elves will also input the size of their own paper sheets
- the output is a footprint image of how to cut the paper to fit all present (when possible) Some Elves expected more specialized tools, some cared especially about saving wrapping paper. However, time is running out, our software Elf-ineer only has a day to build this software or it will be too late to save Christmas.
I am not sure I should reveal this, but, I work in Santa Incorporated oh oh oh.
Do not tell your kids! in exchange Iâll explain you how I built santa-wrap
in
a rush to help the Elf. Thus, the main goal for this article is to relate how
to iterate quickly on a practical one short day project. Secondary goals are to
pike your interest about MiniZinc and introduce you a library named
minizinc-process
.
Approach
The type of problem for cutting wrapping paper is known as a Packing problem. These problems are generally difficult to solve efficiently. However, for small sizes or particular form of constraints, some solutions could be efficiently found (or the litterature may have some heuristics that work well empirically). Generally, the difficulty comes from integrality of constraints, that is you either can wrap a present or not wrap the present, you cannot wrap a fraction of a present. Such problems come up a lot in industrial settings where some resource is scarce. In this particular case, we cheated a bit as we came with an idea to write a blog post. However, with a bit of practice you will recognize packing problems everywhere.
We want to show off how to combine MiniZinc (a mathematical #optimization language and toolchain) with Haskell (which readers of this Advent of Haskell submission are familiar with). We will show the progression of a tool, with building a value-adding prototype in a day or two of work for some Elf familiar with Haskell and Minizinc.
Motivating our Iterations
The author built this tool in four broad iterations: implement a demo solver in MiniZinc, generate simple pictures for each wrapping, introduce the concept of layout, and, bridge the solver. We motivate each iteration before jumping into lengthier explanations.
Implemeting a solver in MiniZinc: I wanted to leverage powerful global
constraints with their own heuristics/optimized solutions that exist in
MiniZinc. Such a model helps us formalize the problem in better terms and
understand what are inputs and what are outputs. In a business settings, such
prototypes allow to raise the important questions. In that case, the whole
problem is solved with a single global contraint named geost_bb
. Knowing
which constraints help modeling a problem is a type of expertise that takes
time to acquire but that can change drastically the viability of such a tool.
Generate simple pictures: our tool needs to generate pictures of the wrapping, and I find a blog-post with picture more appealing than without. Visualizations illustrate the problem. I am not familiar with low-level graphics libraries. I typically use high-level tools for my charts like ggplot in R â they typically get in the way if what you want is pixel-perfect position of image width/height. For this exercise, I found JuicyPixels and I need to validate quickly that JuicyPixels will fit the needs. This library is appealing because I could generate PNGs with little dependencies. I had some experience with Gloss. However, I discarded this choice for I only need static images and not animations (and Gloss has way more dependencies than JuicyPixels).
Introduce the concept of layout: pictures become more interesting when we start to have multiple boxes. Once I know how to print a layout I am done with the graphical aspects of the problem. Minor improvements can still come later but it is mind-relaxing to call something done. Further, at this point it is easy to show some naive layouts.
Bridge the solver: this is the boiler-plate part of the work.
minizinc-process
actually have a TemplateHaskell helper to generate an input
and output representations but this approach is limited: it cannot work with
sets in its current version and does not preclude writing boilerplate to
transform between âbusiness-domainâ representations and solver input/output
representations. Plus, the goal of the article is to illustrate the pro/cons.
This part of boiler-plate is the one requiring the most care, hence my usage of
newtypes to differentiate indices.
Graphically, the following dependency graph shows the various steps I had in mind. In red are the two chunks of work which I assessed has being the most uncertain (i.e., requiring the most discovery) or most meticulous (i.e., spending time building accurate types to reduce the risk of bugs).
Letâs discuss how this four iterations looked like.
The four iterations
We now dive in more details into each iteration. Recall that I time-boxed this project to the equivalent of a short-day-of-work. Readers should be able to read these independently.
A solver in MiniZinc
We basically call the global contstraint geost_bb
. Letâs unpack how it
works, starting from a small MiniZinc overview and a mini tutorial on
geost_bb
.
In MiniZinc you separate the structure of a problem (which we call a model) and its particular instance (determined by input parameters). Thus a model can run for various input parameters. Further, the model separate two families of variables: the par variables (parameters that are given or can be deterministically computed from other parameters) and var variables (decisions that the solver will make). Constraints are statement that link variables together. The solver, then works out solutions (that is assignments of variables) that are consistent with all constraints. It is typically easy to verify that a given assignment matches all constraints. For instance, in our Santa-Wrap tool can visually inspect whether or not cutting guides for different present intersect. Whereas, it is typically hard to find a given assignment for all possible par inputs (or an optimal one when a criteria of goodness exists).
In Haskell parlance: solver :: Model -> Parameters -> Maybe Decisions
, which,
partially-applied for some Model
would give solveProblem :: Parameters -> Maybe Decisions
. To encode this faithfully in Haskell we would need to ensure
that the Parameters
and Decisions
types actually depends on the Model
;
somehow, discussing these type-level programming techniques is out of scope for
this article.
Typically, writing MiniZinc is a bit like SQL queries: you describe what you want and let the engine fetch it for you. The ability to summon a magic tool to solve our wishes comes with some limitations: expressivity is limited, and you need to be careful what you wish for. For instance, when writing MiniZinc you need to discuss about finite inputs/outputs (e.g, no unbounded recursion). Further, small changes on the structure of the problem can drastically change performance (like switching from a index scan to a table scan in SQL query plan).
The comparison with SQL does not stops at the declarative aspect. Typical
domain models in Haskell have collections such as
Lists
or
Maps
or custom recursively-shaped objects etc. Once mapped into tabular form in SQL, special
conversion care is required. For instance, in Haskell, a type User { age :: Int , height :: Int }
would map to a set of integers (one user-id per user)
and two arrays indexed by user-id. So that Haskell domain objects like these.
users :: [ User ]
= [ User 23 172, User 45 180, User 36 165 ] users
Would translate to the following MiniZinc.
set of int: USER = 1..3;
array[USER] of int: age = [ 23, 45, 36 ];
array[USER] of int: height = [ 172, 180, 165 ];
Readers familiar with Relational Algebra, DataParallel Haskell, Columnar
Storage, or Entity Component System will find comonalities. We will work this
out for our problem in more detail in the last iteration. So now letâs look at
geost_bb
.
Here is a snapshot of geost_bb
documentation
. Which sorts of packs blocky-structure on a
multi-dimensional space. This constraint does not try to shrink gaps between
objects, it âmerelyâ (I donât want to downplay how great this is) force
assignments of variables such that no overlap exists between the packed
objects.
We annotate the parameters below to explain why using this function make sense in Santa-Wrap. Besides the opaque name, it does exactly what we want to help the Elves and even a bit more.
- k: here we live in two dimensions, that an easy parameter, weâll hardcode it
- rect_size: size of each rectangles along each dimension
- rect_offset: offset of rectangles long each dimension
- shape: wrapping shapes are built from a set of a rectangles, for instance presents we can wrap in a simple rectangle would have a single rectangle. More elaborated wrapping shapes may require two or more adjacent rectangles. For instance the sword here is built with two overlapping rectangles: a rectangle is horizontal (largest rect_size is the 1st dimension) and another rectangle is vertical (largest rect_size is the 2nd dimension) and both rectangles are translated along an axis (i.e., one non-zero rect_offset). Multiple shapes could re-use the same rectangles if they happen to match (hence, saving a bit on the complexity â but we wonât go into this level of sophistication).
- l and u: upper and lower bounds for each wrapping, in our case all objects can equally be positioned on the wrapping, hence the lower bound is 0 and the upper bound the size of the roll (minus one if you care about millimeters and took no margins of error at all in your measurements)
Then geost_bb
then contributes constraints to tell the solver to make a
number of decisions:
x
: the coordinates of the wrapping shapes (which are themselves built of rectangular blocks). For our sword example, we need to add therect_offset
to thex
coordinate of the shape to position a rectangle.kind
: the shape used by each object. This mechanism allowsgeost_bb
to chose any shape for the different objects. External constraints actually preventgeost_bb
does not know how many times it must be using the wrapping-shape for a sword, and unconstrained the solver would rather prefer the simpler wrapping-shape for a book. This mechanism actually is a feature: the freedom to pick among shapes allows to model rotation of shapes, we will not use this for now.
In our case, for a first version of the tool we do not need to make that many
decisions: only x
above is required. Declaring all inputs to give to
geost_bb
is required, however. We still need to actually declare our par
and var
variables.
We will have to discuss about a number of entities:
- two dimensions:
int: nDims = 2; set of int: DIM = 1..nDims
- wrapping shapes:
int: nShapes; set of int: SHAPE = 1..nShapes
- rectangular blocks:
int: nBlocks; set of int: BLOCK = 1..nBlocks
- each object we need to wrap:
int: nObjs; set of int: OBJ = 1..nObjs
- the roll: it is implicit that we cut one roll, so we do not need to declare anything
Then, some characteristics of entities:
- the size of the roll:
int: x_max; int: y_max
- the size of individual rectangular blocks and their offsets in each dimension:
array[BLOCK,DIM] of int: rect_sizes; array[BLOCK,DIM] of int: rect_offs
- the coordinates of where to position cuts for our solution:
array[OBJ,DIM] of var int: coordinates
Finally there is relational information linking various entities.
- to each object we associate a given shape (present1 is a Guitar, present2 is a book etc.):
array[OBJ] of SHAPE: object_shapes;
- each shape is the collage of a set of rectangle, we could use various
external representation to model the set of rectangles (e.g., an
array[SHAPE,RECTANGLE] of bool
is isomorphic to anarray[SHAPE] of set of RECTANGLE
) but we will stick to theset
representation thatgeost_bb
requires (using another input would require extra constraints to link input andgeost_bb
representations)
All in all, the model is in models/santa-wrap.mzn
and an example input for it is models/santa-wrap001.dzn
. You can run it with minizinc models/santa-wrap.mzn models/santa-wrap001.dzn
. And the output I got was:
coordinates = array2d(1..10, 1..2, [8, 0, 0, 6, 7, 0, 0, 5, 6, 0, 0, 4, 4, 0, 0, 2, 2, 0, 0, 0]);
Which you can read as âfirst objectâ is a (8,0), the âsecond objectâ is at (0,6) etc. Elves could work with such an output, however a visual display is better. Plus they want to prepare their cutting boards, build guides, or simply pass an eye-check that things are in the right place.
Generate simple pictures
I have had little interest in graphics in the past. Thus I vaguely know some
Haskell libraries exist like
diagrams, gloss
,
JuicyPixels
, or reanimate.
For this project, I had to dive a bit more and actually pick some library. My
choice settled on JuicyPixel
for two main reasons: on the first hand, this
library has only a small amount of dependencies (especially system dependencies
â gloss
requires some OpenGL). On the other hand, the documentation was
welcoming: I looked for IO ()
because I looked for some âmainâ or âprintâ
equivalent. The first hit on the top module is generateImage
. No kidding, here
is the screenshot to prove it: first hit .
The type information tells me the library actually scans every pixel one by one. It is very likely super inefficient. That said, the cost of optimizing the packing is the major bottleneck in this problem. Elves can wait a minute to save hours. A warming case that does not corroborate the claim that Haskell libraries are poorly documente. My first action was to verify that the example actually works.
cabal repl
> import Code.Picture
> let path = "images/background.png"
> let pixelRenderer x y = PixelRGB8 (fromIntegral x) (fromIntegral y)
> writePng path $ generateImage pixelRenderer 250 300
And voila! thank you so much JuicyPixels
. Itâs not
âtoo good to be trueâ: the example works.
Having secured that I can generate PNG, what is left? we need to define some Haskell data types to map loosely with what Iâve learnt is required in the MiniZinc optimization. However weâll be using Haskell records rather than columnar values. The idea here is to use these data types as main Haskell types for parsing a format that Elves are likely to know such as JSON. Further we want to allow Elves to pass extra information like quantities of each present they will wrap, colors and names for pictures before crafting the guides etc.
Some excerpt:
data Wrapping = Wrapping {
name :: Name
colorNum :: Int
, quantity :: Quantity
, rectangles :: [ Rectangle ]
,deriving (Show, Generic)
} instance FromJSON Wrapping
data Rectangle = Rectangle {
rectX :: Length
rectY :: Length
, offX :: X
, offY :: Y
,deriving (Show, Generic)
} instance FromJSON Rectangle
Wrappings are built of Rectangles. Each Rectangle as length/width (lengths in
both dimensions rectX
, rectY
) as well as some offset in both dimension. In
addition, each Wrapping has a name, a quantity and some color to visualize more
easily a layout. But before speaking about Layouts, what we can do is to
generate pictures for each Wrapping. This way, Elves can calibrate the units
and verify individual length.
I need to adapt the JuicyPixels
example I copy pasted to build a function
printWrapping :: Wrapping -> IO()
. In short, it has a Wrapping
and its
associated Rectangles
in scope and we need to implement pixelRenderer
.
So Iâll build utility functions findRectangle x y
which finds a rectangle
with the right sizes and offset to contain the pixel (x,y)
.
A single pixel and rectangle test is
=
rectangleContainsPixel x y rect let x0 = scale $ offX rect
= scale $ offX rect + rectX rect
x1 = scale $ offY rect
y0 = scale $ offY rect + rectY rect
y1 in x0 <= x && x < x1 && y0 <= y && y < y1
Which checks for intervals in both X and Y. The scale
function just
multiplies tenfold every magnitude (i.e., a dimension of 1 Length is shown with
10 pixel). With that utility function we can now test every rectangles: if some
is found, we color the Pixel with some value, otherwise we use default
background value.
=
pixelRenderer x y case findRectangle x y of
Nothing -> PixelRGB8 (fromIntegral x `mod` 32) (fromIntegral y `mod` 32) 128
Just idx -> pixelColor idx
= List.findIndex (rectangleContainsPixel x y) (rectangles wrapping) findRectangle x y
I use List.findIndex
so that on a hit i can also attribute some color (see
pixelColor :: Int -> PixelRGB8
which defines a palette) and paint the
rectangle in a different color.
Another utility function is needed to give JuicyPixel
the size of the picture
to create. This size corresponds to the farthest point in the set of off-set
rectangles maxX = maximum (0 : [ rectX r + offX r | r <- rectangles wrapping ])
, I named this value the extentX
of a Wrapping
in later commits. I use
comprehension list notation. I add a 0 so that maximum
does not crash on an
empty list.
Our main
function basically parses stdin
into some [Wrapping]
and
traverse
this list with our PNG generator.
= do
mainParts <- eitherDecode' @[Wrapping] <$> ByteString.getContents
wrappings case wrappings of
Left err -> print err
Right xs -> traverse_ printWrapping xs
putStrLn "done!"
The parsing leverages the automatically-generated instance FromJSON
from
aeson
(extension DeriveGeneric
). Note that we use TypeApplications
for
the syntax @[Wrapping]
which explicitates what we are decoding from the
content in stdin
, which also helps GHC provide less general error message
when I change and break things.
This way, our main binary can generate individual images.
$ santa-wrap parts < input/wrapping001.json
$ done!
The parts are individual .png
files as this listing shows:
. Elves can use these parts to prepare some guides to
later cut wrapping paper rapidly.
Now letâs finally introduce the concept of layout.
Introduce the notion of layout
The layout is a collection of positioned wrappings. Thereâs an obvious Functor
instance for Positioned. I could refactor Rectangle into a Positioned Rect.
This way, a Layout is a [ Positioned [ Positioned Rect ] ]
and it is probably
collapsable into a single list by translating the inner Positioned objects.
Something like flatten :: [ Positioned [ Positioned a ] ] -> [ Positioned a ]
I feel tempted to actually dig in this direction. However, Iâm far from done
and the clock ticks. In short, I will save this for later and perform a good
amount of copy-pasting.
The Haskell code is simply:
data Positioned a = Positioned {
posX :: X
posY :: Y
, item :: a
,deriving (Show, Functor)
}
data Layout = Layout {
positionedWrappings :: [ Positioned Wrapping ]
deriving (Show) }
And now my printLayout
function has a very similar pixelRenderer
.
Two distinctions though:
- to color a pixel you need to determines if a
Wrapping
has aRectangle
and theRectangles
are translated byposX
andposY
. - we want to add a few pixels of
margin
so that a series of adjacent squares do not appear like a long rectangle, this small margin will ease verifying layouts at a glance.
Hence, our rectangleContainsPixel
test takes all these things into account:
rectangleContainsPixel :: X -> Y -> Int -> Int -> Rectangle -> Bool
=
rectangleContainsPixel posx posy x y rect let x0 = scale $ posx + offX rect
= scale $ posx + offX rect + rectX rect
x1 = scale $ posy + offY rect
y0 = scale $ posy + offY rect + rectY rect
y1 in (x0 + margin) <= x && x < (x1 - margin) && (y0 + margin) <= y && y < (y1 - margin)
How do you verify that the margin is pretty enough? Simply generate a naive Layout function. We just lay out shapes left-to-right.
linearLayout :: [ Wrapping ] -> Layout
= Layout $ List.zipWith (\w (x,y) -> Positioned x y w) wrappings xys
linearLayout wrappings where
xys :: [(X,Y)]
= List.scanl placeToTheLeft (0,0) wrappings
xys
placeToTheLeft :: (X, Y) -> Wrapping -> (X, Y)
= (x + extentX w, 0) placeToTheLeft (x,y) w
The interesting aspect is the use of List.scanl
to perform a fold an
accumulate intermediary values. The accumulator is the position for the next
Wrapping
(hence, we start at (0,0)
and then we move right by an amount
large enough to ensure there will be no overlap: what we named the extent of
the wrapping shape.
This function does not even take care of the bounds of the roll. This function
is merely useful to have some comparison point and to exercise the
printLayout
function.
Running the following command will give the image below.
$ santa-wrap linear-layout ./layout.png < input/wrapping003.json
done!
Now we see the margins, the coloring. We also have a MiniZinc model for non-naive layouts. Letâs bring the two things together.
Actually bridge the Haskell and MiniZinc code
Showcasing my minizinc-process
library is, a main motivator (second to saving
Christmasâ spirit, though).
Remember that, we want to approach ergonomics for the solver so that in
Haskell-land it feels like we have a function solver :: Model -> Parameters -> Maybe Decisions
. Here enters minizinc-process
, an helper library that
actually uses MiniZinc support for JSON input/outputs. This is feasible with
runLastMinizincJSON
, which has type:
runLastMinizincJSON ::
ToJSON input, FromJSON answer) =>
(MiniZinc input answer ->
->
input IO (Maybe answer)
= ... runLastMinizincJSON
The typeclass constraints on input
and output
allow to serialize back and
forth using JSON. The Minizinc input answer
actually contains the model
(e.g., a filepath to the models/santa-wrap.mzn
file) and parameters for
driving MiniZinc (i.e., which solver to use, whether there is a timeout or to
search forever, but also, how to create intermediary .json
input files that
play the role of the models/santa-wrap001.dzn
for each different possible
input). For this project, we use the simpleMiniZinc
constructor to get a
Minizinc
that also adds some Hashable
constraint on the input type so that
we can generate filenames. Fortunately, Hashable
is generically-derivable as
well so little boilerplate is incured.
The runLastMinizincJSON
functions runs for up to the timeout provided in the
MiniZinc option and return the latest decisions (if there is any). A more
general function runMinizincJSON
exists. This function takes a state-machine
named ResultHandler
to handle individual results. Indeed, we can ask MiniZinc
to return multiple solutions, prove that there are no more solutions, and
handling this variety of cases asks for more complicated types.
The underlying type are:
data SearchState a
= Exhausted a
| Incomplete a
| Unsatisfiable
| InternalError String
data ResultHandler obj b
= ResultHandler
handleNext :: b -> SearchState obj -> IO (b, Maybe (ResultHandler obj b))
{ }
The result handler is parametrized by obj
which are the decisions that the
solver makes, and the handler is also parametrized by b
â an accumulator
state like in a fold. A handler is called each time the search changes
SearchState. The handler is free to do what it wants but must return an updated
state and an updated handler (or an absence of handler to tell the library that
we can stop the search). Later versions of the library will likely generalize
IO in the handler to some parametric class.
There is not much more to minizinc-process
. Letâs use it in practice.
We need to translate two worldviews for the same information, back and forth. MiniZinc expects data in array shapes and other parameters giving the length of these arrays whereas our Haskell data is meant to help interactions with users. Hence, the Haskell datatypes closely follow Elf-semantics. Typically, translation code between two worldviews benefits from careful newtypes to distinguish array indices. Indeed, every entity becomes identified by an index and it is all too common to make an error. Multi-dimensional arrays have some support for types in MiniZinc but the data serialized in JSON do not.
The risk we want to reduce is to mix-up array indices and mix-up the
terminology. This is where newtypes shine in Haskell. Two extensions,
DerivingStrategies
and GeneralizedNewtypeDeriving
come handy to
save on boilerplate.
newtype ShapeIdx = ShapeIdx Int
deriving stock Show
deriving stock Eq
deriving newtype Hashable
deriving newtype ToJSON
newtype BlockIdx = BlockIdx Int
deriving stock Show
deriving newtype Hashable
deriving newtype ToJSON
We can now write an Haskell data type for the input. This data type has fields
mapping closely the names of par
variables in MiniZinc (which you find
declared in the ./models/santa-wrap001.dzn
). Arrays map to lists, two
dimensional arrays become nested lists. Sets required me to define an
indirection named MinizincSet
(will be merged upstream) to match MiniZinc
expectations for its JSON inputs. Thus, my Input and Output types become.
data Input = Input {
nBlocks :: Int
rect_sizes :: [[Int]]
, rect_offs :: [[Int]]
, nShapes :: Int
, shapes :: [ MinizincSet BlockIdx ]
, nObjs :: Int
, object_shapes :: [ ShapeIdx ]
, x_max :: Int
, y_max :: Int
,deriving (Show, Generic)
} instance Hashable Input
instance ToJSON Input
data Output = Output {
coordinates :: [ (X, Y) ]
}deriving (Show, Generic)
instance FromJSON Output
The rest of the work now requires to build a convertInput
function that takes
Haskell domain values, map that into an Input. This mapping will require to
give integral IDs to entities. For instance, each individual Wrapping
in the
input requires as many OBJECT
as the quantity
requires.
I typically use list-comprehensions and zipping with [1..]
to achieve this indexing.
For instance, each Wrapping
has one SHAPE
in our problem.
Thus, we provide an indexed list of shapes and its âoriginatingâ Wrapping
as follows.
indexedWrappings :: [(ShapeIdx, Wrapping)]
= zip (fmap ShapeIdx [1..]) wrappings indexedWrappings
The BLOCK
and their rect_sizes, rect_offs
are a bit more complicated. To
build everything we want, we need a âtableâ with, for each SHAPE, what BLOCK it
is made of, and the âoriginatingâ Rectangle
to get carry the sizes and
offsets.
indexedRects :: [(ShapeIdx, BlockIdx, Rectangle)]
= zipWith (\blockIdx (shapeIdx, r) -> (shapeIdx, blockIdx, r))
indexedRects fmap BlockIdx [ 1.. ])
(| (shapeIdx,w) <- indexedWrappings, r <- rectangles w ] [ (shapeIdx, r)
We can then compute the number of BLOCK
, which is nBlocks = length indexedRects
. An optimization could have been to share equal blocks when
shared across SHAPEs
(i.e., when a same Rectangle
appears in two
Wrappings
). Letâs not suffer from early optimization.
We can now build the rect_sizes
and rect_offs
of each BLOCK
by directly
looking at the values associated in the âoriginatingâ Rectangle
.
= [ [rectX r, rectY r] | (_,_,r) <- indexedRects ]
rect_sizes = [ [offX r, offY r] | (_,_,r) <- indexedRects ] rect_offs
and the relational information âwhich SHAPE contains which BLOCKâ is a reading
of the âjoinâ of the indexedRects
and the indexedWrappings
tables. Again,
we use comprehension-lists to the cardinal product of the two tables (and
collecting that in MinzincSet
).
= [ MinizincSet
shapes | (shapeIdx2,blockIdx,_) <- indexedRects
[ blockIdx == shapeIdx2
, shapeIdx1
]| (shapeIdx1,_) <- indexedWrappings
]
Performance could become a concern for large amount of Wrapping
and large
amount of Rectangle
. However, again, we are not chasing seconds in
serialization/translation code as we are ready to spend minutes of solver-time
to saves hours to our Elves. If data proves this piece of code is the
bottleneck in practice, we can then try to optimize further this type of code.
Since the code is pure, it is really amenable to property testing under
QuickCheck or HedgeHog: for any data, both the (slow)-reference and
(putatively-optimized)-candidate functions are returning equal outputs.
Finally, our MiniZinc model wants OBJECT
, one per present rather than one per
Wrapping objects (the Haskell data model holds a quantity). I mistakenly did
two things at a time: expanding quantities and associating a Shape to each
Wrapping. flattenedWrappings
in the heat to release something early for our
Elves.
flattenedWrappings :: [(ShapeIdx, Wrapping)]
= mconcat [ replicate (quantity w) (shapeIdx, w) | (shapeIdx, w) <- indexedWrappings ] flattenedWrappings
This code means that if we have Wrapping { name = abc , quantity = 2 }
and Wrapping {name = def, quantity = 5}
then we have to lay out seven OBJECT
, two of SHAPE
associated to abc
and five of SHAPE
associated to def
. The corresponding boilerplate is:
= length flattenedWrappings
nObjs = fmap fst flattenedWrappings object_shapes
I think this covers most of the boiler-plate. Extra function arguments are the size of the roll
= sizeX
x_max = sizeY y_max
It is tempting to just write the âforward translation codeâ and call it a day. In reality, if you were a consumer of such a library, you would like to minimize this horrendous boiler plate, and you still need some âreverse translation codeâ. Be kind to your users. You have done a lot of work to create âindexedâ arrays once, youâd rather not duplicate this code. Even if you do not duplicate the code, having colocated everything within the constraints of a pure function will guarantee you that no non-determinism creeps in. Hence, you should make a âreverse translation functionâ at the same time as you translate forward. Bonus: you can guarantee that calls of unsafe functions (like array indexing) actually are safe because you are writing the providing and consuming dynamically-shaped data at the same place.
Indeed, because we have access to flattenedWrappings
our âreverse translation
codeâ is a single line.
layoutOutput :: Output -> Layout
Output coords) = Layout
layoutOutput ($ zipWith (\w (x,y) -> Positioned x y w) [w | (_,w) <- flattenedWrappings] coords
Writing all this boilerplate takes time (hence why I circled it red at the
beginning in my dependency graph for the tasks in this project). Such
translation code would be hard to generate in the general case. Hence I do not
have very good solutions but to be careful and use newtype
for each index.
The good news, however, is that it is pretty easy to debug: generate simple
cases with one then two Wrapping
, observe the generated .json
file.
At last we can be gratified. A simple main
that ressembles the one for
linearLayout
allows us to generate layouts for given roll sizes.
$ santa-wrap minizinc-layout ./layout-190x150.png 190 150 < input/wrapping003.json
done!
$ santa-wrap minizinc-layout ./layout-170x160.png 170 160 < input/wrapping003.json
done!
$ santa-wrap minizinc-layout ./layout-170x159.png 170 159 < input/wrapping003.json
no layout found!
The first command greets us with the following picture
. We can try shrinking the Y-coordinates,
160 is still good, but 159 is too hard (note: it is actually infeasible but the
runLastMinizincJSON
function makes no distinctions between timeout and
unfeasible). Visual inspection of the Y=160 case seems to show
that the two cross-shaped and the big rectangle are the main reasons to limit
the roll size .
Convince yourself with this picture (done by modifying the input JSON to delete everything but the shoes and the sword entries: .
OK, with this santa-wrap
tool, Elves are in good shape to save Christmasâ
spirit. Thank you MiniZinc and Haskell!
For the fun try packing that yourselves (input is input/wrapping005.json
)
Discussion
Given more times, a number of possible improvements could happen. For a work project more discussions with the Elves would help adding more constraints to the Model. We next discuss some possible âimprovementsâ.
Possible improvements
Elves have deadlines too, although they would love to keep improving their tools, they have other important (albeit slightly more mundane) tasks at their day jobs. Besides the discussion about Positioned being an interesting abstraction, I want to discuss improvements to the solver/problem itself and illustrate how business ideas are generated/pruned with such discussions.
A user may suggest improvements to the tool, but not all improvements are equally easy to introduce. Here are a few options:
- easy: reduce some cost (material spent) ; you would need to change the satisfyability into some optimiziation trying to minimize the area bounded by maximum X-Y coordinates
- medium: pick an orientation (left-right vs. up-down) ; you would need to
change Input, Output, and the model to tell which shapes an object can take,
but
geost
already takes care of that - hard: attribute multiple rolls at once ; you would need to change Input, Output, and the model to pass a number rolls (maybe to each one their size) and modify the MiniZinc model to decide a roll for each object; youâd generate two pictures per problem
- crazy-hard: time taken to cut with cissors ; you would need to significantly alter the model to express the notion of âgapsâ or âexternalâ.
In a typical setup I would propose to deliver the easy and medium improvements before calling the tool done. I would suggest to propose the hard improvement as more data is generated to understand if Elves actually would benefit from such changes. I would root against the crazy hard, and even propose to equip Elves with cutting-tools and drop cissors would probably use total surface as a good enough proxy, simulators can help assess how bad.
Elves would love to hear about your improvement suggestions or merge requests! Feel free to contact me or to open some Issues to discuss them together.
Epilogue
Alright, this blog post was longer than I expected. I wanted to show how
Haskell can be used to solve a very practical problem and also put onto words
some âtactical thinkingâ to avoid losing time on the less-important aspects
(the one I have not circled red in my dependency diagram). My main challenge
was to time-box my work to one day of code: on top of my Elvish missions I have
a day job and all-in-all this is a good constraint to simulate a real-world
situation. The information I had before starting: I knew JuicyPixels
existed
but had never used it before, I sort of framed the Santa-Wrap problem to
illustrate a non-trivial MiniZinc global (geost_bb
arguably is pretty
advanced). I built the minizinc-process
library and wanted a pretext to
show-case it. I could have taken a shortcut as minizinc-process
offers some
TemplateHaskell to generate Input
and Output
datatypes (with no newtypes
and no support for sets, the pedagogy would have suffered).
An advantage of the Haskell + MiniZinc combination is to be able to use each tool for what they are best at. Haskell allows me to write solid backend services and command line tools whereas MiniZinc allows me to model using MiniZinc IDE (e.g., with a nice syntax for multi-dimensional arrays). In this setup the key difficulty is the boilerplate code required to translate inputs and outputs.
I hope this article piqued your interest. Feel free to contact me if you are curious whether these types of techniques are a good fit for you. I gave a presentation of MiniZinc at an online event this year, and you will find much more on the website and on the Coursera lessons (time consuming).
Thanks to Advent of Haskell organizers and all the other articles authors.
I wish you all the best for the end of 2020 and cheers to a better 2021!
Oh oh oh!