Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 85 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,23 @@ Built from first principles and drawing upon 30 years experience scaling laborat
4. [Installation](#installation)
5. [System Architecture](#system-architecture)
6. [Core Data Model](#core-data-model)
7. [Database Schema](#database-schema)
8. [Object Hierarchy](#object-hierarchy)
9. [Template System](#template-system)
10. [Workflow Engine](#workflow-engine)
11. [Action System](#action-system)
12. [File Management](#file-management)
13. [API Layer](#api-layer)
14. [Web Interface](#web-interface)
15. [External Integrations](#external-integrations)
16. [Configuration](#configuration)
17. [Deployment](#deployment)
18. [Testing](#testing)
19. [Regulatory & Compliance](#regulatory--compliance)
20. [Design Principles](#design-principles)
21. [Dev Tools](#dev-tools)
22. [Support, Authors & License](#support)
7. [Subjects vs Objects](#subjects-vs-objects)
8. [Database Schema](#database-schema)
9. [Object Hierarchy](#object-hierarchy)
10. [Template System](#template-system)
11. [Workflow Engine](#workflow-engine)
12. [Action System](#action-system)
13. [File Management](#file-management)
14. [API Layer](#api-layer)
15. [Web Interface](#web-interface)
16. [External Integrations](#external-integrations)
17. [Configuration](#configuration)
18. [Deployment](#deployment)
19. [Testing](#testing)
20. [Regulatory & Compliance](#regulatory--compliance)
21. [Design Principles](#design-principles)
22. [Dev Tools](#dev-tools)
23. [Support, Authors & License](#support)

---

Expand Down Expand Up @@ -301,6 +302,74 @@ super_type / btype / b_sub_type / version

---

## 3.4 Subjects vs Objects

BLOOM distinguishes between **Objects** (facts) and **Subjects** (decision scopes):

### Objects (Facts)
Objects represent concrete, immutable facts about the laboratory:
- A tube exists with EUID `CX123`
- A sample was collected on 2024-01-15
- A sequencing run produced file `run_001.fastq`

Objects are the physical or digital entities that exist in the laboratory.

### Subjects (Decision Scopes)
Subjects are logical aggregates that decisions apply to. They span multiple objects and provide context for:
- Clinical decisions (e.g., "this accession is reportable")
- Workflow decisions (e.g., "this analysis bundle passed QC")
- Regulatory decisions (e.g., "this report was signed out")

### Subject Types

| Subject Kind | Description | Example Use Case |
|--------------|-------------|------------------|
| `accession` | Decision scope for an accession/intake bundle | Clinical reporting decisions |
| `analysis_bundle` | Decision scope for analysis result bundles | QC pass/fail decisions |
| `report` | Decision scope for clinical reportable units | Sign-out decisions |
| `generic` | Fallback for custom use cases | Custom workflows |

### Subject Relationships

```mermaid
graph LR
S[Subject SX1] -->|subject_anchor| A[Accession CX123]
S -->|subject_member| T1[Tube CX124]
S -->|subject_member| T2[Tube CX125]
S -->|subject_member| F[FileSet FX456]
```

- **Anchor**: The primary object that defines the subject (one per subject)
- **Members**: Additional objects associated with the subject (many per subject)

### Using Subjects

```python
from bloom_lims.subjecting import create_subject, add_subject_members, list_subjects_for_object

# Create a subject with an anchor
subject_euid = create_subject(
bob=bloom_obj,
anchor_euid="CX123",
subject_kind="accession",
)

# Add member objects
add_subject_members(bob, subject_euid, ["CX124", "CX125", "FX456"])

# Find all subjects containing an object
subjects = list_subjects_for_object(bob, "CX123")
```

### Key Design Principles

1. **Idempotency**: Creating a subject with the same anchor and kind returns the existing subject
2. **Stable Keys**: Subject keys are deterministic: `{subject_kind}:{anchor_euid}`
3. **Separation of Concerns**: Objects store facts; Subjects store decision context
4. **Audit Trail**: All subject relationships are tracked via lineage records

---

## 4. Database Schema

### 4.1 Primary Tables
Expand Down
24 changes: 24 additions & 0 deletions bloom_lims/config/action/core.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,29 @@
"description": "Add Parent or Child Relationships (aka lineages)."
}
}
},
"create-subject-and-anchor": {
"1.0": {
"action_template": {
"action_name": "Create Subject (Decision Scope)",
"method_name": "do_action_create_subject_and_anchor",
"action_executed": "0",
"max_executions": "-1",
"action_enabled": "1",
"capture_data": "yes",
"captured_data": {
"_subject_kind": "Subject Kind: <select required name=&quot;subject_kind&quot;><option value=&quot;accession&quot;>Accession</option><option value=&quot;analysis_bundle&quot;>Analysis Bundle</option><option value=&quot;report&quot;>Report</option><option value=&quot;generic&quot;>Generic</option></select>",
"_subject_name": "Subject Name (optional): <input type=&quot;text&quot; name=&quot;subject_name&quot; value=&quot;&quot;/>",
"_comments": "Comments: <textarea name=&quot;comments&quot;></textarea>"
},
"deactivate_actions_when_executed": [],
"executed_datetime": [],
"action_order": "0",
"action_simple_value": "",
"action_user": [],
"curr_user": "",
"description": "Create a Subject (decision scope) with this object as the anchor."
}
}
}
}
187 changes: 187 additions & 0 deletions bloom_lims/config/subject/generic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
{
"accession-subject": {
"1.0": {
"description": "Decision scope for an accession (ACC / TRX / intake bundle).",
"properties": {
"name": "",
"comments": "",
"lab_code": "",
"subject_kind": "accession",
"subject_key": "",
"anchor_euid": "",
"status": "active"
},
"ui_form_properties": [
{"property_key": "subject_kind", "form_label": "Subject Kind", "required": true, "value_type": "controlled"},
{"property_key": "anchor_euid", "form_label": "Anchor EUID", "required": true, "value_type": "uid-interactive"},
{"property_key": "subject_key", "form_label": "Subject Key", "required": true, "value_type": "string"},
{"property_key": "status", "form_label": "Status", "required": true, "value_type": "controlled"},
{"property_key": "name", "form_label": "Name", "required": false, "value_type": "string"},
{"property_key": "comments", "form_label": "Comments", "required": false, "value_type": "text"}
],
"controlled_properties": {
"subject_kind": {"type": "string", "enum": ["accession"]},
"status": {"type": "string", "enum": ["active", "retired"]}
},
"expected_inputs": [],
"expected_outputs": [],
"instantiation_layouts": [],
"cogs": {
"state": "inactive",
"cost": "0.00",
"cost_split_by_children": [{"*/*/*/*": {}}],
"allocation_type": ""
},
"singleton": "0",
"action_groups": {},
"action_imports": {
"core": {
"group_order": "1",
"group_name": "Core Actions",
"actions": {
"action/core/*/1.0/": {}
}
}
}
}
},
"analysis-bundle-subject": {
"1.0": {
"description": "Decision scope for analysis result bundles.",
"properties": {
"name": "",
"comments": "",
"lab_code": "",
"subject_kind": "analysis_bundle",
"subject_key": "",
"anchor_euid": "",
"status": "active"
},
"ui_form_properties": [
{"property_key": "subject_kind", "form_label": "Subject Kind", "required": true, "value_type": "controlled"},
{"property_key": "anchor_euid", "form_label": "Anchor EUID", "required": true, "value_type": "uid-interactive"},
{"property_key": "subject_key", "form_label": "Subject Key", "required": true, "value_type": "string"},
{"property_key": "status", "form_label": "Status", "required": true, "value_type": "controlled"},
{"property_key": "name", "form_label": "Name", "required": false, "value_type": "string"},
{"property_key": "comments", "form_label": "Comments", "required": false, "value_type": "text"}
],
"controlled_properties": {
"subject_kind": {"type": "string", "enum": ["analysis_bundle"]},
"status": {"type": "string", "enum": ["active", "retired"]}
},
"expected_inputs": [],
"expected_outputs": [],
"instantiation_layouts": [],
"cogs": {
"state": "inactive",
"cost": "0.00",
"cost_split_by_children": [{"*/*/*/*": {}}],
"allocation_type": ""
},
"singleton": "0",
"action_groups": {},
"action_imports": {
"core": {
"group_order": "1",
"group_name": "Core Actions",
"actions": {
"action/core/*/1.0/": {}
}
}
}
}
},
"report-subject": {
"1.0": {
"description": "Decision scope for clinical reportable units.",
"properties": {
"name": "",
"comments": "",
"lab_code": "",
"subject_kind": "report",
"subject_key": "",
"anchor_euid": "",
"status": "active"
},
"ui_form_properties": [
{"property_key": "subject_kind", "form_label": "Subject Kind", "required": true, "value_type": "controlled"},
{"property_key": "anchor_euid", "form_label": "Anchor EUID", "required": true, "value_type": "uid-interactive"},
{"property_key": "subject_key", "form_label": "Subject Key", "required": true, "value_type": "string"},
{"property_key": "status", "form_label": "Status", "required": true, "value_type": "controlled"},
{"property_key": "name", "form_label": "Name", "required": false, "value_type": "string"},
{"property_key": "comments", "form_label": "Comments", "required": false, "value_type": "text"}
],
"controlled_properties": {
"subject_kind": {"type": "string", "enum": ["report"]},
"status": {"type": "string", "enum": ["active", "retired"]}
},
"expected_inputs": [],
"expected_outputs": [],
"instantiation_layouts": [],
"cogs": {
"state": "inactive",
"cost": "0.00",
"cost_split_by_children": [{"*/*/*/*": {}}],
"allocation_type": ""
},
"singleton": "0",
"action_groups": {},
"action_imports": {
"core": {
"group_order": "1",
"group_name": "Core Actions",
"actions": {
"action/core/*/1.0/": {}
}
}
}
}
},
"generic-subject": {
"1.0": {
"description": "Fallback decision scope for custom use cases.",
"properties": {
"name": "",
"comments": "",
"lab_code": "",
"subject_kind": "generic",
"subject_key": "",
"anchor_euid": "",
"status": "active"
},
"ui_form_properties": [
{"property_key": "subject_kind", "form_label": "Subject Kind", "required": true, "value_type": "controlled"},
{"property_key": "anchor_euid", "form_label": "Anchor EUID", "required": true, "value_type": "uid-interactive"},
{"property_key": "subject_key", "form_label": "Subject Key", "required": true, "value_type": "string"},
{"property_key": "status", "form_label": "Status", "required": true, "value_type": "controlled"},
{"property_key": "name", "form_label": "Name", "required": false, "value_type": "string"},
{"property_key": "comments", "form_label": "Comments", "required": false, "value_type": "text"}
],
"controlled_properties": {
"subject_kind": {"type": "string", "enum": ["generic", "accession", "analysis_bundle", "report"]},
"status": {"type": "string", "enum": ["active", "retired"]}
},
"expected_inputs": [],
"expected_outputs": [],
"instantiation_layouts": [],
"cogs": {
"state": "inactive",
"cost": "0.00",
"cost_split_by_children": [{"*/*/*/*": {}}],
"allocation_type": ""
},
"singleton": "0",
"action_groups": {},
"action_imports": {
"core": {
"group_order": "1",
"group_name": "Core Actions",
"actions": {
"action/core/*/1.0/": {}
}
}
}
}
}
}

6 changes: 6 additions & 0 deletions bloom_lims/config/subject/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"euid_prefixes": {
"default": "SX"
}
}

19 changes: 19 additions & 0 deletions bloom_lims/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,22 @@ class file_instance_lineage(generic_instance_lineage):
}


class subject_template(generic_template):
__mapper_args__ = {
"polymorphic_identity": "subject_template",
}

class subject_instance(generic_instance):
__mapper_args__ = {
"polymorphic_identity": "subject_instance",
}

class subject_instance_lineage(generic_instance_lineage):
__mapper_args__ = {
"polymorphic_identity": "subject_instance_lineage",
}


class BLOOMdb3:
"""
BLOOM LIMS Database Connection Manager.
Expand Down Expand Up @@ -729,6 +745,9 @@ def _register_orm_classes(self) -> None:
health_event_template,
health_event_instance,
health_event_instance_lineage,
subject_template,
subject_instance,
subject_instance_lineage,
]
for cls in classes_to_register:
class_name = cls.__name__
Expand Down
Loading
Loading