def test_parseKeywordLineShorthandEq(self):
     line = "key = @(2*5) 2*10 10 @(4+6) 10"
     expect = [('key', [
         i + 1
     ], (Equation("2*5") if i == 0 else Equation("4+6") if i == 4 else 10))
               for i in range(6)]
     self._test(line, expect)
Exemple #2
0
    def new_default_region(self, region):
        f = ['xmin', 'ymin', 'zmin']
        t = ['xmax', 'ymax', 'zmax']
        typ = 'box'
        if region == 'left':
            t[0] = 'xmin'
            typ = 'YZ-plane'
        elif region == 'right':
            f[0] = 'xmax'
            typ = 'YZ-plane'
        elif region == 'top':
            f[1] = 'ymax'
            typ = 'XZ-plane'
        elif region == 'bottom':
            t[1] = 'ymin'
            typ = 'XZ-plane'
        elif region == 'front':
            f[2] = 'zmax'
            typ = 'XY-plane'
        elif region == 'back':
            t[2] = 'zmin'
            typ = 'XY-plane'

        # convert strings to equations
        extents = [[Equation(e) for e in f], [Equation(e) for e in t]]
        self.new_region(region, extents, typ)
    def test_update_eq(self):
        kw = Keyword('key', Equation('2*4'))

        self.assertIsInstance(kw.value, Equation)

        kw.updateValue('10*2')
        self.assertIsInstance(kw.value, Equation)
        self.assertEqual(20.0, float(kw))
    def parameters(self):
        param_dict = OrderedDict()
        data = self.parameters_dict
        param_names = [val['parameter'] for val in data.values()]
        for k, v in data.items():
            type_ = v['type']
            value = v['value']
            param_name = v['parameter']
            param_value = str(value)

            if (type_ in ('float', 'integer') and
                (re_math.search(param_value) or any(p in param_value
                                                    for p in param_names))):
                param_value = Equation(param_value)
            elif type_ == 'float':
                param_value = float(value)
            elif type_ == 'integer':
                param_value = int(value)

            param_dict[param_name] = param_value

        return param_dict
 def test_e(self):
     eq = Equation("5*e + pi")
     self.assertAlmostEqual(float(eq), 5 * math.e + math.pi)
 def test_pi(self):
     eq = Equation('10*pi / 15')
     self.assertAlmostEqual(float(eq), 10 * math.pi / 15)
 def test_pow(self):
     eq = Equation('10**2')
     self.assertEqual(float(eq), 100)
