Flesh out command stack execution.
authorW. Trevor King <wking@drexel.edu>
Fri, 20 Aug 2010 01:37:44 +0000 (21:37 -0400)
committerW. Trevor King <wking@drexel.edu>
Fri, 20 Aug 2010 01:37:44 +0000 (21:37 -0400)
doc/tutorial.txt
hooke/command.py
hooke/command_stack.py
hooke/plugin/command_stack.py
hooke/plugin/curve.py
hooke/plugin/note.py
hooke/plugin/playlist.py
test/apply_command_stack_to_playlist.py [moved from test/apply_command_stack.py with 51% similarity]
test/command_stack.py
test/multiple_curve_analysis.py [new file with mode: 0644]
test/tutorial.py

index 4ae616b..a2ff0cc 100644 (file)
@@ -327,23 +327,44 @@ capturing a command stack (e.g. to test a complicated command before
 continuing), you can continue adding to the same stack with
 ``restart_command_capture.``
 
+To execute a command stack, run::
+
+    hooke> execute_command_stack
+
+To execute a command stack on every curve in a playlist, run::
+
+    hooke> apply_command_stack_to_playlist
+
+If you decide that there are commands in a curve's stack that you
+don't want, you can clear the stack with::
+
+    hooke> clear_curve_command_stack
+
 You can also save command stacks to disk (and reload them later,
-potentially in a different Hooke session).
+potentially in a different Hooke session).::
 
     hooke> save_command_stack --output my_stack
     hooke> load_command_stack --input my_stack
 
-Multiple curve fitting and measuring
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. todo:: Update multiple curve fitting tutorial section.
-
-You can cycle through all your current playlist obtaining WLC fit, FJC
-fit, rupture force and slope (loading rate) information from each
-curve using the ``multifit`` command.  The collected data can be saved
-in a text file for further analysis in your favourite spreadsheet or
-statistical program.  If you want to check your parameters on the
-current curve before fitting all the files in your playlist, use
-``multifit justone``.  See the ``multifit`` help for more options.
+Multiple curve analysis
+~~~~~~~~~~~~~~~~~~~~~~~
+
+You can analyze multiple curves by combining `Worm like chain and
+freely jointed chain fitting`_ and `Command stacks`_.::
+
+    hooke> start_command_capture
+    hooke> zero_surface_contact_point --block retract
+    hooke> flat_filter_peaks --block retract --min_points 1
+    hooke> zero_surface_contact_point --block retract
+    ...        --ignore_after_last_peak_info_name 'flat filter peaks'
+    hooke> convert_distance_to_force --block retract
+    ...        --deflection_column 'surface deflection (m)'
+    hooke> remove_cantilever_from_extension --block retract
+    hooke> flat_peaks_to_polymer_peaks --block retract
+    hooke> polymer_fit_peaks --block retract
+    hooke> export_block --block retract --output myblock.dat
+    hooke> stop_command_capture
+    hooke> apply_command_stack_to_playlist
 
 Fast curve reviewing and saving
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
index c694bff..d88155c 100644 (file)
@@ -109,6 +109,8 @@ class Command (object):
         self.arguments = [
             Argument(name='help', type='bool', default=False, count=1,
                      help='Print a help message.'),
+            Argument(name='stack', type='bool', default=True, count=1,
+                     help='Add this command to appropriate command stacks.'),
             ] + arguments
         self._help = help
         self.plugin = plugin
index c116473..17a633b 100644 (file)
@@ -86,7 +86,7 @@ class CommandStack (list):
     >>> print [repr(cm) for cm in c]
     []
     """
-    def execute(self, hooke):
+    def execute(self, hooke, stack=False):
         """Execute a stack of commands.
 
         See Also
@@ -96,7 +96,7 @@ class CommandStack (list):
         for command_message in self:
             if self.filter(hooke, command_message) == True:
                 self.execute_command(
-                    hooke=hooke, command_message=command_message)
+                    hooke=hooke, command_message=command_message, stack=stack)
 
     def filter(self, hooke, command_message):
         """Return `True` to execute `command_message`, `False` otherwise.
@@ -105,9 +105,11 @@ class CommandStack (list):
         """
         return True
 
