data.py 21.8 KB
Newer Older
1
import datetime
2
import glob
3
import json
4
import math
Kento HASEGAWA's avatar
Kento HASEGAWA committed
5
import os
6
import queue
Kento HASEGAWA's avatar
Kento HASEGAWA committed
7
import re
8
import subprocess
9 10
import time
import uuid
Kento HASEGAWA's avatar
Kento HASEGAWA committed
11

12
class Problem(object):
Kento HASEGAWA's avatar
Kento HASEGAWA committed
13

14
    def __init__(self, problem_path, solution_path=None):
Kento HASEGAWA's avatar
Kento HASEGAWA committed
15

16
        self.path = problem_path
Kento HASEGAWA's avatar
Kento HASEGAWA committed
17 18 19
        self.name = ''
        self.size = (0, 0)
        self.block_num = 0
Kento HASEGAWA's avatar
Kento HASEGAWA committed
20
        self.blocks = dict()
21
        self.tile_num = 0
22
        self.problem = ''
23
        self.solutions = dict()
24
        self.partial_solutions = list()
25
        self.solution_path = solution_path
26
        self.best_solution = None
27 28 29
        self.line_numbers = list()
        self.connection = tuple()
        self.block_groups = list()
30
        self.null_blocks = list()
Kento HASEGAWA's avatar
Kento HASEGAWA committed
31

32
        self._load_problem(problem_path)
33 34

        if not self.solution_path is None:
35
            solution_files = glob.glob(f'{self.solution_path}/{self.name}/tmp-*.json')
36 37 38 39 40
            for v in solution_files:
                solution = Solution(solution_file=v)
                solution_id = solution.get_id()
                print(f'I: Put a solution: {solution_id}')
                self.solutions[solution_id] = solution
Kento HASEGAWA's avatar
Kento HASEGAWA committed
41 42 43 44
    
    @property
    def size_str(self):
        return f'{self.size[0]}X{self.size[1]}'
45 46 47 48 49 50 51 52 53 54 55
    
    @property
    def board_area(self):
        return self.size[0] * self.size[1]

    @property
    def filling_rate(self):
        if self.board_area == 0:
            return 0
        else:
            return self.tile_num / self.board_area
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84

    @property
    def problem_text(self):
        
        def intplus(v):
            if v.isdecimal():
                return int(v)
            else:
                return v

        _text = ''

        _text += f'SIZE {self.size_str}\n'
        _text += f'BLOCK_NUM {self.block_num}\n'
        _text += '\n'

        for bi, block in self.blocks.items():
            _text += f'BLOCK#{bi} {block["w"]}X{block["h"]}\n'
            for cr in block['cells']:
                for cci, cc in enumerate(cr):
                    if cci > 0:
                        _text += ','
                    if isinstance(cc, int) and cc > 0:
                        cc = self.line_numbers.index(cc)
                    _text += f'{cc}'
                _text += '\n'
            _text += '\n'
        
        return _text
85 86 87 88 89 90 91 92 93 94

    @property
    def group_problem_text(self):

        group_problems = list()

        for g in self.block_groups:
            problem_text = ''
            num_tiles = 0
            line_remap_list = list()
95 96 97
            line_remap_list.append(0)
            block_remap_list = list()
            block_remap_list.append(0)
98 99
            block_text = ''
            for bi, bn in enumerate(g):
100
                block_remap_list.append(bn)
101 102 103 104 105 106 107 108 109 110 111
                b = self.blocks[bn]
                num_tiles += b['num_tiles']

                block_text += f'BLOCK#{bi+1} {b["w"]}X{b["h"]}\n'

                for br in b['cells']:
                    br_cells = list()
                    for bc in br:
                        if isinstance(bc, int) and bc > 0:
                            if not bc in line_remap_list:
                                line_remap_list.append(bc)
112
                            remapped_index = line_remap_list.index(bc)
