Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Libraries

You have been using libraries since Chapter 2. Every time you wrote import ScalarValues::*, you reached into a model library that ships with the specification. This chapter makes that implicit knowledge explicit: what those libraries are, how they are layered, how to find things in them, and how to write one of your own.

The single most useful idea in this chapter is that the library is not a convenience bundle shipped alongside the language. A large part of what the language means is defined by library models, written in the same language you write. That is a deliberate design decision, and it is the reason this chapter is worth your time.

Why the Library Is Part of the Language

KerML is built in three syntactic layers, each adding more specific constructs on the one below (KerML §6.1):

  • The Root Layer holds the most general constructs for structuring models: elements, relationships, annotations, packaging.
  • The Core Layer holds the most general constructs whose semantics are based on classification.
  • The Kernel Layer provides commonly needed modeling capabilities, such as associations and behavior.

Only the Core Layer is grounded directly, by interpreting it in mathematical logic. The Kernel Layer is different: its meaning comes from relating its abstract syntax constructs to model elements in the Kernel Semantic Library, which is written in KerML itself (KerML §6.1). A model you write reuses elements of that library in order to have any semantics at all, and the library models are what state the conditions under which a real system conforms to your model.

The specification is explicit about the payoff, and it is aimed squarely at you: because the library models are expressed in the same language as user models, engineers and tool builders can inspect them to understand what a model actually specifies (KerML §6.1).

So when this chapter tells you to go and read a library file, that is not a debugging tip. It is the intended way to find out what your own model means.

The Library Is Written in the Language

Here is the actual declaration of Real, from ScalarValues.kerml in the shipped standard library (the doc comments are elided):

standard library package ScalarValues {
    private import Base::DataValue;

    abstract datatype ScalarValue specializes DataValue;
    datatype Boolean specializes ScalarValue;
    datatype String specializes ScalarValue;
    abstract datatype NumericalValue specializes ScalarValue;

    abstract datatype Number specializes NumericalValue;
    datatype Complex specializes Number;
    datatype Real specializes Complex;
    datatype Rational specializes Real;
    datatype Integer specializes Rational;
    datatype Natural specializes Integer;
    datatype Positive specializes Natural;
}

That is the whole file. Eleven datatype declarations and one private import.

A model library is a collection of models meant to be reused across many user models, marked with the library keyword so tooling can recognize its contents as library elements (KerML §7.4.14). The libraries that ship with the specification go one step further and add standard, a marker reserved for the kernel and systems libraries and other recognized standard libraries (KerML §7.4.14).

This explains something you may have wondered about. Real is not a reserved word. It is not in the grammar. It is an ordinary datatype declared in an ordinary package, and you import it for exactly the same reason you import CoffeeBeans from your own package – because it lives somewhere else. Misspell it and you get an unresolved name, not a syntax error. Shadow it with a local declaration and the local one wins.

The same is true all the way down. part def Grinder works because Parts.sysml in the Systems Model Library declares abstract part def Part :> Item;, and your definition picks that up implicitly (SysML §9.1; see Chapter 12). Arithmetic works because RealFunctions.kerml declares functions named '+' and '*'.

The clearest case is ordering. When you write a succession in an action body – grind, then brew – and give it no explicit type, what you have created is a connector typed by a library association. Occurrences.kerml declares it:

assoc all HappensBefore specializes HappensLink, Without { <doc and end features> }

An ordering constraint, which certainly feels like it ought to be a grammar rule, turns out to be an association in a library file you can open and read. Nothing here is built in that could have been written down instead.

The Three Layers

The libraries are stacked in three layers, from general to specific.

LayerWhere it is specifiedWhat lives there
Kernel Model LibraryKerML Clause 9The Semantic Library (§9.2), the Data Type Library (§9.3), the Function Library (§9.4)
Systems Model LibrarySysML §9.2Parts, Items, Ports, Attributes, Actions, States, Requirements, Views, and 13 more
Domain LibrariesSysML §9.3 – §9.8Metadata, Analysis, Cause and Effect, Requirement Derivation, Geometry, Quantities and Units

