| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | /**
 | 
					
						
							|  |  |  |  * @file    ISAM2-inl.h | 
					
						
							| 
									
										
										
										
											2009-12-30 12:27:14 +08:00
										 |  |  |  * @brief   Incremental update functionality (ISAM2) for BayesTree, with fluid relinearization. | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  |  * @author  Michael Kaess | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include <boost/foreach.hpp>
 | 
					
						
							|  |  |  | #include <boost/assign/std/list.hpp> // for operator +=
 | 
					
						
							|  |  |  | using namespace boost::assign; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-03 12:57:35 +08:00
										 |  |  | #include <set>
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-16 09:16:59 +08:00
										 |  |  | #include "NonlinearFactorGraph-inl.h"
 | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | #include "GaussianFactor.h"
 | 
					
						
							|  |  |  | #include "VectorConfig.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include "Conditional.h"
 | 
					
						
							|  |  |  | #include "BayesTree-inl.h"
 | 
					
						
							|  |  |  | #include "ISAM2.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace gtsam { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	using namespace std; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 	// from inference-inl.h - need to additionally return the newly created factor for caching
 | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 	boost::shared_ptr<GaussianConditional> _eliminateOne(FactorGraph<GaussianFactor>& graph, CachedFactors& cached, const Symbol& key) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// combine the factors of all nodes connected to the variable to be eliminated
 | 
					
						
							|  |  |  | 		// if no factors are connected to key, returns an empty factor
 | 
					
						
							|  |  |  | 		boost::shared_ptr<GaussianFactor> joint_factor = removeAndCombineFactors(graph,key); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// eliminate that joint factor
 | 
					
						
							|  |  |  | 		boost::shared_ptr<GaussianFactor> factor; | 
					
						
							|  |  |  | 		boost::shared_ptr<GaussianConditional> conditional; | 
					
						
							|  |  |  | 		boost::tie(conditional, factor) = joint_factor->eliminate(key); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// remember the intermediate result to be able to later restart computation in the middle
 | 
					
						
							|  |  |  | 		cached[key] = factor; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// add new factor on separator back into the graph
 | 
					
						
							|  |  |  | 		if (!factor->empty()) graph.push_back(factor); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// return the conditional Gaussian
 | 
					
						
							|  |  |  | 		return conditional; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// from GaussianFactorGraph.cpp, see _eliminateOne above
 | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 	GaussianBayesNet _eliminate(FactorGraph<GaussianFactor>& graph, CachedFactors& cached, const Ordering& ordering) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		GaussianBayesNet chordalBayesNet; // empty
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 		BOOST_FOREACH(const Symbol& key, ordering) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 			GaussianConditional::shared_ptr cg = _eliminateOne(graph, cached, key); | 
					
						
							|  |  |  | 			chordalBayesNet.push_back(cg); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return chordalBayesNet; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 	GaussianBayesNet _eliminate_const(const FactorGraph<GaussianFactor>& graph, CachedFactors& cached, const Ordering& ordering) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		// make a copy that can be modified locally
 | 
					
						
							|  |  |  | 		FactorGraph<GaussianFactor> graph_ignored = graph; | 
					
						
							|  |  |  | 		return _eliminate(graph_ignored, cached, ordering); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 	/** Create an empty Bayes Tree */ | 
					
						
							|  |  |  | 	template<class Conditional, class Config> | 
					
						
							|  |  |  | 	ISAM2<Conditional, Config>::ISAM2() : BayesTree<Conditional>() {} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-30 12:27:14 +08:00
										 |  |  | 	/** Create a Bayes Tree from a nonlinear factor graph */ | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 	template<class Conditional, class Config> | 
					
						
							| 
									
										
										
										
											2009-12-31 13:35:08 +08:00
										 |  |  | 	ISAM2<Conditional, Config>::ISAM2(const NonlinearFactorGraph<Config>& nlfg, const Ordering& ordering, const Config& config) | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 	: BayesTree<Conditional>(nlfg.linearize(config).eliminate(ordering)), nonlinearFactors_(nlfg), linPoint_(config) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		// todo: repeats calculation above, just to set "cached"
 | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 		_eliminate_const(nlfg.linearize(config), cached_, ordering); | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	/* ************************************************************************* */ | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 	// retrieve all factors that ONLY contain the affected variables
 | 
					
						
							|  |  |  | 	// (note that the remaining stuff is summarized in the cached factors)
 | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 	template<class Conditional, class Config> | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 	FactorGraph<GaussianFactor> ISAM2<Conditional, Config>::relinearizeAffectedFactors(const list<Symbol>& affectedKeys) { | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		NonlinearFactorGraph<Config> nonlinearAffectedFactors; | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		typename FactorGraph<NonlinearFactor<Config> >::iterator it; | 
					
						
							|  |  |  | 		for(it = nonlinearFactors_.begin(); it != nonlinearFactors_.end(); it++) { | 
					
						
							|  |  |  | 			bool inside = true; | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 			BOOST_FOREACH(const Symbol& key, (*it)->keys()) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 				if (find(affectedKeys.begin(), affectedKeys.end(), key) == affectedKeys.end()) | 
					
						
							|  |  |  | 					inside = false; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if (inside) | 
					
						
							|  |  |  | 				nonlinearAffectedFactors.push_back(*it); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		return nonlinearAffectedFactors.linearize(linPoint_); | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 	/* ************************************************************************* */ | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 	// find intermediate (linearized) factors from cache that are passed into the affected area
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 	template<class Conditional, class Config> | 
					
						
							|  |  |  | 	FactorGraph<GaussianFactor> ISAM2<Conditional, Config>::getCachedBoundaryFactors(Cliques& orphans) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		FactorGraph<GaussianFactor> cachedBoundary; | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		BOOST_FOREACH(sharedClique orphan, orphans) { | 
					
						
							|  |  |  | 			// find the last variable that is not part of the separator
 | 
					
						
							| 
									
										
										
										
											2010-01-19 02:13:53 +08:00
										 |  |  | 			Symbol oneTooFar = orphan->separator_.front(); | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 			list<Symbol> keys = orphan->keys(); | 
					
						
							|  |  |  | 			list<Symbol>::iterator it = find(keys.begin(), keys.end(), oneTooFar); | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 			it--; | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 			const Symbol& key = *it; | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 			// retrieve the cached factor and add to boundary
 | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 			cachedBoundary.push_back(cached_[key]); | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 		return cachedBoundary; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 	/* ************************************************************************* */ | 
					
						
							|  |  |  | 	template<class Conditional, class Config> | 
					
						
							|  |  |  | 	void ISAM2<Conditional, Config>::update_internal(const NonlinearFactorGraph<Config>& newFactors, | 
					
						
							|  |  |  | 			const Config& config, Cliques& orphans) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | 		// determine which variables to relinearize
 | 
					
						
							| 
									
										
										
										
											2010-01-19 06:37:44 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | 		FactorGraph<GaussianFactor> affectedFactors; | 
					
						
							|  |  |  | 		list<Symbol> newFactorsKeys = newFactors.keys(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 09:18:28 +08:00
										 |  |  | #if 1
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | 		// relinearize all keys that are in newFactors, and already exist (not new variables!)
 | 
					
						
							|  |  |  | 		list<Symbol> keysToRelinearize; | 
					
						
							|  |  |  | 		list<Symbol> oldKeys = nonlinearFactors_.keys(); | 
					
						
							|  |  |  | 		BOOST_FOREACH(const Symbol& key, newFactorsKeys) { | 
					
						
							|  |  |  | 			if (find(oldKeys.begin(), oldKeys.end(), key)!=oldKeys.end()) | 
					
						
							|  |  |  | 				keysToRelinearize.push_back(key); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | 		// basically calculate all the keys contained in the factors that contain any of the keys...
 | 
					
						
							|  |  |  | 		// the goal is to relinearize all variables directly affected by new factors
 | 
					
						
							|  |  |  | 		FactorGraph<NonlinearFactor<Config> > allAffected; | 
					
						
							|  |  |  | 		typename FactorGraph<NonlinearFactor<Config> >::iterator it; | 
					
						
							|  |  |  | 		for(it = nonlinearFactors_.begin(); it != nonlinearFactors_.end(); it++) { | 
					
						
							|  |  |  | 			bool affected = false; | 
					
						
							|  |  |  | 			BOOST_FOREACH(const Symbol& key, (*it)->keys()) { | 
					
						
							|  |  |  | 				if (find(newFactorsKeys.begin(), newFactorsKeys.end(), key) != newFactorsKeys.end()) | 
					
						
							|  |  |  | 					affected = true; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if (affected) | 
					
						
							|  |  |  | 				allAffected.push_back(*it); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		list<Symbol> keysToBeRemoved = allAffected.keys(); | 
					
						
							| 
									
										
										
										
											2010-01-19 09:18:28 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | #else
 | 
					
						
							| 
									
										
										
										
											2010-01-19 09:18:28 +08:00
										 |  |  | 		// debug only: full relinearization in each step
 | 
					
						
							|  |  |  | 		list<Symbol> keysToRelinearize = nonlinearFactors_.keys(); | 
					
						
							|  |  |  | 		list<Symbol> keysToBeRemoved = nonlinearFactors_.keys(); | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | #endif
 | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 09:18:28 +08:00
										 |  |  | 		// remove affected factors
 | 
					
						
							| 
									
										
										
										
											2010-01-19 06:37:44 +08:00
										 |  |  | 		this->removeTop(keysToBeRemoved, affectedFactors, orphans); | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | 		// selectively update the linearization point
 | 
					
						
							|  |  |  | 		VectorConfig selected_delta; | 
					
						
							|  |  |  | 		BOOST_FOREACH(const Symbol& key, keysToRelinearize) { | 
					
						
							|  |  |  | 			if (delta_.contains(key)) // after constructor call, delta is empty...
 | 
					
						
							|  |  |  | 				selected_delta.insert(key, delta_[key]); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		linPoint_ = expmap(linPoint_, selected_delta); // todo-debug only
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 		// relinearize the affected factors ...
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 		list<Symbol> affectedKeys = affectedFactors.keys(); | 
					
						
							|  |  |  | 		FactorGraph<GaussianFactor> factors = relinearizeAffectedFactors(affectedKeys); // todo: searches through all factors, potentially expensive
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// ... add the cached intermediate results from the boundary of the orphans ...
 | 
					
						
							|  |  |  | 		FactorGraph<GaussianFactor> cachedBoundary = getCachedBoundaryFactors(orphans); | 
					
						
							|  |  |  | 		factors.push_back(cachedBoundary); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | 		// add new variables
 | 
					
						
							|  |  |  | 		linPoint_.insert(config); | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 		// ... and finally add the new linearized factors themselves
 | 
					
						
							| 
									
										
										
										
											2010-01-19 08:15:46 +08:00
										 |  |  | 		FactorGraph<GaussianFactor> newFactorsLinearized = newFactors.linearize(linPoint_); | 
					
						
							| 
									
										
										
										
											2009-12-30 12:27:14 +08:00
										 |  |  | 		factors.push_back(newFactorsLinearized); | 
					
						
							| 
									
										
										
										
											2010-01-03 12:57:35 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 		// create an ordering for the new and contaminated factors
 | 
					
						
							|  |  |  | 		Ordering ordering; | 
					
						
							|  |  |  | 		if (true) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 			ordering = factors.getOrdering(); | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 		} else { | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 			list<Symbol> keys = factors.keys(); | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 			keys.sort(); // todo: correct sorting order?
 | 
					
						
							|  |  |  | 			ordering = keys; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// eliminate into a Bayes net
 | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 		BayesNet<Conditional> bayesNet = _eliminate(factors, cached_, ordering); | 
					
						
							| 
									
										
										
										
											2010-01-03 12:57:35 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 		// remember the new factors for later relinearization
 | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 		nonlinearFactors_.push_back(newFactors); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 		// insert conditionals back in, straight into the topless bayesTree
 | 
					
						
							|  |  |  | 		typename BayesNet<Conditional>::const_reverse_iterator rit; | 
					
						
							|  |  |  | 		for ( rit=bayesNet.rbegin(); rit != bayesNet.rend(); ++rit ) | 
					
						
							|  |  |  | 			this->insert(*rit); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// add orphans to the bottom of the new tree
 | 
					
						
							|  |  |  | 		BOOST_FOREACH(sharedClique orphan, orphans) { | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 		  Symbol key = orphan->separator_.front(); | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 			sharedClique parent = (*this)[key]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			parent->children_ += orphan; | 
					
						
							|  |  |  | 			orphan->parent_ = parent; // set new parent!
 | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2010-01-18 12:32:45 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-18 12:53:17 +08:00
										 |  |  | 		// update solution - todo: potentially expensive
 | 
					
						
							| 
									
										
										
										
											2010-01-18 16:05:33 +08:00
										 |  |  | 		delta_ = optimize2(*this); | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	template<class Conditional, class Config> | 
					
						
							| 
									
										
										
										
											2009-12-29 22:54:45 +08:00
										 |  |  | 	void ISAM2<Conditional, Config>::update(const NonlinearFactorGraph<Config>& newFactors, const Config& config) { | 
					
						
							| 
									
										
										
										
											2010-01-17 14:06:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 		Cliques orphans; | 
					
						
							| 
									
										
										
										
											2009-12-29 22:54:45 +08:00
										 |  |  | 		this->update_internal(newFactors, config, orphans); | 
					
						
							| 
									
										
										
										
											2009-12-29 13:57:05 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* ************************************************************************* */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | /// namespace gtsam
 |