diff --git a/.gitignore b/.gitignore index c6c093e6..f0ee47d9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ dist/ env/ doc/html data/ +galaxy/test_galaxy_state.json /static/ sith/settings_custom.py sith/search_indexes/ diff --git a/com/tests.py b/com/tests.py index 2a5206b2..6dde46db 100644 --- a/com/tests.py +++ b/com/tests.py @@ -159,6 +159,33 @@ class NewsTest(TestCase): self.assertFalse(self.new.is_owned_by(self.anonymous)) self.assertFalse(self.new.is_owned_by(self.sli)) + def test_news_viewer(self): + """ + Test that moderated news can be viewed by anyone + and not moderated news only by com admins + """ + # by default a news isn't moderated + self.assertTrue(self.new.can_be_viewed_by(self.com_admin)) + self.assertFalse(self.new.can_be_viewed_by(self.sli)) + self.assertFalse(self.new.can_be_viewed_by(self.anonymous)) + self.assertFalse(self.new.can_be_viewed_by(self.author)) + + self.new.is_moderated = True + self.new.save() + self.assertTrue(self.new.can_be_viewed_by(self.com_admin)) + self.assertTrue(self.new.can_be_viewed_by(self.sli)) + self.assertTrue(self.new.can_be_viewed_by(self.anonymous)) + self.assertTrue(self.new.can_be_viewed_by(self.author)) + + def test_news_editor(self): + """ + Test that only com admins can edit news + """ + self.assertTrue(self.new.can_be_edited_by(self.com_admin)) + self.assertFalse(self.new.can_be_edited_by(self.sli)) + self.assertFalse(self.new.can_be_edited_by(self.anonymous)) + self.assertFalse(self.new.can_be_edited_by(self.author)) + class WeekmailArticleTest(TestCase): @classmethod diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index d89ff1e2..c78de401 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -208,6 +208,8 @@ Welcome to the wiki page! # Here we add a lot of test datas, that are not necessary for the Sith, but that provide a basic development environment if not options["prod"]: + self.now = timezone.now().replace(hour=12) + # Adding user Skia skia = User( username="skia", @@ -914,6 +916,7 @@ Welcome to the wiki page! Membership( user=comunity, club=bar_club, + start_date=self.now, role=settings.SITH_CLUB_ROLES_ID["Board member"], ).save() # Adding user tutu @@ -1072,7 +1075,7 @@ Welcome to the wiki page! ForumTopic(forum=hall) # News - friday = timezone.now() + friday = self.now while friday.weekday() != 4: friday += timedelta(hours=6) friday.replace(hour=20, minute=0, second=0) @@ -1090,8 +1093,8 @@ Welcome to the wiki page! n.save() NewsDate( news=n, - start_date=timezone.now() + timedelta(hours=70), - end_date=timezone.now() + timedelta(hours=72), + start_date=self.now + timedelta(hours=70), + end_date=self.now + timedelta(hours=72), ).save() n = News( title="Repas barman", @@ -1107,8 +1110,8 @@ Welcome to the wiki page! n.save() NewsDate( news=n, - start_date=timezone.now() + timedelta(hours=72), - end_date=timezone.now() + timedelta(hours=84), + start_date=self.now + timedelta(hours=72), + end_date=self.now + timedelta(hours=84), ).save() n = News( title="Repas fromager", @@ -1123,8 +1126,8 @@ Welcome to the wiki page! n.save() NewsDate( news=n, - start_date=timezone.now() + timedelta(hours=96), - end_date=timezone.now() + timedelta(hours=100), + start_date=self.now + timedelta(hours=96), + end_date=self.now + timedelta(hours=100), ).save() n = News( title="SdF", @@ -1140,7 +1143,7 @@ Welcome to the wiki page! NewsDate( news=n, start_date=friday + timedelta(hours=24 * 7 + 1), - end_date=timezone.now() + timedelta(hours=24 * 7 + 9), + end_date=self.now + timedelta(hours=24 * 7 + 9), ).save() # Weekly n = News( @@ -1271,28 +1274,28 @@ Welcome to the wiki page! club=troll, role=9, description="Padawan Troll", - start_date=timezone.now() - timedelta(days=17), + start_date=self.now - timedelta(days=17), ).save() Membership( user=krophil, club=troll, role=10, description="Maitre Troll", - start_date=timezone.now() - timedelta(days=200), + start_date=self.now - timedelta(days=200), ).save() Membership( user=skia, club=troll, role=2, description="Grand Ancien Troll", - start_date=timezone.now() - timedelta(days=400), - end_date=timezone.now() - timedelta(days=86), + start_date=self.now - timedelta(days=400), + end_date=self.now - timedelta(days=86), ).save() Membership( user=richard, club=troll, role=2, description="", - start_date=timezone.now() - timedelta(days=200), - end_date=timezone.now() - timedelta(days=100), + start_date=self.now - timedelta(days=200), + end_date=self.now - timedelta(days=100), ).save() diff --git a/core/models.py b/core/models.py index fb28faa6..c8a38426 100644 --- a/core/models.py +++ b/core/models.py @@ -810,6 +810,10 @@ class AnonymousUser(AuthAnonymousUser): def can_edit(self, obj): return False + @property + def is_com_admin(self): + return False + def can_view(self, obj): if ( hasattr(obj, "view_groups") diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 7abfa8ca..9ba72e7d 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -1031,7 +1031,7 @@ thead { } tbody > tr { - &:nth-child(even) { + &:nth-child(even):not(.highlight) { background: $primary-neutral-light-color; } &.clickable:hover { diff --git a/core/tests.py b/core/tests.py index a2e9c526..61d497a5 100644 --- a/core/tests.py +++ b/core/tests.py @@ -15,17 +15,18 @@ # import os -from datetime import timedelta +from datetime import date, timedelta +import freezegun from django.core.cache import cache from django.test import Client, TestCase from django.urls import reverse -from django.core.management import call_command from django.utils.timezone import now from club.models import Membership -from core.models import User, Group, Page, AnonymousUser from core.markdown import markdown +from core.models import AnonymousUser, Group, Page, User +from core.utils import get_semester_code, get_start_of_semester from sith import settings """ @@ -617,3 +618,75 @@ class UserIsInGroupTest(TestCase): returns False """ self.assertFalse(self.skia.is_in_group(name="This doesn't exist")) + + +class DateUtilsTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.autumn_month = settings.SITH_SEMESTER_START_AUTUMN[0] + cls.autumn_day = settings.SITH_SEMESTER_START_AUTUMN[1] + cls.spring_month = settings.SITH_SEMESTER_START_SPRING[0] + cls.spring_day = settings.SITH_SEMESTER_START_SPRING[1] + + cls.autumn_semester_january = date(2025, 1, 4) + cls.autumn_semester_september = date(2024, 9, 4) + cls.autumn_first_day = date(2024, cls.autumn_month, cls.autumn_day) + + cls.spring_semester_march = date(2023, 3, 4) + cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day) + + def test_get_semester(self): + """ + Test that the get_semester function returns the correct semester string + """ + self.assertEqual(get_semester_code(self.autumn_semester_january), "A24") + self.assertEqual(get_semester_code(self.autumn_semester_september), "A24") + self.assertEqual(get_semester_code(self.autumn_first_day), "A24") + + self.assertEqual(get_semester_code(self.spring_semester_march), "P23") + self.assertEqual(get_semester_code(self.spring_first_day), "P23") + + def test_get_start_of_semester_fixed_date(self): + """ + Test that the get_start_of_semester correctly the starting date of the semester. + """ + automn_2024 = date(2024, self.autumn_month, self.autumn_day) + self.assertEqual( + get_start_of_semester(self.autumn_semester_january), automn_2024 + ) + self.assertEqual( + get_start_of_semester(self.autumn_semester_september), automn_2024 + ) + self.assertEqual(get_start_of_semester(self.autumn_first_day), automn_2024) + + spring_2023 = date(2023, self.spring_month, self.spring_day) + self.assertEqual(get_start_of_semester(self.spring_semester_march), spring_2023) + self.assertEqual(get_start_of_semester(self.spring_first_day), spring_2023) + + def test_get_start_of_semester_today(self): + """ + Test that the get_start_of_semester returns the start of the current semester + when no date is given + """ + with freezegun.freeze_time(self.autumn_semester_september): + self.assertEqual(get_start_of_semester(), self.autumn_first_day) + + with freezegun.freeze_time(self.spring_semester_march): + self.assertEqual(get_start_of_semester(), self.spring_first_day) + + def test_get_start_of_semester_changing_date(self): + """ + Test that the get_start_of_semester correctly gives the starting date of the semester, + even when the semester changes while the server isn't restarted. + """ + spring_2023 = date(2023, self.spring_month, self.spring_day) + autumn_2023 = date(2023, self.autumn_month, self.autumn_day) + mid_spring = spring_2023 + timedelta(days=45) + mid_autumn = autumn_2023 + timedelta(days=45) + + with freezegun.freeze_time(mid_spring) as frozen_time: + self.assertEqual(get_start_of_semester(), spring_2023) + + # forward time to the middle of the next semester + frozen_time.move_to(mid_autumn) + self.assertEqual(get_start_of_semester(), autumn_2023) diff --git a/core/utils.py b/core/utils.py index a053e2d5..d30e3ebf 100644 --- a/core/utils.py +++ b/core/utils.py @@ -15,20 +15,19 @@ # import os -import subprocess import re - -# Image utils - -from io import BytesIO +import subprocess from datetime import date -from PIL import ExifTags +# Image utils +from io import BytesIO +from typing import Optional import PIL - from django.conf import settings from django.core.files.base import ContentFile +from PIL import ExifTags +from django.utils import timezone def get_git_revision_short_hash() -> str: @@ -44,34 +43,54 @@ def get_git_revision_short_hash() -> str: return "" -def get_start_of_semester(d=date.today()): +def get_start_of_semester(today: Optional[date] = None) -> date: """ - This function computes the start date of the semester with respect to the given date (default is today), - and the start date given in settings.SITH_START_DATE. - It takes the nearest past start date. - Exemples: with SITH_START_DATE = (8, 15) - Today -> Start date - 2015-03-17 -> 2015-02-15 - 2015-01-11 -> 2014-08-15 + Return the date of the start of the semester of the given date. + If no date is given, return the start date of the current semester. + + The current semester is computed as follows: + + - If the date is between 15/08 and 31/12 => Autumn semester. + - If the date is between 01/01 and 15/02 => Autumn semester of the previous year. + - If the date is between 15/02 and 15/08 => Spring semester + + :param today: the date to use to compute the semester. If None, use today's date. + :return: the date of the start of the semester """ - today = d - year = today.year - start = date(year, settings.SITH_START_DATE[0], settings.SITH_START_DATE[1]) - start2 = start.replace(month=(start.month + 6) % 12) - spring, autumn = min(start, start2), max(start, start2) - if today > autumn: # autumn semester + if today is None: + today = timezone.now().date() + + autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN) + spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING) + + if today >= autumn: # between 15/08 (included) and 31/12 -> autumn semester return autumn - if today > spring: # spring semester + if today >= spring: # between 15/02 (included) and 15/08 -> spring semester return spring - return autumn.replace(year=year - 1) # autumn semester of last year + # between 01/01 and 15/02 -> autumn semester of the previous year + return autumn.replace(year=autumn.year - 1) -def get_semester(d=date.today()): +def get_semester_code(d: Optional[date] = None) -> str: + """ + Return the semester code of the given date. + If no date is given, return the semester code of the current semester. + + The semester code is an upper letter (A for autumn, P for spring), + followed by the last two digits of the year. + For example, the autumn semester of 2018 is "A18". + + :param d: the date to use to compute the semester. If None, use today's date. + :return: the semester code corresponding to the given date + """ + if d is None: + d = timezone.now().date() + start = get_start_of_semester(d) - if start.month <= 6: - return "P" + str(start.year)[-2:] - else: + + if (start.month, start.day) == settings.SITH_SEMESTER_START_AUTUMN: return "A" + str(start.year)[-2:] + return "P" + str(start.year)[-2:] def file_exist(path): diff --git a/counter/models.py b/counter/models.py index c6389349..476aaf13 100644 --- a/counter/models.py +++ b/counter/models.py @@ -15,7 +15,7 @@ # from __future__ import annotations -from typing import Tuple +from typing import Tuple, Optional from django.db import models from django.db.models import F, Value, Sum, QuerySet, OuterRef, Exists @@ -536,7 +536,7 @@ class Counter(models.Model): .order_by("-perm_sum") ) - def get_top_customers(self, since=get_start_of_semester()) -> QuerySet: + def get_top_customers(self, since: Optional[date] = None) -> QuerySet: """ Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent. @@ -546,6 +546,8 @@ class Counter(models.Model): - the nickname of the customer - the amount of money spent by the customer """ + if since is None: + since = get_start_of_semester() return ( self.sellings.filter(date__gte=since) .annotate( @@ -557,7 +559,8 @@ class Counter(models.Model): ) .annotate(nickname=F("customer__user__nick_name")) .annotate(promo=F("customer__user__promo")) - .values("customer__user", "name", "nickname") + .annotate(user=F("customer__user")) + .values("user", "promo", "name", "nickname") .annotate( selling_sum=Sum( F("unit_price") * F("quantity"), output_field=CurrencyField() @@ -567,15 +570,17 @@ class Counter(models.Model): .order_by("-selling_sum") ) - def get_total_sales(self, since=get_start_of_semester()) -> CurrencyField: + def get_total_sales(self, since=None) -> CurrencyField: """ Compute and return the total turnover of this counter since the date specified in parameter (by default, since the start of the current semester) :param since: timestamp from which to perform the calculation - :type since: datetime | date + :type since: datetime | date | None :return: Total revenue earned at this counter """ + if since is None: + since = get_start_of_semester() if isinstance(since, date): since = datetime.combine(since, datetime.min.time()) total = self.sellings.filter(date__gte=since).aggregate( diff --git a/counter/templates/counter/stats.jinja b/counter/templates/counter/stats.jinja index 03b7f4e0..d6cc14f0 100644 --- a/counter/templates/counter/stats.jinja +++ b/counter/templates/counter/stats.jinja @@ -11,7 +11,9 @@ {% block content %}

{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}

-

{% trans counter_name=counter.name %}Top 100 {{ counter_name }}{% endtrans %}

+

+ {% trans counter_name=counter.name %}Top 100 {{ counter_name }}{% endtrans %} ({{ current_semester }}) +

@@ -35,7 +37,9 @@
-

{% trans counter_name=counter.name %}Top 100 barman {{ counter_name }}{% endtrans %}

+

+ {% trans counter_name=counter.name %}Top 100 barman {{ counter_name }}{% endtrans %} ({{ current_semester }}) +