113 114 115 116 117 118
                            br_cells.append(str(remapped_index))
                        else:
                            br_cells.append(str(bc))
                    block_text += ','.join(br_cells) + '\n'
                block_text += '\n'
            
119
            # board_xy = math.ceil(2 * math.sqrt(num_tiles))
120 121 122
            group_x = min(math.ceil(3 * math.sqrt(num_tiles)), self.size[0])
            group_y = min(math.ceil(3 * math.sqrt(num_tiles)), self.size[1])
            problem_text += f'SIZE {group_x}X{group_y}\n'
123 124 125 126
            problem_text += f'BLOCK_NUM {len(g)}\n'
            problem_text += '\n'
            problem_text += block_text
            
127
            group_problems.append((problem_text, line_remap_list, block_remap_list))
128 129

        return group_problems
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
    
    @property
    def status(self):
        _status = 'None'
        if self.problem != '':
            _status = 'Ready'
        if len(self.solutions) > 0:
            _solved_count = 0
            _trial_count = 0
            for k, v in self.solutions.items():
                _trial_count += 1
                if v.is_valid_solution():
                    _solved_count += 1
            if _solved_count > 0:
                _status = f'Solved ({_solved_count}/{_trial_count})'
            else:
                _status = f'Tried ({_trial_count})'
        if not self.best_solution is None:
            _status = f'Saved'
            
        return _status
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
    
    @property
    def partial_merge_problem(self):
        text = ''
        bx, by = self.size
        bn = len(self.partial_solutions) + len(self.null_blocks)
        # bn = len(self.partial_solutions)

        text += f'SIZE {bx}X{by}\n'
        text += f'BLOCK_NUM {bn}\n'
        text += '\n'

        parts = list()
        part_id = 0

        for v in self.partial_solutions:
            gb = GroupPart(self, v[-1])
            text += gb.group_block_text
            text += '\n'
            parts.append(gb.get_dict())
            part_id += 1
        
        for v in self.null_blocks:
            nb = NullBlockPart(self, self.blocks[v], part_id)
            text += nb.nullblock_text
            text += '\n'
            parts.append(nb.get_dict())
            part_id += 1
        
        return {
            'name': self.name,
            'size': self.size,
            'size_str': self.size_str,
            'block_num': bn,
            'problem': text,
            'group_problems': None,
            'merge_problem': True,
            'parts': parts,
            'status': self.status
        }
Kento HASEGAWA's avatar
Kento HASEGAWA committed
191

192
    def _load_problem(self, path):
Kento HASEGAWA's avatar
Kento HASEGAWA committed
193 194

        with open(path, 'r') as fp:
195
            q_text = fp.read()
Kento HASEGAWA's avatar
Kento HASEGAWA committed
196
        
Kento HASEGAWA's avatar
Kento HASEGAWA committed
197 198
        q_lines = q_text.splitlines()

Kento HASEGAWA's avatar
Kento HASEGAWA committed
199 200
        board_size = [0, 0]
        block_num = 0
Kento HASEGAWA's avatar
Kento HASEGAWA committed
201 202
        blocks = dict()

203 204
        tile_num = 0

Kento HASEGAWA's avatar
Kento HASEGAWA committed
205 206 207 208 209
        def intplus(v):
            if v.isdecimal():
                return int(v)
            else:
                return v
Kento HASEGAWA's avatar
Kento HASEGAWA committed
210

211 212 213 214 215
        line_number_list = [0]
        
        block_to_line = dict()
        line_to_block = dict()

Kento HASEGAWA's avatar
Kento HASEGAWA committed
216 217 218
        li = 0
        while li < len(q_lines):
            _l = q_lines[li]
Kento HASEGAWA's avatar
Kento HASEGAWA committed
219 220 221 222 223
            if "SIZE" in _l:
                board_size_str = _l.strip().split()[1]
                board_size = [int(v) for v in board_size_str.split('X')]
            if 'BLOCK_NUM' in _l:
                block_num = int(_l.strip().split()[1])
