Source code for pyamptools.extract_ff
#!/usr/bin/env python3
import argparse
import os
import re
import sys
from pyamptools.utility.general import load_yaml
from omegaconf.dictconfig import DictConfig
[docs]
def extract_ff(results, outfileName="", fmt=".5f", test_regex=False, no_phases=False, only=None, regex_merge=None, main_dict={}):
    """
    Extract fit fractions and phase differences between pairs of waves from a FitResults object.
    Args:
        results (FitResults): FitResults object containing fit results
        outfileName (str): Output root file name or dump to stdout if empty string
        fmt (str): String format for printing numbers
        test_regex (bool): If True, only test and print regex grouping without calculating intensities
        no_phases (bool): If True, skip calculating phase differences
        only (str): Only dump fit fractions for "acc" or "noacc". Default dumps both.
        regex_merge (List[str]): List of regex pattern/replace pairs for merging amplitudes.
            Pairs are separated by ~>. The substitution happens for all amplitude names.
            All amplitudes with same reduced name will be grouped into a list and a combined fit fraction
            calculated. See AmpTools' FitResults.intensity method.
            Examples:
                - '.*::(.*)::.*~>\\1': Captures text between :: and replaces full match
                - '.*(.)$~>\\1': Captures last character and replaces full match
                - '.*reaction_(000|045|090|135)::(Pos|Neg)(?:Im|Re)::': Removes matched pattern,
                  allowing grouping over polarizations and mirrored sums
        main_dict (Dict): main yaml dictionary. Allows program to load YAML state, i.e. for getting user requested coherent sums
    """
    def write_ff(amp, intensity, error, intensity_corr, error_corr, only=None):
        if only is None:
            outfile.write(f"FIT FRACTION {amp} = {intensity_corr / total_intensity_corr:{fmt}}|{intensity / total_intensity:{fmt}} +- {error_corr / total_intensity_corr:{fmt}}|{error / total_intensity:{fmt}}\n")
        elif only == "acc":
            outfile.write(f"FIT FRACTION {amp} = {intensity / total_intensity:{fmt}} +- {error / total_intensity:{fmt}}\n")
        elif only == "noacc":  # no accepatance = correct for acceptance
            outfile.write(f"FIT FRACTION {amp} = {intensity_corr / total_intensity_corr:{fmt}} +- {error_corr / total_intensity_corr:{fmt}}\n")
    ############### LOAD RESULTS TIME! ################
    if not test_regex:
        outfile = open(outfileName, "w") if outfileName != "" else sys.stdout
        total_intensity, total_error = results.intensity(False)
        total_intensity_corr, total_error_corr = results.intensity(True)
        outfile.write("########################################################################\n")
        outfile.write("# Values on the left of | are acceptance corrected, on the right are not\n")
        outfile.write(f"# bestMinimum = {results.bestMinimum()}\n")
        outfile.write(f"# lastMinuitCommandStatus = {results.lastMinuitCommandStatus()}\n")
        outfile.write(f"# eMatrixStatus = {results.eMatrixStatus()}\n")
        outfile.write("########################################################################\n\n")
        if only is None:
            outfile.write(f"TOTAL EVENTS = {total_intensity_corr:0.2f}|{total_intensity:0.2f} +- {total_error_corr:0.2f}|{total_error:0.2f}\n")
        elif only == "acc":
            outfile.write(f"TOTAL EVENTS = {total_intensity:0.2f} +- {total_error:0.2f}\n")
        elif only == "noacc":
            outfile.write(f"TOTAL EVENTS = {total_intensity_corr:0.2f} +- {total_error_corr:0.2f}\n")
        else:
            raise ValueError(f"Invalid 'only' argument: {only}. Expected either ['acc', 'noacc']")
    uniqueAmps = results.ampList()  # vector<string>
    uniqueAmps = [str(amp) for amp in uniqueAmps]
    ######## DETERMINE UNIQUE AMPLITUDES AND PLOT THEM ALL #########
    if test_regex:
        print("\nAll Unique Amplitudes:")
        for amp in uniqueAmps:
            print(f" -> {amp}")
    elif not test_regex:
        print("\nAll Unique Amplitudes:")
        for amp in uniqueAmps:  # amp ~ "Reaction::Sum::Amp" whereas amp ~ "Amp"
            amp = str(amp)  # .split('::')[-1] # amp is of type TString I think, convert first
            # Print all amplitudes including including constrained ones, polarizations, etc
            useamp = [amp]
            print(f" -> {amp}")
            intensity, error = results.intensity(useamp, False)
            intensity_corr, error_corr = results.intensity(useamp, True)
            write_ff(amp, intensity, error, intensity_corr, error_corr, only)
    ######## MERGE AMPLITUDES REPLACING REGEX MATCHED STRING WITH WHAT COMES AFTER ~> #########
    if regex_merge is not None:
        for regex in regex_merge:
            pattern, replace = regex.split("~>") if "~>" in regex else (regex, "")
            pattern, replace = r"" + pattern, r"" + replace
            print(f"\nMerged Amplitude Groups based on regex sub: r'{pattern}' -> r'{replace}':")
            merged = {}  # dictionary of lists
            for amps in uniqueAmps:
                if re.search(pattern, amps) is None:
                    continue
                filterd_amp = re.sub(pattern, replace, amps)  # regex to remove numbers, r"" force conversion to raw string
                if filterd_amp not in merged:
                    merged[filterd_amp] = [amps]
                else:
                    merged[filterd_amp].append(amps)
            for merged_amp, amps in merged.items():
                # if len(amps) <= 1:
                #     continue  # skip if none merged. Turn off since if Pmag = 1 then PosIm will not exist (only PosRe)
                print(f" -> {merged_amp} merged {len(amps)} amplitudes:")
                for amp in amps:
                    print(f"     {amp}")
                
                if not test_regex:
                    intensity, error = results.intensity(amps, False)
                    intensity_corr, error_corr = results.intensity(amps, True)
                    write_ff(merged_amp, intensity, error, intensity_corr, error_corr, only)
            merged.clear()
    
    ########## CHECK IF results_dump.coherent_sums is in main_dict ##########
    if isinstance(main_dict, (dict, DictConfig)) and "coherent_sums" in main_dict:
        coherent_sums = main_dict["coherent_sums"]
        if coherent_sums is None:
            coherent_sums = {}
        print("\nMerging Amplitude Groups based on user request in YAML field `coherent_sums`")
        merged = {}
        for k, vs in coherent_sums.items():
            vs = vs.split("_")
            print(f" -> {k} merged {len(vs)} amplitudes:")
            for amp in uniqueAmps:
                if any([v in amp for v in vs]):
                    print(f"     {amp}")
                    if k not in merged:
                        merged[k] = [amp]
                    else:
                        merged[k].append(amp)
        for k in merged.keys():
            intensity, error = results.intensity(merged[k], False)
            intensity_corr, error_corr = results.intensity(merged[k], True)
            write_ff(k, intensity, error, intensity_corr, error_corr, only)
    ######### WRITE ALL POSSIBLE PHASE DIFFERENCES ##########
    if not no_phases and not test_regex:
        for amp1 in uniqueAmps:
            for amp2 in uniqueAmps:
                amp1, amp2 = str(amp1), str(amp2)  # amps are TStrings
                if amp1 == amp2:
                    continue
                same_reaction = amp1.split("::")[0] == amp2.split("::")[0]
                same_sum = amp1.split("::")[1] == amp2.split("::")[1]
                if not same_reaction or not same_sum:
                    continue  # interfence only in same {reaction, sum}
                phase, error = results.phaseDiff(amp1, amp2)
                outfile.write(f"PHASE DIFFERENCE {amp1} {amp2} = {phase:{fmt}} +- {error:{fmt}}\n")
    if not test_regex and outfileName != "":
        outfile.close()