-    def execute_command(self, hooke, command_message):
+    def execute_command(self, hooke, command_message, stack=False):
+        arguments = dict(command_message.arguments)
+        arguments['stack'] = stack
         hooke.run_command(command=command_message.command,
-                          arguments=command_message.arguments)
+                          arguments=arguments)
 
     def clear(self):
         while len(self) > 0:
index 3b81c24..8a3ecb8 100644 (file)
@@ -39,6 +39,11 @@ from . import Builtin
 class CommandStackCommand (Command):
     """Subclass to avoid pushing control commands to the stack.
     """
+    def __init__(self, *args, **kwargs):
+        super(CommandStackCommand, self).__init__(*args, **kwargs)
+        stack = [a for a in self.arguments if a.name == 'stack'][0]
+        stack.default = False
+
     def _set_state(self, state):
         try:
             self.plugin.set_state(state)
@@ -102,7 +107,7 @@ class CommandStackPlugin (Builtin):
             StartCaptureCommand(self), StopCaptureCommand(self),
            ReStartCaptureCommand(self),
             PopCommand(self), GetCommand(self), GetStateCommand(self),
-           SaveCommand(self), LoadCommand(self),
+           SaveCommand(self), LoadCommand(self), ExecuteCommand(self),
            ]
         self._settings = [
             Setting(section=self.setting_section, help=self.__doc__),
@@ -261,3 +266,32 @@ File name for the input command stack.
             params['input'] = os.path.join(
                 self.plugin.config['path'], params['input'])
         return params
+
+
+class ExecuteCommand (Command):
+    """Execute a :class:`~hooke.command_stack.CommandStack`.
+    """
+    def __init__(self, plugin):
+        super(ExecuteCommand, self).__init__(
+            name='execute command stack',
+            arguments=[
+                Argument(name='commands', type='command stack',
+                         help="""
+Command stack to apply to each curve.  Defaults to the plugin's
+current stack.
+""".strip()),
+                ],
+            help=self.__doc__, plugin=plugin)
+        stack = [a for a in self.arguments if a.name == 'stack'][0]
+        stack.default = False
+
+    def _run(self, hooke, inqueue, outqueue, params):
+        params = self.__setup_params(hooke=hooke, params=params)
+        if len(params['commands']) == 0:
+            return
+        params['commands'].execute(hooke=hooke, stack=params['stack'])
+
+    def __setup_params(self, hooke, params):
+        if params['commands'] == None:
+            params['commands'] = self.plugin.command_stack
+        return params
index e6d5ce3..d73a67f 100644 (file)
@@ -29,6 +29,7 @@ import copy
 import numpy
 
 from ..command import Command, Argument, Failure
+from ..command_stack import CommandStack
 from ..curve import Data
 from ..engine import CommandMessage
 from ..util.calculus import derivative
@@ -123,14 +124,15 @@ class CurveCommand (Command):
         (e.g. :class:`GetCommand`) to avoid adding themselves to the
         stack entirely.
         """
-        curve = self._curve(hooke=None, params=params)
-        if (len(curve.command_stack) > 0
-            and curve.command_stack[-1].command == self.name
-            and curve.command_stack[-1].arguments == params):
-            pass  # no need to place duplicate calls on the stack.
-        else:
-            curve.command_stack.append(CommandMessage(
-                    self.name, params))
+        if params['stack'] == True:
+            curve = self._curve(hooke=None, params=params)
+            if (len(curve.command_stack) > 0
+                and curve.command_stack[-1].command == self.name
+                and curve.command_stack[-1].arguments == params):
+                pass  # no need to place duplicate calls on the stack.
+            else:
+                curve.command_stack.append(CommandMessage(
+                        self.name, params))
 
 
 class BlockCommand (CurveCommand):
@@ -258,7 +260,8 @@ class CurvePlugin (Builtin):
         self._commands = [
             GetCommand(self), InfoCommand(self), DeltaCommand(self),
             ExportCommand(self), DifferenceCommand(self),
-            DerivativeCommand(self), PowerSpectrumCommand(self)]
+            DerivativeCommand(self), PowerSpectrumCommand(self),
+            ClearStackCommand(self)]
 
 
 # Define commands
@@ -621,6 +624,19 @@ Otherwise, the chunks are end-to-end, and not overlapping.
         return params
 
 
+class ClearStackCommand (CurveCommand):
+    """Empty a curve's command stack.
+    """
+    def __init__(self, plugin):
+        super(ClearStackCommand, self).__init__(
+            name='clear curve command stack',
+            help=self.__doc__, plugin=plugin)
+
+    def _run(self, hooke, inqueue, outqueue, params):
+        curve = self._curve(hooke, params)
+        curve.command_stack = CommandStack()
+
+
 class OldCruft (object):
 
     def do_forcebase(self,args):
index b982075..0d0bada 100644 (file)
@@ -77,7 +77,7 @@ Target object for the note.  Defaults to the current curve.
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
-        outqueue.put(params['target'].info['note'])
+        outqueue.put(params['target'].info.get('note', None))
 
 
 class NoteFilterCommand (FilterCommand):
index 53fc28a..fbd30fa 100644 (file)
@@ -26,8 +26,9 @@ import logging
 import os.path
 
 from ..command import Command, Argument, Failure
-from ..playlist import FilePlaylist
 from ..curve import NotRecognized
+from ..playlist import FilePlaylist
+from ..util.itertools import reverse_enumerate
 from . import Builtin
 
 
@@ -39,7 +40,7 @@ class PlaylistPlugin (Builtin):
             GetCommand(self), IndexCommand(self), CurveListCommand(self),
             SaveCommand(self), LoadCommand(self),
             AddCommand(self), AddGlobCommand(self),
-            RemoveCommand(self), ApplyCommandStack(self),
+            RemoveCommand(self), ApplyCommand(self),
             FilterCommand(self),
             ]
 
@@ -329,19 +330,20 @@ Index of target curve.
         self._playlist(hooke, params).jump(params.index())
 
 
-class ApplyCommandStack (PlaylistCommand):
+class ApplyCommand (PlaylistCommand):
     """Apply a :class:`~hooke.command_stack.CommandStack` to each
     curve in a playlist.
 
     TODO: discuss `evaluate`.
     """
     def __init__(self, plugin):
-        super(ApplyCommandStack, self).__init__(
-            name='apply command stack',
+        super(ApplyCommand, self).__init__(
+            name='apply command stack to playlist',
             arguments=[
-                Argument(name='commands', type='command stack', optional=False,
+                Argument(name='commands', type='command stack',
                          help="""
-Command stack to apply to each curve.
+Command stack to apply to each curve.  Defaults to the `command_stack`
+plugin's current stack.
 """.strip()),
                 Argument(name='evaluate', type='bool', default=False,
                          help="""
@@ -351,18 +353,27 @@ Evaluate the applied command stack immediately.
             help=self.__doc__, plugin=plugin)
 
     def _run(self, hooke, inqueue, outqueue, params):