Kento HASEGAWA's avatar
Kento HASEGAWA committed
224 225 226 227 228 229 230 231 232 233
            if 'BLOCK#' in _l:
                p = r'BLOCK#([0-9]+) +([0-9]+)X([0-9]+)'
                m = re.match(p, _l.strip())
                bi = int(m.group(1))
                bw = int(m.group(2))
                bh = int(m.group(3))
                blocks[bi] = {
                    'index': bi,
                    'w': bw,
                    'h': bh,
234 235
                    'cells': list(),
                    'num_tiles': 0
Kento HASEGAWA's avatar
Kento HASEGAWA committed
236
                }
237
                num_block_tile = 0
238
                is_null_block = True
Kento HASEGAWA's avatar
Kento HASEGAWA committed
239 240 241
                for _h in range(bh):
                    li += 1
                    _l = q_lines[li].strip()
242 243 244 245
                    _block_row = []
                    for v in _l.split(','):
                        _line_num = intplus(v.strip())
                        if isinstance(_line_num, int) and _line_num > 0:
246
                            is_null_block = False
247 248 249 250
                            # Line number conversion
                            if not _line_num in line_number_list:
                                line_number_list.append(_line_num)
                            # _line_num = line_number_list.index(_line_num)
251
                            num_block_tile += 1
252 253 254 255 256 257 258 259

                            # Make connection list
                            if not bi in block_to_line:
                                block_to_line[bi] = list()
                            block_to_line[bi].append(_line_num)
                            if not _line_num in line_to_block:
                                line_to_block[_line_num] = list()
                            line_to_block[_line_num].append(bi)
260 261
                        elif _line_num == '+':
                            num_block_tile += 1
262 263
                        _block_row.append(_line_num)
                    blocks[bi]['cells'].append(_block_row)
264
                blocks[bi]['num_tiles'] = num_block_tile
265 266
                if is_null_block:
                    self.null_blocks.append(bi)
267
                tile_num += num_block_tile
Kento HASEGAWA's avatar
Kento HASEGAWA committed
268 269

            li += 1
Kento HASEGAWA's avatar
Kento HASEGAWA committed
270 271 272 273 274
        
        name = os.path.splitext(os.path.basename(path))[0]

        self.size = board_size
        self.block_num = block_num
Kento HASEGAWA's avatar
Kento HASEGAWA committed
275
        self.blocks = blocks
276
        self.tile_num = tile_num
Kento HASEGAWA's avatar
Kento HASEGAWA committed
277
        self.name = name
278
        self.problem = q_text
279 280
        self.line_numbers = line_number_list
        self.connection = (line_to_block, block_to_line)
281

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
        self._analyze_block_groups()

    def _analyze_block_groups(self):
                
        traversed_block = list()

        line_to_block = self.connection[0]
        block_to_line = self.connection[1]

        block_groups = list()

        for b in block_to_line.keys():

            if b in traversed_block:
                continue

            traverse_block_queue = queue.Queue()
            traverse_block_queue.put(b)

            target_group_blocks = list()

            while not traverse_block_queue.empty():

                target_block = traverse_block_queue.get()
                target_block_lines = block_to_line[target_block]

                if target_block in traversed_block:
                    continue

                traversed_block.append(target_block)
                target_group_blocks.append(target_block)

                for line in target_block_lines:
                    corresponding_blocks = line_to_block[line]
                    if corresponding_blocks[0] == target_block:
                        next_block = corresponding_blocks[1]
                    else:
                        next_block = corresponding_blocks[0]

                    if next_block in traversed_block:
                        continue
                    else:
                        traverse_block_queue.put(next_block)

            block_groups.append(target_group_blocks)
327
            self.partial_solutions.append(list())
328 329 330

        self.block_groups = block_groups

