Mass rollup example from Introduction to the SysML v2 Language

Hello,

I am trying to re-create the mass rollup example from the Intro to the SysML v2 Language-Textual Notation, as shown in slides 72-75. This is what my implementation looks like, which should be functionally the same but with some irrelevant parts of the original example stripped away.

package MassRollup2 {
    private import RealFunctions::sum;
    private import ScalarValues::Real;

    part def MassedThing {
        attribute simpleMass : Real;
        attribute totalMass : Real default simpleMass;
    }

    part compositeThing : MassedThing {
        part subcomponents : MassedThing [*];
        attribute redefines totalMass = simpleMass + sum(subcomponents.totalMass);
    }
}

package CarMassRollupExample2 {
    private import MassRollup2::*;

    part def CarPart specializes MassedThing;

    part car : CarPart subsets compositeThing {
        part carParts : CarPart [*] redefines subcomponents;
        part engine subsets carParts;
        part transmission subsets carParts;
    }

    // Example usage
    part c subsets car {
        attribute redefines simpleMass = 1000;
        part redefines engine {
            attribute redefines simpleMass = 100;
        }
        part redefines transmission {
            attribute redefines simpleMass = 50;
        }
    }
    // c.totalMass --> 1150.0[kg]
}

The python code to evaluate this looks as follows:

model, diagnostics = syside.load_model(["example.sysml"])

