OOF2: The Manual

Name

Property — Base class for material properties

Synopses

C++ Synopsis

This synopsis lists the virtual base class methods that may be redefined when deriving a new Property class. It is not a complete list of Property methods. Do not derive new C++ Properties directly from this class. They should be derived from one of the subclasses listed below.

#include "engine/property.h" 
class Property {
  Property(const std::string& name,
           PyObject* registration);

  virtual void cross_reference(Material* material);
  virtual bool constant_in_space() const;
  virtual void precompute(FEMesh* mesh);
  virtual void clear_mesh_data(FEMesh* mesh,
                               void* data);

  virtual void begin_element(const CSubProblem* subproblem,
                             const Elementelement);

  virtual void end_element(const const CSubProblem* subproblem,
                           const Element* element);

  virtual void post_process(CSubProblem* subproblem,
                            const Elementelement) const;

  virtual bool is_symmetric_K(const CSubProblem* subproblem) const;
  virtual bool is_symmetric_C(const CSubProblem* subproblem) const;
  virtual bool is_symmetric_M(const CSubProblem* subproblem) const;
  virtual void output(const FEMesh* mesh,
                      const Elementelement,
                      const PropertyOutputp_output,
                      const MasterPositionposition,
                      OutputValvalue);

}

In addition, the following non-virtual utility functions are available:

  void set_mesh_data(const FEMesh* mesh,
                     void* data) const;
  void* get_mesh_data(const FEMesh* mesh); 

C++ Source Files

  • SRC/engine/property.C: C++ code
  • SRC/engine/property.h: C++ header
  • SRC/engine/property.swg: SWIG source code
  • SRC/engine/property.spy: Python code included in the SWIG output

Python Synopsis

New Property subclasses written in Python should be derived from PyFluxProperty, PyEqnProperty, or PyAuxProperty. PyFluxProperty and PyEqnProperty share a C++ base class, PyPropertyMethods, that contains the Python C API calls for the methods in Property. This synopsis lists the methods that may be redefined in a Python derived class when creating a new Property.[74]

from ooflib.SWIG.engine import pypropertywrapper 
class PyPropertyMethods:
  def cross_reference(self, material)
  def constant_in_space(self)
  def precompute(self)
  def clear_mesh_data(self, mesh)
  def begin_element(self, subproblem, element)
  def end_element(self, subproblem, element)
  def post_process(self, subproblem, element)
  def is_symmetric_K(self, subproblem)
  def is_symmetric_C(self, subproblem)
  def is_symmetric_M(self, subproblem)
  def output(self, mesh, element, p_output, position)

In addition, the following utility functions are available:

def set_mesh_data(mesh, data)
def get_mesh_data(mesh) 

Source Files

  • SRC/engine/pypropertywrapper.C: C++ code
  • SRC/engine/pypropertywrapper.h: C++ header
  • SRC/engine/pypropertywrapper.swg: SWIG wrapper code
  • SRC/engine/pypropertywrapper.spy: Python code included in the SWIG output

Overview

Property classes can be constructed either in C++ or Python. Python classes are somewhat simpler to program and build, but C++ classes are significantly more computationally efficient.

In C++, new Property classes should be derived from one of three C++ subclasses:

These base classes contain trivial implementations of almost all methods, so that in the derived classes it is safe to omit any functions which are irrelevant to a particular physical property. The exception is integration_order(), which must be provided in all FluxProperty and EqnProperty subclasses.

In Python, new Property classes should be derived from one of

These Python classes are derived via SWIG from C++ classes with the same names. The C++ classes store a pointer to a Python Property object, and use the Python C API to call that object's Python methods (the ones defined in the extension class). A Python subclass does not need to define all of the virtual functions. If a Python method doesn't exist, the default C++ method will be called.[75]

It is possible to mix C++ and Python Properties in the same Material, most of the time. Difficulties arise when one Property needs to call methods or fetch data from another one. Properties can locate other Properties by calling Material::fetchProperty() in their cross_reference methods, but in C++ fetchProperty() will return a generic Property* pointer which cannot provide access to the methods of a Python Property, except for methods defined in the base classes. On the other hand, in Python fetchProperty() returns a derived class object (even an object defined in C++, as long as it is swigged) so a Python Property can access all of the swigged methods of C++ Property.

