Repository: AureumChaos/LEAP

Issue:
Crowding distance goes to NaN when one objective is flat
If all individuals in a rank are equal on an objective, the distance goes to NaN. This is because the scale normalization in the divisor is zero, leading to a divide by zero error. The original paper doesn't describe a solution, but my intuition is to treat it as degenerating in dimensions and just zero out that term for crowding distance.

Discussion:

Patch:
diff --git a/examples/distributed/multiobjective_async_distributed.ipynb b/examples/distributed/multiobjective_async_distributed.ipynb
new file mode 100644
index 0000000..20f3996
--- /dev/null
+++ b/examples/distributed/multiobjective_async_distributed.ipynb
@@ -0,0 +1,645 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from matplotlib import pyplot as plt\n",
+    "import pandas as pd\n",
+    "from distributed import Client, LocalCluster\n",
+    "\n",
+    "from leap_ec.representation import Representation\n",
+    "from leap_ec.ops import tournament_selection, clone, evaluate, pool\n",
+    "from leap_ec.real_rep.initializers import create_real_vector\n",
+    "from leap_ec.real_rep.ops import mutate_gaussian\n",
+    "from leap_ec.probe import print_individual\n",
+    "\n",
+    "from leap_ec.multiobjective.problems import SCHProblem\n",
+    "from leap_ec.multiobjective.asynchronous import steady_state_nsga_2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "POP_SIZE=50\n",
+    "MAX_BIRTHS=5000"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sch_problem = SCHProblem() # Schaffer's Study problem"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# We're only going to do a narrow range, which is sufficient for demonstration purposes. The original\n",
+    "# NSGA-II paper explored a wider range.\n",
+    "sch_representation = Representation(initialize=create_real_vector(bounds=[(-10, 10)]))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pipeline = [tournament_selection, # uses domination comparison in MultiObjective.worse_than()\n",
+    "            clone,\n",
+    "            mutate_gaussian(std=0.5, expected_num_mutations=1),\n",
+    "            pool(size=1)\n",
+    "            ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "cluster = LocalCluster(n_workers=15)\n",
+    "client = Client(cluster)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "final_pop = steady_state_nsga_2(client, MAX_BIRTHS,\n",
+    "                   pop_size=POP_SIZE, init_pop_size=POP_SIZE,\n",
+    "                   problem=sch_problem,\n",
+    "                   representation=sch_representation,\n",
+    "                   offspring_pipeline=pipeline)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>gene</th>\n",
+       "      <th>f1</th>\n",
+       "      <th>f2</th>\n",
+       "      <th>rank</th>\n",
+       "      <th>distance</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>1.313075</td>\n",
+       "      <td>1.724166</td>\n",
+       "      <td>0.471866</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.079197</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>0.376336</td>\n",
+       "      <td>0.141629</td>\n",
+       "      <td>2.636285</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.082190</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>0.946484</td>\n",
+       "      <td>0.895831</td>\n",
+       "      <td>1.109897</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.086698</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>1.352775</td>\n",
+       "      <td>1.830000</td>\n",
+       "      <td>0.418900</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.080070</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>1.623328</td>\n",
+       "      <td>2.635193</td>\n",
+       "      <td>0.141882</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.077820</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>1.998065</td>\n",
+       "      <td>3.992265</td>\n",
+       "      <td>0.000004</td>\n",
+       "      <td>1</td>\n",
+       "      <td>inf</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>6</th>\n",
+       "      <td>1.273719</td>\n",
+       "      <td>1.622360</td>\n",
+       "      <td>0.527484</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.078063</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>7</th>\n",
+       "      <td>1.827686</td>\n",
+       "      <td>3.340437</td>\n",
+       "      <td>0.029692</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.082174</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>8</th>\n",
+       "      <td>0.991991</td>\n",
+       "      <td>0.984045</td>\n",
+       "      <td>1.016083</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.090661</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>9</th>\n",
+       "      <td>0.417102</td>\n",
+       "      <td>0.173974</td>\n",
+       "      <td>2.505567</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081382</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>10</th>\n",
+       "      <td>1.587290</td>\n",
+       "      <td>2.519488</td>\n",
+       "      <td>0.170330</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.072845</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>11</th>\n",
+       "      <td>1.868733</td>\n",
+       "      <td>3.492162</td>\n",
+       "      <td>0.017231</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.084057</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>12</th>\n",
+       "      <td>0.708000</td>\n",
+       "      <td>0.501264</td>\n",
+       "      <td>1.669263</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.078893</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>13</th>\n",
+       "      <td>1.953805</td>\n",
+       "      <td>3.817354</td>\n",
+       "      <td>0.002134</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.086649</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>14</th>\n",
+       "      <td>1.550617</td>\n",
+       "      <td>2.404412</td>\n",
+       "      <td>0.201945</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.075431</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>15</th>\n",
+       "      <td>0.541827</td>\n",
+       "      <td>0.293577</td>\n",
+       "      <td>2.126267</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.086206</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>16</th>\n",
+       "      <td>0.214709</td>\n",
+       "      <td>0.046100</td>\n",
+       "      <td>3.187264</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.078217</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>17</th>\n",
+       "      <td>1.235150</td>\n",
+       "      <td>1.525596</td>\n",
+       "      <td>0.584995</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.075601</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>18</th>\n",
+       "      <td>1.080376</td>\n",
+       "      <td>1.167212</td>\n",
+       "      <td>0.845708</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.085875</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>19</th>\n",
+       "      <td>1.511997</td>\n",
+       "      <td>2.286135</td>\n",
+       "      <td>0.238147</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.078118</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>20</th>\n",
+       "      <td>0.746483</td>\n",
+       "      <td>0.557238</td>\n",
+       "      <td>1.571304</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.076846</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>21</th>\n",
+       "      <td>0.498383</td>\n",
+       "      <td>0.248386</td>\n",
+       "      <td>2.254852</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.084373</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22</th>\n",
+       "      <td>1.036989</td>\n",
+       "      <td>1.075347</td>\n",
+       "      <td>0.927390</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.088538</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23</th>\n",
+       "      <td>0.457589</td>\n",
+       "      <td>0.209388</td>\n",
+       "      <td>2.379031</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081411</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>24</th>\n",
+       "      <td>0.667720</td>\n",
+       "      <td>0.445851</td>\n",
+       "      <td>1.774969</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081788</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>25</th>\n",
+       "      <td>1.433681</td>\n",
+       "      <td>2.055441</td>\n",
+       "      <td>0.320717</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.079784</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>26</th>\n",
+       "      <td>0.584450</td>\n",
+       "      <td>0.341582</td>\n",
+       "      <td>2.003781</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.084656</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>27</th>\n",
+       "      <td>0.784719</td>\n",
+       "      <td>0.615784</td>\n",
+       "      <td>1.476908</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.077604</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>28</th>\n",
+       "      <td>0.001491</td>\n",
+       "      <td>0.000002</td>\n",
+       "      <td>3.994037</td>\n",
+       "      <td>1</td>\n",
+       "      <td>inf</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>29</th>\n",
+       "      <td>1.911583</td>\n",
+       "      <td>3.654149</td>\n",
+       "      <td>0.007818</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.085236</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>30</th>\n",
+       "      <td>1.472641</td>\n",
+       "      <td>2.168673</td>\n",
+       "      <td>0.278107</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.078458</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>31</th>\n",
+       "      <td>1.393002</td>\n",
+       "      <td>1.940454</td>\n",
+       "      <td>0.368447</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081052</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>32</th>\n",
+       "      <td>1.198252</td>\n",
+       "      <td>1.435808</td>\n",
+       "      <td>0.642800</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.075287</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>33</th>\n",
+       "      <td>0.905440</td>\n",
+       "      <td>0.819821</td>\n",
+       "      <td>1.198062</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.080836</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>34</th>\n",
+       "      <td>0.823958</td>\n",
+       "      <td>0.678906</td>\n",
+       "      <td>1.383075</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081202</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>35</th>\n",
+       "      <td>1.786714</td>\n",
+       "      <td>3.192348</td>\n",
+       "      <td>0.045491</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081831</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>36</th>\n",
+       "      <td>0.089389</td>\n",
+       "      <td>0.007990</td>\n",
+       "      <td>3.650434</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.090306</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>37</th>\n",
+       "      <td>0.251777</td>\n",
+       "      <td>0.063392</td>\n",
+       "      <td>3.056282</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.079052</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>38</th>\n",
+       "      <td>1.122716</td>\n",
+       "      <td>1.260492</td>\n",
+       "      <td>0.769627</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.079758</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>39</th>\n",
+       "      <td>1.746009</td>\n",
+       "      <td>3.048549</td>\n",
+       "      <td>0.064511</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081237</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>40</th>\n",
+       "      <td>1.159995</td>\n",
+       "      <td>1.345588</td>\n",
+       "      <td>0.705608</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.075668</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>41</th>\n",
+       "      <td>1.664966</td>\n",
+       "      <td>2.772110</td>\n",
+       "      <td>0.112248</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.082456</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>42</th>\n",
+       "      <td>0.293639</td>\n",
+       "      <td>0.086224</td>\n",
+       "      <td>2.911669</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.083393</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>43</th>\n",
+       "      <td>0.335041</td>\n",
+       "      <td>0.112252</td>\n",
+       "      <td>2.772089</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.082827</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>44</th>\n",
+       "      <td>0.865785</td>\n",
+       "      <td>0.749583</td>\n",
+       "      <td>1.286444</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081619</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>45</th>\n",
+       "      <td>0.626347</td>\n",
+       "      <td>0.392310</td>\n",
+       "      <td>1.886924</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.083406</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46</th>\n",
+       "      <td>0.135509</td>\n",
+       "      <td>0.018363</td>\n",
+       "      <td>3.476325</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.084420</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47</th>\n",
+       "      <td>1.705630</td>\n",
+       "      <td>2.909175</td>\n",
+       "      <td>0.086653</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.081196</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>48</th>\n",
+       "      <td>0.173681</td>\n",
+       "      <td>0.030165</td>\n",
+       "      <td>3.335441</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.079321</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>49</th>\n",
+       "      <td>0.045340</td>\n",
+       "      <td>0.002056</td>\n",
+       "      <td>3.820697</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.088030</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "        gene        f1        f2  rank  distance\n",
+       "0   1.313075  1.724166  0.471866     1  0.079197\n",
+       "1   0.376336  0.141629  2.636285     1  0.082190\n",
+       "2   0.946484  0.895831  1.109897     1  0.086698\n",
+       "3   1.352775  1.830000  0.418900     1  0.080070\n",
+       "4   1.623328  2.635193  0.141882     1  0.077820\n",
+       "5   1.998065  3.992265  0.000004     1       inf\n",
+       "6   1.273719  1.622360  0.527484     1  0.078063\n",
+       "7   1.827686  3.340437  0.029692     1  0.082174\n",
+       "8   0.991991  0.984045  1.016083     1  0.090661\n",
+       "9   0.417102  0.173974  2.505567     1  0.081382\n",
+       "10  1.587290  2.519488  0.170330     1  0.072845\n",
+       "11  1.868733  3.492162  0.017231     1  0.084057\n",
+       "12  0.708000  0.501264  1.669263     1  0.078893\n",
+       "13  1.953805  3.817354  0.002134     1  0.086649\n",
+       "14  1.550617  2.404412  0.201945     1  0.075431\n",
+       "15  0.541827  0.293577  2.126267     1  0.086206\n",
+       "16  0.214709  0.046100  3.187264     1  0.078217\n",
+       "17  1.235150  1.525596  0.584995     1  0.075601\n",
+       "18  1.080376  1.167212  0.845708     1  0.085875\n",
+       "19  1.511997  2.286135  0.238147     1  0.078118\n",
+       "20  0.746483  0.557238  1.571304     1  0.076846\n",
+       "21  0.498383  0.248386  2.254852     1  0.084373\n",
+       "22  1.036989  1.075347  0.927390     1  0.088538\n",
+       "23  0.457589  0.209388  2.379031     1  0.081411\n",
+       "24  0.667720  0.445851  1.774969     1  0.081788\n",
+       "25  1.433681  2.055441  0.320717     1  0.079784\n",
+       "26  0.584450  0.341582  2.003781     1  0.084656\n",
+       "27  0.784719  0.615784  1.476908     1  0.077604\n",
+       "28  0.001491  0.000002  3.994037     1       inf\n",
+       "29  1.911583  3.654149  0.007818     1  0.085236\n",
+       "30  1.472641  2.168673  0.278107     1  0.078458\n",
+       "31  1.393002  1.940454  0.368447     1  0.081052\n",
+       "32  1.198252  1.435808  0.642800     1  0.075287\n",
+       "33  0.905440  0.819821  1.198062     1  0.080836\n",
+       "34  0.823958  0.678906  1.383075     1  0.081202\n",
+       "35  1.786714  3.192348  0.045491     1  0.081831\n",
+       "36  0.089389  0.007990  3.650434     1  0.090306\n",
+       "37  0.251777  0.063392  3.056282     1  0.079052\n",
+       "38  1.122716  1.260492  0.769627     1  0.079758\n",
+       "39  1.746009  3.048549  0.064511     1  0.081237\n",
+       "40  1.159995  1.345588  0.705608     1  0.075668\n",
+       "41  1.664966  2.772110  0.112248     1  0.082456\n",
+       "42  0.293639  0.086224  2.911669     1  0.083393\n",
+       "43  0.335041  0.112252  2.772089     1  0.082827\n",
+       "44  0.865785  0.749583  1.286444     1  0.081619\n",
+       "45  0.626347  0.392310  1.886924     1  0.083406\n",
+       "46  0.135509  0.018363  3.476325     1  0.084420\n",
+       "47  1.705630  2.909175  0.086653     1  0.081196\n",
+       "48  0.173681  0.030165  3.335441     1  0.079321\n",
+       "49  0.045340  0.002056  3.820697     1  0.088030"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "df = pd.DataFrame([(x.genome[0], x.fitness[0], x.fitness[1], x.rank, x.distance) for x in final_pop])\n",
+    "df.columns = ['gene','f1','f2','rank','distance']\n",
+    "df"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<Axes: xlabel='f1', ylabel='f2'>"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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775b1dXVlwUbyaRnGLOpyjZUWlpqOJ1Oo6SkxPjwww+N+++/37j66quNpqYmwzAM40c/+pHxyCOP+MsfOnTIGDx4sPGrX/3K+OMf/2g89thjxpAhQ4yamhqzmtCrcNv3+OOPG/v27TM++eQT4/jx48bf/d3fGcnJycYHH3xgVhN6de7cOaOqqsqoqqoyJBm//vWvjaqqKuPUqVOGYRjGI488YvzoRz/yl6+vrzeGDRtm/OxnPzP++Mc/GuvXrzeSkpKMt956y6wm9CncNj7zzDPGrl27jI8//tioqakxVq5caQwaNMj4/e9/b1YTevXggw8abrfbOHjwoNHY2Oh/nT9/3l/Gyn+H/Wmf1f4OH3nkEaO8vNw4efKk8Yc//MF45JFHDIfDYbz99tuGYVj7+RlG+O2z2vPrSffVUonwDAk3YVq3bp1x3XXXGUOHDjVmzpxpvPfee/735s6dayxdujSg/GuvvWb8xV/8hTF06FDjxhtvNH73u9/FucbhCad9q1at8pdNT083fvCDHxgnTpwwodah6Vr23P3V1aalS5cac+fOveyeadOmGUOHDjVuuOEGY8uWLXGvdzjCbeO///u/G9/73veM5ORkY/jw4ca8efOMd955x5zKh6CntkkKeC5W/jvsT/us9nf4D//wD8bYsWONoUOHGiNGjDBuv/12/xe/YVj7+RlG+O2z2vPrSfdwkwjP0GEYhhG7fiEAAID4Ys4NAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAACwFcINAMsyDEP333+/hg8fLofDoerqarOrBCABEG4AWNZbb72lkpIS7dmzR42NjfL5fFq8eLFGjRolh8OhXbt2mV1FACYg3ACwrE8++UQjR45UXl6eMjIy1NbWpqlTp2r9+vVmVw2AiQabXQEA6I8f//jHevnllyVJDodDY8eO1aeffqqFCxeaXDMAZiPcALCk5557Tt/73ve0efNmHT16VElJSWZXCUCCINwAsCS3262UlBQlJSUpIyPD7OoASCDMuQEAALZCuAEAALZCuAEAALbCnBsAttHa2qq6ujr/zydPnlR1dbWGDx+u6667zsSaAYgnwg0A2zh27Jjmz5/v/3n16tWSpKVLl6qkpMSkWgGIN4dhGIbZlQAAAIgW5twAAABbIdwAAABbIdwAAABbIdwAAABbIdwAAABbIdwAAABbIdwAAABbIdwAAABbIdwAAABbIdwAAABbIdwAAABb+X/kS6lctbQ3LwAAAABJRU5ErkJggg==",
+      "text/plain": [
+       "<Figure size 640x480 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "df.plot(x='f1',y='f2', kind='scatter')"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "leap",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.3"
+  },
+  "orig_nbformat": 4
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/leap_ec/multiobjective/asynchronous.py b/leap_ec/multiobjective/asynchronous.py
new file mode 100644
index 0000000..1c06714
--- /dev/null
+++ b/leap_ec/multiobjective/asynchronous.py
@@ -0,0 +1,195 @@
+import numpy as np
+from leap_ec import ops, util
+from leap_ec.global_vars import context
+from leap_ec.individual import Individual
+from leap_ec.multiobjective.problems import MultiObjectiveProblem
+from leap_ec.multiobjective.ops import fast_nondominated_sort, per_rank_crowding_calc
+from leap_ec.global_vars import context
+from leap_ec.distrib.asynchronous import steady_state
+
+
+def _find_start_layer(ind, layer_pops):
+    """
+    Finds the highest layer in which ind is not dominated.
+    """
+    lo = 0
+    hi = len(layer_pops) - 1
+    
+    while lo <= hi:
+        mid = (lo + hi) // 2
+        if not any(i > ind for i in layer_pops[mid]):
+            hi = mid - 1
+        else:
+            lo = mid + 1
+    
+    return lo
+
+def _split_dominated(moving_points, layer):
+    """
+    Splits layer into populations of points that aren't / are dominated by the
+    zenith of moving_points. We use a maximization rather than minimization
+    objective versus ENLU, so this is zenith / max vs nadir / min
+    """
+    zenith = np.max([
+        ind.fitness * ind.problem.maximize
+        for ind in moving_points
+    ], axis=0)
+    
+    dominated = []
+    nondominated = []
+    for ind in layer:
+        ord_fitness = ind.fitness * ind.problem.maximize
+        if all(zenith >= ord_fitness) and any(zenith > ord_fitness):
+            dominated.append(ind)
+        else:
+            nondominated.append(ind)
+    
+    return nondominated, dominated
+
+def _set_domination(pop_a, pop_b):
+    """
+    Checks for each individual in pop_b whether it was dominated by an individual in pop_a.
+    """
+    return [
+        any(li > ri for li in pop_a)
+        for ri in pop_b
+    ]
+    
+def enlu_inds_rank(start_point, layer_pops):
+    """ Performs the incremental non-dominated sorting ranking process.
+    
+    Based on the ENLU insertion algorithm with the modification of a binary search for the start point.
+    Locates the highest layer where the individual is nondominated and inserts it, propagating
+    layer composition changes down the rankings.
+    
+        - K. Li, K. Deb, Q. Zhang and Q. Zhang, "Efficient Nondomination Level Update Method for
+            Steady-State Evolutionary Multiobjective Optimization," in IEEE Transactions on
+            Cybernetics, vol. 47, no. 9, pp. 2838-2849, Sept. 2017, doi: 10.1109/TCYB.2016.2621008.
+    
+    :param moving points: the set of points descending in rank from the previous layer.
+        In the first recursion this is the inserted individual.
+    :param layer_pops: the population separated into non-dominating layers.
+    :param rank_func: the ranking function used to separate out the dominated group
+        at each recursion.
+    :param depth: the current layer depth the moving points set is dominating.
+    """
+    
+    # CASE I: Find the first layer where start_point is not dominated
+    depth = _find_start_layer(start_point, layer_pops)
+    moving_points = [start_point]
+    
+    while depth < len(layer_pops):
+        # CASE II: If the last layer merged perfectly, done
+        if not moving_points:
+            return
+        
+        # Find the individuals who are dominated by the zenith
+        nondominated, dominated = _split_dominated(moving_points, layer_pops[depth])
+        # Further check if those individuals are properly dominated
+        true_dominated = _set_domination(moving_points, dominated)
+
+        # CASE III: If nondominated is empty, insert moving points as a layer and re-update all ranks
+        if not nondominated:
+            layer_pops.insert(depth, moving_points)
+            for i, lp in enumerate(layer_pops[depth:]):
+                for ind in lp:
+                    ind.rank = depth + i + 1
+            return
+        
+        # CASE IV: Some points are dominated, propagate those onwards
+        # The moving points stay in this layer, while those not dominated by the zenith
+        # remain unchanged
+        layer_pops[depth] = nondominated + moving_points
+        moving_points = []
+        
+        # The truly dominated go to the next layer down, while the rest are added to
+        # the current layer
+        for ind, dom in zip(dominated, true_dominated):
+            if dom:
+                moving_points.append(ind)
+            else:
+                layer_pops[depth].append(ind)
+                
+        for ind in layer_pops[depth]:
+            ind.rank = depth + 1
+        depth += 1
+    
+    # If any points make it all the way through, they form a new layer
+    if moving_points:
+        for ind in moving_points:
+            ind.rank = len(layer_pops) + 1
+        layer_pops.append(moving_points)
+            
+def steady_state_nsga_2(
+            client, max_births: int, init_pop_size: int, pop_size: int,
+            problem: MultiObjectiveProblem,
+            representation,
+            offspring_pipeline,
+            count_nonviable=False,
+            evaluated_probe=None,
+            pop_probe=None,
+            context=context
+        ):
+    """ A steady state version of the NSGA-II multi-objective evolutionary algorithm.
+    
+        - K. Li, K. Deb, Q. Zhang and Q. Zhang, "Efficient Nondomination Level Update Method for
+            Steady-State Evolutionary Multiobjective Optimization," in IEEE Transactions on
+            Cybernetics, vol. 47, no. 9, pp. 2838-2849, Sept. 2017, doi: 10.1109/TCYB.2016.2621008.
+
+    :param client: Dask client that should already be set-up
+    :param max_births: how many births are we allowing?
+    :param init_pop_size: size of initial population sent directly to workers
+           at start
+    :param pop_size: how large should the population be?
+    :param representation: of the individuals
+    :param problem: to be solved
+    :param offspring_pipeline: for creating new offspring from the pop
+    :param inserter: function with signature (new_individual, pop, popsize)
+           used to insert newly evaluated individuals into the population;
+           defaults to greedy_insert_into_pop()
+    :param count_nonviable: True if we want to count non-viable individuals
+           towards the birth budget
+    :param evaluated_probe: is a function taking an individual that is given
+           the next evaluated individual; can be used to print newly evaluated
+           individuals
+    :param pop_probe: is an optional function that writes a snapshot of the
+           population to a CSV formatted stream ever N births
+    :return: the population containing the final individuals
+    """
+
+    # This holds the separated layers, so we don't have to rebuild it each time
+    layer_pops = []
+        
+    def inds_inserter(ind, flat_pop, pop_size):
+        # The bulk of the logic for this insertion happens in inds_rank
+        nonlocal layer_pops
+        enlu_inds_rank(ind, layer_pops)
+        
+        # Rank the layers
+        for lp in layer_pops:
+            per_rank_crowding_calc(lp, lp[0].problem.maximize)
+        
+        # If the population is too big, drop the most crowded
+        if sum(len(lp) for lp in layer_pops) > pop_size:
+            rem_idx = min(range(len(layer_pops[-1])), key=lambda i: layer_pops[-1][i].distance)
+            layer_pops[-1].pop(rem_idx)
+            
+            if layer_pops[-1]:
+                # Since this layer is losing a member, needs recalculation of crowding
+                per_rank_crowding_calc(layer_pops[-1], layer_pops[-1][0].problem.maximize)
+            else:
+                del layer_pops[-1]
+        
+        # Calculate the rankings of each layer and reconstruct flat_pop
+        flat_pop.clear()
+        for lp in layer_pops:
+            flat_pop.extend(lp)
+    
+    # This is functionally just a wrapper around steady state, all of the logic is the same
+    # with the exception of a special population structure and inserter
+    return steady_state(
+            client, max_births, init_pop_size, pop_size,
+            representation, problem, offspring_pipeline,
+            inds_inserter, count_nonviable, evaluated_probe,
+            pop_probe, context
+        )
diff --git a/leap_ec/multiobjective/ops.py b/leap_ec/multiobjective/ops.py
index a0fbabb..f95a3ba 100644
--- a/leap_ec/multiobjective/ops.py
+++ b/leap_ec/multiobjective/ops.py
@@ -163,13 +163,84 @@ def rank_ordinal_sort(population: list, parents: list = None) -> list:
 
     # Use the inverse idx from deduplicating fitnesses to assign rankings
     for ind, rank in zip(population, ranks[orig_inv_idx]):
-        ind.rank = rank
+        ind.rank = int(rank)
 
     return population
 
 ##############################
 # crowding_distance_calc operator
 ##############################
+def per_rank_crowding_calc(ranked_population: list, is_maximizing) -> list:
+    """ Calculate crowding distance within rank
+    :param ranked_population: A population of entirely one rank
+    :returns: population with crowding distance calculate for one rank
+    """
+    # Presuming this is a population with homogeneous objectives, then the size of
+    # the optimization directions array should be equal to the number of objectives.
+    num_objectives = is_maximizing.shape[0]
+    
+    # minimum and maximum fitnesses by objective, so we initialize to the
+    # infinities. At first we assume maximization for all of the objectives,
+    # but then we fine-tune for minimization in the next step.
+    f_min = np.full(num_objectives, np.inf)
+    f_max = np.full(num_objectives, np.NINF)
+
+    for objective in range(num_objectives):
+        if is_maximizing[objective] == -1:
+            f_min[objective] = np.NINF
+            f_max[objective] = np.inf
+
+    # Find ranges of fitness per objective
+    for i in ranked_population:
+        i.distance = 0  # init distances to zero to start
+        for objective in range(num_objectives):  # update fitness ranges
+            if is_maximizing[objective] == -1:
+                # We are *maximizing* for this specific objective
+                f_min[objective] = max(f_min[objective],
+                                        i.fitness[objective])
+                f_max[objective] = min(f_max[objective],
+                                        i.fitness[objective])
+            else:
+                # We are *minimizing* for this specific objective
+                f_min[objective] = min(f_min[objective],
+                                        i.fitness[objective])
+                f_max[objective] = max(f_max[objective],
+                                        i.fitness[objective])
+
+    objective_ranges = f_max - f_min
+
+    sorted_pop = []
+
+    for objective in range(num_objectives):
+        if objective_ranges[objective] == 0:
+            continue
+        
+        # sort by objective being mindful that maximization vs. minimization may
+        # be different for each objective
+        if is_maximizing[objective] == -1:
+            # If we're maximizing in ascending order, that actually means we
+            # want descending order since the larger values are fitter.
+            sorted_pop = sorted(ranked_population,
+                                key=lambda ind: - ind.fitness[objective])
+        else:
+            sorted_pop = sorted(ranked_population,
+                                key=lambda ind: ind.fitness[objective])
+
+        # set first and last elements to infinity
+        sorted_pop[0].distance = sorted_pop[-1].distance = inf
+
+        # update the distance per individuals with a sliding window of
+        # three fitnesses for the current objective starting from the second to
+        # the second to last individual's
+        for i in range(1, len(sorted_pop) - 1):
+            sorted_pop[i].distance = sorted_pop[i].distance + \
+                                        (sorted_pop[i + 1].fitness[objective] -
+                                        sorted_pop[i - 1].fitness[
+                                            objective]) / objective_ranges[
+                                            objective]
+
+    return sorted_pop
+
 @curry
 @listlist_op
 def crowding_distance_calc(population: list) -> list:
@@ -185,78 +256,10 @@ def crowding_distance_calc(population: list) -> list:
     :param population: population to calculate crowding distances
     :returns: individuals with crowding distance calculated
     """
-    def per_rank_calc(ranked_population: list) -> list:
-        """ Calculate crowding distance within rank
-        :param ranked_population: A population of entirely one rank
-        :returns: population with crowding distance calculate for one rank
-        """
-        # minimum and maximum fitnesses by objective, so we initialize to the
-        # infinities. At first we assume maximization for all of the objectives,
-        # but then we fine-tune for minimization in the next step.
-        f_min = np.full(num_objectives, np.inf)
-        f_max = np.full(num_objectives, np.NINF)
-
-        for objective in range(num_objectives):
-            if is_maximizing[objective] == -1:
-                f_min[objective] = np.NINF
-                f_max[objective] = np.inf
-
-        # Find ranges of fitness per objective
-        for i in ranked_population:
-            i.distance = 0  # init distances to zero to start
-            for objective in range(num_objectives):  # update fitness ranges
-                if is_maximizing[objective] == -1:
-                    # We are *maximizing* for this specific objective
-                    f_min[objective] = max(f_min[objective],
-                                           i.fitness[objective])
-                    f_max[objective] = min(f_max[objective],
-                                           i.fitness[objective])
-                else:
-                    # We are *minimizing* for this specific objective
-                    f_min[objective] = min(f_min[objective],
-                                           i.fitness[objective])
-                    f_max[objective] = max(f_max[objective],
-                                           i.fitness[objective])
-
-        objective_ranges = f_max - f_min
-
-        sorted_pop = []
-
-        for objective in range(num_objectives):
-            # sort by objective being mindful that maximization vs. minimization may
-            # be different for each objective
-            if is_maximizing[objective] == -1:
-                # If we're maximizing in ascending order, that actually means we
-                # want descending order since the larger values are fitter.
-                sorted_pop = sorted(ranked_population,
-                                    key=lambda ind: - ind.fitness[objective])
-            else:
-                sorted_pop = sorted(ranked_population,
-                                    key=lambda ind: ind.fitness[objective])
-
-            # set first and last elements to infinity
-            sorted_pop[0].distance = sorted_pop[-1].distance = inf
-
-            # update the distance per individuals with a sliding window of
-            # three fitnesses for the current objective starting from the second to
-            # the second to last individual's
-            for i in range(1, len(sorted_pop) - 1):
-                sorted_pop[i].distance = sorted_pop[i].distance + \
-                                         (sorted_pop[i + 1].fitness[objective] -
-                                          sorted_pop[i - 1].fitness[
-                                              objective]) / objective_ranges[
-                                             objective]
-
-        return sorted_pop
 
     # Ensure that we're dealing with a multi-objective Problem.
     assert isinstance(population[0].problem, MultiObjectiveProblem)
 
-    # Presuming this is a population with homogeneous objectives, then we can
-    # arbitrarily peep at the first individual's fitness values to determine
-    # how many objectives we have.
-    num_objectives = population[0].fitness.shape[0]
-
     # Check if we're maximizing or minimizing; we arbitrarily check the first
     # individual.
     # TODO We *might* have to check on a case by case basis if we have a weird
@@ -274,6 +277,6 @@ def crowding_distance_calc(population: list) -> list:
     # all the sub-populations by rank.
     all_crowd_dist_pop = []
     for rank in pop_by_ranks.keys():
-        all_crowd_dist_pop += per_rank_calc(pop_by_ranks[rank])
+        all_crowd_dist_pop += per_rank_crowding_calc(pop_by_ranks[rank], is_maximizing)
 
     return all_crowd_dist_pop
diff --git a/leap_ec/multiobjective/problems.py b/leap_ec/multiobjective/problems.py
index 02af30e..20ccbe6 100644
--- a/leap_ec/multiobjective/problems.py
+++ b/leap_ec/multiobjective/problems.py
@@ -85,10 +85,10 @@ class MultiObjectiveProblem(Problem):
         :param second_fitnesses: same as `first_fitnesses`, but for a different
             individual
         """
-        assert(first_fitnesses is not None)
-        assert(second_fitnesses is not None)
-        assert(len(first_fitnesses) == len(self.maximize))
-        assert(len(second_fitnesses) == len(self.maximize))
+        # assert(first_fitnesses is not None)
+        # assert(second_fitnesses is not None)
+        # assert(len(first_fitnesses) == len(self.maximize))
+        # assert(len(second_fitnesses) == len(self.maximize))
 
         # Negate the minimization problems, so we can treat all objectives as
         # maximization


Test patch:
diff --git a/tests/multiobjective/test_async.py b/tests/multiobjective/test_async.py
new file mode 100644
index 0000000..fccd0cf
--- /dev/null
+++ b/tests/multiobjective/test_async.py
@@ -0,0 +1,57 @@
+from leap_ec.multiobjective.asynchronous import enlu_inds_rank
+from leap_ec.multiobjective.ops import fast_nondominated_sort, \
+    crowding_distance_calc, rank_ordinal_sort
+from leap_ec.multiobjective.problems import SCHProblem
+from .test_ops import generate_test_pop
+import numpy as np
+
+from leap_ec.individual import Individual
+from leap_ec.representation import Representation
+from leap_ec.real_rep.initializers import create_real_vector
+import time
+import sys
+
+
+def test_inds_rank1():
+    """
+    Tests to see if inds_rank can properly rank the test pop
+    """
+    pop, ranks, _ = generate_test_pop()
+    
+    layer_pops = []
+    for ind in pop:
+        enlu_inds_rank(ind, layer_pops)
+    
+    np.testing.assert_array_equal(
+        [ind.rank for ind in pop],
+        ranks
+    )
+
+
+def test_inds_rank2():
+    """
+    Tests to see if inds_rank can properly rank larger populations with many ranks
+    """
+    try:
+        state = np.random.get_state()
+        np.random.seed(111)
+        
+        prob = SCHProblem()
+        rep = Representation(initialize=create_real_vector(bounds=[(-10, 10)]))
+        
+        pop = rep.create_population(1000, prob)
+        Individual.evaluate_population(pop)
+        
+        layer_pops = []
+        for ind in pop:
+            enlu_inds_rank(ind, layer_pops)
+        
+        ranks_1 = np.array([ind.rank for ind in pop])
+        
+        fast_nondominated_sort(pop)
+        ranks_2 = np.array([ind.rank for ind in pop])
+        
+        np.testing.assert_array_equal(ranks_1, ranks_2)
+        
+    finally:
+        np.random.set_state(state)
\ No newline at end of file
diff --git a/tests/multiobjective/test_ops.py b/tests/multiobjective/test_ops.py
index 431aa87..981be00 100644
--- a/tests/multiobjective/test_ops.py
+++ b/tests/multiobjective/test_ops.py
@@ -48,15 +48,32 @@ def generate_test_pop():
         1, # [4, 0]
         1  # [4, 0] Note: Duplicated fitness
     ]
