-
Notifications
You must be signed in to change notification settings - Fork 21
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
Add streamline resource framework #1253
Conversation
ae8331b
to
ec262c7
Compare
ec262c7
to
c1006cb
Compare
c1006cb
to
12e0ed6
Compare
Adds the core resource interfaces. A Resource is defined as a ThinResource (equivalent to a Supplier) returning an ErrorCatching and Expiring Dynamics. This mirrors the information stored in a cell, which has proven useful to carry with every value tracked by the model.
To support methods like `map` accepting functions of varying arities, we need to define a function interface for each supported arity above 3. Similarly, we need to define `map` and `bind` methods for every supported arity for each monad. Since Java's type system doesn't support abstraction across type functors, we do this by generating the methods with a python script instead.
Adds monads for all core interfaces: * ExpiringMonad * ErrorCatchingMonad * ThinResourceMonad These monads abstract the handling of expiries, errors, and stitching together multiple dynamics segments into a resource. Also adds two monads that compose the above: * DynamicsMonad = ExpiringMonad + ErrorCatchingMonad * ResourceMonad = ThinResourceMonad + DynamicsMonad Monad users can generally write code that operates on base dynamics objects, and use the monad methods to lift that code to fully-wrapped dynamics or resources. This makes sure the wrapping layers are handled correctly and consistently, and keeps downstream code more focused.
Adds MutableResource and supporting types for defining cells and effects. This design emphasizes separation of concerns in two primary ways: * First, since every resource dynamics carries an expiry and stepping behavior, we don't need to define a different cell type depending on how that value is computed (like an Accumulator) nor by what kind of dynamics are stored (e.g. Discrete vs. Real). * Second, since the DynamicsEffect interface defines a fully general effect type, we don't need to define a different cell type depending on the supported class of effects. Taken together, we can define a single cell type. This design also seeks to reduce overhead for modelers to handle effects the "right" way. By this, we mean using semantically correct effects, rather than (ab)using Registers for everything. * Instead of defining a new type for effects, we use a general DynamicsEffect interface. We also have the DynamicsMonad.effect method, so effects can be written against the base dynamics type, often as a small in-line lambda. * To support these "black-box" effects, we use an "automatic" effect trait by default, which tests concurrent effects for commutativity. Since effects are rarely concurrent in practice, this is performant enough in most use cases. Furthermore, it combines with the error-handling wrapper to bubble-up useful error messages, as well as let independent portions of the simulation continue normally. Taken together, the above means there's a single "default" way to build a cell, which provides enough flexibility and performance for most use cases.
Adds tools for debugging incorrect or poorly-performing simulations. * Context and Naming - These allow us to attach names to scopes in the code and to objects in memory. At strategic points, we can query this information to bubble up to the user. For example, we can attach the context an effect was emitted in to that effect. If the effect fails, we can inject that context into the error, which is more useful to debugging than a stack trace. Similarly, we can name resources when they're registered, and we can derive names for one object from others. When debugging resources, this can de-anonymise the lambda functions that make up the bulk of a resource model. * Tracing - Since debuggers struggle to "step" across simulation engine iterations, we borrow tracing techniques from functional programming. Tracing attaches print statement when a resource, task, or condition starts and stops. We also respect nested tracing, yield a log that mirrors the model's structure and lets a programmer "unpack" derived values step-by-step. * Profiling - Since all resource calculations often have the same or very similar stack frames, profilers often don't supply a useful level of detail. We provide profiling tools that distinguish calls by instance, to tell which resources / cells / conditions / tasks are hot spots. * Dependencies - Since it's sometimes useful to visualize the structure of a resource model, we track resource-level dependencies, and these can be queried from a debugger or debugging code.
Adds a registrar wrapping and adapting the standard Merlin Registrar. This registrar unwraps the ErrorCatching and Expiring layers from resources, with options to either throw or log errors.
Adds the Discrete dynamics type, as well as utilities for working with discrete Resources: * DiscreteEffects - utility methods for common operations on MutableResource<Discrete>, like increment/decrement, set value, etc. * DiscreteResources - utility methods for defining and deriving discrete resources, including integer and double-precision arithmetic, boolean logic, etc. Notable / unusual features of this code include: * `DiscreteResources.when` - Converts a discrete boolean resource into a condition, satisfied when the resource is true. This realizes the equivalence between conditions and boolean resources. By deriving boolean resources, we get the equivalent condition with no additional code. * `DiscreteResources.discreteResource` - Declares a discrete MutableResource, with special handling for Double values. When effects are applied to doubles, floating-point precision mismatches can make effects that logically commute appear not to commute. This special constructor for MutableResources uses a toleranced equality for checking commutativity to solve this problem. * Monads - The `Discrete` wrapper forms a (trivial) monad, which can be composed with the other major monads. This means derivations on discrete resources can use the value directly, and monad methods will lift that all the way to acting on resources.
Adds the Linear dynamics type, the analog of Merlin's Real type. This type is named Linear rather than Real, to distinguish it from Polynomial or other potentially real-valued dynamics types.
Adds Clock and VariableClock dynamics types, both of which use Durations to exactly represent time. Using Duration avoids floating-point issues that crop up when using Linear or Polynomial resource to represent time, as well as not needing conversions to and from Duration. Of particular note are the stopwatch-style effects and resource-level comparison functions for VariableClock. Combining these with `DiscreteResources.when` and `Reactions` is especially useful for expressing time-based conditions and behaviors.
Adds polynomial resources, which are the primary non-discrete resource type. Of particular note are the arithmetic and comparison functions for derivations to and from polynomials. These functions incorporate higher-order coefficients correctly, including performing root-finding to calculate when comparisons will expire.
Adds a solver for linear inequality constraints posed on polynomial resources, which proceeds by boundary consistency without backtracking. For some resource problems, specifying behavior in terms of comparisons and arithmetic can be hard to ready and error-prone. In particular, PolynomialResources.clampedIntegrate was found to be this way. These problems are often readily formulated as linear inequality constraints over variables with polynomial dynamics segments for values. Since polynomials form a vector space over the real numbers, such linear inequalities are amenable to solving by boundary consistency without backtracking.* Additionally, by specifying how to resolve under-constrained problems, a simple greedy optimizer can be defined for linear objective functions. See `PolynomialResources.clampedIntegrate` for an example of how this solver is used. *Linear programming can't be used because that requires division.
Adds types and utilities for unit-awareness. In particular, adds the `UnitAware` interface, which is generalized on the type of values to which units are attached. This is to support adding units to both value types, like `Double`, and resource types, like `Resource<Polynomial>` or `Resource<Discrete<Double>>`. Units are restricted to "absolute" units to simplify conversion, representation, and usage. Units are represented by a Dimension and a floating-point scale compared to a base unit for that dimension. Dimensions are represented exactly, as a product of rational powers of incommensurate base dimensions. When using units on resources, we wrap UnitAware around Resource, and not vice-versa. This applies a single unit to the entire lifetime of the resource, rather than letting the unit vary over time. This lets us check dimensionality on resources once during initialization, then "bake in" constant conversion factors to be used during simulation, a setup that has proven performant in Clipper's model.
Unlike other dynamics types, Unstructured and Differentiable dynamics do not present enough information to globally describe their behavior. This means they can express functions that are not otherwise exactly representable, like trignometry functions for Differentiable, or even calls to external libraries through Unstructured. Unstructured resources can represent any function of time and/or other resources, but need to be approximated before being given to a Registrar or some other components. Unstructured resources can also represent continuously-varying values of unusual types. For example, a string representing the current time down to the microsecond. While these kinds of values are not often registered directly, they can form intermediate steps when deriving other (unstructured) resources, which can be approximated and registered. While the Unstructured type itself forms a monad over the values it contains, it does not compose with Resource or Dynamics monads. Instead, we revert to an applicative, which still lets us write derivations on unstructured resources as functions on their plain values.
Adds utilities for constructing approximations. These are particularly useful for Unstructured resources with Double as a value, but some approximations generalize beyond this. Approximation is broken into several stages: 1. Choose an approximation type. The ready-made choices here are "discrete", "secant", or "Taylor" approximations, but the interface permits others to be defined later. 2. Choose a strategy for running those approximations - choosing a divergence estimator or interval function, depending on the approximation type. There are ready-made strategies for using uniformly-spaced samples or attempting to bound the final error. 3. If error-bounding is used, choose an error tolerance and error measurement. Again, there are several ready-made options, based on both direct methods and analytic estimates, depending on the available information. Further parameters may be needed to configure these error estimates. Breaking the problem down like this allows for more focused testing, as well as component re-use. Doing approximation on an Unstructured resource allows the value of the result to influence the sampling strategy - rather than heuristically choosing a sampling strategy to get "good enough" data, we can measure the result to choose samples. This can be an efficiency boon when the approximated resource is cheap, but downstream resources or tasks are expensive.
Adds a small, non-executable Demo class. This class shows some important features of the framework in a condensed way.
Add an executable example model used to test and demonstrate the framework. The example model comprises three main components: * ErrorTestingModel * DataModel * ApproximationModel The ErrorTestingModel demonstrates error-handling behavior for resources. Using the `CauseError` activity, we can trigger one of several kinds of errors from the plan, including effects which fail directly, concurrent effects which conflict, and resources which violate a constraint. We can also test out different combinations of naming and error handling behaviors, to see what information is propagated back to the user for debugging. Finally, we can observe that, when logging errors, portions of the model can fail while independent portions of the model continue to function normally. The DataModel demonstrates a complicated resource modeling problem, leveraging the LinearBoundaryConsistencySolver. This problem models a data system with bin space shared across multiple buckets, where some buckets have priority over others, and each bucket independently sets desired write or delete rates. The problem of allocating bin space to each bucket, allowing higher-priority buckets to overwrite lower-priority ones only if needed, is phrased as linear inequalities on polynomial resources and given to the solver. The `ChangeDesiredRate` activity can be used to set up different scenarios for this model. The ApproximationModel shows three resources, a polynomial, a rational function, and a complicated trig function, approximated various ways. The `ChangeApproximationInput` activity can be used to change the polynomials feeding this model, and the simulation configuration contains a setting for the approximation accuracy, affecting those resources which approximate by bounding their maximum error. In particular, the `approximation/trig/default` resource displays the most complicated resource function, comprising trig and exponentials, defined through the Java Math library, approximated by the default secant approximation method.
Adds unit tests for a variety of classes in the streamline framework. These unit tests were added as-needed to debug problems, and aren't meant to give comprehensive or systematic coverage.
b7475d0
to
d0cad60
Compare
contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't resolve the infinite loop problem, we should create an issue for it, #1305 . I'm approving because I don't think this is a big enough reason to hold it back.
contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java
Show resolved
Hide resolved
contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java
Show resolved
Hide resolved
d0cad60
to
fb15cda
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all your hard work, @DavidLegg. This PR will empower mission modelers to more easily harness the power of Aerie. The terse expressiveness of your libraries, and the fact that I've had a number of people over the past few weeks ask me repeatedly, "when are you going to merge this PR?" are a testament to how much care you've put into the design and implementation of this framework.
This is the beginning, not the end; I expect us to continue to iterate on this framework. Some specific areas I'd like to see us explore are:
- Can we avoid the singleton pattern in Resources.CLOCK?
- Can Resource.signalling leverage expiry, rather than emitting no-op events?
- Can we serialize errors using events instead of resources, for better visualization?
With that, I say
Description
Implements a new resource framework, based on the one currently being used by Clipper and lessons learned from an older framework developed for Clipper.
The most important changes, compared to previous resource frameworks, are:
Avoiding that combinatorial explosion makes adding new dynamics types easier. This in turn gives us better modeling fidelity, since we can define and use an unusual dynamics type if needed. For example, we define and regularly use in derivations an exact clock based on
Duration
.errors
resource with additional debugging information attached. Hence errors and partial sim results are visible in the UI.streamline-framework.pdf
Reactions
class, which defines several useful reaction patterns.lessThan
, we have a functionwhen
to turn a boolean resource into the condition "that resource is true". This is especially useful in conjunction with reactions.PolynomialResources
for examples.Verification
Some core classes were unit-tested as needed for debugging. Additionally, Pranav on SRL has been using a preview version of this framework for a few weeks to help flush out bugs and missing features.
Documentation
streamline-framework.pdf
The intended audience for this document is a modeler looking to use the framework. I'd like to convert this document into a set of wiki pages so they're more easily accessible, and expand it with examples and lessons learned by modelers using this or other resource frameworks.
Future work
Fleshing out standard dynamics types, effects, and resource derivation functions as more models use them and find use cases that are missing or poorly supported.
There are known efficiency improvements around task handling and providing expiry information directly to Aerie when sampling resources. These may have small knock-on effects to this framework.
Also, as mentioned briefly above, Aerie's topic metadata feature will likely supplant the labelling system in this framework in the future.
(PR copied from #1198, to get CI tests to work.)