Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Applying Hindley-Milner to unit-checking #3491

Open
wants to merge 3 commits into
base: MCP/0027
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 336 additions & 0 deletions RationaleMCP/0027/HindleyMilner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
# Unit checking

Unit checking is the process of inferring the units of Modelica variables, and ensuring the variables are used consistently in expressions and equations.

It takes place after any evaluation of evaluable parameters and no evaluable parameters shall be evaluated during this process.
(This must be true as long as tools allow the users to opt-out from unit-checking.)

It is performed by using a type inference algorithm, albeit adapted to work with units.
The algorithm proposed here is based on the Hindley-Milner algorithm.
At a high level the algorithm collects a set of constraints on the units of expressions.
It extracts a set of substitutions from these constraints which once applied determine whether a variable has a unit and what that unit would be.
Any inconsistency discovered during this process is a unit error, and any unit expression that contains remaining unknowns at the end of the process is considered unknown.

The document is organized as follows:
* [Introduction of what is meant by the unit of a variable](#the-unit-of-a-modelica-variable)
* [Description of the objects used in the inference algorithm](#unit-constraints-and-meta-expressions)
* [Definition of unit equivalence and convertibility](#unit-equivalence-and-convertibility)
* [Unit inference algorihtm](#unit-inference)
qlambert-pro marked this conversation as resolved.
Show resolved Hide resolved
* [Application to Modelica](#unit-constraints-of-a-modelica-model)
* [Modelica operators on units](#modelica-operators-on-units)
* [Extensions and limitations](#possible-extensions-and-current-limits)

## The unit of a Modelica variable

The unit of a Modelica variable is the unit associated with the variable after unit-checking has been performed, possibly remaining unknown.

Note that the unit of a variable is not the same concept as the `unit`-attribute of the variable, but in a unit-consistent model, the unit will be equal to the `unit`-attribute when present.

## Unit constraints and meta-expressions

Unit constraints are introduced in the form of an [equivalence](#equivalence) of two unit _meta-expressions_.
After an evaluation stage, a unit constraint of the form _`u1` is equivalent to `u2`_ will be represented by the _single-sided form_ `u1*u2^-1`.

This section outlines the different constructs that comprise meta-expressions.

### Variables
The unit of the Modelica variable `var` is represented by the unit variable `var.unit`.
These are unknowns for the inference algorithm to determine.

### Literals

#### `well-formed` unit
The `well-formed` units correspond to [Modelica unit expressions](https://specification.modelica.org/master/unit-expressions.html).

For the purposes of unit checking, they can be uniquely represented by their base unit factorization and scaling, up to the order of the base units.

(We discuss why we ignore unit offsets later.)

#### `empty` unit
An expression with `empty` unit won't contribute to the unit of a parent expression, and will meet any constraints imposed on it.
In practice, these are used to represent the unit of literal values.
They provide a more flexible solution than considering any literal value to have unit `"1"`.
They also provide a stricter solution than letting the unit of literals be inferred, which would effectively make literals wildcard from the perspective of the unit system.

#### `undefined` unit
An expression with `undefined` unit is used to represent unspecified or error cases, and will meet any constraints imposed on it.

### Operators

#### *
Consider the meta-expression `e1 * e2`.
* If both `e1` and `e2` are `well-formed`, it evaluates to a `well-formed` unit resulting from multiplying `e1` by `e2`.
* If one of them is `empty`, it evaluates to the other one. (In effect, the `empty` unit becomes unit `"1"`)
* If one of them is `undefined`, it evaluates to `undefined`.

#### ^
Consider the meta-expression `b ^ e`, where `e` is a rational.
* If `b` is `well-formed`, it evaluates to a `well-formed` unit resulting from raising `b` to the power `e`.
* If `b` is `empty`, it evaluates to `empty`.
* If `b` is `undefined`, it evaluates to `undefined`.

#### der
Consider the meta-expression `der(e)`.
* In a constraint where a `well-formed` unit is present, it evaluates to `e * ("s" ^ -1)`.
* If `e` is `empty`, it evaluates to `empty`.
* If `e` is `undefined`, it evaluates to `undefined`.

This definition keeps `der(e)` in symbolic form in a model that does not contain any units.
Otherwise, `der(x) = x` would lead to unit errors even if `x` has no declared unit.
This also means that in `k*der(x) = -x`, the unit of `k` won't be inferred to be `"s"`.

### Evaluation
Unit variables can't be found to be `empty` or `undefined` unit.
This means that after evaluation, a unit meta-expression is either `undefined`, `empty` or does not contain any `undefined` or `empty` unit.

After evaluation a constraint will not contain both a well-formed unit and a `der` meta-expression.
This means that all constraints can be simplified to have at most one well-formed unit.

## Unit equivalence and convertibility

### Equivalence
Two well-formed units are said to be equivalent if both of the following are true:
* Their base unit factorizations are equal.
* Their base unit scalings are equal.

Examples:
- `"N"` and `"m/s2"` have different base unit factorizations, they are not equivalent.
- `"s"` and `"ms"` have different base unit scaling, they are not equivalent.
- `"K"` and `"degC"` are equivalent.
- `"kN"` and `"W.s/mm"` are equivalent.

It is a quality of implementation how equality of scalings are checked.

`empty` units and `undefined` units are always equivalent to each other, themselves and any `well-formed` units.

### Convertibility
A well-formed unit `u1` is said to be convertible to another well-formed unit `u2` if:
* Their base unit factorizations are equal.

Examples:
- `"N"` is not convertible to `"m/s2"`, since they have different base unit factorizations.
- `"s"` is convertible to `"ms"`, since they differ by base unit scaling.
- `"K"` is convertible to `"degC"`.
- `"kN"` is convertible to `"W.s/mm"`.

`empty` units and `undefined` units are never convertible to each other, themselves or any `well-formed` units.

## Unit inference

### Modified Hindley-Milner

The original Hindley-Milner algorithm is designed to infer types, and consists of building a set of substitutions that returns the type of any expression of a given program.
Applying this algorithm to perform unit inference in Modelica comes with two important differences:
* Not all variables must have a unit in a given model.
* There are operations on meta-expressions, that is, not all constraints are of the form _`var1.unit` is equivalent to `var2.unit`_.

#### Solvability of constraints

After meta-expression evaluation and gathering of variables in the single-sided form, a constraint's solvability can be determined using:
* A set `V` of unit variables present with non-zero exponent.
* A set `D` of unit variables, present inside a `der` meta-expression.

With this information, a variable `var.unit` can be solved from a given constraint if:
* `var.unit` is present in `V`, but not in `D`.
Comment on lines +133 to +134
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this was designed to handle der(x)=-x; without reporting errors.

However, as far as I understand that also means that if we have der(x)=velocity; we cannot deduce the unit of x, which would be a step backwards. To me it instead makes sense to state that only variables that occur exactly once can be solved from the constraint - which gives a similar result for this der(x)=-x;, but handles der(x)=velocity;.

The disadvantage is that we can then not deduce the unit of x in x*x=area; or x+x=area;. However, such examples are bit silly, especially as they can be written as x^2=area; or 2*x=area;.

Copy link
Collaborator Author

@qlambert-pro qlambert-pro Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If velocity is a wellformed unit, we can simplifies der in the constraint and D becomes empty. So we can in fact solve for x.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least this indicates that the rules are not very clear; if we have to sometimes simplify things in constraints etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

der

Consider the meta-expression der(e).

  • In a constraint where a well-formed unit is present, it evaluates to e * ("s" ^ -1).
    [...]

Evaluation

[...]
After evaluation a constraint will not contain both a well-formed unit and a der meta-expression.
...

Feel free to rephrase so that it is clearer.
At the end of the day the complexity associated with relaxing unit-checking is why we felt the need to spell out the algorithm in painstaking details in this document.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My proposal is not rephrase it, but instead state that only variables that only occur once in a constraint can have their unit inferred.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which would make the following invalid right?

model Test3
  Real x;
  Real y;
  equation
  der(x) = y;
  y = -x;
end Test3;

No, since we cannot start the process.
Similarly as not being able to deduce units for x and y based on:
sin(x/y)=...; cos(y*x)=...;

Copy link
Collaborator Author

@qlambert-pro qlambert-pro Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the language of this proposal, you would want for sin and cos to not impose any constraint on their argument, as well as expressions containing der to have undefined unit?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the language of this proposal, you would want for sin and cos to not impose any constraint on their argument, as well as expressions containing der to have undefined unit?

I would rather that:

  • sin and cos impose unit="1" (or "rad") for input.
  • sin and cos impose unit="1" for output.
  • der(x) impose that unit(der(x))=unit(x) * "s-1" (or something like that)

Copy link
Collaborator Author

@qlambert-pro qlambert-pro Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean that this model is valid according to your algorithm?

model Test4
  Real x;
  Real y;
  Real c(unit = "m");
equation
  c = x + x*y;
end Test4;

Copy link
Collaborator

@HansOlsson HansOlsson Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and x.unit="m" (based on c=x... where c.unit="m") and y.unit="1" (based on c=...+x*y; where c.unit="m" and x.unit="m"). I agree that it takes some care to specify the constraints that unit(a+b)=unit(a)=unit(b); to not get stuck on unit for x*y.


If a constraint has such a variable, the constraint is solvable.

#### Conditional constraints

One notion that doesn't exist in the original Hindley-Milner algorithm is that of a conditional constraint.
In Modelica, errors that are in unused part of a simulation model should typically be ignored (e.g., removed conditional components).
In the context of unit inference, some Modelica constructs generate constraints that are predicated on certain expressions having been evaluated or not.
The constraint conditions are conjunctive, in other words, if even a single condition evaluates to `false` the constraint should be discarded.

#### The algorithm

The Hindley-Milner algorithm modified to perform unit inference in Modelica:
* Traverse the flattened equation system and collect constraints before any evaluation has occured.
* After evaluable parameters have been evaluated, and expressions have been reduced to values when possible, evaluate the constraints, as well as their conditions. (This propagates `empty` and `undefined` units to the top of the constraints.)
* Discard any constraints of the form _`u` must be equivalent to empty unit_, or _`u` must be equivalent to undefined unit_, as they are trivially satisfied.
* Discard any constraints having at least a condition that was evaluated to `false`.
* Represent all constraints in their single-sided form.
* Start with an empty set `S` of substitutions.
* Loop:
* Select a solvable constraint or one that can be checked, if no such constraint exists exit the loop.
* If the constaint is in the form of just a `well-formed` unit, check that it is equivalent to `"1"`, it is an error otherwise.
* If it can be solved for `var.unit`:
* Replace the constraint by the substitution _`var.unit` => `u`_, where `var.unit` is not present in `u`.
* Apply the substitution to the right-hand side of the substitutions already in `S`.
* Add the substitution to `S`.
* Apply the substitution to all remaining constraints.
* The unit of a variable for which there is no substitution in `S` is unknown.
* For a variable `var` that has a substitution `var.unit` => `u` in `S`:
* If `u` is a `well-formed` unit, then this is the unit of `var`. (Given that no unit errors have been detected, a variable with a `unit`-attribute will get the given unit.)
* Otherwise, the unit of `var` is unknown.

## Unit constraints of a Modelica model

The following rules describe the unit meta-expression of a given Modelica expression, where it makes sense.
They also provide the constraints that arise from different Modelica constructs.

When it is specified that _`u1` must be equivalent to `u2`_, the corresponding constraint should be collected.

In this section, the unit meta-expression of expression `e` is called `e.unit` and conditions of conditional constraints are highlighted with [].

### Variables
If `var` has a declared `unit`-attribute, `var.unit` must be equivalent to it.

`var.unit` must be equivalent to the unit of the following attributes if present:
* `start`
* `min`
* `max`
* `nominal`

(`var.unit` should be convertible to the `displayUnit`-attribute of `var` if it is present, but there is no corresponding constraints for the unit inference.)

Consider the expression `e` of the form `var`, where `var` is a Modelica variable.
* If `var` doesn't have a declared unit attribute, and has constant component variablity, `e.unit` is the `empty` unit.
* Otherwise, `e.unit` is `var.unit`.

### Literals
`Real` literals have `empty` unit.

### Binary operations
Consider an expression `e` of the form `e1 op e2`.

#### Additive operators
* `e.unit` must be equivalent to `e1.unit`.
* `e.unit` must be equivalent to `e2.unit`.

Note that this requires introducing an intermediate variable to represent `e.unit`.
This allows unit propagation when one of the operand has `empty` unit.

#### Multiplication
`e.unit` is `e1.unit * e2.unit`.

#### Division
`e.unit` is `e1.unit * (e2.unit ^ -1)`.

#### Relational operators
`e1.unit` must be equivalent to `e2.unit`

#### Power operator
* `e.unit` must be equivalent to `e1.unit ^ e2`.
* If [`e2` wasn't evaluated or is a `Real` expression], `e1.unit` must be equivalent to `"1"`.

Note that this requires introducing an intermediate variable to represent `e.unit`.
Technically, `e2` is a Modelica expression rather than a rational number, which means that `e1.unit ^ e2` cannot be represented as a unit meta-expression.
In practice, if the condition of the second constraint is verified, `e1.unit ^ e2` is `"1"` and if it isn't verified, then it can be represented as a meta-expression.

(If the exponent was not evaluated or is a `Real`, the unit of the base should be `"1"`.)
(It is up for discussion, when `e2` is `Real`, whether `e2.unit` should be equivalent to `"1"`. It could be handled in a similar way as transcendental functions. See below.)

### Function calls
Consider the expression `e` of the form `f(e1, e2, …, en)`.
* If the output of the function has a declared unit, `e.unit` is the declared unit.
* `e.unit` is `undefined` otherwise.

If function input `i` has a declared unit, `ei.unit` must be equivalent to this unit.

The natural generalization to functions with multiple outputs applies.

### Transcendental functions
As examples of transcendental functions, consider `sin`, `sign`, and `atan2`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sign function is as far as I understand not a transcendental function, as they are defined as non-polynomial analytic functions, and I'm a bit unsure about atan2.

More importantly neither of those functions should use the rules for transcendental functions:

  • The sign-function is just comparing a value to zero; the unit of the input doesn't matter.
  • The atan2-function should ideally be defined as atan2(y, x)=atan(y/x); i.e. the two inputs should have the same unit - but it is perfectly normal to use atan2 for variables with unit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The sign-function is just comparing a value to zero; the unit of the input doesn't matter.

It does if it is a temperature.

  • The atan2-function should ideally be defined as atan2(y, x)=atan(y/x); i.e. the two inputs should have the same unit - but it is perfectly normal to use atan2 for variables with unit.

This is what is specified here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The sign-function is just comparing a value to zero; the unit of the input doesn't matter.

It does if it is a temperature.

No.

Or with more words: it's more complicated - the unit sort of matters if it is an absolute temperature, but not if it is a temperature difference (and the unit system doesn't separate the two). And in the SI-system, which we base the unit handling on, absolute temperatures are measures in Kelvin and using the sign for an absolute temperature in Kelvin is kind of meaningless.

Similarly you should normally use sign for voltages, not potentials; and for relative positions (or velocities) - not absolute positions. The unit system doesn't separate them, and there are lots of models using sign for variables with units (fluid, mechanical, etc) - and they make sense.

Copy link
Collaborator Author

@qlambert-pro qlambert-pro Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough for sign, I agree that we would need to ensures that the value is a difference.

Copy link
Collaborator Author

@qlambert-pro qlambert-pro Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just had another look at sign, the specification doesn't enforce any unit on its argument, it is just specifying that sign(-1) should behave as a literal.

Other transcendental functions can be handled similarly.

Consider expression `e` of the form `sin(e1)`.
* If `e1.unit` is `empty`, then so is `e.unit`.
* If `e1.unit` is not `empty`, then it must be equivalent to `"1"` and `e.unit` is `"1"`.

Note that some of these constraints can only be processed once `e1.unit` has been determined.
This avoids introducing a unit `"1"` in the inference algorithm.

Implementation note: The rule above can be implemented as making `e.unit` equivalent to `e1.unit`, and if `e1.unit` is `well-formed` after completed unit inference, then verify that it is equivalent to `"1"`.

Consider an expression `e` of the form `sign(e1)`.
* If `e1.unit` is `empty`, then so is `e.unit`.
* If `e1.unit` is not `empty`, then `e.unit` is `"1"`.

Consider an expression `e` of the form `atan2(e1, e2)`.
* If `e1.unit` must be equivalent to `e2.unit`.
* If `e1.unit` or `e2.unit` is `undefined`, then so is `e.unit`.
* If `e1.unit` and `e2.unit` are `empty`, then so is `e.unit`.
* If `e1.unit` or `e2.unit` is not `empty`, then `e.unit` is `"1"`.

Implementation note: The rule above can be implemented as making `e.unit` equivalent to both `e1.unit * e1.unit^-1` and `e2.unit * e2.unit^-1`.

### Operator der
The unit of the expression `der(e)` is `der(e.unit)`.

### If-expressions
Consider the expression `e` of the form `if cond then e1 else e2`.
If [`cond` was not evaluated]:
* `e.unit` must be equivalent to `e1.unit`.
* `e.unit` must be equivalent to `e2.unit`.

If [`cond` was evaluated], `e.unit` must be equivalent to the unit of the selected branch.

Note that this requires introducing a intermediate variable to represent `e.unit`.

### Equations
Both sides of binding equations, equality equations and connect equations must be unit equivalent.

## Modelica operators on units

The following operators facilitate writing unit-consistent models.

### `withUnit($value$, $unit$)`
Creates an expression with unit `$unit$`, and value `$value$`.
Argument $value$ needs to be a `Real` expression, with `empty` unit.

One application of this operator is to attach a unit to a literal.

### `withoutUnit($value$, $unit$)`
Creates an expression with empty unit, whose value is the numerical value of `$value$` expressed in `$unit$`.
Argument `$value$` needs to be a `Real` expression, with a well-formed unit.
The unit of `$value$` must be convertible to `$unit$`.

### `inUnit($value$, $unit$)`
Creates an expression with unit `$unit$`, whose value is the conversion of `$value$` to `$unit$`.
Argument `$value$` needs to be a `Real` expression, with a well-formed unit.
The unit of `$value$` must be convertible to `$unit$`.

## Possible extensions and current limits

### Units with offset
The current proposal doesn't allow distinguishing `"K"` and `"degC"`.
In order to do that one needs to keep track of whether a variable is meant to represent a difference or not.
We have a proof of concept for such a feature, and the mechanism is similar and orthogonal to the one described here.

### Builtin operators and functions
The current state of this proposal does not cover the unit checking of all Modelica builtin operators and functions.

### Arrays
We don't try to specify how this would work on arrays, although the current proposal works straight-forwardly with element-wise operators.
We are confident that it could be extended to matrix computations too, but haven't a proof of concept for that part.

### Constants
This proposal takes a stance regarding how the unit of constants should be handled but we recognise that this is still very much up for discussion.
It is also somewhat orthogonal to the rest of the proposal.

### Functions
The Hindler-Milner algorithm provides a way to infer unit attributes of the inputs and outputs of functions.
Concretely, it would imply computing the bundles of constraints from the body of each functions and adding them to the algorithm, after adequate substitutions, when processing a function call.

### Variable initialisation pattern
A common pattern used in the MSL is the following:
```
model M
Real d(unit = "s") = 12;
Real v(unit = "m/s") = 0.1 / (3 * d);
end M;
```
where a modeler knows how to express a variable with unit as a function of another, with help of literal values that should really carry a unit.
The way to fix that, with this proposal, is to attach a unit to the literal values using `withUnit`.

### Syntax extension
We could extend the Modelica language to add syntactic sugar for `withUnit`.
The following variants have all been successfully test implemented:
* `0.1["m"]`
* `0.1 'm'` (or as `0.1'm'` to show more clearly that the quoted unit belongs to the literal)
* `0.1."m"`

### unsafeUnit operator
Another way to deal with the pattern described above would be to provide an unsafe operator that effectively drops any constraints generated in its subexpression.
The whole expression would have `undefined` unit.
34 changes: 34 additions & 0 deletions RationaleMCP/0027/ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Modelica Change Proposal MCP-0027<br/>Units of Literal Constants
Francesco Casella, Henrik Tidefelt

**(In Development)**

## Summary
The purpose of this MCP is to allow more unit errors to be detected by giving more expressions the unit `"1"` instead of having an undefined unit.
The problem with undefined unit is that it gets in the way of carrying out checking of units (which tools tend to deal with by not checking units at all where this happens).

## Revisions
| Date | Description |
| --- | --- |
| 2022-10-04 | Henrik Tidefelt. Filling this document with initial content. |

## Contributor License Agreement
All authors of this MCP or their organizations have signed the "Modelica Contributor License Agreement".

## Rationale
FIXME

## Backwards Compatibility
As current Modelica doesn't clearly reject some models with non-sensical combination of units, this MCP will break backwards compatibility by turning at least some of these invalid.

## Tool Implementation
None, so far.

### Experience with Prototype
N/A

## Required Patents
To the best of our knowledge, there are no patents that would conflict with the incorporation of this MCP.

## References
(None.)