Exemple #8
0
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)
        self.parent = parent
        self.parameter_key_map = {}

        get_ui('regions.ui', self)

        self.extent_lineedits = [
            self.lineedit_regions_from_x, self.lineedit_regions_to_x,
            self.lineedit_regions_from_y, self.lineedit_regions_to_y,
            self.lineedit_regions_from_z, self.lineedit_regions_to_z
        ]
        for ext in self.extent_lineedits:
            ext.allow_parameters = True
            ext.dtype = float
            ext.help_text = 'Physical coordinates describing the bounds of the region.'

        self.lineedit_regions_name.help_text = 'Name of the region. Used througout the gui to reference the region'
        self.combobox_stl_shape.help_text = 'Shape to be used to select facets.'
        self.checkbox_slice_facets.help_text = 'Slice facets if the facet is on the selection boundary.'
        for wid in [
                self.lineedit_filter_x, self.lineedit_filter_y,
                self.lineedit_filter_z
        ]:
            wid.allow_parameters = True
            wid.help_text = 'Vector to filter facets with. If the facet '\
                            'normal is the same as the vector, then the facet'\
                            ' will be added to the region, else discarded.'
        self.lineedit_deviation_angle.allow_parameters = True
        self.lineedit_deviation_angle.help_text = 'Angle to provide a'\
            'tolerence to the filtering of facets based on the facet normal.'

        self.toolbutton_region_add.clicked.connect(self.new_region)
        self.toolbutton_region_delete.clicked.connect(self.delete_region)
        self.toolbutton_region_delete.setEnabled(False)  #Need a selection
        self.toolbutton_region_copy.clicked.connect(self.copy_region)
        self.toolbutton_region_copy.setEnabled(False)  #Need a selection
        self.toolbutton_color.clicked.connect(self.change_color)

        tablewidget = self.tablewidget_regions
        tablewidget.dtype = OrderedDict
        tablewidget._setModel()
        tablewidget.set_selection_model()
        tablewidget.set_value(OrderedDict())
        tablewidget.set_columns(['visible', 'color', 'type', 'used by'])
        tablewidget.show_vertical_header(True)
        tablewidget.auto_update_rows(True)
        tablewidget.new_selection.connect(self.update_region_parameters)
        tablewidget.clicked.connect(self.cell_clicked)
        tablewidget.default_value = OrderedDict()
        self.inhibit_toggle = True

        self.widget_region_parameters.setEnabled(False)
        for widget in widget_iter(self.widget_region_parameters):
            if hasattr(widget, 'value_updated'):
                widget.value_updated.connect(self.region_value_changed)

                # example <name>: lineedit_regions_to_x
                name = str(widget.objectName())

                # set extent limits
                if '_to_' in name or '_from_' in name:
                    kinfo = name.split('_')
                    widget.key = '_'.join(kinfo[-2:])
                    widget.dtype = float
                    widget.setValInfo(max=Equation(kinfo[-1] + 'max'),
                                      min=Equation(kinfo[-1] + 'min'))
                elif 'name' in name:
                    widget.key = 'name'
                    widget.dtype = str
                elif 'stl_shape' in name:
                    widget.key = 'stl_shape'
                    widget.dtype = str
                elif 'slice' in name:
                    widget.key = 'slice'
                    widget.dtype = bool
                elif 'filter' in name:
                    widget.key = '_'.join(name.split('_')[-2:])
                    widget.dtype = float
                elif 'deviation_angle' in name:
                    widget.key = 'deviation_angle'
                    widget.dtype = float
                elif 'equilibrant' in name:
                    widget.key = 'equilibrant'
                    widget.dtype = bool
                elif 'invert' in name:
                    widget.key = 'invert'
                    widget.dtype = bool
        self.error = self.parent.error
        self.warning = self.warn = self.parent.warn

        self.checkbox_selectfacets.clicked.connect(self.stl_type_changed)
        self.checkbox_selectfacets.toggled.connect(
            lambda c: self.groupbox_stl.setEnabled(c))
        self.groupbox_filter_facets.clicked.connect(self.filter_facets_changed)

        # default region buttons
        for btn, region in [(self.toolbutton_region_a, 'all'),
                            (self.toolbutton_region_l, 'left'),
                            (self.toolbutton_region_r, 'right'),
                            (self.toolbutton_region_t, 'top'),
                            (self.toolbutton_region_b, 'bottom'),
                            (self.toolbutton_region_f, 'front'),
                            (self.toolbutton_region_back, 'back')]:
            btn.clicked.connect(
                lambda ignore, r=region: self.new_default_region(r))
            btn.setIcon(get_icon(region + '_region.svg'))
            btn.setToolTip(region)
            btn.setIconSize(sub_icon_size())
    def test_eq(self):
        eq = Equation('2*5')
        kw = Keyword('key', eq)

        self.assertEqual(10.0, float(eq))
        self.assertEqual(10.0, float(kw))
 def test_mul(self):
     eq = Equation('2*5')
     self.assertEqual(float(eq), 10.0)
 def test_parseKeywordLine_eq_divide(self):
     line = "key = @( 2/ 3)"
     expect = [('key', [], Equation('2/3'))]
     self._test(line, expect)
 def test_parseKeywordLine_eq_mul_wspace(self):
     line = "key = @( 2*3)"
     expect = [('key', [], Equation('2*3'))]
     self._test(line, expect)
 def test_sub(self):
     eq = Equation('2-5')
     self.assertEqual(float(eq), -3.0)
 def test_add(self):
     eq = Equation('2+5')
     self.assertEqual(float(eq), 7.0)
 def test_parens(self):
     eq = Equation("(1) + (2+3) * (5 - (6+7)) / 137.0")
     self.assertAlmostEqual(float(eq), 1 + (-40 / 137.0))
 def test_sin(self):
     eq = Equation("sin(pi)")
     self.assertAlmostEqual(float(eq), math.sin(math.pi))
 def test_parseKeywordLine_eq_pi(self):
     line = "key = @( 2*pi)"
     expect = [('key', [], Equation('2*pi'))]
     self._test(line, expect)
 def test_div(self):
     eq = Equation('10/2')
     self.assertEqual(float(eq), 5.0)
 def test_parseKeywordLine_shorthand_eq_3(self):
     line = "key =  @(5+ 5) 10 2*10 @(5+5) @ (5 + 5) "
     expect = [('key', [i + 1], 10 if 0 < i < 4 else Equation("5+5"))
               for i in range(6)]
     self._test(line, expect)