-        if len(params['commands']) == 0:
-            return
+        params = self.__setup_params(hooke=hooke, params=params)
         p = self._playlist(hooke, params)
         if params['evaluate'] == True:
+            exec_cmd = hooke.command_by_name['execute command stack']
             for curve in p.items():
-                for command in params['commands']:
-                    curve.command_stack.execute_command(hooke, command)
-                    curve.command_stack.append(command)
+                hooke.run_command(exec_cmd.name,
+                                  {'commands':params['commands'],
+                                   'stack':True})
         else:
             for curve in p:
-                curve.command_stack.extend(params['commands'])
-                curve.unload()  # force command stack execution on next access.
+                for command in params['commands']:
+                    curve.command_stack.append(command)
+                curve.set_hooke(hooke)
+                curve.unload()
+
+    def __setup_params(self, hooke, params):
+        if params['commands'] == None:
+            cstack_plugin = [p for p in hooke.plugins
+                             if p.name == 'command_stack'][0]
+            params['commands'] = cstack_plugin.command_stack
+        return params
 
 
 class FilterCommand (PlaylistAddingCommand, PlaylistCommand):
similarity index 51%
rename from test/apply_command_stack.py
rename to test/apply_command_stack_to_playlist.py
index e6a659e..483d032 100644 (file)
@@ -42,40 +42,97 @@ engine message from load playlist (<class 'hooke.command.Success'>):
 ...         CommandMessage('zero surface contact point'),
 ...         ])
 
