changeset 0:59453ad6e219 draft

"planemo upload for repository https://github.com/ximg-chess/galaxytools/tools/hedm_lattice_parameter_correction commit 420bbed5449757711b88200208bb050e0797f5a3"
author ximgchess
date Thu, 19 May 2022 16:48:22 +0000
parents
children 6e5e9abb4139
files hedm_lattice_parameter_correction.xml lattice_parameter_correction.py lattice_parameter_correction.py.orig macros.xml test-data/config.out.yml test-data/config.yml test-data/dexelas_id3a_20200130.yml test-data/grains.out test-data/lattice_parameter_correction.py test-data/lp_avg.tsv test-data/m_out.h5 test-data/materials_h5.h5
diffstat 12 files changed, 707 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hedm_lattice_parameter_correction.xml	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,62 @@
+<tool id="hedm_lattice_parameter_correction" name="HEDM lattice parameter correction" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" python_template_version="3.5">
+    <description>Extract average lattice parameters from grains.out</description>
+    <macros>
+        <import>macros.xml</import>
+    </macros>
+    <expand macro="requirements" />
+    <command detect_errors="exit_code"><![CDATA[
+        @CMD_IMPORTS@
+        #set $material_file= $ln_name($material,'h5')
+        cp '$material' '$material_file' &&
+        #set $instrument_file = $ln_name($instrument,'yml')
+        cp '$instrument' '$instrument_file' &&
+        ls -ltr &&
+        $__tool_directory__/lattice_parameter_correction.py
+        --min-completeness $min_completeness
+        $force_symmetry
+        $hexrd_cfg
+        $grains
+        --average_lattice '$lattice_out'
+        --materials_out '$materials_out'
+        && ls -ltr
+    ]]></command>
+    <configfiles>
+        <configfile name="hexrd_cfg"><![CDATA[#slurp
+@CMD_IMPORTS@
+#slurp
+#set $material_file = $ln_name($material,'h5')
+#set $instrument_file = $ln_name($instrument,'yml')
+$update_config($config, $material_file, $instrument_file)
+        ]]></configfile>
+    </configfiles>
+
+    <inputs>
+        <param name="config" type="data" format="hexrd.yml"  label="config file from hexrd fitgrains"/>
+        <param name="instrument" type="data" format="hexrd.yml"  label="instrument file from hexrd fitgrains"/>
+        <param name="material" type="data" format="hexrd.materials.h5"  label="material file used in hexrd fitgrains"/>
+        <param name="grains" type="data" format="tabular"  label="grains.out from hexrd fitgrains"/>
+        <param name="min_completeness" argument="--min-completeness" type="float" value="0.75" min="0.0" max="1.0" label="completeness threshold"/>
+        <param name="force_symmetry" argument="--force-symmetry" type="boolean" truevalue="--force-symmetry" falsevalue="" checked="false" label="force symmetry"/>
+    </inputs>
+    <outputs>
+        <data name="lattice_out" format="tabular" label="${tool.name} ${on_string} average lattice"/>
+        <data name="materials_out" format="hexrd.materials.h5" label="material_avg.h5"/>
+    </outputs>
+    <tests>
+        <test>
+            <param name="config" ftype="hexrd.yml" value="config.yml"/>
+            <param name="instrument" ftype="hexrd.yml" value="dexelas_id3a_20200130.yml"/>
+            <param name="material" ftype="hexrd.materials.h5" value="materials_h5.h5"/>
+            <param name="grains" ftype="tabular" value="grains.out"/>
+            <output name="lattice_out" ftype="tabular">
+                <assert_contents>
+                    <has_text_matching expression="4.75\d*\t4.7\d*\t12.9\d*\t90.\d*\t90.\d*\t120.\d*"/>
+                </assert_contents>
+            </output>
+        </test>
+    </tests>
+    <help><![CDATA[
+Update a hexrd materials file with averaged latice parameters.
+    ]]></help>
+    <expand macro="citations" />
+</tool>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lattice_parameter_correction.py	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+
+import argparse
+
+import numpy as np
+
+from hexrd import rotations as rot
+from hexrd import matrixutil as mutil
+from hexrd import config
+from hexrd import material
+
+
+def _lp_calc(v_mat, f_mat, r_mat):
+    """
+    do strained lappi
+
+    Parameters
+    ----------
+    vmat : TYPE
+        DESCRIPTION.
+    f_mat : TYPE
+        DESCRIPTION.
+    r_mat : TYPE
+        DESCRIPTION.
+
+    Returns
+    -------
+    lparams : TYPE
+        DESCRIPTION.
+
+    """
+    f_prime = np.dot(v_mat, np.dot(r_mat, f_mat))
+    f_prime_hat = mutil.unitVector(f_prime)
+
+    lp_mags = mutil.columnNorm(f_prime)
+
+    lp_angs = np.hstack(
+        [np.arccos(np.dot(f_prime_hat[:, 1], f_prime_hat[:, 2])),
+         np.arccos(np.dot(f_prime_hat[:, 2], f_prime_hat[:, 0])),
+         np.arccos(np.dot(f_prime_hat[:, 0], f_prime_hat[:, 1]))]
+    )
+    return np.hstack([lp_mags, np.degrees(lp_angs)])
+
+
+def extract_lattice_parameters(
+        cfg_file, grains_file,
+        min_completeness=0.90, force_symmetry=False,
+        materials_out=None
+        ):
+    """
+    """
+    cfg = config.open(cfg_file)[0]
+    mcfg = cfg.material
+    m = mcfg.materials[mcfg.active]
+    # print(f'material.latticeParameters: {m.latticeParameters}')
+    # print(f'material.planeData: {m.planeData}')
+
+    pd = cfg.material.plane_data
+    qsym_mats = rot.quatProductMatrix(pd.getQSym())
+
+    f_mat = pd.latVecOps['F']
+
+    gt = np.loadtxt(grains_file)
+
+    idx = gt[:, 1] >= min_completeness
+
+    expMaps = gt[idx, 3:6].T
+    vinv = gt[idx, 9:15]
+
+    lparms = []
+    for i in range(sum(idx)):
+        quat = rot.quatOfExpMap(expMaps[:, i].reshape(3, 1))
+        v_mat = np.linalg.inv(mutil.vecMVToSymm(vinv[i, :].reshape(6, 1)))
+        if force_symmetry:
+            for qpm in qsym_mats:
+                lparms.append(
+                    _lp_calc(v_mat, f_mat, rot.rotMatOfQuat(np.dot(qpm, quat)))
+                )
+        else:
+            lparms.append(
+                    _lp_calc(v_mat, f_mat, rot.rotMatOfQuat(quat))
+                )
+    lp_avg = np.average(np.vstack(lparms), axis=0)
+    if materials_out is not None:
+        m.latticeParameters = lp_avg 
+        # print(f'material.latticeParameters: {m.latticeParameters}')
+        # print(f'material.latticeParameters: {mcfg.materials[mcfg.active].latticeParameters}')
+        # print(f'material.planeData: {m.planeData}')
+        material.save_materials_hdf5(materials_out, mcfg.materials) 
+    return lp_avg, sum(idx), len(idx)
+
+
+if __name__ == '__main__':
+    """
+    maybe make this smart enough to average properly for symmetry?
+    """
+    parser = argparse.ArgumentParser(
+        description="Extract average lattice parameters from grains.out"
+    )
+    parser.add_argument(
+        'cfg', help="ff-HEDM config YAML file", type=str
+        )
+    parser.add_argument(
+        'grains_file', help="grains.out file", type=str
+    )
+    parser.add_argument(
+        '-m', '--min-completeness', help="completeness threshold", type=float,
+        default=0.75
+    )
+    parser.add_argument(
+        '-f', '--force-symmetry',
+        help="symmetrize results",
+        action="store_true"
+    )
+    parser.add_argument(
+        '-a', '--average_lattice', default="None", help="average lattice file"
+    )
+    parser.add_argument(
+        '-o', '--materials_out', default="None", help="updated material file"
+    )
+    args = parser.parse_args()
+
+    cfg_file = args.cfg
+    grains_file = args.grains_file
+    min_completeness = args.min_completeness
+    force_symmetry = args.force_symmetry
+    materials_out = args.materials_out
+
+    lp_avg, n_used, n_total = extract_lattice_parameters(
+        cfg_file, grains_file,
+        min_completeness=min_completeness, force_symmetry=force_symmetry,
+        materials_out=materials_out  
+    )
+    print("Using %d grains out of %d above %.2f%% completeness threshold:"
+          % (n_used, n_total, 100*min_completeness))
+    print("---------------------------------------------------------------")
+    print("a       \tb       \tc       "
+          + "\talpha   \tbeta    \tgamma \n"
+          + "%.6f\t%.6f\t%.6f\t%.6f\t%.6f\t%.6f" % tuple(lp_avg))
+    if args.average_lattice:
+        with open(args.average_lattice,'w') as fh:
+            print("%.6f\t%.6f\t%.6f\t%.6f\t%.6f\t%.6f" % tuple(lp_avg), file=fh)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lattice_parameter_correction.py.orig	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+
+import argparse
+
+import numpy as np
+
+from hexrd import rotations as rot
+from hexrd import matrixutil as mutil
+from hexrd import config
+
+
+def _lp_calc(v_mat, f_mat, r_mat):
+    """
+    do strained lappi
+
+    Parameters
+    ----------
+    vmat : TYPE
+        DESCRIPTION.
+    f_mat : TYPE
+        DESCRIPTION.
+    r_mat : TYPE
+        DESCRIPTION.
+
+    Returns
+    -------
+    lparams : TYPE
+        DESCRIPTION.
+
+    """
+    f_prime = np.dot(v_mat, np.dot(r_mat, f_mat))
+    f_prime_hat = mutil.unitVector(f_prime)
+
+    lp_mags = mutil.columnNorm(f_prime)
+
+    lp_angs = np.hstack(
+        [np.arccos(np.dot(f_prime_hat[:, 1], f_prime_hat[:, 2])),
+         np.arccos(np.dot(f_prime_hat[:, 2], f_prime_hat[:, 0])),
+         np.arccos(np.dot(f_prime_hat[:, 0], f_prime_hat[:, 1]))]
+    )
+    return np.hstack([lp_mags, np.degrees(lp_angs)])
+
+
+def extract_lattice_parameters(
+        cfg_file, grains_file,
+        min_completeness=0.90, force_symmetry=False
+        ):
+    """
+    """
+    cfg = config.open(cfg_file)[0]
+
+    pd = cfg.material.plane_data
+    qsym_mats = rot.quatProductMatrix(pd.getQSym())
+
+    f_mat = pd.latVecOps['F']
+
+    gt = np.loadtxt(grains_file)
+
+    idx = gt[:, 1] >= min_completeness
+
+    expMaps = gt[idx, 3:6].T
+    vinv = gt[idx, 9:15]
+
+    lparms = []
+    for i in range(sum(idx)):
+        quat = rot.quatOfExpMap(expMaps[:, i].reshape(3, 1))
+        v_mat = np.linalg.inv(mutil.vecMVToSymm(vinv[i, :].reshape(6, 1)))
+        if force_symmetry:
+            for qpm in qsym_mats:
+                lparms.append(
+                    _lp_calc(v_mat, f_mat, rot.rotMatOfQuat(np.dot(qpm, quat)))
+                )
+        else:
+            lparms.append(
+                    _lp_calc(v_mat, f_mat, rot.rotMatOfQuat(quat))
+                )
+    lp_avg = np.average(np.vstack(lparms), axis=0)
+    return lp_avg, sum(idx), len(idx)
+
+
+if __name__ == '__main__':
+    """
+    maybe make this smart enough to average properly for symmetry?
+    """
+    parser = argparse.ArgumentParser(
+        description="Extract average lattice parameters from grains.out"
+    )
+    parser.add_argument(
+        'cfg', help="ff-HEDM config YAML file", type=str
+        )
+    parser.add_argument(
+        'grains_file', help="grains.out file", type=str
+    )
+    parser.add_argument(
+        '-m', '--min-completeness', help="completeness threshold", type=float,
+        default=0.75
+    )
+
+    parser.add_argument(
+        '-f', '--force-symmetry',
+        help="symmetrize results",
+        action="store_true"
+    )
+
+    args = parser.parse_args()
+
+    cfg_file = args.cfg
+    grains_file = args.grains_file
+    min_completeness = args.min_completeness
+    force_symmetry = args.force_symmetry
+
+    lp_avg, n_used, n_total = extract_lattice_parameters(
+        cfg_file, grains_file,
+        min_completeness=min_completeness, force_symmetry=force_symmetry
+    )
+    print("Using %d grains out of %d above %.2f%% completeness threshold:"
+          % (n_used, n_total, 100*min_completeness))
+    print("---------------------------------------------------------------")
+    print("a       \tb       \tc       "
+          + "\talpha   \tbeta    \tgamma \n"
+          + "%.6f\t%.6f\t%.6f\t%.6f\t%.6f\t%.6f" % tuple(lp_avg))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/macros.xml	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,50 @@
+<macros>
+    <token name="@TOOL_VERSION@">0.8.19</token>
+    <token name="@VERSION_SUFFIX@">0</token>
+    <token name="@PROFILE@">21.09</token>
+    <xml name="requirements">
+        <requirements>
+        <requirement type="package" version="@TOOL_VERSION@">hexrd</requirement>
+            <yield/>
+        </requirements>
+    </xml>
+    <xml name="citations">
+        <citations>
+            <citation type="doi">10.1016/j.matchar.2020.110366</citation>
+            <yield />
+        </citations>
+    </xml>
+
+    <token name="@CMD_IMPORTS@">
+#import re
+#import yaml
+#def identifier_or_name($input1)
+    #if hasattr($input1, 'element_identifier')
+        #return $input1.element_identifier
+    #elif hasattr($input1, 'name')
+        #return $input1.name
+    #else
+        #return str($input1)
+    #end if
+#end def
+#def clean($name1)
+    #set $name_clean = $re.sub('[^\w\-_]', '_', $re.sub('(?i)[.](npz|hexrd|yml|dat|out)$','', $name1.split()[-1]))
+    #return $name_clean
+#end def
+#def ln_name($ds,$ext)
+    #set $lname = "%s.%s" % ($clean($identifier_or_name($ds)),$ext)
+    #return $lname
+#end def
+#def update_config($cfg_in, $mat_path, $inst_path)
+    #set $cfg = $yaml.safe_load(open(str($cfg_in),'r'))    
+    #if $mat_path and 'material' in $cfg and 'definitions' in $cfg['material']
+        #silent $cfg['material']['definitions'] = str($mat_path)
+    #end if
+    #if $inst_path and 'instrument' in $cfg 
+        #silent $cfg['instrument'] = str($inst_path)
+    #end if
+#slurp
+$yaml.dump($cfg)
+#end def
+    </token>
+</macros>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/config.out.yml	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,37 @@
+analysis_name: analysis
+find_orientations:
+  orientation_maps:
+    file: analysis_ruby_eta-ome_maps.npz
+fit_grains:
+  do_fit: true
+  npdiv: 2
+  refit:
+  - 1.5
+  - 1.0
+  threshold: 25
+  tolerance:
+    eta:
+    - 2.0
+    omega:
+    - 1.0
+    tth:
+    - 3.0
+  tth_max: true
+image_series:
+  data:
+  - args: {}
+    file: imageseries/mruby-0129_000004_ff2_000012-cachefile.npz
+    panel: ff2
+  - args: {}
+    file: imageseries/mruby-0129_000004_ff1_000012-cachefile.npz
+    panel: ff1
+  format: frame-cache
+instrument: dexelas_id3a_20200130.yml
+material:
+  active: ruby
+  definitions: /Users/jj/gx/gh/ximg/galaxytools-2/tools/hedm_lattice_parameter_correction/test-data/materials_h5.h5
+  dmin: 1.0
+  min_sfac_ratio: 0.05
+  tth_width: 0.25
+multiprocessing: -1
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/config.yml	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,46 @@
+
+
+    
+analysis_name: analysis
+
+multiprocessing: -1
+
+material:
+  definitions: materials_h5.h5
+  active: ruby
+  dmin: 1.0  # defaults to 1.0 angstrom
+  tth_width: 0.25  # defaults to 0.25 degrees
+  min_sfac_ratio: 0.05  # min percentage of max |F|^2 to exclude; default None
+
+image_series:
+  format: frame-cache
+  data:
+    - file: imageseries/mruby-0129_000004_ff2_000012-cachefile.npz
+      args: {}
+      panel: ff2  # must match detector key
+    - file: imageseries/mruby-0129_000004_ff1_000012-cachefile.npz
+      args: {}
+      panel: ff1  # must match detector key
+
+instrument: dexelas_id3a_20200130.yml
+
+
+find_orientations:
+  orientation_maps:
+    # A file name must be specified. If it doesn't exist, one will be created
+    file: analysis_ruby_eta-ome_maps.npz
+
+fit_grains:
+  do_fit: true # if false, extracts grains but doesn't fit. defaults to true
+  # estimate: null
+  npdiv: 2 # number of polar pixel grid subdivisions, defaults to 2
+  threshold: 25
+
+  tolerance:
+    tth: [3.0] # tolerance lists must be identical length
+    eta: [2.0]
+    omega: [1.0]
+
+  refit: [1.5,1.0]
+  tth_max: true # true, false, or a non-negative value, defaults to true
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/dexelas_id3a_20200130.yml	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,102 @@
+beam:
+  energy: 41.9906
+  vector:
+    azimuth: 90.0
+    polar_angle: 90.0
+calibration_crystal:
+  0:
+    inv_stretch:
+    - 1.0
+    - 1.0
+    - 1.0
+    - 0.0
+    - 0.0
+    - 0.0
+    orientation:
+    - 1.2148784044413221
+    - 0.6945507909200794
+    - 0.765702046701113
+    position:
+    - 0.06060121926310595
+    - 0.1312120981443553
+    - -0.06694059935413639
+  1:
+    inv_stretch:
+    - 1.0
+    - 1.0
+    - 1.0
+    - 0.0
+    - 0.0
+    - 0.0
+    orientation:
+    - 1.4105172207115466
+    - -0.14336674130145605
+    - -0.37238882282338215
+    position:
+    - 0.016940740257706914
+    - -0.1183857552099456
+    - 0.34767784259162654
+  2:
+    inv_stretch:
+    - 1.0
+    - 1.0
+    - 1.0
+    - 0.0
+    - 0.0
+    - 0.0
+    orientation:
+    - 0.34356534660437343
+    - 0.8170405732053853
+    - -0.01649123635877074
+    position:
+    - 0.01052568572502354
+    - -0.11080094260256373
+    - -0.33689416855383947
+detectors:
+  ff1:
+    buffer:
+    - 2.0
+    - 2.0
+    pixels:
+      columns: 3072
+      rows: 3888
+      size:
+      - 0.0748
+      - 0.0748
+    saturation_level: 15800.0
+    transform:
+      tilt:
+      - 0.010486950176001908
+      - -0.006152055149709758
+      - 0.0014922461489979366
+      translation:
+      - 123.3706062259476
+      - 3.325955053634411
+      - -670.7508650125019
+  ff2:
+    buffer:
+    - 2.0
+    - 2.0
+    pixels:
+      columns: 3072
+      rows: 3888
+      size:
+      - 0.0748
+      - 0.0748
+    saturation_level: 15800.0
+    transform:
+      tilt:
+      - 0.00991389059922992
+      - -0.01010540645355334
+      - 0.00026733734489633163
+      translation:
+      - -123.5603890819458
+      - 3.186895450085356
+      - -671.04758940985
+id: dexelas_id3a_20200130
+oscillation_stage:
+  chi: 0.0007535217427322955
+  translation:
+  - 0.0
+  - 0.0
+  - 0.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/grains.out	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,4 @@
+# grain ID	completeness	chi^2	exp_map_c[0]	exp_map_c[1]	exp_map_c[2]	t_vec_c[0]	t_vec_c[1]	t_vec_c[2]	inv(V_s)[0,0]	inv(V_s)[1,1]	inv(V_s)[2,2]	inv(V_s)[1,2]*sqrt(2)	inv(V_s)[0,2]*sqrt(2)	inv(V_s)[0,1]*sqrt(2)	ln(V_s)[0,0]	ln(V_s)[1,1]	ln(V_s)[2,2]	ln(V_s)[1,2]	ln(V_s)[0,2]	ln(V_s)[0,1]	
+0	1.000000	5.369697e-04	1.4105338333495454e+00	-1.4357856278205916e-01	-3.7236476312134781e-01	1.4606505177249375e-02	-1.3140046151347134e-01	3.4060190489934772e-01	1.0003775982839029e+00	9.9944300862177005e-01	1.0002266996126810e+00	-2.0098254132149165e-05	-4.9238869081289537e-05	1.9534109219804900e-04	-3.7751686766115180e-04	5.5715620077280111e-04	-2.2667321347921970e-04	1.4211555470441899e-05	3.4805640272123023e-05	-1.3813916516906802e-04
+1	0.989130	7.511846e-04	1.2147550631708239e+00	6.9402347166184886e-01	7.6543118665132748e-01	5.9612267368878889e-02	1.1405034788195756e-01	-7.2183402157879217e-02	1.0004241448431779e+00	9.9933252120647686e-01	9.9997982101162919e-01	1.4989477496824173e-04	5.2684926604946789e-05	5.1080735497269734e-04	-4.2398900339413604e-04	6.6777254859082887e-04	2.0185504529689508e-05	-1.0602134697524727e-04	-3.7227202925975544e-05	-3.6123737394211304e-04
+2	1.000000	8.138777e-04	3.4401229493783497e-01	8.1672452573494558e-01	-1.6886951921517571e-02	1.3499398451489408e-02	-1.2236493959833296e-01	-3.3233691582841279e-01	9.9982579134269456e-01	9.9996756647100571e-01	1.0002294836828227e+00	3.1419240600766599e-04	-5.3475171509938853e-04	7.6955776196857811e-04	1.7444345183427053e-04	3.2606840387861337e-05	-2.2936117649414523e-04	-2.2224861045200161e-04	3.7817662691540954e-04	-5.4425783361271827e-04
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/lattice_parameter_correction.py	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+
+import argparse
+
+import numpy as np
+
+from hexrd import rotations as rot
+from hexrd import matrixutil as mutil
+from hexrd import config
+from hexrd import material
+
+
+def _lp_calc(v_mat, f_mat, r_mat):
+    """
+    do strained lappi
+
+    Parameters
+    ----------
+    vmat : TYPE
+        DESCRIPTION.
+    f_mat : TYPE
+        DESCRIPTION.
+    r_mat : TYPE
+        DESCRIPTION.
+
+    Returns
+    -------
+    lparams : TYPE
+        DESCRIPTION.
+
+    """
+    f_prime = np.dot(v_mat, np.dot(r_mat, f_mat))
+    f_prime_hat = mutil.unitVector(f_prime)
+
+    lp_mags = mutil.columnNorm(f_prime)
+
+    lp_angs = np.hstack(
+        [np.arccos(np.dot(f_prime_hat[:, 1], f_prime_hat[:, 2])),
+         np.arccos(np.dot(f_prime_hat[:, 2], f_prime_hat[:, 0])),
+         np.arccos(np.dot(f_prime_hat[:, 0], f_prime_hat[:, 1]))]
+    )
+    return np.hstack([lp_mags, np.degrees(lp_angs)])
+
+
+def extract_lattice_parameters(
+        cfg_file, grains_file,
+        min_completeness=0.90, force_symmetry=False,
+        materials_out=None
+        ):
+    """
+    """
+    cfg = config.open(cfg_file)[0]
+    mcfg = cfg.material
+    m = mcfg.materials[mcfg.active]
+    print(f'material.latticeParameters: {m.latticeParameters}')
+    print(f'material.planeData: {m.planeData}')
+
+    pd = cfg.material.plane_data
+    qsym_mats = rot.quatProductMatrix(pd.getQSym())
+
+    f_mat = pd.latVecOps['F']
+
+    gt = np.loadtxt(grains_file)
+
+    idx = gt[:, 1] >= min_completeness
+
+    expMaps = gt[idx, 3:6].T
+    vinv = gt[idx, 9:15]
+
+    lparms = []
+    for i in range(sum(idx)):
+        quat = rot.quatOfExpMap(expMaps[:, i].reshape(3, 1))
+        v_mat = np.linalg.inv(mutil.vecMVToSymm(vinv[i, :].reshape(6, 1)))
+        if force_symmetry:
+            for qpm in qsym_mats:
+                lparms.append(
+                    _lp_calc(v_mat, f_mat, rot.rotMatOfQuat(np.dot(qpm, quat)))
+                )
+        else:
+            lparms.append(
+                    _lp_calc(v_mat, f_mat, rot.rotMatOfQuat(quat))
+                )
+    lp_avg = np.average(np.vstack(lparms), axis=0)
+    if materials_out is not None:
+        m.latticeParameters = lp_avg 
+        print(f'material.latticeParameters: {m.latticeParameters}')
+        print(f'material.latticeParameters: {mcfg.materials[mcfg.active].latticeParameters}')
+        print(f'material.planeData: {m.planeData}')
+        material.save_materials_hdf5(materials_out, mcfg.materials) 
+    return lp_avg, sum(idx), len(idx)
+
+
+if __name__ == '__main__':
+    """
+    maybe make this smart enough to average properly for symmetry?
+    """
+    parser = argparse.ArgumentParser(
+        description="Extract average lattice parameters from grains.out"
+    )
+    parser.add_argument(
+        'cfg', help="ff-HEDM config YAML file", type=str
+        )
+    parser.add_argument(
+        'grains_file', help="grains.out file", type=str
+    )
+    parser.add_argument(
+        '-m', '--min-completeness', help="completeness threshold", type=float,
+        default=0.75
+    )
+    parser.add_argument(
+        '-f', '--force-symmetry',
+        help="symmetrize results",
+        action="store_true"
+    )
+    parser.add_argument(
+        '-a', '--average_lattice', default="None", help="average lattice file"
+    )
+    parser.add_argument(
+        '-o', '--output', default="None", help="updated material file"
+    )
+    args = parser.parse_args()
+
+    cfg_file = args.cfg
+    grains_file = args.grains_file
+    min_completeness = args.min_completeness
+    force_symmetry = args.force_symmetry
+    materials_out = args.output
+
+    lp_avg, n_used, n_total = extract_lattice_parameters(
+        cfg_file, grains_file,
+        min_completeness=min_completeness, force_symmetry=force_symmetry,
+        materials_out=materials_out  
+    )
+    print("Using %d grains out of %d above %.2f%% completeness threshold:"
+          % (n_used, n_total, 100*min_completeness))
+    print("---------------------------------------------------------------")
+    print("a       \tb       \tc       "
+          + "\talpha   \tbeta    \tgamma \n"
+          + "%.6f\t%.6f\t%.6f\t%.6f\t%.6f\t%.6f" % tuple(lp_avg))
+    if args.average_lattice:
+        with open(args.average_lattice,'w') as fh:
+            print("%.6f\t%.6f\t%.6f\t%.6f\t%.6f\t%.6f" % tuple(lp_avg), file=fh)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/lp_avg.tsv	Thu May 19 16:48:22 2022 +0000
@@ -0,0 +1,1 @@
+4.758953	4.759407	12.998517	90.015292	90.002978	120.001436
Binary file test-data/m_out.h5 has changed
Binary file test-data/materials_h5.h5 has changed