← Back to Main Documentation Core Systems Index

Plings Functional Relationships System

This document defines functional relationships—links that describe how objects work together rather than where they are. These relationships complement spatial, ownership, and lifecycle data to give a complete digital twin of every physical object.

📜 Overview

Functional (a.k.a. “compatibility”) relations capture an object’s purpose, compatibility, power flow, control hierarchy, pairing, and replacement logic. Example: “Charger ➡️ powers ➡️ Laptop”, or “Remote ➡️ controls ➡️ TV”.

While spatial predicates answer “Where is it?”, functional predicates answer “How does it interact?”.

Core Design Principles

  1. Direction Matters — Except When It Doesn’t
    • Many relations are directional and come in inverse pairs (POWERS / POWERED_BY).
    • Some are symmetric (WORKS_WITH, COMPATIBLE_WITH): order is irrelevant.
  2. Freedom of Modelling — Users decide which predicates make sense; the system never blocks unusual setups.
  3. Explicit Catalogue — All valid predicates are enumerated (see tables below) and exposed by the API so clients can render friendly names and icons.
  4. Light-Weight Safeguards — Backend blocks duplicates and self-loops, auto-creates inverse edges if needed, but imposes no semantic rules like “a phone may have only one charger”. Such business logic belongs in higher-level workflows, not the core graph.

Predicate Categories & Vocabulary

Below is the canonical list (v1). The same list exists in code as FUNCTIONAL_PREDICATES (Python) and is mirrored in the GraphQL enum FunctionalPredicate.

1. Compatibility

Predicate Inverse Symmetric? Description Examples
IS_FOR ACCEPTS No Designed/intended for target Remote is_for TV
ACCEPTS IS_FOR No Accepts/works with source TV accepts Remote
WORKS_WITH Yes Function together as a system Scanner works_with Computer
COMPATIBLE_WITH Yes Can be used together USB-C cable compatible_with Phone

2. Control

Predicate Inverse Symmetric? Description Examples
CONTROLS CONTROLLED_BY No Operates/manages target Remote controls TV
CONTROLLED_BY CONTROLS No Is managed by source TV controlled_by Remote
OPERATES OPERATED_BY No Activates/runs target Switch operates Light
OPERATED_BY OPERATES No Activated by source Light operated_by Switch

3. Power

Predicate Inverse Symmetric? Description Examples
POWERS POWERED_BY No Provides electrical power Battery powers Flashlight
POWERED_BY POWERS No Receives electrical power Laptop powered_by Charger
CHARGES CHARGED_BY No Replenishes energy of Charger charges Phone
CHARGED_BY CHARGES No Energy replenished by Phone charged_by Charger

4. Access

Predicate Inverse Symmetric? Description Examples
UNLOCKS UNLOCKED_BY No Provides access to target Key unlocks Door
UNLOCKED_BY UNLOCKS No Is opened by source Door unlocked_by Key
OPENS OPENED_BY No Opens/activates target Remote opens Garage Door
OPENED_BY OPENS No Opened/activated by source Garage Door opened_by Remote

5. Pairing

Predicate Inverse Symmetric? Description Examples
PAIRS_WITH Yes Designed to be used as a pair Left glove pairs_with Right glove
MATCHES Yes Correspond / form a set Wine glass matches Dinner plate
BELONGS_WITH Yes Naturally go together TV remote belongs_with TV

6. Replacement

Predicate Inverse Symmetric? Description Examples
REPLACES REPLACED_BY No Serves as substitute for target Backup battery replaces Main battery
REPLACED_BY REPLACES No Is substituted by source Main battery replaced_by Backup

Data Storage Model

Neo4j

  • Each predicate maps to a relationship type in Neo4j, using the same uppercase name.
  • Direction = (:ObjectInstance)-[:POWERS]->(:ObjectInstance).
  • Symmetric predicates are stored once following a canonical direction (min(nodeId) → max(nodeId)), enforced in the API layer.
  • Inverse pairs are auto-created by the API so queries stay simple.

Postgres (Optional Cache)

If UI performance requires, functional relations can be mirrored in a functional_relations table with RLS constraints (see Frontend Views & Permissions doc).

GraphQL API