-Test `apply command stack`.
+Test `apply command stack to playlist`.
 
->>> h.run_command('apply command stack',
+>>> h.run_command('apply command stack to playlist',
 ...     {'commands': stack, 'evaluate': True})  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF
-engine running internal <CommandMessage apply command stack
+engine running internal <CommandMessage apply command stack to playlist
   {commands: [<CommandMessage get curve>,
               <CommandMessage zero surface contact point>],
    evaluate: True}>
 loading curve 20071120a_i27_t33.100 with driver ...
-engine running internal <CommandMessage get curve>
+engine running internal <CommandMessage execute command stack {...}>
+engine running internal <CommandMessage get curve {stack: True}>
 engine message from get curve (<class 'hooke.curve.Curve'>): <Curve 20071120a_i27_t33.100>
 engine message from get curve (<class 'hooke.command.Success'>): 
-engine running internal <CommandMessage zero surface contact point>
+engine running internal <CommandMessage zero surface contact point {...}>
 engine message from zero surface contact point (<type 'dict'>): {...}
 engine message from zero surface contact point (<class 'hooke.command.Success'>): 
+engine message from execute command stack (<class 'hooke.command.Success'>): 
 loading curve 20071120a_i27_t33.101 with driver ...
-engine running internal <CommandMessage get curve>
+engine running internal <CommandMessage execute command stack {...}>
+engine running internal <CommandMessage get curve {stack: True}>
 engine message from get curve (<class 'hooke.curve.Curve'>): <Curve 20071120a_i27_t33.101>
 engine message from get curve (<class 'hooke.command.Success'>): 
-engine running internal <CommandMessage zero surface contact point>
+engine running internal <CommandMessage zero surface contact point {...}>
 engine message from zero surface contact point (<type 'dict'>): {...}
 engine message from zero surface contact point (<class 'hooke.command.Success'>): 
+engine message from execute command stack (<class 'hooke.command.Success'>): 
 loading curve 20071120a_i27_t33.102 with driver ...
+
 ...
 loading curve 20071120a_i27_t33.199 with driver ...
-engine running internal <CommandMessage get curve>
+engine running internal <CommandMessage execute command stack {...}>
+engine running internal <CommandMessage get curve {stack: True}>
 engine message from get curve (<class 'hooke.curve.Curve'>): <Curve 20071120a_i27_t33.199>
 engine message from get curve (<class 'hooke.command.Success'>): 
-engine running internal <CommandMessage zero surface contact point>
+engine running internal <CommandMessage zero surface contact point {...}>
 engine message from zero surface contact point (<type 'dict'>): {...}
 engine message from zero surface contact point (<class 'hooke.command.Success'>): 
+engine message from execute command stack (<class 'hooke.command.Success'>): 
 loading curve 0x06130001 with driver ...
 unloading curve 20071120a_i27_t33.100
-engine running internal <CommandMessage get curve>
+engine running internal <CommandMessage execute command stack
+  {commands: [<CommandMessage get curve>,
+              <CommandMessage zero surface contact point>],
+   curve: ...}>
+engine running internal <CommandMessage get curve {stack: True}>
+...
+engine message from apply command stack to playlist (<class 'hooke.command.Success'>): 
+>>> curve = h.playlists.current().current(
+...     )  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+loading curve 20071120a_i27_t33.100 with driver ...
+engine running internal <CommandMessage zero surface contact point
+  {block: None, curve: <Curve 20071120a_i27_t33.100>, ...}>
+engine message from zero surface contact point (<type 'dict'>): {...}
+engine message from zero surface contact point (<class 'hooke.command.Success'>): 
+unloading curve 20071120a_i27_t33.102
+>>> curve
+<Curve 20071120a_i27_t33.100>
+>>> curve.command_stack  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+[<CommandMessage zero surface contact point
+    {block: None, curve: <Curve 20071120a_i27_t33.100>, ...}>]
+
+Test `apply command stack to playlist` without evaluating.
+
+>>> stack = CommandStack([
+...         CommandMessage('flat_filter_peaks --block retract --min_points 1'),
+...         ])
+>>> h.run_command('apply command stack to playlist',
+...     {'commands': stack, 'evaluate': False}
+...     )  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF
+engine running internal
+  <CommandMessage apply command stack to playlist {...}>
+unloading curve 20071120a_i27_t33.100
+unloading curve 20071120a_i27_t33.101
+unloading curve 20071120a_i27_t33.102
 ...
-engine message from apply command stack (<class 'hooke.command.Success'>): 
+unloading curve 20071120a_i27_t33.199
+unloading curve 0x06130001
+unloading curve 0x07200000
+engine message from apply command stack to playlist
+  (<class 'hooke.command.Success'>): 
+>>> for c in curve.command_stack:
+...     print c  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF
+<CommandMessage zero surface contact point
+  {..., curve: <Curve 20071120a_i27_t33.100>...}>
+<CommandMessage flat_filter_peaks --block retract --min_points 1>
+
+Test `clear curve command stack`.
+
+>>> h.run_command('clear curve command stack', arguments={})
+engine running internal <CommandMessage clear curve command stack>
+engine message from clear curve command stack (<class 'hooke.command.Success'>): 
+>>> curve.command_stack
+[]
 """
index 48ef4d1..a706d67 100644 (file)
@@ -17,6 +17,8 @@
 # <http://www.gnu.org/licenses/>.
 
 """
+>>> import logging
+>>> import sys
 >>> from hooke.hooke import Hooke, HookeRunner
 >>> h = Hooke()
 >>> r = HookeRunner()
@@ -158,4 +160,45 @@ Success
 []
 Success
 <BLANKLINE>
+
+Building command stacks is fun, but its useless if you can't execute
+them.  First, lets repopulate the in-memory stack.
+
+>>> h = r.run_lines(h, ['start_command_capture',
+...                     'debug --attribute config',
+...                     'version',
+...                     'stop_command_capture']
+...     )  # doctest: +REPORT_UDIFF
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+
+Setup logging so we can check command output in the doctest.
+
+>>> log = logging.getLogger('hooke')
+>>> stdout_handler = logging.StreamHandler(sys.stdout)
+>>> log.addHandler(stdout_handler)
+
+Execute the stack.  We use `h.run_command` because `sys.stdout` is
+replaced by a `doctest._SpoofOut`, and doctest has no way to collect
+those results from `run_lines`'s engine subprocess.
+
+>>> h.run_command('execute command stack', arguments={}
+...     )  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF
+engine running internal <CommandMessage execute command stack>
+engine running internal <CommandMessage debug {attribute: config, ...}>
+engine message from debug (<type 'instance'>):
+ <hooke.config.HookeConfigParser instance at 0x...>
+engine message from debug (<class 'hooke.command.Success'>):
+engine running internal <CommandMessage version {stack: False}>
+engine message from version (<type 'str'>): Hooke 1.0.0.alpha (Ninken)
+----
+...
+engine message from version (<class 'hooke.command.Success'>):
+engine message from execute command stack (<class 'hooke.command.Success'>):
 """
diff --git a/test/multiple_curve_analysis.py b/test/multiple_curve_analysis.py
new file mode 100644 (file)
index 0000000..d7c82c6
--- /dev/null
@@ -0,0 +1,116 @@
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# This file is part of Hooke.
+#
+# Hooke 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.
+#
+# Hooke 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 Hooke.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+"""
+>>> from hooke.hooke import Hooke, HookeRunner
+>>> h = Hooke()
+>>> r = HookeRunner()
+
+Setup a playlist to act on.
+
+>>> h = r.run_lines(h, ['load_playlist test/data/vclamp_picoforce/playlist'])
+<FilePlaylist playlist.hkp>
+Success
+<BLANKLINE>
+
+Build the command stack.
+
+>>> h = r.run_lines(h, [
+...         'start_command_capture',
+...         'zero_surface_contact_point --block retract',
+...         'flat_filter_peaks --block retract --min_points 1',
+...         'zero_surface_contact_point --block retract --ignore_after_last_peak_info_name "flat filter peaks"',
+...         'convert_distance_to_force --block retract --deflection_column "surface deflection (m)"',
+...         'remove_cantilever_from_extension --block retract',
+...         'flat_peaks_to_polymer_peaks --block retract',
+...         'polymer_fit_peaks --block retract',
+...         'stop_command_capture',
+...         ])  # doctest: +REPORT_UDIFF
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+Success
+<BLANKLINE>
+
+Apply the command stack.
+
+>>> h = r.run_lines(h, ['apply_command_stack_to_playlist'])
+Success
+<BLANKLINE>
+
+Verify successful application.
+
+>>> curve = h.playlists.current().current()
+>>> curve
+<Curve 20071120a_i27_t33.100>
+>>> for c in curve.command_stack:
+...     print c  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF
+<CommandMessage zero surface contact point {block: retract}>
+<CommandMessage flat filter peaks {block: retract, min points: 1}>
+<CommandMessage zero surface contact point {block: retract, ignore after last peak info name: flat filter peaks}>
+<CommandMessage convert distance to force {block: retract, deflection column: surface deflection (m)}>
+<CommandMessage remove cantilever from extension {block: retract}>
+<CommandMessage flat peaks to polymer peaks {block: retract}>
+<CommandMessage polymer fit peaks {block: retract}>
+>>> for c in curve.data[-1].info['columns']:
+...     print c  # doctest: +REPORT_UDIFF
+z piezo (m)
+deflection (m)
+surface distance (m)
+surface deflection (m)
+flat filter peaks (m)
+deflection (N)
+cantilever adjusted extension (m)
+polymer peak 0 (N)
+>>> h.playlists.current().next()
+>>> curve = h.playlists.current().current()
+>>> curve
+<Curve 20071120a_i27_t33.101>
+>>> for c in curve.command_stack:
+...     print c  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE, +REPORT_UDIFF
+<CommandMessage zero surface contact point {block: retract}>
+<CommandMessage flat filter peaks {block: retract, min points: 1}>
+<CommandMessage zero surface contact point {block: retract, ignore after last peak info name: flat filter peaks}>
+<CommandMessage convert distance to force {block: retract, deflection column: surface deflection (m)}>
+<CommandMessage remove cantilever from extension {block: retract}>
+<CommandMessage flat peaks to polymer peaks {block: retract}>
+<CommandMessage polymer fit peaks {block: retract}>
+>>> for c in curve.data[-1].info['columns']:
+...     print c  # doctest: +REPORT_UDIFF
+z piezo (m)
+deflection (m)
+surface distance (m)
+surface deflection (m)
+flat filter peaks (m)
+deflection (N)
+cantilever adjusted extension (m)
+polymer peak 0 (N)
+"""
index 6a6ea6c..7c115d6 100644 (file)
@@ -38,6 +38,7 @@ Command: load_playlist
 Arguments:
 <BLANKLINE>
 help BOOL (bool) Print a help message.
+stack BOOL (bool) Add this command to appropriate command stacks.
 output_playlist STRING (string) Name of the new playlist (defaults to
     an auto-generated name).
 input FILE (file) File name for the input playlist.
@@ -69,6 +70,7 @@ hooke
 Success
 <BLANKLINE>
 >>> h = r.run_lines(h, ['new_playlist --output_playlist mylist'])
+<FilePlaylist mylist>
 Success
 <BLANKLINE>
 >>> h = r.run_lines(h, ['jump_to_playlist -- -1'])
@@ -233,9 +235,13 @@ See :file:`delta.py`.
 
 *Command stacks*
 
-See :file:`command_stack.py`and :file:`command_stack_save_load.py`.
+See :file:`command_stack.py`,
+:file:`apply_command_stack_to_playlist.py`, and
+:file:`command_stack_save_load.py`.
+
+*Multiple curve analysis*
 
-*Multiple curve fitting and measuring*
+See :file:`multiple_curve_analysis`.
 
 *Fast curve reviewing and saving*