Configuration Management

replaceable

What really enables the configuration management features in Modelica is the replaceable keyword. It is used to identify components in a model whose type can be changed (or “redeclared”) in the future. One way to think about replaceable is that it allows the model developer to define “slots” in the model that are either “blank” to begin with (where an interface model is the original type in the declaration) or at least “configurable”.

The advantage of using the replaceable keyword is that it allows new models to be created without having to reconnect anything. This not only imposes a structural framework on future models (to ensure naming conventions are followed, interfaces are common, etc.), it also helps avoid potential errors by eliminating an error prone task from the model development process, i.e., creating connections.

To make a component replaceable the only thing that is necessary is to add the replaceable keyword in front of the declaration, i.e.,

replaceable DeclaredType variableName;

where DeclaredType is the initial type for a variable named variableName. In such a declaration, the variableName variable can be given a new type (we will discuss how very shortly). But any new type used for variableName must be plug-compatible.

constrainedby

As we just mentioned, by default the new type of any replaceable component must be plug-compatible with the initial type. But this doesn’t have to be the case. As our earlier discussion on Constraining Types pointed out, it is possible to specify both a default type for the variable to have and a separate constraining type that any new type needs to be compatible with.

Specifying an alternative constraining type requires the use of the constrainedby keyword. The syntax for using the constrainedby keyword is:

replaceable DefaultType variableName constrainedby ConstrainingType;

where variableName is again the name of the variable being declared, DefaultType represents the type of variableName of the type of variableName is never changed and ConstrainingType indicates the constraining type. As mentioned previously, any new type attributed to the variableName variable must be plug-compatible with the constraining type. But, of course, the DefaultType must also be plug-compatible with the constraining type.

constrainedby vs. extends

Older versions of Modelica didn’t include the constrainedby keyword. Instead, the extends keyword was used instead. But it was felt that inheritance and plug-compatibility were distinct enough that a separate keyword would be less confusing. So don’t be confused if you are looking at Modelica code and the keyword extends is used where you would expect to see the constrainedby keyword (i.e., following a replaceable declaration).

redeclare

So now we know that by using the replaceable keyword, we can change the type of a variable in the future. Changing the type is called “redeclaring” the variable (i.e., to have a different type). For this reason, it is fitting that the keyword used to indicate a redeclaration is redeclare. Assume that we have the following system model:

model System
  Plant plant;
  Controller controller;
  Actuator actuator;
  replaceable Sensor sensor;
end System;

In this System model, only the sensor is replaceable. So the types of each of the other subsystems (i.e., plant, controller and actuator) cannot be changed.

If we wanted to extend this model, but use a different model for the sensor subsystem, we would use the redeclare keyword as follows:

model SystemVariation
  extends System(
    redeclare CheapSensor sensor
  );
end SystemVariation;

What this tells the Modelica compiler is that in the context of the SystemVariation model, the sensor subsystem should be an instance of the CheapSensor model, not the (otherwise default) Sensor model. However, the CheapSensor model (or any other type chosen during redeclaration) must be plug-compatible with that variable’s constraining type.

The syntax of a redeclare statement is really exactly the same as a normal declaration except that it is preceded by the redeclare keyword. Obviously, any variable that is redeclared had to be declared in the first place (i.e., you cannot use this syntax to declare a variable, only to redeclare it).

It is very important to understand that when you redeclare a component, the new redeclaration supersedes the previous one. For example, after the following redeclaration:

redeclare CheapSensor sensor;

the sensor component is no longer replaceable. This is because the new declaration doesn’t include the replaceable keyword. As a result, it is as if it was never there. If we wanted the component to remain replaceable, the redeclaration would need to be:

redeclare replaceable CheapSensor sensor;

Furthermore, if we choose to make the redeclared variable replaceable, we also have the option to redeclare the constraining type, like this:

redeclare replaceable CheapSensor sensor constrainedby NewSensorType;

However, the original constraining type still plays a role even in this case because the type NewSensorType must be plug-compatible with the original constraining type. In the terminology of programming languages, we can narrow the type (reducing the set of things that are plug-compatible), but we can never widen the type (which would make things that were previously not plug-compatible now plug-compatible).

Earlier, when discussing Arrays of Component, we made the point that it was not possible to redeclare individual elements in arrays. Instead, a redeclaration must be applied to the entire array. In other words, if we declare something initially as:

replaceable Sensor sensors[5];