c = get_node(model, ["CarMassRollupExample2", "c"])
simpleMass = get_namespace_member(c, "simpleMass")
totalMass = get_namespace_member(c, "totalMass")
print(simpleMass, get_attribute_value(simpleMass, c))
print(totalMass, get_attribute_value(totalMass, c)

When this is executed, the following is printed out:

CarMassRollupExample2::c::simpleMass 1000
MassRollup2::MassedThing::totalMass 1000

The totalMass value here is 1000 instead of the expected 1150. It seems as though the attribute redefinition in the compositeThing part usage does not have the expected effect, and the totalMass of c remains as its default value of simpleMass as written in the MassedThing part definition.

I can confirm this by changing the totalMass attribute redefinition in compositeThing so that it is only driven by the totalMass of the subcomponents and ignores the simpleMass of the compositeThing itself:

part compositeThing : MassedThing {
    part subcomponents : MassedThing [*];
    attribute redefines totalMass = sum(subcomponents.totalMass);
}

With this change, the printout is unchanged, even though the expected value is 150:

CarMassRollupExample2::c::simpleMass 1000
MassRollup2::MassedThing::totalMass 1000

Do you have any insight into what the issue is here? On the one hand it could be that something is going wrong at the subsetting of engine and transmission in the car part usage, such that engine and transmission don’t end up within the carParts collection? Or could it be that in part car : CarPart subsets compositeThing, car is not properly inheriting the attribute redefinition of totalMass from compositeThing?

Any guidance or tips for troubleshooting this is appreciated. Thanks!

For reference, these are the helper functions being used:

def get_node(model: syside.Model, qualified_name: Sequence[str]) -> syside.Element:
    # Using the function provided in syside_helpers.py in the state machine example

def get_attribute_value(node: syside.AttributeUsage, scope=None):
    if node.feature_value_expression is None:
        return None

    if scope is None:
        result, _ = syside.Compiler().evaluate(node.feature_value_expression)
    else:
        result, _ = syside.Compiler().evaluate_feature(
            feature=node.feature_value_expression, scope=scope
        )

    return result

def get_namespace_member(node: syside.Namespace | syside.Element, name: str):
    if not isinstance(node, syside.Namespace):
        raise TypeError

    queue: list[syside.Namespace] = [node]
    visited: set[syside.Namespace] = set()
    while queue:
        element = queue.pop(0)
        if isinstance(element, syside.Feature):
            element = element.feature_target
        if element in visited:
            continue
        visited.add(element)
        child = element.get_member(name)
        if child is not None:
            return child
        if isinstance(element, syside.Type):
            queue[0:0] = element.heritage.elements
    return None

This happens because subsettings are not used when evaluating expressions. Specification does not provide evaluation semantics hence the current behaviour. Additionally, it is not clear whether all subsetting features are unique or are overlapping which leads to multiple other issues if this were to be implemented.

Hi @Daumantas, I must admit I’m not quite sure I understand exactly what you mean by subsettings not being used when evaluating expressions.

Either way, since I thought the rollup example seemed overly complicated, I tried to create a simpler one using just part definitions and specializations instead. It seems like it works better, but I think the root issue may be that the sum function is not returning a value. Perhaps this is what you were referring to?

Model:

package MyMassRollup {
    private import RealFunctions::sum;
    private import ScalarValues::Real;

    part def MassedThing {
        attribute simpleMass : Real;
        derived attribute totalMass : Real default simpleMass;
    }

    part def CompositeThing specializes MassedThing {
        part subcomponents : CompositeThing [*];
        derived attribute redefines totalMass = simpleMass + sum(subcomponents.totalMass);
    }
}

package MyMassRollupExample {
    private import MyMassRollup::*;

    part def SystemProduct specializes CompositeThing;

    part system : SystemProduct {
        part subproducts : SystemProduct [*] redefines subcomponents;
        
        attribute redefines simpleMass = 20;

        part subsystem1 subsets subproducts {
            attribute redefines simpleMass = 100;
        }
        part subsystem2 subsets subproducts {
            attribute redefines simpleMass = 50;
        }
    }
}

Python code:

system = get_node(model, ["MyMassRollupExample", "system"])
subsystem1 = get_namespace_member(system, "subsystem1")
subsystem2 = get_namespace_member(system, "subsystem2")

simpleMass0 = get_namespace_member(system, "simpleMass")
totalMass0 = get_namespace_member(system, "totalMass")
print()
print(simpleMass0, get_attribute_value(simpleMass0, system))
print(totalMass0, get_attribute_value(totalMass0, system))

simpleMass1 = get_namespace_member(subsystem1, "simpleMass")
totalMass1 = get_namespace_member(subsystem1, "totalMass")
print()
print(simpleMass1, get_attribute_value(simpleMass1, subsystem1))
print(totalMass1, get_attribute_value(totalMass1, subsystem1))

simpleMass2 = get_namespace_member(subsystem2, "simpleMass")
totalMass2 = get_namespace_member(subsystem2, "totalMass")
print()
print(simpleMass2, get_attribute_value(simpleMass2, subsystem2))
print(totalMass2, get_attribute_value(totalMass2, subsystem2))

Output:

MyMassRollupExample::system::simpleMass 20
MyMassRollup::CompositeThing::totalMass 21

MyMassRollupExample::system::subsystem1::simpleMass 100
MyMassRollup::CompositeThing::totalMass 101

MyMassRollupExample::system::subsystem2::simpleMass 50
MyMassRollup::CompositeThing::totalMass 51

The thing is, if I replace the totalMass attribute redefinition in CompositeThing to something that does not use the sum function, such as this:

part def CompositeThing specializes MassedThing {
    part subcomponents : CompositeThing [*];
    derived attribute redefines totalMass = simpleMass + 1;
}

Then the attributes seem to be evaluated just as expected, which indicates that the pattern is being inherited properly through the subsetting.

MyMassRollupExample::system::simpleMass 20
MyMassRollup::CompositeThing::totalMass 21

MyMassRollupExample::system::subsystem1::simpleMass 100
MyMassRollup::CompositeThing::totalMass 101

MyMassRollupExample::system::subsystem2::simpleMass 50
MyMassRollup::CompositeThing::totalMass 51

It’s the summing among subcomponents that is not working. Should I be using a different function than RealFunctions::sum?

1 Like

Sorry if I wasn’t clear. I meant that subsystem1 and subsystem2 are not treated as values of subproducts, i.e. subproducts is an empty list as far as the current expression evaluation is concerned.

Alright, I understand. That’s a shame :confused:

Do you know of some other way to get the elements subsystem1 and subsystem2 into the collection subproducts? Or is it completely outside the feature set of Automator at the moment?

Is there some other way to implement rollups that is supported?

Textually, you would want

part subproducts : SystemProduct [*] redefines subcomponents = (
    subsystem1, subsystem2
)

It is an OperatorExpression with Operator.Comma, and each argument a FeatureReferenceExpression referencing a subsystem<N>, e.g.:

_, expr = subproducts.feature_value_member.set_member_element(
    syside.OperatorExpression
)
expr.operator = syside.ExplicitOperator.Comma

for subsystem in subsystems:
    _, ref = expr.arguments.append(syside.FeatureReferenceExpression)
    ref.referent_member.set_member_element(subsystem)

Thanks for the example, I didn’t know you could list the members of the collection in that way! So doing it in that way, the sysml code looks like this:

part system : SystemProduct {
    part subproducts : SystemProduct [*] redefines subcomponents = (subsystem1, subsystem2);

    attribute redefines simpleMass = 20;

    part subsystem1 subsets subproducts {
        attribute redefines simpleMass = 100;
    }
    part subsystem2 subsets subproducts {
        attribute redefines simpleMass = 50;
    }
}

I can confirm that the two subsystems are contained in subproducts like you indicated by printing the following:

subproducts = get_node(model, ["MyMassRollupExample", "system", "subproducts"])

print(f"Printing members of {subproducts}")
for arg in subproducts.feature_value_member.member_element.arguments.collect():
    print(arg.referent_member.member_element)

Output:

Printing members of MyMassRollupExample::system::subproducts
MyMassRollupExample::system::subsystem1
MyMassRollupExample::system::subsystem2

However, even now it is the case that all of the totalMass attributes evaluate to None, just like before. For troubleshooting, I made an even simpler example in line with your code snippet to see if I could get the sum function to work.

Model:

private import RealFunctions::sum;
part simpleSystem {
    part subproducts [*] = (subsystem1, subsystem2);

    part subsystem1 {
        attribute value = 1;
    }
    part subsystem2 {
        attribute value = 2;
    }
    attribute valueSum = sum(subproducts.value);
}

Code:

simpleSystemSubproducts = get_node(model, ["MyMassRollupExample", "simpleSystem", "subproducts"])

print(f"Printing members of {simpleSystemSubproducts}")
for member in simpleSystemSubproducts.feature_value_member.member_element.arguments.collect():
    print(member.referent_member.member_element)

value1 = get_node(model, ["MyMassRollupExample", "simpleSystem", "subsystem1", "value"])
value2 = get_node(model, ["MyMassRollupExample", "simpleSystem", "subsystem2", "value"])
valueSum = get_node(model, ["MyMassRollupExample", "simpleSystem", "valueSum"])

print()
print(value1, get_attribute_value(value1))
print(value2, get_attribute_value(value2))
print(valueSum, get_attribute_value(valueSum))

Output:

Printing members of MyMassRollupExample::simpleSystem::subproducts
MyMassRollupExample::simpleSystem::subsystem1
MyMassRollupExample::simpleSystem::subsystem2

MyMassRollupExample::simpleSystem::subsystem1::value 1
MyMassRollupExample::simpleSystem::subsystem2::value 2
MyMassRollupExample::simpleSystem::valueSum None

As you see, attribute valueSum = sum(subproducts.value) is being evaluated as None even now. Is this how it is supposed to work? Am I perhaps using the wrong function?

Thanks for all your help :slight_smile:

At the moment, RealFunctions::sum is not implemented but NumericalFunctions::sum is. However, because of limited type inference, subproducts.value is inferred to sum over subsystem1::value, there definitely needs to be a diagnostic for this. In the meantime, you will need to add a common base type for all subparts with an attribute value that would get resolved instead.

Ok thanks! I changed to use NumericalFunctions::sum and to use a common base type.

Model:

private import ScalarValues::Real;
    private import NumericalFunctions::sum;

    part def simpleSubsystem {
        attribute value : Real;
    }

    part simpleSystem {
        part subproducts [*] = (subsystem1, subsystem2);

        part subsystem1 : simpleSubsystem {
            attribute redefines value = 1;
        }
        part subsystem2 : simpleSubsystem {
            attribute redefines value = 2;
        }
        attribute valueSum = sum(subproducts.value);
    }

The python code is unchanged, and the output becomes this:

MyMassRollupExample::simpleSystem::subsystem1::value 1
MyMassRollupExample::simpleSystem::subsystem2::value 2
MyMassRollupExample::simpleSystem::valueSum 2

As you see, the sum evaluates to 2 instead of 3. After trying with a few more numbers, I get the impression that sum(subproducts.value) is just evaluating to 2 * subsystem1.value.

MyMassRollupExample::simpleSystem::subsystem1::value 10
MyMassRollupExample::simpleSystem::subsystem2::value 2
MyMassRollupExample::simpleSystem::valueSum 20
MyMassRollupExample::simpleSystem::subsystem1::value 10
MyMassRollupExample::simpleSystem::subsystem2::value 4
MyMassRollupExample::simpleSystem::valueSum 20
MyMassRollupExample::simpleSystem::subsystem1::value 4
MyMassRollupExample::simpleSystem::subsystem2::value 10
MyMassRollupExample::simpleSystem::valueSum 8

Do you think this is a bug with NumericalFunctions::sum, or that something else is the problem?

At the moment, subproducts should also be typed by simpleSubsystem. Type inference needs to be improved to infer a common base type rather than just picking the first.

Thank you for spotting that mistake! With that fix, this mass rollup indeed working for this simple example :slight_smile:

There is just one puzzle piece still missing before I’ve got a fully working rollup pattern. I would like to be able to apply this mass rollup pattern throughout the whole product tree and have it work recursively. However. this means that in the case of the leaves (the lowest level subproducts) their subproducts list will remain empty.

Currently I see that attribute valueSum = sum(subproducts.value) is being evaluated as None in that case. However, in order for this pattern to be applicable for lowest-level products I would need sum(subproducts.value) to evaluate to 0.

Do you know of some way to achieve this? Is there something I can change to make valueSum evaluate to 0 in these cases where subsystems is empty?

Setting value default 0 on the common ancestor should work.

My apologies, I realized that I had made a mistake in my last reply. The fix you proposed indeed works in the example from earlier. But to get this to work recursively so it can be applied to deeply nested parts, we will need the valueSum of a parent part to be equal to the sum of its children’s valueSum, not just their value

Defining it in this way, as attribute valueSum = sum(subproducts.valueSum), results in it being evaluated as None as you see below. Is there some workaround for this?

Model:

part def RecursiveProduct {
    part subproducts : RecursiveProduct [*];
    attribute value : Real default 0;
    attribute valueSum : Real = value + sum(subproducts.valueSum);
}

part highLevelProduct : RecursiveProduct {
    attribute redefines value = 3;
    part redefines subproducts = (lowLevelProduct1, lowLevelProduct2);
    part lowLevelProduct1 subsets subproducts {
        attribute redefines value = 10;
    }
    part lowLevelProduct2 subsets subproducts {
        attribute redefines value = 5;
    }
}

Output:

MyMassRollupExample::highLevelProduct::lowLevelProduct1::value 10
MyMassRollupExample::RecursiveProduct::valueSum None
MyMassRollupExample::highLevelProduct::lowLevelProduct2::value 5
MyMassRollupExample::RecursiveProduct::valueSum None
MyMassRollupExample::highLevelProduct::value 3
MyMassRollupExample::RecursiveProduct::valueSum None

There is an infinite recursion loop when evaluating valueSum that triggers max_steps limit, I will see if I can fix it.

1 Like

Because of how SysML feature reference evaluation works, features without feature values evaluate to themselves, matching Pilot implementation. The infinite recursion is then due to repeatedly attempting to evaluate subproducts.valueSum as subproducts returns itself instead of null. The fix is to change

part subproducts : RecursiveProduct [*] default null;

In the meantime, found and fixed a bug where feature chainings were evaluated in the outer scope, rather than the left-hand side scope of the chained access. For now, evaluation will still return None due to infinite recursion even with default null but next release will correctly evaluate valueSum to 18.

1 Like

Thanks for the fix, I’ve added the default null and I will test this in the next release.

Any idea of how long until 0.8.2 will come out? Should I expect it before the MBSE2025 conference 5-7 Nov?

Hopefully early next week but no promises.

1 Like

For posterity, the working pattern looks like this:

package Rollup {
    private import NumericalFunctions::sum;
    private import ScalarValues::Real;

    part def CompositeThing {
        part components : CompositeThing [*] default null;

        attribute value : Real default 0;
        attribute totalValue : Real = value + sum(components.totalValue);
    }
}