The Kernel Model Library is the foundation. The Semantic Library gives every element its meaning – Base, Occurrences, Performances, Transfers, Links, Objects and the rest (KerML §9.2). The Data Type Library holds the values: ScalarValues, Collections, VectorValues (KerML §9.3). The Function Library holds everything an expression can call (KerML §9.4). You name the Data Type Library constantly and the other two almost never.

The Systems Model Library is SysML’s own layer, and it is what turns a KerML classifier into a part, an action, or a requirement. You rarely import from it by hand, because every definition and usage you write already specializes something in it implicitly. part def Grinder picks up Parts::Part whether you ask for it or not.

Domain Libraries are the ones you import deliberately. They model concepts from domains of particular importance in systems engineering, and they are normative – available in every conformant model, not an optional add-on (SysML §9.1). Six of them ship: the Metadata Domain Library (§9.3), Analysis (§9.4), Cause and Effect (§9.5), Requirement Derivation (§9.6), Geometry (§9.7), and Quantities and Units (§9.8).

Loading is a tooling concern. In sysml-rs the kernel and systems layers load in every project with no configuration, while the domain libraries are opt-in through the [stdlib] section of sysml.toml. That split is the toolchain’s, not the language’s – the spec calls all six domain libraries normative. Chapter 15 covers the manifest.

What a Wildcard Import Actually Brings In

You have written import ScalarValues::* in every chapter. Now you can say precisely what it does, because you have seen the file. It brings in eleven names:

ScalarValue, Boolean, String, NumericalValue, Number, Complex, Real, Rational, Integer, Natural, Positive.

It does not bring in DataValue, even though ScalarValues imports it, because that import is written private import Base::DataValue. Which brings us to the rule that matters most when you start writing libraries of your own.

<visibility> import <QualifiedName>::*;
library package CoffeeIndustryTypes {
    private import ScalarValues::*;
    public import ISQ::CelsiusTemperatureValue;

    enum def RoastLevel {
        light;
        medium;
        dark;
    }

    item def CoffeeBeans {
        attribute roast : RoastLevel;
        attribute origin : String;
        attribute recommendedBrewTemp : CelsiusTemperatureValue;
    }
}

An import has its own visibility, independent of the visibility of the things it imports. A public import re-exports: anyone who imports CoffeeIndustryTypes also gets CelsiusTemperatureValue. A private import does not: String is usable inside CoffeeIndustryTypes but does not leak out of it.

The trap is the default. An import written with no visibility keyword at all is private (KerML §7.2.5.4). So this consumer does not compile the way you might expect:

package CoffeeMachineModel {
    import CoffeeIndustryTypes::*;
    import ScalarValues::*;

    part def Grinder {
        attribute currentBeans : CoffeeBeans;
        attribute targetTemp : CelsiusTemperatureValue;
    }
}

CoffeeBeans and RoastLevel arrive because they are owned public members. CelsiusTemperatureValue arrives only because the library re-exported it with public import. Real and String do not arrive through CoffeeIndustryTypes at all – hence the second import line. If you are writing a library and you want consumers to get a type you depend on, you must say public import. The standard library is meticulous about this: ISQ.sysml writes private import ScalarValues::Real; for its internal need and public import ISQBase::*; for the twelve ISO 80000 parts it exists to aggregate.

The other thing to know about wildcards is what happens on a collision. If an imported name clashes with an owned member, or with a visible name from another imported namespace, the conflicting membership is hidden – it is simply excluded from the importing namespace (KerML §7.2.5.4). There is no error. Two wildcard imports that both export Temperature will quietly cancel each other out, and the failure surfaces later as an unresolved name at the point of use. This is the concrete cost of a wildcard import, and it is why the advice from Chapter 4 hardens as a model grows: wildcard while exploring, explicit once it matters.

A false positive to expect. sysml-rs currently reports IM006import references unknown standard library namespace – for any single-member import from a standard library package, including import ScalarValues::Real;. It fires only in workspace mode (sysml inspect --workspace); the same file checked on its own is clean, and wildcard forms like import ISQ::* are never flagged. The check is reading the final segment of a membership import as though it named a package. The language is fine here and so is your model – import ScalarValues::Real is exactly the form Chapter 4 recommends. Do not restructure your imports to silence it.

There is a third form worth knowing, because the library documentation uses it:

import <QualifiedName>::**;