It is then possible to redeclare the array, e.g.,

redeclare CheapSensor sensors[5];

But the important point is that the redeclaration affects every element of the sensors array. There is no way to redeclare only one element.

Modifications

One important complexity that comes with replaceability is what happens to modifications in the case of a redeclaration. To understand the issue, consider the following example.

replaceable SampleHoldSensor sensor(sample_rate=0.01)
  constrainedby Sensor;

Now, what happens if we were to redeclare the sensor as follows:

redeclare IdealSensor sensor;

Is the value for sample_rate lost? We would hope so since the IdealSensor model probably doesn’t have a parameter called sample_rate to set.

But let’s consider another case:

replaceable Resistor R1(R=100);

Now imagine we had another resistor model, SensitiveResistor that was plug-compatible with Resistor (i.e., it had a parameter called R) but included an additional parameter, dRdT, indicating the (linear) sensitivity of the resistance to temperature. We might want to do something like this:

redeclare SensitiveResistor R1(dRdT=0.1);

What happens to R in this case? In this case, we would actually like to preserve the value of R so it persists across the redeclaration. Otherwise, we’d need to restate it all the time, i.e.,

redeclare SensitiveResistor R1(R=100, dRdT=0.1);

and this would violate the DRY principle. The result would be that any change in the original value of R would be overridden by any redeclarations.

So, we’ve seen two cases valid use cases. In one case, we don’t want a modification to persist following a redeclaration and in the other we would like the modification to persist. Fortunately, Modelica has a way to express both of these. The normal Modelica semantics take care of the first case. If we redeclare something, all modifications from the original declaration are erased. But what about the second case? In that case, the solution is to apply the modifications to the constraining type. So for our resistor example, our original declaration would need to be:

replaceable Resistor R1 constrainedby Resistor(R=100);

Here we explicitly list both the default type Resistor and the constraining type Resistor(R=100) separately because the constraining type now includes a modification. By moving the modification to the constraining type, that modification will automatically be applied to both the original declaration and any subsequent redeclarations. So in this case, the resistor instance R1 will have an R value of 100 even though the modification isn’t directly applied after the variable name. But furthermore, if we perform the redeclaration we discussed previously, i.e.,

redeclare SensitiveResistor R1(dRdT=0.1);

the R=100 modification will automatically be applied here as well.

In summary, if you want a modification to apply only to a specific declaration and not in subsequent redeclarations, apply it after the variable name. If you want it to persist through subsequent redeclarations, apply it to the constraining type.

Redefinitions

It turns out that the replaceable keyword can also be associated with definitions, not just declarations. The main use of this feature is to be able to change the type of multiple components at once. For example, imagine a circuit model with several different resistor components:

model Circuit
  Resistor R1(R=100);
  Resistor R2(R=150);
  Resistor R4(R=45);
  Resistor R5(R=90);
  // ...
equation
  connect(R1.p, R2.n);
  connect(R1.n, R3.p);
  // ...
end Circuit;

Now imagine we wanted one version of this model with ordinary Resistor components and the other where each resistor was an instance of the SensitiveResistor model. One way we could achieve this would be to define our Circuit as follows:

model Circuit
  replaceable Resistor R1 constrainedby Resistor(R=100);
  replaceable Resistor R2 constrainedby Resistor(R=150);
  replaceable Resistor R4 constrainedby Resistor(R=45);
  replaceable Resistor R5 constrainedby Resistor(R=90);
  // ...
equation
  connect(R1.p, R2.n);
  connect(R1.n, R3.p);
  // ...
end Circuit;

But in that case, our circuit with SensitiveResistor components would be defined as:

model SensitiveCircuit
  extends Circuit(
    redeclare SensitiveResistor R1(dRdT=0.1),
    redeclare SensitiveResistor R2(dRdT=0.1),
    redeclare SensitiveResistor R3(dRdT=0.1),
    redeclare SensitiveResistor R4(dRdT=0.1)
  );
end SensitiveCircuit;

Note that we don’t have to specify resistance values because the modifications that set the resistance were applied to the constraining type in our Circuit model. But, it is a bit tedious that we have to change each individual resistor and specify dRdT over and over again even though they are all the same value. However, Modelica gives us a way to do them all at once. The first step is to define a local type within the model like this:

model Circuit
  model ResistorModel = Resistor;
  ResistorModel R1(R=100);
  ResistorModel R2(R=150);
  ResistorModel R4(R=45);
  ResistorModel R5(R=90);
  // ...