Exemple #20
0
    def extract_regions(self, proj, proj_dir=None):
        """ extract regions from IC, BC, PS, IS, VTK"""
        if self.tablewidget_regions.value:
            # We assume regions_dict has been initialized correctly
            # from mfix_gui_comments.
            return

        stl_files = []
        stl_nums = []
        if proj_dir:
            # look for geometry_#####.stl files
            stl_files = glob.glob(os.path.join(proj_dir, 'geometry_*.stl'))
            # extract numbers
            stl_nums = [
                safe_int(f.split('.')[0].split('_')[-1]) for f in stl_files
            ]

        for prefix, conds in (('ic_', proj.ics), ('bc_', proj.bcs),
                              ('is_', proj.iss), ('ps_', proj.pss),
                              ('vtk_', proj.vtks)):  # XXX TODO monitors?
            for cond in conds:
                extents = []
                extents_keys = False
                for key in ('x_w', 'x_e', 'y_s', 'y_n', 'z_b', 'z_t'):
                    key = prefix + key
                    if key in cond:
                        extents.append(float(cond[key]))
                        extents_keys = True
                    else:
                        extents.append(0.0)

                # reformat extents
                extents = [extents[::2], extents[1::2]]

                # infer region type from extents
                rtype = self.get_region_type(extents)

                # create a name
                name = prefix.upper() + str(cond.ind)  # "BC_1"

                add = False
                # handle CG_* regions
                if ('bc_type' in cond
                        and cond['bc_type'].value.lower().startswith('cg')):
                    rtype = 'STL'
                    add = True
                    # single stl bc, assume region fills domain
                    if cond.ind == proj.get_value('stl_bc_id'):
                        ext = [
                            Equation(s) for s in
                            ['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']
                        ]
                        extents = [ext[::2], ext[1::2]]
                        for key, value in [('from', ext[::2]),
                                           ('to', ext[1::2])]:
                            for v, k in zip(value, ['x', 'y', 'z']):
                                self.update_parameter_map(
                                    v, name, '_'.join([key, k]))
                    else:
                        if cond.ind in stl_nums:
                            extents = self.vtkwidget.get_stl_extents(
                                stl_files[stl_nums.index(cond.ind)])
                            # reformat extents
                            extents = [extents[::2], extents[1::2]]

                # if extents are not already used by a region, add it
                elif not self.check_extents_in_regions(
                        extents) and extents_keys:
                    add = True
                elif not extents_keys:
                    self.warn(
                        '{} does not have extents defined and is not a cartesian grid, ignoring'
                        .format(name))
                #else: # XXX FIXME this happens when regions are shared
                #    self.warn('could not infer region from {}'.format(name))

                if add:
                    self.new_region(name, extents, rtype, defer_update=True)
 def test_parseKeywordLine_shorthand_eq_exp(self):
     line = "key = 4*6.7 @(1*6.7)"
     expect = [('key', [i + 1], 6.7)
               for i in range(4)] + [('key', [5], Equation("1*6.7"))]
     self._test(line, expect)
Exemple #22
0
    def value(self):
        text = self.text().strip()
        parameters = VALID_EXP_NAMES + sorted(list(PARAMETER_DICT.keys()))
        if len(text) == 0:
            if self.required:
                self.report_value_required(self.key)
                return self.updateValue(None, self.saved_value)
            else:  # should we return None?
                return ''

        if self.dtype is str:
            return text
        elif self.dtype is float:
            if re_float.match(text) or re_int.match(text):
                try:
                    f = float(text)
                except ValueError as e:  # Should not really happen, unless our regexes are bad
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)
                try:
                    self.check_range(f)
                    self.saved_value = f
                    return f
                except ValueError as e:
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)
            elif re_float_exp.match(text):
                try:
                    f = make_FloatExp(text)
                except ValueError as e:
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)
                try:
                    self.check_range(f)
                    self.saved_value = f
                    return f
                except ValueError as e:
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)
            elif re_math.search(text) or any(par in text
                                             for par in parameters):
                if text.startswith('@(') and text.endswith(')'):
                    text = text[2:-1]
                try:
                    eq = Equation(text, dtype=float)
                except ValueError as e:
                    self.report_value_error("Failed to create equation: %s" %
                                            e)
                    return self.updateValue(None, self.saved_value)
                try:
                    f = float(eq)
                except ValueError as e:
                    self.report_value_error("Failed to evaluate equation: %s" %
                                            e)
                    return self.updateValue(None, self.saved_value)
                try:
                    self.check_range(f)
                    self.saved_value = eq
                    return eq
                except ValueError as e:
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)
            else:
                return self.updateValue(None, self.saved_value)

        elif self.dtype is int:
            if re_math.search(text) or any(par in text for par in parameters):
                # integer equations?  do we use this?
                try:
                    eq = Equation(text, dtype=int)
                except ValueError as e:
                    self.report_value_error("Failed to create equation: %s" %
                                            e)
                    return self.updateValue(None, self.saved_value)
                try:
                    i = int(eq)
                except ValueError as e:
                    self.report_value_error("Failed to evaluate equation: %s" %
                                            e)
                    return self.updateValue(None, self.saved_value)
                try:
                    self.check_range(i)
                    self.saved_value = eq
                    return eq
                except ValueError as e:
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)
            else:
                float_val = None
                int_val = None
                try:
                    float_val = float(text)
                    int_val = int(float_val)
                except ValueError as e:
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)
                try:
                    self.check_range(int_val)
                    self.saved_value = int_val
                    if int_val != float_val:
                        return self.updateValue(None, int_val)
                    else:
                        return int_val
                except ValueError as e:
                    self.report_value_error(e)
                    return self.updateValue(None, self.saved_value)

        else:
            raise TypeError(self.dtype)