Operation Purpose
functionalRelations(predicateIn, direction) Query relations for an object
addFunctionalRelation(fromId, toId, predicate) Create edge (and inverse)
removeFunctionalRelation(...) Delete edge(s)

All enums and documentation are introspectable by the client so dropdowns can stay in sync.


Technical Safeguards

Safeguard Why Behaviour
Self-loop Block Prevents A POWERS A nonsense API rejects if fromId == toId
Duplicate Block Avoid redundant edges & UI clutter Reject if identical edge exists
Inverse Creation Keeps graph query-friendly API automatically writes inverse for directional pairs
Symmetric Normalisation Prevents A WORKS_WITH B + B WORKS_WITH A duplicates Store once in canonical order

No other business rules are enforced—users are free to model multiple chargers per phone, etc.


Class-Level Templates & Inheritance

Real-world products are often sold as kits whose components are defined at the class layer—e.g. “Any ✂️ Trimmer is_for any 🔋 Charger of class USB-C Charger.” To avoid constantly writing duplicate edges for every instance, Plings supports query-time inference of functional relations:

  1. Class edges
    Relations may be stored between ObjectClass nodes, using the same predicate vocabulary ((:ObjectClass)-[:POWERS]->(:ObjectClass)).
  2. Instance edges (explicit)
    Users can still create direct instance→instance edges when they need to lock a specific pairing.
  3. Instance edges (inherited / virtual)
    When a client asks for functionalRelations on an ObjectInstance, the resolver returns the union of:
    • explicit edges stored on the instance
    • virtual edges derived from its class relations.

Resolver Algorithm (simplified)

// 1. explicit
MATCH (src:ObjectInstance {id:$id})-[r:POWERS]->(t:ObjectInstance)
RETURN t, type(r)   // plus inherited = false
UNION
// 2. inherited
MATCH (src:ObjectInstance {id:$id})-[:INSTANCE_OF]->(c1:ObjectClass)
MATCH (c1)-[rc:POWERS]->(c2:ObjectClass)
MATCH (t:ObjectInstance)-[:INSTANCE_OF]->(c2)
RETURN DISTINCT t, type(rc)   // plus inherited = true

GraphQL Signature

type FunctionalRelationEdge {
  target: ObjectInstance!
  predicate: FunctionalPredicate!
  inherited: Boolean!
  sourceClassId: ID       # present when inherited = true
}

extend type ObjectInstance {
  functionalRelations(
    predicateIn: [FunctionalPredicate!]
    includeInherited: Boolean = true
  ): [FunctionalRelationEdge!]!
}

The inherited flag lets UIs render template-based relations in a lighter colour or behind a toggle.

Why Query-Time (Virtual) Inheritance?

  • Always current – If a class gains a new relation tomorrow, all its instances reflect that instantly.
  • No extra writes – Keeps Neo4j smaller; no need for migration jobs.
  • Flexible – Users may still add explicit overrides without worrying about duplicates (explicit > virtual in UI ranking).

If performance profiling ever shows this UNION is a bottleneck, the API can materialise frequently-used combinations with a background job, setting inherited=true so clients can keep treating them the same way.


Example Queries

Cypher – Find what powers a laptop

MATCH (l:ObjectInstance {id:$laptop})<-[:POWERS|CHARGES|POWERED_BY|CHARGED_BY]-(src)
RETURN src

GraphQL – Accessories accepted by a shaver

query($id:ID!){
  object(id:$id){
    functionalRelations(predicateIn:[ACCEPTS]){ id name }
  }
}

Frontend Integration

See frontend_views_and_permissions.mdObject Detail → Functional tab for component breakdown and UX flow. A static list of predicates can be loaded on app start to populate human-friendly labels and icons.


Future Extensions

  1. Capacity & Compatibility Metadata – e.g., rated_voltage property on edges.
  2. Temporal Validity – start/end timestamps for temporary replacements.
  3. Versioning – track changes to relations across time for audit trail.
  4. Predicate Customisation – allow organisations to define custom functional predicates scoped to their tenant (requires namespace strategy).

Functional relationships, together with spatial, ownership, and lifecycle data, allow Plings to represent both where every object is and how it cooperates with the things around it—forming a richly connected, queryable universal object graph.