diff --git a/counter/tests.py b/counter/tests.py index ed83e935..6079099a 100644 --- a/counter/tests.py +++ b/counter/tests.py @@ -13,6 +13,7 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # +from datetime import date, timedelta import json import re import string @@ -322,42 +323,49 @@ class CounterStatsTest(TestCase): Test the result of Counter.get_top_customers() is correct """ top = iter(self.counter.get_top_customers()) - self.assertEqual( - next(top), + expected_results = [ { - "customer__user": self.sli.id, + "user": self.sli.id, "name": f"{self.sli.first_name} {self.sli.last_name}", + "promo": self.sli.promo, "nickname": self.sli.nick_name, "selling_sum": 2000, }, - ) - self.assertEqual( - next(top), { - "customer__user": self.skia.id, + "user": self.skia.id, "name": f"{self.skia.first_name} {self.skia.last_name}", + "promo": self.skia.promo, "nickname": self.skia.nick_name, "selling_sum": 1000, }, - ) - self.assertEqual( - next(top), { - "customer__user": self.krophil.id, + "user": self.krophil.id, "name": f"{self.krophil.first_name} {self.krophil.last_name}", + "promo": self.krophil.promo, "nickname": self.krophil.nick_name, "selling_sum": 100, }, - ) - self.assertEqual( - next(top), { - "customer__user": self.root.id, + "user": self.root.id, "name": f"{self.root.first_name} {self.root.last_name}", + "promo": self.root.promo, "nickname": self.root.nick_name, "selling_sum": 2, }, - ) + ] + + for result in expected_results: + self.assertEqual( + next(top), + { + "user": result["user"], + "name": result["name"], + "promo": result["promo"], + "nickname": result["nickname"], + "selling_sum": result["selling_sum"], + }, + ) + self.assertIsNone(next(top, None)) diff --git a/counter/views.py b/counter/views.py index 4d5af292..6bbc819d 100644 --- a/counter/views.py +++ b/counter/views.py @@ -48,7 +48,7 @@ import pytz from datetime import timedelta, datetime from http import HTTPStatus -from core.utils import get_start_of_semester +from core.utils import get_start_of_semester, get_semester_code from core.views import CanViewMixin, TabedViewMixin, CanEditMixin from core.views.forms import LoginForm from core.models import User @@ -1354,13 +1354,14 @@ class CounterStatView(DetailView, CounterAdminMixin): def get_context_data(self, **kwargs): """Add stats to the context""" - counter = self.object + counter: Counter = self.object semester_start = get_start_of_semester() office_hours = counter.get_top_barmen() kwargs = super(CounterStatView, self).get_context_data(**kwargs) kwargs.update( { "counter": counter, + "current_semester": get_semester_code(), "total_sellings": counter.get_total_sales(since=semester_start), "top_customers": counter.get_top_customers(since=semester_start)[:100], "top_barman": office_hours[:100], diff --git a/galaxy/management/commands/generate_galaxy_test_data.py b/galaxy/management/commands/generate_galaxy_test_data.py new file mode 100644 index 00000000..f6442487 --- /dev/null +++ b/galaxy/management/commands/generate_galaxy_test_data.py @@ -0,0 +1,411 @@ +# -*- coding:utf-8 -* +# +# Copyright 2023 +# - Skia +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# +import warnings +from typing import Final, Optional + +from django.conf import settings +from django.core.files.base import ContentFile +from django.core.management.base import BaseCommand +from django.utils import timezone + +from datetime import timedelta + +import logging + +from club.models import Club, Membership +from core.models import User, Group, Page, SithFile +from subscription.models import Subscription +from sas.models import Album, Picture, PeoplePictureRelation + + +RED_PIXEL_PNG: Final[bytes] = ( + b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" + b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53" + b"\xde\x00\x00\x00\x0c\x49\x44\x41\x54\x08\xd7\x63\xf8\xcf\xc0\x00" + b"\x00\x03\x01\x01\x00\x18\xdd\x8d\xb0\x00\x00\x00\x00\x49\x45\x4e" + b"\x44\xae\x42\x60\x82" +) + +USER_PACK_SIZE: Final[int] = 1000 + + +class Command(BaseCommand): + help = "Procedurally generate representative data for developing the Galaxy" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.now = timezone.now().replace(hour=12) + + self.users: Optional[list[User]] = None + self.clubs: Optional[list[Club]] = None + self.picts: Optional[list[Picture]] = None + self.pictures_tags: Optional[list[PeoplePictureRelation]] = None + + def add_arguments(self, parser): + parser.add_argument( + "--user-pack-count", + help=f"Number of packs of {USER_PACK_SIZE} users to create", + type=int, + default=1, + ) + parser.add_argument( + "--club-count", help="Number of clubs to create", type=int, default=50 + ) + + def handle(self, *args, **options): + self.logger = logging.getLogger("main") + if options["verbosity"] < 0 or 2 < options["verbosity"]: + warnings.warn("verbosity level should be between 0 and 2 included") + + if options["verbosity"] == 2: + self.logger.setLevel(logging.DEBUG) + elif options["verbosity"] == 1: + self.logger.setLevel(logging.INFO) + else: + self.logger.setLevel(logging.ERROR) + + self.logger.info("The Galaxy is being populated by the Sith.") + + self.logger.info("Cleaning old Galaxy population") + Club.objects.filter(unix_name__startswith="galaxy-").delete() + Group.objects.filter(name__startswith="galaxy-").delete() + Page.objects.filter(name__startswith="galaxy-").delete() + User.objects.filter(username__startswith="galaxy-").delete() + Picture.objects.filter(name__startswith="galaxy-").delete() + Album.objects.filter(name__startswith="galaxy-").delete() + self.logger.info("Done. Populating a new Galaxy") + + self.NB_USERS = options["user_pack_count"] * USER_PACK_SIZE + self.NB_CLUBS = options["club_count"] + + root = User.objects.filter(username="root").first() + sas = SithFile.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID) + self.galaxy_album = Album.objects.create( + name="galaxy-register-file", + owner=root, + is_moderated=True, + is_in_sas=True, + parent=sas, + ) + + self.make_clubs() + self.make_users() + self.make_families() + self.make_club_memberships() + self.make_pictures() + self.make_pictures_memberships() + half_pack = USER_PACK_SIZE // 2 + for u in range(half_pack, self.NB_USERS, half_pack): + self.make_important_citizen(u) + + def make_clubs(self): + """ + Create all the clubs (:class:`club.models.Club`) + and store them in `self.clubs` for fast access later. + Don't create the meta groups (:class:`core.models.MetaGroup`) + nor the pages of the clubs (:class:`core.models.Page`) + """ + self.clubs = [] + for i in range(self.NB_CLUBS): + self.clubs.append(Club(unix_name=f"galaxy-club-{i}", name=f"club-{i}")) + # We don't need to create corresponding groups here, as the Galaxy doesn't care about them + Club.objects.bulk_create(self.clubs) + self.clubs = Club.objects.filter(unix_name__startswith="galaxy-").all() + + def make_users(self): + """ + Create all the users and store them in `self.users` for fast access later. + + Also create a subscription for all the generated users. + """ + self.users = [] + for i in range(self.NB_USERS): + u = User( + username=f"galaxy-user-{i}", + email=f"{i}@galaxy.test", + first_name="Citizen", + last_name=f"n°{i}", + ) + self.logger.info(f"Creating {u}") + self.users.append(u) + User.objects.bulk_create(self.users) + self.users = User.objects.filter(username__startswith="galaxy-").all() + + # now that users are created, create their subscription + subs = [] + for i in range(self.NB_USERS): + u = self.users[i] + self.logger.info(f"Registering {u}") + subs.append( + Subscription( + member=u, + subscription_start=Subscription.compute_start( + self.now - timedelta(days=self.NB_USERS - i) + ), + subscription_end=Subscription.compute_end(duration=2), + ) + ) + Subscription.objects.bulk_create(subs) + + def make_families(self): + """ + Generate the godfather/godchild relations for the users contained in :attr:`self.users`. + + The :meth:`make_users` method must have been called beforehand. + + This will iterate on all citizen after the 200th. + Then it will take 14 other citizen among the previous 200 + (godfathers are usually older), and apply another + heuristic to determine whether they should have a family link + """ + if self.users is None: + raise RuntimeError( + "The `make_users()` method must be called before `make_families()`" + ) + for i in range(200, self.NB_USERS): + godfathers = [] + for j in range(i - 200, i, 14): # this will loop 14 times (14² = 196) + if (i / 10) % 10 == (i + j) % 10: + u1 = self.users[i] + u2 = self.users[j] + self.logger.info(f"Making {u2} the godfather of {u1}") + godfathers.append(u2) + u1.godfathers.set(godfathers) + + def make_club_memberships(self): + """ + Assign users to clubs and give them a role in a pseudo-random way. + + The :meth:`make_users` and :meth:`make_clubs` methods + must have been called beforehand. + + Work by making multiples passes on all users to affect + them some pseudo-random roles in some clubs. + The multiple passes are useful to get some variations over who goes where. + Each pass for each user has a chance to affect her to two different clubs, + increasing a bit more the created chaos, while remaining purely deterministic. + """ + if self.users is None: + raise RuntimeError( + "The `make_users()` method must be called before `make_club_memberships()`" + ) + if self.clubs is None: + raise RuntimeError( + "The `make_clubs()` method must be called before `make_club_memberships()`" + ) + memberships = [] + for i in range(1, 11): # users can be in up to 20 clubs + self.logger.info(f"Club membership, pass {i}") + for uid in range( + i, self.NB_USERS, i + ): # Pass #1 will make sure every user is at least in one club + user = self.users[uid] + club = self.clubs[(uid + i**2) % self.NB_CLUBS] + + start = self.now - timedelta( + days=(((self.NB_USERS - uid) * i) // 110) + ) # older users were in clubs before newer users + end = start + timedelta(days=180) # about one semester + self.logger.debug( + f"Making {user} a member of club {club} from {start} to {end}" + ) + memberships.append( + Membership( + user=user, + club=club, + role=(uid + i) % 10 + 1, # spread the different roles + start_date=start, + end_date=end, + ) + ) + + for uid in range( + 10 + i * 2, self.NB_USERS, 10 + i * 2 + ): # Make a second affectation that will skip most users, to make a few citizen more important + user = self.users[uid] + club = self.clubs[(uid + i**3) % self.NB_CLUBS] + + start = self.now - timedelta( + days=(((self.NB_USERS - uid) * i) // 100) + ) # older users were in clubs before newer users + end = start + timedelta(days=180) # about one semester + self.logger.debug( + f"Making {user} a member of club {club} from {start} to {end}" + ) + memberships.append( + Membership( + user=user, + club=club, + role=((uid // 10) + i) % 10 + 1, # spread the different roles + start_date=start, + end_date=end, + ) + ) + Membership.objects.bulk_create(memberships) + + def make_pictures(self): + """ + Create pictures for users to be tagged on later. + + The :meth:`make_users` method must have been called beforehand. + """ + if self.users is None: + raise RuntimeError( + "The `make_users()` method must be called before `make_families()`" + ) + self.picts = [] + # Create twice as many pictures as users + for i in range(self.NB_USERS * 2): + u = self.users[i % self.NB_USERS] + self.logger.info(f"Making Picture {i // self.NB_USERS} for {u}") + self.picts.append( + Picture( + owner=u, + name=f"galaxy-picture {u} {i // self.NB_USERS}", + is_moderated=True, + is_folder=False, + parent=self.galaxy_album, + is_in_sas=True, + file=ContentFile(RED_PIXEL_PNG), + compressed=ContentFile(RED_PIXEL_PNG), + thumbnail=ContentFile(RED_PIXEL_PNG), + mime_type="image/png", + size=len(RED_PIXEL_PNG), + ) + ) + self.picts[i].file.name = self.picts[i].name + self.picts[i].compressed.name = self.picts[i].name + self.picts[i].thumbnail.name = self.picts[i].name + Picture.objects.bulk_create(self.picts) + self.picts = Picture.objects.filter(name__startswith="galaxy-").all() + + def make_pictures_memberships(self): + """ + Assign users to pictures and make enough of them for our + created users to be eligible for promotion as citizen. + + See :meth:`galaxy.models.Galaxy.rule` for details on promotion to citizen. + """ + self.pictures_tags = [] + + # We don't want to handle limits, users in the middle will be far enough + def _tag_neighbors(uid, neighbor_dist, pict_offset, pict_dist): + u2 = self.users[uid - neighbor_dist] + u3 = self.users[uid + neighbor_dist] + self.pictures_tags += [ + PeoplePictureRelation(user=u2, picture=self.picts[uid + pict_offset]), + PeoplePictureRelation(user=u3, picture=self.picts[uid + pict_offset]), + PeoplePictureRelation(user=u2, picture=self.picts[uid - pict_dist]), + PeoplePictureRelation(user=u3, picture=self.picts[uid - pict_dist]), + PeoplePictureRelation(user=u2, picture=self.picts[uid + pict_dist]), + PeoplePictureRelation(user=u3, picture=self.picts[uid + pict_dist]), + ] + + for uid in range(200, self.NB_USERS - 200): + u1 = self.users[uid] + self.logger.info(f"Pictures of {u1}") + self.pictures_tags += [ + PeoplePictureRelation(user=u1, picture=self.picts[uid]), + PeoplePictureRelation(user=u1, picture=self.picts[uid - 14]), + PeoplePictureRelation(user=u1, picture=self.picts[uid + 14]), + PeoplePictureRelation(user=u1, picture=self.picts[uid - 20]), + PeoplePictureRelation(user=u1, picture=self.picts[uid + 20]), + PeoplePictureRelation(user=u1, picture=self.picts[uid - 21]), + PeoplePictureRelation(user=u1, picture=self.picts[uid + 21]), + PeoplePictureRelation(user=u1, picture=self.picts[uid - 22]), + PeoplePictureRelation(user=u1, picture=self.picts[uid + 22]), + PeoplePictureRelation(user=u1, picture=self.picts[uid - 30]), + PeoplePictureRelation(user=u1, picture=self.picts[uid + 30]), + PeoplePictureRelation(user=u1, picture=self.picts[uid - 31]), + PeoplePictureRelation(user=u1, picture=self.picts[uid + 31]), + PeoplePictureRelation(user=u1, picture=self.picts[uid - 32]), + PeoplePictureRelation(user=u1, picture=self.picts[uid + 32]), + ] + + if uid % 3 == 0: + _tag_neighbors(uid, 1, 0, 40) + if uid % 5 == 0: + _tag_neighbors(uid, 2, 0, 50) + if uid % 10 == 0: + _tag_neighbors(uid, 3, 0, 60) + if uid % 20 == 0: + _tag_neighbors(uid, 5, 0, 70) + if uid % 25 == 0: + _tag_neighbors(uid, 10, 0, 80) + + if uid % 2 == 1: + _tag_neighbors(uid, 1, self.NB_USERS, 90) + if uid % 15 == 0: + _tag_neighbors(uid, 5, self.NB_USERS, 100) + if uid % 30 == 0: + _tag_neighbors(uid, 4, self.NB_USERS, 110) + PeoplePictureRelation.objects.bulk_create(self.pictures_tags) + + def make_important_citizen(self, uid: int): + """ + Make the user whose uid is given in parameter a more important citizen, + thus triggering many more connections to others (lanes) + and dragging him towards the center of the Galaxy. + + This promotion is obtained by adding more family links + and by tagging the user in more pictures. + + The users chosen to be added to this user's family shall + also be tagged in more pictures, thus making them also + more important. + + :param uid: the id of the user to make more important + """ + u1 = self.users[uid] + u2 = self.users[uid - 100] + u3 = self.users[uid + 100] + u1.godfathers.add(u2) + u1.godchildren.add(u3) + self.logger.info(f"{u1} will be important and close to {u2} and {u3}") + pictures_tags = [] + for p in range( # Mix them with other citizen for more chaos + uid - 400, uid - 200 + ): + # users may already be on the pictures + if not self.picts[p].people.filter(user=u1).exists(): + pictures_tags.append( + PeoplePictureRelation(user=u1, picture=self.picts[p]) + ) + if not self.picts[p].people.filter(user=u2).exists(): + pictures_tags.append( + PeoplePictureRelation(user=u2, picture=self.picts[p]) + ) + if not self.picts[p + self.NB_USERS].people.filter(user=u1).exists(): + pictures_tags.append( + PeoplePictureRelation( + user=u1, picture=self.picts[p + self.NB_USERS] + ) + ) + if not self.picts[p + self.NB_USERS].people.filter(user=u2).exists(): + pictures_tags.append( + PeoplePictureRelation( + user=u2, picture=self.picts[p + self.NB_USERS] + ) + ) + PeoplePictureRelation.objects.bulk_create(pictures_tags) diff --git a/galaxy/management/commands/rule_galaxy.py b/galaxy/management/commands/rule_galaxy.py index 1db3c975..55cb9ae9 100644 --- a/galaxy/management/commands/rule_galaxy.py +++ b/galaxy/management/commands/rule_galaxy.py @@ -21,6 +21,7 @@ # Place - Suite 330, Boston, MA 02111-1307, USA. # # +import warnings from django.core.management.base import BaseCommand from django.db import connection @@ -41,19 +42,21 @@ class Command(BaseCommand): def handle(self, *args, **options): logger = logging.getLogger("main") - if options["verbosity"] > 1: + if options["verbosity"] < 0 or 2 < options["verbosity"]: + warnings.warn("verbosity level should be between 0 and 2 included") + + if options["verbosity"] == 2: logger.setLevel(logging.DEBUG) - elif options["verbosity"] > 0: + elif options["verbosity"] == 1: logger.setLevel(logging.INFO) else: - logger.setLevel(logging.NOTSET) + logger.setLevel(logging.ERROR) logger.info("The Galaxy is being ruled by the Sith.") - Galaxy.rule() - logger.info( - "Caching current Galaxy state for a quicker display of the Empire's power." - ) - Galaxy.make_state() + galaxy = Galaxy.objects.create() + galaxy.rule() + logger.info("Sending old galaxies' remains to garbage.") + Galaxy.objects.filter(state__isnull=True).delete() logger.info("Ruled the galaxy in {} queries.".format(len(connection.queries))) if options["verbosity"] > 2: diff --git a/galaxy/migrations/0002_auto_20230412_1130.py b/galaxy/migrations/0002_auto_20230412_1130.py new file mode 100644 index 00000000..be01af99 --- /dev/null +++ b/galaxy/migrations/0002_auto_20230412_1130.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.16 on 2023-04-12 09:30 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("galaxy", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="galaxy", + options={"ordering": ["pk"]}, + ), + migrations.AddField( + model_name="galaxystar", + name="galaxy", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="stars", + to="galaxy.galaxy", + verbose_name="the galaxy this star belongs to", + ), + ), + migrations.AlterField( + model_name="galaxy", + name="state", + field=models.JSONField(null=True, verbose_name="The galaxy current state"), + ), + migrations.AlterField( + model_name="galaxystar", + name="owner", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stars", + to=settings.AUTH_USER_MODEL, + verbose_name="star owner", + ), + ), + ] diff --git a/galaxy/models.py b/galaxy/models.py index cc4a3e72..744cca79 100644 --- a/galaxy/models.py +++ b/galaxy/models.py @@ -22,16 +22,19 @@ # # +from __future__ import annotations + import math import logging +import time + +from typing import List, TypedDict, NamedTuple, Union, Optional -from typing import Tuple from django.db import models from django.db.models import Q, Case, F, Value, When, Count from django.db.models.functions import Concat from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from typing import List, TypedDict from core.models import User from club.models import Club @@ -40,30 +43,58 @@ from sas.models import Picture class GalaxyStar(models.Model): """ - This class defines a star (vertex -> user) in the galaxy graph, storing a reference to its owner citizen, and being - referenced by GalaxyLane. + Define a star (vertex -> user) in the galaxy graph, + storing a reference to its owner citizen. - It also stores the individual mass of this star, used to push it towards the center of the galaxy. + Stars are linked to each others through the :class:`GalaxyLane` model. + + Each GalaxyStar has a mass which push it towards the center of the galaxy. + This mass is proportional to the number of pictures the owner of the star + is tagged on. """ - owner = models.OneToOneField( + owner = models.ForeignKey( User, verbose_name=_("star owner"), - related_name="galaxy_user", + related_name="stars", on_delete=models.CASCADE, ) mass = models.PositiveIntegerField( _("star mass"), default=0, ) + galaxy = models.ForeignKey( + "Galaxy", + verbose_name=_("the galaxy this star belongs to"), + related_name="stars", + on_delete=models.CASCADE, + null=True, + ) def __str__(self): return str(self.owner) +@property +def current_star(self) -> Optional[GalaxyStar]: + """ + The star of this user in the :class:`Galaxy`. + Only take into account the most recent active galaxy. + + :return: The star of this user if there is an active Galaxy + and this user is a citizen of it, else ``None`` + """ + return self.stars.filter(galaxy=Galaxy.get_current_galaxy()).last() + + +# Adding a shortcut to User class for getting its star belonging to the latest ruled Galaxy +setattr(User, "current_star", current_star) + + class GalaxyLane(models.Model): """ - This class defines a lane (edge -> link between galaxy citizen) in the galaxy map, storing a reference to both its + Define a lane (edge -> link between galaxy citizen) + in the galaxy map, storing a reference to both its ends and the distance it covers. Score details between citizen owning the stars is also stored here. """ @@ -110,7 +141,35 @@ class GalaxyDict(TypedDict): links: List +class RelationScore(NamedTuple): + family: int + pictures: int + clubs: int + + class Galaxy(models.Model): + """ + The Galaxy, a graph linking the active users between each others. + The distance between two users is given by a relation score which takes + into account a few parameter like the number of pictures they are both tagged on, + the time during which they were in the same clubs and whether they are + in the same family. + + The citizens of the Galaxy are represented by :class:`GalaxyStar` + and their relations by :class:`GalaxyLane`. + + Several galaxies can coexist. In this case, only the most recent active one + shall usually be taken into account. + This is useful to keep the current galaxy while generating a new one + and swapping them only at the very end. + + Please take into account that generating the galaxy is a very expensive + operation. For this reason, try not to call the :meth:`rule` method more + than once a day in production. + + To quickly access to the state of a galaxy, use the :attr:`state` attribute. + """ + logger = logging.getLogger("main") GALAXY_SCALE_FACTOR = 2_000 @@ -118,77 +177,44 @@ class Galaxy(models.Model): PICTURE_POINTS = 2 # Equivalent to two days as random members of a club. CLUBS_POINTS = 1 # One day together as random members in a club is one point. - state = models.JSONField("current state") + state = models.JSONField(_("The galaxy current state"), null=True) - @staticmethod - def make_state() -> None: - """ - Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/ - """ - without_nickname = Concat( - F("owner__first_name"), Value(" "), F("owner__last_name") - ) - with_nickname = Concat( - F("owner__first_name"), - Value(" "), - F("owner__last_name"), - Value(" ("), - F("owner__nick_name"), - Value(")"), - ) - stars = GalaxyStar.objects.annotate( - owner_name=Case( - When(owner__nick_name=None, then=without_nickname), - default=with_nickname, - ) - ) - lanes = GalaxyLane.objects.annotate( - star1_owner=F("star1__owner__id"), - star2_owner=F("star2__owner__id"), - ) - json = GalaxyDict( - nodes=[ - StarDict(id=star.owner_id, name=star.owner_name, mass=star.mass) - for star in stars - ], - links=[], - ) - # Make bidirectional links - # TODO: see if this impacts performance with a big graph - for path in lanes: - json["links"].append( - { - "source": path.star1_owner, - "target": path.star2_owner, - "value": path.distance, - } - ) - json["links"].append( - { - "source": path.star2_owner, - "target": path.star1_owner, - "value": path.distance, - } - ) - Galaxy.objects.all().delete() - Galaxy(state=json).save() + class Meta: + ordering = ["pk"] + + def __str__(self): + stars_count = self.stars.count() + s = f"GLX-ID{self.pk}-SC{stars_count}-" + if self.state is None: + s += "CHS" # CHAOS + else: + s += "RLD" # RULED + return s + + @classmethod + def get_current_galaxy( + cls, + ) -> Galaxy: # __future__.annotations is required for this + return Galaxy.objects.filter(state__isnull=False).last() ################### # User self score # ################### @classmethod - def compute_user_score(cls, user) -> int: + def compute_user_score(cls, user: User) -> int: """ - This compute an individual score for each citizen. It will later be used by the graph algorithm to push + Compute an individual score for each citizen. + It will later be used by the graph algorithm to push higher scores towards the center of the galaxy. Idea: This could be added to the computation: - - Forum posts - - Picture count - - Counter consumption - - Barman time - - ... + + - Forum posts + - Picture count + - Counter consumption + - Barman time + - ... """ user_score = 1 user_score += cls.query_user_score(user) @@ -203,7 +229,11 @@ class Galaxy(models.Model): return user_score @classmethod - def query_user_score(cls, user) -> int: + def query_user_score(cls, user: User) -> int: + """ + Perform the db query to get the individual score + of the given user in the galaxy. + """ score_query = ( User.objects.filter(id=user.id) .annotate( @@ -230,26 +260,48 @@ class Galaxy(models.Model): #################### @classmethod - def compute_users_score(cls, user1, user2) -> Tuple[int, int, int, int]: + def compute_users_score(cls, user1: User, user2: User) -> RelationScore: + """ + Compute the relationship scores of the two given users + in the following fields : + + - family: if they have some godfather/godchild relation + - pictures: in how many pictures are both tagged + - clubs: during how many days they were members of the same clubs + """ family = cls.compute_users_family_score(user1, user2) pictures = cls.compute_users_pictures_score(user1, user2) clubs = cls.compute_users_clubs_score(user1, user2) - score = family + pictures + clubs - return score, family, pictures, clubs + return RelationScore(family=family, pictures=pictures, clubs=clubs) @classmethod - def compute_users_family_score(cls, user1, user2) -> int: + def compute_users_family_score(cls, user1: User, user2: User) -> int: + """ + Compute the family score of the relation between the given users. + This takes into account mutual godfathers. + + :return: 366 if user1 is the godfather of user2 (or vice versa) else 0 + """ link_count = User.objects.filter( Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1) ).count() - if link_count: + if link_count > 0: cls.logger.debug( f"\t\t- '{user1}' and '{user2}' have {link_count} direct family link" ) return link_count * cls.FAMILY_LINK_POINTS @classmethod - def compute_users_pictures_score(cls, user1, user2) -> int: + def compute_users_pictures_score(cls, user1: User, user2: User) -> int: + """ + Compute the pictures score of the relation between the given users. + + The pictures score is obtained by counting the number + of :class:`Picture` in which they have been both identified. + This score is then multiplied by 2. + + :return: The number of pictures both users have in common, times 2 + """ picture_count = ( Picture.objects.filter(people__user__in=(user1,)) .filter(people__user__in=(user2,)) @@ -262,7 +314,21 @@ class Galaxy(models.Model): return picture_count * cls.PICTURE_POINTS @classmethod - def compute_users_clubs_score(cls, user1, user2) -> int: + def compute_users_clubs_score(cls, user1: User, user2: User) -> int: + """ + Compute the clubs score of the relation between the given users. + + The club score is obtained by counting the number of days + during which the memberships (see :class:`club.models.Membership`) + of both users overlapped. + + For example, if user1 was a member of Unitec from 01/01/2020 to 31/12/2021 + (two years) and user2 was a member of the same club from 01/01/2021 to + 31/12/2022 (also two years, but with an offset of one year), then their + club score is 365. + + :return: the number of days during which both users were in the same club + """ common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter( members__in=user2.memberships.all() ) @@ -272,6 +338,7 @@ class Galaxy(models.Model): score = 0 for user1_membership in user1_memberships: if user1_membership.end_date is None: + # user1_membership.save() is not called in this function, hence this is safe user1_membership.end_date = timezone.now().date() query = Q( # start2 <= start1 <= end2 start_date__lte=user1_membership.start_date, @@ -312,54 +379,17 @@ class Galaxy(models.Model): ################### @classmethod - def rule(cls) -> None: - GalaxyStar.objects.all().delete() - # The following is a no-op thanks to cascading, but in case that changes in the future, better keep it anyway. - GalaxyLane.objects.all().delete() - rulable_users = ( - User.objects.filter(subscriptions__isnull=False) - .filter( - Q(godchildren__isnull=False) - | Q(godfathers__isnull=False) - | Q(pictures__isnull=False) - | Q(memberships__isnull=False) - ) - .distinct() - ) - # force fetch of the whole query to make sure there won't - # be any more db hits - # this is memory expensive but prevents a lot of db hits, therefore - # is far more time efficient - rulable_users = list(rulable_users) - while len(rulable_users) > 0: - user1 = rulable_users.pop() - for user2 in rulable_users: - cls.logger.debug("") - cls.logger.debug(f"\t> Ruling '{user1}' against '{user2}'") - star1, _ = GalaxyStar.objects.get_or_create(owner=user1) - star2, _ = GalaxyStar.objects.get_or_create(owner=user2) - if star1.mass == 0: - star1.mass = cls.compute_user_score(user1) - star1.save() - if star2.mass == 0: - star2.mass = cls.compute_user_score(user2) - star2.save() - users_score, family, pictures, clubs = cls.compute_users_score( - user1, user2 - ) - if users_score > 0: - GalaxyLane( - star1=star1, - star2=star2, - distance=cls.scale_distance(users_score), - family=family, - pictures=pictures, - clubs=clubs, - ).save() + def scale_distance(cls, value: Union[int, float]) -> int: + """ + Given a numeric value, return a scaled value which can + be used in the Galaxy's graphical interface to set the distance + between two stars - @classmethod - def scale_distance(cls, value) -> int: + :return: the scaled value usable in the Galaxy's 3d graph + """ # TODO: this will need adjustements with the real, typical data on Taiste + if value == 0: + return 4000 # Following calculus would give us +∞, we cap it to 4000 cls.logger.debug(f"\t\t> Score: {value}") # Invert score to draw close users together @@ -376,3 +406,224 @@ class Galaxy(models.Model): ) cls.logger.debug(f"\t\t> Scaled distance: {value}") return int(value) + + def rule(self, picture_count_threshold=10) -> None: + """ + Main function of the Galaxy. + Iterate over all the rulable users to promote them to citizens. + A citizen is a user who has a corresponding star in the Galaxy. + Also build up the lanes, which are the links between the different citizen. + + Users who can be ruled are defined with the `picture_count_threshold`: + all users who are identified in a strictly lower number of pictures + won't be promoted to citizens. + This does very effectively limit the quantity of computing to do + and only includes users who have had a minimum of activity. + + This method still remains very expensive, so think thoroughly before + you call it, especially in production. + + :param picture_count_threshold: the minimum number of picture to have to be + included in the galaxy + """ + total_time = time.time() + self.logger.info("Listing rulable citizen.") + rulable_users = ( + User.objects.filter(subscriptions__isnull=False) + .annotate(pictures_count=Count("pictures")) + .filter(pictures_count__gt=picture_count_threshold) + .distinct() + ) + + # force fetch of the whole query to make sure there won't + # be any more db hits + # this is memory expensive but prevents a lot of db hits, therefore + # is far more time efficient + + rulable_users = list(rulable_users) + rulable_users_count = len(rulable_users) + user1_count = 0 + self.logger.info( + f"{rulable_users_count} citizen have been listed. Starting to rule." + ) + + stars = [] + self.logger.info("Creating stars for all citizen") + for user in rulable_users: + star = GalaxyStar( + owner=user, galaxy=self, mass=self.compute_user_score(user) + ) + stars.append(star) + GalaxyStar.objects.bulk_create(stars) + + stars = {} + for star in GalaxyStar.objects.filter(galaxy=self): + stars[star.owner.id] = star + + self.logger.info("Creating lanes between stars") + # Display current speed every $speed_count_frequency users + speed_count_frequency = max(rulable_users_count // 10, 1) # ten time at most + global_avg_speed_accumulator = 0 + global_avg_speed_count = 0 + t_global_start = time.time() + while len(rulable_users) > 0: + user1 = rulable_users.pop() + user1_count += 1 + rulable_users_count2 = len(rulable_users) + + star1 = stars[user1.id] + + user_avg_speed = 0 + user_avg_speed_count = 0 + + tstart = time.time() + lanes = [] + for user2_count, user2 in enumerate(rulable_users, start=1): + self.logger.debug("") + self.logger.debug( + f"\t> Examining '{user1}' ({user1_count}/{rulable_users_count}) with '{user2}' ({user2_count}/{rulable_users_count2})" + ) + + star2 = stars[user2.id] + + score = Galaxy.compute_users_score(user1, user2) + distance = self.scale_distance(sum(score)) + if distance < 30: # TODO: this needs tuning with real-world data + lanes.append( + GalaxyLane( + star1=star1, + star2=star2, + distance=distance, + family=score.family, + pictures=score.pictures, + clubs=score.clubs, + ) + ) + + if user2_count % speed_count_frequency == 0: + tend = time.time() + delta = tend - tstart + speed = float(speed_count_frequency) / delta + user_avg_speed += speed + user_avg_speed_count += 1 + self.logger.debug( + f"\tSpeed: {speed:.2f} users per second (time for last {speed_count_frequency} citizens: {delta:.2f} second)" + ) + tstart = time.time() + + GalaxyLane.objects.bulk_create(lanes) + + self.logger.info("") + + t_global_end = time.time() + global_delta = t_global_end - t_global_start + speed = 1.0 / global_delta + global_avg_speed_accumulator += speed + global_avg_speed_count += 1 + global_avg_speed = global_avg_speed_accumulator / global_avg_speed_count + + self.logger.info(f" Ruling of {self} ".center(60, "#")) + self.logger.info( + f"Progression: {user1_count}/{rulable_users_count} citizen -- {rulable_users_count - user1_count} remaining" + ) + self.logger.info(f"Speed: {60.0*global_avg_speed:.2f} citizen per minute") + + # We can divide the computed ETA by 2 because each loop, there is one citizen less to check, and maths tell + # us that this averages to a division by two + eta = rulable_users_count2 / global_avg_speed / 2 + eta_hours = int(eta // 3600) + eta_minutes = int(eta // 60 % 60) + self.logger.info( + f"ETA: {eta_hours} hours {eta_minutes} minutes ({eta / 3600 / 24:.2f} days)" + ) + self.logger.info("#" * 60) + t_global_start = time.time() + + # Here, we get the IDs of the old galaxies that we'll need to delete. In normal operation, only one galaxy + # should be returned, and we can't delete it yet, as it's the one still displayed by the Sith. + old_galaxies_pks = list( + Galaxy.objects.filter(state__isnull=False).values_list("pk", flat=True) + ) + self.logger.info( + f"These old galaxies will be deleted once the new one is ready: {old_galaxies_pks}" + ) + + # Making the state sets this new galaxy as being ready. From now on, the Sith will show us to the world. + self.make_state() + + # Avoid accident if there is nothing to delete + if len(old_galaxies_pks) > 0: + # Former galaxies can now be deleted. + Galaxy.objects.filter(pk__in=old_galaxies_pks).delete() + + total_time = time.time() - total_time + total_time_hours = int(total_time // 3600) + total_time_minutes = int(total_time // 60 % 60) + total_time_seconds = int(total_time % 60) + self.logger.info( + f"{self} ruled in {total_time:.2f} seconds ({total_time_hours} hours, {total_time_minutes} minutes, {total_time_seconds} seconds)" + ) + + def make_state(self) -> None: + """ + Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/ + """ + self.logger.info( + "Caching current Galaxy state for a quicker display of the Empire's power." + ) + + without_nickname = Concat( + F("owner__first_name"), Value(" "), F("owner__last_name") + ) + with_nickname = Concat( + F("owner__first_name"), + Value(" "), + F("owner__last_name"), + Value(" ("), + F("owner__nick_name"), + Value(")"), + ) + stars = ( + GalaxyStar.objects.filter(galaxy=self) + .order_by( + "owner" + ) # This helps determinism for the tests and doesn't cost much + .annotate( + owner_name=Case( + When(owner__nick_name=None, then=without_nickname), + default=with_nickname, + ) + ) + ) + lanes = ( + GalaxyLane.objects.filter(star1__galaxy=self) + .order_by( + "star1" + ) # This helps determinism for the tests and doesn't cost much + .annotate( + star1_owner=F("star1__owner__id"), + star2_owner=F("star2__owner__id"), + ) + ) + json = GalaxyDict( + nodes=[ + StarDict( + id=star.owner_id, + name=star.owner_name, + mass=star.mass, + ) + for star in stars + ], + links=[], + ) + for path in lanes: + json["links"].append( + { + "source": path.star1_owner, + "target": path.star2_owner, + "value": path.distance, + } + ) + self.state = json + self.save() + self.logger.info(f"{self} is now ready!") diff --git a/galaxy/ref_galaxy_state.json b/galaxy/ref_galaxy_state.json new file mode 100644 index 00000000..7c19b796 --- /dev/null +++ b/galaxy/ref_galaxy_state.json @@ -0,0 +1 @@ +{"nodes": [{"id": 215, "name": "Citizen n\u00b0202", "mass": 5}, {"id": 219, "name": "Citizen n\u00b0206", "mass": 5}, {"id": 221, "name": "Citizen n\u00b0208", "mass": 5}, {"id": 225, "name": "Citizen n\u00b0212", "mass": 5}, {"id": 227, "name": "Citizen n\u00b0214", "mass": 5}, {"id": 228, "name": "Citizen n\u00b0215", "mass": 5}, {"id": 231, "name": "Citizen n\u00b0218", "mass": 5}, {"id": 233, "name": "Citizen n\u00b0220", "mass": 5}, {"id": 243, "name": "Citizen n\u00b0230", "mass": 5}, {"id": 245, "name": "Citizen n\u00b0232", "mass": 5}, {"id": 248, "name": "Citizen n\u00b0235", "mass": 5}, {"id": 249, "name": "Citizen n\u00b0236", "mass": 5}, {"id": 251, "name": "Citizen n\u00b0238", "mass": 5}, {"id": 255, "name": "Citizen n\u00b0242", "mass": 5}, {"id": 257, "name": "Citizen n\u00b0244", "mass": 5}, {"id": 261, "name": "Citizen n\u00b0248", "mass": 5}, {"id": 263, "name": "Citizen n\u00b0250", "mass": 5}, {"id": 273, "name": "Citizen n\u00b0260", "mass": 6}, {"id": 275, "name": "Citizen n\u00b0262", "mass": 5}, {"id": 278, "name": "Citizen n\u00b0265", "mass": 5}, {"id": 279, "name": "Citizen n\u00b0266", "mass": 5}, {"id": 281, "name": "Citizen n\u00b0268", "mass": 5}, {"id": 285, "name": "Citizen n\u00b0272", "mass": 5}, {"id": 287, "name": "Citizen n\u00b0274", "mass": 5}, {"id": 291, "name": "Citizen n\u00b0278", "mass": 5}, {"id": 293, "name": "Citizen n\u00b0280", "mass": 6}, {"id": 303, "name": "Citizen n\u00b0290", "mass": 6}, {"id": 305, "name": "Citizen n\u00b0292", "mass": 5}, {"id": 309, "name": "Citizen n\u00b0296", "mass": 5}, {"id": 311, "name": "Citizen n\u00b0298", "mass": 5}, {"id": 315, "name": "Citizen n\u00b0302", "mass": 5}, {"id": 317, "name": "Citizen n\u00b0304", "mass": 5}, {"id": 321, "name": "Citizen n\u00b0308", "mass": 5}, {"id": 323, "name": "Citizen n\u00b0310", "mass": 6}, {"id": 333, "name": "Citizen n\u00b0320", "mass": 5}, {"id": 335, "name": "Citizen n\u00b0322", "mass": 5}, {"id": 339, "name": "Citizen n\u00b0326", "mass": 5}, {"id": 341, "name": "Citizen n\u00b0328", "mass": 5}, {"id": 345, "name": "Citizen n\u00b0332", "mass": 5}, {"id": 347, "name": "Citizen n\u00b0334", "mass": 5}, {"id": 348, "name": "Citizen n\u00b0335", "mass": 5}, {"id": 351, "name": "Citizen n\u00b0338", "mass": 5}, {"id": 353, "name": "Citizen n\u00b0340", "mass": 6}, {"id": 363, "name": "Citizen n\u00b0350", "mass": 5}, {"id": 365, "name": "Citizen n\u00b0352", "mass": 5}, {"id": 369, "name": "Citizen n\u00b0356", "mass": 5}, {"id": 371, "name": "Citizen n\u00b0358", "mass": 5}, {"id": 375, "name": "Citizen n\u00b0362", "mass": 5}, {"id": 377, "name": "Citizen n\u00b0364", "mass": 5}, {"id": 378, "name": "Citizen n\u00b0365", "mass": 5}, {"id": 381, "name": "Citizen n\u00b0368", "mass": 5}, {"id": 383, "name": "Citizen n\u00b0370", "mass": 5}, {"id": 393, "name": "Citizen n\u00b0380", "mass": 5}, {"id": 395, "name": "Citizen n\u00b0382", "mass": 5}, {"id": 398, "name": "Citizen n\u00b0385", "mass": 5}, {"id": 399, "name": "Citizen n\u00b0386", "mass": 5}, {"id": 401, "name": "Citizen n\u00b0388", "mass": 5}, {"id": 405, "name": "Citizen n\u00b0392", "mass": 5}, {"id": 407, "name": "Citizen n\u00b0394", "mass": 5}, {"id": 411, "name": "Citizen n\u00b0398", "mass": 5}, {"id": 413, "name": "Citizen n\u00b0400", "mass": 10}, {"id": 423, "name": "Citizen n\u00b0410", "mass": 6}, {"id": 425, "name": "Citizen n\u00b0412", "mass": 5}, {"id": 428, "name": "Citizen n\u00b0415", "mass": 5}, {"id": 429, "name": "Citizen n\u00b0416", "mass": 5}, {"id": 431, "name": "Citizen n\u00b0418", "mass": 5}, {"id": 435, "name": "Citizen n\u00b0422", "mass": 5}, {"id": 437, "name": "Citizen n\u00b0424", "mass": 5}, {"id": 441, "name": "Citizen n\u00b0428", "mass": 5}, {"id": 443, "name": "Citizen n\u00b0430", "mass": 5}, {"id": 453, "name": "Citizen n\u00b0440", "mass": 6}, {"id": 455, "name": "Citizen n\u00b0442", "mass": 5}, {"id": 459, "name": "Citizen n\u00b0446", "mass": 5}, {"id": 461, "name": "Citizen n\u00b0448", "mass": 5}, {"id": 465, "name": "Citizen n\u00b0452", "mass": 5}, {"id": 467, "name": "Citizen n\u00b0454", "mass": 5}, {"id": 471, "name": "Citizen n\u00b0458", "mass": 5}, {"id": 473, "name": "Citizen n\u00b0460", "mass": 6}, {"id": 483, "name": "Citizen n\u00b0470", "mass": 5}, {"id": 485, "name": "Citizen n\u00b0472", "mass": 5}, {"id": 489, "name": "Citizen n\u00b0476", "mass": 5}, {"id": 491, "name": "Citizen n\u00b0478", "mass": 5}, {"id": 495, "name": "Citizen n\u00b0482", "mass": 5}, {"id": 497, "name": "Citizen n\u00b0484", "mass": 5}, {"id": 498, "name": "Citizen n\u00b0485", "mass": 5}, {"id": 501, "name": "Citizen n\u00b0488", "mass": 5}, {"id": 503, "name": "Citizen n\u00b0490", "mass": 6}, {"id": 513, "name": "Citizen n\u00b0500", "mass": 10}, {"id": 515, "name": "Citizen n\u00b0502", "mass": 5}, {"id": 519, "name": "Citizen n\u00b0506", "mass": 5}, {"id": 521, "name": "Citizen n\u00b0508", "mass": 5}, {"id": 525, "name": "Citizen n\u00b0512", "mass": 5}, {"id": 527, "name": "Citizen n\u00b0514", "mass": 5}, {"id": 528, "name": "Citizen n\u00b0515", "mass": 5}, {"id": 531, "name": "Citizen n\u00b0518", "mass": 5}, {"id": 533, "name": "Citizen n\u00b0520", "mass": 5}, {"id": 543, "name": "Citizen n\u00b0530", "mass": 5}, {"id": 545, "name": "Citizen n\u00b0532", "mass": 5}, {"id": 548, "name": "Citizen n\u00b0535", "mass": 5}, {"id": 549, "name": "Citizen n\u00b0536", "mass": 5}, {"id": 551, "name": "Citizen n\u00b0538", "mass": 5}, {"id": 555, "name": "Citizen n\u00b0542", "mass": 5}, {"id": 557, "name": "Citizen n\u00b0544", "mass": 5}, {"id": 561, "name": "Citizen n\u00b0548", "mass": 5}, {"id": 563, "name": "Citizen n\u00b0550", "mass": 5}, {"id": 573, "name": "Citizen n\u00b0560", "mass": 6}, {"id": 575, "name": "Citizen n\u00b0562", "mass": 5}, {"id": 578, "name": "Citizen n\u00b0565", "mass": 5}, {"id": 579, "name": "Citizen n\u00b0566", "mass": 5}, {"id": 581, "name": "Citizen n\u00b0568", "mass": 5}, {"id": 585, "name": "Citizen n\u00b0572", "mass": 5}, {"id": 587, "name": "Citizen n\u00b0574", "mass": 5}, {"id": 591, "name": "Citizen n\u00b0578", "mass": 5}, {"id": 593, "name": "Citizen n\u00b0580", "mass": 5}, {"id": 603, "name": "Citizen n\u00b0590", "mass": 6}, {"id": 605, "name": "Citizen n\u00b0592", "mass": 5}, {"id": 609, "name": "Citizen n\u00b0596", "mass": 5}, {"id": 611, "name": "Citizen n\u00b0598", "mass": 5}, {"id": 615, "name": "Citizen n\u00b0602", "mass": 5}, {"id": 617, "name": "Citizen n\u00b0604", "mass": 5}, {"id": 621, "name": "Citizen n\u00b0608", "mass": 5}, {"id": 623, "name": "Citizen n\u00b0610", "mass": 6}, {"id": 633, "name": "Citizen n\u00b0620", "mass": 5}, {"id": 635, "name": "Citizen n\u00b0622", "mass": 5}, {"id": 639, "name": "Citizen n\u00b0626", "mass": 5}, {"id": 641, "name": "Citizen n\u00b0628", "mass": 5}, {"id": 645, "name": "Citizen n\u00b0632", "mass": 5}, {"id": 647, "name": "Citizen n\u00b0634", "mass": 5}, {"id": 648, "name": "Citizen n\u00b0635", "mass": 5}, {"id": 651, "name": "Citizen n\u00b0638", "mass": 5}, {"id": 653, "name": "Citizen n\u00b0640", "mass": 6}, {"id": 663, "name": "Citizen n\u00b0650", "mass": 5}, {"id": 665, "name": "Citizen n\u00b0652", "mass": 5}, {"id": 669, "name": "Citizen n\u00b0656", "mass": 5}, {"id": 671, "name": "Citizen n\u00b0658", "mass": 5}, {"id": 675, "name": "Citizen n\u00b0662", "mass": 5}, {"id": 677, "name": "Citizen n\u00b0664", "mass": 5}, {"id": 678, "name": "Citizen n\u00b0665", "mass": 5}, {"id": 681, "name": "Citizen n\u00b0668", "mass": 5}, {"id": 683, "name": "Citizen n\u00b0670", "mass": 5}, {"id": 693, "name": "Citizen n\u00b0680", "mass": 5}, {"id": 695, "name": "Citizen n\u00b0682", "mass": 5}, {"id": 698, "name": "Citizen n\u00b0685", "mass": 5}, {"id": 699, "name": "Citizen n\u00b0686", "mass": 5}, {"id": 701, "name": "Citizen n\u00b0688", "mass": 5}, {"id": 705, "name": "Citizen n\u00b0692", "mass": 5}, {"id": 707, "name": "Citizen n\u00b0694", "mass": 5}, {"id": 711, "name": "Citizen n\u00b0698", "mass": 5}, {"id": 713, "name": "Citizen n\u00b0700", "mass": 6}, {"id": 723, "name": "Citizen n\u00b0710", "mass": 6}, {"id": 725, "name": "Citizen n\u00b0712", "mass": 5}, {"id": 728, "name": "Citizen n\u00b0715", "mass": 5}, {"id": 729, "name": "Citizen n\u00b0716", "mass": 5}, {"id": 731, "name": "Citizen n\u00b0718", "mass": 5}, {"id": 735, "name": "Citizen n\u00b0722", "mass": 5}, {"id": 737, "name": "Citizen n\u00b0724", "mass": 5}, {"id": 741, "name": "Citizen n\u00b0728", "mass": 5}, {"id": 743, "name": "Citizen n\u00b0730", "mass": 5}, {"id": 753, "name": "Citizen n\u00b0740", "mass": 6}, {"id": 755, "name": "Citizen n\u00b0742", "mass": 5}, {"id": 759, "name": "Citizen n\u00b0746", "mass": 5}, {"id": 761, "name": "Citizen n\u00b0748", "mass": 5}, {"id": 765, "name": "Citizen n\u00b0752", "mass": 5}, {"id": 767, "name": "Citizen n\u00b0754", "mass": 5}, {"id": 771, "name": "Citizen n\u00b0758", "mass": 5}, {"id": 773, "name": "Citizen n\u00b0760", "mass": 6}, {"id": 783, "name": "Citizen n\u00b0770", "mass": 5}, {"id": 785, "name": "Citizen n\u00b0772", "mass": 5}, {"id": 789, "name": "Citizen n\u00b0776", "mass": 5}, {"id": 791, "name": "Citizen n\u00b0778", "mass": 5}, {"id": 795, "name": "Citizen n\u00b0782", "mass": 5}, {"id": 797, "name": "Citizen n\u00b0784", "mass": 5}, {"id": 798, "name": "Citizen n\u00b0785", "mass": 5}, {"id": 801, "name": "Citizen n\u00b0788", "mass": 5}, {"id": 803, "name": "Citizen n\u00b0790", "mass": 5}], "links": [{"source": 228, "target": 225, "value": 8}, {"source": 231, "target": 221, "value": 9}, {"source": 233, "target": 221, "value": 4}, {"source": 245, "target": 233, "value": 4}, {"source": 248, "target": 219, "value": 9}, {"source": 248, "target": 233, "value": 4}, {"source": 248, "target": 245, "value": 8}, {"source": 249, "target": 228, "value": 8}, {"source": 251, "target": 245, "value": 10}, {"source": 251, "target": 249, "value": 9}, {"source": 255, "target": 245, "value": 9}, {"source": 255, "target": 251, "value": 7}, {"source": 257, "target": 219, "value": 8}, {"source": 257, "target": 233, "value": 4}, {"source": 257, "target": 245, "value": 8}, {"source": 257, "target": 248, "value": 7}, {"source": 261, "target": 221, "value": 10}, {"source": 261, "target": 249, "value": 8}, {"source": 273, "target": 219, "value": 10}, {"source": 273, "target": 221, "value": 2}, {"source": 273, "target": 231, "value": 9}, {"source": 273, "target": 248, "value": 9}, {"source": 273, "target": 257, "value": 9}, {"source": 273, "target": 261, "value": 8}, {"source": 275, "target": 225, "value": 3}, {"source": 275, "target": 228, "value": 8}, {"source": 278, "target": 225, "value": 7}, {"source": 278, "target": 228, "value": 3}, {"source": 278, "target": 249, "value": 9}, {"source": 278, "target": 275, "value": 8}, {"source": 279, "target": 221, "value": 4}, {"source": 279, "target": 227, "value": 9}, {"source": 279, "target": 233, "value": 4}, {"source": 281, "target": 221, "value": 10}, {"source": 281, "target": 231, "value": 3}, {"source": 281, "target": 243, "value": 8}, {"source": 281, "target": 273, "value": 10}, {"source": 285, "target": 233, "value": 4}, {"source": 285, "target": 245, "value": 10}, {"source": 285, "target": 248, "value": 10}, {"source": 285, "target": 261, "value": 8}, {"source": 285, "target": 273, "value": 8}, {"source": 287, "target": 225, "value": 8}, {"source": 287, "target": 263, "value": 9}, {"source": 293, "target": 221, "value": 11}, {"source": 293, "target": 243, "value": 1}, {"source": 293, "target": 245, "value": 9}, {"source": 293, "target": 251, "value": 8}, {"source": 293, "target": 255, "value": 8}, {"source": 293, "target": 281, "value": 8}, {"source": 293, "target": 285, "value": 8}, {"source": 293, "target": 291, "value": 8}, {"source": 303, "target": 227, "value": 9}, {"source": 303, "target": 228, "value": 8}, {"source": 303, "target": 249, "value": 10}, {"source": 303, "target": 278, "value": 9}, {"source": 303, "target": 279, "value": 8}, {"source": 303, "target": 293, "value": 9}, {"source": 305, "target": 245, "value": 9}, {"source": 305, "target": 251, "value": 7}, {"source": 305, "target": 255, "value": 2}, {"source": 305, "target": 293, "value": 8}, {"source": 309, "target": 219, "value": 9}, {"source": 309, "target": 221, "value": 8}, {"source": 309, "target": 248, "value": 8}, {"source": 309, "target": 257, "value": 8}, {"source": 309, "target": 261, "value": 9}, {"source": 309, "target": 263, "value": 11}, {"source": 309, "target": 273, "value": 8}, {"source": 311, "target": 249, "value": 8}, {"source": 311, "target": 261, "value": 3}, {"source": 311, "target": 285, "value": 8}, {"source": 315, "target": 215, "value": 4}, {"source": 317, "target": 221, "value": 10}, {"source": 317, "target": 227, "value": 9}, {"source": 317, "target": 233, "value": 10}, {"source": 317, "target": 243, "value": 4}, {"source": 317, "target": 255, "value": 8}, {"source": 317, "target": 279, "value": 8}, {"source": 317, "target": 293, "value": 3}, {"source": 317, "target": 305, "value": 8}, {"source": 321, "target": 219, "value": 10}, {"source": 321, "target": 221, "value": 2}, {"source": 321, "target": 225, "value": 7}, {"source": 321, "target": 228, "value": 8}, {"source": 321, "target": 233, "value": 4}, {"source": 321, "target": 249, "value": 11}, {"source": 321, "target": 251, "value": 8}, {"source": 321, "target": 261, "value": 9}, {"source": 321, "target": 275, "value": 7}, {"source": 321, "target": 278, "value": 7}, {"source": 321, "target": 279, "value": 4}, {"source": 321, "target": 293, "value": 11}, {"source": 321, "target": 309, "value": 8}, {"source": 323, "target": 219, "value": 10}, {"source": 323, "target": 221, "value": 8}, {"source": 323, "target": 248, "value": 9}, {"source": 323, "target": 257, "value": 9}, {"source": 323, "target": 261, "value": 8}, {"source": 323, "target": 273, "value": 1}, {"source": 323, "target": 309, "value": 8}, {"source": 333, "target": 221, "value": 4}, {"source": 333, "target": 233, "value": 0}, {"source": 333, "target": 243, "value": 9}, {"source": 333, "target": 245, "value": 8}, {"source": 333, "target": 248, "value": 8}, {"source": 333, "target": 257, "value": 4}, {"source": 333, "target": 279, "value": 4}, {"source": 333, "target": 281, "value": 8}, {"source": 333, "target": 285, "value": 9}, {"source": 333, "target": 293, "value": 9}, {"source": 333, "target": 309, "value": 8}, {"source": 333, "target": 317, "value": 9}, {"source": 333, "target": 321, "value": 4}, {"source": 335, "target": 233, "value": 10}, {"source": 335, "target": 243, "value": 11}, {"source": 335, "target": 273, "value": 8}, {"source": 335, "target": 285, "value": 3}, {"source": 335, "target": 293, "value": 10}, {"source": 335, "target": 333, "value": 8}, {"source": 339, "target": 243, "value": 11}, {"source": 339, "target": 293, "value": 10}, {"source": 339, "target": 335, "value": 8}, {"source": 341, "target": 251, "value": 9}, {"source": 341, "target": 291, "value": 3}, {"source": 341, "target": 293, "value": 4}, {"source": 341, "target": 303, "value": 8}, {"source": 345, "target": 233, "value": 4}, {"source": 345, "target": 245, "value": 2}, {"source": 345, "target": 248, "value": 8}, {"source": 345, "target": 257, "value": 8}, {"source": 345, "target": 285, "value": 9}, {"source": 345, "target": 333, "value": 8}, {"source": 347, "target": 221, "value": 8}, {"source": 347, "target": 273, "value": 4}, {"source": 347, "target": 285, "value": 8}, {"source": 347, "target": 293, "value": 7}, {"source": 347, "target": 323, "value": 9}, {"source": 348, "target": 219, "value": 8}, {"source": 348, "target": 233, "value": 4}, {"source": 348, "target": 245, "value": 8}, {"source": 348, "target": 248, "value": 4}, {"source": 348, "target": 257, "value": 7}, {"source": 348, "target": 273, "value": 9}, {"source": 348, "target": 285, "value": 10}, {"source": 348, "target": 309, "value": 8}, {"source": 348, "target": 323, "value": 9}, {"source": 348, "target": 333, "value": 8}, {"source": 348, "target": 345, "value": 8}, {"source": 351, "target": 251, "value": 3}, {"source": 351, "target": 263, "value": 8}, {"source": 351, "target": 309, "value": 9}, {"source": 351, "target": 341, "value": 9}, {"source": 353, "target": 215, "value": 8}, {"source": 353, "target": 227, "value": 4}, {"source": 353, "target": 228, "value": 9}, {"source": 353, "target": 249, "value": 10}, {"source": 353, "target": 278, "value": 9}, {"source": 353, "target": 279, "value": 4}, {"source": 353, "target": 293, "value": 9}, {"source": 353, "target": 303, "value": 1}, {"source": 353, "target": 315, "value": 8}, {"source": 353, "target": 341, "value": 8}, {"source": 363, "target": 255, "value": 9}, {"source": 363, "target": 261, "value": 9}, {"source": 363, "target": 263, "value": 2}, {"source": 363, "target": 285, "value": 8}, {"source": 363, "target": 287, "value": 9}, {"source": 363, "target": 305, "value": 8}, {"source": 363, "target": 309, "value": 10}, {"source": 363, "target": 311, "value": 9}, {"source": 363, "target": 317, "value": 7}, {"source": 363, "target": 351, "value": 8}, {"source": 365, "target": 215, "value": 4}, {"source": 365, "target": 225, "value": 9}, {"source": 365, "target": 227, "value": 4}, {"source": 365, "target": 228, "value": 10}, {"source": 365, "target": 275, "value": 9}, {"source": 365, "target": 278, "value": 10}, {"source": 365, "target": 291, "value": 8}, {"source": 365, "target": 293, "value": 9}, {"source": 365, "target": 315, "value": 3}, {"source": 365, "target": 317, "value": 4}, {"source": 365, "target": 321, "value": 9}, {"source": 365, "target": 341, "value": 8}, {"source": 365, "target": 353, "value": 8}, {"source": 369, "target": 219, "value": 4}, {"source": 369, "target": 221, "value": 9}, {"source": 369, "target": 231, "value": 8}, {"source": 369, "target": 248, "value": 8}, {"source": 369, "target": 257, "value": 8}, {"source": 369, "target": 273, "value": 5}, {"source": 369, "target": 281, "value": 8}, {"source": 369, "target": 309, "value": 9}, {"source": 369, "target": 321, "value": 10}, {"source": 369, "target": 323, "value": 10}, {"source": 369, "target": 348, "value": 8}, {"source": 371, "target": 221, "value": 4}, {"source": 371, "target": 261, "value": 10}, {"source": 371, "target": 293, "value": 11}, {"source": 371, "target": 309, "value": 8}, {"source": 371, "target": 321, "value": 3}, {"source": 375, "target": 225, "value": 4}, {"source": 375, "target": 228, "value": 8}, {"source": 375, "target": 275, "value": 4}, {"source": 375, "target": 278, "value": 8}, {"source": 375, "target": 321, "value": 7}, {"source": 375, "target": 365, "value": 9}, {"source": 377, "target": 221, "value": 10}, {"source": 377, "target": 225, "value": 9}, {"source": 377, "target": 227, "value": 4}, {"source": 377, "target": 231, "value": 8}, {"source": 377, "target": 243, "value": 10}, {"source": 377, "target": 255, "value": 10}, {"source": 377, "target": 273, "value": 4}, {"source": 377, "target": 275, "value": 9}, {"source": 377, "target": 279, "value": 10}, {"source": 377, "target": 281, "value": 8}, {"source": 377, "target": 285, "value": 9}, {"source": 377, "target": 293, "value": 10}, {"source": 377, "target": 303, "value": 9}, {"source": 377, "target": 305, "value": 10}, {"source": 377, "target": 317, "value": 9}, {"source": 377, "target": 335, "value": 4}, {"source": 377, "target": 339, "value": 8}, {"source": 377, "target": 353, "value": 4}, {"source": 377, "target": 365, "value": 4}, {"source": 377, "target": 369, "value": 7}, {"source": 377, "target": 375, "value": 8}, {"source": 378, "target": 225, "value": 8}, {"source": 378, "target": 228, "value": 4}, {"source": 378, "target": 249, "value": 8}, {"source": 378, "target": 275, "value": 8}, {"source": 378, "target": 278, "value": 4}, {"source": 378, "target": 303, "value": 9}, {"source": 378, "target": 321, "value": 8}, {"source": 378, "target": 353, "value": 9}, {"source": 378, "target": 365, "value": 10}, {"source": 378, "target": 375, "value": 8}, {"source": 381, "target": 221, "value": 10}, {"source": 381, "target": 231, "value": 4}, {"source": 381, "target": 233, "value": 4}, {"source": 381, "target": 243, "value": 8}, {"source": 381, "target": 257, "value": 8}, {"source": 381, "target": 273, "value": 10}, {"source": 381, "target": 281, "value": 2}, {"source": 381, "target": 291, "value": 9}, {"source": 381, "target": 293, "value": 8}, {"source": 381, "target": 333, "value": 2}, {"source": 381, "target": 341, "value": 9}, {"source": 381, "target": 369, "value": 8}, {"source": 381, "target": 377, "value": 8}, {"source": 383, "target": 221, "value": 4}, {"source": 383, "target": 233, "value": 1}, {"source": 383, "target": 257, "value": 8}, {"source": 383, "target": 279, "value": 4}, {"source": 383, "target": 317, "value": 9}, {"source": 383, "target": 321, "value": 4}, {"source": 383, "target": 333, "value": 1}, {"source": 383, "target": 335, "value": 9}, {"source": 383, "target": 381, "value": 7}, {"source": 393, "target": 243, "value": 1}, {"source": 393, "target": 245, "value": 9}, {"source": 393, "target": 251, "value": 8}, {"source": 393, "target": 255, "value": 8}, {"source": 393, "target": 281, "value": 8}, {"source": 393, "target": 293, "value": 1}, {"source": 393, "target": 305, "value": 8}, {"source": 393, "target": 317, "value": 3}, {"source": 393, "target": 333, "value": 9}, {"source": 393, "target": 335, "value": 10}, {"source": 393, "target": 339, "value": 10}, {"source": 393, "target": 377, "value": 9}, {"source": 393, "target": 381, "value": 8}, {"source": 395, "target": 233, "value": 4}, {"source": 395, "target": 245, "value": 4}, {"source": 395, "target": 248, "value": 8}, {"source": 395, "target": 285, "value": 10}, {"source": 395, "target": 333, "value": 8}, {"source": 395, "target": 345, "value": 3}, {"source": 395, "target": 348, "value": 8}, {"source": 398, "target": 219, "value": 8}, {"source": 398, "target": 233, "value": 4}, {"source": 398, "target": 243, "value": 9}, {"source": 398, "target": 245, "value": 8}, {"source": 398, "target": 248, "value": 4}, {"source": 398, "target": 257, "value": 8}, {"source": 398, "target": 273, "value": 10}, {"source": 398, "target": 281, "value": 8}, {"source": 398, "target": 285, "value": 10}, {"source": 398, "target": 293, "value": 9}, {"source": 398, "target": 309, "value": 9}, {"source": 398, "target": 323, "value": 9}, {"source": 398, "target": 333, "value": 4}, {"source": 398, "target": 345, "value": 7}, {"source": 398, "target": 348, "value": 3}, {"source": 398, "target": 369, "value": 8}, {"source": 398, "target": 381, "value": 8}, {"source": 398, "target": 393, "value": 9}, {"source": 398, "target": 395, "value": 8}, {"source": 399, "target": 228, "value": 9}, {"source": 399, "target": 249, "value": 4}, {"source": 399, "target": 251, "value": 10}, {"source": 399, "target": 278, "value": 8}, {"source": 399, "target": 303, "value": 11}, {"source": 399, "target": 321, "value": 11}, {"source": 399, "target": 353, "value": 10}, {"source": 399, "target": 378, "value": 8}, {"source": 401, "target": 251, "value": 4}, {"source": 401, "target": 263, "value": 8}, {"source": 401, "target": 341, "value": 9}, {"source": 401, "target": 351, "value": 3}, {"source": 401, "target": 363, "value": 8}, {"source": 405, "target": 215, "value": 9}, {"source": 405, "target": 233, "value": 10}, {"source": 405, "target": 245, "value": 10}, {"source": 405, "target": 251, "value": 8}, {"source": 405, "target": 255, "value": 2}, {"source": 405, "target": 263, "value": 11}, {"source": 405, "target": 293, "value": 8}, {"source": 405, "target": 303, "value": 9}, {"source": 405, "target": 305, "value": 2}, {"source": 405, "target": 309, "value": 8}, {"source": 405, "target": 315, "value": 9}, {"source": 405, "target": 317, "value": 8}, {"source": 405, "target": 333, "value": 10}, {"source": 405, "target": 335, "value": 8}, {"source": 405, "target": 351, "value": 9}, {"source": 405, "target": 353, "value": 4}, {"source": 405, "target": 363, "value": 4}, {"source": 405, "target": 365, "value": 9}, {"source": 405, "target": 377, "value": 11}, {"source": 405, "target": 383, "value": 10}, {"source": 405, "target": 393, "value": 8}, {"source": 407, "target": 233, "value": 4}, {"source": 407, "target": 245, "value": 8}, {"source": 407, "target": 257, "value": 4}, {"source": 407, "target": 333, "value": 4}, {"source": 407, "target": 345, "value": 7}, {"source": 407, "target": 381, "value": 8}, {"source": 407, "target": 383, "value": 9}, {"source": 411, "target": 249, "value": 8}, {"source": 411, "target": 261, "value": 4}, {"source": 411, "target": 285, "value": 8}, {"source": 411, "target": 311, "value": 4}, {"source": 411, "target": 363, "value": 9}, {"source": 413, "target": 215, "value": 26}, {"source": 413, "target": 219, "value": 27}, {"source": 413, "target": 221, "value": 26}, {"source": 413, "target": 225, "value": 6}, {"source": 413, "target": 227, "value": 28}, {"source": 413, "target": 228, "value": 6}, {"source": 413, "target": 231, "value": 27}, {"source": 413, "target": 233, "value": 27}, {"source": 413, "target": 243, "value": 29}, {"source": 413, "target": 245, "value": 28}, {"source": 413, "target": 248, "value": 28}, {"source": 413, "target": 249, "value": 29}, {"source": 413, "target": 251, "value": 28}, {"source": 413, "target": 255, "value": 28}, {"source": 413, "target": 257, "value": 29}, {"source": 413, "target": 261, "value": 6}, {"source": 413, "target": 263, "value": 1}, {"source": 413, "target": 273, "value": 7}, {"source": 413, "target": 275, "value": 6}, {"source": 413, "target": 278, "value": 6}, {"source": 413, "target": 279, "value": 29}, {"source": 413, "target": 287, "value": 4}, {"source": 413, "target": 309, "value": 8}, {"source": 413, "target": 321, "value": 7}, {"source": 413, "target": 323, "value": 8}, {"source": 413, "target": 339, "value": 7}, {"source": 413, "target": 351, "value": 7}, {"source": 413, "target": 363, "value": 1}, {"source": 413, "target": 365, "value": 8}, {"source": 413, "target": 375, "value": 8}, {"source": 413, "target": 378, "value": 8}, {"source": 413, "target": 401, "value": 8}, {"source": 413, "target": 405, "value": 9}, {"source": 423, "target": 219, "value": 10}, {"source": 423, "target": 221, "value": 8}, {"source": 423, "target": 248, "value": 8}, {"source": 423, "target": 257, "value": 9}, {"source": 423, "target": 261, "value": 8}, {"source": 423, "target": 273, "value": 1}, {"source": 423, "target": 309, "value": 8}, {"source": 423, "target": 323, "value": 2}, {"source": 423, "target": 347, "value": 8}, {"source": 423, "target": 348, "value": 8}, {"source": 423, "target": 369, "value": 10}, {"source": 423, "target": 398, "value": 9}, {"source": 423, "target": 413, "value": 8}, {"source": 425, "target": 225, "value": 2}, {"source": 425, "target": 228, "value": 8}, {"source": 425, "target": 275, "value": 4}, {"source": 425, "target": 278, "value": 8}, {"source": 425, "target": 287, "value": 8}, {"source": 425, "target": 321, "value": 8}, {"source": 425, "target": 365, "value": 9}, {"source": 425, "target": 375, "value": 3}, {"source": 425, "target": 377, "value": 9}, {"source": 425, "target": 378, "value": 8}, {"source": 425, "target": 413, "value": 8}, {"source": 428, "target": 225, "value": 8}, {"source": 428, "target": 228, "value": 4}, {"source": 428, "target": 249, "value": 8}, {"source": 428, "target": 275, "value": 8}, {"source": 428, "target": 278, "value": 4}, {"source": 428, "target": 303, "value": 9}, {"source": 428, "target": 321, "value": 8}, {"source": 428, "target": 353, "value": 9}, {"source": 428, "target": 365, "value": 9}, {"source": 428, "target": 375, "value": 7}, {"source": 428, "target": 378, "value": 3}, {"source": 428, "target": 399, "value": 8}, {"source": 428, "target": 413, "value": 8}, {"source": 428, "target": 425, "value": 8}, {"source": 429, "target": 221, "value": 11}, {"source": 429, "target": 225, "value": 8}, {"source": 429, "target": 233, "value": 11}, {"source": 429, "target": 243, "value": 9}, {"source": 429, "target": 255, "value": 8}, {"source": 429, "target": 279, "value": 4}, {"source": 429, "target": 287, "value": 9}, {"source": 429, "target": 291, "value": 8}, {"source": 429, "target": 293, "value": 8}, {"source": 429, "target": 305, "value": 8}, {"source": 429, "target": 317, "value": 8}, {"source": 429, "target": 333, "value": 11}, {"source": 429, "target": 335, "value": 9}, {"source": 429, "target": 339, "value": 9}, {"source": 429, "target": 341, "value": 8}, {"source": 429, "target": 377, "value": 4}, {"source": 429, "target": 381, "value": 9}, {"source": 429, "target": 383, "value": 10}, {"source": 429, "target": 393, "value": 8}, {"source": 429, "target": 405, "value": 8}, {"source": 429, "target": 425, "value": 9}, {"source": 431, "target": 221, "value": 10}, {"source": 431, "target": 231, "value": 4}, {"source": 431, "target": 243, "value": 9}, {"source": 431, "target": 273, "value": 10}, {"source": 431, "target": 281, "value": 2}, {"source": 431, "target": 293, "value": 9}, {"source": 431, "target": 333, "value": 8}, {"source": 431, "target": 369, "value": 7}, {"source": 431, "target": 377, "value": 7}, {"source": 431, "target": 381, "value": 2}, {"source": 431, "target": 393, "value": 9}, {"source": 431, "target": 398, "value": 8}, {"source": 435, "target": 273, "value": 8}, {"source": 435, "target": 285, "value": 4}, {"source": 435, "target": 335, "value": 4}, {"source": 435, "target": 377, "value": 9}, {"source": 437, "target": 225, "value": 8}, {"source": 437, "target": 228, "value": 8}, {"source": 437, "target": 249, "value": 8}, {"source": 437, "target": 263, "value": 9}, {"source": 437, "target": 278, "value": 8}, {"source": 437, "target": 285, "value": 8}, {"source": 437, "target": 287, "value": 4}, {"source": 437, "target": 293, "value": 9}, {"source": 437, "target": 303, "value": 10}, {"source": 437, "target": 347, "value": 9}, {"source": 437, "target": 353, "value": 10}, {"source": 437, "target": 363, "value": 9}, {"source": 437, "target": 378, "value": 8}, {"source": 437, "target": 399, "value": 8}, {"source": 437, "target": 413, "value": 4}, {"source": 437, "target": 425, "value": 8}, {"source": 437, "target": 428, "value": 7}, {"source": 437, "target": 429, "value": 9}, {"source": 441, "target": 291, "value": 4}, {"source": 441, "target": 293, "value": 5}, {"source": 441, "target": 303, "value": 8}, {"source": 441, "target": 341, "value": 2}, {"source": 441, "target": 353, "value": 8}, {"source": 441, "target": 365, "value": 8}, {"source": 441, "target": 381, "value": 9}, {"source": 441, "target": 429, "value": 8}, {"source": 443, "target": 243, "value": 2}, {"source": 443, "target": 281, "value": 8}, {"source": 443, "target": 293, "value": 1}, {"source": 443, "target": 317, "value": 4}, {"source": 443, "target": 333, "value": 9}, {"source": 443, "target": 335, "value": 10}, {"source": 443, "target": 339, "value": 10}, {"source": 443, "target": 377, "value": 9}, {"source": 443, "target": 381, "value": 8}, {"source": 443, "target": 393, "value": 1}, {"source": 443, "target": 398, "value": 9}, {"source": 443, "target": 429, "value": 8}, {"source": 443, "target": 431, "value": 9}, {"source": 453, "target": 215, "value": 4}, {"source": 453, "target": 227, "value": 4}, {"source": 453, "target": 228, "value": 8}, {"source": 453, "target": 249, "value": 10}, {"source": 453, "target": 263, "value": 9}, {"source": 453, "target": 278, "value": 8}, {"source": 453, "target": 279, "value": 4}, {"source": 453, "target": 293, "value": 10}, {"source": 453, "target": 303, "value": 1}, {"source": 453, "target": 315, "value": 4}, {"source": 453, "target": 341, "value": 8}, {"source": 453, "target": 353, "value": 0}, {"source": 453, "target": 363, "value": 9}, {"source": 453, "target": 365, "value": 4}, {"source": 453, "target": 377, "value": 4}, {"source": 453, "target": 378, "value": 8}, {"source": 453, "target": 399, "value": 9}, {"source": 453, "target": 401, "value": 8}, {"source": 453, "target": 405, "value": 3}, {"source": 453, "target": 413, "value": 9}, {"source": 453, "target": 428, "value": 8}, {"source": 453, "target": 437, "value": 9}, {"source": 453, "target": 441, "value": 8}, {"source": 455, "target": 245, "value": 10}, {"source": 455, "target": 251, "value": 8}, {"source": 455, "target": 255, "value": 4}, {"source": 455, "target": 263, "value": 9}, {"source": 455, "target": 293, "value": 8}, {"source": 455, "target": 305, "value": 4}, {"source": 455, "target": 363, "value": 9}, {"source": 455, "target": 377, "value": 11}, {"source": 455, "target": 393, "value": 8}, {"source": 455, "target": 401, "value": 8}, {"source": 455, "target": 405, "value": 3}, {"source": 455, "target": 413, "value": 9}, {"source": 455, "target": 429, "value": 8}, {"source": 455, "target": 453, "value": 7}, {"source": 459, "target": 263, "value": 11}, {"source": 459, "target": 309, "value": 4}, {"source": 459, "target": 333, "value": 8}, {"source": 459, "target": 351, "value": 10}, {"source": 459, "target": 363, "value": 10}, {"source": 459, "target": 405, "value": 7}, {"source": 459, "target": 413, "value": 10}, {"source": 461, "target": 215, "value": 8}, {"source": 461, "target": 221, "value": 9}, {"source": 461, "target": 249, "value": 8}, {"source": 461, "target": 261, "value": 2}, {"source": 461, "target": 263, "value": 8}, {"source": 461, "target": 273, "value": 8}, {"source": 461, "target": 285, "value": 8}, {"source": 461, "target": 287, "value": 8}, {"source": 461, "target": 309, "value": 4}, {"source": 461, "target": 311, "value": 4}, {"source": 461, "target": 315, "value": 8}, {"source": 461, "target": 321, "value": 9}, {"source": 461, "target": 323, "value": 8}, {"source": 461, "target": 333, "value": 8}, {"source": 461, "target": 339, "value": 10}, {"source": 461, "target": 353, "value": 8}, {"source": 461, "target": 363, "value": 4}, {"source": 461, "target": 365, "value": 7}, {"source": 461, "target": 371, "value": 9}, {"source": 461, "target": 405, "value": 9}, {"source": 461, "target": 411, "value": 3}, {"source": 461, "target": 413, "value": 2}, {"source": 461, "target": 423, "value": 8}, {"source": 461, "target": 437, "value": 8}, {"source": 461, "target": 453, "value": 4}, {"source": 461, "target": 459, "value": 8}, {"source": 465, "target": 215, "value": 4}, {"source": 465, "target": 227, "value": 8}, {"source": 465, "target": 315, "value": 4}, {"source": 465, "target": 317, "value": 9}, {"source": 465, "target": 353, "value": 8}, {"source": 465, "target": 365, "value": 2}, {"source": 465, "target": 377, "value": 8}, {"source": 465, "target": 405, "value": 9}, {"source": 465, "target": 453, "value": 4}, {"source": 465, "target": 461, "value": 7}, {"source": 467, "target": 243, "value": 9}, {"source": 467, "target": 255, "value": 10}, {"source": 467, "target": 293, "value": 4}, {"source": 467, "target": 305, "value": 8}, {"source": 467, "target": 317, "value": 4}, {"source": 467, "target": 363, "value": 8}, {"source": 467, "target": 393, "value": 4}, {"source": 467, "target": 405, "value": 7}, {"source": 467, "target": 443, "value": 9}, {"source": 471, "target": 221, "value": 4}, {"source": 471, "target": 261, "value": 10}, {"source": 471, "target": 293, "value": 11}, {"source": 471, "target": 309, "value": 8}, {"source": 471, "target": 321, "value": 4}, {"source": 471, "target": 371, "value": 4}, {"source": 471, "target": 461, "value": 8}, {"source": 473, "target": 219, "value": 9}, {"source": 473, "target": 221, "value": 4}, {"source": 473, "target": 248, "value": 8}, {"source": 473, "target": 257, "value": 9}, {"source": 473, "target": 261, "value": 8}, {"source": 473, "target": 273, "value": 1}, {"source": 473, "target": 285, "value": 8}, {"source": 473, "target": 309, "value": 8}, {"source": 473, "target": 323, "value": 1}, {"source": 473, "target": 335, "value": 8}, {"source": 473, "target": 347, "value": 4}, {"source": 473, "target": 348, "value": 8}, {"source": 473, "target": 369, "value": 10}, {"source": 473, "target": 377, "value": 9}, {"source": 473, "target": 398, "value": 8}, {"source": 473, "target": 413, "value": 9}, {"source": 473, "target": 423, "value": 1}, {"source": 473, "target": 435, "value": 8}, {"source": 473, "target": 461, "value": 8}, {"source": 483, "target": 221, "value": 4}, {"source": 483, "target": 233, "value": 1}, {"source": 483, "target": 257, "value": 8}, {"source": 483, "target": 279, "value": 4}, {"source": 483, "target": 317, "value": 9}, {"source": 483, "target": 321, "value": 4}, {"source": 483, "target": 333, "value": 1}, {"source": 483, "target": 335, "value": 10}, {"source": 483, "target": 381, "value": 8}, {"source": 483, "target": 383, "value": 2}, {"source": 483, "target": 405, "value": 10}, {"source": 483, "target": 407, "value": 8}, {"source": 483, "target": 429, "value": 9}, {"source": 485, "target": 233, "value": 4}, {"source": 485, "target": 245, "value": 9}, {"source": 485, "target": 248, "value": 9}, {"source": 485, "target": 273, "value": 8}, {"source": 485, "target": 285, "value": 2}, {"source": 485, "target": 293, "value": 8}, {"source": 485, "target": 333, "value": 8}, {"source": 485, "target": 335, "value": 4}, {"source": 485, "target": 345, "value": 9}, {"source": 485, "target": 347, "value": 8}, {"source": 485, "target": 348, "value": 9}, {"source": 485, "target": 377, "value": 10}, {"source": 485, "target": 395, "value": 9}, {"source": 485, "target": 398, "value": 9}, {"source": 485, "target": 435, "value": 3}, {"source": 485, "target": 437, "value": 9}, {"source": 485, "target": 473, "value": 8}, {"source": 489, "target": 243, "value": 3}, {"source": 489, "target": 251, "value": 8}, {"source": 489, "target": 263, "value": 8}, {"source": 489, "target": 281, "value": 8}, {"source": 489, "target": 287, "value": 9}, {"source": 489, "target": 293, "value": 2}, {"source": 489, "target": 317, "value": 10}, {"source": 489, "target": 333, "value": 10}, {"source": 489, "target": 335, "value": 8}, {"source": 489, "target": 339, "value": 4}, {"source": 489, "target": 341, "value": 9}, {"source": 489, "target": 351, "value": 8}, {"source": 489, "target": 363, "value": 8}, {"source": 489, "target": 377, "value": 8}, {"source": 489, "target": 381, "value": 8}, {"source": 489, "target": 393, "value": 2}, {"source": 489, "target": 398, "value": 9}, {"source": 489, "target": 401, "value": 8}, {"source": 489, "target": 413, "value": 2}, {"source": 489, "target": 429, "value": 9}, {"source": 489, "target": 431, "value": 8}, {"source": 489, "target": 437, "value": 8}, {"source": 489, "target": 443, "value": 3}, {"source": 489, "target": 461, "value": 4}, {"source": 489, "target": 467, "value": 10}, {"source": 491, "target": 291, "value": 4}, {"source": 491, "target": 293, "value": 10}, {"source": 491, "target": 341, "value": 4}, {"source": 491, "target": 365, "value": 8}, {"source": 491, "target": 381, "value": 9}, {"source": 491, "target": 429, "value": 7}, {"source": 491, "target": 441, "value": 3}, {"source": 495, "target": 233, "value": 4}, {"source": 495, "target": 245, "value": 4}, {"source": 495, "target": 248, "value": 8}, {"source": 495, "target": 285, "value": 10}, {"source": 495, "target": 333, "value": 8}, {"source": 495, "target": 345, "value": 4}, {"source": 495, "target": 348, "value": 8}, {"source": 495, "target": 395, "value": 4}, {"source": 495, "target": 398, "value": 8}, {"source": 495, "target": 485, "value": 8}, {"source": 497, "target": 221, "value": 8}, {"source": 497, "target": 263, "value": 5}, {"source": 497, "target": 273, "value": 4}, {"source": 497, "target": 285, "value": 8}, {"source": 497, "target": 293, "value": 8}, {"source": 497, "target": 309, "value": 4}, {"source": 497, "target": 323, "value": 9}, {"source": 497, "target": 347, "value": 4}, {"source": 497, "target": 351, "value": 4}, {"source": 497, "target": 363, "value": 4}, {"source": 497, "target": 405, "value": 4}, {"source": 497, "target": 413, "value": 4}, {"source": 497, "target": 423, "value": 9}, {"source": 497, "target": 437, "value": 9}, {"source": 497, "target": 459, "value": 4}, {"source": 497, "target": 473, "value": 4}, {"source": 497, "target": 485, "value": 8}, {"source": 498, "target": 219, "value": 8}, {"source": 498, "target": 233, "value": 4}, {"source": 498, "target": 245, "value": 8}, {"source": 498, "target": 248, "value": 4}, {"source": 498, "target": 257, "value": 8}, {"source": 498, "target": 273, "value": 10}, {"source": 498, "target": 285, "value": 10}, {"source": 498, "target": 309, "value": 9}, {"source": 498, "target": 323, "value": 10}, {"source": 498, "target": 333, "value": 9}, {"source": 498, "target": 345, "value": 8}, {"source": 498, "target": 348, "value": 4}, {"source": 498, "target": 369, "value": 8}, {"source": 498, "target": 395, "value": 8}, {"source": 498, "target": 398, "value": 4}, {"source": 498, "target": 423, "value": 9}, {"source": 498, "target": 473, "value": 9}, {"source": 498, "target": 485, "value": 9}, {"source": 498, "target": 495, "value": 8}, {"source": 501, "target": 249, "value": 8}, {"source": 501, "target": 251, "value": 4}, {"source": 501, "target": 261, "value": 9}, {"source": 501, "target": 263, "value": 8}, {"source": 501, "target": 311, "value": 9}, {"source": 501, "target": 341, "value": 10}, {"source": 501, "target": 351, "value": 4}, {"source": 501, "target": 363, "value": 8}, {"source": 501, "target": 401, "value": 2}, {"source": 501, "target": 411, "value": 9}, {"source": 501, "target": 413, "value": 8}, {"source": 501, "target": 453, "value": 8}, {"source": 501, "target": 455, "value": 8}, {"source": 501, "target": 461, "value": 9}, {"source": 501, "target": 489, "value": 8}, {"source": 503, "target": 227, "value": 8}, {"source": 503, "target": 228, "value": 8}, {"source": 503, "target": 245, "value": 8}, {"source": 503, "target": 249, "value": 9}, {"source": 503, "target": 251, "value": 9}, {"source": 503, "target": 257, "value": 8}, {"source": 503, "target": 278, "value": 8}, {"source": 503, "target": 279, "value": 9}, {"source": 503, "target": 293, "value": 10}, {"source": 503, "target": 303, "value": 2}, {"source": 503, "target": 341, "value": 8}, {"source": 503, "target": 345, "value": 8}, {"source": 503, "target": 351, "value": 9}, {"source": 503, "target": 353, "value": 1}, {"source": 503, "target": 377, "value": 8}, {"source": 503, "target": 378, "value": 8}, {"source": 503, "target": 399, "value": 9}, {"source": 503, "target": 401, "value": 9}, {"source": 503, "target": 405, "value": 9}, {"source": 503, "target": 407, "value": 7}, {"source": 503, "target": 428, "value": 8}, {"source": 503, "target": 437, "value": 9}, {"source": 503, "target": 441, "value": 8}, {"source": 503, "target": 453, "value": 1}, {"source": 503, "target": 501, "value": 8}, {"source": 513, "target": 215, "value": 26}, {"source": 513, "target": 219, "value": 27}, {"source": 513, "target": 221, "value": 26}, {"source": 513, "target": 225, "value": 6}, {"source": 513, "target": 227, "value": 29}, {"source": 513, "target": 228, "value": 6}, {"source": 513, "target": 231, "value": 28}, {"source": 513, "target": 233, "value": 29}, {"source": 513, "target": 243, "value": 29}, {"source": 513, "target": 245, "value": 28}, {"source": 513, "target": 248, "value": 29}, {"source": 513, "target": 249, "value": 29}, {"source": 513, "target": 251, "value": 28}, {"source": 513, "target": 255, "value": 28}, {"source": 513, "target": 257, "value": 29}, {"source": 513, "target": 261, "value": 29}, {"source": 513, "target": 263, "value": 1}, {"source": 513, "target": 273, "value": 28}, {"source": 513, "target": 275, "value": 6}, {"source": 513, "target": 278, "value": 6}, {"source": 513, "target": 287, "value": 3}, {"source": 513, "target": 309, "value": 8}, {"source": 513, "target": 321, "value": 7}, {"source": 513, "target": 351, "value": 7}, {"source": 513, "target": 363, "value": 1}, {"source": 513, "target": 365, "value": 9}, {"source": 513, "target": 375, "value": 8}, {"source": 513, "target": 378, "value": 8}, {"source": 513, "target": 401, "value": 8}, {"source": 513, "target": 405, "value": 9}, {"source": 513, "target": 413, "value": 0}, {"source": 513, "target": 425, "value": 8}, {"source": 513, "target": 428, "value": 8}, {"source": 513, "target": 437, "value": 4}, {"source": 513, "target": 453, "value": 9}, {"source": 513, "target": 455, "value": 9}, {"source": 513, "target": 459, "value": 9}, {"source": 513, "target": 461, "value": 4}, {"source": 513, "target": 489, "value": 4}, {"source": 513, "target": 497, "value": 4}, {"source": 513, "target": 501, "value": 8}, {"source": 515, "target": 215, "value": 4}, {"source": 515, "target": 315, "value": 4}, {"source": 515, "target": 353, "value": 8}, {"source": 515, "target": 365, "value": 4}, {"source": 515, "target": 405, "value": 9}, {"source": 515, "target": 453, "value": 4}, {"source": 515, "target": 461, "value": 7}, {"source": 515, "target": 465, "value": 3}, {"source": 519, "target": 219, "value": 4}, {"source": 519, "target": 221, "value": 9}, {"source": 519, "target": 231, "value": 8}, {"source": 519, "target": 248, "value": 9}, {"source": 519, "target": 257, "value": 8}, {"source": 519, "target": 273, "value": 5}, {"source": 519, "target": 281, "value": 8}, {"source": 519, "target": 309, "value": 10}, {"source": 519, "target": 321, "value": 10}, {"source": 519, "target": 323, "value": 11}, {"source": 519, "target": 348, "value": 9}, {"source": 519, "target": 369, "value": 2}, {"source": 519, "target": 377, "value": 8}, {"source": 519, "target": 381, "value": 8}, {"source": 519, "target": 398, "value": 8}, {"source": 519, "target": 423, "value": 10}, {"source": 519, "target": 431, "value": 8}, {"source": 519, "target": 473, "value": 10}, {"source": 519, "target": 498, "value": 8}, {"source": 521, "target": 221, "value": 2}, {"source": 521, "target": 233, "value": 8}, {"source": 521, "target": 261, "value": 10}, {"source": 521, "target": 279, "value": 8}, {"source": 521, "target": 293, "value": 11}, {"source": 521, "target": 309, "value": 8}, {"source": 521, "target": 321, "value": 2}, {"source": 521, "target": 333, "value": 8}, {"source": 521, "target": 371, "value": 4}, {"source": 521, "target": 383, "value": 8}, {"source": 521, "target": 461, "value": 9}, {"source": 521, "target": 471, "value": 3}, {"source": 521, "target": 483, "value": 8}, {"source": 525, "target": 225, "value": 2}, {"source": 525, "target": 228, "value": 8}, {"source": 525, "target": 251, "value": 8}, {"source": 525, "target": 273, "value": 8}, {"source": 525, "target": 275, "value": 4}, {"source": 525, "target": 278, "value": 8}, {"source": 525, "target": 285, "value": 9}, {"source": 525, "target": 287, "value": 8}, {"source": 525, "target": 321, "value": 8}, {"source": 525, "target": 335, "value": 9}, {"source": 525, "target": 351, "value": 8}, {"source": 525, "target": 365, "value": 10}, {"source": 525, "target": 375, "value": 4}, {"source": 525, "target": 377, "value": 4}, {"source": 525, "target": 378, "value": 8}, {"source": 525, "target": 401, "value": 8}, {"source": 525, "target": 413, "value": 8}, {"source": 525, "target": 425, "value": 2}, {"source": 525, "target": 428, "value": 8}, {"source": 525, "target": 429, "value": 9}, {"source": 525, "target": 435, "value": 9}, {"source": 525, "target": 437, "value": 8}, {"source": 525, "target": 473, "value": 8}, {"source": 525, "target": 485, "value": 9}, {"source": 525, "target": 501, "value": 8}, {"source": 525, "target": 503, "value": 8}, {"source": 525, "target": 513, "value": 8}, {"source": 527, "target": 227, "value": 4}, {"source": 527, "target": 279, "value": 10}, {"source": 527, "target": 303, "value": 9}, {"source": 527, "target": 317, "value": 10}, {"source": 527, "target": 353, "value": 4}, {"source": 527, "target": 365, "value": 4}, {"source": 527, "target": 377, "value": 4}, {"source": 527, "target": 453, "value": 4}, {"source": 527, "target": 465, "value": 7}, {"source": 527, "target": 503, "value": 8}, {"source": 528, "target": 225, "value": 8}, {"source": 528, "target": 228, "value": 4}, {"source": 528, "target": 249, "value": 8}, {"source": 528, "target": 275, "value": 8}, {"source": 528, "target": 278, "value": 4}, {"source": 528, "target": 303, "value": 10}, {"source": 528, "target": 321, "value": 8}, {"source": 528, "target": 353, "value": 10}, {"source": 528, "target": 365, "value": 10}, {"source": 528, "target": 375, "value": 8}, {"source": 528, "target": 378, "value": 4}, {"source": 528, "target": 399, "value": 8}, {"source": 528, "target": 413, "value": 8}, {"source": 528, "target": 425, "value": 8}, {"source": 528, "target": 428, "value": 4}, {"source": 528, "target": 437, "value": 7}, {"source": 528, "target": 453, "value": 9}, {"source": 528, "target": 503, "value": 8}, {"source": 528, "target": 513, "value": 8}, {"source": 528, "target": 525, "value": 8}, {"source": 531, "target": 221, "value": 10}, {"source": 531, "target": 231, "value": 4}, {"source": 531, "target": 273, "value": 4}, {"source": 531, "target": 279, "value": 9}, {"source": 531, "target": 281, "value": 4}, {"source": 531, "target": 285, "value": 8}, {"source": 531, "target": 335, "value": 8}, {"source": 531, "target": 369, "value": 8}, {"source": 531, "target": 377, "value": 4}, {"source": 531, "target": 381, "value": 4}, {"source": 531, "target": 429, "value": 9}, {"source": 531, "target": 431, "value": 4}, {"source": 531, "target": 435, "value": 7}, {"source": 531, "target": 473, "value": 8}, {"source": 531, "target": 485, "value": 7}, {"source": 531, "target": 519, "value": 8}, {"source": 531, "target": 525, "value": 9}, {"source": 533, "target": 221, "value": 4}, {"source": 533, "target": 233, "value": 0}, {"source": 533, "target": 243, "value": 9}, {"source": 533, "target": 245, "value": 8}, {"source": 533, "target": 248, "value": 8}, {"source": 533, "target": 257, "value": 4}, {"source": 533, "target": 279, "value": 4}, {"source": 533, "target": 281, "value": 8}, {"source": 533, "target": 285, "value": 9}, {"source": 533, "target": 291, "value": 9}, {"source": 533, "target": 293, "value": 9}, {"source": 533, "target": 317, "value": 8}, {"source": 533, "target": 321, "value": 4}, {"source": 533, "target": 333, "value": 0}, {"source": 533, "target": 335, "value": 10}, {"source": 533, "target": 341, "value": 9}, {"source": 533, "target": 345, "value": 8}, {"source": 533, "target": 348, "value": 8}, {"source": 533, "target": 381, "value": 2}, {"source": 533, "target": 383, "value": 1}, {"source": 533, "target": 393, "value": 9}, {"source": 533, "target": 395, "value": 8}, {"source": 533, "target": 398, "value": 4}, {"source": 533, "target": 405, "value": 11}, {"source": 533, "target": 407, "value": 4}, {"source": 533, "target": 429, "value": 4}, {"source": 533, "target": 431, "value": 8}, {"source": 533, "target": 441, "value": 9}, {"source": 533, "target": 443, "value": 9}, {"source": 533, "target": 483, "value": 1}, {"source": 533, "target": 485, "value": 8}, {"source": 533, "target": 489, "value": 8}, {"source": 533, "target": 491, "value": 9}, {"source": 533, "target": 495, "value": 8}, {"source": 533, "target": 498, "value": 8}, {"source": 533, "target": 521, "value": 8}, {"source": 543, "target": 243, "value": 2}, {"source": 543, "target": 281, "value": 8}, {"source": 543, "target": 293, "value": 1}, {"source": 543, "target": 317, "value": 4}, {"source": 543, "target": 333, "value": 10}, {"source": 543, "target": 335, "value": 9}, {"source": 543, "target": 339, "value": 9}, {"source": 543, "target": 377, "value": 8}, {"source": 543, "target": 381, "value": 8}, {"source": 543, "target": 393, "value": 1}, {"source": 543, "target": 398, "value": 9}, {"source": 543, "target": 429, "value": 7}, {"source": 543, "target": 431, "value": 9}, {"source": 543, "target": 443, "value": 2}, {"source": 543, "target": 467, "value": 8}, {"source": 543, "target": 489, "value": 2}, {"source": 543, "target": 533, "value": 8}, {"source": 545, "target": 228, "value": 9}, {"source": 545, "target": 233, "value": 4}, {"source": 545, "target": 243, "value": 9}, {"source": 545, "target": 245, "value": 2}, {"source": 545, "target": 248, "value": 8}, {"source": 545, "target": 249, "value": 8}, {"source": 545, "target": 257, "value": 8}, {"source": 545, "target": 273, "value": 10}, {"source": 545, "target": 278, "value": 9}, {"source": 545, "target": 285, "value": 10}, {"source": 545, "target": 293, "value": 9}, {"source": 545, "target": 303, "value": 11}, {"source": 545, "target": 317, "value": 8}, {"source": 545, "target": 323, "value": 10}, {"source": 545, "target": 333, "value": 8}, {"source": 545, "target": 345, "value": 2}, {"source": 545, "target": 348, "value": 8}, {"source": 545, "target": 353, "value": 11}, {"source": 545, "target": 378, "value": 8}, {"source": 545, "target": 393, "value": 9}, {"source": 545, "target": 395, "value": 4}, {"source": 545, "target": 398, "value": 8}, {"source": 545, "target": 399, "value": 8}, {"source": 545, "target": 407, "value": 8}, {"source": 545, "target": 423, "value": 9}, {"source": 545, "target": 428, "value": 8}, {"source": 545, "target": 437, "value": 8}, {"source": 545, "target": 443, "value": 9}, {"source": 545, "target": 453, "value": 9}, {"source": 545, "target": 473, "value": 9}, {"source": 545, "target": 485, "value": 9}, {"source": 545, "target": 495, "value": 3}, {"source": 545, "target": 498, "value": 8}, {"source": 545, "target": 503, "value": 4}, {"source": 545, "target": 528, "value": 8}, {"source": 545, "target": 533, "value": 8}, {"source": 545, "target": 543, "value": 8}, {"source": 548, "target": 219, "value": 8}, {"source": 548, "target": 233, "value": 4}, {"source": 548, "target": 245, "value": 8}, {"source": 548, "target": 248, "value": 4}, {"source": 548, "target": 257, "value": 8}, {"source": 548, "target": 273, "value": 10}, {"source": 548, "target": 285, "value": 10}, {"source": 548, "target": 309, "value": 9}, {"source": 548, "target": 323, "value": 10}, {"source": 548, "target": 333, "value": 9}, {"source": 548, "target": 345, "value": 8}, {"source": 548, "target": 348, "value": 4}, {"source": 548, "target": 369, "value": 8}, {"source": 548, "target": 395, "value": 8}, {"source": 548, "target": 398, "value": 4}, {"source": 548, "target": 423, "value": 9}, {"source": 548, "target": 473, "value": 9}, {"source": 548, "target": 485, "value": 9}, {"source": 548, "target": 495, "value": 7}, {"source": 548, "target": 498, "value": 3}, {"source": 548, "target": 519, "value": 8}, {"source": 548, "target": 533, "value": 8}, {"source": 548, "target": 545, "value": 8}, {"source": 549, "target": 228, "value": 9}, {"source": 549, "target": 249, "value": 2}, {"source": 549, "target": 251, "value": 10}, {"source": 549, "target": 261, "value": 8}, {"source": 549, "target": 263, "value": 9}, {"source": 549, "target": 278, "value": 9}, {"source": 549, "target": 303, "value": 11}, {"source": 549, "target": 309, "value": 9}, {"source": 549, "target": 311, "value": 8}, {"source": 549, "target": 321, "value": 11}, {"source": 549, "target": 351, "value": 8}, {"source": 549, "target": 353, "value": 10}, {"source": 549, "target": 363, "value": 9}, {"source": 549, "target": 378, "value": 8}, {"source": 549, "target": 399, "value": 4}, {"source": 549, "target": 405, "value": 9}, {"source": 549, "target": 411, "value": 8}, {"source": 549, "target": 413, "value": 8}, {"source": 549, "target": 428, "value": 8}, {"source": 549, "target": 437, "value": 8}, {"source": 549, "target": 453, "value": 10}, {"source": 549, "target": 459, "value": 8}, {"source": 549, "target": 461, "value": 8}, {"source": 549, "target": 497, "value": 4}, {"source": 549, "target": 501, "value": 8}, {"source": 549, "target": 503, "value": 9}, {"source": 549, "target": 513, "value": 8}, {"source": 549, "target": 528, "value": 8}, {"source": 549, "target": 545, "value": 8}, {"source": 551, "target": 251, "value": 4}, {"source": 551, "target": 341, "value": 10}, {"source": 551, "target": 351, "value": 4}, {"source": 551, "target": 401, "value": 4}, {"source": 551, "target": 489, "value": 7}, {"source": 551, "target": 501, "value": 3}, {"source": 551, "target": 503, "value": 9}, {"source": 551, "target": 525, "value": 8}, {"source": 555, "target": 245, "value": 10}, {"source": 555, "target": 251, "value": 8}, {"source": 555, "target": 255, "value": 4}, {"source": 555, "target": 293, "value": 8}, {"source": 555, "target": 305, "value": 4}, {"source": 555, "target": 377, "value": 11}, {"source": 555, "target": 393, "value": 8}, {"source": 555, "target": 405, "value": 4}, {"source": 555, "target": 429, "value": 8}, {"source": 555, "target": 455, "value": 4}, {"source": 557, "target": 219, "value": 8}, {"source": 557, "target": 233, "value": 3}, {"source": 557, "target": 245, "value": 8}, {"source": 557, "target": 248, "value": 8}, {"source": 557, "target": 255, "value": 8}, {"source": 557, "target": 257, "value": 2}, {"source": 557, "target": 273, "value": 11}, {"source": 557, "target": 305, "value": 8}, {"source": 557, "target": 309, "value": 9}, {"source": 557, "target": 317, "value": 9}, {"source": 557, "target": 323, "value": 10}, {"source": 557, "target": 333, "value": 3}, {"source": 557, "target": 335, "value": 9}, {"source": 557, "target": 345, "value": 8}, {"source": 557, "target": 348, "value": 8}, {"source": 557, "target": 363, "value": 9}, {"source": 557, "target": 369, "value": 8}, {"source": 557, "target": 381, "value": 8}, {"source": 557, "target": 383, "value": 4}, {"source": 557, "target": 398, "value": 8}, {"source": 557, "target": 405, "value": 4}, {"source": 557, "target": 407, "value": 4}, {"source": 557, "target": 423, "value": 9}, {"source": 557, "target": 467, "value": 9}, {"source": 557, "target": 473, "value": 9}, {"source": 557, "target": 483, "value": 4}, {"source": 557, "target": 498, "value": 8}, {"source": 557, "target": 503, "value": 7}, {"source": 557, "target": 519, "value": 8}, {"source": 557, "target": 533, "value": 2}, {"source": 557, "target": 545, "value": 8}, {"source": 557, "target": 548, "value": 7}, {"source": 561, "target": 249, "value": 8}, {"source": 561, "target": 261, "value": 2}, {"source": 561, "target": 273, "value": 8}, {"source": 561, "target": 285, "value": 8}, {"source": 561, "target": 311, "value": 4}, {"source": 561, "target": 323, "value": 8}, {"source": 561, "target": 363, "value": 10}, {"source": 561, "target": 411, "value": 4}, {"source": 561, "target": 413, "value": 9}, {"source": 561, "target": 423, "value": 8}, {"source": 561, "target": 461, "value": 2}, {"source": 561, "target": 473, "value": 8}, {"source": 561, "target": 501, "value": 9}, {"source": 561, "target": 549, "value": 8}, {"source": 563, "target": 225, "value": 8}, {"source": 563, "target": 228, "value": 9}, {"source": 563, "target": 263, "value": 2}, {"source": 563, "target": 275, "value": 8}, {"source": 563, "target": 278, "value": 9}, {"source": 563, "target": 287, "value": 8}, {"source": 563, "target": 309, "value": 9}, {"source": 563, "target": 321, "value": 8}, {"source": 563, "target": 351, "value": 8}, {"source": 563, "target": 363, "value": 2}, {"source": 563, "target": 365, "value": 9}, {"source": 563, "target": 375, "value": 8}, {"source": 563, "target": 378, "value": 9}, {"source": 563, "target": 401, "value": 8}, {"source": 563, "target": 405, "value": 9}, {"source": 563, "target": 413, "value": 1}, {"source": 563, "target": 425, "value": 8}, {"source": 563, "target": 428, "value": 9}, {"source": 563, "target": 437, "value": 8}, {"source": 563, "target": 453, "value": 9}, {"source": 563, "target": 455, "value": 10}, {"source": 563, "target": 459, "value": 9}, {"source": 563, "target": 461, "value": 8}, {"source": 563, "target": 489, "value": 8}, {"source": 563, "target": 497, "value": 4}, {"source": 563, "target": 501, "value": 8}, {"source": 563, "target": 513, "value": 1}, {"source": 563, "target": 525, "value": 8}, {"source": 563, "target": 528, "value": 8}, {"source": 563, "target": 549, "value": 8}, {"source": 573, "target": 219, "value": 9}, {"source": 573, "target": 221, "value": 2}, {"source": 573, "target": 227, "value": 8}, {"source": 573, "target": 233, "value": 8}, {"source": 573, "target": 248, "value": 8}, {"source": 573, "target": 249, "value": 8}, {"source": 573, "target": 251, "value": 4}, {"source": 573, "target": 257, "value": 8}, {"source": 573, "target": 261, "value": 8}, {"source": 573, "target": 273, "value": 1}, {"source": 573, "target": 279, "value": 8}, {"source": 573, "target": 285, "value": 8}, {"source": 573, "target": 293, "value": 10}, {"source": 573, "target": 309, "value": 8}, {"source": 573, "target": 317, "value": 10}, {"source": 573, "target": 321, "value": 2}, {"source": 573, "target": 323, "value": 1}, {"source": 573, "target": 333, "value": 9}, {"source": 573, "target": 335, "value": 8}, {"source": 573, "target": 347, "value": 4}, {"source": 573, "target": 348, "value": 8}, {"source": 573, "target": 351, "value": 9}, {"source": 573, "target": 365, "value": 4}, {"source": 573, "target": 369, "value": 9}, {"source": 573, "target": 371, "value": 9}, {"source": 573, "target": 377, "value": 4}, {"source": 573, "target": 383, "value": 9}, {"source": 573, "target": 398, "value": 8}, {"source": 573, "target": 399, "value": 8}, {"source": 573, "target": 401, "value": 9}, {"source": 573, "target": 413, "value": 9}, {"source": 573, "target": 423, "value": 1}, {"source": 573, "target": 435, "value": 8}, {"source": 573, "target": 461, "value": 8}, {"source": 573, "target": 465, "value": 8}, {"source": 573, "target": 471, "value": 9}, {"source": 573, "target": 473, "value": 1}, {"source": 573, "target": 483, "value": 8}, {"source": 573, "target": 485, "value": 8}, {"source": 573, "target": 497, "value": 4}, {"source": 573, "target": 498, "value": 8}, {"source": 573, "target": 501, "value": 9}, {"source": 573, "target": 503, "value": 7}, {"source": 573, "target": 519, "value": 9}, {"source": 573, "target": 521, "value": 4}, {"source": 573, "target": 525, "value": 4}, {"source": 573, "target": 527, "value": 7}, {"source": 573, "target": 531, "value": 7}, {"source": 573, "target": 533, "value": 9}, {"source": 573, "target": 545, "value": 9}, {"source": 573, "target": 548, "value": 8}, {"source": 573, "target": 549, "value": 8}, {"source": 573, "target": 551, "value": 9}, {"source": 573, "target": 557, "value": 8}, {"source": 573, "target": 561, "value": 8}, {"source": 575, "target": 225, "value": 4}, {"source": 575, "target": 228, "value": 8}, {"source": 575, "target": 275, "value": 4}, {"source": 575, "target": 278, "value": 8}, {"source": 575, "target": 321, "value": 8}, {"source": 575, "target": 365, "value": 10}, {"source": 575, "target": 375, "value": 4}, {"source": 575, "target": 377, "value": 10}, {"source": 575, "target": 378, "value": 8}, {"source": 575, "target": 413, "value": 8}, {"source": 575, "target": 425, "value": 4}, {"source": 575, "target": 428, "value": 8}, {"source": 575, "target": 513, "value": 8}, {"source": 575, "target": 525, "value": 3}, {"source": 575, "target": 528, "value": 8}, {"source": 575, "target": 563, "value": 8}, {"source": 578, "target": 225, "value": 8}, {"source": 578, "target": 228, "value": 4}, {"source": 578, "target": 249, "value": 8}, {"source": 578, "target": 275, "value": 8}, {"source": 578, "target": 278, "value": 4}, {"source": 578, "target": 303, "value": 10}, {"source": 578, "target": 321, "value": 8}, {"source": 578, "target": 353, "value": 10}, {"source": 578, "target": 365, "value": 10}, {"source": 578, "target": 375, "value": 8}, {"source": 578, "target": 378, "value": 4}, {"source": 578, "target": 399, "value": 8}, {"source": 578, "target": 413, "value": 8}, {"source": 578, "target": 425, "value": 8}, {"source": 578, "target": 428, "value": 4}, {"source": 578, "target": 437, "value": 8}, {"source": 578, "target": 453, "value": 9}, {"source": 578, "target": 503, "value": 8}, {"source": 578, "target": 513, "value": 8}, {"source": 578, "target": 525, "value": 7}, {"source": 578, "target": 528, "value": 3}, {"source": 578, "target": 545, "value": 8}, {"source": 578, "target": 549, "value": 8}, {"source": 578, "target": 563, "value": 9}, {"source": 578, "target": 575, "value": 8}, {"source": 579, "target": 221, "value": 11}, {"source": 579, "target": 233, "value": 12}, {"source": 579, "target": 279, "value": 4}, {"source": 579, "target": 317, "value": 8}, {"source": 579, "target": 333, "value": 11}, {"source": 579, "target": 383, "value": 10}, {"source": 579, "target": 429, "value": 4}, {"source": 579, "target": 483, "value": 10}, {"source": 579, "target": 531, "value": 9}, {"source": 579, "target": 533, "value": 9}, {"source": 581, "target": 221, "value": 11}, {"source": 581, "target": 231, "value": 4}, {"source": 581, "target": 243, "value": 8}, {"source": 581, "target": 273, "value": 11}, {"source": 581, "target": 281, "value": 2}, {"source": 581, "target": 291, "value": 8}, {"source": 581, "target": 293, "value": 8}, {"source": 581, "target": 333, "value": 9}, {"source": 581, "target": 341, "value": 8}, {"source": 581, "target": 369, "value": 8}, {"source": 581, "target": 377, "value": 8}, {"source": 581, "target": 381, "value": 2}, {"source": 581, "target": 393, "value": 8}, {"source": 581, "target": 398, "value": 9}, {"source": 581, "target": 429, "value": 8}, {"source": 581, "target": 431, "value": 2}, {"source": 581, "target": 441, "value": 9}, {"source": 581, "target": 443, "value": 8}, {"source": 581, "target": 489, "value": 7}, {"source": 581, "target": 491, "value": 8}, {"source": 581, "target": 519, "value": 8}, {"source": 581, "target": 531, "value": 3}, {"source": 581, "target": 533, "value": 4}, {"source": 581, "target": 543, "value": 8}, {"source": 585, "target": 243, "value": 9}, {"source": 585, "target": 273, "value": 8}, {"source": 585, "target": 281, "value": 8}, {"source": 585, "target": 285, "value": 2}, {"source": 585, "target": 293, "value": 2}, {"source": 585, "target": 333, "value": 8}, {"source": 585, "target": 335, "value": 4}, {"source": 585, "target": 347, "value": 4}, {"source": 585, "target": 377, "value": 10}, {"source": 585, "target": 381, "value": 8}, {"source": 585, "target": 393, "value": 9}, {"source": 585, "target": 398, "value": 8}, {"source": 585, "target": 431, "value": 8}, {"source": 585, "target": 435, "value": 4}, {"source": 585, "target": 437, "value": 4}, {"source": 585, "target": 443, "value": 9}, {"source": 585, "target": 473, "value": 8}, {"source": 585, "target": 485, "value": 2}, {"source": 585, "target": 489, "value": 9}, {"source": 585, "target": 497, "value": 4}, {"source": 585, "target": 525, "value": 9}, {"source": 585, "target": 531, "value": 7}, {"source": 585, "target": 533, "value": 7}, {"source": 585, "target": 543, "value": 9}, {"source": 585, "target": 573, "value": 8}, {"source": 585, "target": 581, "value": 8}, {"source": 587, "target": 225, "value": 9}, {"source": 587, "target": 263, "value": 9}, {"source": 587, "target": 285, "value": 9}, {"source": 587, "target": 287, "value": 4}, {"source": 587, "target": 291, "value": 8}, {"source": 587, "target": 335, "value": 9}, {"source": 587, "target": 341, "value": 8}, {"source": 587, "target": 363, "value": 9}, {"source": 587, "target": 381, "value": 10}, {"source": 587, "target": 413, "value": 4}, {"source": 587, "target": 425, "value": 8}, {"source": 587, "target": 429, "value": 4}, {"source": 587, "target": 435, "value": 9}, {"source": 587, "target": 437, "value": 4}, {"source": 587, "target": 441, "value": 8}, {"source": 587, "target": 461, "value": 8}, {"source": 587, "target": 485, "value": 9}, {"source": 587, "target": 489, "value": 9}, {"source": 587, "target": 491, "value": 8}, {"source": 587, "target": 513, "value": 4}, {"source": 587, "target": 525, "value": 7}, {"source": 587, "target": 533, "value": 9}, {"source": 587, "target": 563, "value": 8}, {"source": 587, "target": 581, "value": 8}, {"source": 587, "target": 585, "value": 8}, {"source": 591, "target": 291, "value": 4}, {"source": 591, "target": 293, "value": 10}, {"source": 591, "target": 341, "value": 4}, {"source": 591, "target": 365, "value": 8}, {"source": 591, "target": 381, "value": 10}, {"source": 591, "target": 429, "value": 8}, {"source": 591, "target": 441, "value": 4}, {"source": 591, "target": 491, "value": 4}, {"source": 591, "target": 533, "value": 9}, {"source": 591, "target": 581, "value": 8}, {"source": 591, "target": 587, "value": 8}, {"source": 593, "target": 243, "value": 1}, {"source": 593, "target": 245, "value": 10}, {"source": 593, "target": 251, "value": 8}, {"source": 593, "target": 255, "value": 8}, {"source": 593, "target": 281, "value": 8}, {"source": 593, "target": 293, "value": 1}, {"source": 593, "target": 305, "value": 8}, {"source": 593, "target": 317, "value": 2}, {"source": 593, "target": 333, "value": 10}, {"source": 593, "target": 335, "value": 9}, {"source": 593, "target": 339, "value": 9}, {"source": 593, "target": 377, "value": 8}, {"source": 593, "target": 381, "value": 8}, {"source": 593, "target": 393, "value": 1}, {"source": 593, "target": 398, "value": 9}, {"source": 593, "target": 405, "value": 8}, {"source": 593, "target": 429, "value": 8}, {"source": 593, "target": 431, "value": 9}, {"source": 593, "target": 443, "value": 1}, {"source": 593, "target": 455, "value": 8}, {"source": 593, "target": 467, "value": 4}, {"source": 593, "target": 489, "value": 2}, {"source": 593, "target": 533, "value": 8}, {"source": 593, "target": 543, "value": 1}, {"source": 593, "target": 545, "value": 9}, {"source": 593, "target": 555, "value": 8}, {"source": 593, "target": 581, "value": 8}, {"source": 593, "target": 585, "value": 8}, {"source": 603, "target": 227, "value": 8}, {"source": 603, "target": 228, "value": 8}, {"source": 603, "target": 249, "value": 9}, {"source": 603, "target": 278, "value": 8}, {"source": 603, "target": 279, "value": 9}, {"source": 603, "target": 293, "value": 10}, {"source": 603, "target": 303, "value": 2}, {"source": 603, "target": 341, "value": 8}, {"source": 603, "target": 353, "value": 1}, {"source": 603, "target": 377, "value": 8}, {"source": 603, "target": 378, "value": 8}, {"source": 603, "target": 399, "value": 9}, {"source": 603, "target": 405, "value": 9}, {"source": 603, "target": 428, "value": 8}, {"source": 603, "target": 437, "value": 8}, {"source": 603, "target": 441, "value": 8}, {"source": 603, "target": 453, "value": 1}, {"source": 603, "target": 503, "value": 2}, {"source": 603, "target": 527, "value": 8}, {"source": 603, "target": 528, "value": 8}, {"source": 603, "target": 545, "value": 9}, {"source": 603, "target": 549, "value": 9}, {"source": 603, "target": 578, "value": 8}, {"source": 605, "target": 215, "value": 8}, {"source": 605, "target": 231, "value": 8}, {"source": 605, "target": 245, "value": 10}, {"source": 605, "target": 251, "value": 8}, {"source": 605, "target": 255, "value": 2}, {"source": 605, "target": 281, "value": 8}, {"source": 605, "target": 293, "value": 8}, {"source": 605, "target": 305, "value": 2}, {"source": 605, "target": 315, "value": 8}, {"source": 605, "target": 317, "value": 8}, {"source": 605, "target": 353, "value": 8}, {"source": 605, "target": 363, "value": 8}, {"source": 605, "target": 365, "value": 8}, {"source": 605, "target": 377, "value": 11}, {"source": 605, "target": 381, "value": 8}, {"source": 605, "target": 393, "value": 8}, {"source": 605, "target": 405, "value": 2}, {"source": 605, "target": 429, "value": 8}, {"source": 605, "target": 431, "value": 8}, {"source": 605, "target": 453, "value": 4}, {"source": 605, "target": 455, "value": 4}, {"source": 605, "target": 461, "value": 8}, {"source": 605, "target": 465, "value": 9}, {"source": 605, "target": 467, "value": 8}, {"source": 605, "target": 515, "value": 8}, {"source": 605, "target": 531, "value": 8}, {"source": 605, "target": 555, "value": 3}, {"source": 605, "target": 557, "value": 8}, {"source": 605, "target": 581, "value": 8}, {"source": 605, "target": 593, "value": 8}, {"source": 609, "target": 221, "value": 8}, {"source": 609, "target": 261, "value": 10}, {"source": 609, "target": 263, "value": 12}, {"source": 609, "target": 309, "value": 2}, {"source": 609, "target": 321, "value": 8}, {"source": 609, "target": 333, "value": 8}, {"source": 609, "target": 351, "value": 10}, {"source": 609, "target": 363, "value": 11}, {"source": 609, "target": 371, "value": 8}, {"source": 609, "target": 405, "value": 8}, {"source": 609, "target": 413, "value": 10}, {"source": 609, "target": 459, "value": 4}, {"source": 609, "target": 461, "value": 4}, {"source": 609, "target": 471, "value": 8}, {"source": 609, "target": 497, "value": 4}, {"source": 609, "target": 513, "value": 10}, {"source": 609, "target": 521, "value": 8}, {"source": 609, "target": 549, "value": 8}, {"source": 609, "target": 563, "value": 9}, {"source": 611, "target": 219, "value": 8}, {"source": 611, "target": 248, "value": 8}, {"source": 611, "target": 249, "value": 9}, {"source": 611, "target": 257, "value": 8}, {"source": 611, "target": 261, "value": 4}, {"source": 611, "target": 273, "value": 9}, {"source": 611, "target": 285, "value": 8}, {"source": 611, "target": 309, "value": 8}, {"source": 611, "target": 311, "value": 4}, {"source": 611, "target": 323, "value": 9}, {"source": 611, "target": 348, "value": 8}, {"source": 611, "target": 363, "value": 10}, {"source": 611, "target": 369, "value": 9}, {"source": 611, "target": 398, "value": 8}, {"source": 611, "target": 411, "value": 4}, {"source": 611, "target": 423, "value": 9}, {"source": 611, "target": 461, "value": 4}, {"source": 611, "target": 473, "value": 8}, {"source": 611, "target": 498, "value": 8}, {"source": 611, "target": 501, "value": 9}, {"source": 611, "target": 519, "value": 8}, {"source": 611, "target": 548, "value": 8}, {"source": 611, "target": 549, "value": 7}, {"source": 611, "target": 557, "value": 8}, {"source": 611, "target": 561, "value": 3}, {"source": 611, "target": 573, "value": 8}, {"source": 615, "target": 215, "value": 4}, {"source": 615, "target": 219, "value": 8}, {"source": 615, "target": 248, "value": 9}, {"source": 615, "target": 257, "value": 9}, {"source": 615, "target": 263, "value": 8}, {"source": 615, "target": 273, "value": 11}, {"source": 615, "target": 309, "value": 10}, {"source": 615, "target": 315, "value": 4}, {"source": 615, "target": 323, "value": 11}, {"source": 615, "target": 348, "value": 9}, {"source": 615, "target": 353, "value": 8}, {"source": 615, "target": 363, "value": 9}, {"source": 615, "target": 365, "value": 4}, {"source": 615, "target": 369, "value": 8}, {"source": 615, "target": 398, "value": 9}, {"source": 615, "target": 405, "value": 10}, {"source": 615, "target": 413, "value": 9}, {"source": 615, "target": 423, "value": 10}, {"source": 615, "target": 453, "value": 4}, {"source": 615, "target": 461, "value": 8}, {"source": 615, "target": 465, "value": 4}, {"source": 615, "target": 473, "value": 10}, {"source": 615, "target": 498, "value": 8}, {"source": 615, "target": 513, "value": 9}, {"source": 615, "target": 515, "value": 4}, {"source": 615, "target": 519, "value": 8}, {"source": 615, "target": 548, "value": 8}, {"source": 615, "target": 557, "value": 8}, {"source": 615, "target": 563, "value": 8}, {"source": 615, "target": 573, "value": 9}, {"source": 615, "target": 605, "value": 8}, {"source": 615, "target": 611, "value": 8}, {"source": 617, "target": 221, "value": 10}, {"source": 617, "target": 233, "value": 11}, {"source": 617, "target": 243, "value": 9}, {"source": 617, "target": 255, "value": 10}, {"source": 617, "target": 279, "value": 8}, {"source": 617, "target": 293, "value": 4}, {"source": 617, "target": 305, "value": 8}, {"source": 617, "target": 317, "value": 2}, {"source": 617, "target": 333, "value": 10}, {"source": 617, "target": 363, "value": 8}, {"source": 617, "target": 383, "value": 10}, {"source": 617, "target": 393, "value": 4}, {"source": 617, "target": 405, "value": 8}, {"source": 617, "target": 429, "value": 8}, {"source": 617, "target": 443, "value": 9}, {"source": 617, "target": 467, "value": 4}, {"source": 617, "target": 483, "value": 9}, {"source": 617, "target": 489, "value": 10}, {"source": 617, "target": 533, "value": 9}, {"source": 617, "target": 543, "value": 8}, {"source": 617, "target": 557, "value": 8}, {"source": 617, "target": 579, "value": 8}, {"source": 617, "target": 593, "value": 4}, {"source": 617, "target": 605, "value": 8}, {"source": 621, "target": 221, "value": 1}, {"source": 621, "target": 231, "value": 8}, {"source": 621, "target": 233, "value": 8}, {"source": 621, "target": 261, "value": 10}, {"source": 621, "target": 273, "value": 3}, {"source": 621, "target": 279, "value": 8}, {"source": 621, "target": 281, "value": 8}, {"source": 621, "target": 293, "value": 12}, {"source": 621, "target": 309, "value": 8}, {"source": 621, "target": 321, "value": 2}, {"source": 621, "target": 323, "value": 9}, {"source": 621, "target": 333, "value": 8}, {"source": 621, "target": 347, "value": 8}, {"source": 621, "target": 369, "value": 8}, {"source": 621, "target": 371, "value": 4}, {"source": 621, "target": 377, "value": 8}, {"source": 621, "target": 381, "value": 8}, {"source": 621, "target": 383, "value": 8}, {"source": 621, "target": 423, "value": 8}, {"source": 621, "target": 431, "value": 8}, {"source": 621, "target": 461, "value": 9}, {"source": 621, "target": 471, "value": 4}, {"source": 621, "target": 473, "value": 4}, {"source": 621, "target": 483, "value": 8}, {"source": 621, "target": 497, "value": 8}, {"source": 621, "target": 519, "value": 8}, {"source": 621, "target": 521, "value": 2}, {"source": 621, "target": 531, "value": 8}, {"source": 621, "target": 533, "value": 8}, {"source": 621, "target": 573, "value": 2}, {"source": 621, "target": 581, "value": 8}, {"source": 621, "target": 609, "value": 8}, {"source": 623, "target": 219, "value": 9}, {"source": 623, "target": 221, "value": 8}, {"source": 623, "target": 248, "value": 8}, {"source": 623, "target": 257, "value": 8}, {"source": 623, "target": 261, "value": 9}, {"source": 623, "target": 273, "value": 1}, {"source": 623, "target": 309, "value": 8}, {"source": 623, "target": 323, "value": 2}, {"source": 623, "target": 347, "value": 8}, {"source": 623, "target": 348, "value": 8}, {"source": 623, "target": 369, "value": 9}, {"source": 623, "target": 398, "value": 8}, {"source": 623, "target": 413, "value": 10}, {"source": 623, "target": 423, "value": 2}, {"source": 623, "target": 461, "value": 8}, {"source": 623, "target": 473, "value": 1}, {"source": 623, "target": 497, "value": 8}, {"source": 623, "target": 498, "value": 8}, {"source": 623, "target": 519, "value": 9}, {"source": 623, "target": 545, "value": 10}, {"source": 623, "target": 548, "value": 8}, {"source": 623, "target": 557, "value": 8}, {"source": 623, "target": 561, "value": 7}, {"source": 623, "target": 573, "value": 1}, {"source": 623, "target": 611, "value": 7}, {"source": 623, "target": 615, "value": 8}, {"source": 623, "target": 621, "value": 7}, {"source": 633, "target": 221, "value": 4}, {"source": 633, "target": 233, "value": 0}, {"source": 633, "target": 245, "value": 8}, {"source": 633, "target": 248, "value": 8}, {"source": 633, "target": 257, "value": 4}, {"source": 633, "target": 279, "value": 4}, {"source": 633, "target": 285, "value": 10}, {"source": 633, "target": 317, "value": 8}, {"source": 633, "target": 321, "value": 4}, {"source": 633, "target": 333, "value": 1}, {"source": 633, "target": 335, "value": 10}, {"source": 633, "target": 345, "value": 8}, {"source": 633, "target": 348, "value": 8}, {"source": 633, "target": 381, "value": 4}, {"source": 633, "target": 383, "value": 1}, {"source": 633, "target": 395, "value": 8}, {"source": 633, "target": 398, "value": 8}, {"source": 633, "target": 405, "value": 11}, {"source": 633, "target": 407, "value": 4}, {"source": 633, "target": 429, "value": 9}, {"source": 633, "target": 483, "value": 1}, {"source": 633, "target": 485, "value": 9}, {"source": 633, "target": 495, "value": 8}, {"source": 633, "target": 498, "value": 8}, {"source": 633, "target": 521, "value": 8}, {"source": 633, "target": 533, "value": 1}, {"source": 633, "target": 545, "value": 8}, {"source": 633, "target": 548, "value": 8}, {"source": 633, "target": 557, "value": 2}, {"source": 633, "target": 573, "value": 8}, {"source": 633, "target": 579, "value": 9}, {"source": 633, "target": 617, "value": 8}, {"source": 633, "target": 621, "value": 8}, {"source": 635, "target": 273, "value": 9}, {"source": 635, "target": 285, "value": 4}, {"source": 635, "target": 335, "value": 4}, {"source": 635, "target": 377, "value": 10}, {"source": 635, "target": 435, "value": 4}, {"source": 635, "target": 473, "value": 8}, {"source": 635, "target": 485, "value": 4}, {"source": 635, "target": 525, "value": 9}, {"source": 635, "target": 531, "value": 8}, {"source": 635, "target": 573, "value": 8}, {"source": 635, "target": 585, "value": 3}, {"source": 635, "target": 587, "value": 9}, {"source": 639, "target": 243, "value": 12}, {"source": 639, "target": 293, "value": 11}, {"source": 639, "target": 335, "value": 8}, {"source": 639, "target": 339, "value": 4}, {"source": 639, "target": 377, "value": 8}, {"source": 639, "target": 393, "value": 11}, {"source": 639, "target": 413, "value": 8}, {"source": 639, "target": 429, "value": 10}, {"source": 639, "target": 443, "value": 10}, {"source": 639, "target": 461, "value": 10}, {"source": 639, "target": 489, "value": 4}, {"source": 639, "target": 543, "value": 10}, {"source": 639, "target": 593, "value": 9}, {"source": 641, "target": 291, "value": 4}, {"source": 641, "target": 293, "value": 5}, {"source": 641, "target": 303, "value": 8}, {"source": 641, "target": 341, "value": 2}, {"source": 641, "target": 353, "value": 8}, {"source": 641, "target": 365, "value": 8}, {"source": 641, "target": 381, "value": 10}, {"source": 641, "target": 429, "value": 8}, {"source": 641, "target": 441, "value": 2}, {"source": 641, "target": 453, "value": 8}, {"source": 641, "target": 491, "value": 4}, {"source": 641, "target": 503, "value": 8}, {"source": 641, "target": 533, "value": 9}, {"source": 641, "target": 581, "value": 8}, {"source": 641, "target": 587, "value": 7}, {"source": 641, "target": 591, "value": 3}, {"source": 641, "target": 603, "value": 8}, {"source": 645, "target": 233, "value": 4}, {"source": 645, "target": 245, "value": 2}, {"source": 645, "target": 248, "value": 8}, {"source": 645, "target": 251, "value": 8}, {"source": 645, "target": 255, "value": 8}, {"source": 645, "target": 257, "value": 8}, {"source": 645, "target": 285, "value": 10}, {"source": 645, "target": 293, "value": 8}, {"source": 645, "target": 305, "value": 8}, {"source": 645, "target": 333, "value": 8}, {"source": 645, "target": 345, "value": 2}, {"source": 645, "target": 348, "value": 8}, {"source": 645, "target": 393, "value": 8}, {"source": 645, "target": 395, "value": 4}, {"source": 645, "target": 398, "value": 8}, {"source": 645, "target": 405, "value": 8}, {"source": 645, "target": 407, "value": 8}, {"source": 645, "target": 455, "value": 8}, {"source": 645, "target": 485, "value": 9}, {"source": 645, "target": 495, "value": 4}, {"source": 645, "target": 498, "value": 8}, {"source": 645, "target": 503, "value": 8}, {"source": 645, "target": 533, "value": 8}, {"source": 645, "target": 545, "value": 2}, {"source": 645, "target": 548, "value": 8}, {"source": 645, "target": 555, "value": 8}, {"source": 645, "target": 557, "value": 8}, {"source": 645, "target": 593, "value": 7}, {"source": 645, "target": 605, "value": 8}, {"source": 645, "target": 633, "value": 8}, {"source": 647, "target": 221, "value": 9}, {"source": 647, "target": 273, "value": 4}, {"source": 647, "target": 285, "value": 9}, {"source": 647, "target": 293, "value": 8}, {"source": 647, "target": 323, "value": 9}, {"source": 647, "target": 347, "value": 4}, {"source": 647, "target": 423, "value": 9}, {"source": 647, "target": 437, "value": 9}, {"source": 647, "target": 473, "value": 4}, {"source": 647, "target": 485, "value": 8}, {"source": 647, "target": 497, "value": 4}, {"source": 647, "target": 573, "value": 4}, {"source": 647, "target": 585, "value": 4}, {"source": 647, "target": 621, "value": 8}, {"source": 647, "target": 623, "value": 8}, {"source": 648, "target": 219, "value": 8}, {"source": 648, "target": 233, "value": 4}, {"source": 648, "target": 245, "value": 8}, {"source": 648, "target": 248, "value": 4}, {"source": 648, "target": 257, "value": 8}, {"source": 648, "target": 273, "value": 11}, {"source": 648, "target": 285, "value": 10}, {"source": 648, "target": 309, "value": 9}, {"source": 648, "target": 323, "value": 10}, {"source": 648, "target": 333, "value": 9}, {"source": 648, "target": 345, "value": 8}, {"source": 648, "target": 348, "value": 4}, {"source": 648, "target": 369, "value": 8}, {"source": 648, "target": 395, "value": 8}, {"source": 648, "target": 398, "value": 4}, {"source": 648, "target": 423, "value": 10}, {"source": 648, "target": 473, "value": 9}, {"source": 648, "target": 485, "value": 9}, {"source": 648, "target": 495, "value": 8}, {"source": 648, "target": 498, "value": 4}, {"source": 648, "target": 519, "value": 8}, {"source": 648, "target": 533, "value": 8}, {"source": 648, "target": 545, "value": 8}, {"source": 648, "target": 548, "value": 4}, {"source": 648, "target": 557, "value": 7}, {"source": 648, "target": 573, "value": 8}, {"source": 648, "target": 611, "value": 8}, {"source": 648, "target": 615, "value": 8}, {"source": 648, "target": 623, "value": 8}, {"source": 648, "target": 633, "value": 8}, {"source": 648, "target": 645, "value": 8}, {"source": 651, "target": 251, "value": 4}, {"source": 651, "target": 263, "value": 8}, {"source": 651, "target": 341, "value": 10}, {"source": 651, "target": 351, "value": 4}, {"source": 651, "target": 363, "value": 8}, {"source": 651, "target": 401, "value": 2}, {"source": 651, "target": 413, "value": 8}, {"source": 651, "target": 453, "value": 8}, {"source": 651, "target": 455, "value": 9}, {"source": 651, "target": 489, "value": 8}, {"source": 651, "target": 501, "value": 2}, {"source": 651, "target": 503, "value": 9}, {"source": 651, "target": 513, "value": 8}, {"source": 651, "target": 525, "value": 8}, {"source": 651, "target": 551, "value": 4}, {"source": 651, "target": 563, "value": 8}, {"source": 651, "target": 573, "value": 9}, {"source": 653, "target": 215, "value": 8}, {"source": 653, "target": 227, "value": 4}, {"source": 653, "target": 228, "value": 8}, {"source": 653, "target": 249, "value": 8}, {"source": 653, "target": 263, "value": 8}, {"source": 653, "target": 278, "value": 8}, {"source": 653, "target": 279, "value": 3}, {"source": 653, "target": 293, "value": 10}, {"source": 653, "target": 303, "value": 1}, {"source": 653, "target": 315, "value": 8}, {"source": 653, "target": 341, "value": 8}, {"source": 653, "target": 353, "value": 1}, {"source": 653, "target": 363, "value": 8}, {"source": 653, "target": 365, "value": 8}, {"source": 653, "target": 377, "value": 4}, {"source": 653, "target": 378, "value": 8}, {"source": 653, "target": 399, "value": 9}, {"source": 653, "target": 401, "value": 8}, {"source": 653, "target": 405, "value": 4}, {"source": 653, "target": 413, "value": 8}, {"source": 653, "target": 428, "value": 8}, {"source": 653, "target": 429, "value": 8}, {"source": 653, "target": 437, "value": 8}, {"source": 653, "target": 441, "value": 8}, {"source": 653, "target": 453, "value": 0}, {"source": 653, "target": 455, "value": 8}, {"source": 653, "target": 461, "value": 8}, {"source": 653, "target": 465, "value": 8}, {"source": 653, "target": 501, "value": 8}, {"source": 653, "target": 503, "value": 1}, {"source": 653, "target": 513, "value": 8}, {"source": 653, "target": 515, "value": 8}, {"source": 653, "target": 527, "value": 4}, {"source": 653, "target": 528, "value": 8}, {"source": 653, "target": 531, "value": 8}, {"source": 653, "target": 545, "value": 9}, {"source": 653, "target": 549, "value": 9}, {"source": 653, "target": 563, "value": 8}, {"source": 653, "target": 578, "value": 8}, {"source": 653, "target": 579, "value": 8}, {"source": 653, "target": 603, "value": 1}, {"source": 653, "target": 605, "value": 8}, {"source": 653, "target": 615, "value": 8}, {"source": 653, "target": 641, "value": 8}, {"source": 653, "target": 651, "value": 7}, {"source": 663, "target": 221, "value": 8}, {"source": 663, "target": 261, "value": 9}, {"source": 663, "target": 263, "value": 2}, {"source": 663, "target": 287, "value": 8}, {"source": 663, "target": 309, "value": 4}, {"source": 663, "target": 321, "value": 8}, {"source": 663, "target": 351, "value": 9}, {"source": 663, "target": 363, "value": 2}, {"source": 663, "target": 371, "value": 8}, {"source": 663, "target": 401, "value": 8}, {"source": 663, "target": 405, "value": 8}, {"source": 663, "target": 413, "value": 1}, {"source": 663, "target": 437, "value": 8}, {"source": 663, "target": 453, "value": 9}, {"source": 663, "target": 455, "value": 10}, {"source": 663, "target": 459, "value": 9}, {"source": 663, "target": 461, "value": 4}, {"source": 663, "target": 471, "value": 8}, {"source": 663, "target": 489, "value": 8}, {"source": 663, "target": 497, "value": 4}, {"source": 663, "target": 501, "value": 8}, {"source": 663, "target": 513, "value": 1}, {"source": 663, "target": 521, "value": 9}, {"source": 663, "target": 549, "value": 8}, {"source": 663, "target": 563, "value": 2}, {"source": 663, "target": 587, "value": 8}, {"source": 663, "target": 609, "value": 4}, {"source": 663, "target": 615, "value": 8}, {"source": 663, "target": 621, "value": 8}, {"source": 663, "target": 651, "value": 8}, {"source": 663, "target": 653, "value": 8}, {"source": 665, "target": 215, "value": 4}, {"source": 665, "target": 227, "value": 8}, {"source": 665, "target": 315, "value": 4}, {"source": 665, "target": 317, "value": 10}, {"source": 665, "target": 353, "value": 8}, {"source": 665, "target": 365, "value": 2}, {"source": 665, "target": 377, "value": 8}, {"source": 665, "target": 405, "value": 10}, {"source": 665, "target": 453, "value": 4}, {"source": 665, "target": 461, "value": 8}, {"source": 665, "target": 465, "value": 2}, {"source": 665, "target": 515, "value": 4}, {"source": 665, "target": 527, "value": 8}, {"source": 665, "target": 573, "value": 7}, {"source": 665, "target": 605, "value": 8}, {"source": 665, "target": 615, "value": 3}, {"source": 665, "target": 653, "value": 8}, {"source": 669, "target": 219, "value": 4}, {"source": 669, "target": 221, "value": 5}, {"source": 669, "target": 231, "value": 8}, {"source": 669, "target": 233, "value": 10}, {"source": 669, "target": 245, "value": 8}, {"source": 669, "target": 248, "value": 9}, {"source": 669, "target": 257, "value": 9}, {"source": 669, "target": 273, "value": 5}, {"source": 669, "target": 279, "value": 8}, {"source": 669, "target": 281, "value": 8}, {"source": 669, "target": 309, "value": 10}, {"source": 669, "target": 317, "value": 8}, {"source": 669, "target": 321, "value": 10}, {"source": 669, "target": 323, "value": 11}, {"source": 669, "target": 333, "value": 10}, {"source": 669, "target": 345, "value": 8}, {"source": 669, "target": 348, "value": 9}, {"source": 669, "target": 369, "value": 2}, {"source": 669, "target": 377, "value": 8}, {"source": 669, "target": 381, "value": 8}, {"source": 669, "target": 383, "value": 9}, {"source": 669, "target": 395, "value": 8}, {"source": 669, "target": 398, "value": 9}, {"source": 669, "target": 423, "value": 10}, {"source": 669, "target": 429, "value": 8}, {"source": 669, "target": 431, "value": 8}, {"source": 669, "target": 473, "value": 10}, {"source": 669, "target": 483, "value": 9}, {"source": 669, "target": 495, "value": 8}, {"source": 669, "target": 498, "value": 8}, {"source": 669, "target": 519, "value": 2}, {"source": 669, "target": 531, "value": 8}, {"source": 669, "target": 533, "value": 8}, {"source": 669, "target": 545, "value": 8}, {"source": 669, "target": 548, "value": 8}, {"source": 669, "target": 557, "value": 8}, {"source": 669, "target": 573, "value": 9}, {"source": 669, "target": 579, "value": 8}, {"source": 669, "target": 581, "value": 8}, {"source": 669, "target": 611, "value": 9}, {"source": 669, "target": 615, "value": 7}, {"source": 669, "target": 617, "value": 7}, {"source": 669, "target": 621, "value": 8}, {"source": 669, "target": 623, "value": 9}, {"source": 669, "target": 633, "value": 8}, {"source": 669, "target": 645, "value": 8}, {"source": 669, "target": 648, "value": 7}, {"source": 671, "target": 219, "value": 8}, {"source": 671, "target": 221, "value": 4}, {"source": 671, "target": 225, "value": 8}, {"source": 671, "target": 228, "value": 8}, {"source": 671, "target": 261, "value": 10}, {"source": 671, "target": 275, "value": 8}, {"source": 671, "target": 278, "value": 8}, {"source": 671, "target": 293, "value": 12}, {"source": 671, "target": 309, "value": 8}, {"source": 671, "target": 321, "value": 2}, {"source": 671, "target": 365, "value": 10}, {"source": 671, "target": 369, "value": 8}, {"source": 671, "target": 371, "value": 4}, {"source": 671, "target": 375, "value": 8}, {"source": 671, "target": 378, "value": 8}, {"source": 671, "target": 413, "value": 8}, {"source": 671, "target": 425, "value": 8}, {"source": 671, "target": 428, "value": 8}, {"source": 671, "target": 461, "value": 9}, {"source": 671, "target": 471, "value": 4}, {"source": 671, "target": 513, "value": 8}, {"source": 671, "target": 519, "value": 8}, {"source": 671, "target": 521, "value": 4}, {"source": 671, "target": 525, "value": 8}, {"source": 671, "target": 528, "value": 8}, {"source": 671, "target": 563, "value": 9}, {"source": 671, "target": 573, "value": 9}, {"source": 671, "target": 575, "value": 7}, {"source": 671, "target": 578, "value": 8}, {"source": 671, "target": 609, "value": 7}, {"source": 671, "target": 621, "value": 3}, {"source": 671, "target": 663, "value": 8}, {"source": 671, "target": 669, "value": 7}, {"source": 675, "target": 225, "value": 4}, {"source": 675, "target": 228, "value": 8}, {"source": 675, "target": 275, "value": 4}, {"source": 675, "target": 278, "value": 8}, {"source": 675, "target": 321, "value": 8}, {"source": 675, "target": 365, "value": 10}, {"source": 675, "target": 375, "value": 4}, {"source": 675, "target": 377, "value": 10}, {"source": 675, "target": 378, "value": 8}, {"source": 675, "target": 413, "value": 8}, {"source": 675, "target": 425, "value": 4}, {"source": 675, "target": 428, "value": 8}, {"source": 675, "target": 513, "value": 8}, {"source": 675, "target": 525, "value": 4}, {"source": 675, "target": 528, "value": 8}, {"source": 675, "target": 563, "value": 8}, {"source": 675, "target": 575, "value": 3}, {"source": 675, "target": 578, "value": 8}, {"source": 675, "target": 671, "value": 7}, {"source": 677, "target": 225, "value": 8}, {"source": 677, "target": 227, "value": 4}, {"source": 677, "target": 243, "value": 11}, {"source": 677, "target": 279, "value": 10}, {"source": 677, "target": 287, "value": 8}, {"source": 677, "target": 293, "value": 11}, {"source": 677, "target": 303, "value": 9}, {"source": 677, "target": 317, "value": 10}, {"source": 677, "target": 335, "value": 8}, {"source": 677, "target": 339, "value": 8}, {"source": 677, "target": 353, "value": 4}, {"source": 677, "target": 365, "value": 4}, {"source": 677, "target": 377, "value": 2}, {"source": 677, "target": 393, "value": 10}, {"source": 677, "target": 425, "value": 8}, {"source": 677, "target": 429, "value": 4}, {"source": 677, "target": 437, "value": 8}, {"source": 677, "target": 443, "value": 10}, {"source": 677, "target": 453, "value": 4}, {"source": 677, "target": 465, "value": 8}, {"source": 677, "target": 489, "value": 8}, {"source": 677, "target": 503, "value": 9}, {"source": 677, "target": 525, "value": 8}, {"source": 677, "target": 527, "value": 4}, {"source": 677, "target": 543, "value": 9}, {"source": 677, "target": 573, "value": 8}, {"source": 677, "target": 587, "value": 8}, {"source": 677, "target": 593, "value": 9}, {"source": 677, "target": 603, "value": 8}, {"source": 677, "target": 639, "value": 8}, {"source": 677, "target": 653, "value": 4}, {"source": 677, "target": 665, "value": 8}, {"source": 678, "target": 225, "value": 8}, {"source": 678, "target": 228, "value": 4}, {"source": 678, "target": 249, "value": 8}, {"source": 678, "target": 261, "value": 8}, {"source": 678, "target": 273, "value": 8}, {"source": 678, "target": 275, "value": 8}, {"source": 678, "target": 278, "value": 4}, {"source": 678, "target": 303, "value": 10}, {"source": 678, "target": 321, "value": 8}, {"source": 678, "target": 323, "value": 8}, {"source": 678, "target": 353, "value": 10}, {"source": 678, "target": 365, "value": 10}, {"source": 678, "target": 375, "value": 8}, {"source": 678, "target": 378, "value": 4}, {"source": 678, "target": 399, "value": 8}, {"source": 678, "target": 413, "value": 4}, {"source": 678, "target": 423, "value": 8}, {"source": 678, "target": 425, "value": 8}, {"source": 678, "target": 428, "value": 4}, {"source": 678, "target": 437, "value": 8}, {"source": 678, "target": 453, "value": 9}, {"source": 678, "target": 461, "value": 8}, {"source": 678, "target": 473, "value": 8}, {"source": 678, "target": 503, "value": 9}, {"source": 678, "target": 513, "value": 8}, {"source": 678, "target": 525, "value": 8}, {"source": 678, "target": 528, "value": 4}, {"source": 678, "target": 545, "value": 8}, {"source": 678, "target": 549, "value": 8}, {"source": 678, "target": 561, "value": 8}, {"source": 678, "target": 563, "value": 9}, {"source": 678, "target": 573, "value": 8}, {"source": 678, "target": 575, "value": 8}, {"source": 678, "target": 578, "value": 4}, {"source": 678, "target": 603, "value": 8}, {"source": 678, "target": 623, "value": 8}, {"source": 678, "target": 653, "value": 8}, {"source": 678, "target": 671, "value": 7}, {"source": 678, "target": 675, "value": 8}, {"source": 681, "target": 221, "value": 11}, {"source": 681, "target": 231, "value": 4}, {"source": 681, "target": 243, "value": 8}, {"source": 681, "target": 273, "value": 11}, {"source": 681, "target": 281, "value": 2}, {"source": 681, "target": 293, "value": 8}, {"source": 681, "target": 333, "value": 10}, {"source": 681, "target": 369, "value": 8}, {"source": 681, "target": 377, "value": 8}, {"source": 681, "target": 381, "value": 2}, {"source": 681, "target": 393, "value": 8}, {"source": 681, "target": 398, "value": 9}, {"source": 681, "target": 431, "value": 2}, {"source": 681, "target": 443, "value": 8}, {"source": 681, "target": 489, "value": 8}, {"source": 681, "target": 519, "value": 9}, {"source": 681, "target": 531, "value": 4}, {"source": 681, "target": 533, "value": 9}, {"source": 681, "target": 543, "value": 8}, {"source": 681, "target": 581, "value": 2}, {"source": 681, "target": 585, "value": 9}, {"source": 681, "target": 593, "value": 8}, {"source": 681, "target": 605, "value": 8}, {"source": 681, "target": 621, "value": 8}, {"source": 681, "target": 669, "value": 8}, {"source": 683, "target": 221, "value": 4}, {"source": 683, "target": 233, "value": 1}, {"source": 683, "target": 257, "value": 8}, {"source": 683, "target": 279, "value": 4}, {"source": 683, "target": 317, "value": 8}, {"source": 683, "target": 321, "value": 4}, {"source": 683, "target": 333, "value": 1}, {"source": 683, "target": 335, "value": 10}, {"source": 683, "target": 381, "value": 8}, {"source": 683, "target": 383, "value": 2}, {"source": 683, "target": 405, "value": 11}, {"source": 683, "target": 407, "value": 8}, {"source": 683, "target": 429, "value": 9}, {"source": 683, "target": 483, "value": 2}, {"source": 683, "target": 521, "value": 8}, {"source": 683, "target": 533, "value": 1}, {"source": 683, "target": 557, "value": 4}, {"source": 683, "target": 573, "value": 9}, {"source": 683, "target": 579, "value": 9}, {"source": 683, "target": 617, "value": 8}, {"source": 683, "target": 621, "value": 7}, {"source": 683, "target": 633, "value": 1}, {"source": 683, "target": 669, "value": 8}, {"source": 693, "target": 243, "value": 1}, {"source": 693, "target": 245, "value": 10}, {"source": 693, "target": 251, "value": 8}, {"source": 693, "target": 255, "value": 8}, {"source": 693, "target": 281, "value": 9}, {"source": 693, "target": 293, "value": 0}, {"source": 693, "target": 303, "value": 8}, {"source": 693, "target": 305, "value": 8}, {"source": 693, "target": 317, "value": 2}, {"source": 693, "target": 333, "value": 10}, {"source": 693, "target": 335, "value": 8}, {"source": 693, "target": 339, "value": 8}, {"source": 693, "target": 341, "value": 8}, {"source": 693, "target": 353, "value": 8}, {"source": 693, "target": 377, "value": 8}, {"source": 693, "target": 381, "value": 8}, {"source": 693, "target": 393, "value": 1}, {"source": 693, "target": 398, "value": 9}, {"source": 693, "target": 405, "value": 8}, {"source": 693, "target": 429, "value": 8}, {"source": 693, "target": 431, "value": 9}, {"source": 693, "target": 441, "value": 8}, {"source": 693, "target": 443, "value": 1}, {"source": 693, "target": 453, "value": 8}, {"source": 693, "target": 455, "value": 8}, {"source": 693, "target": 467, "value": 4}, {"source": 693, "target": 489, "value": 2}, {"source": 693, "target": 503, "value": 8}, {"source": 693, "target": 533, "value": 9}, {"source": 693, "target": 543, "value": 1}, {"source": 693, "target": 545, "value": 9}, {"source": 693, "target": 555, "value": 8}, {"source": 693, "target": 581, "value": 8}, {"source": 693, "target": 585, "value": 9}, {"source": 693, "target": 593, "value": 1}, {"source": 693, "target": 603, "value": 8}, {"source": 693, "target": 605, "value": 8}, {"source": 693, "target": 617, "value": 4}, {"source": 693, "target": 639, "value": 8}, {"source": 693, "target": 641, "value": 7}, {"source": 693, "target": 645, "value": 8}, {"source": 693, "target": 653, "value": 8}, {"source": 693, "target": 677, "value": 8}, {"source": 693, "target": 681, "value": 8}, {"source": 695, "target": 233, "value": 4}, {"source": 695, "target": 245, "value": 2}, {"source": 695, "target": 248, "value": 8}, {"source": 695, "target": 257, "value": 8}, {"source": 695, "target": 285, "value": 10}, {"source": 695, "target": 333, "value": 8}, {"source": 695, "target": 345, "value": 2}, {"source": 695, "target": 348, "value": 8}, {"source": 695, "target": 395, "value": 4}, {"source": 695, "target": 398, "value": 8}, {"source": 695, "target": 407, "value": 8}, {"source": 695, "target": 485, "value": 9}, {"source": 695, "target": 495, "value": 4}, {"source": 695, "target": 498, "value": 8}, {"source": 695, "target": 503, "value": 8}, {"source": 695, "target": 533, "value": 8}, {"source": 695, "target": 545, "value": 2}, {"source": 695, "target": 548, "value": 8}, {"source": 695, "target": 557, "value": 8}, {"source": 695, "target": 633, "value": 7}, {"source": 695, "target": 645, "value": 2}, {"source": 695, "target": 648, "value": 8}, {"source": 695, "target": 669, "value": 8}, {"source": 698, "target": 219, "value": 8}, {"source": 698, "target": 233, "value": 4}, {"source": 698, "target": 245, "value": 8}, {"source": 698, "target": 248, "value": 4}, {"source": 698, "target": 257, "value": 8}, {"source": 698, "target": 273, "value": 11}, {"source": 698, "target": 285, "value": 11}, {"source": 698, "target": 309, "value": 10}, {"source": 698, "target": 323, "value": 10}, {"source": 698, "target": 333, "value": 9}, {"source": 698, "target": 345, "value": 8}, {"source": 698, "target": 348, "value": 4}, {"source": 698, "target": 369, "value": 8}, {"source": 698, "target": 395, "value": 8}, {"source": 698, "target": 398, "value": 4}, {"source": 698, "target": 423, "value": 10}, {"source": 698, "target": 473, "value": 9}, {"source": 698, "target": 485, "value": 10}, {"source": 698, "target": 495, "value": 8}, {"source": 698, "target": 498, "value": 4}, {"source": 698, "target": 519, "value": 8}, {"source": 698, "target": 533, "value": 8}, {"source": 698, "target": 545, "value": 8}, {"source": 698, "target": 548, "value": 4}, {"source": 698, "target": 557, "value": 8}, {"source": 698, "target": 573, "value": 9}, {"source": 698, "target": 611, "value": 8}, {"source": 698, "target": 615, "value": 8}, {"source": 698, "target": 623, "value": 8}, {"source": 698, "target": 633, "value": 8}, {"source": 698, "target": 645, "value": 7}, {"source": 698, "target": 648, "value": 3}, {"source": 698, "target": 669, "value": 8}, {"source": 698, "target": 695, "value": 8}, {"source": 699, "target": 221, "value": 8}, {"source": 699, "target": 228, "value": 9}, {"source": 699, "target": 249, "value": 4}, {"source": 699, "target": 251, "value": 10}, {"source": 699, "target": 273, "value": 4}, {"source": 699, "target": 278, "value": 9}, {"source": 699, "target": 293, "value": 10}, {"source": 699, "target": 303, "value": 4}, {"source": 699, "target": 321, "value": 11}, {"source": 699, "target": 323, "value": 8}, {"source": 699, "target": 341, "value": 8}, {"source": 699, "target": 347, "value": 8}, {"source": 699, "target": 353, "value": 4}, {"source": 699, "target": 378, "value": 9}, {"source": 699, "target": 399, "value": 4}, {"source": 699, "target": 423, "value": 8}, {"source": 699, "target": 428, "value": 9}, {"source": 699, "target": 437, "value": 8}, {"source": 699, "target": 441, "value": 8}, {"source": 699, "target": 453, "value": 4}, {"source": 699, "target": 473, "value": 4}, {"source": 699, "target": 497, "value": 8}, {"source": 699, "target": 503, "value": 4}, {"source": 699, "target": 528, "value": 8}, {"source": 699, "target": 545, "value": 8}, {"source": 699, "target": 549, "value": 4}, {"source": 699, "target": 573, "value": 2}, {"source": 699, "target": 578, "value": 8}, {"source": 699, "target": 603, "value": 4}, {"source": 699, "target": 621, "value": 8}, {"source": 699, "target": 623, "value": 8}, {"source": 699, "target": 641, "value": 8}, {"source": 699, "target": 647, "value": 8}, {"source": 699, "target": 653, "value": 4}, {"source": 699, "target": 678, "value": 7}, {"source": 699, "target": 693, "value": 8}, {"source": 701, "target": 227, "value": 8}, {"source": 701, "target": 249, "value": 8}, {"source": 701, "target": 251, "value": 4}, {"source": 701, "target": 261, "value": 8}, {"source": 701, "target": 263, "value": 8}, {"source": 701, "target": 279, "value": 10}, {"source": 701, "target": 303, "value": 9}, {"source": 701, "target": 311, "value": 8}, {"source": 701, "target": 341, "value": 10}, {"source": 701, "target": 351, "value": 4}, {"source": 701, "target": 353, "value": 4}, {"source": 701, "target": 363, "value": 8}, {"source": 701, "target": 377, "value": 8}, {"source": 701, "target": 401, "value": 2}, {"source": 701, "target": 411, "value": 8}, {"source": 701, "target": 413, "value": 8}, {"source": 701, "target": 453, "value": 3}, {"source": 701, "target": 455, "value": 9}, {"source": 701, "target": 461, "value": 8}, {"source": 701, "target": 489, "value": 8}, {"source": 701, "target": 501, "value": 2}, {"source": 701, "target": 503, "value": 4}, {"source": 701, "target": 513, "value": 8}, {"source": 701, "target": 525, "value": 8}, {"source": 701, "target": 527, "value": 8}, {"source": 701, "target": 549, "value": 8}, {"source": 701, "target": 551, "value": 4}, {"source": 701, "target": 561, "value": 8}, {"source": 701, "target": 563, "value": 8}, {"source": 701, "target": 573, "value": 10}, {"source": 701, "target": 603, "value": 8}, {"source": 701, "target": 611, "value": 8}, {"source": 701, "target": 651, "value": 2}, {"source": 701, "target": 653, "value": 2}, {"source": 701, "target": 663, "value": 8}, {"source": 701, "target": 677, "value": 8}, {"source": 705, "target": 245, "value": 11}, {"source": 705, "target": 251, "value": 8}, {"source": 705, "target": 255, "value": 2}, {"source": 705, "target": 293, "value": 9}, {"source": 705, "target": 305, "value": 2}, {"source": 705, "target": 317, "value": 8}, {"source": 705, "target": 363, "value": 8}, {"source": 705, "target": 377, "value": 11}, {"source": 705, "target": 393, "value": 8}, {"source": 705, "target": 405, "value": 2}, {"source": 705, "target": 429, "value": 8}, {"source": 705, "target": 455, "value": 4}, {"source": 705, "target": 467, "value": 8}, {"source": 705, "target": 555, "value": 4}, {"source": 705, "target": 557, "value": 9}, {"source": 705, "target": 593, "value": 8}, {"source": 705, "target": 605, "value": 2}, {"source": 705, "target": 617, "value": 8}, {"source": 705, "target": 645, "value": 8}, {"source": 705, "target": 693, "value": 8}, {"source": 707, "target": 233, "value": 4}, {"source": 707, "target": 245, "value": 9}, {"source": 707, "target": 257, "value": 4}, {"source": 707, "target": 333, "value": 4}, {"source": 707, "target": 345, "value": 8}, {"source": 707, "target": 381, "value": 8}, {"source": 707, "target": 383, "value": 9}, {"source": 707, "target": 407, "value": 4}, {"source": 707, "target": 483, "value": 9}, {"source": 707, "target": 503, "value": 8}, {"source": 707, "target": 533, "value": 4}, {"source": 707, "target": 545, "value": 8}, {"source": 707, "target": 557, "value": 4}, {"source": 707, "target": 633, "value": 4}, {"source": 707, "target": 645, "value": 7}, {"source": 707, "target": 683, "value": 8}, {"source": 707, "target": 695, "value": 8}, {"source": 711, "target": 249, "value": 9}, {"source": 711, "target": 261, "value": 4}, {"source": 711, "target": 285, "value": 8}, {"source": 711, "target": 311, "value": 4}, {"source": 711, "target": 363, "value": 10}, {"source": 711, "target": 411, "value": 4}, {"source": 711, "target": 461, "value": 4}, {"source": 711, "target": 501, "value": 9}, {"source": 711, "target": 549, "value": 8}, {"source": 711, "target": 561, "value": 4}, {"source": 711, "target": 611, "value": 4}, {"source": 711, "target": 701, "value": 8}, {"source": 713, "target": 225, "value": 8}, {"source": 713, "target": 228, "value": 8}, {"source": 713, "target": 255, "value": 10}, {"source": 713, "target": 261, "value": 8}, {"source": 713, "target": 263, "value": 1}, {"source": 713, "target": 275, "value": 8}, {"source": 713, "target": 278, "value": 8}, {"source": 713, "target": 285, "value": 8}, {"source": 713, "target": 287, "value": 4}, {"source": 713, "target": 291, "value": 9}, {"source": 713, "target": 293, "value": 8}, {"source": 713, "target": 305, "value": 8}, {"source": 713, "target": 309, "value": 8}, {"source": 713, "target": 311, "value": 8}, {"source": 713, "target": 317, "value": 8}, {"source": 713, "target": 321, "value": 8}, {"source": 713, "target": 341, "value": 9}, {"source": 713, "target": 351, "value": 9}, {"source": 713, "target": 363, "value": 1}, {"source": 713, "target": 365, "value": 4}, {"source": 713, "target": 375, "value": 8}, {"source": 713, "target": 378, "value": 8}, {"source": 713, "target": 401, "value": 8}, {"source": 713, "target": 405, "value": 4}, {"source": 713, "target": 411, "value": 8}, {"source": 713, "target": 413, "value": 1}, {"source": 713, "target": 425, "value": 8}, {"source": 713, "target": 428, "value": 8}, {"source": 713, "target": 437, "value": 4}, {"source": 713, "target": 441, "value": 9}, {"source": 713, "target": 453, "value": 9}, {"source": 713, "target": 455, "value": 10}, {"source": 713, "target": 459, "value": 8}, {"source": 713, "target": 461, "value": 2}, {"source": 713, "target": 467, "value": 8}, {"source": 713, "target": 489, "value": 4}, {"source": 713, "target": 491, "value": 9}, {"source": 713, "target": 497, "value": 4}, {"source": 713, "target": 501, "value": 8}, {"source": 713, "target": 513, "value": 1}, {"source": 713, "target": 525, "value": 8}, {"source": 713, "target": 528, "value": 8}, {"source": 713, "target": 549, "value": 8}, {"source": 713, "target": 557, "value": 9}, {"source": 713, "target": 561, "value": 8}, {"source": 713, "target": 563, "value": 1}, {"source": 713, "target": 575, "value": 8}, {"source": 713, "target": 578, "value": 8}, {"source": 713, "target": 587, "value": 4}, {"source": 713, "target": 591, "value": 9}, {"source": 713, "target": 605, "value": 8}, {"source": 713, "target": 609, "value": 9}, {"source": 713, "target": 611, "value": 8}, {"source": 713, "target": 615, "value": 9}, {"source": 713, "target": 617, "value": 8}, {"source": 713, "target": 641, "value": 9}, {"source": 713, "target": 651, "value": 8}, {"source": 713, "target": 653, "value": 8}, {"source": 713, "target": 663, "value": 1}, {"source": 713, "target": 671, "value": 7}, {"source": 713, "target": 675, "value": 8}, {"source": 713, "target": 678, "value": 8}, {"source": 713, "target": 701, "value": 8}, {"source": 713, "target": 705, "value": 7}, {"source": 713, "target": 711, "value": 7}, {"source": 723, "target": 219, "value": 8}, {"source": 723, "target": 221, "value": 8}, {"source": 723, "target": 248, "value": 8}, {"source": 723, "target": 257, "value": 8}, {"source": 723, "target": 261, "value": 9}, {"source": 723, "target": 273, "value": 1}, {"source": 723, "target": 309, "value": 9}, {"source": 723, "target": 323, "value": 2}, {"source": 723, "target": 347, "value": 8}, {"source": 723, "target": 348, "value": 8}, {"source": 723, "target": 369, "value": 8}, {"source": 723, "target": 398, "value": 8}, {"source": 723, "target": 413, "value": 10}, {"source": 723, "target": 423, "value": 2}, {"source": 723, "target": 461, "value": 8}, {"source": 723, "target": 473, "value": 1}, {"source": 723, "target": 497, "value": 8}, {"source": 723, "target": 498, "value": 8}, {"source": 723, "target": 519, "value": 8}, {"source": 723, "target": 545, "value": 10}, {"source": 723, "target": 548, "value": 8}, {"source": 723, "target": 557, "value": 8}, {"source": 723, "target": 561, "value": 8}, {"source": 723, "target": 573, "value": 1}, {"source": 723, "target": 611, "value": 8}, {"source": 723, "target": 615, "value": 8}, {"source": 723, "target": 621, "value": 8}, {"source": 723, "target": 623, "value": 2}, {"source": 723, "target": 647, "value": 8}, {"source": 723, "target": 648, "value": 8}, {"source": 723, "target": 669, "value": 8}, {"source": 723, "target": 678, "value": 8}, {"source": 723, "target": 698, "value": 8}, {"source": 723, "target": 699, "value": 8}, {"source": 725, "target": 225, "value": 2}, {"source": 725, "target": 228, "value": 8}, {"source": 725, "target": 273, "value": 8}, {"source": 725, "target": 275, "value": 4}, {"source": 725, "target": 278, "value": 8}, {"source": 725, "target": 285, "value": 8}, {"source": 725, "target": 287, "value": 8}, {"source": 725, "target": 321, "value": 8}, {"source": 725, "target": 335, "value": 8}, {"source": 725, "target": 365, "value": 10}, {"source": 725, "target": 375, "value": 4}, {"source": 725, "target": 377, "value": 4}, {"source": 725, "target": 378, "value": 8}, {"source": 725, "target": 413, "value": 8}, {"source": 725, "target": 425, "value": 2}, {"source": 725, "target": 428, "value": 8}, {"source": 725, "target": 429, "value": 10}, {"source": 725, "target": 435, "value": 8}, {"source": 725, "target": 437, "value": 8}, {"source": 725, "target": 473, "value": 8}, {"source": 725, "target": 485, "value": 8}, {"source": 725, "target": 513, "value": 8}, {"source": 725, "target": 525, "value": 2}, {"source": 725, "target": 528, "value": 8}, {"source": 725, "target": 531, "value": 8}, {"source": 725, "target": 563, "value": 9}, {"source": 725, "target": 573, "value": 8}, {"source": 725, "target": 575, "value": 4}, {"source": 725, "target": 578, "value": 8}, {"source": 725, "target": 585, "value": 8}, {"source": 725, "target": 587, "value": 8}, {"source": 725, "target": 635, "value": 8}, {"source": 725, "target": 671, "value": 7}, {"source": 725, "target": 675, "value": 3}, {"source": 725, "target": 677, "value": 8}, {"source": 725, "target": 678, "value": 7}, {"source": 725, "target": 713, "value": 8}, {"source": 728, "target": 225, "value": 8}, {"source": 728, "target": 228, "value": 4}, {"source": 728, "target": 249, "value": 8}, {"source": 728, "target": 275, "value": 8}, {"source": 728, "target": 278, "value": 4}, {"source": 728, "target": 303, "value": 11}, {"source": 728, "target": 321, "value": 8}, {"source": 728, "target": 353, "value": 10}, {"source": 728, "target": 365, "value": 10}, {"source": 728, "target": 375, "value": 8}, {"source": 728, "target": 378, "value": 4}, {"source": 728, "target": 399, "value": 8}, {"source": 728, "target": 413, "value": 8}, {"source": 728, "target": 425, "value": 8}, {"source": 728, "target": 428, "value": 4}, {"source": 728, "target": 437, "value": 8}, {"source": 728, "target": 453, "value": 10}, {"source": 728, "target": 503, "value": 9}, {"source": 728, "target": 513, "value": 8}, {"source": 728, "target": 525, "value": 8}, {"source": 728, "target": 528, "value": 4}, {"source": 728, "target": 545, "value": 8}, {"source": 728, "target": 549, "value": 8}, {"source": 728, "target": 563, "value": 9}, {"source": 728, "target": 575, "value": 8}, {"source": 728, "target": 578, "value": 4}, {"source": 728, "target": 603, "value": 9}, {"source": 728, "target": 653, "value": 8}, {"source": 728, "target": 671, "value": 8}, {"source": 728, "target": 675, "value": 7}, {"source": 728, "target": 678, "value": 3}, {"source": 728, "target": 699, "value": 8}, {"source": 728, "target": 713, "value": 8}, {"source": 728, "target": 725, "value": 8}, {"source": 729, "target": 221, "value": 11}, {"source": 729, "target": 233, "value": 12}, {"source": 729, "target": 279, "value": 4}, {"source": 729, "target": 291, "value": 8}, {"source": 729, "target": 317, "value": 9}, {"source": 729, "target": 333, "value": 11}, {"source": 729, "target": 341, "value": 8}, {"source": 729, "target": 381, "value": 9}, {"source": 729, "target": 383, "value": 11}, {"source": 729, "target": 429, "value": 2}, {"source": 729, "target": 441, "value": 8}, {"source": 729, "target": 483, "value": 10}, {"source": 729, "target": 491, "value": 8}, {"source": 729, "target": 531, "value": 9}, {"source": 729, "target": 533, "value": 4}, {"source": 729, "target": 579, "value": 4}, {"source": 729, "target": 581, "value": 9}, {"source": 729, "target": 587, "value": 8}, {"source": 729, "target": 591, "value": 8}, {"source": 729, "target": 617, "value": 8}, {"source": 729, "target": 633, "value": 9}, {"source": 729, "target": 641, "value": 8}, {"source": 729, "target": 653, "value": 8}, {"source": 729, "target": 669, "value": 8}, {"source": 729, "target": 683, "value": 9}, {"source": 731, "target": 221, "value": 11}, {"source": 731, "target": 231, "value": 4}, {"source": 731, "target": 273, "value": 11}, {"source": 731, "target": 281, "value": 4}, {"source": 731, "target": 369, "value": 8}, {"source": 731, "target": 377, "value": 8}, {"source": 731, "target": 381, "value": 4}, {"source": 731, "target": 431, "value": 4}, {"source": 731, "target": 519, "value": 9}, {"source": 731, "target": 531, "value": 4}, {"source": 731, "target": 581, "value": 4}, {"source": 731, "target": 605, "value": 8}, {"source": 731, "target": 621, "value": 8}, {"source": 731, "target": 669, "value": 7}, {"source": 731, "target": 681, "value": 3}, {"source": 735, "target": 273, "value": 9}, {"source": 735, "target": 285, "value": 4}, {"source": 735, "target": 335, "value": 4}, {"source": 735, "target": 377, "value": 10}, {"source": 735, "target": 435, "value": 4}, {"source": 735, "target": 473, "value": 8}, {"source": 735, "target": 485, "value": 4}, {"source": 735, "target": 525, "value": 9}, {"source": 735, "target": 531, "value": 8}, {"source": 735, "target": 573, "value": 8}, {"source": 735, "target": 585, "value": 4}, {"source": 735, "target": 587, "value": 9}, {"source": 735, "target": 635, "value": 4}, {"source": 735, "target": 725, "value": 8}, {"source": 737, "target": 225, "value": 9}, {"source": 737, "target": 228, "value": 9}, {"source": 737, "target": 249, "value": 8}, {"source": 737, "target": 263, "value": 9}, {"source": 737, "target": 278, "value": 9}, {"source": 737, "target": 287, "value": 4}, {"source": 737, "target": 303, "value": 11}, {"source": 737, "target": 353, "value": 11}, {"source": 737, "target": 363, "value": 9}, {"source": 737, "target": 378, "value": 8}, {"source": 737, "target": 399, "value": 8}, {"source": 737, "target": 413, "value": 4}, {"source": 737, "target": 425, "value": 8}, {"source": 737, "target": 428, "value": 8}, {"source": 737, "target": 429, "value": 10}, {"source": 737, "target": 437, "value": 2}, {"source": 737, "target": 453, "value": 10}, {"source": 737, "target": 461, "value": 8}, {"source": 737, "target": 489, "value": 9}, {"source": 737, "target": 503, "value": 10}, {"source": 737, "target": 513, "value": 4}, {"source": 737, "target": 525, "value": 8}, {"source": 737, "target": 528, "value": 8}, {"source": 737, "target": 545, "value": 8}, {"source": 737, "target": 549, "value": 8}, {"source": 737, "target": 563, "value": 8}, {"source": 737, "target": 578, "value": 8}, {"source": 737, "target": 587, "value": 4}, {"source": 737, "target": 603, "value": 9}, {"source": 737, "target": 653, "value": 8}, {"source": 737, "target": 663, "value": 8}, {"source": 737, "target": 677, "value": 8}, {"source": 737, "target": 678, "value": 7}, {"source": 737, "target": 699, "value": 8}, {"source": 737, "target": 713, "value": 4}, {"source": 737, "target": 725, "value": 7}, {"source": 737, "target": 728, "value": 7}, {"source": 741, "target": 219, "value": 8}, {"source": 741, "target": 228, "value": 8}, {"source": 741, "target": 233, "value": 4}, {"source": 741, "target": 245, "value": 8}, {"source": 741, "target": 248, "value": 8}, {"source": 741, "target": 249, "value": 8}, {"source": 741, "target": 251, "value": 8}, {"source": 741, "target": 278, "value": 8}, {"source": 741, "target": 285, "value": 10}, {"source": 741, "target": 291, "value": 4}, {"source": 741, "target": 293, "value": 5}, {"source": 741, "target": 303, "value": 4}, {"source": 741, "target": 321, "value": 9}, {"source": 741, "target": 333, "value": 8}, {"source": 741, "target": 339, "value": 8}, {"source": 741, "target": 341, "value": 2}, {"source": 741, "target": 345, "value": 8}, {"source": 741, "target": 348, "value": 8}, {"source": 741, "target": 351, "value": 8}, {"source": 741, "target": 353, "value": 4}, {"source": 741, "target": 365, "value": 8}, {"source": 741, "target": 369, "value": 8}, {"source": 741, "target": 378, "value": 8}, {"source": 741, "target": 381, "value": 10}, {"source": 741, "target": 395, "value": 8}, {"source": 741, "target": 398, "value": 8}, {"source": 741, "target": 399, "value": 8}, {"source": 741, "target": 401, "value": 8}, {"source": 741, "target": 413, "value": 8}, {"source": 741, "target": 428, "value": 8}, {"source": 741, "target": 429, "value": 8}, {"source": 741, "target": 437, "value": 8}, {"source": 741, "target": 441, "value": 2}, {"source": 741, "target": 453, "value": 4}, {"source": 741, "target": 461, "value": 9}, {"source": 741, "target": 485, "value": 9}, {"source": 741, "target": 489, "value": 4}, {"source": 741, "target": 491, "value": 4}, {"source": 741, "target": 495, "value": 8}, {"source": 741, "target": 498, "value": 8}, {"source": 741, "target": 501, "value": 8}, {"source": 741, "target": 503, "value": 4}, {"source": 741, "target": 519, "value": 9}, {"source": 741, "target": 528, "value": 8}, {"source": 741, "target": 533, "value": 4}, {"source": 741, "target": 545, "value": 4}, {"source": 741, "target": 548, "value": 8}, {"source": 741, "target": 549, "value": 8}, {"source": 741, "target": 551, "value": 8}, {"source": 741, "target": 578, "value": 8}, {"source": 741, "target": 581, "value": 9}, {"source": 741, "target": 587, "value": 8}, {"source": 741, "target": 591, "value": 4}, {"source": 741, "target": 603, "value": 4}, {"source": 741, "target": 633, "value": 8}, {"source": 741, "target": 639, "value": 8}, {"source": 741, "target": 641, "value": 2}, {"source": 741, "target": 645, "value": 7}, {"source": 741, "target": 648, "value": 8}, {"source": 741, "target": 651, "value": 8}, {"source": 741, "target": 653, "value": 4}, {"source": 741, "target": 669, "value": 8}, {"source": 741, "target": 671, "value": 7}, {"source": 741, "target": 678, "value": 7}, {"source": 741, "target": 693, "value": 8}, {"source": 741, "target": 695, "value": 7}, {"source": 741, "target": 698, "value": 7}, {"source": 741, "target": 699, "value": 4}, {"source": 741, "target": 701, "value": 8}, {"source": 741, "target": 713, "value": 8}, {"source": 741, "target": 728, "value": 8}, {"source": 741, "target": 729, "value": 8}, {"source": 741, "target": 737, "value": 8}, {"source": 743, "target": 243, "value": 2}, {"source": 743, "target": 281, "value": 9}, {"source": 743, "target": 293, "value": 1}, {"source": 743, "target": 317, "value": 4}, {"source": 743, "target": 333, "value": 10}, {"source": 743, "target": 335, "value": 8}, {"source": 743, "target": 339, "value": 8}, {"source": 743, "target": 377, "value": 8}, {"source": 743, "target": 381, "value": 8}, {"source": 743, "target": 393, "value": 1}, {"source": 743, "target": 398, "value": 9}, {"source": 743, "target": 429, "value": 8}, {"source": 743, "target": 431, "value": 9}, {"source": 743, "target": 443, "value": 2}, {"source": 743, "target": 467, "value": 8}, {"source": 743, "target": 489, "value": 2}, {"source": 743, "target": 533, "value": 9}, {"source": 743, "target": 543, "value": 2}, {"source": 743, "target": 545, "value": 9}, {"source": 743, "target": 581, "value": 8}, {"source": 743, "target": 585, "value": 9}, {"source": 743, "target": 593, "value": 1}, {"source": 743, "target": 617, "value": 8}, {"source": 743, "target": 639, "value": 8}, {"source": 743, "target": 677, "value": 8}, {"source": 743, "target": 681, "value": 7}, {"source": 743, "target": 693, "value": 1}, {"source": 753, "target": 215, "value": 8}, {"source": 753, "target": 227, "value": 4}, {"source": 753, "target": 228, "value": 8}, {"source": 753, "target": 249, "value": 8}, {"source": 753, "target": 278, "value": 8}, {"source": 753, "target": 279, "value": 4}, {"source": 753, "target": 293, "value": 10}, {"source": 753, "target": 303, "value": 1}, {"source": 753, "target": 315, "value": 8}, {"source": 753, "target": 341, "value": 9}, {"source": 753, "target": 353, "value": 1}, {"source": 753, "target": 365, "value": 8}, {"source": 753, "target": 377, "value": 4}, {"source": 753, "target": 378, "value": 8}, {"source": 753, "target": 399, "value": 8}, {"source": 753, "target": 405, "value": 4}, {"source": 753, "target": 428, "value": 8}, {"source": 753, "target": 437, "value": 8}, {"source": 753, "target": 441, "value": 8}, {"source": 753, "target": 453, "value": 0}, {"source": 753, "target": 461, "value": 8}, {"source": 753, "target": 465, "value": 8}, {"source": 753, "target": 503, "value": 1}, {"source": 753, "target": 515, "value": 8}, {"source": 753, "target": 527, "value": 4}, {"source": 753, "target": 528, "value": 8}, {"source": 753, "target": 545, "value": 8}, {"source": 753, "target": 549, "value": 8}, {"source": 753, "target": 578, "value": 8}, {"source": 753, "target": 603, "value": 1}, {"source": 753, "target": 605, "value": 9}, {"source": 753, "target": 615, "value": 8}, {"source": 753, "target": 641, "value": 8}, {"source": 753, "target": 653, "value": 1}, {"source": 753, "target": 665, "value": 7}, {"source": 753, "target": 677, "value": 4}, {"source": 753, "target": 678, "value": 8}, {"source": 753, "target": 693, "value": 8}, {"source": 753, "target": 699, "value": 4}, {"source": 753, "target": 701, "value": 3}, {"source": 753, "target": 728, "value": 8}, {"source": 753, "target": 737, "value": 8}, {"source": 753, "target": 741, "value": 4}, {"source": 755, "target": 245, "value": 11}, {"source": 755, "target": 251, "value": 8}, {"source": 755, "target": 255, "value": 4}, {"source": 755, "target": 263, "value": 12}, {"source": 755, "target": 293, "value": 9}, {"source": 755, "target": 303, "value": 8}, {"source": 755, "target": 305, "value": 4}, {"source": 755, "target": 309, "value": 8}, {"source": 755, "target": 351, "value": 10}, {"source": 755, "target": 353, "value": 8}, {"source": 755, "target": 363, "value": 11}, {"source": 755, "target": 377, "value": 11}, {"source": 755, "target": 393, "value": 8}, {"source": 755, "target": 405, "value": 2}, {"source": 755, "target": 413, "value": 10}, {"source": 755, "target": 429, "value": 8}, {"source": 755, "target": 453, "value": 8}, {"source": 755, "target": 455, "value": 4}, {"source": 755, "target": 459, "value": 8}, {"source": 755, "target": 497, "value": 4}, {"source": 755, "target": 503, "value": 8}, {"source": 755, "target": 513, "value": 10}, {"source": 755, "target": 549, "value": 9}, {"source": 755, "target": 555, "value": 4}, {"source": 755, "target": 563, "value": 9}, {"source": 755, "target": 593, "value": 8}, {"source": 755, "target": 603, "value": 8}, {"source": 755, "target": 605, "value": 4}, {"source": 755, "target": 609, "value": 8}, {"source": 755, "target": 645, "value": 8}, {"source": 755, "target": 653, "value": 8}, {"source": 755, "target": 663, "value": 9}, {"source": 755, "target": 693, "value": 7}, {"source": 755, "target": 705, "value": 3}, {"source": 755, "target": 713, "value": 8}, {"source": 755, "target": 753, "value": 7}, {"source": 759, "target": 263, "value": 12}, {"source": 759, "target": 309, "value": 4}, {"source": 759, "target": 333, "value": 8}, {"source": 759, "target": 351, "value": 10}, {"source": 759, "target": 363, "value": 11}, {"source": 759, "target": 405, "value": 8}, {"source": 759, "target": 413, "value": 11}, {"source": 759, "target": 459, "value": 4}, {"source": 759, "target": 461, "value": 9}, {"source": 759, "target": 497, "value": 4}, {"source": 759, "target": 513, "value": 10}, {"source": 759, "target": 549, "value": 9}, {"source": 759, "target": 563, "value": 9}, {"source": 759, "target": 609, "value": 4}, {"source": 759, "target": 663, "value": 9}, {"source": 759, "target": 713, "value": 8}, {"source": 759, "target": 755, "value": 8}, {"source": 761, "target": 249, "value": 9}, {"source": 761, "target": 261, "value": 2}, {"source": 761, "target": 273, "value": 4}, {"source": 761, "target": 285, "value": 8}, {"source": 761, "target": 311, "value": 4}, {"source": 761, "target": 323, "value": 4}, {"source": 761, "target": 363, "value": 10}, {"source": 761, "target": 411, "value": 4}, {"source": 761, "target": 413, "value": 4}, {"source": 761, "target": 423, "value": 4}, {"source": 761, "target": 461, "value": 2}, {"source": 761, "target": 473, "value": 4}, {"source": 761, "target": 501, "value": 9}, {"source": 761, "target": 549, "value": 8}, {"source": 761, "target": 561, "value": 2}, {"source": 761, "target": 573, "value": 4}, {"source": 761, "target": 611, "value": 4}, {"source": 761, "target": 623, "value": 4}, {"source": 761, "target": 678, "value": 4}, {"source": 761, "target": 701, "value": 8}, {"source": 761, "target": 711, "value": 3}, {"source": 761, "target": 713, "value": 8}, {"source": 761, "target": 723, "value": 4}, {"source": 765, "target": 215, "value": 4}, {"source": 765, "target": 225, "value": 8}, {"source": 765, "target": 227, "value": 8}, {"source": 765, "target": 228, "value": 8}, {"source": 765, "target": 275, "value": 8}, {"source": 765, "target": 278, "value": 8}, {"source": 765, "target": 291, "value": 8}, {"source": 765, "target": 293, "value": 10}, {"source": 765, "target": 315, "value": 4}, {"source": 765, "target": 317, "value": 10}, {"source": 765, "target": 321, "value": 8}, {"source": 765, "target": 341, "value": 8}, {"source": 765, "target": 353, "value": 9}, {"source": 765, "target": 365, "value": 1}, {"source": 765, "target": 375, "value": 8}, {"source": 765, "target": 377, "value": 8}, {"source": 765, "target": 378, "value": 8}, {"source": 765, "target": 405, "value": 10}, {"source": 765, "target": 413, "value": 8}, {"source": 765, "target": 425, "value": 8}, {"source": 765, "target": 428, "value": 8}, {"source": 765, "target": 441, "value": 8}, {"source": 765, "target": 453, "value": 4}, {"source": 765, "target": 461, "value": 8}, {"source": 765, "target": 465, "value": 2}, {"source": 765, "target": 491, "value": 8}, {"source": 765, "target": 513, "value": 8}, {"source": 765, "target": 515, "value": 4}, {"source": 765, "target": 525, "value": 8}, {"source": 765, "target": 527, "value": 8}, {"source": 765, "target": 528, "value": 8}, {"source": 765, "target": 563, "value": 8}, {"source": 765, "target": 573, "value": 8}, {"source": 765, "target": 575, "value": 8}, {"source": 765, "target": 578, "value": 8}, {"source": 765, "target": 591, "value": 8}, {"source": 765, "target": 605, "value": 9}, {"source": 765, "target": 615, "value": 4}, {"source": 765, "target": 641, "value": 8}, {"source": 765, "target": 653, "value": 8}, {"source": 765, "target": 665, "value": 2}, {"source": 765, "target": 671, "value": 8}, {"source": 765, "target": 675, "value": 8}, {"source": 765, "target": 677, "value": 7}, {"source": 765, "target": 678, "value": 8}, {"source": 765, "target": 713, "value": 4}, {"source": 765, "target": 725, "value": 8}, {"source": 765, "target": 728, "value": 8}, {"source": 765, "target": 741, "value": 8}, {"source": 765, "target": 753, "value": 7}, {"source": 767, "target": 225, "value": 8}, {"source": 767, "target": 228, "value": 8}, {"source": 767, "target": 243, "value": 9}, {"source": 767, "target": 255, "value": 10}, {"source": 767, "target": 275, "value": 8}, {"source": 767, "target": 278, "value": 8}, {"source": 767, "target": 293, "value": 4}, {"source": 767, "target": 305, "value": 9}, {"source": 767, "target": 317, "value": 4}, {"source": 767, "target": 321, "value": 8}, {"source": 767, "target": 363, "value": 8}, {"source": 767, "target": 365, "value": 9}, {"source": 767, "target": 375, "value": 8}, {"source": 767, "target": 378, "value": 8}, {"source": 767, "target": 393, "value": 4}, {"source": 767, "target": 405, "value": 8}, {"source": 767, "target": 413, "value": 8}, {"source": 767, "target": 425, "value": 8}, {"source": 767, "target": 428, "value": 8}, {"source": 767, "target": 443, "value": 9}, {"source": 767, "target": 467, "value": 4}, {"source": 767, "target": 489, "value": 10}, {"source": 767, "target": 513, "value": 8}, {"source": 767, "target": 525, "value": 8}, {"source": 767, "target": 528, "value": 8}, {"source": 767, "target": 543, "value": 8}, {"source": 767, "target": 557, "value": 9}, {"source": 767, "target": 563, "value": 8}, {"source": 767, "target": 575, "value": 8}, {"source": 767, "target": 578, "value": 8}, {"source": 767, "target": 593, "value": 4}, {"source": 767, "target": 605, "value": 8}, {"source": 767, "target": 617, "value": 4}, {"source": 767, "target": 671, "value": 8}, {"source": 767, "target": 675, "value": 8}, {"source": 767, "target": 678, "value": 8}, {"source": 767, "target": 693, "value": 4}, {"source": 767, "target": 705, "value": 7}, {"source": 767, "target": 713, "value": 4}, {"source": 767, "target": 725, "value": 8}, {"source": 767, "target": 728, "value": 8}, {"source": 767, "target": 743, "value": 8}, {"source": 767, "target": 765, "value": 7}, {"source": 771, "target": 221, "value": 4}, {"source": 771, "target": 261, "value": 11}, {"source": 771, "target": 293, "value": 12}, {"source": 771, "target": 309, "value": 9}, {"source": 771, "target": 321, "value": 4}, {"source": 771, "target": 371, "value": 4}, {"source": 771, "target": 461, "value": 10}, {"source": 771, "target": 471, "value": 4}, {"source": 771, "target": 521, "value": 4}, {"source": 771, "target": 573, "value": 9}, {"source": 771, "target": 609, "value": 8}, {"source": 771, "target": 621, "value": 4}, {"source": 771, "target": 663, "value": 9}, {"source": 771, "target": 671, "value": 4}, {"source": 773, "target": 219, "value": 8}, {"source": 773, "target": 221, "value": 2}, {"source": 773, "target": 233, "value": 8}, {"source": 773, "target": 248, "value": 8}, {"source": 773, "target": 257, "value": 8}, {"source": 773, "target": 261, "value": 9}, {"source": 773, "target": 273, "value": 1}, {"source": 773, "target": 279, "value": 8}, {"source": 773, "target": 285, "value": 8}, {"source": 773, "target": 309, "value": 9}, {"source": 773, "target": 321, "value": 4}, {"source": 773, "target": 323, "value": 1}, {"source": 773, "target": 333, "value": 8}, {"source": 773, "target": 335, "value": 8}, {"source": 773, "target": 347, "value": 4}, {"source": 773, "target": 348, "value": 8}, {"source": 773, "target": 369, "value": 8}, {"source": 773, "target": 377, "value": 10}, {"source": 773, "target": 383, "value": 8}, {"source": 773, "target": 398, "value": 8}, {"source": 773, "target": 413, "value": 10}, {"source": 773, "target": 423, "value": 1}, {"source": 773, "target": 435, "value": 8}, {"source": 773, "target": 461, "value": 8}, {"source": 773, "target": 473, "value": 1}, {"source": 773, "target": 483, "value": 8}, {"source": 773, "target": 485, "value": 8}, {"source": 773, "target": 497, "value": 4}, {"source": 773, "target": 498, "value": 8}, {"source": 773, "target": 519, "value": 8}, {"source": 773, "target": 521, "value": 8}, {"source": 773, "target": 525, "value": 9}, {"source": 773, "target": 531, "value": 8}, {"source": 773, "target": 533, "value": 8}, {"source": 773, "target": 545, "value": 10}, {"source": 773, "target": 548, "value": 8}, {"source": 773, "target": 557, "value": 8}, {"source": 773, "target": 561, "value": 8}, {"source": 773, "target": 573, "value": 0}, {"source": 773, "target": 585, "value": 8}, {"source": 773, "target": 611, "value": 8}, {"source": 773, "target": 615, "value": 8}, {"source": 773, "target": 621, "value": 2}, {"source": 773, "target": 623, "value": 1}, {"source": 773, "target": 633, "value": 8}, {"source": 773, "target": 635, "value": 8}, {"source": 773, "target": 647, "value": 4}, {"source": 773, "target": 648, "value": 8}, {"source": 773, "target": 669, "value": 8}, {"source": 773, "target": 678, "value": 8}, {"source": 773, "target": 683, "value": 8}, {"source": 773, "target": 698, "value": 7}, {"source": 773, "target": 699, "value": 4}, {"source": 773, "target": 723, "value": 1}, {"source": 773, "target": 725, "value": 8}, {"source": 773, "target": 735, "value": 8}, {"source": 773, "target": 761, "value": 4}, {"source": 783, "target": 221, "value": 4}, {"source": 783, "target": 225, "value": 9}, {"source": 783, "target": 231, "value": 8}, {"source": 783, "target": 233, "value": 1}, {"source": 783, "target": 245, "value": 8}, {"source": 783, "target": 248, "value": 8}, {"source": 783, "target": 257, "value": 8}, {"source": 783, "target": 279, "value": 4}, {"source": 783, "target": 281, "value": 8}, {"source": 783, "target": 285, "value": 10}, {"source": 783, "target": 287, "value": 8}, {"source": 783, "target": 317, "value": 8}, {"source": 783, "target": 321, "value": 4}, {"source": 783, "target": 333, "value": 1}, {"source": 783, "target": 335, "value": 10}, {"source": 783, "target": 345, "value": 8}, {"source": 783, "target": 348, "value": 8}, {"source": 783, "target": 381, "value": 4}, {"source": 783, "target": 383, "value": 2}, {"source": 783, "target": 395, "value": 8}, {"source": 783, "target": 398, "value": 8}, {"source": 783, "target": 405, "value": 11}, {"source": 783, "target": 407, "value": 8}, {"source": 783, "target": 425, "value": 8}, {"source": 783, "target": 429, "value": 4}, {"source": 783, "target": 431, "value": 8}, {"source": 783, "target": 437, "value": 8}, {"source": 783, "target": 483, "value": 2}, {"source": 783, "target": 485, "value": 9}, {"source": 783, "target": 495, "value": 8}, {"source": 783, "target": 498, "value": 8}, {"source": 783, "target": 521, "value": 8}, {"source": 783, "target": 525, "value": 8}, {"source": 783, "target": 531, "value": 8}, {"source": 783, "target": 533, "value": 1}, {"source": 783, "target": 545, "value": 8}, {"source": 783, "target": 548, "value": 8}, {"source": 783, "target": 557, "value": 4}, {"source": 783, "target": 573, "value": 9}, {"source": 783, "target": 579, "value": 8}, {"source": 783, "target": 581, "value": 8}, {"source": 783, "target": 587, "value": 8}, {"source": 783, "target": 605, "value": 8}, {"source": 783, "target": 617, "value": 8}, {"source": 783, "target": 621, "value": 8}, {"source": 783, "target": 633, "value": 1}, {"source": 783, "target": 645, "value": 8}, {"source": 783, "target": 648, "value": 8}, {"source": 783, "target": 669, "value": 8}, {"source": 783, "target": 677, "value": 8}, {"source": 783, "target": 681, "value": 8}, {"source": 783, "target": 683, "value": 2}, {"source": 783, "target": 695, "value": 8}, {"source": 783, "target": 698, "value": 8}, {"source": 783, "target": 707, "value": 8}, {"source": 783, "target": 725, "value": 8}, {"source": 783, "target": 729, "value": 8}, {"source": 783, "target": 731, "value": 7}, {"source": 783, "target": 737, "value": 7}, {"source": 783, "target": 741, "value": 8}, {"source": 783, "target": 773, "value": 7}, {"source": 785, "target": 273, "value": 9}, {"source": 785, "target": 285, "value": 2}, {"source": 785, "target": 293, "value": 8}, {"source": 785, "target": 335, "value": 4}, {"source": 785, "target": 347, "value": 8}, {"source": 785, "target": 377, "value": 10}, {"source": 785, "target": 435, "value": 4}, {"source": 785, "target": 437, "value": 9}, {"source": 785, "target": 473, "value": 8}, {"source": 785, "target": 485, "value": 2}, {"source": 785, "target": 497, "value": 8}, {"source": 785, "target": 525, "value": 9}, {"source": 785, "target": 531, "value": 8}, {"source": 785, "target": 573, "value": 8}, {"source": 785, "target": 585, "value": 2}, {"source": 785, "target": 587, "value": 9}, {"source": 785, "target": 635, "value": 4}, {"source": 785, "target": 647, "value": 8}, {"source": 785, "target": 725, "value": 8}, {"source": 785, "target": 735, "value": 3}, {"source": 785, "target": 773, "value": 7}, {"source": 789, "target": 228, "value": 8}, {"source": 789, "target": 243, "value": 12}, {"source": 789, "target": 249, "value": 8}, {"source": 789, "target": 251, "value": 8}, {"source": 789, "target": 278, "value": 8}, {"source": 789, "target": 293, "value": 12}, {"source": 789, "target": 303, "value": 10}, {"source": 789, "target": 335, "value": 8}, {"source": 789, "target": 339, "value": 4}, {"source": 789, "target": 341, "value": 10}, {"source": 789, "target": 351, "value": 8}, {"source": 789, "target": 353, "value": 10}, {"source": 789, "target": 377, "value": 8}, {"source": 789, "target": 378, "value": 8}, {"source": 789, "target": 393, "value": 11}, {"source": 789, "target": 399, "value": 8}, {"source": 789, "target": 401, "value": 8}, {"source": 789, "target": 413, "value": 8}, {"source": 789, "target": 428, "value": 8}, {"source": 789, "target": 429, "value": 10}, {"source": 789, "target": 437, "value": 8}, {"source": 789, "target": 443, "value": 10}, {"source": 789, "target": 453, "value": 9}, {"source": 789, "target": 461, "value": 10}, {"source": 789, "target": 489, "value": 2}, {"source": 789, "target": 501, "value": 8}, {"source": 789, "target": 503, "value": 9}, {"source": 789, "target": 528, "value": 8}, {"source": 789, "target": 543, "value": 10}, {"source": 789, "target": 545, "value": 8}, {"source": 789, "target": 549, "value": 8}, {"source": 789, "target": 551, "value": 8}, {"source": 789, "target": 578, "value": 8}, {"source": 789, "target": 593, "value": 9}, {"source": 789, "target": 603, "value": 8}, {"source": 789, "target": 639, "value": 4}, {"source": 789, "target": 651, "value": 8}, {"source": 789, "target": 653, "value": 8}, {"source": 789, "target": 677, "value": 8}, {"source": 789, "target": 678, "value": 7}, {"source": 789, "target": 693, "value": 9}, {"source": 789, "target": 699, "value": 8}, {"source": 789, "target": 701, "value": 7}, {"source": 789, "target": 728, "value": 7}, {"source": 789, "target": 737, "value": 7}, {"source": 789, "target": 741, "value": 2}, {"source": 789, "target": 743, "value": 8}, {"source": 789, "target": 753, "value": 8}, {"source": 791, "target": 291, "value": 4}, {"source": 791, "target": 293, "value": 10}, {"source": 791, "target": 341, "value": 4}, {"source": 791, "target": 365, "value": 8}, {"source": 791, "target": 381, "value": 10}, {"source": 791, "target": 429, "value": 8}, {"source": 791, "target": 441, "value": 4}, {"source": 791, "target": 491, "value": 4}, {"source": 791, "target": 533, "value": 9}, {"source": 791, "target": 581, "value": 9}, {"source": 791, "target": 587, "value": 8}, {"source": 791, "target": 591, "value": 4}, {"source": 791, "target": 641, "value": 4}, {"source": 791, "target": 713, "value": 9}, {"source": 791, "target": 729, "value": 7}, {"source": 791, "target": 741, "value": 3}, {"source": 791, "target": 765, "value": 8}, {"source": 795, "target": 233, "value": 4}, {"source": 795, "target": 245, "value": 4}, {"source": 795, "target": 248, "value": 8}, {"source": 795, "target": 285, "value": 10}, {"source": 795, "target": 333, "value": 9}, {"source": 795, "target": 345, "value": 4}, {"source": 795, "target": 348, "value": 8}, {"source": 795, "target": 395, "value": 4}, {"source": 795, "target": 398, "value": 8}, {"source": 795, "target": 485, "value": 9}, {"source": 795, "target": 495, "value": 4}, {"source": 795, "target": 498, "value": 8}, {"source": 795, "target": 533, "value": 8}, {"source": 795, "target": 545, "value": 4}, {"source": 795, "target": 548, "value": 8}, {"source": 795, "target": 633, "value": 8}, {"source": 795, "target": 645, "value": 4}, {"source": 795, "target": 648, "value": 7}, {"source": 795, "target": 669, "value": 8}, {"source": 795, "target": 695, "value": 4}, {"source": 795, "target": 698, "value": 7}, {"source": 795, "target": 741, "value": 7}, {"source": 795, "target": 783, "value": 8}, {"source": 797, "target": 221, "value": 9}, {"source": 797, "target": 225, "value": 8}, {"source": 797, "target": 245, "value": 4}, {"source": 797, "target": 251, "value": 8}, {"source": 797, "target": 257, "value": 8}, {"source": 797, "target": 263, "value": 12}, {"source": 797, "target": 273, "value": 3}, {"source": 797, "target": 275, "value": 8}, {"source": 797, "target": 285, "value": 9}, {"source": 797, "target": 293, "value": 8}, {"source": 797, "target": 309, "value": 8}, {"source": 797, "target": 323, "value": 4}, {"source": 797, "target": 341, "value": 10}, {"source": 797, "target": 345, "value": 4}, {"source": 797, "target": 347, "value": 4}, {"source": 797, "target": 351, "value": 4}, {"source": 797, "target": 363, "value": 11}, {"source": 797, "target": 375, "value": 8}, {"source": 797, "target": 377, "value": 9}, {"source": 797, "target": 395, "value": 8}, {"source": 797, "target": 401, "value": 8}, {"source": 797, "target": 405, "value": 8}, {"source": 797, "target": 407, "value": 8}, {"source": 797, "target": 413, "value": 10}, {"source": 797, "target": 423, "value": 4}, {"source": 797, "target": 425, "value": 8}, {"source": 797, "target": 437, "value": 10}, {"source": 797, "target": 459, "value": 8}, {"source": 797, "target": 473, "value": 2}, {"source": 797, "target": 485, "value": 8}, {"source": 797, "target": 489, "value": 8}, {"source": 797, "target": 495, "value": 8}, {"source": 797, "target": 497, "value": 2}, {"source": 797, "target": 501, "value": 8}, {"source": 797, "target": 503, "value": 8}, {"source": 797, "target": 513, "value": 10}, {"source": 797, "target": 525, "value": 8}, {"source": 797, "target": 545, "value": 2}, {"source": 797, "target": 549, "value": 9}, {"source": 797, "target": 551, "value": 8}, {"source": 797, "target": 557, "value": 8}, {"source": 797, "target": 563, "value": 9}, {"source": 797, "target": 573, "value": 2}, {"source": 797, "target": 575, "value": 8}, {"source": 797, "target": 585, "value": 4}, {"source": 797, "target": 609, "value": 8}, {"source": 797, "target": 621, "value": 8}, {"source": 797, "target": 623, "value": 4}, {"source": 797, "target": 645, "value": 4}, {"source": 797, "target": 647, "value": 4}, {"source": 797, "target": 651, "value": 8}, {"source": 797, "target": 663, "value": 9}, {"source": 797, "target": 669, "value": 7}, {"source": 797, "target": 675, "value": 8}, {"source": 797, "target": 695, "value": 4}, {"source": 797, "target": 699, "value": 8}, {"source": 797, "target": 701, "value": 8}, {"source": 797, "target": 707, "value": 8}, {"source": 797, "target": 713, "value": 8}, {"source": 797, "target": 723, "value": 4}, {"source": 797, "target": 725, "value": 8}, {"source": 797, "target": 741, "value": 8}, {"source": 797, "target": 755, "value": 7}, {"source": 797, "target": 759, "value": 8}, {"source": 797, "target": 773, "value": 2}, {"source": 797, "target": 785, "value": 7}, {"source": 797, "target": 789, "value": 7}, {"source": 797, "target": 795, "value": 7}, {"source": 798, "target": 219, "value": 8}, {"source": 798, "target": 233, "value": 5}, {"source": 798, "target": 245, "value": 8}, {"source": 798, "target": 248, "value": 4}, {"source": 798, "target": 257, "value": 8}, {"source": 798, "target": 273, "value": 11}, {"source": 798, "target": 285, "value": 11}, {"source": 798, "target": 309, "value": 10}, {"source": 798, "target": 323, "value": 11}, {"source": 798, "target": 333, "value": 9}, {"source": 798, "target": 345, "value": 8}, {"source": 798, "target": 348, "value": 4}, {"source": 798, "target": 369, "value": 8}, {"source": 798, "target": 395, "value": 8}, {"source": 798, "target": 398, "value": 4}, {"source": 798, "target": 423, "value": 10}, {"source": 798, "target": 473, "value": 10}, {"source": 798, "target": 485, "value": 10}, {"source": 798, "target": 495, "value": 8}, {"source": 798, "target": 498, "value": 4}, {"source": 798, "target": 519, "value": 8}, {"source": 798, "target": 533, "value": 8}, {"source": 798, "target": 545, "value": 8}, {"source": 798, "target": 548, "value": 4}, {"source": 798, "target": 557, "value": 8}, {"source": 798, "target": 573, "value": 9}, {"source": 798, "target": 611, "value": 9}, {"source": 798, "target": 615, "value": 8}, {"source": 798, "target": 623, "value": 9}, {"source": 798, "target": 633, "value": 8}, {"source": 798, "target": 645, "value": 8}, {"source": 798, "target": 648, "value": 4}, {"source": 798, "target": 669, "value": 8}, {"source": 798, "target": 695, "value": 8}, {"source": 798, "target": 698, "value": 4}, {"source": 798, "target": 723, "value": 8}, {"source": 798, "target": 741, "value": 7}, {"source": 798, "target": 773, "value": 8}, {"source": 798, "target": 783, "value": 8}, {"source": 798, "target": 795, "value": 8}, {"source": 801, "target": 251, "value": 4}, {"source": 801, "target": 263, "value": 8}, {"source": 801, "target": 341, "value": 10}, {"source": 801, "target": 351, "value": 4}, {"source": 801, "target": 363, "value": 8}, {"source": 801, "target": 401, "value": 2}, {"source": 801, "target": 413, "value": 8}, {"source": 801, "target": 453, "value": 9}, {"source": 801, "target": 455, "value": 10}, {"source": 801, "target": 489, "value": 8}, {"source": 801, "target": 501, "value": 2}, {"source": 801, "target": 503, "value": 9}, {"source": 801, "target": 513, "value": 8}, {"source": 801, "target": 525, "value": 8}, {"source": 801, "target": 551, "value": 4}, {"source": 801, "target": 563, "value": 8}, {"source": 801, "target": 573, "value": 10}, {"source": 801, "target": 651, "value": 2}, {"source": 801, "target": 653, "value": 8}, {"source": 801, "target": 663, "value": 8}, {"source": 801, "target": 701, "value": 2}, {"source": 801, "target": 713, "value": 7}, {"source": 801, "target": 741, "value": 8}, {"source": 801, "target": 789, "value": 7}, {"source": 801, "target": 797, "value": 8}, {"source": 803, "target": 227, "value": 8}, {"source": 803, "target": 228, "value": 8}, {"source": 803, "target": 249, "value": 8}, {"source": 803, "target": 278, "value": 8}, {"source": 803, "target": 279, "value": 10}, {"source": 803, "target": 293, "value": 10}, {"source": 803, "target": 303, "value": 2}, {"source": 803, "target": 341, "value": 9}, {"source": 803, "target": 353, "value": 1}, {"source": 803, "target": 377, "value": 8}, {"source": 803, "target": 378, "value": 8}, {"source": 803, "target": 399, "value": 8}, {"source": 803, "target": 405, "value": 10}, {"source": 803, "target": 428, "value": 8}, {"source": 803, "target": 437, "value": 8}, {"source": 803, "target": 441, "value": 8}, {"source": 803, "target": 453, "value": 1}, {"source": 803, "target": 503, "value": 2}, {"source": 803, "target": 527, "value": 8}, {"source": 803, "target": 528, "value": 8}, {"source": 803, "target": 545, "value": 8}, {"source": 803, "target": 549, "value": 8}, {"source": 803, "target": 578, "value": 8}, {"source": 803, "target": 603, "value": 2}, {"source": 803, "target": 641, "value": 8}, {"source": 803, "target": 653, "value": 1}, {"source": 803, "target": 677, "value": 8}, {"source": 803, "target": 678, "value": 8}, {"source": 803, "target": 693, "value": 8}, {"source": 803, "target": 699, "value": 4}, {"source": 803, "target": 701, "value": 8}, {"source": 803, "target": 728, "value": 7}, {"source": 803, "target": 737, "value": 8}, {"source": 803, "target": 741, "value": 3}, {"source": 803, "target": 753, "value": 1}, {"source": 803, "target": 755, "value": 8}, {"source": 803, "target": 789, "value": 7}]} \ No newline at end of file diff --git a/galaxy/templates/galaxy/user.jinja b/galaxy/templates/galaxy/user.jinja index 1c6e4c7e..38abb6de 100644 --- a/galaxy/templates/galaxy/user.jinja +++ b/galaxy/templates/galaxy/user.jinja @@ -1,37 +1,42 @@ {% extends "core/base.jinja" %} {% block title %} -{% trans user_name=user.get_display_name() %}{{ user_name }}'s Galaxy{% endtrans %} +{% trans user_name=object.get_display_name() %}{{ user_name }}'s Galaxy{% endtrans %} {% endblock %} {% block content %} -{% if object.galaxy_user %} -

