solve.h (17864B)
1 #define H48_STARTING_MOVES 4 2 3 #if H48_STARTING_MOVES == 3 4 #define H48_STARTING_CUBES 3240 /* Number of 3-move sequences */ 5 #elif H48_STARTING_MOVES == 4 6 #define H48_STARTING_CUBES 43254 /* Number of 4-move sequences */ 7 #endif 8 9 #define H48_SORT_TASKS_MIN_DEPTH 16 10 #define H48_LOG_PROGRESS_MIN_DEPTH 15 11 12 typedef struct { 13 cube_t cube; 14 uint8_t moves[H48_STARTING_MOVES]; 15 int64_t rank; 16 uint64_t tmask[H48_STARTING_MOVES]; 17 } solve_h48_task_t; 18 19 typedef struct { 20 cube_t start_cube; 21 cube_t cube; 22 cube_t inverse; 23 int8_t target_depth; 24 solution_moves_t *solution_moves; 25 solution_settings_t *solution_settings; 26 const uint64_t *tmask; 27 solution_list_t *solution_list; 28 int8_t lb_normal; 29 int8_t lb_inverse; 30 bool use_lb_normal; 31 bool use_lb_inverse; 32 uint8_t h; 33 uint8_t base; 34 const uint32_t *cocsepdata; 35 const unsigned char *h48data; 36 const unsigned char *h48data_fallback_eoesep; 37 uint64_t movemask_normal; 38 uint64_t movemask_inverse; 39 uint64_t nodes_visited; 40 uint64_t table_fallbacks; 41 uint64_t table_lookups; 42 int8_t threads; 43 int ntasks; 44 solve_h48_task_t *tasks; 45 int thread_id; 46 wrapthread_define_struct_mutex_t(*solutions_mutex); 47 wrapthread_atomic int *status; 48 wrapthread_atomic bool thread_done; 49 } dfsarg_solve_h48_t; 50 51 typedef struct { 52 cube_t cube; 53 int8_t nmoves; 54 uint8_t moves[H48_STARTING_MOVES]; 55 int8_t minmoves; 56 int8_t maxmoves; 57 int8_t *shortest_sol; 58 uint64_t tmask[H48_STARTING_MOVES]; 59 } dfsarg_solve_h48_maketasks_t; 60 61 STATIC long long solve_h48_dispatch(oriented_cube_t, const char *, unsigned, 62 unsigned, unsigned, unsigned, unsigned, unsigned, unsigned long long, 63 const unsigned char *, unsigned, char *, 64 long long [static NISSY_SIZE_SOLVE_STATS], int (*)(void *), void *); 65 STATIC_INLINE bool solve_h48_stop(dfsarg_solve_h48_t [static 1]); 66 STATIC int64_t solve_h48_maketasks( 67 dfsarg_solve_h48_t [static 1], dfsarg_solve_h48_maketasks_t [static 1], 68 solve_h48_task_t [static H48_STARTING_CUBES], int [static 1]); 69 STATIC void *solve_h48_runthread(void *); 70 STATIC int64_t solve_h48_dfs(dfsarg_solve_h48_t [static 1]); 71 STATIC void solve_h48_log_solutions(solution_list_t [static 1], size_t); 72 STATIC int solve_h48_compare_tasks(const void *, const void *); 73 STATIC int64_t solve_h48(oriented_cube_t, uint8_t, uint8_t, uint64_t, uint8_t, 74 uint8_t, uint64_t, const unsigned char *, size_t, char *, 75 long long [static NISSY_SIZE_SOLVE_STATS], int (*)(void *), void *); 76 77 STATIC long long solve_h48_dispatch( 78 oriented_cube_t oc, 79 const char *solver, 80 unsigned nissflag, 81 unsigned minmoves, 82 unsigned maxmoves, 83 unsigned maxsols, 84 unsigned optimal, 85 unsigned threads, 86 unsigned long long data_size, 87 const unsigned char *data, 88 unsigned sols_size, 89 char *sols, 90 long long stats[static NISSY_SIZE_SOLVE_STATS], 91 int (*poll_status)(void *), 92 void *poll_status_data 93 ) 94 { 95 uint8_t h; 96 long long err; 97 98 err = parse_h48h(solver, &h); 99 if (err != NISSY_OK) 100 return err; 101 102 return solve_h48(oc, minmoves, maxmoves, maxsols, optimal, threads, 103 data_size, data, sols_size, sols, stats, 104 poll_status, poll_status_data); 105 } 106 107 STATIC_INLINE bool 108 solve_h48_stop(dfsarg_solve_h48_t arg[static 1]) 109 { 110 uint32_t data, data_inv; 111 int64_t coord; 112 int8_t target, nh, n; 113 uint8_t pval, pval_min, pval_eoesep; 114 115 arg->movemask_normal = arg->movemask_inverse = MM18_ALLMOVES; 116 arg->nodes_visited++; 117 118 n = arg->solution_moves->nmoves + arg->solution_moves->npremoves; 119 target = arg->target_depth - n; 120 121 /* We'll never get a bound higher than base + 3 */ 122 if (arg->base + 3 <= target) 123 return false; 124 125 /* Preliminary probing using last computed bound, if possible */ 126 127 if ((arg->use_lb_normal && arg->lb_normal > target) || 128 (arg->use_lb_inverse && arg->lb_inverse > target)) 129 return true; 130 131 /* Preliminary corner probing */ 132 133 if (get_h48_cdata(arg->cube, arg->cocsepdata, &data) > target || 134 get_h48_cdata(arg->inverse, arg->cocsepdata, &data_inv) > target) 135 return true; 136 137 /* Inverse probing */ 138 139 if (!arg->use_lb_inverse) { 140 arg->table_lookups++; 141 arg->use_lb_inverse = true; 142 coord = coord_h48_edges( 143 arg->inverse, COCLASS(data_inv), TTREP(data_inv), arg->h); 144 pval = get_h48_pval_and_min(arg->h48data, coord, &pval_min); 145 146 if (pval == 0) { 147 arg->table_fallbacks++; 148 149 pval_eoesep = get_eoesep_pval_cube( 150 arg->h48data_fallback_eoesep, arg->inverse); 151 pval = MAX(pval_min, pval_eoesep); 152 } else { 153 pval += arg->base; 154 } 155 156 arg->lb_inverse = pval; 157 } 158 159 if (arg->lb_inverse > target) 160 return true; 161 nh = arg->lb_inverse == target; 162 arg->movemask_normal = nh * MM18_NOHALFTURNS + (1-nh) * MM18_ALLMOVES; 163 164 /* Normal probing */ 165 166 if (!arg->use_lb_normal) { 167 arg->table_lookups++; 168 arg->use_lb_normal = true; 169 coord = coord_h48_edges( 170 arg->cube, COCLASS(data), TTREP(data), arg->h); 171 pval = get_h48_pval_and_min(arg->h48data, coord, &pval_min); 172 173 if (pval == 0) { 174 arg->table_fallbacks++; 175 176 pval_eoesep = get_eoesep_pval_cube( 177 arg->h48data_fallback_eoesep, arg->cube); 178 pval = MAX(pval_min, pval_eoesep); 179 } else { 180 pval += arg->base; 181 } 182 183 arg->lb_normal = pval; 184 } 185 186 if (arg->lb_normal > target) 187 return true; 188 nh = arg->lb_normal == target; 189 arg->movemask_inverse = nh * MM18_NOHALFTURNS + (1-nh) * MM18_ALLMOVES; 190 191 return false; 192 } 193 194 STATIC int64_t 195 solve_h48_dfs(dfsarg_solve_h48_t arg[static 1]) 196 { 197 int64_t ret, n; 198 uint8_t m, nm, lbn, lbi, t; 199 uint64_t mm_normal, mm_inverse; 200 bool ulbi, ulbn; 201 cube_t backup_cube, backup_inverse; 202 203 nm = arg->solution_moves->nmoves + arg->solution_moves->npremoves; 204 if (equal(arg->cube, SOLVED_CUBE)) { 205 if (arg->target_depth != nm) 206 return 0; 207 wrapthread_mutex_lock(arg->solutions_mutex); 208 ret = appendsolution(arg->solution_moves, H48_STARTING_MOVES, 209 arg->tmask, arg->solution_settings, arg->solution_list); 210 wrapthread_mutex_unlock(arg->solutions_mutex); 211 return ret; 212 } 213 214 if (solve_h48_stop(arg)) 215 return 0; 216 217 t = arg->solution_list->shortest_sol + arg->solution_settings->optimal; 218 if (nm + 1 > MIN(t, arg->target_depth) || 219 arg->solution_list->nsols >= arg->solution_settings->maxsolutions) 220 return 0; 221 222 backup_cube = arg->cube; 223 backup_inverse = arg->inverse; 224 lbn = arg->lb_normal; 225 lbi = arg->lb_inverse; 226 ulbn = arg->use_lb_normal; 227 ulbi = arg->use_lb_inverse; 228 229 ret = 0; 230 mm_normal = arg->movemask_normal; 231 if (arg->solution_moves->nmoves > 0) { 232 m = arg->solution_moves->moves[arg->solution_moves->nmoves-1]; 233 mm_normal &= allowedmask[movebase(m)]; 234 } 235 mm_inverse = arg->movemask_inverse; 236 if (arg->solution_moves->npremoves > 0) { 237 m = arg->solution_moves->premoves[arg->solution_moves->npremoves-1]; 238 mm_inverse &= allowedmask[movebase(m)]; 239 } 240 if (popcount_u32(mm_normal) <= popcount_u32(mm_inverse)) { 241 arg->solution_moves->nmoves++; 242 for (m = 0; m < 18; m++) { 243 if (!(mm_normal & MM_SINGLE(m))) 244 continue; 245 arg->solution_moves->moves[ 246 arg->solution_moves->nmoves-1] = m; 247 arg->cube = move(backup_cube, m); 248 arg->inverse = premove(backup_inverse, m); 249 arg->lb_inverse = lbi; 250 arg->use_lb_normal = false; 251 arg->use_lb_inverse = ulbi && m % 3 == 1; 252 n = solve_h48_dfs(arg); 253 if (n < 0) 254 return n; 255 ret += n; 256 } 257 arg->solution_moves->nmoves--; 258 } else { 259 arg->solution_moves->npremoves++; 260 for (m = 0; m < 18; m++) { 261 if(!(mm_inverse & MM_SINGLE(m))) 262 continue; 263 arg->solution_moves->premoves[ 264 arg->solution_moves->npremoves-1] = m; 265 arg->inverse = move(backup_inverse, m); 266 arg->cube = premove(backup_cube, m); 267 arg->lb_normal = lbn; 268 arg->use_lb_inverse = false; 269 arg->use_lb_normal = ulbn && m % 3 == 1; 270 n = solve_h48_dfs(arg); 271 if (n < 0) 272 return n; 273 ret += n; 274 } 275 arg->solution_moves->npremoves--; 276 } 277 278 arg->cube = backup_cube; 279 arg->inverse = backup_inverse; 280 281 return ret; 282 } 283 284 STATIC void * 285 solve_h48_runthread(void *arg) 286 { 287 int i, j; 288 uint8_t lastmove; 289 int64_t nprev; 290 dfsarg_solve_h48_t *dfsarg; 291 292 dfsarg = (dfsarg_solve_h48_t *)arg; 293 294 nprev = 0; 295 for (i = dfsarg->thread_id; i < dfsarg->ntasks; i += dfsarg->threads) { 296 if (*dfsarg->status == NISSY_STATUS_STOP) 297 goto solve_h48_runthread_end; 298 while (*dfsarg->status == NISSY_STATUS_PAUSE) 299 msleep(BASE_SLEEP_TIME); 300 301 solution_moves_reset(dfsarg->solution_moves); 302 memcpy(dfsarg->solution_moves->moves, 303 dfsarg->tasks[i].moves, H48_STARTING_MOVES); 304 dfsarg->solution_moves->nmoves = H48_STARTING_MOVES; 305 306 dfsarg->cube = dfsarg->start_cube; 307 for (j = 0; j < H48_STARTING_MOVES; j++) 308 dfsarg->cube = 309 move(dfsarg->cube, dfsarg->tasks[i].moves[j]); 310 dfsarg->inverse = inverse(dfsarg->cube); 311 312 dfsarg->lb_normal = 0; 313 dfsarg->lb_inverse = 0; 314 dfsarg->use_lb_normal = false; 315 dfsarg->use_lb_inverse = false; 316 dfsarg->movemask_normal = MM18_ALLMOVES; 317 dfsarg->movemask_inverse = MM18_ALLMOVES; 318 dfsarg->tmask = dfsarg->tasks[i].tmask; 319 320 solve_h48_dfs(dfsarg); 321 322 /* 323 We compute the "rank" of each taks, which is used in the next 324 step of the IDFS to sort them. This heuristically leads us 325 faster to a solution. The rank is computed by taking the 326 number of nodes visited, adjusted by a factor of about 327 sqrt(2)/sqrt(3) because there are more sequences starting with 328 U, R and F than with D, L and B, as we don't allow e.g. both 329 U D and D U, but only U D. 330 This trick was suggested by Chen Shuang, the implementation is 331 inspired by Andrew Skalski's vcube. 332 */ 333 lastmove = dfsarg->tasks[i].moves[H48_STARTING_MOVES-1]; 334 dfsarg->tasks[i].rank = (dfsarg->nodes_visited - nprev) * 335 (movebase(lastmove) % 2 == 0 ? 47525 : 58206); 336 nprev = dfsarg->nodes_visited; 337 } 338 339 solve_h48_runthread_end: 340 dfsarg->thread_done = true; 341 return NULL; 342 } 343 344 STATIC int64_t 345 solve_h48_maketasks( 346 dfsarg_solve_h48_t solve_arg[static 1], 347 dfsarg_solve_h48_maketasks_t mtarg[static 1], 348 solve_h48_task_t tasks[static H48_STARTING_CUBES], 349 int ntasks[static 1] 350 ) 351 { 352 int r; 353 int64_t appret; 354 uint8_t m, t; 355 uint64_t mm; 356 cube_t backup_cube; 357 solution_moves_t moves; 358 359 if (equal(mtarg->cube, SOLVED_CUBE)) { 360 if (mtarg->nmoves > mtarg->maxmoves || 361 mtarg->nmoves < mtarg->minmoves || 362 solutions_done(solve_arg->solution_list, 363 solve_arg->solution_settings, mtarg->nmoves)) 364 return NISSY_OK; 365 366 solution_moves_reset(&moves); 367 moves.nmoves = mtarg->nmoves; 368 memcpy(moves.moves, mtarg->moves, mtarg->nmoves); 369 370 appret = appendsolution(&moves, mtarg->nmoves, mtarg->tmask, 371 solve_arg->solution_settings, solve_arg->solution_list); 372 return appret < 0 ? appret : NISSY_OK; 373 } 374 375 if (mtarg->nmoves == H48_STARTING_MOVES) { 376 tasks[*ntasks].cube = mtarg->cube; 377 memcpy(tasks[*ntasks].moves, mtarg->moves, 378 H48_STARTING_MOVES * sizeof(uint8_t)); 379 memcpy(tasks[*ntasks].tmask, mtarg->tmask, 380 H48_STARTING_MOVES * sizeof(uint64_t)); 381 (*ntasks)++; 382 return NISSY_OK; 383 } 384 385 if (mtarg->nmoves == 0) { 386 mm = MM18_ALLMOVES; 387 } else { 388 m = mtarg->moves[mtarg->nmoves-1]; 389 mm = allowedmask[movebase(m)]; 390 } 391 392 mtarg->tmask[mtarg->nmoves] = symmetry_mask(mtarg->cube); 393 394 mtarg->nmoves++; 395 backup_cube = mtarg->cube; 396 for (m = 0; m < 18; m++) { 397 if (!(mm & MM_SINGLE(m))) 398 continue; 399 400 mtarg->moves[mtarg->nmoves-1] = m; 401 mtarg->cube = move(backup_cube, m); 402 r = solve_h48_maketasks(solve_arg, mtarg, tasks, ntasks); 403 if (r < 0) 404 return r; 405 406 /* Avoid symmetry-equivalent moves from the starting cube */ 407 for (t = 0; t < NTRANS; t++) 408 if (mtarg->tmask[mtarg->nmoves-1] & TM_SINGLE(t)) 409 mm &= ~MM_SINGLE(transform_move(m, t)); 410 } 411 mtarg->nmoves--; 412 mtarg->cube = backup_cube; 413 414 return NISSY_OK; 415 } 416 417 STATIC void 418 solve_h48_log_solutions(solution_list_t s[static 1], size_t e) 419 { 420 size_t i; 421 char b; 422 while (e != s->used) { 423 LOG("[h48 solve] Found solution: "); 424 for (i = e; s->buf[i] != '\n' && s->buf[i] != '\0'; i++) ; 425 b = s->buf[i]; 426 s->buf[i] = '\0'; 427 LOG("%s\n", s->buf + e); 428 s->buf[i] = b; 429 e = i + 1; 430 } 431 } 432 433 STATIC int 434 solve_h48_compare_tasks(const void *x, const void *y) 435 { 436 int64_t nodes_x = ((solve_h48_task_t *)x)->rank; 437 int64_t nodes_y = ((solve_h48_task_t *)y)->rank; 438 439 /* Same as returning nodes_y - nodes_x, but avoids overflow */ 440 return (nodes_x < nodes_y) - (nodes_x > nodes_y); 441 } 442 443 STATIC int64_t 444 solve_h48( 445 oriented_cube_t oc, 446 uint8_t minmoves, 447 uint8_t maxmoves, 448 uint64_t maxsolutions, 449 uint8_t optimal, 450 uint8_t threads, 451 uint64_t data_size, 452 const unsigned char *data, 453 size_t solutions_size, 454 char *solutions, 455 long long stats[static NISSY_SIZE_SOLVE_STATS], 456 int (*poll_status)(void *), 457 void *poll_status_data 458 ) 459 { 460 int i, ntasks; 461 bool td; 462 wrapthread_atomic int status, prev_status; 463 size_t lastused; 464 int8_t d; 465 dfsarg_solve_h48_t arg[THREADS]; 466 solve_h48_task_t tasks[H48_STARTING_CUBES]; 467 dfsarg_solve_h48_maketasks_t mtarg; 468 long double fallback_rate, lookups_per_node; 469 uint64_t offset; 470 uint64_t nodes_visited, table_lookups, table_fallbacks; 471 tableinfo_t info, fbinfo2; 472 const uint32_t *cocsepdata; 473 const unsigned char *h48data; 474 const unsigned char *eoesep; 475 solution_moves_t solution_moves[THREADS]; 476 solution_settings_t settings; 477 solution_list_t sollist; 478 wrapthread_define_var_thread_t(thread[THREADS]); 479 wrapthread_define_var_mutex_t(solutions_mutex); 480 481 if (!solution_list_init(&sollist, solutions_size, solutions)) 482 goto solve_h48_error_solutions_buffer; 483 484 if (readtableinfo_n(data_size, data, 2, &info) != NISSY_OK) 485 goto solve_h48_error_data; 486 487 cocsepdata = (uint32_t *)(data + INFOSIZE); 488 h48data = data + COCSEP_FULLSIZE + INFOSIZE; 489 490 /* Read additional eoesep table */ 491 offset = info.next; 492 if (readtableinfo_n(data_size, data, 3, &fbinfo2) 493 != NISSY_OK) 494 goto solve_h48_error_data; 495 496 /* Some heuristic check to see that it is eoesep */ 497 if (fbinfo2.bits != 4 || fbinfo2.type != TABLETYPE_SPECIAL) 498 goto solve_h48_error_data; 499 eoesep = h48data + offset; 500 501 settings = (solution_settings_t) { 502 .unniss = true, 503 .maxmoves = maxmoves, 504 .maxsolutions = maxsolutions, 505 .optimal = optimal, 506 .orientation = oc.orientation, 507 }; 508 509 for (i = 0; i < threads; i++) { 510 arg[i] = (dfsarg_solve_h48_t) { 511 .start_cube = oc.cube, 512 .cube = oc.cube, 513 .h = info.h48h, 514 .base = info.base, 515 .cocsepdata = cocsepdata, 516 .h48data = h48data, 517 .h48data_fallback_eoesep = eoesep, 518 .solution_moves = &solution_moves[i], 519 .solution_settings = &settings, 520 .solution_list = &sollist, 521 .nodes_visited = 0, 522 .table_fallbacks = 0, 523 .table_lookups = 0, 524 .threads = threads, 525 .thread_id = i, 526 .solutions_mutex = &solutions_mutex, 527 .status = &status, 528 }; 529 530 } 531 532 wrapthread_mutex_init(&solutions_mutex, NULL); 533 534 mtarg = (dfsarg_solve_h48_maketasks_t) { 535 .cube = oc.cube, 536 .nmoves = 0, 537 .minmoves = minmoves, 538 .maxmoves = maxmoves, 539 }; 540 ntasks = 0; 541 solve_h48_maketasks(&arg[0], &mtarg, tasks, &ntasks); 542 if (ntasks < 0) 543 goto solve_h48_error_solutions_buffer; 544 if (solutions_done(&sollist, &settings, 545 MAX(minmoves, H48_STARTING_MOVES))) 546 goto solve_h48_done; 547 548 for (i = 0; i < threads; i++) { 549 arg[i].ntasks = ntasks; 550 arg[i].tasks = tasks; 551 } 552 553 LOG("[H48 solve] Prepared %d tasks\n", ntasks); 554 555 solve_h48_log_solutions(&sollist, 0); 556 lastused = sollist.used; 557 status = poll_status == NULL ? NISSY_STATUS_RUN : 558 poll_status(poll_status_data); 559 if (!NISSY_CANSLEEP) { 560 LOG("[solve h48] Pause / Stop / Resume functionality won't " 561 "be available on this system (can't sleep()).\n"); 562 } 563 for ( 564 d = MAX(minmoves, H48_STARTING_MOVES + 1); 565 !(solutions_done(&sollist, &settings, d)) && 566 status != NISSY_STATUS_STOP; 567 d++ 568 ) { 569 if (d >= H48_LOG_PROGRESS_MIN_DEPTH) { 570 LOG("[H48 solve] Found %" PRIu64 " solutions, " 571 "searching at depth %" PRId8 "\n", 572 sollist.nsols, d); 573 } 574 575 if (d >= H48_SORT_TASKS_MIN_DEPTH) 576 qsort(tasks, ntasks, sizeof(solve_h48_task_t), 577 solve_h48_compare_tasks); 578 579 for (i = 0; i < threads; i++) { 580 arg[i].target_depth = d; 581 arg[i].thread_done = false; 582 wrapthread_create( 583 &thread[i], NULL, solve_h48_runthread, &arg[i]); 584 } 585 586 /* Log solutions and handle pause / stop / resume */ 587 if (poll_status != NULL && d >= 15 && NISSY_CANSLEEP) { 588 td = false; 589 while (!td && status != NISSY_STATUS_STOP) { 590 msleep(BASE_SLEEP_TIME); 591 592 wrapthread_mutex_lock(&solutions_mutex); 593 solve_h48_log_solutions(&sollist, lastused); 594 lastused = sollist.used; 595 wrapthread_mutex_unlock(&solutions_mutex); 596 597 prev_status = status; 598 status = poll_status(poll_status_data); 599 if (status != prev_status) { 600 if (status == NISSY_STATUS_PAUSE) 601 LOG("[H48 solve] Paused\n"); 602 if (status == NISSY_STATUS_RUN) 603 LOG("[H48 solve] Resumed\n"); 604 } 605 606 for (td = true, i = 0; i < threads; i++) 607 td = td && arg[i].thread_done; 608 } 609 } 610 611 for (i = 0; i < threads; i++) 612 wrapthread_join(thread[i], NULL); 613 614 solve_h48_log_solutions(&sollist, lastused); 615 lastused = sollist.used; 616 } 617 618 if (status == NISSY_STATUS_STOP) { 619 LOG("[H48 solve] Received stop request, ending solution " 620 "search early.\n"); 621 } 622 623 solve_h48_done: 624 nodes_visited = table_lookups = table_fallbacks = 0; 625 for (i = 0; i < threads; i++) { 626 nodes_visited += arg[i].nodes_visited; 627 table_fallbacks += arg[i].table_fallbacks; 628 table_lookups += arg[i].table_lookups; 629 } 630 631 stats[0] = nodes_visited; 632 stats[1] = table_lookups; 633 stats[2] = table_fallbacks; 634 lookups_per_node = table_lookups / (long double)nodes_visited; 635 fallback_rate = nodes_visited == 0 ? 0.0 : 636 (table_fallbacks * 100) / (long double)table_lookups; 637 LOG("[H48 solve] Nodes visited: %" PRId64 "\n", nodes_visited); 638 LOG("[H48 solve] Lookups: %" PRId64 " (%.3Lf per node)\n", 639 table_lookups, lookups_per_node); 640 LOG("[H48 solve] Table fallbacks: %" PRId64 " (%.3Lf%%)\n", 641 table_fallbacks, fallback_rate); 642 643 return sollist.nsols; 644 645 solve_h48_error_data: 646 LOG("[H48 solve] Error reading data table\n"); 647 return NISSY_ERROR_DATA; 648 649 solve_h48_error_solutions_buffer: 650 return NISSY_ERROR_BUFFER_SIZE; 651 }