Exemple #23
0
    def setup_dem_tab(self):
        # Ensures all constraints (items enabled/disabled) are set
        # called by each 'set_' function, so don't call those here

        ui = self.ui.solids
        # Inline comments from MFIX-UI_SRS as of 2016-07-01
        #  Please update as needed!

        #MFIX-UI_SRS
        #Enable automatic particle generation
        # Enabled sets keyword GENER_PART_CONFIG to true
        # Disabled enables the user to specify number of entries in particle input file
        # Default value is 0
        default = 0
        # Sets keyword PARTICLES
        gener_part_config = self.project.get_value('gener_part_config')
        particles = self.project.get_value('particles')

        if gener_part_config:
            if particles:  # Should not both be set
                self.warning("gener_part_config set, particles=%s" % particles)
                particles = default
        else:
            if particles is None:  # set to 0 if not set
                particles = default
            elif particles < 0:
                self.warning("Invalid particles %s" % particles)
                particles = default
        self.update_keyword('particles', particles)

        enabled = not gener_part_config
        for item in (ui.label_particles, ui.lineedit_keyword_particles):
            item.setEnabled(enabled)

        #Select numerical integration method
        # Selection always available
        # Available selections
        #Euler [DEFAULT]
        # Selection always available
        # Sets keyword DES_INTG_METHOD to 'EULER'
        #Adams-Bashforth
        # Selection always available
        # Sets keyword DES_INTG_METHOD to 'ADAMS_BASHFORTH'1
        des_intg_method = self.project.get_value('des_intg_method',
                                                 default='EULER')
        if des_intg_method not in des_intg_methods:
            self.warn("Invalid des_intg_method %s" % des_intg_method)
            des_intg_method = 'EULER'
        ui.combobox_des_intg_method.setCurrentIndex(
            des_intg_methods.index(des_intg_method))

        #Selection collision model
        # Selection always available
        # Available selections
        #Linear Spring-Dashpot [DEFAULT]
        # Selection always available
        # Sets keyword DES_COLL_MODEL to 'LSD'
        #Hertzian
        # Selection always available
        # Sets keyword DES_COLL_MODEL to 'HERTZIAN'
        des_coll_model = self.project.get_value('des_coll_model',
                                                default='LSD')
        if des_coll_model not in des_coll_models:
            self.warn("Invalid des_coll_model %s" % des_coll_model)
            des_coll_model = 'LSD'
        ui.combobox_des_coll_model.setCurrentIndex(
            des_coll_models.index(des_coll_model))

        #Select gas-solids coupling scheme:
        # Selection unavailable if fluid model is disabled
        # Available selections:
        # One-way Coupled
        # Selection always available
        # Sets keyword DES_ONEWAY_COUPLED true
        # Fully Coupled
        # Selection always available
        # Sets keyword DES_ONEWAY_COUPLED false
        enabled = not self.fluid_solver_disabled
        for item in (ui.label_coupling_method, ui.combobox_coupling_method):
            item.setEnabled(enabled)
        des_oneway_coupled = self.project.get_value('des_oneway_coupled',
                                                    default=False)
        if des_oneway_coupled not in (True, False):
            self.warn("Invalid des_oneway_coupled %s" % des_oneway_coupled)
            des_oneway_coupled = False
            self.update_keyword('des_oneway_coupled', des_oneway_coupled)
        ui.combobox_coupling_method.setCurrentIndex(
            0 if des_oneway_coupled else 1)

        #Optional to enable explicitly coupled simulation
        # Unavailable for GARG_2012 interpolation
        des_interp_scheme = self.project.get_value('des_interp_scheme')
        enabled = (des_interp_scheme != 'GARG_2012')
        ui.checkbox_keyword_des_explicitly_coupled.setEnabled(enabled)

        #Select interpolation framework:
        # Selection always available
        # Available selections:
        # Field-to-Particle and Particle-to-Field [DEFAULT]
        #  Sets keyword DES_INTERP_ON to true
        #  Sets keyword DES_INTERP_MEAN_FIELDS to true
        # Field-to-Particle only
        #  Sets keyword DES_INTERP_ON to true
        #  Sets keyword DES_INTERP_MEAN_FIELDS to false
        # Particle-to-Field only
        #  Sets keyword DES_INTERP_ON to false
        #  Sets keyword DES_INTERP_MEAN_FIELDS to true
        # No Interpolation
        #  Sets keyword DES_INTERP_ON to false
        #  Sets keyword DES_INTERP_MEAN_FIELDS to false
        #
        # issues/116 must also set DES_INTERP_SCHEME to None when no-interpolation
        des_interp_on = self.project.get_value('des_interp_on', default=True)
        if des_interp_on not in (True, False):
            self.warn("Invalid des_interp_on %s" % des_interp_on)
            des_interp_on = True
            self.update_keyword('des_interp_on', des_interp_on)

        des_interp_mean_fields = self.project.get_value(
            'des_interp_mean_fields', default=True)
        if des_interp_mean_fields not in (True, False):
            self.warn("Invalid des_interp_mean_fields %s" %
                      des_interp_mean_fields)
            des_interp_mean_fields = True
            self.update_keyword('des_interp_mean_fields',
                                des_interp_mean_fields)

        index = 2 * (1 - des_interp_on) + (1 - des_interp_mean_fields)
        ui.combobox_des_interp.setCurrentIndex(index)

        #Select interpolation scheme:
        # Selection available except when no-interpolation framework is selected
        # Available selections:
        #  None [locked default for no-interpolation framework]
        #  Selection always available
        #  Sets keyword DES_INTERP_SCHEME='NONE'
        # Garg 2012
        #  Selection not available with explicit coupling enabled
        #  Sets keyword DES_INTERP_SCHEME='GARG_2012'
        # Square DPVM
        #  Selection always available
        #  Requires an interpolation width, DES_INTERP_WIDTH
        #  Sets keyword DES_INTERP_SCHEME='SQUARE_DPVM'
        #
        cb = ui.combobox_des_interp_scheme
        label = ui.label_des_interp_scheme
        des_interp_scheme = self.project.get_value('des_interp_scheme')
        des_explicitly_coupled = self.project.get_value(
            'des_explicitly_coupled')
        interp_enabled = des_interp_on or des_interp_mean_fields  # not no-interp
        for item in (cb, label):
            item.setEnabled(interp_enabled)
        if not interp_enabled:
            cb.setCurrentIndex(NONE)
            des_interp_scheme = 'NONE'
        else:
            if des_interp_scheme not in des_interp_schemes:
                des_interp_scheme = 'NONE'
            cb.setCurrentIndex(des_interp_schemes.index(des_interp_scheme))

        #
        # per-item enable flags
        enabled = (True, True, not des_explicitly_coupled)
        for (i, e) in enumerate(enabled):
            set_item_enabled(get_combobox_item(cb, i), e)
        # Make sure we don't leave the combobox on an invalid item!
        if not enabled[cb.currentIndex()]:
            idx = enabled.index(True)  # 2 is always True so this is safe
            cb.setCurrentIndex(idx)
            des_interp_scheme = des_interp_schemes[idx]
        # Value of des_interp_scheme may have changed
        self.update_keyword('des_interp_scheme', des_interp_scheme)

        #Define interpolation width (DPVM only) (required)
        # Specification only available with SQUARE_DPVM interpolation scheme
        # Sets keyword DES_INTERP_WIDTH
        # TODO default?
        key = 'des_interp_width'
        enabled = interp_enabled and (des_interp_scheme == 'SQUARE_DPVM')  #?
        for item in (ui.label_des_interp_width,
                     ui.lineedit_keyword_des_interp_width,
                     ui.label_des_interp_width_units):
            item.setEnabled(enabled)
        if des_interp_scheme != 'SQUARE_DPVM':
            self.unset_keyword(key)
        else:
            self.update_keyword(key,
                                ui.lineedit_keyword_des_interp_width.value)

        #Option to enable diffusion of particle data
        # Selection unavailable with GARG_2012 interpolation scheme
        # No keyword is set by this option
        # Enables the user to specify a diffusion width
        # Sets keyword DES_DIFFUSE_WIDTH
        key = 'des_diffuse_width'
        enabled = (des_interp_scheme != 'GARG_2012')
        ui.checkbox_enable_des_diffuse_width.setEnabled(enabled)
        if not enabled:
            ui.checkbox_enable_des_diffuse_width.setChecked(False)
            self.unset_keyword(key)
            ui.lineedit_keyword_des_diffuse_width.clear()  # ??? FIXME
        enabled = ui.checkbox_enable_des_diffuse_width.isChecked()
        for item in (ui.label_des_diffuse_width,
                     ui.lineedit_keyword_des_diffuse_width,
                     ui.label_des_diffuse_width_units):
            item.setEnabled(enabled)
            if enabled:
                self.update_keyword(
                    key, ui.lineedit_keyword_des_diffuse_width.value)

        #Specify friction coefficient
        # Specification always required
        # Sets keyword MEW (MEW_W)
        pass

        #Specify normal spring constant
        # Only available for LSD collision model
        # Sets keyword KN (KN_W)
        #Specify tangential spring constant factor
        # Only available for LSD collision model
        # Sets keyword KT_FAC (KT_W_FAC)
        # Default values of 2.0/7.0
        #Specify tangential damping coefficient factor
        # Only available for LSD collision model
        # Sets keyword DES_ETAT_FAC (DES_ETAT_W_FAC)
        # Default values of 0.5
        enabled = (des_coll_model == 'LSD')
        for item in (ui.label_kn, ui.lineedit_keyword_kn,
                     ui.lineedit_keyword_kn_w, ui.label_kn_units,
                     ui.label_kt_fac, ui.lineedit_keyword_kt_fac,
                     ui.lineedit_keyword_kt_w_fac, ui.label_des_etat_fac,
                     ui.lineedit_keyword_des_etat_fac,
                     ui.lineedit_keyword_des_etat_w_fac):
            item.setEnabled(enabled)

        if enabled:  # TODO set these defaults at load-time, not when this tab is shown
            for (key, default) in [('kt_fac', Equation('2/7')),
                                   ('kt_w_fac', Equation('2/7')),
                                   ('des_etat_fac', 0.5),
                                   ('des_etat_w_fac', 0.5)]:
                if self.project.get_value(key) is None:
                    self.update_keyword(key, default)

        # Unset keywords if not enabled?
        #Specify Young's modulus
        # Only available for Hertzian collision model
        # Sets keyword E_YOUNG (EW_YOUNG)

        layout = ui.gridlayout_dem_parameters
        enabled = (des_coll_model == 'HERTZIAN')
        for item in (ui.label_e_young, ui.lineedit_keyword_ew_young,
                     ui.label_e_young_units, ui.label_v_poisson,
                     ui.lineedit_keyword_vw_poisson):
            item.setEnabled(enabled)

        key = 'e_young'
        solids_names = list(self.solids.keys())
        if self.solids_dem_saved_solids_names != solids_names:

            # We put these back after inserting rows
            for item in (ui.label_v_poisson, ui.lineedit_keyword_vw_poisson):
                item.hide()
                layout.removeWidget(item)

            # Delete all the old ones...
            for idx in range(layout.count() - 1, -1, -1):
                item = layout.itemAt(idx)
                w = item.widget()
                if not w:
                    continue
                name = w.objectName()
                if '_args_' in name:
                    w.hide()
                    if isinstance(w, LineEdit):
                        self.project.unregister_widget(w)
                    layout.removeWidget(w)
                    w.setParent(None)
                    w.deleteLater()

            # ...and make new ones
            idx = layout.indexOf(ui.lineedit_keyword_ew_young)
            columns = 4
            row = 1 + int(idx / columns)
            for (p, name) in enumerate(self.solids.keys(), 1):
                row += 1
                label = QLabel('    ' + name)
                label.setObjectName('label_%s_args_%s' % (key, p))
                # FIXME use dynamic_widgets instead of setattr
                setattr(ui, label.objectName(), label)
                label.args = [p]
                self.add_tooltip(label, key)
                layout.addWidget(label, row, 0, 1, 1)
                label.setEnabled(enabled)

                le = LineEdit()
                le.setMaximumWidth(150)  # matches ui file
                le.key = key
                le.args = [p]
                le.dtype = float
                le.setValInfo(min=0.0)
                le.setObjectName('lineedit_keyword_%s_args_%s' % (key, p))
                setattr(ui, le.objectName(), le)
                self.add_tooltip(le, key)
                layout.addWidget(le, row, 1, 1, 1)
                le.setEnabled(enabled)
                val = self.project.get_value(key, args=[p])
                if val is not None:
                    le.updateValue(key, val)
                self.project.register_widget(le, keys=[key], args=[p])

                label = QLabel('Pa')
                label.setObjectName('label_%s_units_args_%s' % (key, p))
                layout.addWidget(label, row, 3, 1, 1)
                label.setEnabled(enabled)

            #Specify Poisson ratio:
            # Only available for Hertzian collision model
            # Sets keyword V_POISSON (VW_POISSON)
            row += 1
            layout.addWidget(ui.label_v_poisson, row, 0, 1, 1)
            layout.addWidget(ui.lineedit_keyword_vw_poisson, row, 2, 1, 1)
            for item in (ui.label_v_poisson, ui.lineedit_keyword_vw_poisson):
                item.show()
                item.setEnabled(enabled)

            row += 1
            key = 'v_poisson'

            for (p, name) in enumerate(self.solids.keys(), 1):
                row += 1
                label = QLabel('    ' + name)
                label.setObjectName('label_%s_args_%s' % (key, p))
                setattr(ui, label.objectName(), label)
                label.args = [p]
                self.add_tooltip(label, key)
                layout.addWidget(label, row, 0, 1, 1)
                label.setEnabled(enabled)
                le = LineEdit()
                le.setMaximumWidth(150)  #?
                le.key = key
                le.args = [p]
                le.dtype = float
                le.setValInfo(min=0.0)
                le.setObjectName('lineedit_keyword_%s_args_%s' % (key, p))
                setattr(ui, le.objectName(), le)
                self.add_tooltip(le, key)
                layout.addWidget(le, row, 1, 1, 1)
                le.setEnabled(enabled)
                val = self.project.get_value(key, args=[p])
                if val is not None:
                    le.updateValue(key, val)
                self.project.register_widget(le, keys=[key], args=[p])

        # Use dynamic_widgets here instead of setattr/getattr
        for key in 'e_young', 'v_poisson':
            for p in range(1, 1 + len(self.solids.keys())):
                for pat in 'label_%s_args_%s', 'lineedit_keyword_%s_args_%s':
                    w = getattr(ui, pat % (key, p), None)
                    if w:
                        w.setEnabled(enabled)

        #Specify normal restitution coefficient
        # Specification always required
        # Sets keyword DES_EN_INPUT (DES_EN_WALL_INPUT)
        # Input given as an upper triangular matrix
        mmax = self.project.get_value('mmax', default=len(self.solids))
        tw = ui.tablewidget_des_en_input

        def make_item(str):
            item = QTableWidgetItem(str)
            set_item_noedit(item)
            set_item_enabled(item, False)
            return item

        if (self.solids_dem_saved_solids_names != solids_names
                or tw.rowCount() != mmax + 1 or tw.columnCount() != mmax):

            # Clear out old lineedit widgets
            for row in range(tw.rowCount()):
                for col in range(tw.columnCount()):
                    w = tw.cellWidget(row, col)
                    if w:
                        self.project.unregister_widget(w)
                        w.deleteLater()
            tw.clearContents()

            # Make a new batch
            tw.setRowCount(mmax + 1)  # extra row for "Wall"
            tw.setColumnCount(mmax)
            tw.setHorizontalHeaderLabels(solids_names)
            tw.setVerticalHeaderLabels(solids_names + ['Wall'])

            arg = 1  # One-based
            key = 'des_en_input'
            for row in range(mmax):
                for col in range(mmax):
                    if col < row:
                        tw.setItem(row, col, make_item('--'))
                    else:
                        le = LineEdit()
                        le.setMaximumWidth(150)
                        le.key = key
                        le.args = [arg]
                        le.setdtype('dp')
                        self.add_tooltip(le, key)
                        tw.setCellWidget(row, col, le)
                        val = self.project.get_value(key, args=[arg])
                        if val is not None:
                            le.updateValue(key, val)
                        self.project.register_widget(le,
                                                     keys=[key],
                                                     args=[arg])
                        arg += 1
            arg = 1
            key = 'des_en_wall_input'
            row = mmax
            for col in range(mmax):
                le = LineEdit()
                le.setMaximumWidth(150)
                le.key = key
                le.args = [arg]
                le.setdtype('dp')
                self.add_tooltip(le, key)
                tw.setCellWidget(row, col, le)
                val = self.project.get_value(key, args=[arg])
                if val is not None:
                    le.updateValue(key, val)
                self.project.register_widget(le, keys=[key], args=[arg])
                arg += 1

        self.fixup_solids_table(tw, stretch_column=mmax - 1)
        # This makes the table look a little nicer
        tw.setShowGrid(False)
        # Move column headers to left so they line up with lineedits
        for i in range(tw.columnCount()):
            item = tw.horizontalHeaderItem(i)
            if item:
                item.setTextAlignment(Qt.AlignLeft)

        #Specify tangential restitution coefficient
        # Specification available for Hertzian collision model
        # Sets keyword DES_ET_INPUT (DES_ET_WALL_INPUT)
        # Input given as an upper triangular matrix
        enabled = (des_coll_model == 'HERTZIAN')
        ui.label_des_et_input.setEnabled(enabled)
        tw = ui.tablewidget_des_et_input
        # note - this is too much of a duplicate of des_en_input above
        if not enabled:
            # Clear out old lineedit widgets
            for row in range(tw.rowCount()):
                for col in range(tw.columnCount()):
                    w = tw.cellWidget(row, col)
                    if w:
                        self.project.unregister_widget(w)
                        w.deleteLater()
            tw.clearContents()
            tw.setRowCount(0)
            tw.setColumnCount(0)

        if enabled:
            if (self.solids_dem_saved_solids_names != solids_names
                    or tw.rowCount() != mmax + 1 or tw.columnCount() != mmax):

                # Clear out old lineedit widgets
                for row in range(tw.rowCount()):
                    for col in range(tw.columnCount()):
                        w = tw.cellWidget(row, col)
                        if w:
                            self.project.unregister_widget(w)
                            w.deleteLater()
                tw.clearContents()
                # Make a new batch
                tw.setRowCount(mmax + 1)  # extra row for "Wall"
                tw.setColumnCount(mmax)
                tw.setHorizontalHeaderLabels(solids_names)
                tw.setVerticalHeaderLabels(solids_names + ['Wall'])

                arg = 1
                key = 'des_et_input'
                for row in range(mmax):
                    for col in range(mmax):
                        if col < row:
                            tw.setItem(row, col, make_item('--'))
                        else:
                            le = LineEdit()
                            le.setMaximumWidth(150)
                            le.key = key
                            le.args = [arg]
                            le.setdtype('dp')
                            self.add_tooltip(le, key)
                            tw.setCellWidget(row, col, le)
                            val = self.project.get_value(key, args=[arg])
                            if val is not None:
                                le.updateValue(key, val)
                            self.project.register_widget(le,
                                                         keys=[key],
                                                         args=[arg])
                            arg += 1
                key = 'des_et_wall_input'
                row = mmax
                arg = 1
                for col in range(mmax):
                    le = LineEdit()
                    le.setMaximumWidth(150)
                    le.key = key
                    le.args = [arg]
                    le.setdtype('dp')
                    tw.setCellWidget(row, col, le)
                    val = self.project.get_value(key, args=[arg])
                    if val is not None:
                        le.updateValue(key, val)
                    self.project.register_widget(le, keys=[key], args=[arg])
                    arg += 1
        self.fixup_solids_table(tw, stretch_column=mmax - 1)
        # This makes the table look a little nicer
        tw.setShowGrid(False)
        # Move column headers to left so they line up with lineedits
        for i in range(tw.columnCount()):
            item = tw.horizontalHeaderItem(i)
            if item:
                item.setTextAlignment(Qt.AlignLeft)

        #Select cohesion model
        # Selection always available
        # Available selections
        #None [DEFAULT]
        #Selection always available
        #Sets keyword USE_COHESION to false
        #Sets keyword VAN_DER_WAALS to false
        #Van der Waals
        #Selection always available
        #Sets keyword USE_COHESION to true
        #Sets keyword VAN_DER_WAALS to true
        use_cohesion = self.project.get_value('use_cohesion')
        van_der_waals = self.project.get_value('van_der_waals')
        cb = ui.combobox_cohesion_model
        if use_cohesion:
            if not van_der_waals:
                self.warn('inconsistent value for keyword van_der_waals')
                self.unset_keyword('van_der_waals')
            cb.setCurrentIndex(1)
        else:
            if van_der_waals:
                self.warn('inconsistent value for keyword van_der_waals')
                self.update_keyword('van_der_waals', True)
            cb.setCurrentIndex(0)

        #Specify Hamaker constant
        # Specification only available for Van der Waals cohesion model
        # Sets keyword HAMAKER_CONSTANT (WALL_HAMAKER_CONSTANT)
        #Specify outer cutoff
        # Specification only available for Van der Waals cohesion model
        # Sets keyword VDW_OUTER_CUTOFF (WALL_OUTER_CUTOFF)
        #Specify inner cutoff
        # Specification only available for Van der Waals cohesion model
        # Sets keyword VDW_INNER_CUTOFF (WALL_INNER_CUTOFF)
        #Specify asperities
        # Specification only available for Van der Waals cohesion model
        # Sets keyword ASPERITIES
        enabled = bool(van_der_waals)
        ui.groupbox_cohesion_parameters.setEnabled(enabled)
        # (settings handled by keyword widgets.  TODO:
        #  decide if we want to unset keywords if not enabled

        #List the following options under an 'Advanced' section header.
        #Select Neighbor Search Method
        # Selection always available
        # Available selection
        #Grid-based [DEFAULT]
        #Selection always available
        #Sets keyword DES_NEIGHBOR_SEARCH 4
        #N-Square
        #Selection always available
        #Sets keyword DES_NEIGHBOR_SEARCH 1
        des_neighbor_search = self.project.get_value('des_neighbor_search',
                                                     default=4)
        if des_neighbor_search not in (1, 4):
            self.warn("Invalid des_neighbor_search %s" % des_neighbor_search)
            des_neighbor_search = 4
            self.update_keyword('des_neighbor_search', des_neighbor_search)
        cb = ui.combobox_des_neighbor_search
        cb.setCurrentIndex(0 if des_neighbor_search == 4 else 1)

        #Specify maximum steps between neighbor search
        #Specification always available
        # Sets keyword NEIGHBOR_SEARCH_N
        #Specify factor defining particle neighborhood
        #Specification always available
        # Sets keyword FACTOR_RLM
        #Specify neighborhood search radius ratio
        #Specification always available
        #Sets keyword NEIGHBOR_SEARCH_RAD_RATIO
        #Specify search grid partitions (optional)
        #Specification always available
        #Sets keyword DESGRIDSEARCH_IMAX
        #Sets keyword DESGRIDSEARCH_JMAX
        #Sets keyword DESGRIDSEARCH_KMAX
        pass  # handled by keyword widgets

        #Enable user scalar tracking
        #Selection always available
        #Does not directly set any keywords
        #Enables specification of number of user scalars
        # Sets keyword DES_USR_VAR_SIZE
        des_usr_var_size = self.project.get_value('des_usr_var_size',
                                                  default=None)
        enabled = (des_usr_var_size is not None)
        cb = ui.checkbox_enable_des_usr_var_size
        cb.setChecked(enabled)
        ui.lineedit_keyword_des_usr_var_size.setEnabled(enabled)

        #Define minimum distance for contact conduction (optional)
        #Unavailable if not solving energy equations
        #Define fluid lens proportion constant (optional)
        # Unavailable if not solving energy equations
        enabled = self.project.get_value('energy_eq', default=True)
        for item in (ui.label_des_min_cond_dist,
                     ui.lineedit_keyword_des_min_cond_dist,
                     ui.label_des_min_cond_dist_units, ui.label_flpc,
                     ui.lineedit_keyword_flpc):
            item.setEnabled(enabled)

        # Remember the names of solids phases, to track changes
        self.solids_dem_saved_solids_names = solids_names