Reset on {{ object.get_display_name() }}

-

Self score: {{ object.galaxy_user.mass }}

-
- - - - - - - - - - {% for lane in lanes %} - - - - - - - - - - {% endfor %} -
CitizenScoreDistanceFamilyPicturesClubs
Locate{{ lane.other_star_name }}{{ lane.other_star_mass }}{{ lane.distance }}{{ lane.family }}{{ lane.pictures }}{{ lane.clubs }}
+{% if object.current_star %} +
+
-
+
+

Reset on {{ object.get_display_name() }}

+

Self score: {{ object.current_star.mass }}

+ + + + + + + + + + + {% for lane in lanes %} + + + + + + + + + + {% endfor %} +
CitizenScoreDistanceFamilyPicturesClubs
Locate{{ lane.other_star_name }}{{ lane.other_star_mass }}{{ lane.distance }}{{ lane.family }}{{ lane.pictures }}{{ lane.clubs }}
+
+
+

#{{ object.current_star.galaxy }}#

{% else %}

This citizen has not yet joined the galaxy

{% endif %} @@ -53,9 +58,31 @@ return Graph.graphData().nodes.find(n => n.id === id); } + function get_links_from_node_id(id) { + return Graph.graphData().links.filter(l => l.source.id === id || l.target.id === id); + } + function focus_node(node) { + highlightNodes.clear(); + highlightLinks.clear(); + + hoverNode = node || null; + if (node) { // collect neighbors and links for highlighting + get_links_from_node_id(node.id).forEach(link => { + highlightLinks.add(link); + highlightNodes.add(link.source); + highlightNodes.add(link.target); + }); + } + + // refresh node and link display + Graph + .nodeThreeObject(Graph.nodeThreeObject()) + .linkWidth(Graph.linkWidth()) + .linkDirectionalParticles(Graph.linkDirectionalParticles()); + // Aim at node from outside it - const distance = 200; + const distance = 42; const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z); const newPos = node.x || node.y || node.z @@ -69,25 +96,44 @@ ); } + const highlightNodes = new Set(); + const highlightLinks = new Set(); + let hoverNode = null; + document.addEventListener("DOMContentLoaded", () => { + var graph_div = document.getElementById('3d-graph'); Graph = ForceGraph3D(); - Graph(document.getElementById('3d-graph')); + Graph(graph_div); Graph .jsonUrl('{{ url("galaxy:data") }}') - .width(1000) - .height(700) - .nodeAutoColorBy('id') - .nodeLabel(node => `${node.name}`) - .onNodeClick(node => focus_node(node)) - .linkDirectionalParticles(3) - .linkDirectionalParticleWidth(0.8) - .linkDirectionalParticleSpeed(0.006) + .width(graph_div.parentElement.clientWidth > 1200 ? 1200 : graph_div.parentElement.clientWidth) // Not perfect at all. JS-fu master from the future, please fix this :-) + .height(1000) + .enableNodeDrag(false) // allow easier navigation + .onNodeClick(node => { + camera = Graph.cameraPosition(); + var distance = Math.sqrt(Math.pow(node.x - camera.x, 2) + Math.pow(node.y - camera.y, 2) + Math.pow(node.z - camera.z, 2)) + if (distance < 120 || highlightNodes.has(node)) { + focus_node(node); + } + }) + .linkWidth(link => highlightLinks.has(link) ? 0.4 : 0.0) + .linkColor(link => highlightLinks.has(link) ? 'rgba(255,160,0,1)' : 'rgba(128,255,255,0.6)') + .linkVisibility(link => highlightLinks.has(link)) + .nodeVisibility(node => highlightNodes.has(node) || node.mass > 4) + // .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 1) // kinda buggy for now, and slows this a bit, but would be great to help visualize lanes + .linkDirectionalParticleWidth(0.2) + .linkDirectionalParticleSpeed(-0.006) .nodeThreeObject(node => { const sprite = new SpriteText(node.name); sprite.material.depthWrite = false; // make sprite background transparent - sprite.color = node.color; - sprite.textHeight = 5; + sprite.color = highlightNodes.has(node) ? node === hoverNode ? 'rgba(200,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.2)'; + sprite.textHeight = 2; + sprite.center = new THREE.Vector2(1.2, 0.5); return sprite; + }) + .onEngineStop( () => { + focus_node(get_node_from_id({{ object.id }})); + Graph.onEngineStop(() => {}); // don't call ourselves in a loop while moving the focus }); // Set distance between stars @@ -98,9 +144,6 @@ Graph.d3Force('positionX', d3.forceX().strength(node => { return 1 - (1 / node.mass); })); Graph.d3Force('positionY', d3.forceY().strength(node => { return 1 - (1 / node.mass); })); Graph.d3Force('positionZ', d3.forceZ().strength(node => { return 1 - (1 / node.mass); })); - - // Focus current user - setTimeout(() => focus_node(get_node_from_id({{ object.id }})), 1000); }) {% endblock %} diff --git a/galaxy/tests.py b/galaxy/tests.py index c30ec8cf..70314574 100644 --- a/galaxy/tests.py +++ b/galaxy/tests.py @@ -22,14 +22,19 @@ # # -from django.test import TestCase +import json + +from pathlib import Path + from django.core.management import call_command +from django.test import TestCase +from django.urls import reverse from core.models import User from galaxy.models import Galaxy -class GalaxyTest(TestCase): +class GalaxyTestModel(TestCase): def setUp(self): self.root = User.objects.get(username="root") self.skia = User.objects.get(username="skia") @@ -41,6 +46,9 @@ class GalaxyTest(TestCase): self.com = User.objects.get(username="comunity") def test_user_self_score(self): + """ + Test that individual user scores are correct + """ with self.assertNumQueries(8): self.assertEqual(Galaxy.compute_user_score(self.root), 9) self.assertEqual(Galaxy.compute_user_score(self.skia), 10) @@ -52,6 +60,10 @@ class GalaxyTest(TestCase): self.assertEqual(Galaxy.compute_user_score(self.com), 1) def test_users_score(self): + """ + Test on the default dataset generated by the `populate` command + that the relation scores are correct + """ expected_scores = { "krophil": { "comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0}, @@ -112,33 +124,78 @@ class GalaxyTest(TestCase): while len(users) > 0: user1 = users.pop(0) for user2 in users: - score, family, pictures, clubs = Galaxy.compute_users_score( - user1, user2 - ) + score = Galaxy.compute_users_score(user1, user2) u1 = computed_scores.get(user1.username, {}) u1[user2.username] = { - "score": score, - "family": family, - "pictures": pictures, - "clubs": clubs, + "score": sum(score), + "family": score.family, + "pictures": score.pictures, + "clubs": score.clubs, } computed_scores[user1.username] = u1 self.maxDiff = None # Yes, we want to see the diff if any self.assertDictEqual(expected_scores, computed_scores) + def test_rule(self): + """ + Test on the default dataset generated by the `populate` command + that the number of queries to rule the galaxy is stable. + """ + galaxy = Galaxy.objects.create() + with self.assertNumQueries(58): + galaxy.rule(0) # We want everybody here + + +class GalaxyTestView(TestCase): + @classmethod + def setUpTestData(cls): + """ + Generate a plausible Galaxy once for every test + """ + call_command("generate_galaxy_test_data", "-v", "0") + galaxy = Galaxy.objects.create() + galaxy.rule(26) # We want a fast test + def test_page_is_citizen(self): - Galaxy.rule() + """ + Test that users can access the galaxy page of users who are citizens + """ self.client.login(username="root", password="plop") - response = self.client.get("/galaxy/1/") + user = User.objects.get(last_name="n°500") + response = self.client.get(reverse("galaxy:user", args=[user.id])) self.assertContains( response, - 'Locate', + f'Reset on {user}', status_code=200, ) def test_page_not_citizen(self): - Galaxy.rule() + """ + Test that trying to access the galaxy page of a user who is not + citizens return a 404 + """ self.client.login(username="root", password="plop") - response = self.client.get("/galaxy/2/") + user = User.objects.get(last_name="n°1") + response = self.client.get(reverse("galaxy:user", args=[user.id])) self.assertEquals(response.status_code, 404) + + def test_full_galaxy_state(self): + """ + Test on the more complex dataset generated by the `generate_galaxy_test_data` + command that the relation scores are correct, and that the view exposes the + right data. + """ + self.client.login(username="root", password="plop") + response = self.client.get(reverse("galaxy:data")) + state = response.json() + + galaxy_dir = Path(__file__).parent + + # Dump computed state, either for easier debugging, or to copy as new reference if changes are legit + (galaxy_dir / "test_galaxy_state.json").write_text(json.dumps(state)) + + self.assertEqual( + state, + json.loads((galaxy_dir / "ref_galaxy_state.json").read_text()), + ) diff --git a/galaxy/views.py b/galaxy/views.py index 47228505..3dda003e 100644 --- a/galaxy/views.py +++ b/galaxy/views.py @@ -45,32 +45,29 @@ class GalaxyUserView(CanViewMixin, UserTabsMixin, DetailView): def get_object(self, *args, **kwargs): user: User = super(GalaxyUserView, self).get_object(*args, **kwargs) - if not hasattr(user, "galaxy_user"): + if user.current_star is None: raise Http404(_("This citizen has not yet joined the galaxy")) return user - def get_queryset(self): - return super(GalaxyUserView, self).get_queryset().select_related("galaxy_user") - def get_context_data(self, **kwargs): kwargs = super(GalaxyUserView, self).get_context_data(**kwargs) kwargs["lanes"] = ( GalaxyLane.objects.filter( - Q(star1=self.object.galaxy_user) | Q(star2=self.object.galaxy_user) + Q(star1=self.object.current_star) | Q(star2=self.object.current_star) ) .order_by("distance") .annotate( other_star_id=Case( - When(star1=self.object.galaxy_user, then=F("star2__owner__id")), + When(star1=self.object.current_star, then=F("star2__owner__id")), default=F("star1__owner__id"), ), other_star_mass=Case( - When(star1=self.object.galaxy_user, then=F("star2__mass")), + When(star1=self.object.current_star, then=F("star2__mass")), default=F("star1__mass"), ), other_star_name=Case( When( - star1=self.object.galaxy_user, + star1=self.object.current_star, then=Case( When( star2__owner__nick_name=None, @@ -101,4 +98,4 @@ class GalaxyUserView(CanViewMixin, UserTabsMixin, DetailView): class GalaxyDataView(FormerSubscriberMixin, View): def get(self, request, *args, **kwargs): - return JsonResponse(Galaxy.objects.first().state) + return JsonResponse(Galaxy.get_current_galaxy().state) diff --git a/poetry.lock b/poetry.lock index debfd0c5..23ef0bda 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.12" description = "A configurable sidebar-enabled Sphinx theme" -category = "main" optional = true python-versions = "*" files = [ @@ -16,7 +15,6 @@ files = [ name = "appnope" version = "0.1.3" description = "Disable App Nap on macOS >= 10.9" -category = "dev" optional = false python-versions = "*" files = [ @@ -28,7 +26,6 @@ files = [ name = "asgiref" version = "3.6.0" description = "ASGI specs, helper code, and adapters" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -43,7 +40,6 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "babel" version = "2.11.0" description = "Internationalization utilities" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -58,7 +54,6 @@ pytz = ">=2015.7" name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "dev" optional = false python-versions = "*" files = [ @@ -70,7 +65,6 @@ files = [ name = "black" version = "23.3.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -120,7 +114,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -132,7 +125,6 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -209,7 +201,6 @@ pycparser = "*" name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = true python-versions = ">=3.6.0" files = [ @@ -224,7 +215,6 @@ unicode-backport = ["unicodedata2"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -239,7 +229,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -251,7 +240,6 @@ files = [ name = "coverage" version = "5.5" description = "Code coverage measurement for Python" -category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" files = [ @@ -316,7 +304,6 @@ toml = ["toml"] name = "cryptography" version = "40.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -358,7 +345,6 @@ tox = ["tox"] name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -370,7 +356,6 @@ files = [ name = "dict2xml" version = "1.7.3" description = "Small utility to convert a python dictionary into an XML string" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -385,7 +370,6 @@ tests = ["noseofyeti[black] (==2.4.1)", "pytest (==7.2.1)"] name = "django" version = "3.2.18" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -406,7 +390,6 @@ bcrypt = ["bcrypt"] name = "django-ajax-selects" version = "2.2.0" description = "Edit ForeignKey, ManyToManyField and CharField in Django Admin using jQuery UI AutoComplete." -category = "main" optional = false python-versions = "*" files = [ @@ -417,7 +400,6 @@ files = [ name = "django-countries" version = "7.5.1" description = "Provides a country field for Django models." -category = "main" optional = false python-versions = "*" files = [ @@ -439,7 +421,6 @@ test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytes name = "django-debug-toolbar" version = "4.0.0" description = "A configurable set of panels that display various debug information about the current request/response." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -455,7 +436,6 @@ sqlparse = ">=0.2" name = "django-haystack" version = "3.2.1" description = "Pluggable search for Django." -category = "main" optional = false python-versions = "*" files = [ @@ -472,7 +452,6 @@ elasticsearch = ["elasticsearch (>=5,<8)"] name = "django-jinja" version = "2.10.2" description = "Jinja2 templating language integrated in Django." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -486,21 +465,19 @@ jinja2 = ">=3" [[package]] name = "django-ordered-model" -version = "3.6" +version = "3.7.4" description = "Allows Django models to be ordered and provides a simple admin interface for reordering them." -category = "main" optional = false python-versions = "*" files = [ - {file = "django-ordered-model-3.6.tar.gz", hash = "sha256:62161a6bc51d8b402644854b257605d7b5183d01fd349826682a87e9227c05b5"}, - {file = "django_ordered_model-3.6-py3-none-any.whl", hash = "sha256:0006b111f472a2348f75554a4e77bee2b1f379a0f96726af6b1a3ebf3a950789"}, + {file = "django-ordered-model-3.7.4.tar.gz", hash = "sha256:f258b9762525c00a53009e82f8b8bf2a3aa315e8b453e281e8fdbbfe2b8cb3ba"}, + {file = "django_ordered_model-3.7.4-py3-none-any.whl", hash = "sha256:dfcd3183fe0749dad1c9971cba1d6240ce7328742a30ddc92feca41107bb241d"}, ] [[package]] name = "django-phonenumber-field" version = "6.4.0" description = "An international phone number field for django models." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -519,7 +496,6 @@ phonenumberslite = ["phonenumberslite (>=7.0.2)"] name = "django-ranged-response" version = "0.2.0" description = "Modified Django FileResponse that adds Content-Range headers." -category = "main" optional = false python-versions = "*" files = [ @@ -533,7 +509,6 @@ django = "*" name = "django-simple-captcha" version = "0.5.17" description = "A very simple, yet powerful, Django captcha application" -category = "main" optional = false python-versions = "*" files = [ @@ -553,7 +528,6 @@ test = ["testfixtures"] name = "djangorestframework" version = "3.14.0" description = "Web APIs for Django, made easy." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -569,7 +543,6 @@ pytz = "*" name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" -category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -577,11 +550,24 @@ files = [ {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] +[[package]] +name = "freezegun" +version = "1.2.2" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.6" +files = [ + {file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"}, + {file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = true python-versions = ">=3.5" files = [ @@ -593,7 +579,6 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -605,7 +590,6 @@ files = [ name = "importlib-metadata" version = "6.0.0" description = "Read metadata from Python packages" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -625,7 +609,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag name = "ipython" version = "7.34.0" description = "IPython: Productive Interactive Computing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -662,7 +645,6 @@ test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments" name = "jedi" version = "0.18.2" description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -682,7 +664,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -698,32 +679,22 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "libsass" -version = "0.21.0" +version = "0.22.0" description = "Sass for Python: A straightforward binding of libsass for Python." -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "libsass-0.21.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb"}, - {file = "libsass-0.21.0-cp27-cp27m-win32.whl", hash = "sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb"}, - {file = "libsass-0.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7"}, - {file = "libsass-0.21.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613"}, - {file = "libsass-0.21.0-cp36-abi3-macosx_10_14_x86_64.whl", hash = "sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529"}, - {file = "libsass-0.21.0-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6"}, - {file = "libsass-0.21.0-cp36-abi3-win32.whl", hash = "sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a"}, - {file = "libsass-0.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e"}, - {file = "libsass-0.21.0-cp38-abi3-macosx_12_0_arm64.whl", hash = "sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da"}, - {file = "libsass-0.21.0.tar.gz", hash = "sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2"}, + {file = "libsass-0.22.0-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f1efc1b612299c88aec9e39d6ca0c266d360daa5b19d9430bdeaffffa86993f9"}, + {file = "libsass-0.22.0-cp37-abi3-macosx_10_15_x86_64.whl", hash = "sha256:081e256ab3c5f3f09c7b8dea3bf3bf5e64a97c6995fd9eea880639b3f93a9f9a"}, + {file = "libsass-0.22.0-cp37-abi3-win32.whl", hash = "sha256:89c5ce497fcf3aba1dd1b19aae93b99f68257e5f2026b731b00a872f13324c7f"}, + {file = "libsass-0.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:65455a2728b696b62100eb5932604aa13a29f4ac9a305d95773c14aaa7200aaf"}, + {file = "libsass-0.22.0.tar.gz", hash = "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425"}, ] -[package.dependencies] -six = "*" - [[package]] name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -773,7 +744,6 @@ files = [ name = "matplotlib-inline" version = "0.1.6" description = "Inline Matplotlib backend for Jupyter" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -788,7 +758,6 @@ traitlets = "*" name = "mistune" version = "0.8.4" description = "The fastest markdown parser in pure Python" -category = "main" optional = false python-versions = "*" files = [ @@ -800,7 +769,6 @@ files = [ name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = "*" files = [ @@ -812,7 +780,6 @@ files = [ name = "packaging" version = "23.0" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -824,7 +791,6 @@ files = [ name = "parso" version = "0.8.3" description = "A Python Parser" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -840,7 +806,6 @@ testing = ["docopt", "pytest (<6.0.0)"] name = "pathspec" version = "0.10.3" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -852,7 +817,6 @@ files = [ name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." -category = "dev" optional = false python-versions = "*" files = [ @@ -867,7 +831,6 @@ ptyprocess = ">=0.5" name = "phonenumbers" version = "8.13.4" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." -category = "main" optional = false python-versions = "*" files = [ @@ -879,7 +842,6 @@ files = [ name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" optional = false python-versions = "*" files = [ @@ -891,7 +853,6 @@ files = [ name = "pillow" version = "9.4.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -982,7 +943,6 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa name = "platformdirs" version = "2.6.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -998,7 +958,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest- name = "prompt-toolkit" version = "3.0.36" description = "Library for building powerful interactive command lines in Python" -category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -1013,7 +972,6 @@ wcwidth = "*" name = "psycopg2-binary" version = "2.9.3" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1082,7 +1040,6 @@ files = [ name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1094,7 +1051,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1106,7 +1062,6 @@ files = [ name = "pygments" version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1121,7 +1076,6 @@ plugins = ["importlib-metadata"] name = "pygraphviz" version = "1.10" description = "Python interface to Graphviz" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1132,7 +1086,6 @@ files = [ name = "pyopenssl" version = "23.1.1" description = "Python wrapper module around the OpenSSL library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1151,7 +1104,6 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1166,7 +1118,6 @@ six = ">=1.5" name = "pytz" version = "2021.3" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -1178,7 +1129,6 @@ files = [ name = "reportlab" version = "3.6.12" description = "The Reportlab Toolkit" -category = "main" optional = false python-versions = ">=3.7,<4" files = [ @@ -1240,7 +1190,6 @@ rlpycairo = ["rlPyCairo (>=0.1.0)"] name = "requests" version = "2.28.1" description = "Python HTTP for Humans." -category = "main" optional = true python-versions = ">=3.7, <4" files = [ @@ -1262,7 +1211,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "sentry-sdk" version = "1.21.0" description = "Python client for Sentry (https://sentry.io)" -category = "main" optional = false python-versions = "*" files = [ @@ -1304,7 +1252,6 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "65.6.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1321,7 +1268,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1333,7 +1279,6 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" optional = true python-versions = "*" files = [ @@ -1345,7 +1290,6 @@ files = [ name = "sphinx" version = "4.5.0" description = "Python documentation generator" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1381,7 +1325,6 @@ test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] name = "sphinx-copybutton" version = "0.4.0" description = "Add a copy button to each of your code cells." -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1400,7 +1343,6 @@ rtd = ["ipython", "sphinx", "sphinx-book-theme"] name = "sphinx-rtd-theme" version = "1.1.1" description = "Read the Docs theme for Sphinx" -category = "main" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ @@ -1419,7 +1361,6 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] name = "sphinxcontrib-applehelp" version = "1.0.3" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "main" optional = true python-versions = ">=3.8" files = [ @@ -1435,7 +1376,6 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "main" optional = true python-versions = ">=3.5" files = [ @@ -1451,7 +1391,6 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1467,7 +1406,6 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "main" optional = true python-versions = ">=3.5" files = [ @@ -1482,7 +1420,6 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "main" optional = true python-versions = ">=3.5" files = [ @@ -1498,7 +1435,6 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "main" optional = true python-versions = ">=3.5" files = [ @@ -1512,21 +1448,24 @@ test = ["pytest"] [[package]] name = "sqlparse" -version = "0.4.3" +version = "0.4.4" description = "A non-validating SQL parser." -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, - {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, + {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, + {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, ] +[package.extras] +dev = ["build", "flake8"] +doc = ["sphinx"] +test = ["pytest", "pytest-cov"] + [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1538,7 +1477,6 @@ files = [ name = "traitlets" version = "5.8.1" description = "Traitlets Python configuration system" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1554,7 +1492,6 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1566,7 +1503,6 @@ files = [ name = "urllib3" version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1583,7 +1519,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "wcwidth" version = "0.2.5" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1595,7 +1530,6 @@ files = [ name = "xapian-bindings" version = "0.1.0" description = "Meta-package to build and install xapian-bindings extension." -category = "main" optional = false python-versions = "*" files = [ @@ -1606,7 +1540,6 @@ files = [ name = "xapian-haystack" version = "3.0.1" description = "A Xapian backend for Haystack" -category = "main" optional = false python-versions = "*" files = [ @@ -1621,7 +1554,6 @@ django-haystack = ">=2.8.0" name = "zipp" version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1634,10 +1566,10 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] -docs = ["Sphinx", "sphinx-rtd-theme", "sphinx-copybutton"] +docs = ["Sphinx", "sphinx-copybutton", "sphinx-rtd-theme"] testing = ["coverage"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "5069f58a9ba4b47c16b08e1a4191b0d2be68c20163300fc550b41d68c8e26d73" +content-hash = "62519616aff5a472dac3dd8071a6404b1ee8eab12a197af717a0520f7ded0331" diff --git a/pyproject.toml b/pyproject.toml index 742f7a2e..cb1a6aaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,8 @@ reportlab = "^3.6" django-haystack = "^3.2.1" xapian-haystack = "^3.0.1" xapian-bindings = "^0.1.0" -libsass = "^0.21" -django-ordered-model = "^3.6" +libsass = "^0.22" +django-ordered-model = "^3.7" django-simple-captcha = "^0.5.17" python-dateutil = "^2.8.2" psycopg2-binary = "2.9.3" @@ -59,6 +59,7 @@ testing = ["coverage"] docs = ["Sphinx", "sphinx-rtd-theme", "sphinx-copybutton"] [tool.poetry.dev-dependencies] +freezegun = "^1.2.2" # used to test time-dependent code django-debug-toolbar = "^4.0.0" ipython = "^7.28.0" black = "^23.3.0" diff --git a/sith/settings.py b/sith/settings.py index 26a013a0..5ed279af 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -327,7 +327,8 @@ SITH_CLUB_ROOT_PAGE = "clubs" # Define the date in the year serving as reference for the subscriptions calendar # (month, day) -SITH_START_DATE = (8, 15) # 15th August +SITH_SEMESTER_START_AUTUMN = (8, 15) # 15 August +SITH_SEMESTER_START_SPRING = (2, 15) # 15 February # Used to determine the valid promos SITH_SCHOOL_START_YEAR = 1999 diff --git a/subscription/models.py b/subscription/models.py index ee4334f1..f1c2b2d5 100644 --- a/subscription/models.py +++ b/subscription/models.py @@ -114,12 +114,12 @@ class Subscription(models.Model): return "No user - " + str(self.pk) @staticmethod - def compute_start(d=None, duration=1, user=None): + def compute_start(d: date = None, duration: int = 1, user: User = None) -> date: """ This function computes the start date of the subscription with respect to the given date (default is today), - and the start date given in settings.SITH_START_DATE. + and the start date given in settings.SITH_SEMESTER_START_AUTUMN. It takes the nearest past start date. - Exemples: with SITH_START_DATE = (8, 15) + Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15) Today -> Start date 2015-03-17 -> 2015-02-15 2015-01-11 -> 2014-08-15 @@ -135,9 +135,9 @@ class Subscription(models.Model): return get_start_of_semester(d) @staticmethod - def compute_end(duration, start=None, user=None): + def compute_end(duration: int, start: date = None, user: User = None) -> date: """ - This function compute the end date of the subscription given a start date and a duration in number of semestre + This function compute the end date of the subscription given a start date and a duration in number of semester Exemple: Start - Duration -> End date 2015-09-18 - 1 -> 2016-03-18 @@ -153,7 +153,7 @@ class Subscription(models.Model): days=math.ceil((6 * duration - round(6 * duration)) * 30), ) - def can_be_edited_by(self, user): + def can_be_edited_by(self, user: User): return user.is_board_member or user.is_root def is_valid_now(self): diff --git a/trombi/models.py b/trombi/models.py index e4439c1a..18f36526 100644 --- a/trombi/models.py +++ b/trombi/models.py @@ -31,7 +31,7 @@ from django.core.exceptions import ValidationError from datetime import timedelta, date from core.models import User -from core.utils import get_start_of_semester, get_semester +from core.utils import get_start_of_semester, get_semester_code from club.models import Club @@ -164,14 +164,14 @@ class TrombiUser(models.Model): if m.description: role += " (%s)" % m.description if m.end_date: - end_date = get_semester(m.end_date) + end_date = get_semester_code(m.end_date) else: end_date = "" TrombiClubMembership( user=self, club=str(m.club), role=role[:64], - start=get_semester(m.start_date), + start=get_semester_code(m.start_date), end=end_date, ).save()