The ::** suffix makes the import recursive – it imports the namespace, its visible members, and then recurses into each imported member that is itself a namespace (KerML §7.2.5.4). Reach for it rarely. import ISQ::** is a very large hammer, as the next section makes clear.

Quantities and Units

attribute waterTemp : Real says almost nothing. Celsius, Fahrenheit, kelvin? The Quantities and Units Domain Library (SysML §9.8) exists to close that gap, and it is the most practical thing in this chapter.

Two packages do the work. ISQ gives you the quantity types – the International System of Quantities from ISO/IEC 80000. SI gives you the units. A dimensioned literal pairs a number with a unit:

<number> [ <unit> ]
package CoffeeMachineWithUnits {
    import ISQ::CelsiusTemperatureValue;
    import ISQ::PressureValue;
    import ISQ::VolumeValue;
    import ISQ::DurationValue;
    import SI::*;

    part def Brewer {
        attribute waterTemp : CelsiusTemperatureValue;
        attribute brewPressure : PressureValue = 9.0 [SI::Pa];
        attribute brewTime : DurationValue = 25.0 [SI::s];
    }

    part def WaterTank {
        attribute currentLevel : VolumeValue;
        attribute maxCapacity : VolumeValue = 1.5 [SI::L];
    }
}

The square brackets are not array syntax. [ is a genuine operator, declared in the library as calc def '[' { in num: Number[1]; in mRef: ScalarMeasurementReference[1]; return quantity : ScalarQuantityValue[1]; } in QuantityCalculations. It takes a number and a measurement reference and constructs a quantity value. The specification uses the same form in an accept action: accept after 30 [SI::s] (SysML §8.4.13.6).

SI::Pa, SI::s, SI::L are the unit short names. SI.sysml declares each unit with both, as in attribute <Pa> pascal : PressureUnit = N/m^2; – so SI::Pa and SI::pascal name the same thing, and the unit’s definition in terms of the base units is right there in the model.

Watch the temperature names

This is worth a paragraph because it will bite you. There is no ISQ::TemperatureValue declared anywhere. The name resolves, but only as an alias: ISQThermodynamics.sysml contains alias TemperatureValue for ThermodynamicTemperatureValue;. Thermodynamic temperature is measured in kelvin. So attribute waterTemp : ISQ::TemperatureValue = 93.0 [SI::K] describes water 180 degrees below freezing.

There are three temperature quantities, and the distinction between them is real physics rather than library clutter:

package TempOptions {
    import ISQ::ThermodynamicTemperatureValue;
    import ISQ::CelsiusTemperatureValue;
    import ISQ::TemperatureDifferenceValue;
    import SI::*;

    part def Brewer {
        attribute targetTemp : ThermodynamicTemperatureValue = 366.15 [SI::K];
        attribute elementRise : TemperatureDifferenceValue = 6.0 [SI::'°C'];
        attribute brewTemp : CelsiusTemperatureValue = 93.0 [SI::'°C_abs'];
    }
}

ThermodynamicTemperatureValue (ISQBase.sysml) is absolute temperature in kelvin. CelsiusTemperatureValue (ISQThermodynamics.sysml) is the Celsius reading. TemperatureDifferenceValue is declared in ISQ.sysml itself, and its doc comment explains why it has to exist separately: a difference of 6 °C is not the same kind of quantity as a temperature of 6 °C.

The unit names repay a close look, because they are how the library encodes that difference. SI::'°C' is typed TemperatureDifferenceUnit, and it converts to kelvin with a factor of 1. SI::'°C_abs' is not a unit at all: it is typed IntervalScale, which sits under MeasurementScale rather than MeasurementUnit, and it carries an explicit zero shift against the kelvin scale.

The reason for that split is visible in the library. UnitConversion declares exactly three features – referenceUnit, conversionFactor, isExact. There is no offset anywhere in it, so unit conversion is purely multiplicative. An absolute Celsius reading needs a zero shift, so it cannot be expressed as a MeasurementUnit, and the library models it as a scale instead. Celsius is an interval scale, not a ratio scale, and the type hierarchy says so.

The 273.15 is not missing, either – it is inside the '°C_abs' declaration, as a placement of one coordinate frame relative to another:

private attribute zeroDegreeCelsiusInKelvin: ThermodynamicTemperatureValue = 273.15 [K];
attribute zeroDegreeCelsiusToKelvinShift : CoordinateFramePlacement :>> transformation {
    :>> source = K; :>> origin = zeroDegreeCelsiusInKelvin;
}

That is worth noticing, because it reframes what looks like a gap. The library has not forgotten about Celsius. It has decided that a scale with a shifted origin is a coordinate frame placement, not a unit conversion – which is precisely why Celsius cannot be another derived unit in the manner of km or kPa.

Quote both names – ° is not an identifier character, so SI::'°C' needs the single quotes.

Note that SI ships no instance typed CelsiusTemperatureUnit, the type that CelsiusTemperatureValue redefines its mRef to. The interval scale is what you use in practice, and sysml-rs accepts it. SI.sysml is candid about gaps of this kind in its own header: “This is a representative but not yet complete list of measurement units.” When you need a unit the library does not define, declare it in your own library package rather than waiting for one to appear.

The scale of it

Some sense of proportion, since it bears on how you import. ISQ is almost entirely an aggregator: twelve public imports, one for base quantities and eleven for the numbered ISO/IEC 80000 parts, plus the three temperature-difference declarations it owns. Those twelve packages declare 945 attribute defs between them, of which 524 are distinct *Value quantity types, plus 250 aliases. SI adds 255 units. SI also does public import ISQ::*, so import SI::* pulls every quantity type in behind the units.

That is fine – name resolution does not care about volume – but it is a lot of surface for a collision to hide in. If your model needs four quantity types, import four quantity types.

What sysml-rs actually checks. The runtime carries dimensions through expressions and reports mismatches as UQ001. Given attribute confused : DurationValue = brewPressure * brewTime; it reports: attribute ‘confused’ declares dimension [T] but its default value has dimension [L^-1·M·T^-1]. Two limits are worth knowing. A bare literal is not compared against its attribute’s type, so attribute brewPressure : PressureValue = 9.0 [SI::s]; passes silently; the check fires on expressions and on references to other attributes. And mixing dimensions under + yields an unconstrained result rather than an error, so brewPressure + brewTime is accepted. Dimensional checking here catches real mistakes but is not yet a proof of dimensional soundness.

Finding Things in the Library

The question you will actually have is “does something for this already exist, and what is it called?” You do not need to guess. The library is source on disk, in libraries/standard/, split into library.kernel/, library.systems/, and library.domain/.

Two habits answer almost everything.

Grep the library. Functions are declared two different ways, so search for both. KerML uses function with no def; SysML uses calc def:

grep -rnE "(function|calc def) '?sqrt'?" libraries/standard/
library.domain/Quantities and Units/QuantityCalculations.sysml:46:	calc def sqrt{ in x: ScalarQuantityValue[1]; return : ScalarQuantityValue[1]; }
library.kernel/RealFunctions.kerml:39:	function sqrt{ in x: Real[1]; return : Real[1]; }

Two answers, and the signatures tell you which one applies to which argument type. The '? in that pattern matters: many library names are quoted, and a plain grep "function sqrt" would have missed nothing here but will give you false negatives elsewhere.

For a quantity type, search the units directory and search for aliases separately, or you will conclude a name does not exist when it does:

grep -rn 'TemperatureValue' 'libraries/standard/library.domain/Quantities and Units/'

Evaluate the name. sysml eval runs a single expression with no model file, which confirms in about a second that a function exists and behaves the way you think:

sysml eval 'sqrt(16.0)'      # 4
sysml eval 'max(3.0, 7.5)'   # 7.5
sysml eval 'round(3.5)'      # 4
sysml eval 'deg(3.14159)'    # 179.9998479605043
sysml eval 'size(1..3)'      # 3

Two caveats on eval. It tells you what this toolchain evaluates, which is a subset of what the library declares. StringFunctions declares Length, but sysml eval 'Length("espresso")' reports unknown function: Length – the name is real, the evaluator has not implemented it. And eval has no model in scope, so anything needing a type or a feature reference has to go in a file instead. Read the library for what the language declares; use eval for what you can run today.

One habit not to rely on: a clean parse. sysml inspect --diagnostics does not report unresolved import names, so a file full of misspelled library types comes back with zero diagnostics. Confirm names against the library, not against a green parse.

Reading a kernel library file

Open a file under library.systems/ and it looks like the SysML you have been writing all book. Open one under library.kernel/ and the first line may not. The kernel libraries are written in KerML, one layer below SysML, and they use declaration keywords SysML models never need. Here is enough to stop being surprised by them.

KerMLWhat it declaresReal use
structA structure. SysML’s part def and item def sit on this.abstract struct Object specializes Occurrence
assocAn association – a relationship as a classifier.assoc all HappensBefore specializes HappensLink, Without
assoc structAn association that is also a structure, so the link itself has features.abstract assoc struct LinkObject specializes Link, Object
functionA function. This is the KerML spelling; SysML writes calc def.function sqrt{ in x: Real[1]; return : Real[1]; }
behaviorA behavior. SysML’s action def sits on this.abstract behavior Performance specializes Occurrence
invAn invariant – a constraint that must hold.inv { flattenedSize == size(elements) }
boolA boolean expression, not a boolean type.bool guard[*] subsets enclosedPerformances;

bool is the one most likely to mislead, so it is worth being explicit: the grammar production is BooleanExpression, and bool introduces an expression that yields a boolean. It is not a shorthand for the Boolean datatype.

You will also meet disjoint, which asserts that two types can have no common instances:

abstract behavior Performance specializes Occurrence disjoint from Object { <body> }

That says a performance is never an object – something happening is not something existing. It reads oddly the first time and then becomes one of the more useful things in the semantic library.

Three further keywords exist for writing a relationship as a standalone, separately named declaration rather than inline: subclassifier X specializes Y, disjoining <name> disjoint X from Y, and inverting <name> inverse X of Y. They are rare – across all 36 kernel library files, subclassifier is used once and the other two never. If you meet them, they are the same relationships in a different shape.

That is the whole surprise. The kernel libraries are not a different language; they are the layer SysML’s own keywords are defined on top of, which is exactly what KerML §6.1 says they are.

The Function Library

The libraries are not only a type vocabulary. They also declare every function an expression can call:

package TankMath {
    import ScalarValues::*;

    part def WaterTank {
        attribute maxCapacity : Real default = 1500.0;
        attribute currentLevel : Real := 0.0;

        attribute headroom : Real = max(0.0, maxCapacity - currentLevel);
        attribute fillFraction : Real = currentLevel / maxCapacity;
    }
}

max is not built into the language. It is declared in RealFunctions, and it is in scope because the Function Library loads with every project. Here is the useful subset, with the package each name actually comes from:

GroupPackageNames
Real arithmeticRealFunctionsabs, min, max, sqrt, floor, round, sum, product, re, im, arg
Cross-numericNumericalFunctionsisZero, isUnit, abs, min, max, sum, product, sum0, product1
ConversionsRealFunctions, IntegerFunctions, RationalFunctionsToInteger, ToRational, ToReal, ToNatural, ToString
TrigonometricTrigFunctionssin, cos, tan, cot, arcsin, arccos, arctan, deg, rad
StringsStringFunctionsLength, Substring, ToString
SequencesSequenceFunctionssize, isEmpty, notEmpty, includes, excludes, union, intersection, including, excluding, subsequence, head, tail, last
CollectionsCollectionFunctionssize, isEmpty, notEmpty, contains, containsAll, head, tail, last
Iteration and filteringControlFunctionsselect, selectOne, collect, reject, reduce, forAll, exists, allTrue, anyTrue, minimize, maximize
OccurrencesOccurrenceFunctionsisDuring, create, destroy, addNew, addNewAt
QuantitiesQuantityCalculationsConvertQuantity, ToDimensionOneValue, plus dimension-aware abs, min, max, sqrt, sum, product

Two things to keep straight.

First, arithmetic, ordering and equality are written as operators, not as function calls. Exponentiation is ** or ^; ordering is < > <= >=; inequality is !=. There is no pow, no lessThan, no notEquals – reach for the operator. Appendix A has the full operator table.

But – and this is the interesting part – those operators are library functions. RealFunctions declares function '**' and function '<'. BaseFunctions declares function '==', function 'istype', function 'as' and function 'meta'. ControlFunctions declares function 'if', function 'and', function 'or', function 'implies'. The names are quoted because they are not identifiers, and that is the only reason a grep for function istype comes back empty while function 'istype' finds it. So the operators are not a separate mechanism bolted onto the grammar; they are library functions with special syntax, which is why QuantityCalculations can declare its own '+' and give addition a dimension-aware meaning for quantity values.

Second, that table is a subset, not an inventory. Each number type has its own package (IntegerFunctions, NaturalFunctions, RationalFunctions, ComplexFunctions, VectorFunctions), and there are more for booleans, data values and scalars. Look for a library name before writing your own calc def.

Writing Your Own Library Package

Everything above applies to libraries you write. The declaration takes one extra keyword:

library package <Name> { ... }
library package CoffeeIndustryTypes {
    private import ScalarValues::*;
    public import ISQ::CelsiusTemperatureValue;

    enum def RoastLevel {
        light;
        medium;
        dark;
    }

    item def CoffeeBeans {
        attribute roast : RoastLevel;
        attribute origin : String;
        attribute recommendedBrewTemp : CelsiusTemperatureValue;
    }
}

library marks the package as reusable vocabulary rather than part of one particular design, which lets tooling treat everything inside it as a library element (KerML §7.4.14). Define domain vocabulary once here, import it everywhere else, and the definitions cannot drift apart.

Three things to get right when you write one:

  • Re-export what your consumers need. public import for anything that appears in a signature they will use; private import for your internal dependencies. Getting this backwards produces a library whose types nobody can name.
  • Keep the export surface small. Your consumers will wildcard-import you. Every public name is a potential silent collision.
  • Do not write standard. standard library package is reserved for the kernel and systems libraries and other recognized standard libraries (KerML §7.4.14). It is not a quality marker for your own work.

Libraries the Runtime Recognizes

A handful of library packages are structural entry points: specialize from them and a simulation runtime knows what your model means without further configuration. They span two layers, which is worth keeping straight when you go looking for them on disk.

From the Analysis Domain Library (SysML §9.4):

PackageWhat it provides
StateSpaceRepresentationThe ODE pattern: calc def GetDerivative and calc def GetOutput to subtype, action def ContinuousStateSpaceDynamics to specialize (see Chapter 16)
TradeStudiesTradeStudy, EvaluationFunction, MinimizeObjective, MaximizeObjective
SampledFunctionsSampledFunction, SamplePair, and the Domain / Range / Sample / Interpolate calculations
AnalysisToolingExactly two declarations, both metadata: metadata def ToolExecution and metadata def ToolVariable, for tying a model to an external analysis tool

From the Kernel Model Library (KerML §9.2):

PackageWhat it provides
OccurrencesOccurrence, Life, and the snapshots / startShot / endShot structure – anything with temporal extent
ClocksClock, BasicClock, and the universalClock feature
SpatialFramesSpatialFrame, CartesianSpatialFrame, and the defaultFrame feature
PerformancesThe Performance and Evaluation hierarchy behind actions and expressions

Typing for dynamics. In sysml-rs, when the simulation runtime sees connections between ISQ-typed ports it classifies each port’s role in a bond graph – effort, flow, displacement, momentum – and synthesizes the conservation laws from the dimensions. Nobody writes the balance equations by hand; the runtime reads them off the types. The rule of thumb is to type any attribute that participates in dynamics with an ISQ quantity rather than Real. Appendix E has the details.

What You Have Built

You now know what the standard library is, and why it is not optional. It is roughly ninety files of KerML and SysML source, layered as kernel, systems, and domain, and a large share of the language’s meaning is defined there rather than in the grammar – which is why the specification expects you to read it. You can say exactly what import ScalarValues::* brings into scope and why import without a visibility keyword does not re-export. You can attach real dimensions to the coffee machine’s attributes with ISQ and SI, you know which of the three temperature types you actually want, and you know how far your tool’s dimensional checking goes. You can open a kernel file without being stopped by assoc or inv. And when you need a function, you have two ways to find out whether it already exists rather than writing a calc def the library already has.

The remaining question is where the library comes from – which libraries a project loads, how versions get pinned, and how you ship your own library package so somebody else can depend on it. That is Chapter 15.