def _cli_extract_ff():
    """Command line interface for extracting fit fractions from an amptools fit results"""
    ############## PARSE COMMANDLINE ARGUMENTS ##############
    parser = argparse.ArgumentParser(description="Extract Fit Fractions from FitResults")
    parser.add_argument("fitFile", type=str, default="", help="Amptools FitResults file name")
    parser.add_argument("--outputfileName", type=str, default="extracted_fitfracs.txt", help="Output file name")
    parser.add_argument("--fmt", type=str, default=".5f", help="Format string for printing")
    parser.add_argument("--test_regex", action="store_true", help="Only test regex grouping without calculating intensities")
    parser.add_argument("--no_phases", action="store_true", help="Do not dump phase differences")
    parser.add_argument("--only", type=str, default=None, help="Only dump fit fractions for ['acc', 'noacc']")
    parser.add_argument("--regex_merge", type=str, nargs="+", help="Merge amplitudes: Regex pair (pattern, replace) separated by ~>")
    parser.add_argument("--main_yaml", type=str, default="", help="YAML file name")
    args = parser.parse_args(sys.argv[1:])
    ################### LOAD LIBRARIES ##################
    from pyamptools import atiSetup
    atiSetup.setup(globals())
    ############## LOAD FIT RESULTS ##############
    fitFile = args.fitFile
    outfileName = args.outputfileName
    fmt = args.fmt
    regex_merge = args.regex_merge
    no_phases = args.no_phases
    only = args.only
    main_yaml = args.main_yaml
    main_dict = {}
    if main_yaml != "":
        main_dict = load_yaml(main_yaml)
    assert os.path.isfile(fitFile), "Fit Results file does not exist at specified path"
    ############## LOAD FIT RESULTS OBJECT ##############
    results = FitResults(fitFile)
    if not results.valid():
        print(f"Invalid fit result in file: {fitFile}")
        exit()
    ############## EXTRACT FIT FRACTIONS ##############
    extract_ff(results, outfileName, fmt, args.test_regex, no_phases, only, regex_merge, main_dict)
if __name__ == "__main__":
    _cli_extract_ff()