331 332 333 334 335 336
    def get_dict(self):
        return {
            'name': self.name,
            'size': self.size,
            'size_str': self.size_str,
            'block_num': self.block_num,
337
            'problem': self.problem,
338 339
            'group_problems': self.group_problem_text,
            'status': self.status
340
        }
Kento HASEGAWA's avatar
Kento HASEGAWA committed
341 342 343 344 345 346 347 348 349
    
    def get_d3json(self):

        return {
            'block': self.blocks,
            'w': self.size[0],
            'h': self.size[1],
            'n': self.block_num
        }
350 351 352 353 354 355 356

    def put_solution(self, data):
        solution = Solution(data)
        solution_id = solution.get_id()
        print(f'I: Put a solution: {solution_id}')
        self.solutions[solution_id] = solution

357
        if solution.status == 'done':
Kento HASEGAWA's avatar
Kento HASEGAWA committed
358 359 360
            outdir = f"{self.solution_path}/{self.name}"
            if not os.path.exists(outdir):
                os.mkdir(outdir)
361
            problem_path = self.path
Kento HASEGAWA's avatar
Kento HASEGAWA committed
362
            solution_path = f'{outdir}/tmp-{solution.request_id}-{solution.worker}.txt'
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
            with open(solution_path, 'w') as fp:
                fp.write(solution.solution)

            BASEDIR = os.path.abspath(os.path.dirname(__file__))
            NLCHECK = f"{BASEDIR}/../adc2019/server/adc2019.py"

            exec_cmd = f"python3 {NLCHECK} --Q-file {problem_path} --A-file {solution_path}".strip()
            print("ADCCLI: {}".format(exec_cmd))
            p = subprocess.run(exec_cmd, stdout=subprocess.PIPE, shell=True)
            try:
                res = float(p.stdout.decode().strip())
            except:
                res = -1
            solution.nlcheck = res
    
378 379
        # Save solution
        solution.save(self.solution_path)
380

381 382 383 384
    def put_partial_solution(self, data):
    
        idx = int(data['part_id'])
        self.partial_solutions[idx].append(data)
385 386 387 388
        
        outdir = f"{self.solution_path}/{self.name}"
        if not os.path.exists(outdir):
            os.mkdir(outdir)
389
        outpath = f"{outdir}/part-{data['request_id']}-{data['worker']}-p{data['part_id']}.json".replace(":", ".")
390 391 392
        with open(outpath, 'w') as fp:
            json.dump(self.get_dict(), fp, indent=4)

393 394 395 396 397
        if all([len(v)>0 for v in self.partial_solutions]):
            return True
        else:
            return False
    
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
    def save_best_solution(self):

        best_score = None
        for k, v in self.solutions.items():
            if v.is_valid_solution():
                _score = v.score
                if (best_score is None) or (_score < best_score[1]):
                    best_score = (k, _score)

        if best_score is not None:

            solution_name = self.name
            outpath = f'{self.solution_path}/submit'

            if not os.path.exists(outpath):
                os.mkdir(outpath)
            
            best_solution_key = best_score[0]

            with open(f'{outpath}/{solution_name}.txt', 'w') as fp:
                fp.write(self.solutions[best_solution_key].solution)

            with open(f'{outpath}/{solution_name}.json', 'w') as fp:
                json.dump(self.solutions[best_solution_key].get_dict(), fp, indent=4)
            
            self.best_solution = best_solution_key
424

425 426
    def get_solutions(self):
        return self.solutions
Kento HASEGAWA's avatar
Kento HASEGAWA committed
427 428 429 430 431 432
    
    def get_solution(self, solution_id):
        if solution_id in self.solutions:
            return self.solutions[solution_id]
        else:
            return None
433 434 435

class Solution(object):

436 437 438 439 440
    def __init__(self, data=None, solution_file=None):

        if not solution_file is None:
            with open(solution_file, 'r') as fp:
                data = json.load(fp)
441

442
        self.problem = data['problem']
443 444 445 446
        self.request_id = data['request_id']
        self.worker = data['worker']
        self.elapsed_time = data['elapsed_time']
        self.solution = data['solution']
