diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c91cd005..5f53a444 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,7 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: python: "mambaforge-4.10" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..30659a62 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Rostok + +Rostok is an open source Python framework for generative design of linkage mechanisms for robotic purposes. It provides a framework to describe mechanisms as a graph, set an environment, perform simulation of generated mechanisms, get a reward as a quantitative value of the generated design, and search for the best possible design. + +A user can utilize the entire framework as a pipeline to generate a set of suboptimal designs, or utilize the modules and submodules as independent parts. The framework allows to implement custom generative rules, modify search and optimization algorithms. + +Currently the framework allows to perform co-design of open chain linkage mechanisms. Co-design consists in simultaneously searching for the mechanical structure and the trajectories of the robot to get the best possible performance. \ No newline at end of file diff --git a/docs/source/_templates/custom-class-template.rst b/docs/source/_templates/custom-class-template.rst new file mode 100644 index 00000000..c6e00634 --- /dev/null +++ b/docs/source/_templates/custom-class-template.rst @@ -0,0 +1,31 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :inherited-members: + + {% block methods %} + .. automethod:: __init__ + + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} \ No newline at end of file diff --git a/docs/source/_templates/custom-module-template.rst b/docs/source/_templates/custom-module-template.rst new file mode 100644 index 00000000..7824cd50 --- /dev/null +++ b/docs/source/_templates/custom-module-template.rst @@ -0,0 +1,66 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module Attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :template: custom-class-template.rst + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/docs/source/advanced_usage/algorithm.rst b/docs/source/advanced_usage/algorithm.rst index 80c20662..1d27963c 100644 --- a/docs/source/advanced_usage/algorithm.rst +++ b/docs/source/advanced_usage/algorithm.rst @@ -9,18 +9,21 @@ Search for grasping mechanism with open kinematic chain The framework generates planar mechanisms for industrial grippers to grasp objects defined by a user. A graph is generated by a set of rules which can be modified by a user. The graphs constructed by the set of rules form the space of possible solutions and the algorithm searches that space to obtain the best design. Currently, the rules are set to generate the open kinematic chain mechanisms. -The ability of the generated mechanism/graph to grasp the object is simulated with the physical engine Pychrono. The simulation of the physical properties of the grasping process results in the scalar reward that is calculated using the four criterions: - 1. Criterion of isotropy of contact forces - Description: list cont contains mean values of contact force for each body element. If sum of cont is equal zero (no body is in contact), then criterion f1 = 0. - Otherwise, delta_u (standard deviation of contact forces) is calculated. We want to minimize deviation. - 2. Criterion of number of contact surfaces - Description: f2 is equal the ratio of mean value of contact surfaces (during simulation) to the overall potential number of contact surfaces. - 3. Criterion of mean values of distance between each of fingers - Description: algorithm has to minimize mean values of the distance for each finger. - 4. Criterion of simulation time - Description: algorithm has to maximize simulation time - -In addition to the design of the parts, the second essential part of the mechanism is the optimal trajectories of the actuators. For each design we optimize these trajectories. The optimization is based on the same reward. Final reward for a design is calculated with the optimized trajectories and is used to search for the most effective design. +The ability of the generated mechanism/graph to grasp the object is simulated with the physical engine Pychrono. The simulation of the physical properties of the grasping process results in the scalar reward that is calculated using the six criterions: + 1. Time period for the grasp + Description: We abort the simulation prematurely if the mechanism cannot touch an object or the contact is lost. This criterion is used to separate unsuccessful designs based on the idea that longer contact with the object is better. All the mechanisms that successfully secure an object in place receive a score of 1. + 2. The fraction of phalanxes contacting with the object + Description: The idea is to reward the design for effective use of the phalanxes and prevent the growth of unnecessary components. Equals 1 if the body is in contact with the object. + 3. Dispersion of the contact forces + Description: The effective gripper should apply similar forces to the object. The grasp that rely on applying much force on specific point could damage an object or a corresponding element of the gripper, while the weak contact cannot withstand the external force + 4. Distance between object center and the geometric center of the contact points + Description: The small distance reduce inertia effects during the gripping process and helps to achieve a stable grip + 5. Grasp time + Description: The time required to fix an object in place. The faster designs get higher rewards + 6. The ability to withstand external forces + Description: In simulation, in a few time steps after the grasp event we apply gradually increasing force to the object for several seconds. If the object lose the contact with the mechanism the hold is considered as failed and the design get the 0 reward. The designs that managed to hold an object get a reward that is calculated based on the object shift from the grasp position + +In addition to the design of the parts, the second essential part of the mechanism is the optimal trajectories of the actuators. For each design we optimize these trajectories. The optimization is based on the same reward. Final reward for a design is calculated with the optimized trajectories and is used to search for the most effective design. The graph generating rules are fed to the searching algorithm to get the list of available actions at each step of the generation. Currently we use Monte Carlo Tree Search algorithm to explore the space of the possible designs. Therefore, at any step the algorithm gradually grow several designs and calculate their rewards in order to make a step in generation. The input data should include two different parts: @@ -34,4 +37,4 @@ The input data should include two different parts: The output is the designed mechanism, in the graph form, the information about actuator trajectories and the obtained reward The output can be used to start and visualize the simulation of the final design -The history of the exploration of the design space is saved within log file +The history of the exploration of the design space is saved within log file \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 00000000..31cd5a4c --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,9 @@ +=== +API +=== + +.. autosummary:: + :toctree: generated + :recursive: + + rostok \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index de374fe7..8c35b441 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,10 +9,12 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import sys +import os from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent / '../../')) -sys.path.insert(0, str(Path(__file__).parent / '../../rostok')) +sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath('../../')) +sys.path.insert(0, os.path.abspath('../../rostok')) #The master toctree document master_doc = 'index' @@ -23,13 +25,24 @@ project = 'rostok' copyright = '2022, BE2R Lab, ITMO' author = 'Ivan Borisov, Kirill Zharkov, Yefim Osipov, Dmitriy Ivolga, Kirill Nasonov, Sergey Kolyubin' -release = '0.0.1' +release = '1.0.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon','sphinx.ext.githubpages'] +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.githubpages', + 'sphinx.ext.autosummary'] +autosummary_generate = True + +source_suffix = { + '.rst': 'restructuredtext', + '.txt': 'markdown', + '.md': 'markdown', +} +autodoc_member_order = 'groupwise' napoleon_google_docstring = False napoleon_use_param = False @@ -40,6 +53,6 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] html_logo = '../images/logo_rostok.jpg' \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 90bb57de..3dd34bb3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -25,6 +25,7 @@ Content introduction/tutorials advanced_usage/algorithm modules/index + api contact_us Indices and tables diff --git a/docs/source/introduction/tutorials.rst b/docs/source/introduction/tutorials.rst index e3e675a8..0585d697 100644 --- a/docs/source/introduction/tutorials.rst +++ b/docs/source/introduction/tutorials.rst @@ -5,8 +5,8 @@ Tutorials In order to use the full potential of the `Rostok` library a user needs to get accustomed with features of the framework. We prepared tutorials that show what one can do with the library. -1. `Create node and rule vocabularies <../_static/tutorial_rules_and_nodes_vocabulary.html>`_ -2. `Create graphs using nodes and rules <../_static/tutorial_graph.html>`_ -3. `Set the object to grasp and use the built graphs to start simulation of the grasping mechanism <../_static/tutorial_sym_step.html>`_ -4. `Use optimizer to get optimal joint trajectories and best reward for the mechanism <../_static/tutorial_control_optimization.html>`_ -5. `Use node and rule vocabularies to search the design of grasping robot for a simple body <../_static/tutorial_search.html>`_ \ No newline at end of file +1. `Create node and rule vocabularies <_static/tutorial_rules_and_nodes_vocabulary.html>`_ +2. `Create graphs using nodes and rules <_static/tutorial_graph.html>`_ +3. `Set the object to grasp and use the built graphs to start simulation of the grasping mechanism <_static/tutorial_sym_step.html>`_ +4. `Use optimizer to get optimal joint trajectories and best reward for the mechanism <_static/tutorial_control_optimization.html>`_ +5. `Use node and rule vocabularies to search the design of grasping robot for a simple body <_static/tutorial_search.html>`_ \ No newline at end of file diff --git a/docs/source/modules/block_builder.rst b/docs/source/modules/block_builder.rst index d3d938fe..13d0b2d9 100644 --- a/docs/source/modules/block_builder.rst +++ b/docs/source/modules/block_builder.rst @@ -2,22 +2,23 @@ Block builder ============= -Block render -============ +Block blueprints +================ -.. automodule:: rostok.block_builder.node_render +.. automodule:: rostok.block_builder_api.block_blueprints :members: + :undoc-members: -Control -======= +Block parameters +================ -.. automodule:: rostok.block_builder.control +.. automodule:: rostok.block_builder_api.block_parameters :members: :undoc-members: Shapes ====== -.. automodule:: rostok.block_builder.envbody_shapes +.. automodule:: rostok.block_builder_api.easy_body_shapes :members: :undoc-members: \ No newline at end of file diff --git a/docs/source/modules/criterion.rst b/docs/source/modules/criterion.rst index 5022636d..517212f8 100644 --- a/docs/source/modules/criterion.rst +++ b/docs/source/modules/criterion.rst @@ -5,11 +5,11 @@ Criterion Criterion calculation ===================== -.. automodule:: rostok.criterion.criterion_calc +.. automodule:: rostok.criterion.criterion_calculation :members: Flag stop simulation ==================== -.. automodule:: rostok.criterion.flags_simualtions +.. automodule:: rostok.criterion.simulation_flags :members: \ No newline at end of file diff --git a/docs/source/modules/graph_generators.rst b/docs/source/modules/graph_generators.rst index 751e87e0..c4625ae9 100644 --- a/docs/source/modules/graph_generators.rst +++ b/docs/source/modules/graph_generators.rst @@ -2,9 +2,25 @@ Graph generation ================ -Graph environments +Design environment ================== -.. automodule:: rostok.graph_generators.graph_environment +.. automodule:: rostok.graph_generators.environments.design_environment :members: - :undoc-members: \ No newline at end of file + :undoc-members: + +MCTS +================== + +.. automodule:: rostok.graph_generators.search_algorithms.mcts + :members: + :undoc-members: + + +MCTS Manager +============ + +.. automodule:: rostok.graph_generators.mcts_manager + :members: + :undoc-members: + \ No newline at end of file diff --git a/docs/source/modules/graph_grammar.rst b/docs/source/modules/graph_grammar.rst index 58e306a2..569d24c4 100644 --- a/docs/source/modules/graph_grammar.rst +++ b/docs/source/modules/graph_grammar.rst @@ -8,10 +8,10 @@ Node .. automodule:: rostok.graph_grammar.node :members: -Nodes division -============== +Graph-grammar explorer +====================== -.. automodule:: rostok.graph_grammar.nodes_division +.. automodule:: rostok.graph_grammar.graphgrammar_explorer :members: Node Vocabulary @@ -26,11 +26,6 @@ Rule Vocabulary .. automodule:: rostok.graph_grammar.rule_vocabulary :members: -Graph grammar explorer -====================== - -.. automodule:: rostok.graph_grammar.graphgrammar_explorer - :members: Random graph ============ diff --git a/docs/source/modules/index.rst b/docs/source/modules/index.rst index 0ea48f87..da924388 100644 --- a/docs/source/modules/index.rst +++ b/docs/source/modules/index.rst @@ -12,6 +12,7 @@ That is about modules into rostok graph_generators criterion trajectory_optimizer + simulation_chrono virtual_experiment utils diff --git a/docs/source/modules/simulation_chrono.rst b/docs/source/modules/simulation_chrono.rst new file mode 100644 index 00000000..a3bb00cb --- /dev/null +++ b/docs/source/modules/simulation_chrono.rst @@ -0,0 +1,21 @@ +=========================== +Simulation in ProjectChrono +=========================== + +Simulation scenario +=================== + +.. automodule:: rostok.simulation_chrono.simulation_scenario + :members: + +Simulation +========== + +.. automodule:: rostok.simulation_chrono.simulation + :members: + +Simulation utils +================ + +.. automodule:: rostok.simulation_chrono.simulation_utils + :members: \ No newline at end of file diff --git a/docs/source/modules/utils.rst b/docs/source/modules/utils.rst index 10743c5b..b3233228 100644 --- a/docs/source/modules/utils.rst +++ b/docs/source/modules/utils.rst @@ -3,7 +3,7 @@ Utils ===== Pickle save -================= +=========== .. automodule:: rostok.utils.pickle_save :members: diff --git a/docs/source/modules/virtual_experiment.rst b/docs/source/modules/virtual_experiment.rst index d29ada78..638368dd 100644 --- a/docs/source/modules/virtual_experiment.rst +++ b/docs/source/modules/virtual_experiment.rst @@ -2,14 +2,14 @@ Virtual experiment ================== -Simulation step -=============== +Robot +===== -.. automodule:: rostok.virtual_experiment.simulation_step +.. automodule:: rostok.virtual_experiment.robot_new :members: -Auxilarity sensors -================== +Sensors +======= -.. automodule:: rostok.virtual_experiment.auxilarity_sensors +.. automodule:: rostok.virtual_experiment.sensors :members: \ No newline at end of file diff --git a/docs_environment.yml b/docs_environment.yml index 389e97f5..1776d2d7 100644 --- a/docs_environment.yml +++ b/docs_environment.yml @@ -3,12 +3,15 @@ channels: - intel - conda-forge dependencies: - - python==3.9.15 + - python=3.10.8 + - projectchrono::pychrono=8.* + - irrlicht=1.8.5 - numpy - yapf - mypy - pylint - - tk==8.6.12 + - tk + - pygraphviz - pip - pip: - networkx==2.8.6 @@ -16,5 +19,9 @@ dependencies: - scipy - mcts==1.0.4 - pytest - - projectchrono::pychrono=7 - - irrlicht=1.8.5 \ No newline at end of file + - open3d + - thegolem==0.3.1 + - more-itertools + - sphinx-autoapi + - sphinx-autopackagesummary + - sphinx-rtd-theme diff --git a/rostok/block_builder_api/block_blueprints.py b/rostok/block_builder_api/block_blueprints.py index 404428c6..c7b411f2 100644 --- a/rostok/block_builder_api/block_blueprints.py +++ b/rostok/block_builder_api/block_blueprints.py @@ -89,7 +89,7 @@ def __init__(self, *args: object) -> None: super().__init__('Need implementation for method in child class') -class BlockCreatorInterface(): +class BlockCreatorInterface: """ To use it, you need to implement functions for creating from blueprints. Raises: diff --git a/rostok/criterion/simulation_flags.py b/rostok/criterion/simulation_flags.py index 5f603c28..2a8c80d3 100644 --- a/rostok/criterion/simulation_flags.py +++ b/rostok/criterion/simulation_flags.py @@ -11,21 +11,22 @@ class EventCommands(Enum): """Commands available for the events. The simulation class handles these commands""" + STOP = 0 CONTINUE = 1 ACTIVATE = 2 class SimulationSingleEvent(ABC): - """ The abstract class for the event that can occur during the simulation only once. + """The abstract class for the event that can occur during the simulation only once. - At each step of the simulation the event is checked using the current states of the sensors. - step_n is a single value because the event can occur only once in simulation. + At each step of the simulation the event is checked using the current states of the sensors. + step_n is a single value because the event can occur only once in simulation. - Attributes: - state (bool): event occurrence flag - step_n (int): the step of the event occurrence - verbosity (int): controls the console output of the event + Attributes: + state (bool): event occurrence flag + step_n (int): the step of the event occurrence + verbosity (int): controls the console output of the event """ def __init__(self, verbosity=0): @@ -40,11 +41,13 @@ def reset(self): self.step_n = None @abstractmethod - def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor): + def event_check( + self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor + ): """Simulation calls that method each step to check if the event occurred. Args: - current_time (float): time from the start of the simulation + current_time (float): time from the start of the simulation step_n (int): step number of the simulation robot_data (_type_): current state of the robot sensors env_data (_type_): current state of the environment sensors @@ -62,17 +65,22 @@ def __str__(self) -> str: class EventContact(SimulationSingleEvent): """Event of contact between robot and object - Attributes: - from_body (bool): flag determines the source of contact information. + Attributes: + from_body (bool): flag determines the source of contact information. """ - def __init__(self, take_from_body:bool = False): + + def __init__(self, take_from_body: bool = False): super().__init__() self.from_body = take_from_body - def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor): + def event_check( + self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor + ): if self.state: if __debug__: - raise Exception("The EventContact.event_check is called after occurrence in simulation.") + raise Exception( + "The EventContact.event_check is called after occurrence in simulation." + ) return EventCommands.CONTINUE if not self.from_body: @@ -99,10 +107,11 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ class EventContactTimeOut(SimulationSingleEvent): - """Event that occurs if the robot doesn't contact with body during the reference time from the start of the simulation. + """ + Event that occurs if the robot doesn't contact with body during the reference time from the start of the simulation. Attributes: - reference_time (float): the moment of time where the simulation is interrupted + reference_time (float): the moment of time where the simulation is interrupted if there is no contact with the body contact (bool): the flag that determines if there was a contact with body """ @@ -112,15 +121,20 @@ def __init__(self, ref_time: float, contact_event: EventContact): self.reference_time = ref_time self.contact_event: EventContact = contact_event - def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor): - """Return STOP if the time exceeds the reference time and there was no contact with body. + def event_check( + self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor + ): + """ + Return STOP if the time exceeds the reference time and there was no contact with body. Returns: EventCommands: return a command for simulation """ if self.state: if __debug__: - raise Exception("The EventContactTimeOut.event_check is called after occurrence in simulation.") + raise Exception( + "The EventContactTimeOut.event_check is called after occurrence in simulation." + ) return EventCommands.STOP # if the contact has already occurred in simulation, return CONTINUE @@ -136,17 +150,20 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ class EventFlyingApart(SimulationSingleEvent): - """The event that stops simulation if the robot parts have flown apart. + """ + The event that stops simulation if the robot parts have flown apart. - Attributes:: - max_distance (float): the max distance for robot parts + Attributes: + max_distance (float): the max distance for robot parts """ def __init__(self, max_distance: float): super().__init__() self.max_distance = max_distance - def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor): + def event_check( + self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor + ): """Return STOP if the current position of a part is max_distance away from the robot base body. Returns: @@ -154,14 +171,19 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ """ if self.state: if __debug__: - raise Exception("The EventFlyingApart.event_check is called after occurrence in simulation.") + raise Exception( + "The EventFlyingApart.event_check is called after occurrence in simulation." + ) return EventCommands.STOP trajectory_points = robot_data.get_body_trajectory_point() # It takes the position of the first block in the list, that should be the base body base_position = trajectory_points[next(iter(trajectory_points))] for position in trajectory_points.values(): - if np.linalg.norm(np.array(base_position) - np.array(position)) > self.max_distance: + if ( + np.linalg.norm(np.array(base_position) - np.array(position)) + > self.max_distance + ): self.state = True self.step_n = step_n return EventCommands.STOP @@ -170,7 +192,8 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ class EventSlipOut(SimulationSingleEvent): - """The event that stops simulation if the body slips out from the grasp after the contact. + """ + The event that stops simulation if the body slips out from the grasp after the contact. Attributes: time_last_contact (float): time of last contact of robot and body @@ -186,15 +209,20 @@ def reset(self): self.time_last_contact = None super().reset() - def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor): - """Return STOP if the body and mech lose contact for the reference time. + def event_check( + self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor + ): + """ + Return STOP if the body and mech lose contact for the reference time. Returns: EventCommands: return a command for simulation """ if self.state: if __debug__: - raise Exception("The EventSlipOut.event_check is called after occurrence in simulation.") + raise Exception( + "The EventSlipOut.event_check is called after occurrence in simulation." + ) return EventCommands.STOP robot_contacts = robot_data.get_amount_contacts() @@ -221,7 +249,8 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ class EventGrasp(SimulationSingleEvent): - """Event that activates the force if + """ + Event that activates the force if Attributes: grasp_steps (int): the amount of consecutive steps body is not moving @@ -232,11 +261,13 @@ class EventGrasp(SimulationSingleEvent): force_test_time (float): the time period of the force test of the grasp """ - def __init__(self, - grasp_limit_time: float, - contact_event: EventContact, - verbosity: int = 0, - simulation_stop:bool = False): + def __init__( + self, + grasp_limit_time: float, + contact_event: EventContact, + verbosity: int = 0, + simulation_stop: bool = False, + ): super().__init__(verbosity=verbosity) self.grasp_steps: int = 0 self.grasp_time: Optional[float] = None @@ -270,9 +301,12 @@ def check_grasp_current_step(self, env_data: Sensor, robot_data: Sensor): else: self.grasp_steps = 0 - def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor): - """Return ACTIVATE if the body was in contact with the robot and after that at some - point doesn't move for at least 10 steps. Return STOP if the grasp didn't occur during grasp_limit_time. + def event_check( + self, current_time: float, step_n: int, robot_data: Sensor, env_data: Sensor + ): + """ + Return ACTIVATE if the body was in contact with the robot and after that at some + point doesn't move for at least 10 steps. Return STOP if the grasp didn't occur during grasp_limit_time. Returns: EventCommands: return a command for simulation @@ -280,7 +314,9 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ if self.state: if __debug__: - raise Exception("The EventGrasp.event_check is called after occurrence in simulation.") + raise Exception( + "The EventGrasp.event_check is called after occurrence in simulation." + ) return EventCommands.CONTINUE if self.check_grasp_timeout(current_time): @@ -294,9 +330,9 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ self.step_n = step_n self.grasp_time = current_time if self.verbosity > 0: - print('Grasp event!', current_time) + print("Grasp event!", current_time) if self.simulation_stop > 0: - input('press enter to continue') + input("press enter to continue") return EventCommands.ACTIVATE @@ -304,7 +340,10 @@ def event_check(self, current_time: float, step_n: int, robot_data: Sensor, env_ class EventStopExternalForce(SimulationSingleEvent): - """Event that controls external force and stops simulation after reference time has passed.""" + """ + Event that controls external force and stops simulation after reference time has passed. + """ + def __init__(self, grasp_event: EventGrasp, force_test_time: float): super().__init__() self.grasp_event = grasp_event diff --git a/rostok/graph_grammar/node_vocabulary.py b/rostok/graph_grammar/node_vocabulary.py index fbd4897e..71536819 100644 --- a/rostok/graph_grammar/node_vocabulary.py +++ b/rostok/graph_grammar/node_vocabulary.py @@ -1,17 +1,16 @@ -""" Module contains NodeVocabulary class.""" - from typing import Optional from rostok.block_builder_api.block_blueprints import ALL_BLUEPRINT from rostok.graph_grammar.node import ROOT, Node -class NodeVocabulary(): +class NodeVocabulary: """The class contains dictionary of nodes and methods to manipulate with it. + This is a class to manage a dictionary of nodes. The keys are labels of the nodes and the values are Node objects. User can create or add nodes to vocabulary, get individual nodes or list of nodes. - + Attributes: node_dict (List[Node]): dictionary of all nodes. terminal_node_dict (List[Node]): dictionary of only terminal nodes. @@ -25,16 +24,19 @@ def __init__(self): self.nonterminal_node_dict = {} def add_node(self, node: Node): - """Add an already created node to the vocabulary. - + """ + Add an already created node to the vocabulary. + Args: node (Node): node to be added to vocabulary. - + Raises: Exception: Attempt to add a Node with a label that is already in dictionary! """ if node.label in self.node_dict: - raise Exception('Attempt to add a Node with a label that is already in dictionary!') + raise Exception( + "Attempt to add a Node with a label that is already in dictionary!" + ) self.node_dict[node.label] = node if node.is_terminal: @@ -42,25 +44,30 @@ def add_node(self, node: Node): else: self.nonterminal_node_dict[node.label] = node - def create_node(self, - label: str, - is_terminal: bool = False, - block_blueprint: Optional[ALL_BLUEPRINT] = None): - """Create a node and add it to the vocabulary. - + def create_node( + self, + label: str, + is_terminal: bool = False, + block_blueprint: Optional[ALL_BLUEPRINT] = None, + ): + """ + Create a node and add it to the vocabulary. + Args: label (str): the label of the new node. is_terminal (bool, optional): defines if the new node is a terminal node. Default is False. block_blueprint (BlockBlueprint, optional): the object that contains physical properties of the node. Default is None. - + Raises: Exception: Attempt to add a Node with a label that is already in dictionary! """ if label in self.node_dict: - raise Exception('Attempt to create a Node with a label that is already in dictionary!') + raise Exception( + "Attempt to create a Node with a label that is already in dictionary!" + ) node = Node(label, is_terminal, block_blueprint) self.node_dict[label] = node @@ -70,14 +77,15 @@ def create_node(self, self.nonterminal_node_dict[label] = node def get_node(self, label: str) -> Node: - """Return a node corresponding to the label. - + """ + Return a node corresponding to the label. + Args: label(str): the label of the node that should be returned. - + Returns: A requested node as a Node class object. - + Raises Exception: Node with given label not found! """ @@ -89,11 +97,12 @@ def get_node(self, label: str) -> Node: return node def check_node(self, label: str) -> bool: - """Check if the label is in the vocabulary. - + """ + Check if the label is in the vocabulary. + Args: label(str): the label of the node that should be checked. - + Returns: bool: True is the label is in dictionary, False otherwise. """ @@ -109,11 +118,12 @@ def __str__(self): return str(self.node_dict.keys()) def get_list_of_nodes(self, nodes: list[str]) -> list[Node]: - """Returns list of Node objects corresponding to list of labels. - + """ + Returns list of Node objects corresponding to list of labels. + Args: nodes (list[str]): list of labels to construct a list of Node objects. - + Returns: list of Node objects corresponding to the list of passed labels. """ @@ -123,14 +133,14 @@ def get_list_of_nodes(self, nodes: list[str]) -> list[Node]: return result -if __name__ == '__main__': +if __name__ == "__main__": node_vocab = NodeVocabulary() node_vocab.add_node(ROOT) - node_vocab.create_node('A') - node_vocab.create_node('B') - node_vocab.create_node('C') - node_vocab.create_node('D') - node_vocab.create_node('A1', is_terminal=True) - node_vocab.create_node('B1', is_terminal=True) - node_vocab.create_node('C1', is_terminal=True) - print(node_vocab) \ No newline at end of file + node_vocab.create_node("A") + node_vocab.create_node("B") + node_vocab.create_node("C") + node_vocab.create_node("D") + node_vocab.create_node("A1", is_terminal=True) + node_vocab.create_node("B1", is_terminal=True) + node_vocab.create_node("C1", is_terminal=True) + print(node_vocab) diff --git a/rostok/graph_grammar/rule_vocabulary.py b/rostok/graph_grammar/rule_vocabulary.py index b6a7a0cb..cb0e2d58 100644 --- a/rostok/graph_grammar/rule_vocabulary.py +++ b/rostok/graph_grammar/rule_vocabulary.py @@ -1,5 +1,3 @@ -"""Module contains RuleVocabulary class.""" - import matplotlib.pyplot as plt import networkx as nx import numpy as np @@ -8,8 +6,10 @@ from rostok.graph_grammar.node_vocabulary import NodeVocabulary -class RuleVocabulary(): - """The class that contains the rules for building the :py:class:`rostok.graph_grammar.node.GraphGrammar` object. +class RuleVocabulary: + """ + The class that contains the rules for building the :py:class:`rostok.graph_grammar.node.GraphGrammar` object. + All rules for mechanism generation should be created with an instance of :py:class:`rostok.graph_grammar.rule_vocabulary.RuleVocabulary`. This class provides utility methods for the rules. @@ -30,6 +30,7 @@ class RuleVocabulary(): def __init__(self, node_vocab: NodeVocabulary = NodeVocabulary()): """Create a new empty vocabulary object. + Args: node_vocab (NodeVocabulary, optional): the node vocabulary for the rules. Default is an empty node vocabulary. @@ -43,17 +44,20 @@ def __init__(self, node_vocab: NodeVocabulary = NodeVocabulary()): self.terminal_dict: dict[str, list[str]] = {} self._completed = False - def create_rule(self, - name: str, - current_nodes: list[str], - new_nodes: list[str], - current_in_edges: int, - current_out_edges: int, - new_edges: list[tuple[int, int]] = [], - current_links: list[tuple[int, int]] = []): - """Create a rule and add it to the dictionary. - The method checks the created rule. There is no method to add already created rule to - the vocabulary. + def create_rule( + self, + name: str, + current_nodes: list[str], + new_nodes: list[str], + current_in_edges: int, + current_out_edges: int, + new_edges: list[tuple[int, int]] = [], + current_links: list[tuple[int, int]] = [], + ): + """ + Create a rule and add it to the dictionary. + + The method checks the created rule. There is no method to add already created rule to the vocabulary. Args: name (str): name of the new rule. @@ -73,28 +77,34 @@ def create_rule(self, """ if name in self.rule_dict: - raise Exception('This name is already in the rule vocabulary!') + raise Exception("This name is already in the rule vocabulary!") # Currently the GraphGrammar class can only apply rules that replace one node with the # new system of nodes. # But in future we may apply replacement of the linked set of nodes if len(current_nodes) != 1: - raise Exception(f'Prohibited length of the current_nodes: {len(current_nodes)}!') + raise Exception( + f"Prohibited length of the current_nodes: {len(current_nodes)}!" + ) # Check that all nodes are in vocabulary for node in current_nodes: if not self.node_vocab.check_node(node): - raise Exception(f'Label {node} not in node vocabulary!') + raise Exception(f"Label {node} not in node vocabulary!") for node in new_nodes: if not self.node_vocab.check_node(node): - raise Exception(f'Label {node} not in node vocabulary!') + raise Exception(f"Label {node} not in node vocabulary!") # if the rule deletes a node dont check its in and out connections if len(new_nodes) != 0: - #Currently current_ins_links and current_out_links should be just numbers - if current_in_edges > len(new_nodes) - 1 or current_out_edges > len( - new_nodes) - 1 or current_in_edges < 0 or current_out_edges < 0: + # Currently current_ins_links and current_out_links should be just numbers + if ( + current_in_edges > len(new_nodes) - 1 + or current_out_edges > len(new_nodes) - 1 + or current_in_edges < 0 + or current_out_edges < 0 + ): raise Exception("Invalid linking of the in or out edges!") i = 0 @@ -105,9 +115,14 @@ def create_rule(self, i += 1 for edge in new_edges: - if edge[0] > len(new_nodes) - 1 or edge[1] > len( - new_nodes) - 1 or edge[0] < 0 or edge[1] < 0 or edge[0] == edge[1]: - raise Exception(f'Invalid edge {edge}') + if ( + edge[0] > len(new_nodes) - 1 + or edge[1] > len(new_nodes) - 1 + or edge[0] < 0 + or edge[1] < 0 + or edge[0] == edge[1] + ): + raise Exception(f"Invalid edge {edge}") new_graph.add_edge(*edge) # graph_insert set the terminal status for the rule @@ -130,20 +145,32 @@ def create_rule(self, self.rules_nonterminal_node_set.update(set(new_nodes)) def __str__(self): - """Print the rules from the dictionary of rules.""" - result = '' + """ + Print the rules from the dictionary of rules. + """ + result = "" for rule_tule in self.rule_dict.items(): rule_graph = rule_tule[1].graph_insert rule_node = rule_tule[1].replaced_node - result = result + rule_tule[0] + ": " + rule_node.label + " ==> " + str([ - node[1]['Node'].label for node in rule_graph.nodes.items() - ]) + ' ' + str([edge for edge in rule_graph.edges]) + '\n' + result = ( + result + + rule_tule[0] + + ": " + + rule_node.label + + " ==> " + + str([node[1]["Node"].label for node in rule_graph.nodes.items()]) + + " " + + str([edge for edge in rule_graph.edges]) + + "\n" + ) return result # Check set of rules itself, without any graph def check_rules(self): - """Check set of rules itself, without any graph. + """ + Check set of rules itself, without any graph. + Check the rules for having at least one terminal rule for every node that appears in the end graph of a nonterminal rule. """ # Check if all nonterminal nodes from vocab are in the rules. If not print a warning @@ -153,9 +180,13 @@ def check_rules(self): print(f"Nodes {diff} are not used as end nodes in the nonterminal rules!") # Check if all nodes in the end graphs of nonterminal rules have a terminal rule - diff = self.rules_nonterminal_node_set.difference(set(self.terminal_dict.keys())) + diff = self.rules_nonterminal_node_set.difference( + set(self.terminal_dict.keys()) + ) if len(diff) > 0: - print(f"Nodes {diff} don't have terminal rules! The set of rules is not completed!") + print( + f"Nodes {diff} don't have terminal rules! The set of rules is not completed!" + ) else: self._completed = True @@ -169,8 +200,10 @@ def get_rule(self, name: str) -> Rule: def get_list_of_applicable_rules(self, grammar: GraphGrammar): """Return the total list of applicable rules for the current graph. + Args: grammar (GraphGrammar): a :py:class:`rostok.graph_grammar.node.GraphGrammar` object analyze. + Returns: list of rule names for rules that can be applied for the graph. """ @@ -181,14 +214,15 @@ def get_list_of_applicable_rules(self, grammar: GraphGrammar): rule_name = rule_tuple[0] label_to_replace = rule.replaced_node.label for node in grammar.nodes.items(): - #find a node that can be replaced using the rule - if label_to_replace == node[1]['Node'].label: + # find a node that can be replaced using the rule + if label_to_replace == node[1]["Node"].label: list_of_applicable_rules.append(rule_name) return list(set(list_of_applicable_rules)) def get_list_of_applicable_nonterminal_rules(self, grammar: GraphGrammar): - """Return the list of non-terminal applicable rules for the current graph. + """ + Return the list of non-terminal applicable rules for the current graph. Args: grammar (GraphGrammar): a :py:class:`rostok.graph_grammar.node.GraphGrammar` object analyze. @@ -203,14 +237,15 @@ def get_list_of_applicable_nonterminal_rules(self, grammar: GraphGrammar): rule_name = rule_tuple[0] label_to_replace = rule.replaced_node.label for node in grammar.nodes.items(): - #find a node that can be replaced using the rule - if label_to_replace == node[1]['Node'].label: + # find a node that can be replaced using the rule + if label_to_replace == node[1]["Node"].label: list_of_applicable_rules.append(rule_name) return list(set(list_of_applicable_rules)) def get_list_of_applicable_terminal_rules(self, grammar: GraphGrammar): - """Return the list of terminal applicable rules for the current graph. + """ + Return the list of terminal applicable rules for the current graph. Args: grammar (GraphGrammar): a :py:class:`rostok.graph_grammar.node.GraphGrammar` object analyze. @@ -225,14 +260,15 @@ def get_list_of_applicable_terminal_rules(self, grammar: GraphGrammar): rule_name = rule_tuple[0] label_to_replace = rule.replaced_node.label for node in grammar.nodes.items(): - #find a node that can be replaced using the rule - if label_to_replace == node[1]['Node'].label: + # find a node that can be replaced using the rule + if label_to_replace == node[1]["Node"].label: list_of_applicable_rules.append(rule_name) return list(set(list_of_applicable_rules)) def terminal_rules_for_node(self, node_name: str): - """Return a list of the terminal rules for the node + """ + Return a list of the terminal rules for the node Args: node_name (str): a node label for which function returns the list of the terminal rules. @@ -248,7 +284,8 @@ def terminal_rules_for_node(self, node_name: str): return rule_list def make_graph_terminal(self, grammar: GraphGrammar): - """Converts a graph into a graph with only terminal nodes. + """ + Converts a graph into a graph with only terminal nodes. For each non-terminal node the function apply a random rule that make it terminal. @@ -258,14 +295,15 @@ def make_graph_terminal(self, grammar: GraphGrammar): rule_list = [] for node in grammar.nodes.items(): if not node[1]["Node"].is_terminal: - rules = self.terminal_rules_for_node(node[1]['Node'].label) + rules = self.terminal_rules_for_node(node[1]["Node"].label) rule = self.terminal_rule_dict[rules[np.random.choice(len(rules))]] rule_list.append(rule) for rule in rule_list: grammar.apply_rule(rule) def rule_vis(self, name: str): - """Visualize the rule. + """ + Visualize the rule. Args: name (str): name of the rule to visualize @@ -281,11 +319,13 @@ def rule_vis(self, name: str): node.add_node(1, Node=rule.replaced_node) ax1 = plt.subplot(121) ax1.set_title("Replaced node") - nx.draw_networkx(node, - with_labels=True, - pos=nx.shell_layout(node, dim=2), - node_size=800, - labels={n: node.nodes[n]["Node"].label for n in node}) + nx.draw_networkx( + node, + with_labels=True, + pos=nx.shell_layout(node, dim=2), + node_size=800, + labels={n: node.nodes[n]["Node"].label for n in node}, + ) ax1.axis("off") ax2 = plt.subplot(122) @@ -295,32 +335,34 @@ def rule_vis(self, name: str): # node_size=500, # labels={n: graph.nodes[n]["Node"].label for n in graph}) - nx.draw_networkx(graph, - with_labels=True, - pos=nx.planar_layout(graph, dim=2, scale=5), - node_size=800, - labels={n: graph.nodes[n]["Node"].label for n in graph}) + nx.draw_networkx( + graph, + with_labels=True, + pos=nx.planar_layout(graph, dim=2, scale=5), + node_size=800, + labels={n: graph.nodes[n]["Node"].label for n in graph}, + ) ax2.axis("off") plt.show() -if __name__ == '__main__': +if __name__ == "__main__": node_vocab = NodeVocabulary() node_vocab.add_node(ROOT) - node_vocab.create_node('A') - node_vocab.create_node('B') - node_vocab.create_node('C') - node_vocab.create_node('D') - - node_vocab.create_node('A1', is_terminal=True) - node_vocab.create_node('B1', is_terminal=True) - node_vocab.create_node('C1', is_terminal=True) + node_vocab.create_node("A") + node_vocab.create_node("B") + node_vocab.create_node("C") + node_vocab.create_node("D") + + node_vocab.create_node("A1", is_terminal=True) + node_vocab.create_node("B1", is_terminal=True) + node_vocab.create_node("C1", is_terminal=True) rule_vocab = RuleVocabulary(node_vocab) - rule_vocab.create_rule("First_Rule", ['A'], ['B', 'C'], 0, 1, [(0, 1)]) - rule_vocab.create_rule("AT", ['A'], ['A1'], 0, 0) - rule_vocab.create_rule("BT", ['B'], ['B1'], 0, 0) - rule_vocab.create_rule("CT", ['C'], ['C1'], 0, 0) + rule_vocab.create_rule("First_Rule", ["A"], ["B", "C"], 0, 1, [(0, 1)]) + rule_vocab.create_rule("AT", ["A"], ["A1"], 0, 0) + rule_vocab.create_rule("BT", ["B"], ["B1"], 0, 0) + rule_vocab.create_rule("CT", ["C"], ["C1"], 0, 0) rule_vocab.create_rule("ROOT", ["ROOT"], ["A"], 0, 0) rule_vocab.create_rule("CD", ["C"], [], 0, 0) print(rule_vocab) diff --git a/rostok/simulation_chrono/simulation_scenario.py b/rostok/simulation_chrono/simulation_scenario.py index 0435d41b..aacd8324 100644 --- a/rostok/simulation_chrono/simulation_scenario.py +++ b/rostok/simulation_chrono/simulation_scenario.py @@ -1,6 +1,6 @@ from copy import deepcopy import json -from typing import List +from typing import List, Optional import pychrono as chrono