diff --git a/docker-compose.grpc.only.yaml b/docker-compose.grpc.only.yaml index 0d32840..40681bf 100644 --- a/docker-compose.grpc.only.yaml +++ b/docker-compose.grpc.only.yaml @@ -7,10 +7,12 @@ services: context: . dockerfile: api/Dockerfile env_file: .env + restart: always envoy: image: ghcr.io/apoorva64/eternity2/envoy:latest build: context: envoy ports: - - '50052:50052' \ No newline at end of file + - '50052:50052' + restart: always \ No newline at end of file diff --git a/gui/eternitylib/board.py b/gui/eternitylib/board.py index eb84937..08b9db7 100644 --- a/gui/eternitylib/board.py +++ b/gui/eternitylib/board.py @@ -19,7 +19,7 @@ def generate_inner_symbols(size, number_of_symbols): class Board: def __init__(self): - self.pieces = [] + self.pieces: list[Piece] = [] self._size = 0 self._pattern_count = 0 self.hints = [] diff --git a/gui/eternitylib/piece.py b/gui/eternitylib/piece.py index 24968ba..21f10be 100644 --- a/gui/eternitylib/piece.py +++ b/gui/eternitylib/piece.py @@ -8,6 +8,7 @@ from PIL import ImageDraw from eternitylib.pattern import Pattern +from scheduler.solver.v1 import solver_pb2 class Piece: @@ -80,3 +81,13 @@ def __repr__(self): @property def hash(self): return hashlib.md5(".".join(pattern.pattern_code for pattern in self.patterns).encode()).hexdigest() + + + def to_grpc(self): + return solver_pb2.Piece( + top=int(self.patterns[0].pattern_code, 2), + right=int(self.patterns[1].pattern_code, 2), + bottom=int(self.patterns[2].pattern_code, 2), + left=int(self.patterns[3].pattern_code, 2), + + ) \ No newline at end of file diff --git a/gui/requirements.txt b/gui/requirements.txt index 686f568..3018e98 100644 --- a/gui/requirements.txt +++ b/gui/requirements.txt @@ -3,4 +3,5 @@ colour Pillow gradio plotly -kaleido \ No newline at end of file +kaleido +lxml \ No newline at end of file diff --git a/gui/scheduler/README.md b/gui/scheduler/README.md new file mode 100644 index 0000000..ddf092a --- /dev/null +++ b/gui/scheduler/README.md @@ -0,0 +1,5 @@ +# Generate ProtoBuf files + +```bash +python3 -m grpc_tools.protoc --python_out=. --pyi_out=. --grpc_python_out=. ../../api/proto/solver/v1/solver.proto --proto_path ../api/proto/ +``` \ No newline at end of file diff --git a/gui/scheduler/__init__.py b/gui/scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/scheduler/main.py b/gui/scheduler/main.py new file mode 100644 index 0000000..6c438e1 --- /dev/null +++ b/gui/scheduler/main.py @@ -0,0 +1,33 @@ +from eternitylib.board import Board +from solve import solve +from solver.v1 import solver_pb2 + +servers = [ + 'localhost:50051', + 'node-apoorva2.k3s.hs.ozeliurs.com:50051', + 'node-apoorva3-abklev50.k3s.hs.ozeliurs.com:50051', + # 'vmpx15.polytech.hs.ozeliurs.com:50051' +] + + + +if __name__ == '__main__': + board = Board() + board.generate(16, 22) + + pieces = [piece.to_grpc() for piece in board.pieces] + + # Create a request + request = solver_pb2.SolverSolveRequest( + pieces=pieces, + threads=50, + hash_threshold=7, + wait_time=1000, + use_cache=True, + cache_pull_interval=10 + ) + + # Call the server + response = solve(servers, request) + + print(response) diff --git a/gui/scheduler/solve.py b/gui/scheduler/solve.py new file mode 100644 index 0000000..38be2d4 --- /dev/null +++ b/gui/scheduler/solve.py @@ -0,0 +1,67 @@ +from typing import Dict + +import grpc +import solver.v1.solver_pb2_grpc as solver_pb2_grpc +import time +# launch in threads +import threading + +from solver.v1 import solver_pb2 +import logging + +logger = logging.getLogger(__name__) +logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) + + +def solve(_servers: list[str], _request: solver_pb2.SolverSolveRequest): + stubs = map(lambda _server: (_server, solver_pb2_grpc.SolverStub(grpc.insecure_channel(_server))), _servers) + + # Make the + responses: Dict[str, list[ + solver_pb2.SolverSolveResponse + ]] = {} + for server in _servers: + responses[server] = [] + + found = [False] + + def internal_solve(server_stub: tuple[str, solver_pb2_grpc.SolverStub], _request: solver_pb2.SolverSolveRequest, + _found: list[bool | solver_pb2.SolverSolveResponse]): + _stub = server_stub[1] + _server = server_stub[0] + for _response in _stub.Solve(_request): + responses[_server].append(_response) + if _found[0]: + return + _found[0] = responses[_server][-1] + + threads = [] + for stub in stubs: + thread = threading.Thread(target=internal_solve, args=(stub, _request, found)) + threads.append(thread) + thread.start() + + # if a thread has finished before the others, stop the others + while not found[0]: + time.sleep(1) + # print + for server in _servers: + server_responses = responses[server] + if len(server_responses) > 0: + # count the number of pieces + piece_count = len(server_responses[-1].rotated_pieces) + # count the number of null pieces + null_piece_count = sum([1 for piece in server_responses[-1].rotated_pieces if piece.piece.top == 0 and piece.piece.right == 0 and piece.piece.bottom == 0 and piece.piece.left == 0]) + logger.info(f"Server {server}:") + logger.info(f" Time: {server_responses[-1].time}") + logger.info(f" Hashes per second: {server_responses[-1].hashes_per_second}") + logger.info(f" Hash table size: {server_responses[-1].hash_table_size}") + logger.info(f" Boards per second: {server_responses[-1].boards_per_second}") + logger.info(f" Boards analyzed: {server_responses[-1].boards_analyzed}") + logger.info(f" Hash table hits: {server_responses[-1].hash_table_hits}") + logger.info(f" Placed pieces: {piece_count - null_piece_count}/{piece_count}") + + # stop the other threads + for thread in threads: + thread.join() + return found diff --git a/gui/scheduler/solver/__init__.py b/gui/scheduler/solver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/scheduler/solver/v1/__init__.py b/gui/scheduler/solver/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/scheduler/solver/v1/solver_pb2.py b/gui/scheduler/solver/v1/solver_pb2.py new file mode 100644 index 0000000..9da4d30 --- /dev/null +++ b/gui/scheduler/solver/v1/solver_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: solver/v1/solver.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16solver/v1/solver.proto\x12\tsolver.v1\"\xa2\x01\n\x12SolverSolveRequest\x12 \n\x06pieces\x18\x01 \x03(\x0b\x32\x10.solver.v1.Piece\x12\x0f\n\x07threads\x18\x02 \x01(\r\x12\x16\n\x0ehash_threshold\x18\x03 \x01(\r\x12\x11\n\twait_time\x18\x04 \x01(\r\x12\x11\n\tuse_cache\x18\x05 \x01(\x08\x12\x1b\n\x13\x63\x61\x63he_pull_interval\x18\x06 \x01(\r\"A\n\x05Piece\x12\x0b\n\x03top\x18\x01 \x01(\r\x12\r\n\x05right\x18\x02 \x01(\r\x12\x0e\n\x06\x62ottom\x18\x03 \x01(\r\x12\x0c\n\x04left\x18\x04 \x01(\r\"d\n\x15PieceWithOptionalHint\x12\x1f\n\x05piece\x18\x01 \x01(\x0b\x32\x10.solver.v1.Piece\x12\x0e\n\x01x\x18\x02 \x01(\x05H\x00\x88\x01\x01\x12\x0e\n\x01y\x18\x03 \x01(\x05H\x01\x88\x01\x01\x42\x04\n\x02_xB\x04\n\x02_y\"P\n\x0cRotatedPiece\x12\x1f\n\x05piece\x18\x01 \x01(\x0b\x32\x10.solver.v1.Piece\x12\x10\n\x08rotation\x18\x02 \x01(\r\x12\r\n\x05index\x18\x03 \x01(\r\"K\n\x18SolverStepByStepResponse\x12/\n\x0erotated_pieces\x18\x01 \x03(\x0b\x32\x17.solver.v1.RotatedPiece\"\xd5\x01\n\x13SolverSolveResponse\x12\x0c\n\x04time\x18\x01 \x01(\x01\x12\x19\n\x11hashes_per_second\x18\x02 \x01(\x01\x12\x17\n\x0fhash_table_size\x18\x03 \x01(\r\x12\x19\n\x11\x62oards_per_second\x18\x04 \x01(\x01\x12\x17\n\x0f\x62oards_analyzed\x18\x05 \x01(\r\x12\x17\n\x0fhash_table_hits\x18\x06 \x01(\r\x12/\n\x0erotated_pieces\x18\x07 \x03(\x0b\x32\x17.solver.v1.RotatedPiece2\xaf\x01\n\x06Solver\x12J\n\x05Solve\x12\x1d.solver.v1.SolverSolveRequest\x1a\x1e.solver.v1.SolverSolveResponse\"\x00\x30\x01\x12Y\n\x0fSolveStepByStep\x12\x1d.solver.v1.SolverSolveRequest\x1a#.solver.v1.SolverStepByStepResponse\"\x00\x30\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'solver.v1.solver_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SOLVERSOLVEREQUEST']._serialized_start=38 + _globals['_SOLVERSOLVEREQUEST']._serialized_end=200 + _globals['_PIECE']._serialized_start=202 + _globals['_PIECE']._serialized_end=267 + _globals['_PIECEWITHOPTIONALHINT']._serialized_start=269 + _globals['_PIECEWITHOPTIONALHINT']._serialized_end=369 + _globals['_ROTATEDPIECE']._serialized_start=371 + _globals['_ROTATEDPIECE']._serialized_end=451 + _globals['_SOLVERSTEPBYSTEPRESPONSE']._serialized_start=453 + _globals['_SOLVERSTEPBYSTEPRESPONSE']._serialized_end=528 + _globals['_SOLVERSOLVERESPONSE']._serialized_start=531 + _globals['_SOLVERSOLVERESPONSE']._serialized_end=744 + _globals['_SOLVER']._serialized_start=747 + _globals['_SOLVER']._serialized_end=922 +# @@protoc_insertion_point(module_scope) diff --git a/gui/scheduler/solver/v1/solver_pb2.pyi b/gui/scheduler/solver/v1/solver_pb2.pyi new file mode 100644 index 0000000..a6853c2 --- /dev/null +++ b/gui/scheduler/solver/v1/solver_pb2.pyi @@ -0,0 +1,78 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class SolverSolveRequest(_message.Message): + __slots__ = ("pieces", "threads", "hash_threshold", "wait_time", "use_cache", "cache_pull_interval") + PIECES_FIELD_NUMBER: _ClassVar[int] + THREADS_FIELD_NUMBER: _ClassVar[int] + HASH_THRESHOLD_FIELD_NUMBER: _ClassVar[int] + WAIT_TIME_FIELD_NUMBER: _ClassVar[int] + USE_CACHE_FIELD_NUMBER: _ClassVar[int] + CACHE_PULL_INTERVAL_FIELD_NUMBER: _ClassVar[int] + pieces: _containers.RepeatedCompositeFieldContainer[Piece] + threads: int + hash_threshold: int + wait_time: int + use_cache: bool + cache_pull_interval: int + def __init__(self, pieces: _Optional[_Iterable[_Union[Piece, _Mapping]]] = ..., threads: _Optional[int] = ..., hash_threshold: _Optional[int] = ..., wait_time: _Optional[int] = ..., use_cache: bool = ..., cache_pull_interval: _Optional[int] = ...) -> None: ... + +class Piece(_message.Message): + __slots__ = ("top", "right", "bottom", "left") + TOP_FIELD_NUMBER: _ClassVar[int] + RIGHT_FIELD_NUMBER: _ClassVar[int] + BOTTOM_FIELD_NUMBER: _ClassVar[int] + LEFT_FIELD_NUMBER: _ClassVar[int] + top: int + right: int + bottom: int + left: int + def __init__(self, top: _Optional[int] = ..., right: _Optional[int] = ..., bottom: _Optional[int] = ..., left: _Optional[int] = ...) -> None: ... + +class PieceWithOptionalHint(_message.Message): + __slots__ = ("piece", "x", "y") + PIECE_FIELD_NUMBER: _ClassVar[int] + X_FIELD_NUMBER: _ClassVar[int] + Y_FIELD_NUMBER: _ClassVar[int] + piece: Piece + x: int + y: int + def __init__(self, piece: _Optional[_Union[Piece, _Mapping]] = ..., x: _Optional[int] = ..., y: _Optional[int] = ...) -> None: ... + +class RotatedPiece(_message.Message): + __slots__ = ("piece", "rotation", "index") + PIECE_FIELD_NUMBER: _ClassVar[int] + ROTATION_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + piece: Piece + rotation: int + index: int + def __init__(self, piece: _Optional[_Union[Piece, _Mapping]] = ..., rotation: _Optional[int] = ..., index: _Optional[int] = ...) -> None: ... + +class SolverStepByStepResponse(_message.Message): + __slots__ = ("rotated_pieces",) + ROTATED_PIECES_FIELD_NUMBER: _ClassVar[int] + rotated_pieces: _containers.RepeatedCompositeFieldContainer[RotatedPiece] + def __init__(self, rotated_pieces: _Optional[_Iterable[_Union[RotatedPiece, _Mapping]]] = ...) -> None: ... + +class SolverSolveResponse(_message.Message): + __slots__ = ("time", "hashes_per_second", "hash_table_size", "boards_per_second", "boards_analyzed", "hash_table_hits", "rotated_pieces") + TIME_FIELD_NUMBER: _ClassVar[int] + HASHES_PER_SECOND_FIELD_NUMBER: _ClassVar[int] + HASH_TABLE_SIZE_FIELD_NUMBER: _ClassVar[int] + BOARDS_PER_SECOND_FIELD_NUMBER: _ClassVar[int] + BOARDS_ANALYZED_FIELD_NUMBER: _ClassVar[int] + HASH_TABLE_HITS_FIELD_NUMBER: _ClassVar[int] + ROTATED_PIECES_FIELD_NUMBER: _ClassVar[int] + time: float + hashes_per_second: float + hash_table_size: int + boards_per_second: float + boards_analyzed: int + hash_table_hits: int + rotated_pieces: _containers.RepeatedCompositeFieldContainer[RotatedPiece] + def __init__(self, time: _Optional[float] = ..., hashes_per_second: _Optional[float] = ..., hash_table_size: _Optional[int] = ..., boards_per_second: _Optional[float] = ..., boards_analyzed: _Optional[int] = ..., hash_table_hits: _Optional[int] = ..., rotated_pieces: _Optional[_Iterable[_Union[RotatedPiece, _Mapping]]] = ...) -> None: ... diff --git a/gui/scheduler/solver/v1/solver_pb2_grpc.py b/gui/scheduler/solver/v1/solver_pb2_grpc.py new file mode 100644 index 0000000..4d740de --- /dev/null +++ b/gui/scheduler/solver/v1/solver_pb2_grpc.py @@ -0,0 +1,100 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from solver.v1 import solver_pb2 as solver_dot_v1_dot_solver__pb2 + + +class SolverStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Solve = channel.unary_stream( + '/solver.v1.Solver/Solve', + request_serializer=solver_dot_v1_dot_solver__pb2.SolverSolveRequest.SerializeToString, + response_deserializer=solver_dot_v1_dot_solver__pb2.SolverSolveResponse.FromString, + ) + self.SolveStepByStep = channel.unary_stream( + '/solver.v1.Solver/SolveStepByStep', + request_serializer=solver_dot_v1_dot_solver__pb2.SolverSolveRequest.SerializeToString, + response_deserializer=solver_dot_v1_dot_solver__pb2.SolverStepByStepResponse.FromString, + ) + + +class SolverServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Solve(self, request, context): + """streams the data in response + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SolveStepByStep(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_SolverServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Solve': grpc.unary_stream_rpc_method_handler( + servicer.Solve, + request_deserializer=solver_dot_v1_dot_solver__pb2.SolverSolveRequest.FromString, + response_serializer=solver_dot_v1_dot_solver__pb2.SolverSolveResponse.SerializeToString, + ), + 'SolveStepByStep': grpc.unary_stream_rpc_method_handler( + servicer.SolveStepByStep, + request_deserializer=solver_dot_v1_dot_solver__pb2.SolverSolveRequest.FromString, + response_serializer=solver_dot_v1_dot_solver__pb2.SolverStepByStepResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'solver.v1.Solver', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Solver(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Solve(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/solver.v1.Solver/Solve', + solver_dot_v1_dot_solver__pb2.SolverSolveRequest.SerializeToString, + solver_dot_v1_dot_solver__pb2.SolverSolveResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SolveStepByStep(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/solver.v1.Solver/SolveStepByStep', + solver_dot_v1_dot_solver__pb2.SolverSolveRequest.SerializeToString, + solver_dot_v1_dot_solver__pb2.SolverStepByStepResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata)