def blur(img, fwhm, gradient=True, subdir='tmp'): # note c3d can take voxel rather than fwhm specification, but the Algorithms interface # currently doesn't allow this to be used ... maybe an argument from switching from mincblur if fwhm in (-1, 0, None): if gradient: raise ValueError( "can't compute gradient without a positive FWHM") return Result(stages=Stages(), output=Namespace(img=img)) if gradient: out_gradient = img.newname_with("_blur%s_grad" % fwhm) else: out_gradient = None out_img = img.newname_with("_blurred%s" % fwhm) cmd = CmdStage(cmd=[ 'c3d', '-smooth', "%smm" % fwhm, '-o', out_img.path, img.path ] + (['-gradient', '-o', out_gradient.path] if gradient else []), inputs=(img), outputs=(out_img, out_gradient) if gradient else (out_img, )) return Result(stages=Stages((cmd, )), output=Namespace(img=out_img, gradient=out_gradient) if gradient else Namespace(img=out_img))
def itk_convert_xfm(xfm: ITKXfmAtom, out_ext: str) -> Result[ITKXfmAtom]: if xfm.ext == out_ext: return Result(stages=Stages(), output=xfm) else: out_xfm = xfm.newext(out_ext) cmd = CmdStage( inputs=(xfm, ), outputs=(out_xfm, ), cmd=["itk_convert_xfm", "--clobber", xfm.path, out_xfm.path]) return Result(stages=Stages((cmd, )), output=out_xfm)
def deep_segment( image: FileAtom, deep_segment_pipeline: FileAtom, anatomical_suffix: str, count_suffix: str, outline_suffix: str = None, cell_min_area: int = None, cell_mean_area: float = None, cell_max_area: int = None, temp_dir: str = None, ): anatomical = image.newname_with_suffix("_" + anatomical_suffix) count = image.newname_with_suffix("_" + count_suffix) outline = image.newname_with_suffix( "_" + outline_suffix) if outline_suffix else None stage = CmdStage(inputs=(image, deep_segment_pipeline), outputs=(anatomical, count), cmd=['deep_segment.py', '--segment-intensity 1', '--temp-dir %s' % temp_dir if temp_dir else "", '--learner %s' % deep_segment_pipeline.path, '--image %s' % image.path, '--image-output %s' % anatomical.path, '--centroids-output %s' % count.path, '--outlines-output %s' % outline.path if outline_suffix else "" '--cell-min-area %s' % cell_min_area if cell_min_area else "", '--process-clusters --cell-mean-area %s --cell-max-area %s' % (cell_mean_area, cell_max_area)\ if (cell_mean_area and cell_max_area) else "" ]) return Result(stages=Stages([stage]), output=(anatomical, count, outline))
def antsRegistration(fixed: MincAtom, moving: MincAtom, transform: XfmAtom, output_dir: str, warped: str = "Warped.nii.gz", inversewarped: str = "InverseWarped.nii.gz", dimensionality: int = 3): #TODO warped and inversewarped output to the working directory stage = CmdStage( inputs=(fixed, moving), outputs=(transform, ), cmd=[ 'antsRegistration', '--verbose 1', '--float 0', '--minc', '--dimensionality %s' % dimensionality, '--output [%s,%s,%s]' % (transform.path.replace( '0_GenericAffine.xfm', ''), warped, inversewarped), '--interpolation Linear', '--use-histogram-matching 0', '--winsorize-image-intensities [0.01,0.99]', '--initial-moving-transform [%s,%s,1]' % (fixed.path, moving.path), #1 indicates center of mass '--transform Translation[0.1]', '--metric MI[%s,%s,1,32,Regular,0.25]' % (fixed.path, moving.path), '--convergence [1000x500x250x0,1e-6,10]', '--shrink-factors 12x8x4x2', '--smoothing-sigmas 4x3x2x1vox' ], log_file=os.path.join(output_dir, "join_sections.log")) return Result(stages=Stages([stage]), output=(transform))
def tamarack_pipeline(options): output_dir = options.application.output_directory pipeline_name = options.application.pipeline_name #processed_dir = os.path.join(output_dir, pipeline_name + "_processed") first_level_dir = os.path.join(output_dir, pipeline_name + "_first_level") s = Stages() with open(options.application.csv_file, 'r') as f: files_df = (pd.read_csv( filepath_or_buffer=f, usecols=['group', 'filename']).assign(file=lambda df: df.apply( axis="columns", func=lambda r: MincAtom(r.filename.strip(), pipeline_sub_dir=os.path.join( first_level_dir, "%s_processed" % r .group.strip()))))) check_MINC_input_files(files_df.file.apply(lambda img: img.path)) #grouped_files_df = pd.DataFrame({'file' : pd.concat([imgs])}).assign(group=lambda df: df.index) tamarack_result = s.defer(tamarack(files_df, options=options)) tamarack_result.first_level_results.applymap(maybe_deref_path).to_csv( "first_level_results.csv", index=False) tamarack_result.resampled_determinants.applymap(maybe_deref_path).to_csv( "resampled_determinants.csv", index=False) tamarack_result.overall_determinants.applymap(maybe_deref_path).to_csv( "overall_determinants.csv", index=False) return Result(stages=s, output=tamarack_result)
def f( imgs: List[MincAtom], nlin_dir: str, conf: nlin_module.MultilevelConf, initial_target: MincAtom, nlin_prefix: str, #output_dir_for_avg: str = None, #output_name_wo_ext: str = None ): s = Stages() pairwise_result = s.defer( pairwise(nlin_module, max_images=25, max_pairs=None).build_model( imgs=imgs, nlin_dir=nlin_dir, conf=nlin_module.hierarchical_to_single(conf)[-1] if conf else None, initial_target=initial_target, nlin_prefix=nlin_prefix #, output_name_wo_ext=output_name_wo_ext #, algorithms=nlin_module.algorithms )) build_model_result = s.defer( build_model(nlin_module).build_model( imgs=imgs, nlin_dir=nlin_dir, conf=conf, initial_target=pairwise_result.avg_img, nlin_prefix=nlin_prefix #, output_name_wo_ext=output_name_wo_ext #, algorithms=algorithms )) return Result(stages=s, output=build_model_result)
def build_model(imgs, conf, nlin_dir, nlin_prefix, initial_target, output_name_wo_ext = None): s = Stages() mincify = base_build_model.ToMinc imgs = tuple(s.defer(mincify.from_mnc(img)) for img in imgs) result = s.defer(base_build_model.build_model(imgs=imgs, conf=conf, nlin_dir=nlin_dir, nlin_prefix=nlin_prefix, initial_target=s.defer(mincify.from_mnc(initial_target)) #output_name_wo_ext=output_name_wo_ext )) def wrap_output_xfmh(xfmh): return XfmHandler(source=s.defer(mincify.to_mnc(xfmh.source)) if xfmh.source else None, target=s.defer(mincify.to_mnc(xfmh.target)) if xfmh.target else None, resampled=s.defer(mincify.to_mnc(xfmh.resampled)) if xfmh.has_resampled() else None, xfm=s.defer(mincify.to_mni_xfm(xfmh.xfm)), inverse=wrap_output_xfmh(xfmh.inverse) if xfmh.has_inverse() else None) return Result(stages=s, output=WithAvgImgs(avg_imgs=[s.defer(mincify.to_mnc(img)) for img in result.avg_imgs], avg_img=s.defer(mincify.to_mnc(result.avg_img)), output=[wrap_output_xfmh(x) for x in result.output]))
def det_and_log_det( displacement_grid: MincAtom, fwhm: Optional[float], annotation: str = "" ) -> Result[Namespace]: # (det=MincAtom, log_det=MincAtom)]: """ When this function is called, you might (or should) know what kind of deformation grid is passed along. This allows you to provide a proper annotation for the produced log determinant file. For instance "absolute" or "relative" for transformations that include an affine linear part, or that have the linear part taken out respectively. """ s = Stages() # TODO: naming doesn't correspond with the (automagic) file naming: d-1 <=> det(f), det <=> det+1(f) det = s.defer( determinant( s.defer(smooth_vector(source=displacement_grid, fwhm=fwhm) ) if fwhm else displacement_grid)) output_filename_wo_ext = displacement_grid.filename_wo_ext + "_log_det" + annotation if fwhm: output_filename_wo_ext += "_fwhm" + str(fwhm) log_det = s.defer( mincmath(op='log', vols=[det], subdir="stats-volumes", new_name=output_filename_wo_ext)) return Result(stages=s, output=Namespace(det=det, log_det=log_det))
def asymmetry_pipeline(options): output_dir = options.application.output_directory pipeline_name = options.application.pipeline_name processed_dir = os.path.join(output_dir, pipeline_name + "_processed") s = Stages() #imgs_ = [MincAtom(f, pipeline_sub_dir=processed_dir) for f in options.application.files] imgs_ = get_imgs(options.application) check_MINC_input_files([img.path for img in imgs_]) imgs = pd.Series(imgs_, index=[img.filename_wo_ext for img in imgs_]) flipped_imgs = imgs.apply(lambda img: s.defer(volflip(img)) ) # TODO add flags to control flip axis ... # TODO ugly - MincAtom API should allow this somehow without mutation (also, how to pass into `volflip`, etc.?) for f_i in flipped_imgs: f_i.output_sub_dir += "_flipped" check_MINC_input_files(imgs.apply(lambda img: img.path)) grouped_files_df = pd.DataFrame({ 'file': pd.concat([imgs, flipped_imgs]) }).assign(group=lambda df: df.index) two_level_result = s.defer(two_level(grouped_files_df, options=options)) return Result(stages=s, output=two_level_result)
def shift_modify_header(img: MincAtom, shifted_img: MincAtom, newx: float, newy: float, newz: float): s = Stages() #Copy file to new location stage = CmdStage(inputs=(img,), outputs=(shifted_img,), memory=1, cmd=['cp', img.path, shifted_img.path]) print(stage.render()) s.add(stage) #Alter header of copied image to shift xspace_start = 'xspace:start='+newx yspace_start = 'yspace:start='+newy zspace_start = 'zspace:start='+newz stage = CmdStage(inputs=(shifted_img,), outputs=(shifted_img,), memory=1, cmd=['minc_modify_header','-dinsert',xspace_start, '-dinsert',yspace_start, '-dinsert',zspace_start, shifted_img.path]) print(stage.render()) s.add(stage) #Alter header of copied image with header modification append_history_string = ':history= >>> copy and shift: '+ shifted_img.path +' to '+ shifted_img.path stage = CmdStage(inputs=(shifted_img,), outputs=(shifted_img,), memory=1, cmd=['minc_modify_header','-sappend', append_history_string, shifted_img.path]) print(stage.render()) s.add(stage) return Result(stages=s, output=shifted_img)
def lsq6_pipeline(options): # TODO could also allow pluggable pipeline parts e.g. LSQ6 could be substituted out for the modified LSQ6 # for the kidney tips, etc... output_dir = options.application.output_directory pipeline_name = options.application.pipeline_name # TODO this is tedious and annoyingly similar to the registration chain and MBM ... lsq6_dir = os.path.join(output_dir, pipeline_name + "_lsq6") imgs = get_imgs(options.application) s = Stages() # FIXME: why do we have to call registration_targets *outside* of lsq6_nuc_inorm? is it just because of the extra # options required? targets = s.defer( registration_targets(lsq6_conf=options.lsq6, app_conf=options.application, reg_conf=options.registration, first_input_file=imgs[0].path)) # TODO this is quite tedious and duplicates stuff in the registration chain ... resolution = (options.registration.resolution or get_resolution_from_file( targets.registration_standard.path)) # This must happen after calling registration_targets otherwise it will resample to options.registration.resolution options.registration = options.registration.replace(resolution=resolution) lsq6_result = s.defer( lsq6_nuc_inorm(imgs=imgs, resolution=resolution, registration_targets=targets, lsq6_dir=lsq6_dir, lsq6_options=options.lsq6)) return Result(stages=s, output=lsq6_result)
def nlin_part(xfm: XfmHandler, inv_xfm: Optional[XfmHandler] = None) -> Result[XfmHandler]: """ *** = non linear deformations --- = linear (affine) deformations Input: xfm : ******------> inv_xfm : <******------ (optional) Calculated: inv_lin_xfm : <------ Returned: concat : ******------> + <------ equals : ******> Compute the nonlinear part of a transform as follows: go forwards across xfm and then backwards across the linear part of the inverse xfm (by first calculating the inverse or using the one supplied) Finally, use minc_displacement to compute the resulting gridfile of the purely nonlinear part. The optional inv_xfm (which must be the inverse!) is an optimization - we don't go looking for an inverse by filename munging and don't programmatically keep a log of operations applied, so any preexisting inverse must be supplied explicitly. """ s = Stages() inv_xfm = inv_xfm or s.defer(invert_xfmhandler(xfm)) inv_lin_part = s.defer(lin_from_nlin(inv_xfm)) xfm = s.defer(concat_xfmhandlers([xfm, inv_lin_part])) return Result(stages=s, output=xfm)
def as_deformation(xfm): c = CmdStage(cmd=[ "transformix", "-def", "all", "-out", dirname, "-tp", xfm.path, "-xfm", out_path ], inputs=(xfm, ), outputs=NotImplemented) return Result(stages=Stages([c]), output=NotImplemented)
def scale_transform(xfm, scale, newname_wo_ext): scaled_xfm = xfm.newname_with_suffix("_scaled%s" % scale) c = CmdStage( cmd=["dramms-defop", "-m", str(scale), xfm.path, scaled_xfm.path], inputs=(xfm, ), outputs=(scaled_xfm, )) return Result(stages=Stages([c]), output=scaled_xfm)
def determinant(displacement_grid: MincAtom) -> Result[MincAtom]: """ Takes a displacement field (deformation grid, vector field, those are all the same thing) and calculates the proper determinant (mincblob() takes care of adding 1 to the silly output of running mincblob directly) """ s = Stages() det = s.defer(mincblob(op='determinant', grid=displacement_grid)) return Result(stages=s, output=det)
def smooth_vector(source: MincAtom, fwhm: float) -> Result[MincAtom]: outf = source.newname_with_suffix( "_smooth_fwhm%s" % fwhm, subdir="tmp") # TODO smooth_displacement_? cmd = [ 'smooth_vector', '--clobber', '--filter', '--fwhm=%s' % fwhm, source.path, outf.path ] stage = CmdStage(inputs=(source, ), outputs=(outf, ), cmd=cmd) return Result(stages=Stages([stage]), output=outf)
def surface_mask2(input: MincAtom, surface: FileAtom, args: List[str] = []) -> Result[MincAtom]: mask_vol = surface.newname_with_suffix("_mask", ext=".mnc") stage = CmdStage(inputs=(input, surface), outputs=(mask_vol, ), cmd=["surface_mask2", "-clobber"] + args + [input.path, surface.path, mask_vol.path]) return Result(stages=Stages([stage]), output=mask_vol)
def dramms_warp( img: NiiAtom, # TODO change to ITKAtom ?! xfm: XfmAtom, # TODO: update to handler? like: NiiAtom, invert: bool = False, use_nn_interpolation=None, new_name_wo_ext: str = None, subdir: str = None, postfix: str = None) -> Result[NiiAtom]: s = Stages() if not subdir: subdir = 'resampled' # we need to get the filename without extension here in case we have # masks/labels associated with the input file. When that's the case, # we supply its name with "_mask" and "_labels" for which we need # to know what the main file will be resampled as if not new_name_wo_ext: # FIXME this is wrong when invert=True new_name_wo_ext = xfm.filename_wo_ext + '-resampled' new_img = s.defer( dramms_warp_simple( img=img, xfm=xfm, like=like, #extra_flags=extra_flags, invert=invert, #interpolation=interpolation, use_nn_interpolation=use_nn_interpolation, new_name_wo_ext=new_name_wo_ext, subdir=subdir)) new_img.mask = s.defer( dramms_warp_simple(img=img.mask, xfm=xfm, like=like, use_nn_interpolation=True, invert=invert, new_name_wo_ext=new_name_wo_ext + "_mask", subdir=subdir)) if img.mask is not None else None new_img.labels = s.defer( dramms_warp_simple(img=img.labels, xfm=xfm, like=like, use_nn_interpolation=True, invert=invert, new_name_wo_ext=new_name_wo_ext + "_labels", subdir=subdir)) if img.labels is not None else None # Note that new_img can't be used for anything until the mask/label files are also resampled. # This shouldn't create a problem with stage dependencies as long as masks/labels appear in inputs/outputs of CmdStages. # (If this isn't automatic, a relevant helper function would be trivial.) # TODO: can/should this be done semi-automatically? probably ... return Result(stages=s, output=new_img)
def register( source: MincAtom, target: MincAtom, conf: Conf, initial_source_transform: Optional[XfmAtom] = None, transform_name_wo_ext: str = None, generation: int = None, # not used; remove from API (fix ANTS) resample_source: bool = False, resample_subdir: str = "resampled") -> Result[XfmHandler]: if conf is None: raise ValueError("no configuration supplied") out_dir = os.path.join( source.pipeline_sub_dir, source.output_sub_dir, "%s_elastix_to_%s" % (source.filename_wo_ext, target.filename_wo_ext)) # elastix chooses this for us: out_img = NiiAtom( name=os.path.join(out_dir, "result.%d.mnc" % 0), # TODO number of param files ?!?! pipeline_sub_dir=source.pipeline_sub_dir, output_sub_dir=source.output_sub_dir) #out_xfm = XfmAtom(name = "%s_elastix_to_%s.xfm" % (source.filename_wo_ext, target.filename_wo_ext), # pipeline_sub_dir=source.pipeline_sub_dir, output_sub_dir=source.output_sub_dir) out_xfm = XfmAtom( name=os.path.join(out_dir, "TransformParameters.%d.txt" % 0), # TODO number of param files ?!?! pipeline_sub_dir=source.pipeline_sub_dir, output_sub_dir=source.output_sub_dir) cmd = (['elastix', '-f', source.path, '-m', target.path] + (flatten(*[["-p", x] for x in conf])) + (["-fMask", source.mask.path] if source.mask else []) + (["-mMask", target.mask.path] if target.mask else []) + (["-t0", initial_source_transform.path] if initial_source_transform else []) + (["-out", out_dir])) s = CmdStage(cmd=cmd, inputs=(source, target) + ((source.mask, ) if source.mask else ()) + ((target.mask, ) if target.mask else ()), outputs=(out_xfm, out_img)) #s2 = CmdStage(cmd=['transformix', '-out', os.path.join(resample_subdir, "%s" % c), # "-tp", out_xfm, "-in", out_name], # inputs=(), outputs=()) xfm = XfmHandler(source=source, target=target, xfm=out_xfm, resampled=out_img) return Result(stages=Stages([s]), output=xfm) # one question is whether we should have separate NLIN/LSQ12/LSQ6 interfaces or not, given that these differences seem # like they should be rather irrelevant to general registration procedures ... at present minctracc # is the main difficulty, since it uses different
def transform_objects( input_obj: FileAtom, xfm: XfmAtom) -> Result[FileAtom]: # XfmAtom -> XfmHandler?? output_obj = input_obj.newname_with_suffix("_resampled_via_%s" % xfm.filename_wo_ext) stage = CmdStage( inputs=(input_obj, xfm), outputs=(output_obj, ), cmd=["transform_objects", input_obj.path, xfm.path, output_obj.path]) return Result(stages=Stages([stage]), output=output_obj)
def dramms_invert(defs: DrammsXfmAtom, out_defs: Optional[DrammsXfmAtom] = None): out_defs = out_defs or defs.newname_with_suffix("_inverted") return Result(stages=Stages([ CmdStage(cmd=['dramms-defop', '-i', defs.path, out_defs.path], inputs=(defs, ), outputs=(out_defs, )) ]), output=out_defs)
def mincmath(imgs: List[MincAtom], result: MincAtom, output_dir: str): stage = CmdStage(inputs=tuple(imgs), outputs=(result, ), cmd=[ 'mincmath', '-clobber', '-add', ' '.join(img.path for img in imgs.__iter__()), result.path ], log_file=os.path.join(output_dir, "join_sections.log")) return Result(stages=Stages([stage]), output=result)
def concat_xfm(xfms: List[XfmAtom], outxfm: XfmAtom, output_dir: str): stage = CmdStage(inputs=tuple(xfms), outputs=(outxfm, ), cmd=[ 'xfmconcat', '-clobber', ' '.join(xfm.path for xfm in xfms.__iter__()), outxfm.path ], log_file=os.path.join(output_dir, "join_sections.log")) return Result(stages=Stages([stage]), output=(outxfm))
def reconstitute_laplacian_grid(cortex: MincAtom, grid: MincAtom, midline: MincAtom) -> Result[MincAtom]: output_grid = grid.newname_with_suffix("_reconstituted") stage = CmdStage(inputs=(cortex, grid, midline), outputs=(output_grid, ), cmd=[ "reconstitute_laplacian_grid", midline.path, cortex.path, grid.path, output_grid.path ]) return Result(stages=Stages([stage]), output=output_grid)
def average_transforms(xfms, avg_xfm): s = Stages() itk_xfms = copy.deepcopy(xfms) for xfm in itk_xfms: xfm.xfm = s.defer(dramms_to_itk(xfm.xfm)) return Result(stages=s, output=s.defer( itk_to_dramms( s.defer( itk.Algorithms.average_transforms( itk_xfms, avg_xfm)))))
def dist_corr_basket(img: MincAtom, dc_img: MincAtom): stage = CmdStage(inputs=(img,), outputs=(dc_img,), memory=4, cmd=['distortion_correction_september_2014.pl', '-spawn','-output-dir', img.pipeline_sub_dir, img.path]) print(stage.render()) stage.set_log_file(log_file_name=os.path.join(img.pipeline_sub_dir,"dist_corr.log")) return Result(stages=Stages([stage]), output=dc_img)
def dramms_combine(t1: DrammsXfmAtom, t2: DrammsXfmAtom, name: Optional[str] = None): # TODO this is a hack ... implement more sane naming (factor from xfmconcat?) out_xfm = t1.newname(name="concat_of_%s_and_%s" % (t1.filename_wo_ext, t2.filename_wo_ext), subdir="transforms") s = CmdStage(cmd=['dramms-combine', '-c', t1.path, t2.path, out_xfm.path], inputs=(t1, t2), outputs=(out_xfm)) return Result(stages=Stages([s]), output=out_xfm)
def itk_to_dramms(xfm: ITKXfmAtom) -> Result[DrammsXfmAtom]: out_xfm = xfm.newname_with_suffix("_dramms") return Result(stages=Stages([ CmdStage(cmd=[ 'dramms-convert', '-f', 'ITK', '-i', xfm.path, '-F', 'DRAMMS', '-o', out_xfm.path ], inputs=(xfm, ), outputs=(out_xfm, )) ]), output=out_xfm)
def NLIN_pipeline(options): if options.application.files is None: raise ValueError("Please, some files! (or try '--help')") # TODO make a util procedure for this output_dir = options.application.output_directory pipeline_name = options.application.pipeline_name # TODO this is tedious and annoyingly similar to the registration chain and MBM and LSQ6 ... processed_dir = os.path.join(output_dir, pipeline_name + "_processed") nlin_dir = os.path.join(output_dir, pipeline_name + "_nlin") resolution = (options.registration.resolution # TODO does using the finest resolution here make sense? or min([get_resolution_from_file(f) for f in options.application.files])) imgs = [MincAtom(f, pipeline_sub_dir=processed_dir) for f in options.application.files] # determine NLIN settings by overriding defaults with # any settings present in protocol file, if it exists # could add a hook to print a message announcing completion, output files, # add more stages here to make a CSV initial_target_mask = MincAtom(options.nlin.target_mask) if options.nlin.target_mask else None initial_target = MincAtom(options.nlin.target, mask=initial_target_mask) full_hierarchy = get_nonlinear_configuration_from_options(nlin_protocol=options.nlin.nlin_protocol, flag_nlin_protocol=next(iter(options.nlin.flags_.nlin_protocol)), reg_method=options.nlin.reg_method, file_resolution=resolution) s = Stages() nlin_result = s.defer(nlin_build_model(imgs, initial_target=initial_target, conf=full_hierarchy, nlin_dir=nlin_dir)) # TODO return these? inverted_xfms = [s.defer(invert_xfmhandler(xfm)) for xfm in nlin_result.output] if options.stats.calc_stats: # TODO: put the stats part behind a flag ... determinants = [s.defer(determinants_at_fwhms( xfm=inv_xfm, inv_xfm=xfm, blur_fwhms=options.stats.stats_kernels)) for xfm, inv_xfm in zip(nlin_result.output, inverted_xfms)] return Result(stages=s, output=Namespace(nlin_xfms=nlin_result, avg_img=nlin_result.avg_img, determinants=determinants)) else: # there's no consistency in what gets returned, yikes ... return Result(stages=s, output=Namespace(nlin_xfms=nlin_result, avg_img=nlin_result.avg_img))
def scale_transform(xfm, scale): scaled_xfm = xfm.newname_with_suffix("_scaled_%s" % scale) # don't actually evaluate the resulting vector field: s = CmdStage(cmd=[ "echo", ('(Transform "WeightedCombinationTransform")\n' '(SubTransforms %s)\n' '(Scales %s)\n') % (xfm.path, scale) ], inputs=(xfm, ), outputs=(scaled_xfm, )) return Result(stages=Stages([s]), output=scaled_xfm)