equation
  connect(R1.p, R2.n);
  connect(R1.n, R3.p);
  // ...
end Circuit;

What this does is establish ResistorModel as a kind of alias for Resistor. This by itself doesn’t help us with changing the type of each resistor easily. But making ResistorModel replaceable does:

model Circuit
  replaceable model ResistorModel = Resistor;
  ResistorModel R1(R=100);
  ResistorModel R2(R=150);
  ResistorModel R4(R=45);
  ResistorModel R5(R=90);
  // ...
equation
  connect(R1.p, R2.n);
  connect(R1.n, R3.p);
  // ...
end Circuit;

If our Circuit is defined in this way, we can create the SensitiveCircuit model as follows:

model SensitiveCircuit
  extends Circuit(
    redeclare ResistorModel = SensitiveResistor(dRdT=0.1)
  );
end SensitiveCircuit;

All our resistor components are still of type ResistorModel, we didn’t have to redeclare any of them. What we did do was redefine what a ResistorModel is by changing its definition to SensitiveResistor(dRdT=0.1). Note that the modification dRdT=0.1 will be applied to all components of type ResistorModel. Technically, this isn’t a redeclaration of a component’s type, it is a redefinition of a type. But we reuse the redeclare keyword.

Interestingly, with these redefinitions we still have the notion of a default type and a constraining type. The general syntax for a redefinable type is:

replaceable model AliasType = DefaultType(...) constrainedby ConstrainingType(...);

Just as with a replaceable component, any modifications associated with the default type, DefaultType, are only applied in the case that AliasType isn’t redefined. But, any modification associated with the constraining type, ConstrainingType, will persist across redefinitions. Furthermore, AliasType must always be plug compatible with the constraining type.

Although this aspect of the language is less frequently used, compared to replaceable components, it can save time and help avoid errors in some cases.

Choices

This section has focused on configuration management and we’ve learned that the constraining type controls what options are available when doing a redeclare. If a single model developer creates an architecture and all compatible implementations, then they have a very good sense of what potential configurations will satisfy the constraining types involved.

But what if you are using an architecture developed by someone else? How can you determine what possibilities exist? Fortunately, the Modelica specification includes a few standard annotations that help address this issue.

choices

The choices annotation allows the original model developer to associate a list of modifications with a given declaration. The very simplest use case for this could be to specify values for a given parameter:

parameter Modelica.SIunits.Density rho
  annotation(choices(choice=1.1455 "Air",
                     choice=992.2 "Water"));

In this case, the model developer has listed several possible values that the user might want to give to the rho parameter. Each choice is a modification to be applied to the rho variable. This information is commonly used by graphical Modelica tools to provide users with intelligent choices.

This feature can just as easily be used in the context of configuration management. Consider the following example:

replaceable IdealSensor sensor constrainedby Sensor
  annotation(
    choices(
      choice=redeclare SampleHoldSensor sensor
             "Sample and hold sensor",
      choice=redeclare IdealSensor sensor
             "An ideal sensor"));

Again, the model developer is embedding a set of possible modifications along with the declaration. These choice values can also be used by graphical tools to provide a reasonable set of choices when configuring a system.

choicesAllMatching

But one problem here is that it is not only tedious to have to explicitly list all of these choices, but the set of possibilities might change. After all, other developers (besides the original model developer) might come along and create implementations that satisfy a given constraining type. How about giving users the option of seeing all legal options when configuring their system?

Fortunately, Modelica includes just such an annotation. It is the choicesAllMatching annotation. By setting the value of this annotation to true on a given declaration (or replaceable definition), this instructs the tool to find all possible legal options and present them through the user interface. For example,

replaceable IdealSensor sensor constrainedby Sensor
  annotation(choicesAllMatching=true);

By adding this annotation, the tool knows to find all legal redeclarations when a user is reconfiguring their models through the graphical user interface. This can increase the usability of architecture based models enormously because it presents users with the full range of options at their disposal with trivial effort on the part of the model developer.

Conclusion

In this section, we’ve discussed the configuration management features in Modelica. As with other aspects of the Modelica language, the goals here are the same: promote reuse, increase productivity and ensure correctness. Modelica includes many powerful options for redeclaring components and redefining types. By combining this with the choicesAllMatching annotation, models can be built to support a large combination of possible configurations using clearly defined choice points.