{ "cells": [ { "cell_type": "markdown", "id": "f0435d52-4276-4026-bfeb-8a741b01fc31", "metadata": {}, "source": [ "# Realist Vampire(s) Seeking Consenting Suicidal Person(s)" ] }, { "cell_type": "markdown", "id": "b0f77714-e9ad-4a32-8382-9527016422b1", "metadata": {}, "source": [ ":::note\n", "This is the companion notebook to the article *Realist Vampire(s) Seeking Consenting Suicidal Person(s)* (SAGT 2026, Springer LNCS) and to its extended version (available on HAL and arXiv). It is NOT intended to be self-contained. Please refer to the paper for context and explanations.\n", ":::\n", "" ] }, { "cell_type": "markdown", "id": "922af91d-3fd2-4696-aa90-e49f3c1f0d64", "metadata": {}, "source": [ "## Preliminaries" ] }, { "cell_type": "markdown", "id": "99b65834-2028-469a-8681-6db920df9d70", "metadata": {}, "source": [ "### Basic Blood Types" ] }, { "cell_type": "markdown", "id": "42dec34d-9cf2-4aa1-a814-ccddeaa2c464", "metadata": {}, "source": "First, we define the different blood types. In addition to the $2^3$ main types depending on the presence of $A$, $B$, and $Rhesus$ markers, we add a void type that will be used later to model non-ghoul humans.\n\nAn important property is *can_feed*, that tells whether a vampire of a given type can feed on a ghoul of another type." }, { "cell_type": "code", "execution_count": 1, "id": "3ccbc307-90c6-4a63-87d3-1984672fa51d", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:17.279414Z", "iopub.status.busy": "2026-07-01T11:20:17.278769Z", "iopub.status.idle": "2026-07-01T11:20:17.646872Z", "shell.execute_reply": "2026-07-01T11:20:17.644864Z", "shell.execute_reply.started": "2026-07-01T11:20:17.279368Z" } }, "outputs": [], "source": [ "import numpy as np \n", "\n", "class BloodType:\n", " A = [\"\", \"A\"]\n", " B = [\"\", \"B\"]\n", " R = [\"-\", \"+\"]\n", "\n", " def __init__(self, i, j, k):\n", " self.markers = np.array([i, j, k])\n", " self.x = False\n", "\n", " def __repr__(self):\n", " if self.x:\n", " return 'X'\n", " i, j, k = self.markers\n", " result = f\"{self.A[i]}{self.B[j]}{self.R[k]}\"\n", " if len(result) == 1:\n", " return f\"O{result}\"\n", " else:\n", " return result\n", "\n", " @property\n", " def sanitized_name(self):\n", " res = self.__repr__()\n", " return res.replace('-','m').replace('+','p')\n", "\n", " def can_feed_on(self, other):\n", " return np.all(other.markers <= self.markers)\n", "\n", " @classmethod\n", " def void(cls):\n", " res = cls(1, 1, 1)\n", " res.x = True\n", " return res" ] }, { "cell_type": "markdown", "id": "f7ff914a-2295-4e1f-894a-0b7747d8f4d0", "metadata": {}, "source": [ "Let us compute all possible groups. We'll store them in a list, a class, and a dict for easy access." ] }, { "cell_type": "code", "execution_count": 2, "id": "f6a15fa9-0e85-4410-a9da-2070519d2cfe", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:17.648826Z", "iopub.status.busy": "2026-07-01T11:20:17.648227Z", "iopub.status.idle": "2026-07-01T11:20:17.673039Z", "shell.execute_reply": "2026-07-01T11:20:17.670691Z", "shell.execute_reply.started": "2026-07-01T11:20:17.648780Z" } }, "outputs": [ { "data": { "text/plain": [ "[O-, O+, B-, B+, A-, A+, AB-, AB+]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Groups:\n", " pass\n", "\n", "groups = [BloodType(i, j, k) for i in range(2) for j in range(2) for k in range(2)]\n", "g = Groups\n", "for t in groups:\n", " setattr(g, t.sanitized_name, t)\n", "groups_ = {str(g): g for g in groups}\n", "\n", "groups" ] }, { "cell_type": "markdown", "id": "8c3d6104-7410-4463-9bff-466f7cf4d8bd", "metadata": {}, "source": [ "### Vampire/Ghoul Types" ] }, { "cell_type": "markdown", "id": "96ef629a-334e-4e29-b28c-f649ece2c40e", "metadata": {}, "source": "We now define Vampire/Ghoul types, which are defined by two blood types. The following properties are defined:\n- *autarkic*: the vampire can feed on their ghoul.\n- *compatible_with*: each vampire of the two pairs can feed on the other pair's ghoul.\n- *swinger*: a non plug-and-drink pair compatible with another non plug-and-drink pair.\n- *helpless*: a non plug-and-drink pair that is only compatible with plug-and-drink pairs.\n- *three_compatible*: each vampire of three pairs can feed on a distinct available ghoul, in a cyclic fashion." }, { "cell_type": "code", "execution_count": 3, "id": "245429b6-8b16-41d8-9495-3275928ce083", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:17.674661Z", "iopub.status.busy": "2026-07-01T11:20:17.674211Z", "iopub.status.idle": "2026-07-01T11:20:17.687724Z", "shell.execute_reply": "2026-07-01T11:20:17.686020Z", "shell.execute_reply.started": "2026-07-01T11:20:17.674621Z" } }, "outputs": [], "source": [ "class VGType:\n", " def __init__(self, v, g):\n", " self.v = v\n", " self.g = g\n", " self.compatible_types = []\n", "\n", " def __repr__(self):\n", " return f\"{self.v}/{self.g}\"\n", "\n", " @property\n", " def category(self):\n", " if self.auto_compatible:\n", " return \"autarkic\"\n", " if all(c.auto_compatible for c in self.compatible_types):\n", " return \"helpless\"\n", " return \"swinger\"\n", " \n", " @property\n", " def auto_compatible(self):\n", " return self.v.can_feed_on(self.g)\n", "\n", " def compatible_with(self, other):\n", " return self.v.can_feed_on(other.g) and other.v.can_feed_on(self.g)\n", "\n", " def three_cycle(self, other1, other2):\n", " return self.v.can_feed_on(other1.g) and other1.v.can_feed_on(other2.g) and other2.v.can_feed_on(self.g)\n", " \n", " def three_compatible_with(self, other1, other2):\n", " return self.three_cycle(other1, other2) or self.three_cycle(other2, other1)" ] }, { "cell_type": "markdown", "id": "2d9aad78-0beb-492d-ab80-622214fd2aa8", "metadata": {}, "source": "We now prepare all possible VG pairs and observe their distribution." }, { "cell_type": "code", "execution_count": 4, "id": "2643225d-c14d-49da-b1c6-8e201312f1b9", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:17.689993Z", "iopub.status.busy": "2026-07-01T11:20:17.688933Z", "iopub.status.idle": "2026-07-01T11:20:17.766544Z", "shell.execute_reply": "2026-07-01T11:20:17.764702Z", "shell.execute_reply.started": "2026-07-01T11:20:17.689952Z" } }, "outputs": [ { "data": { "text/plain": [ "Counter({'autarkic': 27, 'helpless': 19, 'swinger': 18})" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from collections import Counter\n", "vgs = [VGType(v, g) for v in groups for g in groups]\n", "for v1 in vgs:\n", " for v2 in vgs:\n", " if v1.compatible_with(v2):\n", " v1.compatible_types.append(v2)\n", "vgs_ = {str(vg): i for i, vg in enumerate(vgs)}\n", "Counter(vg.category for vg in vgs)" ] }, { "cell_type": "markdown", "id": "3651463f-d2d3-4aa1-8183-2cd4becbcce9", "metadata": {}, "source": [ "### Human types" ] }, { "cell_type": "markdown", "id": "2c4c2aec-db78-43fb-a94c-b2de6bfeadfd", "metadata": {}, "source": "We call *human* a potential ghoul not yet affiliated to any vampire. Humans can be modelled as a VG pair where the vampire is a placeholder, denoted $X$, compatible with everyone, e.g. *X/O+*" }, { "cell_type": "code", "execution_count": 5, "id": "079e1f8e-76c9-4ad3-b8d9-c2b9ce0bc6fe", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:17.768390Z", "iopub.status.busy": "2026-07-01T11:20:17.767957Z", "iopub.status.idle": "2026-07-01T11:20:17.774746Z", "shell.execute_reply": "2026-07-01T11:20:17.772759Z", "shell.execute_reply.started": "2026-07-01T11:20:17.768351Z" } }, "outputs": [], "source": [ "class Human(VGType):\n", " category = \"human\"\n", " def __init__(self, g):\n", " self.v = BloodType.void() # alway compatible\n", " self.g = g" ] }, { "cell_type": "code", "execution_count": 6, "id": "f7cbb332-3ff7-4156-8fcf-f41877e400b1", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:17.776444Z", "iopub.status.busy": "2026-07-01T11:20:17.775990Z", "iopub.status.idle": "2026-07-01T11:20:17.785178Z", "shell.execute_reply": "2026-07-01T11:20:17.783296Z", "shell.execute_reply.started": "2026-07-01T11:20:17.776405Z" } }, "outputs": [ { "data": { "text/plain": [ "[X/O-, X/O+, X/B-, X/B+, X/A-, X/A+, X/AB-, X/AB+]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "humans = [Human(g) for g in groups]\n", "humans" ] }, { "cell_type": "markdown", "id": "eead2b8c-db17-40a7-aef9-3942ddc9696c", "metadata": {}, "source": [ "### Blood type distribution" ] }, { "cell_type": "markdown", "id": "0173162c-7e8f-44f3-926e-b07b359af486", "metadata": {}, "source": [ "For numeric computation, we need statistics on the blood type probabilities. We leverage Wikipedia to access recent data." ] }, { "cell_type": "code", "execution_count": 7, "id": "aae163b5-45ee-4cb0-a05d-b686dccee2f3", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:17.786991Z", "iopub.status.busy": "2026-07-01T11:20:17.786472Z", "iopub.status.idle": "2026-07-01T11:20:18.679031Z", "shell.execute_reply": "2026-07-01T11:20:18.677548Z", "shell.execute_reply.started": "2026-07-01T11:20:17.786949Z" } }, "outputs": [], "source": [ "import requests\n", "import re\n", "from bs4 import BeautifulSoup as bs\n", "from fake_useragent import UserAgent\n", "ua = UserAgent()\n", "soup = bs(requests.get(\"https://en.wikipedia.org/wiki/Blood_type_distribution_by_country\", headers={'User-Agent': ua.random}).content)\n", "\n", "table = soup('table')[1]('tr')\n", "names = [s.text.strip().replace(\"−\", \"-\") for s in table[0]('th')[2:]]\n", "blood_type_by_country = dict()\n", "for line in table[1:]:\n", " try:\n", " country = line.th.text.strip().split('[')[0]\n", " except AttributeError:\n", " continue\n", " stats = {n: float(s.text.strip()[:-1]) if s.text[0].isdigit() else 0.0 for n, s in zip(names, line('td')[1:])}\n", " total = sum(stats.values())\n", " blood_type_by_country[country] = {k: v/total for k, v in stats.items()} \n", " blood_type_by_country[country]['X'] = 1.0" ] }, { "cell_type": "code", "execution_count": 8, "id": "54ea290e-2722-427f-867b-7f7ff511b361", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:18.680616Z", "iopub.status.busy": "2026-07-01T11:20:18.680362Z", "iopub.status.idle": "2026-07-01T11:20:18.686577Z", "shell.execute_reply": "2026-07-01T11:20:18.685350Z", "shell.execute_reply.started": "2026-07-01T11:20:18.680595Z" } }, "outputs": [ { "data": { "text/plain": [ "{'O+': 0.374,\n", " 'A+': 0.35700000000000004,\n", " 'B+': 0.085,\n", " 'AB+': 0.034,\n", " 'O-': 0.066,\n", " 'A-': 0.063,\n", " 'B-': 0.015,\n", " 'AB-': 0.006,\n", " 'X': 1.0}" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "blood_type_by_country['United States']" ] }, { "cell_type": "code", "execution_count": 9, "id": "56001340-90a6-4b9e-816d-64a25d322e96", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:18.687360Z", "iopub.status.busy": "2026-07-01T11:20:18.687147Z", "iopub.status.idle": "2026-07-01T11:20:18.697915Z", "shell.execute_reply": "2026-07-01T11:20:18.696549Z", "shell.execute_reply.started": "2026-07-01T11:20:18.687341Z" } }, "outputs": [ { "data": { "text/plain": [ "{'O+': 0.365,\n", " 'A+': 0.382,\n", " 'B+': 0.077,\n", " 'AB+': 0.025,\n", " 'O-': 0.065,\n", " 'A-': 0.068,\n", " 'B-': 0.013999999999999999,\n", " 'AB-': 0.004,\n", " 'X': 1.0}" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "blood_type_by_country['France']" ] }, { "cell_type": "code", "execution_count": 10, "id": "65d30348-180a-4508-823f-f30970696912", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:18.698714Z", "iopub.status.busy": "2026-07-01T11:20:18.698514Z", "iopub.status.idle": "2026-07-01T11:20:18.705355Z", "shell.execute_reply": "2026-07-01T11:20:18.704133Z", "shell.execute_reply.started": "2026-07-01T11:20:18.698696Z" } }, "outputs": [ { "data": { "text/plain": [ "{'O+': 0.3878396121603878,\n", " 'A+': 0.2757297242702757,\n", " 'B+': 0.0818099181900818,\n", " 'AB+': 0.0201999798000202,\n", " 'O-': 0.1323098676901323,\n", " 'A-': 0.0818099181900818,\n", " 'B-': 0.0201999798000202,\n", " 'AB-': 0.00010099989900010099,\n", " 'X': 1.0}" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "blood_type_by_country['World']" ] }, { "cell_type": "markdown", "id": "1670d114-d53f-416f-b8e3-aefec6fb4a68", "metadata": {}, "source": [ "Given a list of VG types and a country, issue normalized arrival rates." ] }, { "cell_type": "code", "execution_count": 11, "id": "b33926a1-cd56-4b27-b759-7c891c1f57d6", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:18.707255Z", "iopub.status.busy": "2026-07-01T11:20:18.707046Z", "iopub.status.idle": "2026-07-01T11:20:18.723609Z", "shell.execute_reply": "2026-07-01T11:20:18.721953Z", "shell.execute_reply.started": "2026-07-01T11:20:18.707238Z" } }, "outputs": [], "source": [ "def arrival_rates(vgs, country=\"United States\"):\n", " probs = blood_type_by_country[country]\n", " res = [probs[str(vg.v)]*probs[str(vg.g)] for vg in vgs ]\n", " total = sum(res)\n", " return [r/total for r in res]" ] }, { "cell_type": "markdown", "id": "069f6607-4dd2-482b-9f0e-170d9c5c48c9", "metadata": {}, "source": "For the record, one can compute the distribution per VG category:" }, { "cell_type": "code", "execution_count": 12, "id": "91c7059b-f913-4595-8218-5576831828fb", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:18.725104Z", "iopub.status.busy": "2026-07-01T11:20:18.724818Z", "iopub.status.idle": "2026-07-01T11:20:18.740606Z", "shell.execute_reply": "2026-07-01T11:20:18.738473Z", "shell.execute_reply.started": "2026-07-01T11:20:18.725083Z" } }, "outputs": [ { "data": { "text/plain": [ "{'autarkic': 56.6078, 'helpless': 28.1786, 'swinger': 15.2136}" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from collections import defaultdict\n", "shares = defaultdict(float)\n", "rates = arrival_rates(vgs)\n", "for i, vg in enumerate(vgs):\n", " shares[vg.category] += float(100*rates[i])\n", "dict(shares)" ] }, { "cell_type": "markdown", "id": "0c81aba7-689a-4894-88a7-313f5184d5cf", "metadata": {}, "source": [ "## Instability study" ] }, { "cell_type": "markdown", "id": "773494dc-126b-44b2-8187-00d0ab512e5a", "metadata": {}, "source": "We know that the base model is not stabilisable. Yet, we can still investigate through simulations the practical performance of a robust matching algorithm in that setting.\n\nTowards that goal, we write a function that converts a list of VG types into a matching problem." }, { "cell_type": "code", "execution_count": 13, "id": "1d0549fc-6fb6-4717-a697-4de6adfd52aa", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:18.741819Z", "iopub.status.busy": "2026-07-01T11:20:18.741536Z", "iopub.status.idle": "2026-07-01T11:20:22.819686Z", "shell.execute_reply": "2026-07-01T11:20:22.818056Z", "shell.execute_reply.started": "2026-07-01T11:20:18.741794Z" } }, "outputs": [], "source": [ "import stochastic_matching as sm\n", "\n", "def build_model(vgs, country=\"United States\", self=True, relaxed=False, threesomes=False):\n", " edges = []\n", " n = len(vgs)\n", " for i, vg in enumerate(vgs):\n", " if relaxed or (self and vg.auto_compatible):\n", " edges.append([i])\n", " for j in range(i+1, n):\n", " if vg.compatible_with(vgs[j]):\n", " edges.append([i, j])\n", " if not threesomes:\n", " continue\n", " for k in range(j+1, n):\n", " if vg.three_compatible_with(vgs[j], vgs[k]):\n", " if any(str(p.v)==str(p.g) for p in [vg, vgs[j], vgs[k]]):\n", " continue\n", " edges.append([i, j, k])\n", " incidence = np.zeros((n, len(edges)), dtype=int)\n", " for i, e in enumerate(edges):\n", " incidence[e, i] = 1\n", " model = sm.Model(incidence=incidence, names=[str(vg) for vg in vgs], \n", " rates=arrival_rates(vgs, country=country))\n", " model.cats = [vg.category for vg in vgs]\n", " model.edges = edges\n", " try:\n", " model.adjacency = sm.model.incidence_to_adjacency(model.incidence)\n", " except ValueError:\n", " pass\n", " return model" ] }, { "cell_type": "markdown", "id": "34e0619c-661f-4f5b-83dd-77cfe1a5bf34", "metadata": {}, "source": [ "### Swingers-only" ] }, { "cell_type": "markdown", "id": "79ce92e3-42f6-4575-a7a0-d1c49730fef8", "metadata": {}, "source": [ ":::note\n", "The compatibility graphs below are interactive VisJS widgets. They render when the notebook is executed locally (see the README for requirements); most static notebook viewers will not display them.\n", ":::" ] }, { "cell_type": "markdown", "id": "9c58b2df-3662-4e0d-91aa-832a914cb920", "metadata": {}, "source": "We first focus on the interdependent types, because this subgraph is smaller and makes sense from a selfish perspective." }, { "cell_type": "code", "execution_count": 14, "id": "ec596efc-7003-4171-93a9-dc195556e9a6", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T11:20:22.821606Z", "iopub.status.busy": "2026-07-01T11:20:22.821008Z", "iopub.status.idle": "2026-07-01T11:20:22.846736Z", "shell.execute_reply": "2026-07-01T11:20:22.834690Z", "shell.execute_reply.started": "2026-07-01T11:20:22.821577Z" } }, "outputs": [ { "data": { "text/html": [ "\n", "