Attributes#

Both Places and Agents can have a set of attributes attached. However, we cannot do that by adding member variables directly into the Place and Agent class since that will result in severe performance degradation due to uncoalesced memory access. As a result, special APIs are provided for defining and accessing attributes.

Define Attributes#

The setAttribute function is used to define attributes, which takes in a template type argument for the type of that attribute, and together with 4 normal arguments:

  1. tag: An integer tag for identifying the attribute

  2. elementCount: Number of elements for the attribute. Value larger than 1 mean that this is a 2D attribute (array attribute). Value should not be smaller than 1.

  3. initialValue: The initial value of the attribute, should have the same type as the template type argument provided. This argument is optional, but it is recommended to provide it, otherwise it will be uninitialed random bytes.

For Places, there is one additional parameter permernentlyExcludedFromSyncBorder, which is a boolean showing whether this attribute should be permanently excluded during syncPlaceBorder call. This argument is optional with default value of false. It is mainly designed for internal uses and is usually not necessary to be modified. Only set it to true when you are 100% sure what you are doing.

Normally, before actually creating the attributes, it is recommended to define the tags of all the required attributes as an enum in the custom Place or Agent class.

class MyPlace final : public mass::Place {
public:
    __universal__ explicit MyPlace(const mass::Index localIndex) : mass::Place(localIndex) {}

    enum Attribute {
        INT_ATTR,
        DOUBLE_ATTR,
        FLOAT_ARR_ATTR
    };

    __device__ void callMethod(const mass::FunctionId functionId, void*const args) {
        // ...
    }

    // Other member methods...
};

Then, we can define the attributes through the created Places or Agents instance with the tags defined in the enum. After all the attributes being defined, call finalizeAttributes to commit the definition.

const auto places = mass::Mass::createPlaces<MyPlace>(/* ... */);

// an int attribute with inital value 0
places->setAttribute<int>(MyPlace::Attribute::INT_ATTR, 1, 0);
// a double attribute with no initial value
places->setAttribute<double>(MyPlace::Attribute::DOUBLE_ATTR, 1);
// a float 2D attribute (array attribute) with length of 4 
// and inital value 2.0f for each element. 
places->setAttribute<float>(MyPlace::Attribute::FLOAT_ARR_ATTR, 4, 2.0f);

places->finalizeAttributes();

Access Attributes#

Once the attributes are defined with setAttribute and committed with finalizeAttributes, they are ready to be accessed within member methods of the custom Place or Agent class. To access the value of an attribute, use the getAttribute and getArrAttribute methods, where both of them takes in the type of the attribute as a template argument and the tag of the attribute as a normal argument.

The return values of these two methods are pointer-like types pointing to the attribute value, and we can access the value using * or -> operator. For getAttribute, it is designed for 1D attributes (non-array attributes) whose value is a single value, so the return type does not implement the subscript operator []. On the contrary, getArrAttribute is designed for 2D attributes (array attributes), so the return type implements the subscript operator [] for accessing each element.

Continue with the MyPlace class above, we can add a member method and access the attributes as follows.

class MyPlace final : public mass::Place {
public:
    __universal__ explicit MyPlace(const mass::Index localIndex) : mass::Place(localIndex) {}

    enum Attribute {
        INT_ATTR,
        DOUBLE_ATTR,
        FLOAT_ARR_ATTR
    };

    enum Function {
        FUNC1
    }

    __device__ void callMethod(const mass::FunctionId functionId, void*const args) {
        switch (functionId) {
        case FUNC1: func1(); break;
        default:             break;
        }
    }

    __device__ void func1() {
        // get pointers to the attributes
        const auto intAttr = getAttribute<int>(Attribute::INT_ATTR);
        const auto doubleAttr = getAttribute<double>(Attribute::DOUBLE_ATTR);
        const auto floatArrAttr = getArrAttribute<float>(Attribute::FLOAT_ARR_ATTR);

        // read attribute values and do some computation
        const auto v = *intAttr + *doubleAttr * 2;
        
        for (int i = 0; i < 4; i++) {
            // read & write to 2D attribute
            floatArrAttr[i] += static_cast<float>(v + i);
        }
    }
};

Shared Attributes#