This page only discusses those base class functions that are used when deriving new subclasses. See PhysicalProperty, FluxProperty and EqnProperty for additional methods that can be redefined.

In order for C++ and Python Properties to work properly and to appear in the GUI, every subclass must have an associated PropertyRegistration object. PropertyRegistrations must always be created in Python, even for Property subclasses created in C++.

Constructors

C++

All non-abstract C++ Property subclasses must have a constructor of the form

SubProperty::SubProperty(const std::string& name, PyObject *registration, ...) 

and they must invoke the base class constructor like this:

FluxProperty(const std::string& name, PyObject *registration) // or EqnProperty or AuxiliaryProperty

The derived class should treat name and registration as opaque variables, used only by the base class. They just need to be passed on through. The ... in the subclass constructor refers to the Property's parameters, which must appear in the constructor argument list in the same order that they appear in the corresponding PropertyRegistration.

C++ Property classes should not have a copy constructor. The base class copy constructor is private, to prohibit unauthorized duplication, by any means, electronic or otherwise.

Python

The Python constructor for a PyFluxProperty, PyEqnProperty, or PyAuxProperty subclass must look like this:

def __init__(self, name, registration, ...):
    PyFluxProperty.__init__(self, name, registration) # or PyEqnProperty or PyAuxProperty 

As in the C++ constructor, the name and registration parameters should be passed through to the base class unchanged.

The ... in the constructor arguments refers to any parameters that the Property might have. These must have the same names and be in the same order as the parameters in the associated PropertyRegistration. If the Property has no parameters, the derived class constructor does not need to be defined.

Virtual Methods

The following methods can be redefined in new C++ and Python Properties. Most are optional, and can be omitted if the default behavior is satisfactory. Unless otherwise noted, the default behavior is to do nothing. Additional methods are documented in the descriptions of the subclasses.

All of the functions can be accessed from either C++ or Python. If there are non-obvious differences between the C++ and Python invocations, they are noted below. Minor and standard punctuation differences are not noted. Function prototypes are specified in the C++ format, because it is more informative.

void cross_reference(Material* material)

cross_reference is called whenever a new Property is added to a Material. It gives each Property a chance to locate other Properties in the same Material from which it may need information. For example, many Properties of an anisotropic material will need to know the Material's Orientation Property. The argument to cross_reference is the Material object to which the Property belongs.

Material::fetchProperty should be used to look for other Properties. It will raise an exception if the sought-for Property does not exist. This exception should not be caught by the Property — it's handled in the Material.

For example, an anisotropic thermal expansion Property needs to access its Material's Elasticity and Orientation. It stores pointers to them in the class:

class ThermalExpansion : public FluxProperty {
protected:
  Elasticity *elasticity;
  ...
};

class AnisotropicThermalExpansion : public ThermalExpansion {
private:
  OrientationProp *orientation;
  ...
};    

and obtains their values by defining cross_reference() like this:

void AnisotropicThermalExpansion::cross_reference(Material *mat) {
  elasticity = dynamic_cast<Elasticity*>(mat->fetchProperty("Elasticity"));
  orientation = dynamic_cast<OrientationProp*>(mat->fetchProperty("Orientation"));
} 

In Python, it would look like this:

from ooflib.SWIG.engine import pypropertywrapper

class ThermalExpansion(pypropertywrapper.PyFluxProperty):
    def cross_reference(self, material):
        self.elasticity = material.fetchProperty("Elasticity")

class AnisotropicThermalExpansion(ThermalExpansion):
    def cross_reference(self, material):
        self.orientation = material.fetchProperty("Orientation") 

bool constant_in_space() const

constant_in_space() should return false if the Property explicitly depends on position. The default implementation returns true.

constant_in_space() can be used by other Properties to decide whether or not it's possible for them to precompute something. For example, an anisotropic elasticity Property can precompute its rotated stiffness modulus only if its orientation has no spatial variation.

void precompute(FEMesh* mesh)

precompute() is called when a Mesh is being solved. It's called before any other Property methods, other than cross_reference(). It should compute any quantities that the Property needs that depend only upon Property data. It can use Property data from other Properties, having located those other Properties during the cross_reference() step, but it should not assume that precompute() has been called for those Properties yet.

