From 262afae7f45a8c21e17d43c509da75bf246d9a0d Mon Sep 17 00:00:00 2001 From: kfrb Date: Mon, 6 Feb 2023 16:07:25 +0100 Subject: [PATCH 1/2] Extensive changes --- Manifest.toml | 167 +++++------ Project.toml | 2 +- README.md | 1 - examples/guests300.jl | 13 +- src/RoomJuggler.jl | 20 +- src/data_import.jl | 179 ++++++++++++ src/io.jl | 138 +++++++++ src/juggling.jl | 229 +++++++++++++++ src/room_allocation.jl | 619 ----------------------------------------- src/types.jl | 167 +++++++++++ 10 files changed, 799 insertions(+), 736 deletions(-) create mode 100644 src/data_import.jl create mode 100644 src/io.jl create mode 100644 src/juggling.jl delete mode 100644 src/room_allocation.jl create mode 100644 src/types.jl diff --git a/Manifest.toml b/Manifest.toml index af7654d..2fdc5d9 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,11 +2,7 @@ julia_version = "1.8.5" manifest_format = "2.0" -project_hash = "92c449d435236a23c124ec0a0d5d31cb26f799b5" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.1" +project_hash = "1991cecf63a4fd8a17ab6cd45097057d3f0fefc8" [[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" @@ -14,6 +10,12 @@ uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +[[deps.CSV]] +deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "SentinelArrays", "SnoopPrecompile", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] +git-tree-sha1 = "c700cce799b51c9045473de751e9319bdd1c6e94" +uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +version = "0.10.9" + [[deps.ChainRulesCore]] deps = ["Compat", "LinearAlgebra", "SparseArrays"] git-tree-sha1 = "c6d890a52d2c4d55d326439580c3b8d0875a77d9" @@ -22,15 +24,21 @@ version = "1.15.7" [[deps.ChangesOfVariables]] deps = ["ChainRulesCore", "LinearAlgebra", "Test"] -git-tree-sha1 = "38f7a08f19d8810338d4f5085211c7dfa5d5bdd8" +git-tree-sha1 = "844b061c104c408b24537482469400af6075aae4" uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" -version = "0.1.4" +version = "0.1.5" + +[[deps.CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "9c209fb7536406834aa938fb149964b985de6c83" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.1" [[deps.Compat]] deps = ["Dates", "LinearAlgebra", "UUIDs"] -git-tree-sha1 = "00a2cccc7f098ff3b66806862d275ca3db9e6e5a" +git-tree-sha1 = "61fdd77467a5c3ad071ef8277ac6bd6af7dd4c04" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.5.0" +version = "4.6.0" [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] @@ -71,19 +79,21 @@ git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" version = "0.9.3" -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" +[[deps.FilePathsBase]] +deps = ["Compat", "Dates", "Mmap", "Printf", "Test", "UUIDs"] +git-tree-sha1 = "e27c4ebe80e8699540f2d6c805cc12203b614f12" +uuid = "48062228-2e41-5def-b9a4-89aafe57970f" +version = "0.9.20" -[[deps.EzXML]] -deps = ["Printf", "XML2_jll"] -git-tree-sha1 = "0fa3b52a04a4e210aeb1626def9c90df3ae65268" -uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" -version = "1.1.0" +[[deps.Future]] +deps = ["Random"] +uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +[[deps.InlineStrings]] +deps = ["Parsers"] +git-tree-sha1 = "9cc2baf75c6d09f9da536ddf58eb2f29dedaf461" +uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" +version = "1.4.0" [[deps.InteractiveUtils]] deps = ["Markdown"] @@ -105,49 +115,22 @@ git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" uuid = "82899510-4779-5014-852e-03e436cf321d" version = "1.0.0" -[[deps.JLLWrappers]] -deps = ["Preferences"] -git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.4.1" - -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.3" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "7.84.0+0" - [[deps.LibGit2]] deps = ["Base64", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.10.2+0" - [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -[[deps.Libiconv_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "c7cb1f5d892775ba13767a87c7ada0b980ea0a71" -uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" -version = "1.16.1+2" - [[deps.LinearAlgebra]] deps = ["Libdl", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" [[deps.LogExpFunctions]] deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "946607f84feb96220f480e0422d3484c49c00239" +git-tree-sha1 = "45b288af6956e67e621c5cbb2d75a261ab58300b" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.19" +version = "0.3.20" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -156,11 +139,6 @@ uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.0+0" - [[deps.Missings]] deps = ["DataAPI"] git-tree-sha1 = "f66bdc5de519e8f8ae43bdc598782d35a25b1272" @@ -170,10 +148,6 @@ version = "1.1.0" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2022.2.1" - [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" version = "1.2.0" @@ -188,10 +162,17 @@ git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" version = "1.4.1" -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.8.0" +[[deps.Parsers]] +deps = ["Dates", "SnoopPrecompile"] +git-tree-sha1 = "151d91d63d8d6c1a5789ecb7de51547e00480f1b" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.5.4" + +[[deps.PooledArrays]] +deps = ["DataAPI", "Future"] +git-tree-sha1 = "a6062fe4063cdafe78f4a0a81cfffb89721b30e7" +uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" +version = "1.4.2" [[deps.Preferences]] deps = ["TOML"] @@ -209,10 +190,6 @@ git-tree-sha1 = "d7a7aef8f8f2d537104f170139553b14dfe39fe9" uuid = "92933f4c-e287-5a05-a399-4b506db050ca" version = "1.7.2" -[[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" - [[deps.Random]] deps = ["SHA", "Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -221,9 +198,21 @@ uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" +[[deps.SentinelArrays]] +deps = ["Dates", "Random"] +git-tree-sha1 = "c02bd3c9c3fc8463d3591a62a378f90d2d8ab0f3" +uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" +version = "1.3.17" + [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +[[deps.SnoopPrecompile]] +deps = ["Preferences"] +git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c" +uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c" +version = "1.0.3" + [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" @@ -270,15 +259,16 @@ git-tree-sha1 = "c79322d36826aa2f4fd8ecfa96ddb47b174ac78d" uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" version = "1.10.0" -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.1" - [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +[[deps.TranscodingStreams]] +deps = ["Random", "Test"] +git-tree-sha1 = "94f38103c984f89cf77c402f2a68dbd870f8165f" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.9.11" + [[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" @@ -286,23 +276,16 @@ uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -[[deps.XLSX]] -deps = ["Artifacts", "Dates", "EzXML", "Printf", "Tables", "ZipFile"] -git-tree-sha1 = "ccd1adf7d0b22f762e1058a8d73677e7bd2a7274" -uuid = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" -version = "0.8.4" - -[[deps.XML2_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Pkg", "Zlib_jll"] -git-tree-sha1 = "93c41695bc1c08c46c5899f4fe06d6ead504bb73" -uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" -version = "2.10.3+0" +[[deps.WeakRefStrings]] +deps = ["DataAPI", "InlineStrings", "Parsers"] +git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23" +uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" +version = "1.4.2" -[[deps.ZipFile]] -deps = ["Libdl", "Printf", "Zlib_jll"] -git-tree-sha1 = "f492b7fe1698e623024e873244f10d89c95c340a" -uuid = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" -version = "0.10.1" +[[deps.WorkerUtilities]] +git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" +uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60" +version = "1.6.1" [[deps.Zlib_jll]] deps = ["Libdl"] @@ -313,13 +296,3 @@ version = "1.2.12+3" deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" version = "5.1.1+0" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.48.0+0" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+0" diff --git a/Project.toml b/Project.toml index 8b5b987..84a9051 100644 --- a/Project.toml +++ b/Project.toml @@ -4,13 +4,13 @@ authors = ["Kai Friebertshäuser"] version = "0.1.0" [deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" [compat] ProgressMeter = "1" diff --git a/README.md b/README.md index 0447b20..9ef1c9c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # RoomJuggler -[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://kfrb.github.io/RoomJuggler.jl/stable/) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://kfrb.github.io/RoomJuggler.jl/dev/) [![Build Status](https://github.com/kfrb/RoomJuggler.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/kfrb/RoomJuggler.jl/actions/workflows/CI.yml?query=branch%3Amain) diff --git a/examples/guests300.jl b/examples/guests300.jl index c2ce470..37355fd 100644 --- a/examples/guests300.jl +++ b/examples/guests300.jl @@ -1,15 +1,10 @@ -using HappyScheduler +using RoomJuggler guests = joinpath(@__DIR__, "guests300.csv") wishes = joinpath(@__DIR__, "wishes300.csv") rooms = joinpath(@__DIR__, "rooms300.csv") -gwrf, gwrm = get_gwr_split_genders(guests, wishes, rooms) +rjj = RoomJugglerJob(guests, wishes, rooms) +juggle!(rjj) -rapf = RoomAllocationProblem(gwrf...) -simulated_annealing!(rapf; start_temp=1, minimum_temp=1e-7, β=0.999, n_iter=300) -export_results(rapf; dir=@__DIR__, prefix="results_f_") - -rapm = RoomAllocationProblem(gwrm...) -simulated_annealing!(rapm; start_temp=1, minimum_temp=1e-7, β=0.999, n_iter=300) -export_results(rapm; dir=@__DIR__, prefix="results_m_") +export_results(joinpath(@__DIR__, "results"), rjj) diff --git a/src/RoomJuggler.jl b/src/RoomJuggler.jl index 2157be9..4c5ee88 100644 --- a/src/RoomJuggler.jl +++ b/src/RoomJuggler.jl @@ -1,24 +1,26 @@ module RoomJuggler -using DelimitedFiles +# using DelimitedFiles +using CSV using Printf using SparseArrays using LinearAlgebra using StatsBase: sample using ProgressMeter -using XLSX -export Guest, Wish, Room, RoomAllocationProblem, get_gwr_split_genders, get_gwr -export simulated_annealing! +export Guest, Wish, Room, RoomJugglerJob, JuggleConfig +export juggle! export export_results const BANNER = raw""" -░▒█▀▀▄░▄▀▀▄░▄▀▀▄░█▀▄▀█░░░░▒█░█░▒█░█▀▀▀░█▀▀▀░█░░█▀▀░█▀▀▄ -░▒█▄▄▀░█░░█░█░░█░█░▀░█░░░░▒█░█░▒█░█░▀▄░█░▀▄░█░░█▀▀░█▄▄▀ -░▒█░▒█░░▀▀░░░▀▀░░▀░░▒▀░▒█▄▄█░░▀▀▀░▀▀▀▀░▀▀▀▀░▀▀░▀▀▀░▀░▀▀ - + ░▒█▀▀▄░▄▀▀▄░▄▀▀▄░█▀▄▀█░░░░▒█░█░▒█░█▀▀▀░█▀▀▀░█░░█▀▀░█▀▀▄ + ░▒█▄▄▀░█░░█░█░░█░█░▀░█░░░░▒█░█░▒█░█░▀▄░█░▀▄░█░░█▀▀░█▄▄▀ + ░▒█░▒█░░▀▀░░░▀▀░░▀░░▒▀░▒█▄▄█░░▀▀▀░▀▀▀▀░▀▀▀▀░▀▀░▀▀▀░▀░▀▀ """ -include("room_allocation.jl") +include("types.jl") +include("data_import.jl") +include("juggling.jl") +include("io.jl") end diff --git a/src/data_import.jl b/src/data_import.jl new file mode 100644 index 0000000..c44aaa7 --- /dev/null +++ b/src/data_import.jl @@ -0,0 +1,179 @@ + +function get_guests(file::String) + # check correct file extension + !endswith(file, ".csv") && error("$file not a .csv file") + + # read raw guests from csv file + guests_raw = CSV.File(file; types=Dict(:name => String, :gender => Symbol), strict=true) + + # convert raw guests to Vector{Guest} + guests = Vector{Guest}() + for row in guests_raw + push!(guests, Guest(row.name, row.gender)) + end + + # check for duplicates + length(guests) > length(unique(guests)) && error("guest duplicates found") + + return guests +end + +function get_wishes(file::String, guests::Vector{Guest}) + # check correct file extension + !endswith(file, ".csv") && error("$file not a .csv file") + + # read raw wishes from csv file + wishes_raw = CSV.File(file; + header=false, + delim=';', + types=String, + stripwhitespace=true, + silencewarnings=true, + ) + if length(first(wishes_raw)) < 2 + error("found only 1 column in $file, \nneed ';' as delimiter!") + end + + # initializations + unknown_guest_errorflag = false + mixed_gender_wish_errorflag = false + guest_names = [g.name for g in guests] + wishes = Vector{Wish}() + + # loop over all raw wishes + for row in wishes_raw + + # get e-mail from raw wish + mail = first(row) + !contains(mail, "@") && error("$mail is not a valid e-mail adress") + + # get names of the wish friends + names = Vector{String}() + for i in firstindex(row)+1:lastindex(row) + !ismissing(row[i]) && push!(names, row[i]) + end + isempty(names) && error("no names in wish with mail: $mail") + + # find corresponding guest_ids of the wish friends + guest_ids = Vector{Int}() + for name in names + guest_id = findfirst(name .== guest_names) + if isnothing(guest_id) + unknown_guest_errorflag = true + @error "unknown guest in wish:" mail name + else + push!(guest_ids, guest_id) + end + end + + # check for same gender + guests_in_wish = guests[guest_ids] + genders_equal = allequal([g.gender for g in guests_in_wish]) + if genders_equal + gender = guests_in_wish[begin].gender + else + mixed_gender_wish_errorflag = true + @error "mixed gender wish found:" guests_in_wish + end + + # create Wish instance + wish = Wish(mail, guest_ids, gender) + push!(wishes, wish) + end + + # thow errors + unknown_guest_errorflag && error("unkown guests found") + mixed_gender_wish_errorflag && error("mixed gender wishes found") + + check_for_multiple_wishes(wishes, guests) + + return wishes +end + +function check_for_multiple_wishes(wishes::Vector{Wish}, guests::Vector{Guest}) + multiple_wishes_errorflag = false + + # check for each guest + for guest_id in eachindex(guests) + + # create a wishlist with all wish_ids of this guest + wishlist = Vector{Int}() + for (wish_id, wish) in enumerate(wishes) + if guest_id in wish.guest_ids + push!(wishlist, wish_id) + end + end + + # error if guest is found in more than one wish + if length(wishlist) > 1 + multiple_wishes_errorflag = true + name = guests[guest_id].name + wish_mails = [w.mail for w in wishes[wishlist]] + @error "multiple wishes specified!" name wish_mails + end + end + + # throw error + multiple_wishes_errorflag && error("guest(s) with multiple wishes found") + + return nothing +end + +function get_rooms(file::String) + # check correct file extension + !endswith(file, ".csv") && error("$file not a .csv file") + + # read raw rooms from csv file + rooms_raw = CSV.File(file; + types=Dict(:name => String, :capacity => Int, :gender => Symbol), + stripwhitespace=true, + ) + + # convert raw rooms to Vector{Room} + rooms = [Room(r[1], r[2], r[3]) for r in rooms_raw] + + return rooms +end + +function find_relations(wishes::Vector{Wish}, n_beds::Int) + # initialization + relations = spzeros(Int, n_beds, n_beds) + + # loop over all wishes + for wish in wishes + for guest_id in wish.guest_ids + # find the wish friends for each guest + friend_ids = wish.guest_ids[wish.guest_ids .!== guest_id] + + # relation between friends is -1, otherwise 0 + for friend_id in friend_ids + relations[guest_id, friend_id] = -1 + end + end + end + + return relations +end + +function filter_genders(guests::Vector{Guest}, wishes::Vector{Wish}, gender::Symbol) + # get all guests and wishes with the specified gender + guests_gender = filter(x -> x.gender == gender, guests) + wishes_gender = filter(x -> x.gender == gender, wishes) + + # create a Dict mapping old_id => new_id + new_guest_ids = Dict{Int, Int}() + for (new_id, guest) in enumerate(guests_gender) + old_id = findfirst(x -> x == guest, guests) + new_guest_ids[old_id] = new_id + end + + # change the old guest_ids to the new guest_ids in wishes_gender + for wish_id in eachindex(wishes_gender) + old_ids = wishes_gender[wish_id].guest_ids + for (i,old_id) in enumerate(old_ids) + wishes_gender[wish_id].guest_ids[i] = new_guest_ids[old_id] + end + end + + return guests_gender, wishes_gender +end diff --git a/src/io.jl b/src/io.jl new file mode 100644 index 0000000..dd76383 --- /dev/null +++ b/src/io.jl @@ -0,0 +1,138 @@ +function log_init() + printstyled("-"^67 * "\n"; color=:blue, bold=true) + println() + printstyled(BANNER; color=:blue) + printstyled("-"^67 * "\n"; color=:blue, bold=true) + printstyled( + " SOLVING A ROOM OCCUPANCY PROBLEM WITH SIMULATED ANNEALING\n"; + color=:blue, + bold=true, + ) + printstyled("-"^67 * "\n"; color=:blue, bold=true) + return nothing +end + +function log_start(config::JuggleConfig, max_happiness::Int) + @printf("start temperature: %15g\n", config.t_0) + @printf("minimum temperature: %15g\n", config.t_min) + @printf("iterations per temp.: %15d\n", config.n_iter) + @printf("planned guest juggles: %15d\n", config.n_total_iter) + @printf("maximum happiness: %15d\n", max_happiness) + return nothing +end + +function log_results(rop::RoomOccupancyProblem, happiness::Int, n_total_iter::Int) + @printf("total guest switches: %15d\n", n_total_iter) + @printf("happiness: %15d\n", abs(happiness)) + if abs(happiness) == rop.max_happiness + print("all wishes are fulfilled!") + printstyled(" (━☞´◔‿ゝ◔`)━☞ ᕙᓄ(☉ਊ☉)ᓄᕗ\n", color=:blue) + else + fulfilled_wishes_percent = 100 * abs(happiness) / rop.max_happiness + @printf("%.2f %% of all wishes are fulfilled!", fulfilled_wishes_percent) + printstyled(" (͡o‿O͡)\n", color=:blue) + n_fulfilled_wishes = length(findall(rop.fulfilled_wishes .== true)) + n_unfulfilled_wishes = length(findall(rop.fulfilled_wishes .== false)) + @assert rop.n_wishes == n_fulfilled_wishes + n_unfulfilled_wishes + @printf(" %d wishes fulfilled\n", n_fulfilled_wishes) + @printf(" %d wishes not fulfilled\n", n_unfulfilled_wishes) + println() + println("adjust the parameters in JuggleConfig and try again!") + end +end + +function export_wish_overview(io::IO, rop::RoomOccupancyProblem) + for (wish_id, wish) in enumerate(rop.wishes) + wish_checkmark = rop.fulfilled_wishes[wish_id] ? "✔︎" : "✘" + @printf(io, "%s %s (%s):\n", wish_checkmark, wish.mail, wish.gender) + for guest_id in wish.guest_ids + guest_name = rop.guests[guest_id].name + guest_gender = rop.guests[guest_id].gender + guest_room_name = rop.rooms[rop.room_id_of_guest[guest_id]].name + @printf(io, " %s (%s) - %s\n", guest_name, guest_gender, guest_room_name) + end + println(io) + end + return nothing +end + +function export_room_overview(io::IO, rop::RoomOccupancyProblem) + for (room_id, room) in enumerate(rop.rooms) + @printf(io, "%s (%s, %d beds):\n", room.name, room.gender, room.capacity) + guest_ids = rop.guest_ids_of_room[room_id] + for (bed_nr, guest_id) in enumerate(guest_ids) + guest_name = rop.guests[guest_id].name + guest_gender = rop.guests[guest_id].gender + @printf(io, " %d. %s (%s)\n", bed_nr, guest_name, guest_gender) + end + if length(guest_ids) < room.capacity + for bed_nr in range(start=length(guest_ids)+1, stop=room.capacity) + @printf(io, " %d. ---\n", bed_nr) + end + end + println(io) + end + + return nothing +end + +function export_guests_csv(file::String, rjj::RoomJugglerJob) + open(file, "w") do io + println(io, "name;gender;room") + + # females + for (guest_id, guest) in enumerate(rjj.ropf.guests) + room_name = rjj.ropf.rooms[rjj.ropf.room_id_of_guest[guest_id]].name + @printf(io, "%s;%s;%s\n", guest.name, guest.gender, room_name) + end + + # males + for (guest_id, guest) in enumerate(rjj.ropm.guests) + room_name = rjj.ropm.rooms[rjj.ropm.room_id_of_guest[guest_id]].name + @printf(io, "%s;%s;%s\n", guest.name, guest.gender, room_name) + end + end + + return nothing +end + +function export_report(file::String, rjj::RoomJugglerJob) + open(file, "w") do io + println(io, "="^67) + println(io, "OVERVIEW OF ALL WISHES") + println(io, "="^67) + println(io, "\n--- FEMALES: ---\n") + export_wish_overview(io, rjj.ropf) + println(io, "\n--- MALES: ---\n") + export_wish_overview(io, rjj.ropm) + println(io, "="^67) + println(io, "OVERVIEW OF ALL ROOMS") + println(io, "="^67) + println(io, "\n--- FEMALES: ---\n") + export_room_overview(io, rjj.ropf) + println(io, "\n--- MALES: ---\n") + export_room_overview(io, rjj.ropm) + end + + return nothing +end + +function export_results(dir::String, rjj::RoomJugglerJob; force::Bool=false) + # if dir exists, check if overwriting is allowed + if isdir(dir) + if !force + error("directory $dir exists! use keyword argument force=true to overwrite") + end + else + # otherwise create dir + mkpath(dir) + end + + # export csv file for guests and report + export_guests_csv(joinpath(dir, "guests.csv"), rjj) + export_report(joinpath(dir, "report.txt"), rjj) + + println("✔︎ results exported to $dir") + + return nothing +end diff --git a/src/juggling.jl b/src/juggling.jl new file mode 100644 index 0000000..01eba3a --- /dev/null +++ b/src/juggling.jl @@ -0,0 +1,229 @@ +function juggle!(rjj::RoomJugglerJob; config=JuggleConfig()) + log_init() + if typeof(config) == JuggleConfig + printstyled("\nFEMALES:\n"; color=:blue, bold=true, underline=true) + juggle_rop!(rjj.ropf, config) + printstyled("\nMALES:\n"; color=:blue, bold=true, underline=true) + juggle_rop!(rjj.ropm, config) + elseif typeof(config) == Tuple{JuggleConfig, JuggleConfig} + printstyled("\nFEMALES:\n"; color=:blue, bold=true, underline=true) + config_f = config[1] + juggle_rop!(rjj.ropf, config_f) + printstyled("\nMALES:\n"; color=:blue, bold=true, underline=true) + config_m = config[2] + juggle_rop!(rjj.ropm, config_m) + else + error("config not correctly specified!") + end + return nothing +end + +function juggle_rop!(rop::RoomOccupancyProblem, config::JuggleConfig) + # skip if problem already solved and skip juggling if true + if problem_solved(rop) + println("All wishes already fulfilled, skipping juggling.") + return nothing + end + + # initial random room allocation + current_allocation = initialize_allocation(rop) + + # calc happiness for this initial room allocation + current_happiness = calc_happiness(current_allocation, rop.relations) + + # initializations + target_happiness = -rop.max_happiness + all_wishes_fulfilled = false + iteration_counter = 0 + p = Progress(config.n_total_iter; + dt=1, + desc="juggling guests...", + barlen=28, + color=:normal, + ) + + log_start(config, -target_happiness) + + # juggling-loop + elapsed_time = @elapsed begin + for temp in config.t_history, _ in 1:config.n_iter + iteration_counter += 1 + + # get random new allocation + new_allocation = get_new_allocation(current_allocation, rop.n_rooms) + + # calc the new happiness for this allocation + new_happiness = calc_happiness(new_allocation, rop.relations) + + # calc the probability of accepting the new allocation + acceptance_probability = calc_acceptance_probability( + current_happiness, + new_happiness, + temp + ) + + # accept if probability is higher than random value + if acceptance_probability > rand() + current_allocation = new_allocation + current_happiness = new_happiness + end + + # break if all wishes are fulfilled + if current_happiness == target_happiness + all_wishes_fulfilled = true + break + end + + # update ProgressMeter + next!(p, showvalues=gen_showvals(iteration_counter, -current_happiness)) + end + end + finish!(p) + + @printf("✔︎ simulated annealing completed after %g seconds\n", elapsed_time) + + # updates + calc_room_id_of_guest!(rop, current_allocation) + calc_guest_ids_of_room!(rop, current_allocation) + calc_fulfilled_wishes!(rop) + + log_results(rop, current_happiness, iteration_counter) + + return nothing +end + +function problem_solved(rop::RoomOccupancyProblem) + n_unfulfilled_wishes = length(findall(rop.fulfilled_wishes .== false)) + if n_unfulfilled_wishes > 0 + return false + else + return true + end +end + +gen_showvals(iteration, happiness) = ()->[(:iteration, iteration), (:happiness, happiness)] + +function temperature_history(t_0, t_min, β) + t_history = Vector{Float64}() + t = copy(t_0) + while t > t_min + push!(t_history, t) + t *= β + end + return t_history +end + +function initialize_allocation(rap::RoomOccupancyProblem) + # initialize sparse matrix + allocation = spzeros(Int, rap.n_rooms, rap.n_beds) + + # initialize guests + # add ghost-guests, so that n_guests == n_beds + guest_ids = collect(1:rap.n_beds) + + # loop over all rooms and assign the beds + for (room_id, room) in enumerate(rap.rooms) + capacity = room.capacity + sample_guest_ids = sort(sample(guest_ids[guest_ids .> 0], capacity; replace=false)) + allocation[room_id, sample_guest_ids] .= 1 + guest_ids[sample_guest_ids] .= -1 # take guests out if assigned to a room + end + + # check if the capacity of the room is exceeded + used_capacity = sum(allocation; dims=2) + for (room_id, room) in enumerate(rap.rooms) + @assert room.capacity == used_capacity[room_id] + end + + return allocation +end + +calc_happiness(allocation, relations) = tr(allocation * relations * allocation') + +function calc_acceptance_probability(current_happiness, new_happiness, temp) + if new_happiness < current_happiness + return 1 + else + return exp((current_happiness - new_happiness) / temp) + end +end + +function get_new_allocation(allocation, n_rooms) + # copy so that the current one is not modified + allocation_copy = copy(allocation) + + # randomly choose two rooms + room_id_1, room_id_2 = sample(1:n_rooms, 2; replace=false) + + # randomly choose two guests + guest_id_1 = rand(findall(@views allocation_copy[room_id_1, :] .== 1)) + guest_id_2 = rand(findall(@views allocation_copy[room_id_2, :] .== 1)) + + # switch rooms of these two guests + allocation_copy[room_id_1, guest_id_1] = 0 + allocation_copy[room_id_1, guest_id_2] = 1 + allocation_copy[room_id_2, guest_id_2] = 0 + allocation_copy[room_id_2, guest_id_1] = 1 + + # maintaining sparsity + dropzeros!(allocation_copy) + + return allocation_copy +end + +function calc_room_id_of_guest!( + rap::RoomOccupancyProblem, + allocation::SparseMatrixCSC{Int, Int}, +) + # loop over all guests + for (guest_id, col) in enumerate(eachcol(allocation[:, 1:rap.n_guests])) + room_id = findfirst(col .> 0) + # every guest needs a room, error if room_id === nothing + rap.room_id_of_guest[guest_id] = room_id + end + + return nothing +end + +function calc_guest_ids_of_room!( + rap::RoomOccupancyProblem, + allocation::SparseMatrixCSC{Int, Int}, +) + # loop over all rooms + for (room_id, row) in enumerate(eachrow(allocation[:, 1:rap.n_guests])) + guest_ids = findall(row .> 0) + if !isnothing(guest_ids) # ignore unused rooms + rap.guest_ids_of_room[room_id] = guest_ids + end + end + + return nothing +end + +function calc_fulfilled_wishes!(rap::RoomOccupancyProblem) + # loop over all wishes + for (wish_id, wish) in enumerate(rap.wishes) + + # initializations + guest_ids = wish.guest_ids + wish_is_fulfilled = false + friend_counter = 1 + room_id = rap.room_id_of_guest[guest_ids[1]] + + # count the friends in the same room as friend 1 + for friend_id in guest_ids[2:end] + room_id_friend = rap.room_id_of_guest[friend_id] + if room_id == room_id_friend + friend_counter += 1 + end + end + + # if all friends are in the room, the wish is fulfilled + if friend_counter == length(guest_ids) + wish_is_fulfilled = true + end + rap.fulfilled_wishes[wish_id] = wish_is_fulfilled + end + + return nothing +end diff --git a/src/room_allocation.jl b/src/room_allocation.jl deleted file mode 100644 index 7798cc4..0000000 --- a/src/room_allocation.jl +++ /dev/null @@ -1,619 +0,0 @@ -struct Guest - name::String - gender::Symbol -end - -function Base.show(io::IO, ::MIME"text/plain", g::Guest) - print(io, typeof(g), ": ", g.name, " (", g.gender, ")") - return nothing -end - -struct Wish - mail::String - guest_ids::Vector{Int} - gender::Symbol -end - -function Base.show(io::IO, ::MIME"text/plain", w::Wish) - print(io, length(w.guest_ids), "-person ", typeof(w), " from ", w.mail) - return nothing -end - -struct Room - name::String - capacity::Int - gender::Symbol -end - -function Base.show(io::IO, ::MIME"text/plain", r::Room) - print(io, r.capacity, "-person ", typeof(r), " with name: ", r.name) - return nothing -end - -struct RoomAllocationProblem - n_guests::Int - n_wishes::Int - n_rooms::Int - n_beds::Int - max_happiness::Int - guests::Vector{Guest} - wishes::Vector{Wish} - rooms::Vector{Room} - relations::SparseMatrixCSC{Int64, Int64} - room_id_of_guest::Vector{Int} - guest_ids_of_room::Vector{Vector{Int}} - fulfilled_wishes::Vector{Bool} - - function RoomAllocationProblem( - guests::Vector{Guest}, - wishes::Vector{Wish}, - rooms::Vector{Room} - ) - n_guests = length(guests) - n_wishes = length(wishes) - n_rooms = length(rooms) - n_beds = sum([r.capacity for r in rooms]) - if n_guests > n_beds - msg = @sprintf( - "Number of guests = %d; number of beds = %d\n Check the numbers!", - n_guests, - n_beds - ) - error(msg) - end - relations = find_relations(wishes, n_beds) - max_happiness = length(findall(!iszero, relations)) - room_id_of_guest = zeros(Int, n_guests) - guest_ids_of_room = fill(Vector{Int}(), n_rooms) - fulfilled_wishes = zeros(Bool, n_wishes) - new( - n_guests, - n_wishes, - n_rooms, - n_beds, - max_happiness, - guests, - wishes, - rooms, - relations, - room_id_of_guest, - guest_ids_of_room, - fulfilled_wishes - ) - end -end - -function Base.show(io::IO, ::MIME"text/plain", rap::RoomAllocationProblem) - println(io, rap.n_rooms, "-room ", typeof(rap), ":") - @printf(io, " %d beds\n", rap.n_beds) - @printf(io, " %d guests\n", rap.n_guests) - @printf(io, " %d wishes", rap.n_wishes) - return nothing -end - -function get_gwr(guests_file::String, wishes_file::String, rooms_file::String) - guests = get_guests(guests_file) - wishes = get_wishes(wishes_file, guests) - rooms = get_rooms(rooms_file) - return guests, wishes, rooms -end - -function get_gwr(xfile::String) - guests = get_guests(xfile) - wishes = get_wishes(xfile, guests) - rooms = get_rooms(xfile) - return guests, wishes, rooms -end - -function get_gwr_split_genders(files...) - guests, wishes, rooms = get_gwr(files...) - guests_f, wishes_f = filter_genders(guests, wishes, :F) - guests_m, wishes_m = filter_genders(guests, wishes, :M) - rooms_f = filter(x -> x.gender == :F, rooms) - rooms_m = filter(x -> x.gender == :M, rooms) - return (guests_f, wishes_f, rooms_f), (guests_m, wishes_m, rooms_m) -end - -function get_guests_raw(file::String) - if endswith(file, ".csv") - guests_raw_, _ = readdlm(file, ';', String; header=true, skipblanks=true) - guests_raw = strip.(guests_raw_) - elseif endswith(file, ".xlsx") - xf = XLSX.readxlsx(file) - guests_raw = strip.(string.(xf["guests"][:][begin+1:end, :])) - else - error("file $file not known!") - end - return guests_raw -end - -function get_guests(file::String) - guests_raw = get_guests_raw(file) - guests = Vector{Guest}() - for row in eachrow(guests_raw) - name = row[1] - gender = Symbol(row[2]) - push!(guests, Guest(name, gender)) - end - return guests -end - -function get_wishes_raw(file::String) - if endswith(file, ".csv") - wishes_raw = strip.(readdlm(file, ';', String; skipblanks=true)) - elseif endswith(file, ".xlsx") - xf = XLSX.readxlsx(file) - wishes_raw = xf["wishes"][:] - replace!(wishes_raw, missing => "") - wishes_raw = convert.(String, wishes_raw) - else - error("file $file not known!") - end - return wishes_raw -end - -function get_wishes(file::String, guests::Vector{Guest}) - guest_names = [g.name for g in guests] - unknown_guests = Dict{Int, Vector{String}}() - wishes_raw = get_wishes_raw(file) - wishes = Vector{Wish}() - - for (wish_id, data) in enumerate(eachrow(wishes_raw)) - mail = data[1] - names = data[2:end] - guest_ids = Vector{Int}() - unknown_guests_in_wish = Vector{String}() - for name in names - if !isempty(name) - guest_id = findfirst(name .== guest_names) - if isnothing(guest_id) - push!(unknown_guests_in_wish, name) - else - push!(guest_ids, guest_id) - end - end - end - if !isempty(unknown_guests_in_wish) - unknown_guests[wish_id] = unknown_guests_in_wish - end - guests_in_wish = guests[guest_ids] - genders_equal = allequal([g.gender for g in guests_in_wish]) - if genders_equal - gender = guests_in_wish[1].gender - else - gender = :MIX - end - wish = Wish(mail, guest_ids, gender) - push!(wishes, wish) - end - - if !isempty(unknown_guests) - unknown_guests_info_file = joinpath( - dirname(file), - string("unknown_guests_in_",splitext(basename(file))[1],".txt") - ) - open(unknown_guests_info_file, "w") do io - write(io, "The following guests are unknown:\n") - for (wish_id, names) in unknown_guests - println(io) - write(io, string("Wish of ", wishes[wish_id].mail), ":\n") - for name in names - write(io, string("->", name, "<-\n")) - end - end - end - msg = @sprintf( - "%d unknown guests found! Check the file '%s' for more details!", - length(keys(unknown_guests)), - basename(unknown_guests_info_file), - ) - error(msg) - end - - multiple_wishes = check_for_multiple_wishes(wishes, guests) - if !isempty(multiple_wishes) - multiple_wishes_info_file = joinpath( - dirname(file), - string("multiple_wishes_in_",splitext(basename(file))[1],".txt") - ) - open(multiple_wishes_info_file, "w") do io - write(io, "The following guests made multiple wishes:\n") - for (guest_id, wishlist) in multiple_wishes - println(io) - write(io, guests[guest_id].name, ":\n") - for wish_id in wishlist - write(io, string("Contained in wish of ", wishes[wish_id].mail), "\n") - end - end - end - msg = @sprintf( - "%d multiple wishes found! Check the file '%s' for more details!", - length(keys(multiple_wishes)), - basename(multiple_wishes_info_file), - ) - error(msg) - end - - mixed_gender_wishes = [wish_id for wish_id in eachindex(wishes) - if wishes[wish_id].gender == :MIX] - if !isempty(mixed_gender_wishes) - mixed_gender_wishes_info_file = joinpath( - dirname(file), - string("mixed_gender_wishes_in_",splitext(basename(file))[1],".txt") - ) - open(mixed_gender_wishes_info_file, "w") do io - write(io, "The following mixed gender wishes appear:\n") - for wish_id in mixed_gender_wishes - println(io) - write(io, string("Wish of ", wishes[wish_id].mail), ":\n") - for guest_id in wishes[wish_id].guest_ids - write( - io, - string(guests[guest_id].gender, ", ", guests[guest_id].name, "\n"), - ) - end - end - end - msg = @sprintf( - "%d mixed gender wishes found! Check the file '%s' for more details!", - length(mixed_gender_wishes), - basename(mixed_gender_wishes_info_file), - ) - error(msg) - end - - return wishes -end - -function check_for_multiple_wishes(wishes::Vector{Wish}, guests::Vector{Guest}) - multiple_wishes = Dict{Int, Vector{Int}}() - for guest_id in eachindex(guests) - wishlist = Vector{Int}() - for (wish_id, wish) in enumerate(wishes) - if guest_id in wish.guest_ids - push!(wishlist, wish_id) - end - end - if length(wishlist) > 1 - multiple_wishes[guest_id] = wishlist - end - end - return multiple_wishes -end - -function get_rooms_raw(file::String) - if endswith(file, ".csv") - rooms_raw_, _ = readdlm(file, ';', String; header=true, skipblanks=true) - rooms_raw = strip.(rooms_raw_) - elseif endswith(file, ".xlsx") - xf = XLSX.readxlsx(file) - rooms_raw = strip.(string.(xf["rooms"][:][begin+1:end, :])) - else - error("file $file not known!") - end - return rooms_raw -end - -function get_rooms(file::String) - rooms_raw = get_rooms_raw(file) - rooms = Vector{Room}() - for row in eachrow(rooms_raw) - name = strip(row[1]) - capacity = parse(Int, row[2]) - gender = Symbol(strip(row[3])) - push!(rooms, Room(name, capacity, gender)) - end - return rooms -end - -function find_relations(wishes::Vector{Wish}, n_beds::Int) - relations = spzeros(Int, n_beds, n_beds) - for wish in wishes - for guest_id in wish.guest_ids - friend_ids = wish.guest_ids[wish.guest_ids .!== guest_id] - for friend_id in friend_ids - relations[guest_id, friend_id] = -1 - end - end - end - return relations -end - -function filter_genders(guests::Vector{Guest}, wishes::Vector{Wish}, gender::Symbol) - guests_gender = filter(x -> x.gender == gender, guests) - wishes_gender = filter(x -> x.gender == gender, wishes) - new_guest_ids = Dict{Int, Int}() - for (new_id, guest) in enumerate(guests_gender) - old_id = findfirst(x -> x == guest, guests) - new_guest_ids[old_id] = new_id - end - for wish_id in eachindex(wishes_gender) - old_ids = wishes_gender[wish_id].guest_ids - for (i,old_id) in enumerate(old_ids) - wishes_gender[wish_id].guest_ids[i] = new_guest_ids[old_id] - end - end - return guests_gender, wishes_gender -end - -function simulated_annealing!(rap::RoomAllocationProblem; - start_temp=1.0, - minimum_temp=1e-7, - β=0.99, - n_iter=100, -) - log_simulated_annealing_init() - if β >= 1 - error("Infinity-loop: β >= 1. Must be 0 < β < 1") - end - happiness_history = Vector{Float64}() - target_happiness = -rap.max_happiness - all_wishes_fulfilled = false - current_allocation = initialize_allocation(rap) - current_happiness = calc_happiness(current_allocation, rap.relations) - temp_history = temperature_history(start_temp, minimum_temp, β) - n_total_iter = length(temp_history) * n_iter - happiness_history = zeros(Int, n_total_iter) - iteration_counter = 0 - log_simulated_annealing_start(rap, n_total_iter, start_temp, minimum_temp, n_iter) - p = Progress(n_total_iter; - dt=1, - desc="Switching guests...", - barlen=27, - color=:normal, - ) - elapsed_time = @elapsed begin - for temp in temp_history, _ in 1:n_iter - iteration_counter += 1 - new_allocation = get_new_allocation(current_allocation, rap.n_rooms) - new_happiness = calc_happiness(new_allocation, rap.relations) - acceptance_probability = calc_acceptance_probability( - current_happiness, - new_happiness, - temp - ) - if acceptance_probability > rand() - current_allocation = new_allocation - current_happiness = new_happiness - end - happiness_history[iteration_counter] = current_happiness - if current_happiness == target_happiness - all_wishes_fulfilled = true - happiness_history = happiness_history[1:iteration_counter] - break - end - next!(p, showvalues=gen_showvals(iteration_counter, -current_happiness)) - end - finish!(p) - end - @printf("✔︎ simulated annealing completed after %g seconds\n", elapsed_time) - calc_room_id_of_guest!(rap, current_allocation) - calc_guest_ids_of_room!(rap, current_allocation) - calc_fulfilled_wishes!(rap) - log_simulated_annealing_results(rap, current_happiness, iteration_counter) - return nothing -end - -gen_showvals(iteration, happiness) = ()->[(:iteration, iteration), (:happiness, happiness)] - -function temperature_history(temp, temp_min, β) - temp_history = Vector{Float64}() - while temp > temp_min - push!(temp_history, temp) - temp *= β - end - return temp_history -end - -function initialize_allocation(rap::RoomAllocationProblem) - allocation = spzeros(Int, rap.n_rooms, rap.n_beds) - guest_ids = collect(1:rap.n_beds) # ghost-guests, so that n_guests == n_beds - for (room_id, room) in enumerate(rap.rooms) - capacity = room.capacity - sample_guest_ids = sort(sample(guest_ids[guest_ids .> 0], capacity; replace=false)) - allocation[room_id, sample_guest_ids] .= 1 - guest_ids[sample_guest_ids] .= -1 # take guests out if assigned to a room - end - used_capacity = sum(allocation; dims=2) - for (room_id, room) in enumerate(rap.rooms) - @assert room.capacity == used_capacity[room_id] - end - return allocation -end - -calc_happiness(allocation, relations) = tr(allocation * relations * allocation') - -function calc_acceptance_probability(current_happiness, new_happiness, temp) - if new_happiness < current_happiness - return 1 - else - return exp((current_happiness - new_happiness) / temp) - end -end - -function get_new_allocation(allocation, n_rooms) - allocation_copy = copy(allocation) - room_id_1, room_id_2 = sample(1:n_rooms, 2; replace=false) - guest_id_1 = rand(findall(@views allocation_copy[room_id_1, :] .== 1)) - guest_id_2 = rand(findall(@views allocation_copy[room_id_2, :] .== 1)) - allocation_copy[room_id_1, guest_id_1] = 0 # switch rooms for two guests - allocation_copy[room_id_1, guest_id_2] = 1 - allocation_copy[room_id_2, guest_id_2] = 0 - allocation_copy[room_id_2, guest_id_1] = 1 - dropzeros!(allocation_copy) - return allocation_copy -end - -function calc_room_id_of_guest!( - rap::RoomAllocationProblem, - allocation::SparseMatrixCSC{Int, Int}, -) - for (guest_id, col) in enumerate(eachcol(allocation[:, 1:rap.n_guests])) - room_id = findfirst(col .> 0) - if !isnothing(room_id) - rap.room_id_of_guest[guest_id] = room_id - end - end - return nothing -end - -function calc_guest_ids_of_room!( - rap::RoomAllocationProblem, - allocation::SparseMatrixCSC{Int, Int}, -) - for (room_id, row) in enumerate(eachrow(allocation[:, 1:rap.n_guests])) - guest_ids = findall(row .> 0) - if !isnothing(guest_ids) - rap.guest_ids_of_room[room_id] = guest_ids - end - end - return nothing -end - -function calc_fulfilled_wishes!(rap::RoomAllocationProblem) - for (wish_id, wish) in enumerate(rap.wishes) - guest_ids = wish.guest_ids - wish_is_fulfilled = false - friend_counter = 1 - room_id = rap.room_id_of_guest[guest_ids[1]] - for friend_id in guest_ids[2:end] - room_id_friend = rap.room_id_of_guest[friend_id] - if room_id == room_id_friend - friend_counter += 1 - end - end - if friend_counter == length(guest_ids) - wish_is_fulfilled = true - end - rap.fulfilled_wishes[wish_id] = wish_is_fulfilled - end - return nothing -end - -function export_wish_overview(file::String, rap::RoomAllocationProblem) - open(file, "w") do io - println(io, "="^67) - println(io, "OVERVIEW OF ALL WISHES") - println(io, "="^67) - println(io) - for (wish_id, wish) in enumerate(rap.wishes) - wish_checkmark = rap.fulfilled_wishes[wish_id] ? "✔︎" : "✘" - @printf(io, "%s %s (%s):\n", wish_checkmark, wish.mail, wish.gender) - for guest_id in wish.guest_ids - guest_name = rap.guests[guest_id].name - guest_gender = rap.guests[guest_id].gender - guest_room_name = rap.rooms[rap.room_id_of_guest[guest_id]].name - @printf(io, " %s (%s) - %s\n", guest_name, guest_gender, guest_room_name) - end - println(io) - end - end - return nothing -end - -function export_room_overview(file::String, rap::RoomAllocationProblem) - open(file, "w") do io - println(io, "="^67) - println(io, "OVERVIEW OF ALL ROOMS") - println(io, "="^67) - println(io) - for (room_id, room) in enumerate(rap.rooms) - @printf(io, "%s (%s, %d beds):\n", room.name, room.gender, room.capacity) - guest_ids = rap.guest_ids_of_room[room_id] - for (bed_nr, guest_id) in enumerate(guest_ids) - guest_name = rap.guests[guest_id].name - guest_gender = rap.guests[guest_id].gender - @printf(io, " %d. %s (%s)\n", bed_nr, guest_name, guest_gender) - end - if length(guest_ids) < room.capacity - for bed_nr in range(start=length(guest_ids)+1, stop=room.capacity) - @printf(io, " %d. ---\n", bed_nr) - end - end - println(io) - end - end - return nothing -end - -function export_guest_overview(file::String, rap::RoomAllocationProblem) - open(file, "w") do io - println(io, "name;gender;room") - for (guest_id, guest) in enumerate(rap.guests) - room_name = rap.rooms[rap.room_id_of_guest[guest_id]].name - @printf(io, "%s;%s;%s\n", guest.name, guest.gender, room_name) - end - end - return nothing -end - -function export_results(rap::RoomAllocationProblem; dir::String="", prefix="") - export_wish_overview(joinpath(dir, prefix*"wishes.txt"), rap) - export_room_overview(joinpath(dir, prefix*"rooms.txt"), rap) - export_guest_overview(joinpath(dir, prefix*"guests.csv"), rap) - return nothing -end - -function log_simulated_annealing_init() - printstyled("-"^67 * "\n"; color=:blue) - println() - printstyled(BANNER; color=:blue) - printstyled("-"^67 * "\n"; color=:blue) - printstyled( - " SOLVING A ROOM ALLOCATION PROBLEM WITH SIMULATED ANNEALING\n"; - color=:blue - ) - printstyled("-"^67 * "\n"; color=:blue) - println() - return nothing -end - -function log_simulated_annealing_start( - rap::RoomAllocationProblem, - n_total_iter, - start_temp, - minimum_temp, - n_iter, -) - println("ROOM ALLOCATION PROBLEM:") - @printf("number of rooms: %15d\n", rap.n_rooms) - @printf("number of beds: %15d\n", rap.n_beds) - @printf("number of guests: %15d\n", rap.n_guests) - @printf("number of wishes: %15d\n", rap.n_wishes) - println() - println("SIMULATED ANNEALING PARAMETERS:") - @printf("start temperature: %15g\n", start_temp) - @printf("minimum temperature: %15g\n", minimum_temp) - @printf("iterations per temp.: %15d\n", n_iter) - @printf("planned guest switches: %15d\n", n_total_iter) - @printf("maximum happiness: %15d\n", rap.max_happiness) - println() - return nothing -end - -function log_simulated_annealing_results( - rap::RoomAllocationProblem, - happiness::Int, - n_total_iter::Int, -) - println() - @printf("total guest switches: %15d\n", n_total_iter) - @printf("happiness: %15d\n", abs(happiness)) - println() - if abs(happiness) == rap.max_happiness - print("all wishes are fulfilled!") - printstyled(" (━☞´◔‿ゝ◔`)━☞ ᕙᓄ(☉ਊ☉)ᓄᕗ\n", color=:blue) - else - fulfilled_wishes_percent = 100 * abs(happiness) / rap.max_happiness - @printf("%.2f %% of all wishes are fulfilled!", fulfilled_wishes_percent) - printstyled(" (͡o‿O͡)\n", color=:blue) - n_fulfilled_wishes = length(findall(rap.fulfilled_wishes .== true)) - n_unfulfilled_wishes = length(findall(rap.fulfilled_wishes .== false)) - @assert rap.n_wishes == n_fulfilled_wishes + n_unfulfilled_wishes - @printf(" %d wishes fulfilled\n", n_fulfilled_wishes) - @printf(" %d wishes not fulfilled\n", n_unfulfilled_wishes) - println() - println("increase the number of guest switches and try again!") - end - # println() -end diff --git a/src/types.jl b/src/types.jl new file mode 100644 index 0000000..8e5c82e --- /dev/null +++ b/src/types.jl @@ -0,0 +1,167 @@ +struct Guest + name::String + gender::Symbol +end + +function Base.show(io::IO, ::MIME"text/plain", g::Guest) + print(io, typeof(g), ": ", g.name, " (", g.gender, ")") + return nothing +end + +struct Wish + mail::String + guest_ids::Vector{Int} + gender::Symbol +end + +function Base.show(io::IO, ::MIME"text/plain", w::Wish) + print(io, length(w.guest_ids), "-person ", typeof(w), " from ", w.mail) + return nothing +end + +struct Room + name::String + capacity::Int + gender::Symbol +end + +function Base.show(io::IO, ::MIME"text/plain", r::Room) + print(io, r.capacity, "-person ", typeof(r), " ", r.name, " (", r.gender, ")") + return nothing +end + +struct RoomOccupancyProblem + n_guests::Int + n_wishes::Int + n_rooms::Int + n_beds::Int + max_happiness::Int + guests::Vector{Guest} + wishes::Vector{Wish} + rooms::Vector{Room} + relations::SparseMatrixCSC{Int64, Int64} + room_id_of_guest::Vector{Int} + guest_ids_of_room::Vector{Vector{Int}} + fulfilled_wishes::Vector{Bool} + + function RoomOccupancyProblem( + guests::Vector{Guest}, + wishes::Vector{Wish}, + rooms::Vector{Room} + ) + # get the basic numbers + n_guests = length(guests) + n_wishes = length(wishes) + n_rooms = length(rooms) + n_beds = sum([r.capacity for r in rooms]) + + # error if not enough beds + n_guests > n_beds && error("more guests than beds specified") + + # find the relations and the maximum happiness between the guests + relations = find_relations(wishes, n_beds) + max_happiness = length(findall(!iszero, relations)) + + # initializations + room_id_of_guest = zeros(Int, n_guests) + guest_ids_of_room = fill(Vector{Int}(), n_rooms) + fulfilled_wishes = zeros(Bool, n_wishes) + + # create instance of RoomOccupancyProblem + new( + n_guests, + n_wishes, + n_rooms, + n_beds, + max_happiness, + guests, + wishes, + rooms, + relations, + room_id_of_guest, + guest_ids_of_room, + fulfilled_wishes + ) + end +end + +struct RoomJugglerJob + n_guests::Int + n_wishes::Int + n_rooms::Int + n_beds::Int + ropf::RoomOccupancyProblem + ropm::RoomOccupancyProblem + + function RoomJugglerJob( + guests_file::String, + wishes_file::String, + rooms_file::String, + ) + # read the csv files + guests = get_guests(guests_file) + wishes = get_wishes(wishes_file, guests) + rooms = get_rooms(rooms_file) + + # get the basic numbers + n_guests = length(guests) + n_wishes = length(wishes) + n_rooms = length(rooms) + n_beds = sum([r.capacity for r in rooms]) + + # filter genders + guests_f, wishes_f = filter_genders(guests, wishes, :F) + guests_m, wishes_m = filter_genders(guests, wishes, :M) + rooms_f = filter(x -> x.gender == :F, rooms) + rooms_m = filter(x -> x.gender == :M, rooms) + + # RoomOccupancyProblem for the females + ropf = RoomOccupancyProblem(guests_f, wishes_f, rooms_f) + + # RoomOccupancyProblem for the males + ropm = RoomOccupancyProblem(guests_m, wishes_m, rooms_m) + + # create RoomJugglerJob instance + new(n_guests, n_wishes, n_rooms, n_beds, ropf, ropm) + end +end + +function Base.show(io::IO, ::MIME"text/plain", rjj::RoomJugglerJob) + println(io, typeof(rjj), ":") + @printf(io, "%d rooms\n", rjj.n_rooms) + @printf(io, " %d females\n", rjj.ropf.n_rooms) + @printf(io, " %d males\n", rjj.ropm.n_rooms) + @printf(io, "%d beds\n", rjj.n_beds) + @printf(io, " %d females\n", rjj.ropf.n_beds) + @printf(io, " %d males\n", rjj.ropm.n_beds) + @printf(io, "%d guests\n", rjj.n_guests) + @printf(io, " %d females\n", rjj.ropf.n_guests) + @printf(io, " %d males\n", rjj.ropm.n_guests) + @printf(io, "%d wishes\n", rjj.n_wishes) + @printf(io, " %d females\n", rjj.ropf.n_wishes) + @printf(io, " %d males\n", rjj.ropm.n_wishes) + return nothing +end + +struct JuggleConfig + n_iter::Int + n_total_iter::Int + beta::Float64 + t_0::Float64 + t_min::Float64 + t_history::Vector{Float64} + + function JuggleConfig(; + n_iter::Int=300, + beta::Real=0.999, + t_0::Real=1.0, + t_min::Real=1e-7, + ) + # check for bounds of beta + !(0 < beta < 1) && throw(BoundsError("condition 0 < β < 1 violated with β = $beta")) + t_history = temperature_history(t_0, t_min, beta) + n_total_iter = length(t_history) * n_iter + + new(n_iter, n_total_iter, beta, t_0, t_min, t_history) + end +end From d1161513ff7a800e050a109dec0e025d8c870a33 Mon Sep 17 00:00:00 2001 From: kfrb Date: Mon, 6 Feb 2023 17:38:28 +0100 Subject: [PATCH 2/2] Fixing tests & small bugs --- Manifest.toml | 6 +- Project.toml | 1 + examples/guests300.jl | 18 +- src/RoomJuggler.jl | 7 +- src/data_import.jl | 1 + src/io.jl | 19 +- test/Manifest.toml | 2 +- test/Project.toml | 1 + test/{room_allocation => }/data/guests10.csv | 0 .../{room_allocation => }/data/guests1000.csv | 0 test/{room_allocation => }/data/rooms10.csv | 0 .../data/rooms10_neb.csv | 0 test/{room_allocation => }/data/wishes10.csv | 0 .../data/wishes10_mg.csv | 1 + .../data/wishes10_mw.csv | 0 .../data/wishes10_nfw.csv | 0 .../data/wishes10_un.csv | 0 test/integrationtests.jl | 50 +++ test/room_allocation/generate_random_rooms.jl | 46 --- .../room_allocation/generate_random_wishes.jl | 51 ---- test/room_allocation/test_room_allocation.jl | 288 ------------------ test/unittests.jl | 200 ++++++++++++ 22 files changed, 283 insertions(+), 408 deletions(-) rename test/{room_allocation => }/data/guests10.csv (100%) rename test/{room_allocation => }/data/guests1000.csv (100%) rename test/{room_allocation => }/data/rooms10.csv (100%) rename test/{room_allocation => }/data/rooms10_neb.csv (100%) rename test/{room_allocation => }/data/wishes10.csv (100%) rename test/{room_allocation => }/data/wishes10_mg.csv (71%) rename test/{room_allocation => }/data/wishes10_mw.csv (100%) rename test/{room_allocation => }/data/wishes10_nfw.csv (100%) rename test/{room_allocation => }/data/wishes10_un.csv (100%) create mode 100644 test/integrationtests.jl delete mode 100644 test/room_allocation/generate_random_rooms.jl delete mode 100644 test/room_allocation/generate_random_wishes.jl delete mode 100644 test/room_allocation/test_room_allocation.jl create mode 100644 test/unittests.jl diff --git a/Manifest.toml b/Manifest.toml index 2fdc5d9..6aebfb8 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.8.5" manifest_format = "2.0" -project_hash = "1991cecf63a4fd8a17ab6cd45097057d3f0fefc8" +project_hash = "69b43e2bea3da736ca1a52d1a871eeb9c7a51b23" [[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" @@ -128,9 +128,9 @@ uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" [[deps.LogExpFunctions]] deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "45b288af6956e67e621c5cbb2d75a261ab58300b" +git-tree-sha1 = "680e733c3a0a9cea9e935c8c2184aea6a63fa0b5" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.20" +version = "0.3.21" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" diff --git a/Project.toml b/Project.toml index 84a9051..e7ea471 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.1.0" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" diff --git a/examples/guests300.jl b/examples/guests300.jl index 37355fd..f9988d4 100644 --- a/examples/guests300.jl +++ b/examples/guests300.jl @@ -1,10 +1,18 @@ using RoomJuggler -guests = joinpath(@__DIR__, "guests300.csv") -wishes = joinpath(@__DIR__, "wishes300.csv") -rooms = joinpath(@__DIR__, "rooms300.csv") +# the files "guests300.csv", "wishes300.csv", "rooms300.csv" +# need to be in the same directory as the julia process is started! +rjj = RoomJugglerJob("guests300.csv", "wishes300.csv", "rooms300.csv") -rjj = RoomJugglerJob(guests, wishes, rooms) +# if this is not getting all wishes fulfilled, uncomment the line below +# to use custom settings for juggling juggle!(rjj) +# juggle!(rjj; config=JuggleConfig(n_iter=400, beta=0.9999, t_0=1, t_min=1e-8)) -export_results(joinpath(@__DIR__, "results"), rjj) +# exports "guests.csv" and the "report.txt" +# guests.csv -> all guests and their rooms +# report.txt -> overview of all wishes and all rooms +export_results("results", rjj) +# if the directory "results" exist, use +# export_results("results", rjj; force=true) +# to overwrite the existing files diff --git a/src/RoomJuggler.jl b/src/RoomJuggler.jl index 4c5ee88..98d75d6 100644 --- a/src/RoomJuggler.jl +++ b/src/RoomJuggler.jl @@ -7,17 +7,12 @@ using SparseArrays using LinearAlgebra using StatsBase: sample using ProgressMeter +using Logging export Guest, Wish, Room, RoomJugglerJob, JuggleConfig export juggle! export export_results -const BANNER = raw""" - ░▒█▀▀▄░▄▀▀▄░▄▀▀▄░█▀▄▀█░░░░▒█░█░▒█░█▀▀▀░█▀▀▀░█░░█▀▀░█▀▀▄ - ░▒█▄▄▀░█░░█░█░░█░█░▀░█░░░░▒█░█░▒█░█░▀▄░█░▀▄░█░░█▀▀░█▄▄▀ - ░▒█░▒█░░▀▀░░░▀▀░░▀░░▒▀░▒█▄▄█░░▀▀▀░▀▀▀▀░▀▀▀▀░▀▀░▀▀▀░▀░▀▀ -""" - include("types.jl") include("data_import.jl") include("juggling.jl") diff --git a/src/data_import.jl b/src/data_import.jl index c44aaa7..9e41847 100644 --- a/src/data_import.jl +++ b/src/data_import.jl @@ -74,6 +74,7 @@ function get_wishes(file::String, guests::Vector{Guest}) else mixed_gender_wish_errorflag = true @error "mixed gender wish found:" guests_in_wish + gender = :mixed end # create Wish instance diff --git a/src/io.jl b/src/io.jl index dd76383..6acc4ff 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,3 +1,9 @@ +const BANNER = raw""" + ░▒█▀▀▄░▄▀▀▄░▄▀▀▄░█▀▄▀█░░░░▒█░█░▒█░█▀▀▀░█▀▀▀░█░░█▀▀░█▀▀▄ + ░▒█▄▄▀░█░░█░█░░█░█░▀░█░░░░▒█░█░▒█░█░▀▄░█░▀▄░█░░█▀▀░█▄▄▀ + ░▒█░▒█░░▀▀░░░▀▀░░▀░░▒▀░▒█▄▄█░░▀▀▀░▀▀▀▀░▀▀▀▀░▀▀░▀▀▀░▀░▀▀ +""" + function log_init() printstyled("-"^67 * "\n"; color=:blue, bold=true) println() @@ -30,14 +36,11 @@ function log_results(rop::RoomOccupancyProblem, happiness::Int, n_total_iter::In else fulfilled_wishes_percent = 100 * abs(happiness) / rop.max_happiness @printf("%.2f %% of all wishes are fulfilled!", fulfilled_wishes_percent) - printstyled(" (͡o‿O͡)\n", color=:blue) - n_fulfilled_wishes = length(findall(rop.fulfilled_wishes .== true)) - n_unfulfilled_wishes = length(findall(rop.fulfilled_wishes .== false)) - @assert rop.n_wishes == n_fulfilled_wishes + n_unfulfilled_wishes - @printf(" %d wishes fulfilled\n", n_fulfilled_wishes) - @printf(" %d wishes not fulfilled\n", n_unfulfilled_wishes) - println() - println("adjust the parameters in JuggleConfig and try again!") + printstyled(" (͡o‿O͡)\n\n", color=:blue) + printstyled("adjust the parameters in JuggleConfig and try again!\n"; + color=:red, + bold=true, + ) end end diff --git a/test/Manifest.toml b/test/Manifest.toml index ed3d074..8f10e0e 100644 --- a/test/Manifest.toml +++ b/test/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.8.5" manifest_format = "2.0" -project_hash = "d40c28f5e146634587084a6bd4e62b349c5c95f1" +project_hash = "aeeb145fbb9a52221a0a281797527954c9e9b880" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" diff --git a/test/Project.toml b/test/Project.toml index 7c239d8..f21eef3 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,3 +1,4 @@ [deps] +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" diff --git a/test/room_allocation/data/guests10.csv b/test/data/guests10.csv similarity index 100% rename from test/room_allocation/data/guests10.csv rename to test/data/guests10.csv diff --git a/test/room_allocation/data/guests1000.csv b/test/data/guests1000.csv similarity index 100% rename from test/room_allocation/data/guests1000.csv rename to test/data/guests1000.csv diff --git a/test/room_allocation/data/rooms10.csv b/test/data/rooms10.csv similarity index 100% rename from test/room_allocation/data/rooms10.csv rename to test/data/rooms10.csv diff --git a/test/room_allocation/data/rooms10_neb.csv b/test/data/rooms10_neb.csv similarity index 100% rename from test/room_allocation/data/rooms10_neb.csv rename to test/data/rooms10_neb.csv diff --git a/test/room_allocation/data/wishes10.csv b/test/data/wishes10.csv similarity index 100% rename from test/room_allocation/data/wishes10.csv rename to test/data/wishes10.csv diff --git a/test/room_allocation/data/wishes10_mg.csv b/test/data/wishes10_mg.csv similarity index 71% rename from test/room_allocation/data/wishes10_mg.csv rename to test/data/wishes10_mg.csv index 239cb74..96e3152 100644 --- a/test/room_allocation/data/wishes10_mg.csv +++ b/test/data/wishes10_mg.csv @@ -1,2 +1,3 @@ mark.white@test.com; Mark White; John Kinder; Martha Chung +josef@tmobile.com; Joseph Russell; Kylie Green co123@web.com; Catherine Owens; Cami Horton; Barbara Brown diff --git a/test/room_allocation/data/wishes10_mw.csv b/test/data/wishes10_mw.csv similarity index 100% rename from test/room_allocation/data/wishes10_mw.csv rename to test/data/wishes10_mw.csv diff --git a/test/room_allocation/data/wishes10_nfw.csv b/test/data/wishes10_nfw.csv similarity index 100% rename from test/room_allocation/data/wishes10_nfw.csv rename to test/data/wishes10_nfw.csv diff --git a/test/room_allocation/data/wishes10_un.csv b/test/data/wishes10_un.csv similarity index 100% rename from test/room_allocation/data/wishes10_un.csv rename to test/data/wishes10_un.csv diff --git a/test/integrationtests.jl b/test/integrationtests.jl new file mode 100644 index 0000000..c6f6c9d --- /dev/null +++ b/test/integrationtests.jl @@ -0,0 +1,50 @@ +@testitem "juggle!" begin + g = joinpath(@__DIR__, "data", "guests10.csv") + w = joinpath(@__DIR__, "data", "wishes10.csv") + r = joinpath(@__DIR__, "data", "rooms10.csv") + rjj = RoomJugglerJob(g, w, r) + juggle!(rjj) + @test rjj.ropf.fulfilled_wishes == [true] + @test rjj.ropf.room_id_of_guest[4] == rjj.ropf.room_id_of_guest[2] && + rjj.ropf.room_id_of_guest[2] == rjj.ropf.room_id_of_guest[3] + for room_id in 1:rjj.ropf.n_rooms + guest_ids = rjj.ropf.guest_ids_of_room[room_id] + @test length(guest_ids) <= rjj.ropf.rooms[room_id].capacity + genders = [g.gender for g in rjj.ropf.guests[guest_ids]] + @test allequal(genders) + end + for guest_id in 1:rjj.ropf.n_guests + room_id = rjj.ropf.room_id_of_guest[guest_id] + @test guest_id in rjj.ropf.guest_ids_of_room[room_id] + end + @test rjj.ropm.fulfilled_wishes == [true] + @test rjj.ropm.room_id_of_guest[1] == rjj.ropm.room_id_of_guest[5] + for i in 1:rjj.ropm.n_rooms + guest_ids = rjj.ropm.guest_ids_of_room[i] + @test length(guest_ids) <= rjj.ropm.rooms[i].capacity + genders = [g.gender for g in rjj.ropm.guests[guest_ids]] + @test allequal(genders) + end + for guest_id in 1:rjj.ropm.n_guests + room_id = rjj.ropm.room_id_of_guest[guest_id] + @test guest_id in rjj.ropm.guest_ids_of_room[room_id] + end + results_dir = joinpath(@__DIR__, "temp_results") + isdir(results_dir) && rm(results_dir; recursive=true) + export_results(results_dir, rjj) + @test isfile(joinpath(@__DIR__, "temp_results", "guests.csv")) + @test isfile(joinpath(@__DIR__, "temp_results", "report.txt")) + rm(results_dir; recursive=true) + juggle!(rjj) + @test rjj.ropf.room_id_of_guest[4] == rjj.ropf.room_id_of_guest[2] && + rjj.ropf.room_id_of_guest[2] == rjj.ropf.room_id_of_guest[3] +end + +@testitem "not fulfillable wishes" begin + g = joinpath(@__DIR__, "data", "guests10.csv") + w = joinpath(@__DIR__, "data", "wishes10_nfw.csv") + r = joinpath(@__DIR__, "data", "rooms10.csv") + rjj = RoomJugglerJob(g, w, r) + juggle!(rjj; config=JuggleConfig(beta=0.8, n_iter=10)) + @test rjj.ropf.fulfilled_wishes == [false] +end diff --git a/test/room_allocation/generate_random_rooms.jl b/test/room_allocation/generate_random_rooms.jl deleted file mode 100644 index e1e26b1..0000000 --- a/test/room_allocation/generate_random_rooms.jl +++ /dev/null @@ -1,46 +0,0 @@ -using DelimitedFiles - -function generate_random_rooms(n_guests; max_room_size=8) - n_rooms = floor(Int, n_guests / max_room_size) - capacities = length.(defaultdist(n_guests, n_rooms)) - names = ["room $i" for i in 1:n_rooms] - genders = [rand([:M, :F]) for _ in 1:n_rooms] - open(joinpath(@__DIR__, "data", string("rooms",n_guests,".csv")), "w") do io - writedlm(io, [["name" "capacity" "gender"]; [names capacities genders]], "; ") - end -end - -""" - defaultdist(sz::Int, nc::Int) - -Get array of indices for dividing sz into nc chunks. -""" -function defaultdist(sz::Int, nc::Int) - if sz >= nc - chunk_size = div(sz, nc) - remainder = rem(sz, nc) - sidx = zeros(Int64, nc + 1) - for i in 1:(nc + 1) - sidx[i] += (i - 1) * chunk_size + 1 - if i <= remainder - sidx[i] += i - 1 - else - sidx[i] += remainder - end - end - grid = fill(0:0, nc) - for i in 1:nc - grid[i] = sidx[i]:(sidx[i + 1] - 1) - end - return grid - else - sidx = [1:(sz + 1);] - grid = fill(0:0, nc) - for i in 1:sz - grid[i] = sidx[i]:(sidx[i + 1] - 1) - end - return grid - end -end - -generate_random_rooms(300) diff --git a/test/room_allocation/generate_random_wishes.jl b/test/room_allocation/generate_random_wishes.jl deleted file mode 100644 index ad1a013..0000000 --- a/test/room_allocation/generate_random_wishes.jl +++ /dev/null @@ -1,51 +0,0 @@ -using RoomJuggler -using DelimitedFiles -using Random - -function get_random_wishes(guests, max_wish_size::Int=8) - n = length(guests) - guest_names = [g.name for g in guests] - wishes = Vector{Vector{Int}}(undef, 0) - isinwish = Set{Int}() - for _ in 1:floor(n/max_wish_size) - n_persons = rand(2:max_wish_size) - guest_ids = zeros(Int, n_persons) - for i in 1:n_persons - idnotfound = true - id = 0 - while idnotfound - id = rand(1:n) - if !(id in isinwish) - push!(isinwish, id) - idnotfound = false - end - end - guest_ids[i] = id - end - push!(wishes, guest_ids) - end - wishes = [getindex(guest_names, guest_id) for guest_id in wishes] - return wishes -end - -function make_wishes(guests_file, wishes_file, mwm=8, mwf=8) - guests = RoomJuggler.get_guests(guests_file) - Fwishes = get_random_wishes(filter(x -> x.gender == :F, guests), mwf) - Mwishes = get_random_wishes(filter(x -> x.gender == :M, guests), mwm) - open(wishes_file, "w") do io - writedlm(io, Fwishes, "; ") - writedlm(io, Mwishes, "; ") - end - lines = readlines(wishes_file) - open(wishes_file, "w") do io - for line in lines - write(io, randstring(8)*"@"*randstring(4)*".com; "*line*"\n") - end - end - return nothing -end - -make_wishes( - joinpath(@__DIR__, "data", "guests1000.csv"), - joinpath(@__DIR__, "data", "wishes1000.csv"), -) diff --git a/test/room_allocation/test_room_allocation.jl b/test/room_allocation/test_room_allocation.jl deleted file mode 100644 index ab22bb1..0000000 --- a/test/room_allocation/test_room_allocation.jl +++ /dev/null @@ -1,288 +0,0 @@ -@testitem "guests" begin - using RoomJuggler - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - guests = RoomJuggler.get_guests(guests_file) - guests_manually = [ - Guest("Martha Chung", :F), - Guest("John Kinder", :M), - Guest("Cami Horton", :F), - Guest("Asa Martell", :M), - Guest("Barbara Brown", :F), - Guest("Sean Cortez", :M), - Guest("Catherine Owens", :F), - Guest("Joseph Russell", :M), - Guest("Mark White", :M), - Guest("Kylie Green", :F), - ] - @test length(guests) == length(guests_manually) - for (i, guest) in enumerate(guests_manually) - @test guests[i].name == guest.name - @test guests[i].gender == guest.gender - end - io = IOBuffer() - show(IOContext(io), "text/plain", guests_manually[1]) - @test String(take!(io)) == "RoomJuggler.Guest: Martha Chung (F)" -end - -@testitem "wishes" begin - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - guests = RoomJuggler.get_guests(guests_file) - wishes_file = joinpath(@__DIR__, "data", "wishes10.csv") - wishes = RoomJuggler.get_wishes(wishes_file, guests) - wishes_manually = [ - Wish("mark.white@test.com", [9, 2], :M), - Wish("co123@web.com", [7, 3, 5], :F), - ] - @test length(wishes) == length(wishes_manually) - for (i, wish) in enumerate(wishes_manually) - @test wishes[i].mail == wish.mail - @test wishes[i].guest_ids == wish.guest_ids - @test wishes[i].gender == wish.gender - end - io = IOBuffer() - show(IOContext(io), "text/plain", wishes_manually[1]) - @test String(take!(io)) == "2-person RoomJuggler.Wish from mark.white@test.com" -end - -@testitem "mixed gender wishes" begin - mg_info_file = joinpath(@__DIR__, "data", "mixed_gender_wishes_in_wishes10_mg.txt") - if isfile(mg_info_file) - rm(mg_info_file, force=true) - end - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - guests = RoomJuggler.get_guests(guests_file) - wishes_mg_file = joinpath(@__DIR__, "data", "wishes10_mg.csv") # mixed gender - @test_throws ErrorException wishes_mg = RoomJuggler.get_wishes( - wishes_mg_file, - guests, - ) - @test isfile(mg_info_file) - mg_info_file_content = read(mg_info_file, String) - @test occursin("Martha Chung", mg_info_file_content) - @test occursin("mark.white@test.com", mg_info_file_content) - rm(mg_info_file, force=true) -end - -@testitem "multiple wishes per person" begin - mw_info_file = joinpath(@__DIR__, "data", "multiple_wishes_in_wishes10_mw.txt") - if isfile(mw_info_file) - rm(mw_info_file, force=true) - end - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - guests = RoomJuggler.get_guests(guests_file) - wishes_mw_file = joinpath(@__DIR__, "data", "wishes10_mw.csv") # multiple wishes - @test_throws ErrorException wishes_mw = RoomJuggler.get_wishes( - wishes_mw_file, - guests, - ) - @test isfile(mw_info_file) - mw_info_file_content = read(mw_info_file, String) - @test occursin("John Kinder", mw_info_file_content) - @test occursin("mark.white@test.com", mw_info_file_content) - @test occursin("john.kinder@tmobile.com", mw_info_file_content) - rm(mw_info_file, force=true) -end - -@testitem "unknown guests" begin - un_info_file = joinpath(@__DIR__, "data", "unknown_guests_in_wishes10_un.txt") - if isfile(un_info_file) - rm(un_info_file, force=true) - end - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - guests = RoomJuggler.get_guests(guests_file) - wishes_un_file = joinpath(@__DIR__, "data", "wishes10_un.csv") # unknown guest - @test_throws ErrorException wishes_un = RoomJuggler.get_wishes( - wishes_un_file, - guests, - ) - @test isfile(un_info_file) - un_info_file_content = read(un_info_file, String) - @test occursin("Bibi Blocksberg", un_info_file_content) - @test occursin("John Legend", un_info_file_content) - @test occursin("co123@web.com", un_info_file_content) - @test occursin("mark.white@test.com", un_info_file_content) - rm(un_info_file, force=true) -end - -@testitem "rooms" begin - rooms_file = joinpath(@__DIR__, "data", "rooms10.csv") - rooms = RoomJuggler.get_rooms(rooms_file) - rooms_manually = [ - Room("room 1", 3, :F), - Room("room 2", 4, :F), - Room("room 3", 2, :M), - Room("room 4", 5, :M), - ] - @test length(rooms) == length(rooms_manually) - for (i, room) in enumerate(rooms_manually) - @test rooms[i].name == room.name - @test rooms[i].capacity == room.capacity - @test rooms[i].gender == room.gender - end - io = IOBuffer() - show(IOContext(io), "text/plain", rooms_manually[1]) - @test String(take!(io)) == "3-person RoomJuggler.Room with name: room 1" -end - -@testitem "not enough beds" begin - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - wishes_file = joinpath(@__DIR__, "data", "wishes10.csv") - rooms_file = joinpath(@__DIR__, "data", "rooms10_neb.csv") - gwrf, gwrm = get_gwr_split_genders(guests_file, wishes_file, rooms_file) - @test_throws ErrorException RoomAllocationProblem(gwrf...) - @test_throws ErrorException RoomAllocationProblem(gwrm...) -end - -@testitem "get_gwr" begin - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - wishes_file = joinpath(@__DIR__, "data", "wishes10.csv") - rooms_file = joinpath(@__DIR__, "data", "rooms10.csv") - gwr = get_gwr(guests_file, wishes_file, rooms_file) - @test length(gwr[1]) == 10 - @test length(gwr[2]) == 2 - @test length(gwr[3]) == 4 -end - -@testitem "gender separated raps" begin - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - wishes_file = joinpath(@__DIR__, "data", "wishes10.csv") - rooms_file = joinpath(@__DIR__, "data", "rooms10.csv") - gwrf, gwrm = get_gwr_split_genders(guests_file, wishes_file, rooms_file) - rapf = RoomAllocationProblem(gwrf...) - @test rapf.n_guests == 5 - @test rapf.n_wishes == 1 - @test rapf.n_rooms == 2 - @test rapf.n_beds == 7 - @test rapf.max_happiness == 6 - guests_f_manually = [ - Guest("Martha Chung", :F), - Guest("Cami Horton", :F), - Guest("Barbara Brown", :F), - Guest("Catherine Owens", :F), - Guest("Kylie Green", :F), - ] - @test length(rapf.guests) == length(guests_f_manually) - for (i, guest) in enumerate(guests_f_manually) - @test rapf.guests[i].name == guest.name - @test rapf.guests[i].gender == guest.gender - end - io = IOBuffer() - show(IOContext(io), "text/plain", rapf) - @test String(take!(io)) == "2-room RoomJuggler.RoomAllocationProblem:" * - "\n 7 beds\n 5 guests\n 1 wishes" - rapm = RoomAllocationProblem(gwrm...) - @test rapm.n_guests == 5 - @test rapm.n_wishes == 1 - @test rapm.n_rooms == 2 - @test rapm.n_beds == 7 - @test rapm.max_happiness == 2 - guests_m_manually = [ - Guest("John Kinder", :M), - Guest("Asa Martell", :M), - Guest("Sean Cortez", :M), - Guest("Joseph Russell", :M), - Guest("Mark White", :M), - ] - @test length(rapm.guests) == length(guests_m_manually) - for (i, guest) in enumerate(guests_m_manually) - @test rapm.guests[i].name == guest.name - @test rapm.guests[i].gender == guest.gender - end - io = IOBuffer() - show(IOContext(io), "text/plain", rapm) - @test String(take!(io)) == "2-room RoomJuggler.RoomAllocationProblem:" * - "\n 7 beds\n 5 guests\n 1 wishes" -end - -@testitem "simulated annealing" begin - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - wishes_file = joinpath(@__DIR__, "data", "wishes10.csv") - rooms_file = joinpath(@__DIR__, "data", "rooms10.csv") - gwrf, gwrm = get_gwr_split_genders(guests_file, wishes_file, rooms_file) - rapf = RoomAllocationProblem(gwrf...) - simulated_annealing!(rapf; - start_temp=1, - minimum_temp=1e-7, - β=0.999, - n_iter=300, - ) - @test rapf.fulfilled_wishes == [true] - @test rapf.room_id_of_guest[4] == rapf.room_id_of_guest[2] && - rapf.room_id_of_guest[2] == rapf.room_id_of_guest[3] - for room_id in 1:rapf.n_rooms - guest_ids = rapf.guest_ids_of_room[room_id] - @test length(guest_ids) <= rapf.rooms[room_id].capacity - genders = [g.gender for g in rapf.guests[guest_ids]] - @test allequal(genders) - end - for guest_id in 1:rapf.n_guests - room_id = rapf.room_id_of_guest[guest_id] - @test guest_id in rapf.guest_ids_of_room[room_id] - end - resfile_f_guests = joinpath(@__DIR__, "res_f_guests.csv") - resfile_f_wishes = joinpath(@__DIR__, "res_f_wishes.txt") - resfile_f_rooms = joinpath(@__DIR__, "res_f_rooms.txt") - isfile(resfile_f_guests) && rm(resfile_f_guests) - isfile(resfile_f_wishes) && rm(resfile_f_wishes) - isfile(resfile_f_rooms) && rm(resfile_f_rooms) - export_results(rapf; dir=@__DIR__, prefix="res_f_") - @test isfile(resfile_f_guests) - @test isfile(resfile_f_wishes) - @test isfile(resfile_f_rooms) - rm(resfile_f_guests) - rm(resfile_f_wishes) - rm(resfile_f_rooms) - rapm = RoomAllocationProblem(gwrm...) - simulated_annealing!(rapm; - start_temp=1, - minimum_temp=1e-7, - β=0.999, - n_iter=300, - ) - @test rapm.fulfilled_wishes == [true] - @test rapm.room_id_of_guest[1] == rapm.room_id_of_guest[5] - for i in 1:rapm.n_rooms - guest_ids = rapm.guest_ids_of_room[i] - @test length(guest_ids) <= rapm.rooms[i].capacity - genders = [g.gender for g in rapm.guests[guest_ids]] - @test allequal(genders) - end - for guest_id in 1:rapm.n_guests - room_id = rapm.room_id_of_guest[guest_id] - @test guest_id in rapm.guest_ids_of_room[room_id] - end - resfile_m_guests = joinpath(@__DIR__, "res_m_guests.csv") - resfile_m_wishes = joinpath(@__DIR__, "res_m_wishes.txt") - resfile_m_rooms = joinpath(@__DIR__, "res_m_rooms.txt") - isfile(resfile_m_guests) && rm(resfile_m_guests) - isfile(resfile_m_wishes) && rm(resfile_m_wishes) - isfile(resfile_m_rooms) && rm(resfile_m_rooms) - export_results(rapm; dir=@__DIR__, prefix="res_m_") - @test isfile(resfile_m_guests) - @test isfile(resfile_m_wishes) - @test isfile(resfile_m_rooms) - rm(resfile_m_guests) - rm(resfile_m_wishes) - rm(resfile_m_rooms) - @test_throws ErrorException simulated_annealing!(rapm; - start_temp=1, - minimum_temp=1e-7, - β=1.1, # Infinity-Loop! - n_iter=300, - ) -end - -@testitem "not fulfillable wishes" begin - guests_file = joinpath(@__DIR__, "data", "guests10.csv") - wishes_file = joinpath(@__DIR__, "data", "wishes10_nfw.csv") - rooms_file = joinpath(@__DIR__, "data", "rooms10.csv") - gwrf, _ = get_gwr_split_genders(guests_file, wishes_file, rooms_file) - rapf = RoomAllocationProblem(gwrf...) - simulated_annealing!(rapf; - start_temp=1, - minimum_temp=1e-7, - β=0.8, - n_iter=10, - ) - @test rapf.fulfilled_wishes == [false] -end diff --git a/test/unittests.jl b/test/unittests.jl new file mode 100644 index 0000000..e523c79 --- /dev/null +++ b/test/unittests.jl @@ -0,0 +1,200 @@ +@testitem "get_guests" begin + guests_file = joinpath(@__DIR__, "data", "guests10.csv") + guests = RoomJuggler.get_guests(guests_file) + guests_manually = [ + Guest("Martha Chung", :F), + Guest("John Kinder", :M), + Guest("Cami Horton", :F), + Guest("Asa Martell", :M), + Guest("Barbara Brown", :F), + Guest("Sean Cortez", :M), + Guest("Catherine Owens", :F), + Guest("Joseph Russell", :M), + Guest("Mark White", :M), + Guest("Kylie Green", :F), + ] + @test length(guests) == length(guests_manually) + for (i, guest) in enumerate(guests_manually) + @test guests[i].name == guest.name + @test guests[i].gender == guest.gender + end + io = IOBuffer() + show(IOContext(io), "text/plain", guests_manually[1]) + @test String(take!(io)) == "RoomJuggler.Guest: Martha Chung (F)" +end + +@testitem "wishes" begin + guests_file = joinpath(@__DIR__, "data", "guests10.csv") + guests = RoomJuggler.get_guests(guests_file) + wishes_file = joinpath(@__DIR__, "data", "wishes10.csv") + wishes = RoomJuggler.get_wishes(wishes_file, guests) + wishes_manually = [ + Wish("mark.white@test.com", [9, 2], :M), + Wish("co123@web.com", [7, 3, 5], :F), + ] + @test length(wishes) == length(wishes_manually) + for (i, wish) in enumerate(wishes_manually) + @test wishes[i].mail == wish.mail + @test wishes[i].guest_ids == wish.guest_ids + @test wishes[i].gender == wish.gender + end + io = IOBuffer() + show(IOContext(io), "text/plain", wishes_manually[1]) + @test String(take!(io)) == "2-person RoomJuggler.Wish from mark.white@test.com" +end + +@testitem "mixed gender wishes" begin + using Logging + guests_file = joinpath(@__DIR__, "data", "guests10.csv") + guests = RoomJuggler.get_guests(guests_file) + wishes_mg_file = joinpath(@__DIR__, "data", "wishes10_mg.csv") # mixed gender + @test_throws ErrorException wishes_mg = RoomJuggler.get_wishes( + wishes_mg_file, + guests, + ) + io = IOBuffer() + logger = SimpleLogger(io) + with_logger(logger) do + try + wishes_mg = RoomJuggler.get_wishes(wishes_mg_file, guests) + catch + end + end + flush(io) + message = String(take!(io)) + @test occursin("Martha Chung", message) + @test occursin("Mark White", message) + @test occursin("John Kinder", message) + @test occursin("Joseph Russell", message) + @test occursin("Kylie Green", message) +end + +@testitem "multiple wishes per person" begin + using Logging + guests_file = joinpath(@__DIR__, "data", "guests10.csv") + guests = RoomJuggler.get_guests(guests_file) + wishes_mw_file = joinpath(@__DIR__, "data", "wishes10_mw.csv") # multiple wishes + @test_throws ErrorException wishes_mw = RoomJuggler.get_wishes( + wishes_mw_file, + guests, + ) + io = IOBuffer() + logger = SimpleLogger(io) + with_logger(logger) do + try + wishes_mg = RoomJuggler.get_wishes(wishes_mw_file, guests) + catch + end + end + flush(io) + message = String(take!(io)) + @test occursin("John Kinder", message) + @test occursin("mark.white@test.com", message) + @test occursin("john.kinder@tmobile.com", message) +end + +@testitem "unknown guests" begin + using Logging + guests_file = joinpath(@__DIR__, "data", "guests10.csv") + guests = RoomJuggler.get_guests(guests_file) + wishes_un_file = joinpath(@__DIR__, "data", "wishes10_un.csv") # unknown guest + @test_throws ErrorException wishes_un = RoomJuggler.get_wishes( + wishes_un_file, + guests, + ) + io = IOBuffer() + logger = SimpleLogger(io) + with_logger(logger) do + try + wishes_mg = RoomJuggler.get_wishes(wishes_un_file, guests) + catch + end + end + flush(io) + message = String(take!(io)) + @test occursin("Bibi Blocksberg", message) + @test occursin("John Legend", message) + @test occursin("co123@web.com", message) + @test occursin("mark.white@test.com", message) +end + +@testitem "rooms" begin + rooms_file = joinpath(@__DIR__, "data", "rooms10.csv") + rooms = RoomJuggler.get_rooms(rooms_file) + rooms_manually = [ + Room("room 1", 3, :F), + Room("room 2", 4, :F), + Room("room 3", 2, :M), + Room("room 4", 5, :M), + ] + @test length(rooms) == length(rooms_manually) + for (i, room) in enumerate(rooms_manually) + @test rooms[i].name == room.name + @test rooms[i].capacity == room.capacity + @test rooms[i].gender == room.gender + end + io = IOBuffer() + show(IOContext(io), "text/plain", rooms_manually[1]) + @test String(take!(io)) == "3-person RoomJuggler.Room room 1 (F)" +end + +@testitem "not enough beds" begin + g = joinpath(@__DIR__, "data", "guests10.csv") + w = joinpath(@__DIR__, "data", "wishes10.csv") + r = joinpath(@__DIR__, "data", "rooms10_neb.csv") + @test_throws ErrorException("more guests than beds specified") RoomJugglerJob(g, w, r) +end + +@testitem "RoomJugglerJob" begin + g = joinpath(@__DIR__, "data", "guests10.csv") + w = joinpath(@__DIR__, "data", "wishes10.csv") + r = joinpath(@__DIR__, "data", "rooms10.csv") + rjj = RoomJugglerJob(g, w, r) + @test rjj.n_guests == 10 + @test rjj.n_wishes == 2 + @test rjj.n_rooms == 4 + @test rjj.n_beds == 14 + @test rjj.ropf.n_guests == 5 + @test rjj.ropf.n_wishes == 1 + @test rjj.ropf.n_rooms == 2 + @test rjj.ropf.n_beds == 7 + @test rjj.ropf.max_happiness == 6 + guests_f_manually = [ + Guest("Martha Chung", :F), + Guest("Cami Horton", :F), + Guest("Barbara Brown", :F), + Guest("Catherine Owens", :F), + Guest("Kylie Green", :F), + ] + @test length(rjj.ropf.guests) == length(guests_f_manually) + for (i, guest) in enumerate(guests_f_manually) + @test rjj.ropf.guests[i].name == guest.name + @test rjj.ropf.guests[i].gender == guest.gender + end + @test rjj.ropm.n_guests == 5 + @test rjj.ropm.n_wishes == 1 + @test rjj.ropm.n_rooms == 2 + @test rjj.ropm.n_beds == 7 + @test rjj.ropm.max_happiness == 2 + guests_m_manually = [ + Guest("John Kinder", :M), + Guest("Asa Martell", :M), + Guest("Sean Cortez", :M), + Guest("Joseph Russell", :M), + Guest("Mark White", :M), + ] + @test length(rjj.ropm.guests) == length(guests_m_manually) + for (i, guest) in enumerate(guests_m_manually) + @test rjj.ropm.guests[i].name == guest.name + @test rjj.ropm.guests[i].gender == guest.gender + end + io = IOBuffer() + show(IOContext(io), "text/plain", rjj) + @test String(take!(io)) == "RoomJuggler.RoomJugglerJob:\n4 rooms\n 2 females\n 2 " * + "males\n14 beds\n 7 females\n 7 males\n10 guests\n 5 females\n 5 males\n2 " * + "wishes\n 1 females\n 1 males\n" +end + +@testitem "JuggleConfig" begin + @test_throws BoundsError JuggleConfig(; n_iter=300, beta=1.1, t_0=1.0, t_min=1e-7) +end