Normal attributes have individual values for each Place or Agent instance. However, sometimes we might want to store some constants shared by all instances. We can still use normal attributes for that, but it wastes memory since we are storing the same thing repeatedly. For this use case, it is recommended to use the Shared Attributes.

To define a shared attribute, the process is very similar to normal attributes, where we first define the tags for all the shared attribute using enum.

class MyPlace final : public mass::Place {
public:
    __universal__ explicit MyPlace(const mass::Index localIndex) : mass::Place(localIndex) {}

    enum SharedAttribute {
        INT_ATTR,
        FLOAT_ARR_ATTR
    };

    // Other member methods...
};

And then use the addSharedAttribute method to define the shared attributes, which takes in the same arguments as setAttribute method with just 2 exceptions. First, the initialValue argument is not optional and must be provided. Second, the permernentlyExcludedFromSyncBorder argument is not available.

const auto places = mass::Mass::createPlaces<MyPlace>(/* ... */);

// an int shared attribute with inital value 0
places->addSharedAttribute<int>(MyPlace::SharedAttribute::INT_ATTR, 1, 0);
// a float 2D shared attribute (array attribute) with length of 4 
// and inital value 2.0f for each element. 
places->addSharedAttribute<float>(MyPlace::SharedAttribute::FLOAT_ARR_ATTR, 4, 2.0f);

places->finalizeAttributes();

With that, we can then access these shared attributes within the member methods of the custom Place or Agent class using the getSharedAttribute and getSharedArrAttribute methods, which is again very similar to the ones for accessing normal attributes.

The return types of getSharedAttribute and getSharedArrAttribute are the same pointer-like types as the normal attributes, but they are “pointer-to-constant”, meaning that they cannot be used to modify the attribute values. This is by design, since shared attributes are intended to store constants and should not be modified once initialized.

class MyPlace final : public mass::Place {
public:
    __universal__ explicit MyPlace(const mass::Index localIndex) : mass::Place(localIndex) {}

    enum SharedAttribute {
        INT_ATTR,
        FLOAT_ARR_ATTR
    };

    enum Function {
        FUNC1
    }

    __device__ void callMethod(const mass::FunctionId functionId, void*const args) {
        switch (functionId) {
        case FUNC1: func1(); break;
        default:             break;
        }
    }

    __device__ void func1() {
        // get pointers to the shared attributes
        const auto intAttr = getSharedAttribute<int>(SharedAttribute::INT_ATTR);
        const auto floatArrAttr = getSharedArrAttribute<float>(SharedAttribute::FLOAT_ARR_ATTR);

        // read attribute values and do some computation
        const auto v = *intAttr + floatArrAttr[0] + floatArrAttr[2] * 3;

        // ERROR: cannot modify shared attributes
        // floatArrAttr[3] = v;
    }
};

Pre-Defined Attributes#

Besides the custom attributes we defined during simulations, MASS CUDA also provides some pre-defined attributes for both Place and Agent. The value of these attributes can be accessed through provided getters in Place and Agent classes.

For Place, the following pre-defined attributes are provided:

Getter

Description

getIndex()

get the flattened global index of the current Place instance

getLocalIndex()

get the flattened index of the current Place instance within the current block (including Ghost Regions)

getFlattenBlockIndex()

get the flattened index of the current block

getNumPlaces()

get the total number of Place instances within the Places grid

getNeighborsPtr()

get array of pointers to all the neighboring instances of the current Place instance

getAgents()

get the array of pointers to all the Agent instances residing on the current Place instance

getAgentPopulation()

get the number of Agent instances residing on the current Place instance

getPotentialAgents()

get the array of Agent instances trying to migrate into the current Place, but haven’t been moved yet

getPotentialPopulation()

get the number of Agent instances trying to migrate into the current Place, but haven’t been moved yet

For Agent, the following pre-defined attributes are provided:

Getter

Description

getId()

get the unique ID of the current Agent instance

getLocalIndex()

get the flattened index of the current Agent instance on the current device

getIndex()

Same as getLocalIndex()

getPlace()

get pointer to the Place instance where the current Agent instance resides

getPlaceIndex()

get the flattened index of the Place instance where the current Agent instance resides. Same as getPlace()->getIndex()