For example, if a Property has an anisotropic modulus, the Property's precompute() method should rotate the modulus to lab coordinates using the Material's Orientation (but only if the Orientation is constant in space.

void clear_mesh_data(FEMesh* mesh, void* data) const

clear_mesh_data() is called when data stored by set_mesh_data() is about to be overwritten, or if the FEMesh is about to be deleted. In the example given in set_mesh_data() above, clear_mesh_data() would simply delete the allocated pointer:

void MyProperty::clear_mesh_data(FEMesh* mesh, void* data) const {
    delete static_cast<MyMeshData*>(data);
} 

void begin_element(const CSubProblem*, const Element*)

When building the finite element stiffness matrix and other numerical components, OOF2 loops over the elements of the mesh and computes each element's contribution by calling its Properties' computational methods, e.g, FluxProperty::flux_matrix(), or EqnProperty::force_value(). Before calling these functions, however, it first calls begin_element(), passing the current Element as an argument. This allows the Property to precompute any Element dependent properties.

The base class implementation of begin_element() does nothing.

void end_element(const CSubProblem*, const Element*)

end_element is just like begin_element, except that it's called after OOF2 is done computing an Element's contribution. end_element() can be used to clean up any temporary objects allocated by begin_element().

The base class implementation of end_element() does nothing.

void post_process(CSubProblem*, const Element*) const

After solving the finite element equations, post_process is called once on each Element in a CSubProblem. It can be used for any sort of post-processing, such as adjusting material parameters, or generating output files.

Do not confuse post_process() with end_element(). end_element is called during finite element matrix construction, before the equations are solved.

bool is_symmetric_[KCM](const CSubProblem* mesh) const

is_symmetric_X indicates whether or not the finite element matrix constructed from the Property can be made symmetric by using the relationships established by ConjugatePair calls. There are three functions:

  • is_symmetric_K() for the stiffness matrix (coefficient of the Flux gradient),
  • is_symmetric_C() for the damping matrix (coefficient of the first time derivative), and
  • is_symmetric_M() for the mass matrix (coefficient of the second time derivative).

In most cases is_symmetric will simply return true, if the matrix can be symmetrized, or false, if it can't. Some cases are more complicated, however. For example, thermal expansion makes the matrix asymmetric if the temperature field is an active field, because the Property couples the temperature (with no derivatives) to the gradient of the displacement. Therefore, in the ThermalExpansion class, is_symmetric_K is defined like this:

bool ThermalExpansion::is_symmetric_K(const CSubProblem* subproblem) const {
  Equation *forcebalance = Equation::getEquation("Force_Balance");
  return !(forcebalance->is_active(subproblem) &&
	   temperature->is_defined(subproblem) &&
	   temperature->is_active(subproblem));
} 

The base class implementations of all three of these functions return true.

void output(FEMesh* mesh, const Element* element, const PropertyOutput* output, const MasterPosition& pos, OutputVal *data)

A Property's output function is called when quantities that depend on the Property are being computed, usually (but not necessarily) after a solution has been obtained on a mesh. Many different kinds of PropertyOutputs can be defined (see Section 8.7). Each Property class's PropertyRegistration indicates which kinds of PropertyOutput the Property can compute. The output function must determine which PropertyOutput is being computed, get the output's parameter values if necessary, compute its value at a given point in the mesh, and store the value in the given OutputVal object.

The arguments to Property::output in C++ are:

const FEMesh* mesh

The mesh on which the output is being computed. The output function will probably not need to use this variable directly, but must pass it through to other functions.

const Element* element

The element of the mesh containing the point at which output values are desired.

const PropertyOutput* output

The PropertyOutput object being computed. The object is created by the OOF2 menu system and, depending on the type of output, may contain Python arguments specifying exactly what's to be computed. For example, a strain output will specify whether it's computing the total, elastic, thermal or other variety of strain. See the example below.

const MasterPosition& pos

The position in the element's master coordinate space at which the output is to be computed.

OutputVal* data

The object in which the computed data should be stored. In C++, the object must be first cast to an appropriate derived type. The new value should be added to data, to ensure that values computed by other Properties are retained.

In Python, the arguments are the same, except that there's no data argument. Instead, the function returns an OutputVal (of the appropriate type) containing the Property's contribution to the output quantity.

Here is an example of a fairly complicated output function, from SRC/engine/properties/thermalexpansion/thermalexpansion.C in the OOF2 source code. It computes one of two types of output, Strain and Energy, each of which has two variants. It can also return the material parameters.

void ThermalExpansion::output(FEMesh *mesh,
                              const Element *element,
                              const PropertyOutput *output,
                              const MasterPosition &pos,
                              OutputVal *data)
  const 
{
  const std::string &outputname = output->name(); 1
  if(outputname == "Strain") {
    // The parameter is a Python StrainType instance.  Extract its name.
    std::string stype = output->getRegisteredParamName("type"); 2
    SymmMatrix3 *sdata = dynamic_cast<SymmMatrix3*>(data); 3
    // Compute Temperature at position pos.
    double t = temperature->value(mesh, element, pos);  4

    // Compute alpha*T and add it to the output.
    if(stype == "Thermal") 5
      *sdata += expansiontensor(mesh, element, pos)*(t-T0);  6
    else if(stype == "Elastic")
      *sdata -= expansiontensor(mesh, element, pos)*(t-T0);
  } // end outputname == "Strain"

  if(outputname == "Energy") { 7
    // The parameter is a Python Enum instance.  Extract its name.
    std::string etype = output->getEnumParam("etype"); 8
    if(etype == "Total" || etype == "Elastic") { 9
      ScalarOutputVal *edata = dynamic_cast<ScalarOutputVal*>(data); 10
      SymmMatrix3 thermalstrain;
      const Cijkl modulus = elasticity->cijkl(mesh, element, pos); 11
      double t = temperature->value(mesh, element, pos);
      thermalstrain = expansiontensor(mesh, element, pos)*(t-T0);
      SymmMatrix3 thermalstress(modulus*thermalstrain);
      SymmMatrix3 strain;
      findGeometricStrain(mesh, element, pos, strain); 12
      double e = 0;
      for(SpaceIndex i=0; i<3; i++) { 13
        e += thermalstress(i,i)*(-strain(i,i) + 0.5*thermalstrain(i,i));
        SpaceIndex j = (i+1)%3;
        e += 2*thermalstress(i,j)*(-strain(i,j) + 0.5*thermalstrain(i,j));
      }
      *edata += e;
    }
  } // end outputname == "Energy"

  if(outputname == "Material Constants:Couplings:Thermal Expansion T0") {  14
    ScalarOutputVal *sdata = dynamic_cast<ScalarOutputVal*>(data); 15
    *sdata = T0;
  }
} 

1 7

The name of the output indicates what type of quantity is to be computed. A Property can make contributions to more than one type of PropertyOutput, so it is important to check the name. All of the names handled by this routine must be listed in the name parameter of this Property's PropertyOutputRegistration.

2

The Strain output has a parameter named type, and this call to PropertyOutput::getRegisteredParamName() retrieves its value. Parameters for PropertyOutputs are defined in their PropertyOutputRegistrations. The PropertyOutput class provides a number of functions for retrieving parameters of different kinds (integer, float, string, etc.). In this case, type is a RegisteredParameter, so we use getRegisteredParamName().

3

Because we know that this is a Strain output whose value is a 3×3 symmetric matrix, we can cast the data pointer from its base class, OutputVal, to its derived class, SymmMatrix3.

4

ThermalExpansion's contribution to the strain depends on the value of the Temperature Field. This line uses Field::value() to get the Temperature's value at the given point the mesh. The variable temperature was set by the ThermalExpansion constructor, like this:

temperature = dynamic_cast<ScalarField*>(Field::getField("Temperature")); 

5

These lines make the thermal expansion contribution to the strain, with the appropriate sign (depending on the type of strain being computed).

6

T0 is a floating point number stored in the ThermalExpansion class. It is set by the ThermalExpansion constructor. The constructor is called with a T0 argument because its PropertyRegistration includes it in its list of parameters:

PropertyRegistration(
  name="Couplings:ThermalExpansion:Isotropic",
  params = [
       parameter.FloatParameter("T0", 0.0, tip="Reference temperature"),
       parameter.FloatParameter("alpha", 1.0, tip="Expansion coefficient")],
  ...) 

expansiontensor() is a virtual function in the ThermalExpansion class that returns a SymmMatrix3 object. The various isotropic and anistropic ThermalExpansion subclasses compute the matrix from their parameters. For example, the above PropertyRegistration for isotropic thermal expansion includes a scalar alpha parameter, but the registrations for anisotopic properties have tensor parameters.

*sdata is the OutputVal object, cast into a SymmMatrix3. The expansion tensor times temperature is also a SymmMatrix3 and can be added directly to *sdata.

8

The etype parameter for the Energy PropertyOutput is an EnumParameter instance, representing the EnergyType OOF2 Enum. The function PropertyOutput::getEnumParam retrieves the value of the parameter, and returns its name.

9

There are many different types of Energy output, but this Property only contributes to these two.

10

Energy is a scalar, so the OutputVal that was passed in is really a ScalarOutputVal. It must be cast to the derived class in order to be used.

11

This retrieves the elastic modulus Cijkl from the Material's Elasticity property. The variable elasticity is a data member of the ThermalExpansion class. It was set by this Property's cross_reference() function, like this:

void ThermalExpansion::cross_reference(Material *mat) {
  elasticity = dynamic_cast<Elasticity*>(mat->fetchProperty("Elasticity"));
} 

12

findGeometricStrain is defined in SRC/engine/cstrain.C and declared in SRC/engine/cstrain.h. It computes ϵij = (∂ui/∂xj + ∂uj/∂xi) where u is the displacement field.

13

This loop computes the contribution of thermal expansion to the elastic energy. SpaceIndex is a glorified integer that can represent a dimension of space.

14

Material parameters can be output too. Only T0 is handled here in the base class. The thermal expansion modulus is handled in the derived isotropic and anistropic classes.

15

T0 is a scalar, so data needs to be cast to a ScalarOutputVal pointer.

As an example in Python, here is output from SRC/engine/properties/elasticity/pyelasticity.py, which defines an elasticity Property in Python (for illustrative purposes only):

def output(self, mesh, element, output, pos):
    if output.name() == "Energy":
        etype = output.getEnumParam("etype")
        if etype in ("Total", "Elastic"):
            mod = self.modulus()
            # strain is a SymmMatrix3.  modulus is a cijkl.Cijkl
            strain = cstrain.getGeometricStrain(mesh, element, pos, False)
            stress = mod*strain # another SymmMatrix3.
            return outputval.ScalarOutputVal(0.5*stress.contract(strain)) 

Note that a new OutputVal is created and returned, containing the Property's contribution to the output quantity. Because energy is a scalar, the OutputVal is a ScalarOutputVal.

Non-virtual Methods

The following Property base class methods are utilities that may be useful when writing new Property subclasses.

void set_mesh_data(const FEMesh* mesh, void *data) const

set_mesh_data() allows a Property to store arbitrary data in a FEMesh. It is meant to be called from within precompute() to cache mesh-dependent data that will be needed later. Mesh-dependent data cannot be stored directly in a Property because Properties can be shared between FEMeshes. The data will be accessible only to the Property that stored it.

For example, a Property called MyProperty could store its data in a class called MyMeshData and cache it in the mesh like this:

void MyProperty::precompute(FEMesh *mesh) {
    MyMeshData* data = new MyMeshData(mesh);  // some expensive computation is hidden here
    set_mesh_data(mesh, data);
} 

The Python implementation of set_mesh_data is completely independent of the C++ implementation. C++ Properties store data in C++, and Python Properties store data in Python.

Before data stored by set_mesh_data is overwritten or deleted, OOF2 will call clear_mesh_data. A Property subclass should define clear_mesh_data if it needs to do any cleaning up before the data is deleted.

void* get_mesh_data(const FEMesh* mesh) const

get_mesh_data() retrieves the data stored by set_mesh_data(). For example, if MyProperty in the example above is a FluxProperty, its flux_matrix method could retrieve the data like this:

void MyProperty::flux_matrix(const FEMesh* mesh, ...) const {
    MyMeshData* data = static_cast<MyMeshData*>(get_mesh_data(mesh));
    ...
} 



[74] The methods are listed here as if they belong to a Python class called PyPropertyMethods. Don't be deceived. There is no Python version of this class. This listing just shows the Python form of the C++ methods that can be overridden by defining methods in a Python subclass.

[75] The Python classes PyFluxProperty, PyEqnProperty and PyAuxProperty are also derived from the C++ classes FluxProperty, EqnProperty and AuxiliaryProperty via SWIG, but they do not actually give access to any of the C++ class's methods other than the constructors and destructors. All calls to Python Property methods are done via Python API calls in SRC/engine/pypropertywrapper.C.