diff --git a/Event/TrackEvent/include/Event/PrTracksTag.h b/Event/TrackEvent/include/Event/PrTracksTag.h
index a8e78600f2b25e450ae75754d39a7f7fed4ba2eb..3cac36b17c611c6fd622dc87ae48cb1ab5304ca0 100644
--- a/Event/TrackEvent/include/Event/PrTracksTag.h
+++ b/Event/TrackEvent/include/Event/PrTracksTag.h
@@ -130,10 +130,12 @@ namespace LHCb::Pr::TracksInfo {
   inline constexpr std::size_t NumSeedStates   = 3;
   inline constexpr std::size_t NumLongStates   = 2;
   inline constexpr std::size_t NumVeloStates   = 2;
+  inline constexpr std::size_t NumUPStates   = 2;
   inline constexpr std::size_t NumCovXY        = 3;
   inline constexpr std::size_t NumVeloStateCov = NumCovXY * NumVeloStates;
   inline constexpr std::size_t MaxVPHits       = 52;
   inline constexpr std::size_t MaxUTHits       = 8;
+  inline constexpr std::size_t MaxUPHits       = 8;
   inline constexpr std::size_t MaxFTHits       = 12;
   inline constexpr std::size_t MaxMuonHits     = 8;
 
diff --git a/Event/TrackEvent/include/Event/PrUPTracks.h b/Event/TrackEvent/include/Event/PrUPTracks.h
new file mode 100644
index 0000000000000000000000000000000000000000..a62836f4970e5f73b78304e22311acad8315a60d
--- /dev/null
+++ b/Event/TrackEvent/include/Event/PrUPTracks.h
@@ -0,0 +1,133 @@
+/*****************************************************************************\
+* (c) Copyright 2000-2018 CERN for the benefit of the LHCb Collaboration      *
+*                                                                             *
+* This software is distributed under the terms of the GNU General Public      *
+* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   *
+*                                                                             *
+* In applying this licence, CERN does not waive the privileges and immunities *
+* granted to it by virtue of its status as an Intergovernmental Organization  *
+* or submit itself to any jurisdiction.                                       *
+\*****************************************************************************/
+
+#pragma once
+#include "Event/PrProxyHelpers.h"
+#include "Event/PrUPHits.h"
+#include "Event/SIMDEventTypes.h"
+#include "Event/SOACollection.h"
+#include "Kernel/EventLocalAllocator.h"
+#include "Kernel/LHCbID.h"
+#include "Kernel/Traits.h"
+#include "LHCbMath/MatVec.h"
+#include "LHCbMath/SIMDWrapper.h"
+#include "PrTracksTag.h"
+
+/**
+ * Track data for exchanges between VeloTracking and UP
+ *
+ * @author: Arthur Hennequin
+ * 2020-08-25 updated to SOACollection structure by Peilian Li
+ */
+
+namespace LHCb::Pr::UP {
+
+  struct Tag {
+    struct Index : Event::int_field {};
+    struct LHCbID : Event::lhcbid_field {};
+    struct UPHits : Event::vector_field<Event::struct_field<Index, LHCbID>> {};
+    // Technically these are not states, as there is no q/p field :
+    struct States : Event::pos_dirs_field<TracksInfo::NumUPStates> {};
+
+    template <typename T>
+    using up_t = Event::SOACollection<T, UPHits, States>;
+  };
+
+  namespace PD = LHCb::Event::PosDirParameters;
+
+  struct Tracks : Tag::up_t<Tracks> {
+    using base_t = typename Tag::up_t<Tracks>;
+    using tag_t  = Tag; // Needed for tag_type_t helper defined in PrTracksTag.h
+    using base_t::base_t;
+
+    using base_t::allocator_type;
+
+    constexpr static auto NumUPStates   = TracksInfo::NumUPStates;
+    constexpr static auto MaxUPHits       = TracksInfo::MaxUPHits;
+    constexpr static auto MaxLHCbIDs      = TracksInfo::MaxUPHits;
+
+    // -- needs to be explicit, as otherwise the first argument clashes with the
+    // -- default constructor
+    explicit Tracks( bool backward, Zipping::ZipFamilyNumber zipIdentifier = Zipping::generateZipIdentifier(),
+                     allocator_type alloc = {} )
+        : base_t{std::move( zipIdentifier ), std::move( alloc )}, m_backward( backward ) {}
+
+    // Constructor used by zipping machinery when making a copy of a zip
+    Tracks( Zipping::ZipFamilyNumber zn, Tracks const& old )
+        : base_t{std::move( zn ), old}, m_backward{old.m_backward} {}
+
+    [[nodiscard]] auto backward() const { return m_backward; };
+
+    template <SIMDWrapper::InstructionSet simd, ProxyBehaviour behaviour, typename ContainerType>
+    struct UPProxy : Event::Proxy<simd, behaviour, ContainerType> {
+      using base_t = typename Event::Proxy<simd, behaviour, ContainerType>;
+      using base_t::base_t;
+      using base_t::width;
+      using simd_t  = SIMDWrapper::type_map_t<simd>;
+      using int_v   = typename simd_t::int_v;
+      using float_v = typename simd_t::float_v;
+
+      [[nodiscard]] auto                     backward() const { return this->container()->backward(); };
+      [[nodiscard, gnu::always_inline]] auto nHits() const { return this->template field<Tag::UPHits>().size(); }
+      [[nodiscard, gnu::always_inline]] auto nUPHits() const { return nHits(); }
+      [[nodiscard, gnu::always_inline]] auto up_index( std::size_t i ) const {
+        return this->template field<Tag::UPHits>()[i].template get<Tag::Index>();
+      }
+      [[nodiscard, gnu::always_inline]] auto up_indices() const {
+        std::array<int_v, TracksInfo::MaxUPHits> out = {0};
+        for ( auto i = 0; i < nHits().hmax( this->loop_mask() ); i++ ) out[i] = up_index( i );
+        return out;
+      }
+      [[nodiscard, gnu::always_inline]] auto up_lhcbID( std::size_t i ) const {
+        return this->template field<Tag::UPHits>()[i].template get<Tag::LHCbID>();
+      }
+
+      // Retrieve state info
+      [[nodiscard, gnu::always_inline]] auto StatePosDir( std::size_t i ) const {
+        return this->template get<Tag::States>( i );
+      }
+
+      [[nodiscard, gnu::always_inline]] auto StatePos( std::size_t i ) const {
+        auto state = StatePosDir( i );
+        return LHCb::LinAlg::Vec<float_v, 3>{state.x(), state.y(), state.z()};
+      }
+      [[nodiscard, gnu::always_inline]] auto StateDir( std::size_t i ) const {
+        auto state = StatePosDir( i );
+        return LHCb::LinAlg::Vec<float_v, 3>{state.tx(), state.ty(), 1.f};
+      }
+
+      [[nodiscard, gnu::always_inline]] auto slopes() const { return StateDir( 0 ); }
+
+      //  Retrieve the (sorted) set of LHCbIDs
+      [[nodiscard]] std::vector<LHCbID> lhcbIDs() const {
+        std::vector<LHCbID> ids;
+        ids.reserve( TracksInfo::MaxUPHits );
+        for ( auto i = 0; i < nHits().cast(); i++ ) {
+          static_assert( width() == 1, "lhcbIDs() method cannot be used on vector proxies" );
+          ids.emplace_back( up_lhcbID( i ).LHCbID() );
+        }
+        std::sort( ids.begin(), ids.end() );
+        return ids;
+      }
+
+      // flag which indicates client code can go for 'threeMomCovMatrix', `momPosCovMatrix` and `posCovMatrix` and not
+      // for a track-like stateCov -- possible values: yes (for track-like objects ) no (for neutrals, composites),
+      // maybe (particle, check at runtime, calls may return invalid results)
+      static constexpr auto canBeExtrapolatedDownstream = Event::CanBeExtrapolatedDownstream::yes;
+
+    };
+    template <SIMDWrapper::InstructionSet simd, ProxyBehaviour behaviour, typename ContainerType>
+    using proxy_type = UPProxy<simd, behaviour, ContainerType>;
+
+  private:
+    bool m_backward{false};
+  };
+} // namespace LHCb::Pr::Velo
diff --git a/Event/TrackEvent/include/Event/UPTrackUtils.h b/Event/TrackEvent/include/Event/UPTrackUtils.h
new file mode 100644
index 0000000000000000000000000000000000000000..19d859a37d62dfca7353aceb348c181a19522c04
--- /dev/null
+++ b/Event/TrackEvent/include/Event/UPTrackUtils.h
@@ -0,0 +1,86 @@
+/*****************************************************************************\
+* (c) Copyright 2000-2018 CERN for the benefit of the LHCb Collaboration      *
+*                                                                             *
+* This software is distributed under the terms of the GNU General Public      *
+* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   *
+*                                                                             *
+* In applying this licence, CERN does not waive the privileges and immunities *
+* granted to it by virtue of its status as an Intergovernmental Organization  *
+* or submit itself to any jurisdiction.                                       *
+\*****************************************************************************/
+#pragma once
+#include "Event/PrTracksTag.h"
+#include "Event/SOACollection.h"
+
+namespace LHCb::UP::TrackUtils {
+
+  constexpr static int maxNumColsBoundariesNominal    = 3;
+  constexpr static int maxNumRowsBoundariesNominal    = 3;
+  constexpr static int maxNumSectorsBoundariesNominal = maxNumColsBoundariesNominal * maxNumRowsBoundariesNominal;
+
+  constexpr static int maxNumColsBoundariesLoose    = 6;
+  constexpr static int maxNumRowsBoundariesLoose    = 3;
+  constexpr static int maxNumSectorsBoundariesLoose = maxNumColsBoundariesLoose * maxNumRowsBoundariesLoose;
+
+  // -- A "small" state for internal purposes
+  namespace MiniStateTag {
+    struct State : Event::state_field {};
+    struct index : Event::int_field {};
+
+    template <typename T>
+    using ministate_t = Event::SOACollection<T, index, State>;
+  } // namespace MiniStateTag
+
+  struct MiniStates : MiniStateTag::ministate_t<MiniStates> {
+    using base_t = typename MiniStateTag::ministate_t<MiniStates>;
+    using base_t::base_t;
+  };
+
+  // -- Helper for storing the boundaries of sectors one needs to look at
+  // -- This is for the nominal boundaries
+  // -- Wider boundaries can be included by creating a new structure with
+  // -- a larger 'maxNumSectors'
+  namespace BoundariesNominalTag {
+    struct sects : Event::ints_field<maxNumSectorsBoundariesNominal> {};
+    struct xTol : Event::float_field {};
+    struct nPos : Event::int_field {};
+
+    // -- helper to pass the namespace as temp
+    struct types {
+      using sects = BoundariesNominalTag::sects;
+      using xTol  = BoundariesNominalTag::xTol;
+      using nPos  = BoundariesNominalTag::nPos;
+    };
+
+    template <typename T>
+    using boundary_t = Event::SOACollection<T, sects, xTol, nPos>;
+  } // namespace BoundariesNominalTag
+
+  struct BoundariesNominal : BoundariesNominalTag::boundary_t<BoundariesNominal> {
+    using base_t = typename BoundariesNominalTag::boundary_t<BoundariesNominal>;
+    using base_t::base_t;
+  };
+
+  // -- This is for a loose, low-PT configuration of VeloUP, where timing is less relevant
+  namespace BoundariesLooseTag {
+    struct sects : Event::ints_field<maxNumSectorsBoundariesLoose> {};
+    struct xTol : Event::float_field {};
+    struct nPos : Event::int_field {};
+
+    // -- helper to pass the namespace as temp
+    struct types {
+      using sects = BoundariesLooseTag::sects;
+      using xTol  = BoundariesLooseTag::xTol;
+      using nPos  = BoundariesLooseTag::nPos;
+    };
+
+    template <typename T>
+    using boundary_t = Event::SOACollection<T, sects, xTol, nPos>;
+  } // namespace BoundariesLooseTag
+
+  struct BoundariesLoose : BoundariesLooseTag::boundary_t<BoundariesLoose> {
+    using base_t = typename BoundariesLooseTag::boundary_t<BoundariesLoose>;
+    using base_t::base_t;
+  };
+
+} // namespace LHCb::UP::TrackUtils