Source code for diffpy.morph.morphapp

#!/usr/bin/env python
##############################################################################
#
# diffpy.morph      by DANSE Diffraction group
#                   Simon J. L. Billinge
#                   (c) 2010 Trustees of the Columbia University
#                   in the City of New York.  All rights reserved.
#
# File coded by:    Chris Farrow
#
# See AUTHORS.txt for a list of people who contributed.
# See LICENSE.txt for license information.
#
##############################################################################

from __future__ import print_function

import sys
from pathlib import Path

import diffpy.morph.morph_helpers as helpers
import diffpy.morph.morph_io as io
import diffpy.morph.morphs as morphs
import diffpy.morph.pdfplot as pdfplot
import diffpy.morph.refine as refine
import diffpy.morph.tools as tools
from diffpy.morph import __save_morph_as__
from diffpy.morph.version import __version__


[docs] def create_option_parser(): import optparse prog_short = Path( sys.argv[0] ).name # Program name, compatible w/ all OS paths class CustomParser(optparse.OptionParser): def __init__(self, *args, **kwargs): super(CustomParser, self).__init__(*args, **kwargs) def custom_error(self, msg): """custom_error(msg : string) Print a message incorporating 'msg' to stderr and exit. Does not print usage. """ self.exit(2, "%s: error: %s\n" % (self.get_prog_name(), msg)) parser = CustomParser( usage="\n".join( [ "%prog [options] FILE1 FILE2", "Manipulate and compare PDFs.", "Use --help for help.", ] ), epilog="\n".join( [ "Please report bugs to diffpy-users@googlegroups.com.", ( "For more information, see the diffpy.morph website at " "https://www.diffpy.org/diffpy.morph." ), ] ), ) parser.add_option( "-V", "--version", action="version", help="Show program version and exit.", ) parser.version = __version__ parser.add_option( "-s", "--save", metavar="NAME", dest="slocation", help=( "Save the manipulated PDF to a file named NAME. " "Use '-' for stdout.\n" "When --multiple-<targets/morphs> is enabled, " "save each manipulated PDF as a file in a directory named NAME;\n" "you can specify names for each saved PDF file using " "--save-names-file." ), ) parser.add_option( "-v", "--verbose", dest="verbose", action="store_true", help="Print additional header details to saved files.", ) parser.add_option( "--rmin", type="float", help="Minimum r-value to use for PDF comparisons.", ) parser.add_option( "--rmax", type="float", help="Maximum r-value to use for PDF comparisons.", ) parser.add_option( "--pearson", action="store_true", dest="pearson", help=( "Maximize agreement in the Pearson function. " "Note that this is insensitive to scale." ), ) parser.add_option( "--addpearson", action="store_true", dest="addpearson", help="""Maximize agreement in the Pearson function as well as minimizing the residual.""", ) # Manipulations group = optparse.OptionGroup( parser, "Manipulations", ( "These options select the manipulations that are to be applied to " "the PDF from FILE1. " "The passed values will be refined unless specifically " "excluded with the -a or -x options. " "If no option is specified, the PDFs from FILE1 and FILE2 will " "be plotted without any manipulations." ), ) parser.add_option_group(group) group.add_option( "-a", "--apply", action="store_false", dest="refine", help="Apply manipulations but do not refine.", ) group.add_option( "-x", "--exclude", action="append", dest="exclude", metavar="MANIP", help="""Exclude a manipulation from refinement by name. This can appear multiple times.""", ) group.add_option( "--scale", type="float", metavar="SCALE", help="Apply scale factor SCALE.", ) group.add_option( "--stretch", type="float", metavar="STRETCH", help="Stretch PDF by a fraction STRETCH.", ) group.add_option( "--smear", type="float", metavar="SMEAR", help="Smear peaks with a Gaussian of width SMEAR.", ) group.add_option( "--slope", type="float", dest="baselineslope", help="""Slope of the baseline. This is used when applying the smear factor. It will be estimated if not provided.""", ) group.add_option( "--hshift", type="float", metavar="HSHIFT", help="Shift the PDF horizontally by HSHIFT to the right.", ) group.add_option( "--vshift", type="float", metavar="VSHIFT", help="Shift the PDF vertically by VSHIFT upward.", ) group.add_option( "--qdamp", type="float", metavar="QDAMP", help="Dampen PDF by a factor QDAMP.", ) group.add_option( "--radius", type="float", metavar="RADIUS", help=( "Apply characteristic function of sphere with radius " "RADIUS. If PRADIUS is also specified, instead apply " "characteristic function of spheroid with equatorial " "radius RADIUS and polar radius PRADIUS." ), ) group.add_option( "--pradius", type="float", metavar="PRADIUS", help=( "Apply characteristic function of spheroid with " "equatorial radius RADIUS and polar radius PRADIUS. If only " "PRADIUS is specified, instead apply characteristic function of " "sphere with radius PRADIUS." ), ) group.add_option( "--iradius", type="float", metavar="IRADIUS", help=( "Apply inverse characteristic function of sphere with radius " "IRADIUS. If IPRADIUS is also specified, instead apply inverse " "characteristic function of spheroid with equatorial radius " "IRADIUS and polar radius IPRADIUS." ), ) group.add_option( "--ipradius", type="float", metavar="IPRADIUS", help=( "Apply inverse characteristic function of spheroid with " "equatorial radius IRADIUS and polar radius IPRADIUS. If only " "IPRADIUS is specified, instead apply inverse characteristic " "function of sphere with radius IPRADIUS." ), ) # Plot Options group = optparse.OptionGroup( parser, "Plot Options", ( "These options control plotting. The manipulated and target PDFs " "will be plotted against each other with a difference curve " "below. " "When --multiple-<targets/morphs> is enabled, the value of a " "parameter (specified by --plot-parameter) will be plotted " "instead." ), ) parser.add_option_group(group) group.add_option( "-n", "--noplot", action="store_false", dest="plot", help="""Do not show a plot.""", ) group.add_option( "--mlabel", metavar="MLABEL", dest="mlabel", help=( "Set label for morphed data to MLABEL on plot. " "Default label is FILE1." ), ) group.add_option( "--tlabel", metavar="TLABEL", dest="tlabel", help=( "Set label for target data to TLABEL on plot. " "Default label is FILE2." ), ) group.add_option( "--pmin", type="float", help="Minimum r-value to plot. Defaults to RMIN.", ) group.add_option( "--pmax", type="float", help="Maximum r-value to plot. Defaults to RMAX.", ) group.add_option( "--maglim", type="float", help="Magnify plot curves beyond r=MAGLIM by MAG.", ) group.add_option( "--mag", type="float", help="Magnify plot curves beyond r=MAGLIM by MAG.", ) group.add_option( "--lwidth", type="float", help="Line thickness of plotted curves." ) # Multiple morph options group = optparse.OptionGroup( parser, "Multiple Morphs", ( "This program can morph a PDF against multiple targets in one " "command. See -s and Plot Options for how saving and plotting " "functionality changes when performing multiple morphs." ), ) parser.add_option_group(group) group.add_option( "--multiple-morphs", dest="multiple_morphs", action="store_true", help=( f"Changes usage to '{prog_short} [options] FILE DIRECTORY'. " f"FILE will be morphed with each file in DIRECTORY as target. " f"Files in DIRECTORY are sorted by alphabetical order unless a " f"field is specified by --sort-by." ), ) group.add_option( "--multiple-targets", dest="multiple_targets", action="store_true", help=( f"Changes usage to '{prog_short} [options] DIRECTORY FILE'. " f"Each file in DIRECTORY will be morphed with FILE as target. " f"Files in DIRECTORY are sorted by alphabetical order unless a " f"field is specified by --sort-by." ), ) group.add_option( "--sort-by", metavar="FIELD", dest="field", help=( "Used with --multiple-<targets/morphs> to sort files in DIRECTORY " "by FIELD. " "If the FIELD being used has a numerical value, sort from lowest " "to highest. Otherwise, sort in ASCII sort order. FIELD must be " "included in the header of all the PDF files." ), ) group.add_option( "--reverse", dest="reverse", action="store_true", help="""Sort from highest to lowest instead.""", ) group.add_option( "--serial-file", metavar="SERIALFILE", dest="serfile", help="""Look for FIELD in a serial file instead. Must specify name of serial file SERIALFILE.""", ) group.add_option( "--save-names-file", metavar="NAMESFILE", dest="snamesfile", help=( "Used when both -s and --multiple-<targets/morphs> are enabled. " "Specify names for each manipulated PDF when saving (see -s) " "using a serial file NAMESFILE. The format of NAMESFILE should be " "as follows: each target PDF is an entry in NAMESFILE. For each " "entry, there should be a key {__save_morph_as__} whose value " "specifies the name to save the manipulated PDF as. An example " ".json serial file is shown in the diffpy.morph manual." ), ) group.add_option( "--plot-parameter", metavar="PLOTPARAM", dest="plotparam", help=( "Used when both plotting and --multiple-<targets/morphs> are " "enabled. Choose a PLOTPARAM to plot for each morph (i.e. adding " "--plot-parameter=Pearson means the program will display a plot " "of the Pearson correlation coefficient for each morph-target pair" "). PLOTPARAM is not case sensitive, so both Pearson and pearson " "indicate the same parameter. When PLOTPARAM is not specified, Rw " "values for each morph-target pair will be plotted. PLOTPARAM " "will be displayed as the vertical axis label for the plot." ), ) # Defaults parser.set_defaults(multiple=False) parser.set_defaults(reverse=False) parser.set_defaults(plot=True) parser.set_defaults(refine=True) parser.set_defaults(pearson=False) parser.set_defaults(addpearson=False) parser.set_defaults(mag=5) parser.set_defaults(lwidth=1.5) return parser
[docs] def single_morph(parser, opts, pargs, stdout_flag=True): if len(pargs) < 2: parser.error("You must supply FILE1 and FILE2.") elif len(pargs) > 2: parser.error( "Too many arguments. Make sure you only supply FILE1 and FILE2." ) # Get the PDFs x_morph, y_morph = getPDFFromFile(pargs[0]) x_target, y_target = getPDFFromFile(pargs[1]) if y_morph is None: parser.error(f"No data table found in file: {pargs[0]}.") if y_target is None: parser.error(f"No data table found in file: {pargs[1]}.") # Get configuration values scale_in = "None" stretch_in = "None" smear_in = "None" hshift_in = "None" vshift_in = "None" config = {} config["rmin"] = opts.rmin config["rmax"] = opts.rmax config["rstep"] = None if ( opts.rmin is not None and opts.rmax is not None and opts.rmax <= opts.rmin ): e = "rmin must be less than rmax" parser.custom_error(e) # Set up the morphs chain = morphs.MorphChain(config) # Add the r-range morph, we will remove it when saving and plotting chain.append(morphs.MorphRGrid()) refpars = [] # Scale if opts.scale is not None: scale_in = opts.scale chain.append(morphs.MorphScale()) config["scale"] = scale_in refpars.append("scale") # Stretch if opts.stretch is not None: stretch_in = opts.stretch chain.append(morphs.MorphStretch()) config["stretch"] = stretch_in refpars.append("stretch") # Shift if opts.hshift is not None or opts.vshift is not None: chain.append(morphs.MorphShift()) if opts.hshift is not None: hshift_in = opts.hshift config["hshift"] = hshift_in refpars.append("hshift") if opts.vshift is not None: vshift_in = opts.vshift config["vshift"] = vshift_in refpars.append("vshift") # Smear if opts.smear is not None: smear_in = opts.smear chain.append(helpers.TransformXtalPDFtoRDF()) chain.append(morphs.MorphSmear()) chain.append(helpers.TransformXtalRDFtoPDF()) refpars.append("smear") config["smear"] = smear_in # Set baselineslope if not given config["baselineslope"] = opts.baselineslope if opts.baselineslope is None: config["baselineslope"] = -0.5 refpars.append("baselineslope") # Size radii = [opts.radius, opts.pradius] nrad = 2 - radii.count(None) if nrad == 1: radii.remove(None) config["radius"] = tools.nn_value(radii[0], "radius or pradius") chain.append(morphs.MorphSphere()) refpars.append("radius") elif nrad == 2: config["radius"] = tools.nn_value(radii[0], "radius") refpars.append("radius") config["pradius"] = tools.nn_value(radii[1], "pradius") refpars.append("pradius") chain.append(morphs.MorphSpheroid()) iradii = [opts.iradius, opts.ipradius] inrad = 2 - iradii.count(None) if inrad == 1: iradii.remove(None) config["iradius"] = tools.nn_value(iradii[0], "iradius or ipradius") chain.append(morphs.MorphISphere()) refpars.append("iradius") elif inrad == 2: config["iradius"] = tools.nn_value(iradii[0], "iradius") refpars.append("iradius") config["ipradius"] = tools.nn_value(iradii[1], "ipradius") refpars.append("ipradius") chain.append(morphs.MorphISpheroid()) # Resolution if opts.qdamp is not None: chain.append(morphs.MorphResolutionDamping()) refpars.append("qdamp") config["qdamp"] = opts.qdamp # Now remove non-refinable parameters if opts.exclude is not None: refpars = list(set(refpars) - set(opts.exclude)) # Refine or execute the morph refiner = refine.Refiner(chain, x_morph, y_morph, x_target, y_target) if opts.pearson: refiner.residual = refiner._pearson if opts.addpearson: refiner.residual = refiner._add_pearson if opts.refine and refpars: try: # This works better when we adjust scale and smear first. if "smear" in refpars: rptemp = ["smear"] if "scale" in refpars: rptemp.append("scale") refiner.refine(*rptemp) # Adjust all parameters refiner.refine(*refpars) except ValueError as e: parser.custom_error(str(e)) # Smear is not being refined, but baselineslope needs to refined to apply # smear # Note that baselineslope is only added to the refine list if smear is # applied elif "baselineslope" in refpars: try: refiner.refine( "baselineslope", baselineslope=config["baselineslope"] ) except ValueError as e: parser.custom_error(str(e)) else: chain(x_morph, y_morph, x_target, y_target) # Get Rw for the morph range rw = tools.getRw(chain) pcc = tools.get_pearson(chain) # Replace the MorphRGrid with Morph identity chain[0] = morphs.Morph() chain(x_morph, y_morph, x_target, y_target) # Input morph parameters morph_inputs = { "scale": scale_in, "stretch": stretch_in, "smear": smear_in, } morph_inputs.update({"hshift": hshift_in, "vshift": vshift_in}) # Output morph parameters morph_results = dict(config.items()) # Ensure Rw, Pearson last two outputs morph_results.update({"Rw": rw}) morph_results.update({"Pearson": pcc}) # Print summary to terminal and save morph to file if requested try: io.single_morph_output( morph_inputs, morph_results, save_file=opts.slocation, morph_file=pargs[0], xy_out=[chain.x_morph_out, chain.y_morph_out], verbose=opts.verbose, stdout_flag=stdout_flag, ) except (FileNotFoundError, RuntimeError): save_fail_message = "Unable to save to designated location." parser.custom_error(save_fail_message) if opts.plot: pairlist = [chain.xy_morph_out, chain.xy_target_out] labels = [pargs[0], pargs[1]] # Default is to use file names # If user chooses labels if opts.mlabel is not None: labels[0] = opts.mlabel if opts.tlabel is not None: labels[1] = opts.tlabel # Plot extent defaults to calculation extent pmin = opts.pmin if opts.pmin is not None else opts.rmin pmax = opts.pmax if opts.pmax is not None else opts.rmax maglim = opts.maglim mag = opts.mag l_width = opts.lwidth pdfplot.comparePDFs( pairlist, labels, rmin=pmin, rmax=pmax, maglim=maglim, mag=mag, rw=rw, l_width=l_width, ) return morph_results
[docs] def multiple_targets(parser, opts, pargs, stdout_flag=True): # Custom error messages since usage is distinct when --multiple tag is # applied if len(pargs) < 2: parser.custom_error( "You must supply FILE and DIRECTORY. " "See --multiple-targets under --help for usage." ) elif len(pargs) > 2: parser.custom_error( "Too many arguments. You must only supply a FILE and a DIRECTORY." ) # Parse paths morph_file = Path(pargs[0]) if not morph_file.is_file(): parser.custom_error( f"{morph_file} is not a file. Go to --help for usage." ) target_directory = Path(pargs[1]) if not target_directory.is_dir(): parser.custom_error( f"{target_directory} is not a directory. Go to --help for usage." ) # Get list of files from target directory target_list = list(target_directory.iterdir()) for target in target_list: if target.is_dir(): target_list.remove(target) # Do not morph morph_file against itself if it is in the same directory if morph_file in target_list: target_list.remove(morph_file) # Format field name for printing and plotting field = None if opts.field is not None: field_words = opts.field.split() field = "" for word in field_words: field += f"{word[0].upper()}{word[1:].lower()}" field_list = None # Sort files in directory by some field if field is not None: try: target_list, field_list = tools.field_sort( target_list, field, opts.reverse, opts.serfile, get_field_values=True, ) except KeyError: if opts.serfile is not None: parser.custom_error( "The requested field was not found in the metadata file." ) else: parser.custom_error( "The requested field is missing from a PDF file header." ) else: # Default is alphabetical sort target_list.sort(reverse=opts.reverse) # Disable single morph plotting plot_opt = opts.plot opts.plot = False # Set up saving save_directory = opts.slocation # User-given directory for saves save_names_file = ( opts.snamesfile ) # User-given serialfile with names for each morph save_morphs_here = None # Subdirectory for saving morphed PDFs save_names = {} # Dictionary of names to save each morph as if save_directory is not None: try: save_morphs_here = io.create_morphs_directory(save_directory) # Could not create directory or find names to save morphs as except (FileNotFoundError, RuntimeError): save_fail_message = "\nUnable to create directory" parser.custom_error(save_fail_message) try: save_names = io.get_multisave_names( target_list, save_names_file=save_names_file ) # Could not create directory or find names to save morphs as except FileNotFoundError: save_fail_message = "\nUnable to read from save names file" parser.custom_error(save_fail_message) # Morph morph_file against all other files in target_directory morph_results = {} for target_file in target_list: if target_file.is_file: # Set the save file destination to be a file within the SLOC # directory if save_directory is not None: save_as = save_names[target_file.name][__save_morph_as__] opts.slocation = Path(save_morphs_here).joinpath(save_as) # Perform a morph of morph_file against target_file pargs = [morph_file, target_file] morph_results.update( { target_file.name: single_morph( parser, opts, pargs, stdout_flag=False ), } ) target_file_names = [] for key in morph_results.keys(): target_file_names.append(key) morph_inputs = { "scale": opts.scale, "stretch": opts.stretch, "smear": opts.smear, } morph_inputs.update({"hshift": opts.hshift, "vshift": opts.vshift}) try: # Print summary of morphs to terminal and to file (if requested) io.multiple_morph_output( morph_inputs, morph_results, target_file_names, save_directory=save_directory, morph_file=morph_file, target_directory=target_directory, field=field, field_list=field_list, verbose=opts.verbose, stdout_flag=stdout_flag, ) except (FileNotFoundError, RuntimeError): save_fail_message = "Unable to save summary to directory." parser.custom_error(save_fail_message) # Plot the values of some parameter for each target if requested if plot_opt: plot_results = io.tabulate_results(morph_results) # Default parameter is Rw param_name = r"$R_w$" param_list = plot_results["Rw"] # Find parameter if specified if opts.plotparam is not None: param_name = opts.plotparam param_list = tools.case_insensitive_dictionary_search( opts.plotparam, plot_results ) # Not an available parameter to plot or no values found for the # parameter if param_list is None: parser.custom_error( "Cannot find specified plot parameter. No plot shown." ) else: try: if field_list is not None: pdfplot.plot_param( field_list, param_list, param_name, field ) else: pdfplot.plot_param( target_file_names, param_list, param_name ) # Can occur for non-refined plotting parameters # i.e. --smear is not selected as an option, but smear is the # plotting parameter except ValueError: parser.custom_error( "The plot parameter is missing values for at least one " "morph and target pair. No plot shown." ) return morph_results
[docs] def multiple_morphs(parser, opts, pargs, stdout_flag=True): # Custom error messages since usage is distinct when --multiple tag is # applied if len(pargs) < 2: parser.custom_error( "You must supply DIRECTORY and FILE. " "See --multiple-morphs under --help for usage." ) elif len(pargs) > 2: parser.custom_error( "Too many arguments. You must only supply a DIRECTORY and FILE." ) # Parse paths target_file = Path(pargs[1]) if not target_file.is_file(): parser.custom_error( f"{target_file} is not a file. Go to --help for usage." ) morph_directory = Path(pargs[0]) if not morph_directory.is_dir(): parser.custom_error( f"{morph_directory} is not a directory. Go to --help for usage." ) # Get list of files from morph directory morph_list = list(morph_directory.iterdir()) for morph in morph_list: if morph.is_dir(): morph_list.remove(morph) # Do not morph target_file against itself if it is in the same directory if target_file in morph_list: morph_list.remove(target_file) # Format field name for printing and plotting field = None if opts.field is not None: field_words = opts.field.split() field = "" for word in field_words: field += f"{word[0].upper()}{word[1:].lower()}" field_list = None # Sort files in directory by some field if field is not None: try: morph_list, field_list = tools.field_sort( morph_list, field, opts.reverse, opts.serfile, get_field_values=True, ) except KeyError: if opts.serfile is not None: parser.custom_error( "The requested field was not found in the metadata file." ) else: parser.custom_error( "The requested field is missing from a PDF file header." ) else: # Default is alphabetical sort morph_list.sort(reverse=opts.reverse) # Disable single morph plotting plot_opt = opts.plot opts.plot = False # Set up saving save_directory = opts.slocation # User-given directory for saves save_names_file = ( opts.snamesfile ) # User-given serialfile with names for each morph save_morphs_here = None # Subdirectory for saving morphed PDFs save_names = {} # Dictionary of names to save each morph as if save_directory is not None: try: save_morphs_here = io.create_morphs_directory(save_directory) # Could not create directory or find names to save morphs as except (FileNotFoundError, RuntimeError): save_fail_message = "\nUnable to create directory" parser.custom_error(save_fail_message) try: save_names = io.get_multisave_names( morph_list, save_names_file=save_names_file ) # Could not create directory or find names to save morphs as except FileNotFoundError: save_fail_message = "\nUnable to read from save names file" parser.custom_error(save_fail_message) # Morph morph_file against all other files in target_directory morph_results = {} for morph_file in morph_list: if morph_file.is_file: # Set the save file destination to be a file within the SLOC # directory if save_directory is not None: save_as = save_names[morph_file.name][__save_morph_as__] opts.slocation = Path(save_morphs_here).joinpath(save_as) # Perform a morph of morph_file against target_file pargs = [morph_file, target_file] morph_results.update( { morph_file.name: single_morph( parser, opts, pargs, stdout_flag=False ), } ) morph_file_names = [] for key in morph_results.keys(): morph_file_names.append(key) morph_inputs = { "scale": opts.scale, "stretch": opts.stretch, "smear": opts.smear, } morph_inputs.update({"hshift": opts.hshift, "vshift": opts.vshift}) try: # Print summary of morphs to terminal and to file (if requested) io.multiple_morph_output( morph_inputs, morph_results, morph_file_names, save_directory=save_directory, morph_file=target_file, target_directory=morph_directory, field=field, field_list=field_list, verbose=opts.verbose, stdout_flag=stdout_flag, mm=True, ) except (FileNotFoundError, RuntimeError): save_fail_message = "Unable to save summary to directory." parser.custom_error(save_fail_message) # Plot the values of some parameter for each target if requested if plot_opt: plot_results = io.tabulate_results(morph_results) # Default parameter is Rw param_name = r"$R_w$" param_list = plot_results["Rw"] # Find parameter if specified if opts.plotparam is not None: param_name = opts.plotparam param_list = tools.case_insensitive_dictionary_search( opts.plotparam, plot_results ) # Not an available parameter to plot or no values found for the # parameter if param_list is None: parser.custom_error( "Cannot find specified plot parameter. No plot shown." ) else: try: if field_list is not None: pdfplot.plot_param( field_list, param_list, param_name, field ) else: pdfplot.plot_param( morph_file_names, param_list, param_name ) # Can occur for non-refined plotting parameters # i.e. --smear is not selected as an option, but smear is the # plotting parameter except ValueError: parser.custom_error( "The plot parameter is missing values for at least one " "morph and target pair. No plot shown." ) return morph_results
[docs] def getPDFFromFile(fn): from diffpy.morph.tools import readPDF try: r, gr = readPDF(fn) except IOError as errmsg: print("%s: %s" % (fn, errmsg), file=sys.stderr) sys.exit(1) except ValueError: print("Cannot read %s" % fn, file=sys.stderr) sys.exit(1) return r, gr
[docs] def main(): parser = create_option_parser() (opts, pargs) = parser.parse_args() if opts.multiple_targets: multiple_targets(parser, opts, pargs, stdout_flag=True) elif opts.multiple_morphs: multiple_morphs(parser, opts, pargs, stdout_flag=True) else: single_morph(parser, opts, pargs, stdout_flag=True)
if __name__ == "__main__": main()