serif::utils::dependencies serif::utils::dependencies::AllNodesObserver serif::utils::dependencies::Binding serif::utils::dependencies::Bindings serif::utils::dependencies::BoundType serif::utils::dependencies::Computation serif::utils::dependencies::ComputationCache serif::utils::dependencies::ComputationGraph serif::utils::dependencies::DependencyContext serif::utils::dependencies::DependencyContextView serif::utils::dependencies::DependencyContextView< ParentT, KeyList< KeyTs... > > serif::utils::dependencies::EdgeObserver serif::utils::dependencies::GraphTiming serif::utils::dependencies::GraphTimingFactory serif::utils::dependencies::GraphTimingRun serif::utils::dependencies::HasBinding serif::utils::dependencies::KeyList serif::utils::dependencies::KeyListBindings serif::utils::dependencies::KeyListBindings< BindingsT, KeyList< KeyTs... > > serif::utils::dependencies::NodeObserver serif::utils::dependencies::NodeTiming serif::utils::dependencies::NoGraphObservers serif::utils::dependencies::Observers serif::utils::dependencies::PreparationFailure serif::utils::dependencies::PreparationReport serif::utils::dependencies::ProjectedSnapshot serif::utils::dependencies::ResolveKeys serif::utils::dependencies::ResolveKeys< BindingsT, KeyList< KeyTs... > > serif::utils::dependencies::Resource serif::utils::dependencies::SteadyNodeClock serif::utils::dependencies::ValueSnapshot serif::utils::dependencies::detail serif::utils::dependencies::keys NodeOutcome serif::utils::dependencies::NodeOutcome completed rejected exception Describe a node invocation, independently of whether the whole graph request is later accepted. PreparationMode serif::utils::dependencies::PreparationMode reuse_unchanged rebuild PreparationStatus serif::utils::dependencies::PreparationStatus not_required reused computed typename BindingsT typename KeyT BoundType< BindingsT, KeyT >::target_type using serif::utils::dependencies::bound_type_t = BoundType<BindingsT, KeyT>::target_type bound_type_t serif::utils::dependencies::bound_type_t typename... KeyTs KeyTs KeyList< KeyTs... > using serif::utils::dependencies::InputList = KeyList<KeyTs...> InputList serif::utils::dependencies::InputList Computation arguments are supplied in this key order. typename... KeyTs KeyTs KeyList< KeyTs... > using serif::utils::dependencies::RequestedResults = KeyList<KeyTs...> RequestedResults serif::utils::dependencies::RequestedResults Preparing these results also prepares their required intermediate computations. typename BindingsT typename KeysT ResolveKeys< BindingsT, std::remove_cvref_t< KeysT > >::type using serif::utils::dependencies::ResolvedTargets = ResolveKeys<BindingsT, std::remove_cvref_t<KeysT>>::type ResolvedTargets serif::utils::dependencies::ResolvedTargets IsKeyList... Lists Lists detail::MergeKeyLists< KeyList<>, std::remove_cvref_t< Lists >... >::type using serif::utils::dependencies::CombinedKeyList = detail::MergeKeyLists<KeyList<>, std::remove_cvref_t<Lists>...>::type CombinedKeyList serif::utils::dependencies::CombinedKeyList Combine requirements in first-appearance order, retaining distinct keys with shared targets. typename T bool bool serif::utils::dependencies::is_bindings_v is_bindings_v serif::utils::dependencies::is_bindings_v = false typename... BindingTs BindingTs bool bool serif::utils::dependencies::is_bindings_v< Bindings< BindingTs... > > is_bindings_v< Bindings< BindingTs... > > serif::utils::dependencies::is_bindings_v< Bindings< BindingTs... > > = misc::traits::all_unique_types_v<typename BindingTs::Key...> IsBindings BindingsT BindingsT typename KeyT bool bool serif::utils::dependencies::has_binding_v has_binding_v serif::utils::dependencies::has_binding_v = HasBinding<BindingsT, KeyT>::value typename T bool bool serif::utils::dependencies::is_computation_v is_computation_v serif::utils::dependencies::is_computation_v = false typename KeyT typename InputsT typename FunctionT bool OwnsInputs OwnsInputs bool bool serif::utils::dependencies::is_computation_v< Computation< KeyT, InputsT, FunctionT, OwnsInputs > > is_computation_v< Computation< KeyT, InputsT, FunctionT, OwnsInputs > > serif::utils::dependencies::is_computation_v< Computation< KeyT, InputsT, FunctionT, OwnsInputs > > = true typename T bool bool serif::utils::dependencies::is_computation_graph_v is_computation_graph_v serif::utils::dependencies::is_computation_graph_v = false typename... Computations Computations bool bool serif::utils::dependencies::is_computation_graph_v< ComputationGraph< Computations... > > is_computation_graph_v< ComputationGraph< Computations... > > serif::utils::dependencies::is_computation_graph_v< ComputationGraph< Computations... > > = true typename T bool bool serif::utils::dependencies::is_dependency_context_v is_dependency_context_v serif::utils::dependencies::is_dependency_context_v = false typename... Resources Resources bool bool serif::utils::dependencies::is_dependency_context_v< DependencyContext< Resources... > > is_dependency_context_v< DependencyContext< Resources... > > serif::utils::dependencies::is_dependency_context_v< DependencyContext< Resources... > > = true typename ParentT typename KeysT bool bool serif::utils::dependencies::is_dependency_context_v< DependencyContextView< ParentT, KeysT > > is_dependency_context_v< DependencyContextView< ParentT, KeysT > > serif::utils::dependencies::is_dependency_context_v< DependencyContextView< ParentT, KeysT > > = true typename T bool bool serif::utils::dependencies::is_key_list_v is_key_list_v serif::utils::dependencies::is_key_list_v = false typename... KeyTs KeyTs bool bool serif::utils::dependencies::is_key_list_v< KeyList< KeyTs... > > is_key_list_v< KeyList< KeyTs... > > serif::utils::dependencies::is_key_list_v< KeyList< KeyTs... > > = misc::traits::all_unique_types_v<KeyTs...> IsKeyList KeysT KeysT std::size_t std::size_t serif::utils::dependencies::key_count_v key_count_v serif::utils::dependencies::key_count_v = detail::KeyListSize<std::remove_cvref_t<KeysT>>::value Count distinct declared keys without constructing their associated values. std::unordered_map< PreparationStatus, std::string_view > std::unordered_map<PreparationStatus, std::string_view> serif::utils::dependencies::preparation_status_name_map preparation_status_name_map serif::utils::dependencies::preparation_status_name_map = meta::enum_to_enum_string_map<PreparationStatus>() std::unordered_map< PreparationMode, std::string_view > std::unordered_map<PreparationMode, std::string_view> serif::utils::dependencies::preparation_mode_name_map preparation_mode_name_map serif::utils::dependencies::preparation_mode_name_map = meta::enum_to_enum_string_map<PreparationMode>() typename T bool bool serif::utils::dependencies::is_resource_v is_resource_v serif::utils::dependencies::is_resource_v = false typename KeyT typename StorageT typename PolicyT bool bool serif::utils::dependencies::is_resource_v< Resource< KeyT, StorageT, PolicyT > > is_resource_v< Resource< KeyT, StorageT, PolicyT > > serif::utils::dependencies::is_resource_v< Resource< KeyT, StorageT, PolicyT > > = true typename KeyT IsKeyList InputsT InputsT typename FunctionT auto auto serif::utils::dependencies::make_computation (FunctionT &&function) make_computation serif::utils::dependencies::make_computation FunctionT && function std::constructible_from<std::decay_t<FunctionT>, FunctionT> Attach an output key and ordered input keys to a lambda, function, or other callable. typename KeyT IsKeyList InputsT InputsT typename FunctionT auto auto serif::utils::dependencies::make_owning_computation (FunctionT &&function) make_owning_computation serif::utils::dependencies::make_owning_computation FunctionT && function std::constructible_from<std::decay_t<FunctionT>, FunctionT> Supply immutable shared handles to computed prerequisites so results can retain them without copying. Every input must be produced by this graph. Context sources remain borrowed and cannot be retained this way. Resources borrowed inside a prerequisite must still outlive their use; the handle owns only the computed result. typename ErrorT std::string IsComputationGraph GraphT GraphT IsDependencyContext ContextT ContextT typename ObserversT Observers<> auto auto serif::utils::dependencies::make_computation_cache (GraphT &&graph, const ContextT &, ObserversT &&observers=ObserversT{}) make_computation_cache serif::utils::dependencies::make_computation_cache GraphT && graph const ContextT & ObserversT && observers ObserversT{} CanPrepare<GraphT, ContextT, ErrorT> && ObserversFor<ObserversT, GraphT, ContextT> && std::constructible_from<std::remove_cvref_t<GraphT>, GraphT> && std::constructible_from<std::remove_cvref_t<ObserversT>, ObserversT> Fix the graph's source types without retaining the supplied context or evaluating any computations. typename ErrorT std::string IsDependencyContext ContextT ContextT typename... Computations Computations auto auto serif::utils::dependencies::make_computation_cache (const ContextT &context, Computations &&... computations) make_computation_cache serif::utils::dependencies::make_computation_cache const ContextT & context Computations &&... computations CompatibleComputations<Computations...> && (std::constructible_from<std::remove_cvref_t<Computations>, Computations> && ...) && CanPrepare<ComputationGraph<std::remove_cvref_t<Computations>...>, ContextT, ErrorT> Build a checked graph and an empty cache directly from named computations. typename... Computations Computations auto auto serif::utils::dependencies::make_computation_graph (Computations &&... computations) make_computation_graph serif::utils::dependencies::make_computation_graph Computations &&... computations CompatibleComputations<Computations...> && (std::constructible_from<std::remove_cvref_t<Computations>, Computations> && ...) Reject duplicate producers and cyclic computational graphs at the declaration site. typename... Resources Resources auto auto serif::utils::dependencies::make_dependency_context (Resources &&... resources) make_dependency_context serif::utils::dependencies::make_dependency_context Resources &&... resources (IsResource<Resources> && ...) && misc::traits::all_unique_types_v<typename std::remove_cvref_t<Resources>::Key...> Build a context whose schema follows directly from its resource keys and types. IsDependencyContext... Contexts Contexts auto auto serif::utils::dependencies::combine_dependency_contexts (const Contexts &... contexts) combine_dependency_contexts serif::utils::dependencies::combine_dependency_contexts const Contexts &... contexts (detail::combinable_resources(misc::concepts::ConcatT<typename Contexts::Resources...>{})) Copy disjoint resources and their tracking policies into one context; borrowed objects retain their original lifetime. typename KeyT typename KeysT std::size_t std::size_t serif::utils::dependencies::key_index () key_index serif::utils::dependencies::key_index ContainsKey<KeyT, KeysT> Locate a declared key in its ordered list; absent keys are rejected at compile time. typename KeyT typename BeforeT typename AfterT auto auto serif::utils::dependencies::observe_node (BeforeT &&before, AfterT &&after) observe_node serif::utils::dependencies::observe_node BeforeT && before AfterT && after NodeObserverCallbacks<std::decay_t<BeforeT>, std::decay_t<AfterT>> && std::constructible_from<std::decay_t<BeforeT>, BeforeT> && std::constructible_from<std::decay_t<AfterT>, AfterT> Bind before/after callbacks to a result key; callback captures determine any external storage lifetime. typename BeforeT typename AfterT auto auto serif::utils::dependencies::observe_nodes (BeforeT &&before, AfterT &&after) observe_nodes serif::utils::dependencies::observe_nodes BeforeT && before AfterT && after std::constructible_from<std::decay_t<BeforeT>, BeforeT> && std::constructible_from<std::decay_t<AfterT>, AfterT> Observe all executed nodes; cache construction checks the callback contract for every result key. typename InputT typename ConsumerT typename FunctionT auto auto serif::utils::dependencies::observe_edge (FunctionT &&function) observe_edge serif::utils::dependencies::observe_edge FunctionT && function std::constructible_from<std::decay_t<FunctionT>, FunctionT> Bind a read-only callback to one input of one consumer; graph construction validates the edge and value type. typename... Registrations Registrations auto auto serif::utils::dependencies::make_observers (Registrations &&... observers) make_observers serif::utils::dependencies::make_observers Registrations &&... observers (detail::is_observer_v<std::remove_cvref_t<Registrations>> && ...) && (std::constructible_from<std::remove_cvref_t<Registrations>, Registrations> && ...) Collect any number of registrations, including several observers of the same node or edge. typename KeyT typename ValueT typename PolicyT ValueSnapshot auto auto serif::utils::dependencies::own (ValueT &&value, PolicyT policy={}) own serif::utils::dependencies::own ValueT && value PolicyT policy {} Store a value in the context; rvalues transfer ownership and lvalues are copied. typename KeyT typename ValueT typename PolicyT ValueSnapshot auto auto serif::utils::dependencies::borrow (ValueT &value, PolicyT policy={}) borrow serif::utils::dependencies::borrow ValueT & value PolicyT policy {} Bind an existing object whose lifetime must cover every use of the context. typename KeyT typename ValueT typename PolicyT ValueSnapshot auto auto serif::utils::dependencies::borrow (const ValueT &&, PolicyT={})=delete borrow serif::utils::dependencies::borrow const ValueT && PolicyT {} Borrowed storage must never refer to a temporary, including a const temporary. typename FunctionT auto auto serif::utils::dependencies::track_by (FunctionT &&function) track_by serif::utils::dependencies::track_by FunctionT && function Select the state that determines whether a source needs recomputation. typename PolicyT typename ValueT const void * const void * serif::utils::dependencies::source_identity (const PolicyT &policy, const ValueT &value) source_identity serif::utils::dependencies::source_identity const PolicyT & policy const ValueT & value SnapshotPolicyFor<PolicyT, ValueT> Views may identify their underlying resource; ordinary sources retain object identity. NodeTimingClock Clock Clock SteadyNodeClock auto auto serif::utils::dependencies::time_graph_nodes (GraphTiming &timing, Clock clock={}) time_graph_nodes serif::utils::dependencies::time_graph_nodes GraphTiming & timing Clock clock {} Opt into node timings; a custom clock supports deterministic accounting tests without sleeps. Computational Graphs Demonstration code for how to use the SERiF computational graph system The broad idea here is that there are many calculations which depend on each other but which may not necessarily always need to be recomputed just because another part of the model has changed. We model this as a combination of an acyclic computational graph, defined at compile time, and a runtime cache of values. The general workflow is that a developer defines the "context" of the computational graph, think of this as all of the inputs to the graph, or maybe more specifically, anything that could change due to some external event (e.g. a time step, a newton step, etc...) This context is probably the most CS intensive part of this code as the developer needs to think about lifetimes and ownership. Context Contexts are made with the make_dependency_context function, this accepts a variadic pack of bindings between a type and a runtime value. You can generate these yourself using the binding functions and types; however, we provide two convenience wrappers which generally developers should consider using. borrow: Used to bind a type to a reference to a value. This is useful for values which are owned by the caller and which the caller will manage the lifetime of (takes an lvalue). own: Used to bind a type to a value which is owned by the context (copies an lvalue or moves an rvalue) Each of these functions accepts a type as a template parameter and a runtime value as an argument. The type is used as an ID, this is key to understand, if we write usingnamespaceserif::utils::dependencies; doublex{1.0}; autocontext=make_dependency_context(borrow<keys::Input>(x)); we are not saying that x is of type keys::Input, this is clearly not the case. Rather, we are registering in the computational graph a binding between the type keys::Input and the runtime value x. Think of this as naming the various inputs to the graph. Computations After defining a context we can define a computation, each computation defines a node, while its ordered input keys define the incoming edges. Consider the following example (assuming the context from above): autofunc=make_computation<keys::Output, InputList<keys::Input>>( [](constdouble_x)->double{ return_x*_x; } ); what we have defined here is a node which takes a single input tagged keys::Input and produces an output tagged as keys::Output. The input key declares an edge from that source to this node. Other nodes can use keys::Output as an input, and a successfully prepared result can be read with get or retained with share. These connections allow us to form a larger graph. Consider now the following code usingnamespaceserif::utils::dependencies; structtags{ structX{}; structY{}; structOutput1{}; structOutput2{}; }; doublex{1.0}; doubley{2.0}; autocontext=make_dependency_context(borrow<tags::X>(x),borrow<tags::Y>(y)); autofunc1=make_computation<tags::Output1, InputList<tags::X>>( [](constdouble_x)->double{ return_x*_x; } ); autofunc2=make_computation<tags::Output2, InputList<tags::Output1, tags::Y>>( [](constdouble_x,constdouble_y)->double{ return_x+_y; } ); we have now defined two computations, the first takes a single input, whichever input is tagged as tags::X and produces an output tagged as tags::Output1. The second computation takes two inputs, the first is the output of the first computation (lookup by the tag) and the second is whichever input is tagged as tags::Y, and produces an output tagged as tags::Output2. Note that the order of arguments to the lambda must be the same as the order of the tags in the InputList, the dependencies module will automatically order arguments based on the InputList. There is type checking to ensure that the types match, but if you have two inputs of the same type that are flipped there is no way for the system to inspect that and warn you. Further, depending on your compiler and flags there may be implicit casting between convertable types (e.g. int <-> double). TodoWe may want to remove the ability to have implicit casting between types, this is a bit of a footgun and may lead to subtle bugs. This is primary goal of this system, split computations into small pieces with the ability to cache intermediate results and reuse as many of those intermediate results as we can can prove is safe to do. The whole reason this system is so complex is that very purpose. There is a fundamental problem with high dimensional stellar models, they are always going to be slow, therefore we need to accept some degree of complexity in order to make them tractable. Computation Cache Once we have the computations we need to connect them together, generally use the helper function make_computation_cache which takes a context and a variadic pack of computations. There are a lot of compile time checks which run to ensure that the graph is valid, the context describes what is needed to compute the graph, and that each computation is valid. Generally if a static analysis error or compile error pops up here it probably means there is a mistake in the definition of the context or the computations. I've tried to make these errors as clear as possible; however, there is only so much that you can do with the C++ compiler. I recommend first checking, probably with a drawn-out diagram, that all the tags are correct and what you expect. A developer can also choose to use the make_computation_graph function to explicitly name a graph. This is useful when naming or reusing a collection of computations (make_computation_cache calls make_computation_graph and then fixes the source types from the supplied context without retaining the context). Now consider the following code assuming the context and computations from above: autograph_cache=make_computation_cache(context,func1,func2); At this point we have a cache which we can access various parts of; however, no computations have been run yet. To run a computation we must call try_prepare. try_prepare returns std::expected: ordinary rejection is a value, and no candidate results or snapshots are published on failure. Exceptions from a callable or snapshot policy propagate with the same cache protection. Previously retained results remain valid, but get and share require a successful current preparation. Always check the report before reading results. try_prepare takes the context and evaluates the requested nodes and their prerequisites, reusing unchanged results. By default every result is requested, so the first call evaluates the entire graph. RequestedResults can restrict evaluation to a branch. autoreport=graph_cache.try_prepare(context); if(!report){ return;//Handlereport.error()beforereadingthecache. } doublefunc1_result=graph_cache.templateget<tags::Output1>(); doublefunc2_result=graph_cache.templateget<tags::Output2>(); autofunc1_status=report->templatestatus<tags::Output1>(); autofunc2_status=report->templatestatus<tags::Output2>(); std::string_viewfunc1_status_string=preparation_status_name_map[func1_status];//notethisisaninlinemapdefinedinserif::utils::dependencies std::string_viewfunc2_status_string=preparation_status_name_map[func2_status]; std::println("Func1Result:{},Func2Result:{},Func1Status:{},Func2Status:{}",func1_result,func2_result,func1_status_string,func2_status_string); Now what we could do is change only one part of the input context and call try_prepare again. Lets say we change only y, such that only func2 is stale. y=3.0; report=graph_cache.try_prepare(context); if(!report){ return;//Handlereport.error()beforereadingthecache. } doublefunc1_result=graph_cache.templateget<tags::Output1>(); doublefunc2_result=graph_cache.templateget<tags::Output2>(); autofunc1_status=report->templatestatus<tags::Output1>(); autofunc2_status=report->templatestatus<tags::Output2>(); std::string_viewfunc1_status_string=preparation_status_name_map[func1_status];//notethisisaninlinemapdefinedinserif::utils::dependencies std::string_viewfunc2_status_string=preparation_status_name_map[func2_status]; std::println("Func1Result:{},Func2Result:{},Func1Status:{},Func2Status:{}",func1_result,func2_result,func1_status_string,func2_status_string);