447
        self.status = data['status']
448 449 450 451 452
        
        if 'nlcheck' in data:
            self.nlcheck = data['nlcheck']
        else:
            self.nlcheck = -1
453

454 455 456
        self.size = (None, None)
        self.map = None
        self.block = dict()
457

458 459 460 461 462 463 464
        if solution_file is None:
            self.timestamp = time.time()
            self._id = str(uuid.uuid4())
            
        else:
            self.timestamp = data['timestamp']
            self._id = data['id']
465 466

        self._parse_solution()
467

468 469 470 471 472
    def _parse_solution(self):

        if self.status != 'done':
            return

Kento HASEGAWA's avatar
Kento HASEGAWA committed
473 474 475 476 477 478 479 480 481 482 483
        board_size = [0, 0]
        block_num = 0

        _lines = self.solution.splitlines()

        li = 0

        bw, bh = 0, 0
        bmap = list()
        bposition = dict()

484 485
        state = 0

Kento HASEGAWA's avatar
Kento HASEGAWA committed
486 487
        while li < len(_lines):
            _l = _lines[li].strip()
488
            if (state == 0) and ('SIZE' in _l):
Kento HASEGAWA's avatar
Kento HASEGAWA committed
489 490 491 492 493 494 495 496
                board_size_str = _l.strip().split()[1]
                board_solution_size = [int(v) for v in board_size_str.split('X')]
                bw = board_solution_size[0]
                bh = board_solution_size[1]
                for _h in range(bh):
                    li += 1
                    _l = _lines[li].strip()
                    bmap.append([int(v.strip()) for v in _l.split(',')])
497 498
                state = 1
            if (state == 1) and ('BLOCK' in _l):
Kento HASEGAWA's avatar
Kento HASEGAWA committed
499 500 501 502 503 504 505 506 507 508 509 510 511
                p = r'BLOCK#([0-9]+) +@\(([0-9]+), *([0-9]+)\)'
                m = re.match(p, _l.strip())
                bi = int(m.group(1))
                bx = int(m.group(2))
                by = int(m.group(3))
                bposition[bi] = {
                    'index': bi,
                    'x': bx,
                    'y': by,
                }

            li += 1

512 513 514 515 516 517
            self.size = (bw, bh)
            self.map = bmap
            self.block = bposition

    @property
    def score(self):
518
        if self.is_valid_solution():
519 520 521
            return self.size[0] * self.size[1]
        else:
            return None
522 523 524 525 526 527 528
    
    @property
    def size_str(self):
        if self.is_valid_solution():
            return f'{self.size[0]}X{self.size[1]}'
        else:
            return '-'
529

530 531 532 533
    @property
    def nlcheck_str(self):
        return f'{self.nlcheck:.4f}'

534 535 536 537 538 539 540
    def is_valid_solution(self):
        return self.status == 'done'

    def get_id(self):
        return self._id
    
    def get_dict(self):
Kento HASEGAWA's avatar
Kento HASEGAWA committed
541
        return {
542 543 544 545 546 547
            'id': self._id,
            'timestamp': self.timestamp,
            'request_id': self.request_id,
            'worker': self.worker,
            'elapsed_time': self.elapsed_time,
            'problem': self.problem,
548
            'solution': self.solution,
549 550
            'status': self.status,
            'nlcheck': self.nlcheck
Kento HASEGAWA's avatar
Kento HASEGAWA committed
551 552
        }
    
553 554
    def get_d3json(self):

Kento HASEGAWA's avatar
Kento HASEGAWA committed
555
        if self.status == 'done':
556 557 558 559 560 561 562 563 564 565 566 567 568 569
            return {
                'w': self.size[0],
                'h': self.size[1],
                'map': self.map,
                'block': self.block
            }
        else:
            return {
                'w': 0,
                'h': 0,
                'map': [[]],
                'block': dict()
            }
    