+    
+    # 1-1 argsort: 2, 3, 4, 5  [0, 4]
+    # 1-2 argsort: 4, 5, 3, 2  [0, 4]
+    
+    # 2-1 argsort: 1  [1, 1]
+    # 2-2 argsort: 1  [9, 9]
+    
+    # 3-1 argsort: 0  [4, 4]
+    # 3-2 argsort: 0  [16, 16]
+    
+    distances = [
+        np.inf, # Edge
+        np.inf, # Edge
+        np.inf, # Edge
+        (4 - 0) / 4 + (4 - 0) / 4,
+        np.inf, # Edge
+        np.inf  # Edge
+    ]
 
-    return pop, ranks
+    return pop, ranks, distances
 
 
 def test_fast_nondominated_sort():
     """ Test for non-dominated sorting """
-    pop, ranks = generate_test_pop()
-
-    sorted_pop = fast_nondominated_sort(pop)
+    pop, ranks, _ = generate_test_pop()
+    fast_nondominated_sort(pop)
 
     np.testing.assert_array_equal(
         [ind.rank for ind in pop],
@@ -65,9 +82,8 @@ def test_fast_nondominated_sort():
 
 def test_rank_ordinal_sort():
     """ Test for rank ordinal sorting """
-    pop, ranks = generate_test_pop()
-
-    sorted_pop = rank_ordinal_sort(pop)
+    pop, ranks, _ = generate_test_pop()
+    rank_ordinal_sort(pop)
 
     np.testing.assert_array_equal(
         [ind.rank for ind in pop],
@@ -76,20 +92,19 @@ def test_rank_ordinal_sort():
 
 def test_crowding_distance_calc():
     """ Test of crowding distance calculation """
-    pop, _ = generate_test_pop()
-
-    sorted_pop = fast_nondominated_sort(pop)
-
-    sorted_pop = crowding_distance_calc(sorted_pop)
-
-    # TODO add manual checks to verify correct calculation for crowding
-    # distance.
-    pass
+    pop, _, distances = generate_test_pop()
+    fast_nondominated_sort(pop)
+    crowding_distance_calc(pop)
+    
+    np.testing.assert_array_equal(
+        [ind.distance for ind in pop],
+        distances
+    )
 
 
 def test_sorting_criteria():
     """ Test sorting by rank and distance criteria """
-    pop, _ = generate_test_pop()
+    pop, _, _ = generate_test_pop()
     processed_pop = pipe(pop,
                fast_nondominated_sort,
                crowding_distance_calc)