From e64c6542ed4d800e725c6d8a6269aeb4e8e949cc Mon Sep 17 00:00:00 2001 From: David Cattermole Date: Sat, 17 Aug 2019 23:02:10 +0100 Subject: [PATCH] Add solver abstract base class, and sub-classes for existing Solver class. Begin re-writing solver compile. Issue #72 and #57. Collection Execution is a separate module. --- python/mmSolver/_api/action.py | 28 ++ python/mmSolver/_api/collection.py | 641 ++----------------------- python/mmSolver/_api/execute.py | 569 ++++++++++++++++++++++ python/mmSolver/_api/solver.py | 264 ---------- python/mmSolver/_api/solverbase.py | 36 ++ python/mmSolver/_api/solverstandard.py | 92 ++++ python/mmSolver/_api/solverstep.py | 550 +++++++++++++++++++++ python/mmSolver/api.py | 32 +- tests/test/test_api/test_collection.py | 2 +- tests/test/test_api/test_solver.py | 2 +- 10 files changed, 1340 insertions(+), 876 deletions(-) create mode 100644 python/mmSolver/_api/action.py create mode 100644 python/mmSolver/_api/execute.py delete mode 100644 python/mmSolver/_api/solver.py create mode 100644 python/mmSolver/_api/solverbase.py create mode 100644 python/mmSolver/_api/solverstandard.py create mode 100644 python/mmSolver/_api/solverstep.py diff --git a/python/mmSolver/_api/action.py b/python/mmSolver/_api/action.py new file mode 100644 index 000000000..88567a493 --- /dev/null +++ b/python/mmSolver/_api/action.py @@ -0,0 +1,28 @@ +# Copyright (C) 2019 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Actions - a wrapper tuple for a callable function with positional and keyword arguments. +""" + +import collections + + +Action = collections.namedtuple( + 'Action', + ('func', 'args', 'kwargs') +) diff --git a/python/mmSolver/_api/collection.py b/python/mmSolver/_api/collection.py index cf30dcc08..0f9706cfc 100644 --- a/python/mmSolver/_api/collection.py +++ b/python/mmSolver/_api/collection.py @@ -40,68 +40,15 @@ import mmSolver._api.excep as excep import mmSolver._api.constant as const import mmSolver._api.solveresult as solveresult -import mmSolver._api.solver as solver +import mmSolver._api.solverstep as solver import mmSolver._api.marker as marker import mmSolver._api.attribute as attribute import mmSolver._api.sethelper as sethelper -import mmSolver._api.collectionutils as collectionutils +import mmSolver._api.execute as execute LOG = mmSolver.logger.get_logger() -ExecuteOptions = collections.namedtuple( - 'ExecuteOptions', - ('verbose', 'refresh', - 'force_update', 'do_isolate', - 'display_image_plane') -) - - -def createExecuteOptions(verbose=False, - refresh=False, - force_update=False, - do_isolate=False, - display_image_plane=None, - # display_node_types=None, - # TODO: Allow a dict to be - # passed to the function specifying the object type - # and the visibility status during solving. This - # allows us to turn on/off any object type during - # solving. If an argument is not given or is None, - # the object type visibility will not be changed. - ): - """ - Create ExecuteOptions. - - :param verbose: Print extra solver information while a solve is running. - :type verbose: bool - - :param refresh: Should the solver refresh the viewport while solving? - :type refresh: bool - - :param force_update: Force updating the DG network, to help the - solver in case of a Maya evaluation DG bug. - :type force_update: bool - - :param do_isolate: Isolate only solving objects while performing - the solve. - :type do_isolate: bool - - :param display_image_plane: Display image planes in the viewport while performing the solve? - :type display_image_plane: bool - """ - # if display_node_types is None: - # display_node_types = dict() - options = ExecuteOptions( - verbose=verbose, - refresh=refresh, - force_update=force_update, - do_isolate=do_isolate, - display_image_plane=display_image_plane, - # display_node_types=display_node_types - ) - return options - def _create_collection_attributes(node): """ @@ -174,7 +121,7 @@ def __init__(self, node=None, name=None): # Store the keyword arguments for the command, return this if the user # asks for the arguments. Invalidate these arguments and force a # re-compile if the user sets a new value, otherwise it's still valid. - self._kwargs_list = [] + self._actions_list = [] if node is not None: if isinstance(node, (str, unicode)): @@ -215,7 +162,7 @@ def get_node_uid(self): return self._set.get_node_uid() def set_node(self, name): - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return self._set.set_node(name) ############################################################################ @@ -342,7 +289,7 @@ def _dump_solver_list(self, solver_list): data_list.append(data) attr = const.COLLECTION_ATTR_LONG_NAME_SOLVER_LIST self._set_attr_data(attr, data_list) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def _get_solver_list_names(self, solver_list): @@ -367,7 +314,7 @@ def get_solver_list(self): Get Solver objects attached to the collection. :return: Solver objects. - :rtype: list of solver.Solver + :rtype: list of solverop.Solver """ solver_list = None if self._solver_list is None: @@ -460,7 +407,7 @@ def add_marker(self, mkr): assert len(node) > 0 if self._set.member_in_set(node) is False: self._set.add_member(node) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def add_marker_list(self, mkr_list): @@ -470,7 +417,7 @@ def add_marker_list(self, mkr_list): if isinstance(mkr, marker.Marker): node_list.append(mkr.get_node()) self._set.add_members(node_list) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def remove_marker(self, mkr): @@ -478,7 +425,7 @@ def remove_marker(self, mkr): node = mkr.get_node() if self._set.member_in_set(node): self._set.remove_member(node) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def remove_marker_list(self, mkr_list): @@ -488,7 +435,7 @@ def remove_marker_list(self, mkr_list): if isinstance(mkr, marker.Marker): node_list.append(mkr.get_node()) self._set.remove_members(node_list) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def set_marker_list(self, mkr_list): @@ -502,7 +449,7 @@ def set_marker_list(self, mkr_list): after_num = self.get_marker_list_length() if before_num != after_num: - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def clear_marker_list(self): @@ -514,7 +461,7 @@ def clear_marker_list(self): rm_list.append(member) if len(rm_list) > 0: self._set.remove_members(rm_list) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return ############################################################################ @@ -538,7 +485,7 @@ def add_attribute(self, attr): assert isinstance(name, (str, unicode)) if not self._set.member_in_set(name): self._set.add_member(name) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def add_attribute_list(self, attr_list): @@ -548,7 +495,7 @@ def add_attribute_list(self, attr_list): if isinstance(attr, attribute.Attribute): name_list.append(attr.get_name()) self._set.add_members(name_list) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def remove_attribute(self, attr): @@ -556,7 +503,7 @@ def remove_attribute(self, attr): name = attr.get_name() if self._set.member_in_set(name): self._set.remove_member(name) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def remove_attribute_list(self, attr_list): @@ -566,7 +513,7 @@ def remove_attribute_list(self, attr_list): if isinstance(attr, attribute.Attribute): name_list.append(attr.get_name()) self._set.remove_members(name_list) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def set_attribute_list(self, mkr_list): @@ -580,7 +527,7 @@ def set_attribute_list(self, mkr_list): after_num = self.get_attribute_list_length() if before_num != after_num: - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return def clear_attribute_list(self): @@ -592,310 +539,12 @@ def clear_attribute_list(self): rm_list.append(member) if len(rm_list) > 0: self._set.remove_members(rm_list) - self._kwargs_list = [] # reset argument flag cache. + self._actions_list = [] # reset argument flag cache. return ############################################################################ - def __compile_solver(self, sol, mkr_list, attr_list, prog_fn=None): - """ - Compiles data given into flags for a single run of 'mmSolver'. - - :param sol: The solver to compile - :type sol: Solver - - :param mkr_list: Markers to measure - :type mkr_list: list of Marker - - :param attr_list: Attributes to solve for - :type attr_list: list of Attribute - - :param prog_fn: Progress Function, with signature f(int) - :type prog_fn: function - - :return: The keyword arguments for the mmSolver command. - :rtype: None or dict - """ - assert isinstance(sol, solver.Solver) - assert isinstance(mkr_list, list) - assert isinstance(attr_list, list) - assert sol.get_frame_list_length() > 0 - - kwargs = dict() - kwargs['camera'] = [] - kwargs['marker'] = [] - kwargs['attr'] = [] - kwargs['frame'] = [] - - # Get Markers and Cameras - added_cameras = [] - markers = [] - cameras = [] - for mkr in mkr_list: - assert isinstance(mkr, marker.Marker) - mkr_node = mkr.get_node() - assert isinstance(mkr_node, basestring) - bnd = mkr.get_bundle() - if bnd is None: - msg = 'Cannot find bundle from marker, skipping; mkr_node={0}' - msg = msg.format(repr(mkr_node)) - LOG.warning(msg) - continue - bnd_node = bnd.get_node() - if bnd_node is None: - msg = 'Bundle node is invalid, skipping; mkr_node={0}' - msg = msg.format(repr(mkr_node)) - LOG.warning(msg) - continue - cam = mkr.get_camera() - if cam is None: - msg = 'Cannot find camera from marker; mkr={0}' - msg = msg.format(mkr.get_node()) - LOG.warning(msg) - cam_tfm_node = cam.get_transform_node() - cam_shp_node = cam.get_shape_node() - assert isinstance(cam_tfm_node, basestring) - assert isinstance(cam_shp_node, basestring) - markers.append((mkr_node, cam_shp_node, bnd_node)) - if cam_shp_node not in added_cameras: - cameras.append((cam_tfm_node, cam_shp_node)) - added_cameras.append(cam_shp_node) - if len(markers) == 0: - LOG.warning('No Markers found!') - return None - if len(cameras) == 0: - LOG.warning('No Cameras found!') - return None - - # Get Attributes - use_animated = sol.get_attributes_use_animated() - use_static = sol.get_attributes_use_static() - attrs = [] - for attr in attr_list: - assert isinstance(attr, attribute.Attribute) - if attr.is_locked(): - continue - name = attr.get_name() - node_name = attr.get_node() - attr_name = attr.get_attr() - - # If the user does not specify a min/max value then we get it - # from Maya directly, if Maya doesn't have one, we leave - # min/max_value as None and pass it to the mmSolver command - # indicating there is no bound. - min_value = attr.get_min_value() - max_value = attr.get_max_value() - if min_value is None: - min_exists = maya.cmds.attributeQuery( - attr_name, - node=node_name, - minExists=True, - ) - if min_exists: - min_value = maya.cmds.attributeQuery( - attr_name, - node=node_name, - minimum=True, - ) - if len(min_value) == 1: - min_value = min_value[0] - else: - msg = 'Cannot handle attributes with multiple ' - msg += 'minimum values; node={0} attr={1}' - msg = msg.format(node_name, attr_name) - raise excep.NotValid(msg) - - if max_value is None: - max_exists = maya.cmds.attributeQuery( - attr_name, - node=node_name, - maxExists=True, - ) - if max_exists is True: - max_value = maya.cmds.attributeQuery( - attr_name, - node=node_name, - maximum=True, - ) - if len(max_value) == 1: - max_value = max_value[0] - else: - msg = 'Cannot handle attributes with multiple ' - msg += 'maximum values; node={0} attr={1}' - msg = msg.format(node_name, attr_name) - raise excep.NotValid(msg) - - # Scale and Offset - scale_value = None - offset_value = None - attr_type = maya.cmds.attributeQuery( - attr_name, - node=node_name, - attributeType=True) - if attr_type.endswith('Angle'): - offset_value = 360.0 - - animated = attr.is_animated() - static = attr.is_static() - use = False - if use_animated and animated is True: - use = True - if use_static and static is True: - use = True - if use is True: - attrs.append( - (name, - str(min_value), - str(max_value), - str(offset_value), - str(scale_value)) - ) - if len(attrs) == 0: - LOG.warning('No Attributes found!') - return None - - # Get Frames - frm_list = sol.get_frame_list() - frame_use_tags = sol.get_frames_use_tags() - frames = [] - for frm in frm_list: - num = frm.get_number() - tags = frm.get_tags() - use = False - if len(frame_use_tags) > 0 and len(tags) > 0: - for tag in frame_use_tags: - if tag in tags: - use = True - break - else: - use = True - if use is True: - frames.append(num) - if len(frames) == 0: - LOG.warning('No Frames found!') - return None - - kwargs['marker'] = markers - kwargs['camera'] = cameras - kwargs['attr'] = attrs - kwargs['frame'] = frames - - solver_type = sol.get_solver_type() - if solver_type is not None: - kwargs['solverType'] = solver_type - - iterations = sol.get_max_iterations() - if iterations is not None: - kwargs['iterations'] = iterations - - verbose = sol.get_verbose() - if verbose is not None: - kwargs['verbose'] = verbose - - delta_factor = sol.get_delta_factor() - if delta_factor is not None: - kwargs['delta'] = delta_factor - - auto_diff_type = sol.get_auto_diff_type() - if auto_diff_type is not None: - kwargs['autoDiffType'] = auto_diff_type - - tau_factor = sol.get_tau_factor() - if tau_factor is not None: - kwargs['tauFactor'] = tau_factor - - gradient_error_factor = sol.get_gradient_error_factor() - if gradient_error_factor is not None: - kwargs['epsilon1'] = gradient_error_factor - - parameter_error_factor = sol.get_parameter_error_factor() - if parameter_error_factor is not None: - kwargs['epsilon2'] = parameter_error_factor - - error_factor = sol.get_error_factor() - if error_factor is not None: - kwargs['epsilon3'] = error_factor - - # msg = 'kwargs:\n' + pprint.pformat(kwargs) - # LOG.debug(msg) - return kwargs - - def _compile(self, prog_fn=None, status_fn=None): - """ - Take the data in this class and compile it into keyword argument flags. - - :return: list of keyword arguments. - :rtype: list of dict - """ - # TODO: Cache the compiled result internally to speed up - # 'is_valid' then '_compile' calls. - - # If the class attributes haven't been changed, re-use the previously - # generated arguments. - if len(self._kwargs_list) > 0: - return self._kwargs_list - - # Re-compile the arguments. - kwargs_list = [] - col_node = self.get_node() - - # Check Solvers - sol_list = self.get_solver_list() - sol_enabled_list = [sol for sol in sol_list - if sol.get_enabled() is True] - if len(sol_enabled_list) == 0: - msg = 'Collection is not valid, no enabled Solvers given; ' - msg += 'collection={0}' - msg = msg.format(repr(col_node)) - raise excep.NotValid(msg) - - # Check Markers - mkr_list = self.get_marker_list() - if len(mkr_list) == 0: - msg = 'Collection is not valid, no Markers given; collection={0}' - msg = msg.format(repr(col_node)) - raise excep.NotValid(msg) - - # Check Attributes - attr_list = self.get_attribute_list() - if len(attr_list) == 0: - msg = 'Collection is not valid, no Attributes given; collection={0}' - msg = msg.format(repr(col_node)) - raise excep.NotValid(msg) - - # Compile all the solvers - for i, sol in enumerate(sol_enabled_list): - if sol.get_frame_list_length() == 0: - msg = 'Collection is not valid, no frames to solve;' - msg += ' collection={0}' - msg = msg.format(repr(col_node)) - raise excep.NotValid(msg) - kwargs = self.__compile_solver(sol, mkr_list, attr_list) - - # Add a debug file flag to the mmSolver command, only - # triggered during debug mode. - if logging.DEBUG >= LOG.getEffectiveLevel(): - debug_file = maya.cmds.file(query=True, sceneName=True) - debug_file = os.path.basename(debug_file) - debug_file, ext = os.path.splitext(debug_file) - debug_file_path = os.path.join( - os.path.expandvars('${TEMP}'), - debug_file + '_' + str(i).zfill(6) + '.log' - ) - if len(debug_file) > 0 and debug_file_path is not None: - kwargs['debugFile'] = debug_file_path - - if isinstance(kwargs, dict): - kwargs_list.append(kwargs) - else: - msg = 'Collection is not valid, failed to compile solver;' - msg += ' collection={0}' - msg = msg.format(repr(col_node)) - raise excep.NotValid(msg) - - # Set arguments - self._kwargs_list = kwargs_list # save a copy - return self._kwargs_list + # TODO: Add 'logging level' flag to Collection. def is_valid(self, prog_fn=None, status_fn=None): """ @@ -914,248 +563,28 @@ def is_valid(self, prog_fn=None, status_fn=None): :returns: Is the Collection valid to solve? Yes or no. :rtype: bool """ - try: - self._compile(prog_fn=None, status_fn=None) - ret = True - except excep.NotValid: - ret = False - return ret + msg = 'Collection.is_valid is deprecated, use "collection_is_valid" function.' + warnings.warn(msg, DeprecationWarning) + return execute.collection_is_valid( + self, + prog_fn=prog_fn, + status_fn=status_fn + ) def execute(self, options=None, prog_fn=None, status_fn=None, info_fn=None): - """ - Compile the collection, then pass that data to the 'mmSolver' command. - - The mmSolver command will return a list of strings, which will then be - passed to the SolveResult class so the user can query the raw data - using an interface. - - :param options: The options for the execution. - :type options: ExecuteOptions - - :param prog_fn: The function used report progress messages to - the user. - :type prog_fn: callable or None - - :param status_fn: The function used to report status messages - to the user. - :type status_fn: callable or None - - :param info_fn: The function used to report information - messages to the user. - :type info_fn: callable or None - - :return: List of SolveResults from the executed collection. - :rtype: [SolverResult, ..] - """ - if options is None: - options = createExecuteOptions() - - start_time = time.time() - - # Ensure the plug-in is loaded, so we fail before trying to run. - api_utils.load_plugin() - - # TODO: Pause viewport 2.0 while solving? Assumes viewport 2 is used. - # This might not be supported below Maya 2017? - - # If 'refresh' is 'on' change all viewports to 'isolate - # selected' on only the markers and bundles being solved. This - # will speed up computations, especially per-frame solving as - # it will not re-compute any invisible nodes (such as rigs or - # image planes). - panel_objs = {} - panel_img_pl_vis = {} - panels = viewport_utils.get_all_model_panels() - if options.refresh is True: - for panel in panels: - if options.do_isolate is True: - state = maya.cmds.isolateSelect( - panel, - query=True, - state=True) - nodes = None - if state is True: - nodes = viewport_utils.get_isolated_nodes(panel) - panel_objs[panel] = nodes - panel_img_pl_vis[panel] = viewport_utils.get_image_plane_visibility(panel) - - # Save current frame, to revert to later on. - cur_frame = maya.cmds.currentTime(query=True) - - try: - collectionutils.run_progress_func(prog_fn, 0) - ts = solveresult.format_timestamp(time.time()) - collectionutils.run_status_func(status_fn, 'Solve start (%s)' % ts) - api_state.set_solver_running(True) - - # Check for validity - solres_list = [] - if self.is_valid() is False: - LOG.warning('Collection not valid: %r', self.get_node()) - return solres_list - kwargs_list = self._compile() - collectionutils.run_progress_func(prog_fn, 1) - - # Isolate all nodes used in all of the kwargs to be run. - # Note; This assumes the isolated objects are visible, but - # they may actually be hidden. - if options.refresh is True: - s = time.time() - if options.do_isolate is True: - isolate_nodes = set() - for kwargs in kwargs_list: - isolate_nodes |= collectionutils.generate_isolate_nodes(kwargs) - if len(isolate_nodes) == 0: - raise excep.NotValid - isolate_node_list = list(isolate_nodes) - for panel in panels: - viewport_utils.set_isolated_nodes(panel, isolate_node_list, True) - - for panel in panels: - viewport_utils.set_image_plane_visibility( - panel, - options.display_image_plane) - e = time.time() - LOG.debug('Perform Pre-Isolate; time=%r', e - s) - - # Set the first current time to the frame before current. - # This is to help trigger evaluations on the 'current - # frame', if the current frame is the same as the first - # frame. - frame_list = [] - for kwargs in kwargs_list: - frame_list += kwargs.get('frame', []) - frame_list = list(set(frame_list)) - frame_list = list(sorted(frame_list)) - is_whole_solve_single_frame = len(frame_list) == 1 - if is_whole_solve_single_frame is False: - s = time.time() - maya.cmds.currentTime( - cur_frame - 1, - edit=True, - update=options.force_update, - ) - e = time.time() - LOG.debug('Update previous of current time; time=%r', e - s) - - # Run Solver... - marker_nodes = [] - start = 0 - total = len(kwargs_list) - for i, kwargs in enumerate(kwargs_list): - frame = kwargs.get('frame') - collectionutils.run_status_func(info_fn, 'Evaluating frames %r' % frame) - if frame is None or len(frame) == 0: - raise excep.NotValid - - # Write solver flags to a debug file. - debug_file_path = kwargs.get('debugFile', None) - if debug_file_path is not None: - options_file_path = debug_file_path.replace('.log', '.flags') - text = pprint.pformat(kwargs) - with open(options_file_path, 'w') as file_: - file_.write(text) - - # Overriding the verbosity, irrespective of what - # the solver verbosity value is set to. - if options.verbose is True: - kwargs['verbose'] = True - - # HACK for single frame solves. - save_node_attrs = [] - is_single_frame = collectionutils.is_single_frame(kwargs) - if is_single_frame is True: - save_node_attrs = collectionutils.disconnect_animcurves(kwargs) - - # Run Solver Maya plug-in command - solve_data = maya.cmds.mmSolver(**kwargs) - - # Revert special HACK for single frame solves - if is_single_frame is True: - collectionutils.reconnect_animcurves(kwargs, save_node_attrs) - - # Create SolveResult. - solres = solveresult.SolveResult(solve_data) - solres_list.append(solres) - - # Update progress - ratio = float(i) / float(total) - percent = float(start) + (ratio * (100.0 - start)) - collectionutils.run_progress_func(prog_fn, int(percent)) - - # Check if the user wants to stop solving. - cmd_cancel = solres.get_user_interrupted() - gui_cancel = api_state.get_user_interrupt() - if cmd_cancel is True or gui_cancel is True: - msg = 'Cancelled by User' - api_state.set_user_interrupt(False) - collectionutils.run_status_func(status_fn, 'WARNING: ' + msg) - LOG.warning(msg) - break - if solres.get_success() is False: - msg = 'Solver failed!!!' - collectionutils.run_status_func(status_fn, 'ERROR: ' + msg) - LOG.error(msg) - - marker_nodes += [x[0] for x in kwargs.get('marker', [])] - - # Refresh the Viewport. - if options.refresh is True: - # TODO: If we solve per-frame without "refresh" - # on, then we get wacky solvers - # per-frame. Interestingly, the 'force_update' - # does not seem to make a difference, just the - # 'maya.cmds.refresh' call. - # - # Test scene file: - # ./tests/data/scenes/mmSolverBasicSolveD_before.ma - s = time.time() - maya.cmds.currentTime( - frame[0], - edit=True, - update=options.force_update, - ) - # TODO: Refresh should not add to undo queue, we - # should skip it. This should fix the problem of - # stepping over the viewport each time we undo an - # 'animated' solve. Or we pause the viewport while - # we undo? - maya.cmds.refresh() - e = time.time() - LOG.debug('Refresh Viewport; time=%r', e - s) - finally: - if options.refresh is True: - s = time.time() - for panel, objs in panel_objs.items(): - if objs is None: - # No original objects, disable 'isolate - # selected' after resetting the objects. - if options.do_isolate is True: - viewport_utils.set_isolated_nodes(panel, [], False) - img_pl_vis = panel_img_pl_vis.get(panel, True) - viewport_utils.set_image_plane_visibility(panel, img_pl_vis) - else: - if options.do_isolate is True: - viewport_utils.set_isolated_nodes(panel, list(objs), True) - e = time.time() - LOG.debug('Finally; reset isolate selected; time=%r', e - s) - - collectionutils.run_status_func(status_fn, 'Solve Ended') - collectionutils.run_progress_func(prog_fn, 100) - api_state.set_solver_running(False) - maya.cmds.currentTime(cur_frame, edit=True, update=True) - - # Store output information of the solver. - end_time = time.time() - duration = end_time - start_time - self._set_last_solve_timestamp(end_time) - self._set_last_solve_duration(duration) - self._set_last_solve_results(solres_list) - return solres_list + msg = 'Collection.execute is deprecated, use "execute" function.' + warnings.warn(msg, DeprecationWarning) + result = execute.execute( + self, + options=options, + prog_fn=prog_fn, + status_fn=status_fn, + info_fn=info_fn) + return result def update_deviation_on_collection(col, solres_list): diff --git a/python/mmSolver/_api/execute.py b/python/mmSolver/_api/execute.py new file mode 100644 index 000000000..95b9906fb --- /dev/null +++ b/python/mmSolver/_api/execute.py @@ -0,0 +1,569 @@ +# Copyright (C) 2019 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Execute a solve. +""" + + +import time +import pprint +import collections + +import maya.cmds +import maya.mel + +import mmSolver.logger +import mmSolver.utils.viewport as viewport_utils +import mmSolver._api.state as api_state +import mmSolver._api.utils as api_utils +import mmSolver._api.excep as excep +import mmSolver._api.solveresult as solveresult +import mmSolver._api.action as api_action +import mmSolver._api.solverbase as solverbase +import mmSolver._api.collectionutils as collectionutils + +LOG = mmSolver.logger.get_logger() + +ExecuteOptions = collections.namedtuple( + 'ExecuteOptions', + ('verbose', + 'refresh', + 'force_update', + 'pre_solve_force_eval', + 'do_isolate', + 'display_grid', + 'display_node_types') +) + + +def createExecuteOptions(verbose=False, + refresh=False, + force_update=False, + do_isolate=False, + pre_solve_force_eval=True, + display_grid=True, + display_node_types=None, + ): + """ + Create ExecuteOptions. + + :param verbose: Print extra solver information while a solve is running. + :type verbose: bool + + :param refresh: Should the solver refresh the viewport while solving? + :type refresh: bool + + :param force_update: Force updating the DG network, to help the + solver in case of a Maya evaluation DG bug. + :type force_update: bool + + :param do_isolate: Isolate only solving objects while performing + the solve. + :type do_isolate: bool + + :param display_grid: Display grid in the viewport while performing + the solve? + :type display_grid: bool + + :param display_node_types: Allow a dict to be passed to the function + specifying the object type and the + visibility status during solving. This + allows us to turn on/off any object type + during solving. If an argument is not + given or is None, the object type + visibility will not be changed. + """ + if display_node_types is None: + display_node_types = dict() + options = ExecuteOptions( + verbose=verbose, + refresh=refresh, + force_update=force_update, + do_isolate=do_isolate, + pre_solve_force_eval=pre_solve_force_eval, + display_grid=display_grid, + display_node_types=display_node_types + ) + return options + + +def _compile(col_node, sol_list, mkr_list, attr_list, prog_fn=None, status_fn=None): + """ + Take the data in this class and compile it into keyword argument flags. + + :return: list of SolverActions. + :rtype: [SolverAction, ..] + """ + action_list = [] + + # Check Solvers + sol_enabled_list = [sol for sol in sol_list + if sol.get_enabled() is True] + if len(sol_enabled_list) == 0: + msg = 'Collection is not valid, no enabled Solvers given; ' + msg += 'collection={0}' + msg = msg.format(repr(col_node)) + raise excep.NotValid(msg) + + # Check Markers + if len(mkr_list) == 0: + msg = 'Collection is not valid, no Markers given; collection={0}' + msg = msg.format(repr(col_node)) + raise excep.NotValid(msg) + + # Check Attributes + if len(attr_list) == 0: + msg = 'Collection is not valid, no Attributes given; collection={0}' + msg = msg.format(repr(col_node)) + raise excep.NotValid(msg) + + # Compile all the solvers + for i, sol in enumerate(sol_enabled_list): + assert isinstance(sol, solverbase.SolverBase) + if sol.get_frame_list_length() == 0: + msg = 'Collection is not valid, no frames to solve;' + msg += ' collection={0}' + msg = msg.format(repr(col_node)) + raise excep.NotValid(msg) + + actions = sol.compile(mkr_list, attr_list) + assert isinstance(actions, (tuple, list)) + + msg = 'Collection is not valid, failed to compile solver;' + msg += ' collection={0}' + msg = msg.format(repr(col_node)) + if len(actions) == 0: + raise excep.NotValid(msg) + + for action in actions: + if not isinstance(action, api_action.Action): + raise excep.NotValid(msg) + assert action.func is not None + assert action.args is not None + assert action.kwargs is not None + action_list.append(action) + return action_list + + +def preSolve_updateProgress(prog_fn, status_fn): + # Start up solver + collectionutils.run_progress_func(prog_fn, 0) + ts = solveresult.format_timestamp(time.time()) + collectionutils.run_status_func(status_fn, 'Solve start (%s)' % ts) + api_state.set_solver_running(True) + + +def preSolve_queryViewportState(options, panels): + """ + If 'refresh' is 'on' change all viewports to 'isolate + selected' on only the markers and bundles being solved. This + will speed up computations, especially per-frame solving as + it will not re-compute any invisible nodes (such as rigs or + image planes). + + :param options: + :param panels: + :return: + """ + panel_objs = {} + panel_node_type_vis = collections.defaultdict(dict) + if options.refresh is not True: + return panel_objs, panel_node_type_vis + + display_node_types = options.display_node_types + if display_node_types is not None: + assert isinstance(display_node_types, dict) + for panel in panels: + node_types = display_node_types.keys() + node_type_vis = dict() + for node_type in node_types: + value = viewport_utils.get_node_type_visibility(panel, node_type) + node_type_vis[node_type] = value + panel_node_type_vis[panel] = node_type_vis + + if options.do_isolate is True: + for panel in panels: + state = maya.cmds.isolateSelect( + panel, + query=True, + state=True) + nodes = None + if state is True: + nodes = viewport_utils.get_isolated_nodes(panel) + panel_objs[panel] = nodes + + return panel_objs, panel_node_type_vis + + +def preSolve_setIsolatedNodes(actions_list, options, panels): + """ + Prepare frame solve + + Isolate all nodes used in all of the kwargs to be run. + Note; This assumes the isolated objects are visible, but + they may actually be hidden. + """ + if options.refresh is not True: + return + s = time.time() + if options.do_isolate is True: + isolate_nodes = set() + for action in actions_list: + kwargs = action.kwargs + isolate_nodes |= collectionutils.generate_isolate_nodes(kwargs) + if len(isolate_nodes) == 0: + raise excep.NotValid + isolate_node_list = list(isolate_nodes) + for panel in panels: + viewport_utils.set_isolated_nodes(panel, isolate_node_list, True) + + display_node_types = options.display_node_types + if display_node_types is not None: + assert isinstance(display_node_types, dict) + for panel in panels: + for node_type, value in options.display_node_types: + if value is None: + continue + assert isinstance(value, bool) + viewport_utils.set_node_type_visibility(panel, node_type, value) + + e = time.time() + LOG.debug('Perform Pre-Isolate; time=%r', e - s) + return + + +def preSolve_triggerEvaluation(action_list, cur_frame, options): + """ + Set the first current time to the frame before current. + + This is to help trigger evaluations on the 'current + frame', if the current frame is the same as the first + frame. + + :param action_list: + :type action_list: [Action, .. ] + + :param cur_frame: + :type cur_frame: int or float + + :param options: + :type options: + """ + if options.pre_solve_force_eval is not True: + return + s = time.time() + frame_list = [] + for action in action_list: + kwargs = action.kwargs + frame_list += kwargs.get('frame', []) + frame_list = list(set(frame_list)) + frame_list = list(sorted(frame_list)) + is_whole_solve_single_frame = len(frame_list) == 1 + if is_whole_solve_single_frame is False: + maya.cmds.currentTime( + cur_frame - 1, + edit=True, + update=options.force_update, + ) + e = time.time() + LOG.debug('Update previous of current time; time=%r', e - s) + return + + +def postSolve_refreshViewport(options, frame): + # Refresh the Viewport. + if options.refresh is not True: + return + # TODO: If we solve per-frame without "refresh" + # on, then we get wacky solves + # per-frame. Interestingly, the 'force_update' + # does not seem to make a difference, just the + # 'maya.cmds.refresh' call. + # + # Test scene file: + # ./tests/data/scenes/mmSolverBasicSolveD_before.ma + s = time.time() + maya.cmds.currentTime( + frame[0], + edit=True, + update=options.force_update, + ) + # TODO: Refresh should not add to undo queue, we + # should skip it. This should fix the problem of + # stepping over the viewport each time we undo an + # 'animated' solve. Or we pause the viewport while + # we undo? + maya.cmds.refresh() + e = time.time() + LOG.debug('Refresh Viewport; time=%r', e - s) + return + + +def postSolve_setViewportState(options, panel_objs, panel_node_type_vis): + if options.refresh is not True: + return + s = time.time() + for panel, objs in panel_objs.items(): + if objs is None: + # No original objects, disable 'isolate + # selected' after resetting the objects. + if options.do_isolate is True: + viewport_utils.set_isolated_nodes(panel, [], False) + node_types_vis = panel_node_type_vis[panel] + for node_type, value in node_types_vis: + if value is None: + continue + viewport_utils.set_node_type_visibility(panel, node_type, value) + else: + if options.do_isolate is True: + viewport_utils.set_isolated_nodes(panel, list(objs), True) + e = time.time() + LOG.debug('Finally; reset isolate selected; time=%r', e - s) + return + + +def postSolve_setUpdateProgress(progress_min, + progress_value, + progress_max, + solres, # SolveResult or None + prog_fn, status_fn): + stop_solving = False + + # Update progress + ratio = float(progress_value) / float(progress_max) + percent = float(progress_min) + (ratio * (100.0 - progress_min)) + collectionutils.run_progress_func(prog_fn, int(percent)) + + # Check if the user wants to stop solving. + if solres is None: + cmd_cancel = False + else: + cmd_cancel = solres.get_user_interrupted() + gui_cancel = api_state.get_user_interrupt() + if cmd_cancel is True or gui_cancel is True: + msg = 'Cancelled by User' + api_state.set_user_interrupt(False) + collectionutils.run_status_func(status_fn, 'WARNING: ' + msg) + LOG.warning(msg) + stop_solving = True + if (solres is not None) and (solres.get_success() is False): + msg = 'Solver failed!!!' + collectionutils.run_status_func(status_fn, 'ERROR: ' + msg) + LOG.error(msg) + return stop_solving + + +def collection_is_valid(col, prog_fn=None, status_fn=None): + """ + Tests if the current sate of this Collection is valid to solve with. + + :param prog_fn: Progress function callback. If not None this + function will be executed to emit progress + messages. + :type prog_fn: callable + + :param status_fn: Status message function callback. If not + None this function will be executed each + time a status message needs to be printed. + :type status_fn: callable + + :returns: Is the Collection valid to solve? Yes or no. + :rtype: bool + """ + try: + col_node = col.get_node() + sol_list = col.get_solver_list() + mkr_list = col.get_marker_list() + attr_list = col.get_attribute_list() + _compile(col_node, sol_list, mkr_list, attr_list, + prog_fn=None, status_fn=None) + ret = True + except excep.NotValid: + ret = False + return ret + + +def execute(col, + options=None, + prog_fn=None, + status_fn=None, + info_fn=None): + """ + Compile the collection, then pass that data to the 'mmSolver' command. + + The mmSolver command will return a list of strings, which will then be + passed to the SolveResult class so the user can query the raw data + using an interface. + + :param col: The Collection to execute. + :type col: Collection + + :param options: The options for the execution. + :type options: ExecuteOptions + + :param prog_fn: The function used report progress messages to + the user. + :type prog_fn: callable or None + + :param status_fn: The function used to report status messages + to the user. + :type status_fn: callable or None + + :param info_fn: The function used to report information + messages to the user. + :type info_fn: callable or None + + :return: List of SolveResults from the executed collection. + :rtype: [SolverResult, ..] + """ + if options is None: + options = createExecuteOptions() + + start_time = time.time() + + # Ensure the plug-in is loaded, so we fail before trying to run. + api_utils.load_plugin() + assert 'mmSolver' in dir(maya.cmds) + + # TODO: Pause viewport 2.0 while solving? Assumes viewport 2 is used. + # This might not be supported below Maya 2017? + + # TODO: Test if 'isolate selected' works at all perhaps we're + # better off just turning the node types on/off. + + panels = viewport_utils.get_all_model_panels() + panel_objs, panel_node_type_vis = preSolve_queryViewportState( + options, panels + ) + + # Save current frame, to revert to later on. + cur_frame = maya.cmds.currentTime(query=True) + + try: + preSolve_updateProgress(prog_fn, status_fn) + + # Check for validity + solres_list = [] + if col.is_valid() is False: + LOG.warning('Collection not valid: %r', col.get_node()) + return solres_list + col_node = col.get_node() + sol_list = col.get_solver_list() + mkr_list = col.get_marker_list() + attr_list = col.get_attribute_list() + # TODO: Cache the compiled result internally to speed up + # 'is_valid' then '_compile' calls. + + # If the class attributes haven't been changed, re-use the previously + # generated arguments. + # TODO: Don't use kwargs directly, use 'SolverAction' objects (which + # are callable functions with args and kwargs). + # if len(col._kwargs_list) > 0: + # return col._kwargs_list + actions_list = _compile(col_node, sol_list, mkr_list, attr_list, + prog_fn=prog_fn, status_fn=status_fn) + collectionutils.run_progress_func(prog_fn, 1) + + # Prepare frame solve + preSolve_setIsolatedNodes(actions_list, options, panels) + preSolve_triggerEvaluation(actions_list, cur_frame, options) + + # Run Solver Actions... + start = 0 + total = len(actions_list) + for i, action in enumerate(actions_list): + func = action.func + args = list(action.args) + kwargs = action.kwargs.copy() + + frame = kwargs.get('frame') + collectionutils.run_status_func(info_fn, 'Evaluating frames %r' % frame) + if frame is None or len(frame) == 0: + raise excep.NotValid + + # Write solver flags to a debug file. + debug_file_path = kwargs.get('debugFile', None) + if debug_file_path is not None: + options_file_path = debug_file_path.replace('.log', '.flags') + text = pprint.pformat(kwargs) + with open(options_file_path, 'w') as file_: + file_.write(text) + + # Overriding the verbosity, irrespective of what + # the solver verbosity value is set to. + if options.verbose is True: + kwargs['verbose'] = True + + # TODO: Try to test and remove the need to disconnect + # and re-connect animation curves. + # + # HACK for single frame solves. + save_node_attrs = [] + is_single_frame = collectionutils.is_single_frame(kwargs) + if is_single_frame is True: + save_node_attrs = collectionutils.disconnect_animcurves(kwargs) + + # TODO: Detect if we can re-run the mmSolver command + # multiple times, to get better quality. + # + # TODO: Run the solver multiple times for a hierarchy. First, + # solve DAG level 0 nodes, then add DAG level 1, then level 2, + # etc. This will allow us to incrementally add solving of + # hierarchy, without getting the optimiser confused which + # attributes to solve first to get a stable solve. + # + # Run Solver Maya plug-in command + solve_data = func(*args, **kwargs) + + # Revert special HACK for single frame solves + if is_single_frame is True: + collectionutils.reconnect_animcurves(kwargs, save_node_attrs) + + # Create SolveResult. + solres = None + if solve_data is not None: + solres = solveresult.SolveResult(solve_data) + solres_list.append(solres) + + # Update Progress + interrupt = postSolve_setUpdateProgress( + start, i, total, solres, + prog_fn, status_fn + ) + if interrupt is True: + break + + # Refresh the Viewport. + postSolve_refreshViewport(options, frame) + finally: + postSolve_setViewportState( + options, panel_objs, panel_node_type_vis + ) + + collectionutils.run_status_func(status_fn, 'Solve Ended') + collectionutils.run_progress_func(prog_fn, 100) + api_state.set_solver_running(False) + maya.cmds.currentTime(cur_frame, edit=True, update=True) + + # Store output information of the solver. + end_time = time.time() + duration = end_time - start_time + col._set_last_solve_timestamp(end_time) + col._set_last_solve_duration(duration) + col._set_last_solve_results(solres_list) + return solres_list diff --git a/python/mmSolver/_api/solver.py b/python/mmSolver/_api/solver.py deleted file mode 100644 index d78ab7665..000000000 --- a/python/mmSolver/_api/solver.py +++ /dev/null @@ -1,264 +0,0 @@ -# Copyright (C) 2018, 2019 David Cattermole. -# -# This file is part of mmSolver. -# -# mmSolver is free software: you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mmSolver is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with mmSolver. If not, see . -# -""" -Solver related functions. -""" - -import uuid -import mmSolver._api.frame as frame -import mmSolver._api.excep as excep -import mmSolver._api.constant as const - - -class Solver(object): - """ - Solver; the options for how a solver should be executed. - """ - def __init__(self, name=None, data=None): - self._data = const.SOLVER_DATA_DEFAULT.copy() - if isinstance(data, dict): - self.set_data(data) - if isinstance(name, (str, unicode, uuid.UUID)): - self._data['name'] = name - else: - # give the solver a random name. - if 'name' not in self._data: - self._data['name'] = str(uuid.uuid4()) - assert 'name' in self._data - - self._attributes_use = { - 'animated': True, - 'static': True, - } - self._frames_use = { - 'tags': ['primary', 'secondary', 'normal'], - } - return - - def get_name(self): - return self._data.get('name') - - def set_name(self, name): - assert isinstance(name, (str, unicode, uuid.UUID)) - self._data['name'] = str(name) - return - - def get_data(self): - assert isinstance(self._data, dict) - return self._data.copy() - - def set_data(self, data): - assert isinstance(data, dict) - self._data = data.copy() - return self - - ############################################################################ - - def get_enabled(self): - """ - Flags this solver should not be used for solving. - :rtype: bool - """ - return self._data.get('enabled') - - def set_enabled(self, value): - """ - Set if this solver be used? - - :param value: The enabled value. - :type value: bool - """ - if isinstance(value, bool) is False: - raise TypeError('Expected bool value type.') - self._data['enabled'] = value - return - - def get_max_iterations(self): - return self._data.get('max_iterations') - - def set_max_iterations(self, value): - if isinstance(value, int) is False: - raise TypeError('Expected int value type.') - self._data['max_iterations'] = value - return - - def get_delta_factor(self): - return self._data.get('delta') - - def set_delta_factor(self, value): - self._data['delta'] = value - return - - def get_auto_diff_type(self): - return self._data.get('auto_diff_type') - - def set_auto_diff_type(self, value): - if value not in const.AUTO_DIFF_TYPE_LIST: - msg = 'auto_diff_type must be one of %r; value=%r' - msg = msg % (const.AUTO_DIFF_TYPE_LIST, value) - raise ValueError(msg) - self._data['auto_diff_type'] = value - return - - def get_tau_factor(self): - return self._data.get('tau_factor') - - def set_tau_factor(self, value): - self._data['tau_factor'] = value - return - - def get_gradient_error_factor(self): - return self._data.get('gradient_error') - - def set_gradient_error_factor(self, value): - self._data['gradient_error'] = value - return - - def get_parameter_error_factor(self): - return self._data.get('parameter_error') - - def set_parameter_error_factor(self, value): - self._data['parameter_error'] = value - return - - def get_error_factor(self): - return self._data.get('error') - - def set_error_factor(self, value): - self._data['error'] = value - return - - def get_solver_type(self): - return self._data.get('solver_type') - - def set_solver_type(self, value): - self._data['solver_type'] = value - - def get_verbose(self): - return self._data.get('verbose') - - def set_verbose(self, value): - if isinstance(value, bool) is False: - raise TypeError('Expected bool value type.') - self._data['verbose'] = value - - ############################################################################ - - def get_attributes_use_animated(self): - return self._attributes_use.get('animated') - - def set_attributes_use_animated(self, value): - assert isinstance(value, (bool, int)) - self._attributes_use['animated'] = bool(value) - - def get_attributes_use_static(self): - return self._attributes_use.get('static') - - def set_attributes_use_static(self, value): - assert isinstance(value, (bool, int)) - self._attributes_use['static'] = bool(value) - - def get_frames_use_tags(self): - return self._frames_use.get('tags') - - def set_frames_use_tags(self, value): - assert isinstance(value, list) - self._frames_use['tags'] = value - - ############################################################################ - - def get_frame_list(self): - """ - Get frame objects attached to the solver. - - :return: frame objects. - :rtype: list of frame.Frame - """ - frame_list_data = self._data.get('frame_list') - if frame_list_data is None: - return [] - frm_list = [] - for f in frame_list_data: - frm = frame.Frame(0) - frm.set_data(f) # Override the frame number - frm_list.append(frm) - return frm_list - - def get_frame_list_length(self): - return len(self.get_frame_list()) - - def add_frame(self, frm): - assert isinstance(frm, frame.Frame) - key = 'frame_list' - frm_list_data = self._data.get(key) - if frm_list_data is None: - frm_list_data = [] - - # check we won't get a double up. - add_frm_data = frm.get_data() - for frm_data in frm_list_data: - if frm_data.get('number') == add_frm_data.get('number'): - msg = 'Frame already added to Solver, cannot add again: {0}' - msg = msg.format(add_frm_data) - raise excep.NotValid, msg - - frm_list_data.append(add_frm_data) - self._data[key] = frm_list_data - return - - def add_frame_list(self, frm_list): - assert isinstance(frm_list, list) - for frm in frm_list: - self.add_frame(frm) - return - - def remove_frame(self, frm): - assert isinstance(frm, frame.Frame) - key = 'frame_list' - frm_list_data = self._data.get(key) - if frm_list_data is None: - # Nothing to remove, initialise the data structure. - self._data[key] = [] - return - found_index = -1 - rm_frm_data = frm.get_data() - for i, frm_data in enumerate(frm_list_data): - if frm_data.get('number') == rm_frm_data.get('number'): - found_index = i - break - if found_index != -1: - del frm_list_data[found_index] - self._data[key] = frm_list_data - return - - def remove_frame_list(self, frm_list): - assert isinstance(frm_list, list) - for frm in frm_list: - self.remove_frame(frm) - return - - def set_frame_list(self, frm_list): - assert isinstance(frm_list, list) - self.clear_frame_list() - self.add_frame_list(frm_list) - return - - def clear_frame_list(self): - key = 'frame_list' - self._data[key] = [] - return diff --git a/python/mmSolver/_api/solverbase.py b/python/mmSolver/_api/solverbase.py new file mode 100644 index 000000000..325dbed0b --- /dev/null +++ b/python/mmSolver/_api/solverbase.py @@ -0,0 +1,36 @@ +# Copyright (C) 2019 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Solver operation base class. +""" + +import abc + + +class SolverBase(object): + """ + The base class of a Solver operation. + + A Solver Operation may have any number of methods and data, this class + does not enforce a common method interface (yet). + """ + + @abc.abstractmethod + def compile(self, mkr_list, attr_list): + raise NotImplementedError + diff --git a/python/mmSolver/_api/solverstandard.py b/python/mmSolver/_api/solverstandard.py new file mode 100644 index 000000000..7ca09e9ea --- /dev/null +++ b/python/mmSolver/_api/solverstandard.py @@ -0,0 +1,92 @@ +# Copyright (C) 2019 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +The standard solver. +""" + +import mmSolver._api.solverbase as solveropbase +import mmSolver._api.action as api_action + + +# class SolverOpTriangulateBundles(SolverBase): +# """ +# An operation to re-calculate the bundle positions using triangulation. +# """ +# pass + + +# class SolverOpSmoothCameraTranslate(SolverBase): +# """ +# An operation to smooth the translations of a camera. +# """ +# pass + + +class SolverStandard(solveropbase.SolverBase): + # TODO: Write a 'meta-solver' class to hold attributes for solving. + # Issue #57 - Maya Tool - Solver UI - Add Simplified Solver Settings + # Issue #72 - Python API - Re-Design Collection Compiling + # + # Parameters for 'meta solver': + # - Frame Range - with options: + # - "Single Frame" + # - "Time Slider (Inner)" + # - "Time Slider (Outer)" + # - "Custom" + # - Root Frames - A list of integer frame numbers. + # - Solver Method + # - "Solve Everything at Once" option - On or Off + # - "Solve Root Frames Only" option - On or Off + # + # If a Solver is 'Single Frame' (current frame), then we solve both + # animated and static attributes on the current frame, in a single step + # and return. + # + # If the 'Solver Root Frames Only' option is On, then we only solve the + # root frames, with both animated and static attributes. + # + # If the 'Solver Root Frames Only' is Off, then we first solve the root + # frames with both animated and static attributes, then secondly we solve + # only animated attributes for the entire frame range. + # + # If the 'Solve Everything at Once' option is On, then the second solve + # step contains static and animated attributes (not just animated), + # and all frames are solved as one big crunch. + # + # TODO: Before solving root frames we should query the current + # animated attribute values at each root frame, store it, + # then remove all keyframes between the first and last frames to + # solve. Lastly we should re-keyframe the values at the animated + # frames, and ensure the keyframe tangents are linear. This will + # ensure that animated keyframe values do not affect a re-solve. + # Only the root frames need to be initialized with good values. + # + # TODO: Add get/set 'use_single_frame' - bool + # TODO: Add get/set 'single_frame' - Frame or None + # TODO: Add get/set 'root_frame_list' - list of Frame + # TODO: Add get/set 'frame_list' - list of Frame + # TODO: Add get/set 'only_root_frames' - bool + # TODO: Add get/set 'global_solve' - bool + + def compile(self, mkr_list, attr_list): + action = api_action.Action( + func=None, + args=[], + kwargs=dict() + ) + return [action] diff --git a/python/mmSolver/_api/solverstep.py b/python/mmSolver/_api/solverstep.py new file mode 100644 index 000000000..dc9f430fc --- /dev/null +++ b/python/mmSolver/_api/solverstep.py @@ -0,0 +1,550 @@ +# Copyright (C) 2018, 2019 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Solver related functions. +""" + +import uuid + +import maya.cmds +import maya.mel + +import mmSolver.logger +import mmSolver._api.frame as frame +import mmSolver._api.excep as excep +import mmSolver._api.constant as const +import mmSolver._api.action as api_action +import mmSolver._api.solverbase as solverbase +import mmSolver._api.marker as marker +import mmSolver._api.attribute as attribute + +LOG = mmSolver.logger.get_logger() + + +def _compile_markersAndCameras(mkr_list): + # Get Markers and Cameras + added_cameras = [] + markers = [] + cameras = [] + for mkr in mkr_list: + assert isinstance(mkr, marker.Marker) + mkr_node = mkr.get_node() + assert isinstance(mkr_node, basestring) + bnd = mkr.get_bundle() + if bnd is None: + msg = 'Cannot find bundle from marker, skipping; mkr_node={0}' + msg = msg.format(repr(mkr_node)) + LOG.warning(msg) + continue + bnd_node = bnd.get_node() + if bnd_node is None: + msg = 'Bundle node is invalid, skipping; mkr_node={0}' + msg = msg.format(repr(mkr_node)) + LOG.warning(msg) + continue + cam = mkr.get_camera() + if cam is None: + msg = 'Cannot find camera from marker; mkr={0}' + msg = msg.format(mkr.get_node()) + LOG.warning(msg) + cam_tfm_node = cam.get_transform_node() + cam_shp_node = cam.get_shape_node() + assert isinstance(cam_tfm_node, basestring) + assert isinstance(cam_shp_node, basestring) + markers.append((mkr_node, cam_shp_node, bnd_node)) + if cam_shp_node not in added_cameras: + cameras.append((cam_tfm_node, cam_shp_node)) + added_cameras.append(cam_shp_node) + return markers, cameras + + +def _compile_attributes(attr_list, use_animated, use_static): + # Get Attributes + attrs = [] + for attr in attr_list: + assert isinstance(attr, attribute.Attribute) + if attr.is_locked(): + continue + name = attr.get_name() + node_name = attr.get_node() + attr_name = attr.get_attr() + + # If the user does not specify a min/max value then we get it + # from Maya directly, if Maya doesn't have one, we leave + # min/max_value as None and pass it to the mmSolver command + # indicating there is no bound. + min_value = attr.get_min_value() + max_value = attr.get_max_value() + if min_value is None: + min_exists = maya.cmds.attributeQuery( + attr_name, + node=node_name, + minExists=True, + ) + if min_exists: + min_value = maya.cmds.attributeQuery( + attr_name, + node=node_name, + minimum=True, + ) + if len(min_value) == 1: + min_value = min_value[0] + else: + msg = 'Cannot handle attributes with multiple ' + msg += 'minimum values; node={0} attr={1}' + msg = msg.format(node_name, attr_name) + raise excep.NotValid(msg) + + if max_value is None: + max_exists = maya.cmds.attributeQuery( + attr_name, + node=node_name, + maxExists=True, + ) + if max_exists is True: + max_value = maya.cmds.attributeQuery( + attr_name, + node=node_name, + maximum=True, + ) + if len(max_value) == 1: + max_value = max_value[0] + else: + msg = 'Cannot handle attributes with multiple ' + msg += 'maximum values; node={0} attr={1}' + msg = msg.format(node_name, attr_name) + raise excep.NotValid(msg) + + # Scale and Offset + scale_value = None + offset_value = None + attr_type = maya.cmds.attributeQuery( + attr_name, + node=node_name, + attributeType=True) + if attr_type.endswith('Angle'): + offset_value = 360.0 + + animated = attr.is_animated() + static = attr.is_static() + use = False + if use_animated and animated is True: + use = True + if use_static and static is True: + use = True + if use is True: + attrs.append( + (name, + str(min_value), + str(max_value), + str(offset_value), + str(scale_value)) + ) + return attrs + + +def _compile_frames(frm_list, frame_use_tags): + frames = [] + for frm in frm_list: + num = frm.get_number() + tags = frm.get_tags() + use = False + if len(frame_use_tags) > 0 and len(tags) > 0: + for tag in frame_use_tags: + if tag in tags: + use = True + break + else: + use = True + if use is True: + frames.append(num) + return frames + + +class SolverStep(solverbase.SolverBase): + """ + SolverStep; the options for how a solver should be executed. + """ + def __init__(self, name=None, data=None): + self._data = const.SOLVER_DATA_DEFAULT.copy() + if isinstance(data, dict): + self.set_data(data) + if isinstance(name, (str, unicode, uuid.UUID)): + self._data['name'] = name + else: + # give the solver a random name. + if 'name' not in self._data: + self._data['name'] = str(uuid.uuid4()) + assert 'name' in self._data + + self._attributes_use = { + 'animated': True, + 'static': True, + } + self._frames_use = { + 'tags': ['primary', 'secondary', 'normal'], + } + return + + def get_name(self): + return self._data.get('name') + + def set_name(self, name): + assert isinstance(name, (str, unicode, uuid.UUID)) + self._data['name'] = str(name) + return + + def get_data(self): + assert isinstance(self._data, dict) + return self._data.copy() + + def set_data(self, data): + assert isinstance(data, dict) + self._data = data.copy() + return self + + ############################################################################ + + def get_enabled(self): + """ + Flags this solver should not be used for solving. + :rtype: bool + """ + return self._data.get('enabled') + + def set_enabled(self, value): + """ + Set if this solver be used? + + :param value: The enabled value. + :type value: bool + """ + if isinstance(value, bool) is False: + raise TypeError('Expected bool value type.') + self._data['enabled'] = value + return + + def get_max_iterations(self): + return self._data.get('max_iterations') + + def set_max_iterations(self, value): + if isinstance(value, int) is False: + raise TypeError('Expected int value type.') + self._data['max_iterations'] = value + return + + def get_delta_factor(self): + return self._data.get('delta') + + def set_delta_factor(self, value): + self._data['delta'] = value + return + + def get_auto_diff_type(self): + return self._data.get('auto_diff_type') + + def set_auto_diff_type(self, value): + if value not in const.AUTO_DIFF_TYPE_LIST: + msg = 'auto_diff_type must be one of %r; value=%r' + msg = msg % (const.AUTO_DIFF_TYPE_LIST, value) + raise ValueError(msg) + self._data['auto_diff_type'] = value + return + + def get_tau_factor(self): + return self._data.get('tau_factor') + + def set_tau_factor(self, value): + self._data['tau_factor'] = value + return + + def get_gradient_error_factor(self): + return self._data.get('gradient_error') + + def set_gradient_error_factor(self, value): + self._data['gradient_error'] = value + return + + def get_parameter_error_factor(self): + return self._data.get('parameter_error') + + def set_parameter_error_factor(self, value): + self._data['parameter_error'] = value + return + + def get_error_factor(self): + return self._data.get('error') + + def set_error_factor(self, value): + self._data['error'] = value + return + + def get_solver_type(self): + return self._data.get('solver_type') + + def set_solver_type(self, value): + self._data['solver_type'] = value + + def get_verbose(self): + return self._data.get('verbose') + + def set_verbose(self, value): + if isinstance(value, bool) is False: + raise TypeError('Expected bool value type.') + self._data['verbose'] = value + + ############################################################################ + + def get_attributes_use_animated(self): + return self._attributes_use.get('animated') + + def set_attributes_use_animated(self, value): + assert isinstance(value, (bool, int)) + self._attributes_use['animated'] = bool(value) + + def get_attributes_use_static(self): + return self._attributes_use.get('static') + + def set_attributes_use_static(self, value): + assert isinstance(value, (bool, int)) + self._attributes_use['static'] = bool(value) + + def get_frames_use_tags(self): + return self._frames_use.get('tags') + + def set_frames_use_tags(self, value): + assert isinstance(value, list) + self._frames_use['tags'] = value + + ############################################################################ + + def get_frame_list(self): + """ + Get frame objects attached to the solver. + + :return: frame objects. + :rtype: list of frame.Frame + """ + frame_list_data = self._data.get('frame_list') + if frame_list_data is None: + return [] + frm_list = [] + for f in frame_list_data: + frm = frame.Frame(0) + frm.set_data(f) # Override the frame number + frm_list.append(frm) + return frm_list + + def get_frame_list_length(self): + return len(self.get_frame_list()) + + def add_frame(self, frm): + assert isinstance(frm, frame.Frame) + key = 'frame_list' + frm_list_data = self._data.get(key) + if frm_list_data is None: + frm_list_data = [] + + # check we won't get a double up. + add_frm_data = frm.get_data() + for frm_data in frm_list_data: + if frm_data.get('number') == add_frm_data.get('number'): + msg = 'Frame already added to SolverStep, cannot add again: {0}' + msg = msg.format(add_frm_data) + raise excep.NotValid(msg) + + frm_list_data.append(add_frm_data) + self._data[key] = frm_list_data + return + + def add_frame_list(self, frm_list): + assert isinstance(frm_list, list) + for frm in frm_list: + self.add_frame(frm) + return + + def remove_frame(self, frm): + assert isinstance(frm, frame.Frame) + key = 'frame_list' + frm_list_data = self._data.get(key) + if frm_list_data is None: + # Nothing to remove, initialise the data structure. + self._data[key] = [] + return + found_index = -1 + rm_frm_data = frm.get_data() + for i, frm_data in enumerate(frm_list_data): + if frm_data.get('number') == rm_frm_data.get('number'): + found_index = i + break + if found_index != -1: + del frm_list_data[found_index] + self._data[key] = frm_list_data + return + + def remove_frame_list(self, frm_list): + assert isinstance(frm_list, list) + for frm in frm_list: + self.remove_frame(frm) + return + + def set_frame_list(self, frm_list): + assert isinstance(frm_list, list) + self.clear_frame_list() + self.add_frame_list(frm_list) + return + + def clear_frame_list(self): + key = 'frame_list' + self._data[key] = [] + return + + ########################################## + + def compile(self, mkr_list, attr_list, prog_fn=None): + """ + Compiles data given into flags for a single run of 'mmSolver'. + + :param self: The solver to compile + :type self: Solver + + :param mkr_list: Markers to measure + :type mkr_list: list of Marker + + :param attr_list: Attributes to solve for + :type attr_list: list of Attribute + + :param prog_fn: Progress Function, with signature f(int) + :type prog_fn: function + + :return: List of SolverActions to be performed one after the other. + :rtype: [SolverAction, ..] + """ + assert isinstance(self, solverbase.SolverBase) + assert isinstance(mkr_list, list) + assert isinstance(attr_list, list) + assert self.get_frame_list_length() > 0 + + func = maya.cmds.mmSolver + args = [] + kwargs = dict() + kwargs['camera'] = [] + kwargs['marker'] = [] + kwargs['attr'] = [] + kwargs['frame'] = [] + + # Get Markers and Cameras + markers, cameras = _compile_markersAndCameras(mkr_list) + if len(markers) == 0 and len(cameras) == 0: + LOG.warning('No Markers or Cameras found!') + return None + elif len(markers) == 0: + LOG.warning('No Markers found!') + return None + elif len(cameras) == 0: + LOG.warning('No Cameras found!') + return None + + # Get Attributes + use_animated = self.get_attributes_use_animated() + use_static = self.get_attributes_use_static() + attrs = _compile_attributes(attr_list, use_animated, use_static) + if len(attrs) == 0: + LOG.warning('No Attributes found!') + return None + + # Get Frames + frm_list = self.get_frame_list() + frame_use_tags = self.get_frames_use_tags() + frames = _compile_frames(frm_list, frame_use_tags) + if len(frames) == 0: + LOG.warning('No Frames found!') + return None + + kwargs['marker'] = markers + kwargs['camera'] = cameras + kwargs['attr'] = attrs + kwargs['frame'] = frames + + solver_type = self.get_solver_type() + if solver_type is not None: + kwargs['solverType'] = solver_type + + iterations = self.get_max_iterations() + if iterations is not None: + kwargs['iterations'] = iterations + + verbose = self.get_verbose() + if verbose is not None: + kwargs['verbose'] = verbose + + delta_factor = self.get_delta_factor() + if delta_factor is not None: + kwargs['delta'] = delta_factor + + auto_diff_type = self.get_auto_diff_type() + if auto_diff_type is not None: + kwargs['autoDiffType'] = auto_diff_type + + tau_factor = self.get_tau_factor() + if tau_factor is not None: + kwargs['tauFactor'] = tau_factor + + gradient_error_factor = self.get_gradient_error_factor() + if gradient_error_factor is not None: + kwargs['epsilon1'] = gradient_error_factor + + parameter_error_factor = self.get_parameter_error_factor() + if parameter_error_factor is not None: + kwargs['epsilon2'] = parameter_error_factor + + error_factor = self.get_error_factor() + if error_factor is not None: + kwargs['epsilon3'] = error_factor + + # TODO: Add 'robustLossType' flag. + # TODO: Add 'robustLossScale' flag. + # TODO: Add 'autoParamScaling' flag. + # TODO: Add 'debugFile' flag. + # TODO: Add 'printStatistics' flag. + + # # Add a debug file flag to the mmSolver command, only + # # triggered during debug mode. + # # TODO: Wrap this in another function. + # if logging.DEBUG >= LOG.getEffectiveLevel(): + # debug_file = maya.cmds.file(query=True, sceneName=True) + # debug_file = os.path.basename(debug_file) + # debug_file, ext = os.path.splitext(debug_file) + # debug_file_path = os.path.join( + # os.path.expandvars('${TEMP}'), + # debug_file + '_' + str(i).zfill(6) + '.log' + # ) + # if len(debug_file) > 0 and debug_file_path is not None: + # kwargs['debugFile'] = debug_file_path + + action = api_action.Action( + func=func, + args=args, + kwargs=kwargs + ) + # msg = 'kwargs:\n' + pprint.pformat(kwargs) + # LOG.debug(msg) + return [action] + + +Solver = SolverStep diff --git a/python/mmSolver/api.py b/python/mmSolver/api.py index c9d8d1852..30b744358 100644 --- a/python/mmSolver/api.py +++ b/python/mmSolver/api.py @@ -35,13 +35,28 @@ from mmSolver._api.markergroup import MarkerGroup from mmSolver._api.attribute import Attribute from mmSolver._api.collection import ( - createExecuteOptions, - ExecuteOptions, Collection, update_deviation_on_collection ) +from mmSolver._api.execute import ( + createExecuteOptions, + ExecuteOptions, + execute, +) from mmSolver._api.frame import Frame -from mmSolver._api.solver import Solver +from mmSolver._api.action import ( + Action, +) +from mmSolver._api.solverbase import ( + SolverBase, +) +from mmSolver._api.solverstep import ( + Solver, + SolverStep, +) +from mmSolver._api.solverstandard import ( + SolverStandard, +) from mmSolver._api.collectionutils import ( is_single_frame, disconnect_animcurves, @@ -142,9 +157,14 @@ 'Marker', 'MarkerGroup', 'Attribute', + 'ExecuteOptions', 'Collection', 'Frame', - 'Solver', + 'Solver', # Backwards compatibility + 'Action', + 'SolverBase', + 'SolverStandard', + 'SolverStep', 'SolveResult', # Constants @@ -184,6 +204,10 @@ # Collection 'update_deviation_on_collection', + # Execute + 'createExecuteOptions', + 'execute', + # Marker Utils 'calculate_marker_deviation', 'get_markers_start_end_frames', diff --git a/tests/test/test_api/test_collection.py b/tests/test/test_api/test_collection.py index ee2830360..7d102cd94 100644 --- a/tests/test/test_api/test_collection.py +++ b/tests/test/test_api/test_collection.py @@ -30,7 +30,7 @@ import test.test_api.apiutils as test_api_utils import mmSolver._api.utils as api_utils -import mmSolver._api.solver as solver +import mmSolver._api.solverstep as solver import mmSolver._api.frame as frame import mmSolver._api.camera as camera import mmSolver._api.marker as marker diff --git a/tests/test/test_api/test_solver.py b/tests/test/test_api/test_solver.py index d5623952f..3a21fb751 100644 --- a/tests/test/test_api/test_solver.py +++ b/tests/test/test_api/test_solver.py @@ -26,7 +26,7 @@ import maya.cmds import test.test_api.apiutils as test_api_utils -import mmSolver._api.solver as solver +import mmSolver._api.solverstep as solver import mmSolver._api.constant as const