570 571 572 573
    def save(self, basedir):
        outdir = f"{basedir}/{self.problem}"
        if not os.path.exists(outdir):
            os.mkdir(outdir)
574
        outpath = f"{outdir}/tmp-{self.request_id}-{self.worker}.json".replace(":", ".")
575 576
        with open(outpath, 'w') as fp:
            json.dump(self.get_dict(), fp, indent=4)
577 578 579 580 581 582 583

    @property
    def timestamp_str(self):
        dt = datetime.datetime.fromtimestamp(
            self.timestamp,
            datetime.timezone(datetime.timedelta(hours=9)))
        return dt.strftime('%H:%M:%S.%f')
584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712

class GroupPart(object):

    def __init__(self, problem, solution):

        self.problem = problem
        self.solution = solution['solution']
        self.part_id = solution['part_id']
        self.line_map = solution['line_map']
        self.block_map = solution['block_map']

        self.remap = None
        self.remap_block = dict()
    
    @property
    def group_block_text(self):
        
        data = {
            'problem': '',
            'request_id': 0,
            'worker': 'group',
            'elapsed_time': 0,
            'solution': self.solution,
            'status': 'done'
        }

        s = Solution(data)
        bx, by = s.size

        text = ''
        text += f'BLOCK#{self.part_id+1} {bx}X{by}\n'

        _map = s.map
        _remap = list()
        for _r in s.map:
            _remap.append([0 for v in _r])

        _remap_block = dict()

        # ついでにremapもやっておく
        for _bi, _bv in s.block.items():
            bi = self.block_map[_bi]
            _remap_block[bi] = (_bv['x'], _bv['y'])
            b = self.problem.blocks[bi]
            __x = _bv['x']
            __y = _bv['y']
            for _cy, _cr in enumerate(b['cells']):
                for _cx, _cc in enumerate(_cr):
                    if _cc != 0:
                        if _cc != '+':
                            _remap[__y+_cy][__x+_cx] = _cc
                        _map[__y+_cy][__x+_cx] = '+'
        
        for _y, _r in enumerate(_map):
            for _x, _c in enumerate(_r):
                l = _map[_y][_x]
                if l != 0:
                    if l != '+':
                        _remap[_y][_x] = self.line_map[l]
                    _map[_y][_x] = '+'
            text += ','.join([str(v) for v in _map[_y]])
            text += '\n'
        
        self.remap_block = _remap_block
        self.remap = _remap

        return text

    def get_dict(self):
        return {
            'problem': self.group_block_text,
            'part_id': self.part_id,
            'solution': self.solution,
            'line_map': self.line_map,
            'block_map': self.block_map,
            'remap_block': self.remap_block,
            'remap': self.remap
        }

class NullBlockPart(object):
    
    def __init__(self, problem, nullblock, part_id):

        self.problem = problem
        self.nullblock = nullblock
        self.part_id = part_id

        self.remap = None
        self.remap_block = dict()
    
    @property
    def nullblock_text(self):

        bx = self.nullblock['w']
        by = self.nullblock['h']

        text = ''
        text += f'BLOCK#{self.part_id+1} {bx}X{by}\n'

        _map = self.nullblock['cells']
        _remap = list()
        for _r in _map:
            _remap.append([0 for v in _r])
            
        for _y, _r in enumerate(_map):
            for _x, _c in enumerate(_r):
                l = _map[_y][_x]
                if l != 0:
                    if l != '+':
                        _remap[_y][_x] = self.line_map[l]
                    _map[_y][_x] = '+'
            text += ','.join([str(v) for v in _map[_y]])
            text += '\n'

        self.remap = _remap

        return text

    def get_dict(self):
        bi = self.nullblock['index']
        return {
            'problem': self.nullblock_text,
            'part_id': self.part_id,
            'solution': '',
            'line_map': [0],
            'block_map': [0, bi],
            'remap_block': {bi: (0, 0)},
            'remap': self.remap
        }