Custom Sample Types#
This guide explains how to create and configure custom Sample types in your FairDM portal.
Overview#
Samples are physical or digital objects that form the core of your research data. FairDM provides a flexible Sample base class that you extend with your domain-specific fields and behaviors.
Key Features#
Polymorphic Inheritance: All Sample subclasses are stored in a single table with automatic type detection
Rich Metadata: Built-in support for descriptions, dates, identifiers, and contributors
Relationships: Track provenance and relationships between samples
Location Support: Optional spatial data integration
Registry Integration: Automatic form, filter, and admin generation
Basic Sample Creation#
Simple Example#
from fairdm.core.sample.models import Sample
from django.db import models
class RockSample(Sample):
"""Geological rock sample with basic metadata."""
rock_type = models.CharField(
max_length=100,
help_text="Type of rock (e.g., igneous, sedimentary, metamorphic)"
)
collection_date = models.DateField(
help_text="Date the sample was collected"
)
weight_grams = models.FloatField(
null=True,
blank=True,
help_text="Sample weight in grams"
)
class Meta:
verbose_name = "Rock Sample"
verbose_name_plural = "Rock Samples"
Required Fields#
The Sample base class provides these required fields:
name: Short identifier/label for the sampledataset: Foreign key to the parent Datasetuuid: Automatically generated unique identifier
Optional Fields#
local_id: Local identifier within your lab/projectstatus: Sample status (available, used, archived, etc.)location: Spatial location (requires GeoDjango)
Advanced Field Types#
Using QuantityField#
For measurements with units:
from fairdm.db.models import QuantityField
class WaterSample(Sample):
temperature = QuantityField(
base_units="celsius",
help_text="Water temperature at collection"
)
ph_level = models.FloatField(
validators=[MinValueValidator(0), MaxValueValidator(14)]
)
Using ConceptField#
For controlled vocabulary fields:
from fairdm.db.models import ConceptField
class SoilSample(Sample):
soil_type = ConceptField(
vocabulary="soil_taxonomy",
help_text="USDA Soil Taxonomy classification"
)
texture = ConceptField(
vocabulary="soil_texture",
null=True,
blank=True
)
Using PartialDateField#
For dates with varying precision:
from fairdm.db.models import PartialDateField
class ArchaeologicalSample(Sample):
estimated_age = PartialDateField(
help_text="Estimated date (can be year, year-month, or full date)"
)
# Can store: "2024", "2024-03", or "2024-03-15"
Sample Metadata#
Samples support rich metadata through related models:
Descriptions#
Multiple descriptions of different types:
from fairdm.core.sample.models import SampleDescription
# In your view or factory
SampleDescription.objects.create(
related=my_sample,
type="abstract",
value="Basalt sample from mid-ocean ridge"
)
SampleDescription.objects.create(
related=my_sample,
type="methods",
value="Collected using rock hammer and chisel"
)
Available description types:
abstract: Brief summarymethods: Collection/preparation methodsother: Any other description type
Dates#
Track important dates:
from fairdm.core.sample.models import SampleDate
SampleDate.objects.create(
related=my_sample,
type="collected",
value="2024-03-15" # PartialDate format
)
Available date types:
collected: Sample collection dateavailable: Date made availablecreated: Record creation date
Identifiers#
Assign persistent identifiers:
from fairdm.core.sample.models import SampleIdentifier
# IGSN (International Geo Sample Number)
SampleIdentifier.objects.create(
related=my_sample,
type="IGSN",
value="IEABC0123"
)
# Local barcode
SampleIdentifier.objects.create(
related=my_sample,
type="barcode",
value="RS-2024-001"
)
Contributors#
Track who collected, analyzed, or owns samples:
from fairdm.core.sample.models import SampleContribution
SampleContribution.objects.create(
related=my_sample,
contributor=researcher, # User or Contact instance
roles=["collector", "analyst"]
)
Sample Relationships#
Track provenance and relationships between samples:
from fairdm.core.sample.models import SampleRelation
# Parent-child relationship
SampleRelation.objects.create(
source=child_sample,
target=parent_sample,
type="child_of",
description="Split from parent core sample"
)
# Derived sample
SampleRelation.objects.create(
source=powder_sample,
target=rock_sample,
type="derived-from",
description="Crushed to powder for XRD analysis"
)
Common Relationship Types#
child_of: Sample is a child/subsample of anotherderived-from: Sample derived through processingsplit-from: Sample split from a larger samplereplicate-of: Duplicate/replicate sample
Querying Relationships#
# Get child samples
children = parent_sample.get_children()
# Get parent samples
parents = child_sample.get_parents()
# Get all descendants (recursive)
descendants = parent_sample.get_descendants(depth=5)
# Query by relationship type
related = Sample.objects.by_relationship(
related_to=my_sample,
relationship_type="child_of"
)
Polymorphic Queries#
Django-polymorphic automatically returns the correct subclass type:
# Returns RockSample, WaterSample, etc. (not base Sample)
samples = Sample.objects.all()
for sample in samples:
print(type(sample).__name__) # RockSample, WaterSample, etc.
# Can access subclass-specific fields directly
if hasattr(sample, 'rock_type'):
print(f"Rock type: {sample.rock_type}")
Filtering by Type#
# Only get specific types
rocks = Sample.objects.instance_of(RockSample)
water_or_soil = Sample.objects.instance_of(WaterSample, SoilSample)
# Exclude types
non_rocks = Sample.objects.not_instance_of(RockSample)
Query Optimization#
Use provided QuerySet methods to optimize database queries:
# Prefetch related data in bulk
samples = Sample.objects.with_related() # Dataset, location, contributors
# Prefetch metadata
samples = Sample.objects.with_metadata() # Descriptions, dates, identifiers
# Chain optimization methods
samples = (
Sample.objects
.with_related()
.with_metadata()
.filter(dataset=my_dataset)
.order_by('name')
)
# Access prefetched data without additional queries
for sample in samples:
print(sample.dataset.name) # No query
print(list(sample.descriptions.all())) # No query
Custom QuerySet Methods#
Add domain-specific query methods:
from fairdm.core.sample.managers import SampleQuerySet
class RockSampleQuerySet(SampleQuerySet):
"""Custom queryset for RockSample."""
def igneous(self):
"""Filter for igneous rocks only."""
return self.filter(rock_type__icontains="igneous")
def by_weight_range(self, min_grams=None, max_grams=None):
"""Filter by weight range."""
qs = self
if min_grams is not None:
qs = qs.filter(weight_grams__gte=min_grams)
if max_grams is not None:
qs = qs.filter(weight_grams__lte=max_grams)
return qs
class RockSample(Sample):
rock_type = models.CharField(max_length=100)
weight_grams = models.FloatField(null=True, blank=True)
objects = RockSampleQuerySet.as_manager()
class Meta:
verbose_name = "Rock Sample"
verbose_name_plural = "Rock Samples"
# Usage
igneous_rocks = RockSample.objects.igneous()
heavy_rocks = RockSample.objects.by_weight_range(min_grams=100)
Validation#
Add custom validation logic:
from django.core.exceptions import ValidationError
class WaterSample(Sample):
ph_level = models.FloatField()
temperature_celsius = models.FloatField()
def clean(self):
"""Validate pH is in valid range."""
super().clean()
if self.ph_level < 0 or self.ph_level > 14:
raise ValidationError({
'ph_level': 'pH must be between 0 and 14'
})
if self.temperature_celsius < -273.15:
raise ValidationError({
'temperature_celsius': 'Temperature cannot be below absolute zero'
})
Registry Configuration#
Register your Sample type for automatic integration:
# In your app's config.py or registry.py
from fairdm.registry import register
from fairdm.registry.config import ModelConfiguration
@register
class RockSampleConfig(ModelConfiguration):
model = RockSample
# Fields to show in tables/lists
fields = ["name", "local_id", "rock_type", "collection_date", "weight_grams"]
# Display metadata
display_name = "Rock Sample"
description = "Geological rock samples with collection metadata"
# This automatically generates:
# - ModelForm for create/edit
# - FilterSet for filtering
# - Table for list views
# - ModelAdmin for admin site
See Model Configuration for complete registry documentation.
Testing Custom Samples#
Basic Tests#
import pytest
from datetime import date
from myapp.models import RockSample
@pytest.mark.django_db
def test_rock_sample_creation(dataset):
"""Test creating a rock sample with required fields."""
sample = RockSample.objects.create(
name="RS-001",
dataset=dataset,
rock_type="igneous",
collection_date=date.today()
)
assert sample.pk is not None
assert sample.rock_type == "igneous"
assert sample.uuid.startswith("s_")
@pytest.mark.django_db
def test_rock_sample_validation():
"""Test sample validation logic."""
sample = RockSample(
name="Invalid",
rock_type="", # Empty rock_type
)
with pytest.raises(ValidationError):
sample.full_clean()
Testing Relationships#
@pytest.mark.django_db
def test_sample_relationships(dataset):
"""Test creating and querying sample relationships."""
from fairdm.core.sample.models import SampleRelation
parent = RockSample.objects.create(
name="Parent Core",
dataset=dataset,
rock_type="igneous",
collection_date=date.today()
)
child = RockSample.objects.create(
name="Child Sample",
dataset=dataset,
rock_type="igneous",
collection_date=date.today()
)
SampleRelation.objects.create(
source=child,
target=parent,
type="child_of"
)
# Test convenience methods
assert child in parent.get_children()
assert parent in child.get_parents()
Best Practices#
Model Organization#
Keep models focused: One Sample type per physical object type
Use descriptive names:
WaterSamplenotSample1Add help_text: Document every field’s purpose and format
Use appropriate field types:
FloatFieldfor decimals,IntegerFieldfor counts
Field Design#
Avoid null=True on CharFields: Use
blank=Trueand empty string insteadProvide choices where applicable: Use
choicesparameter for fixed optionsSet max_length appropriately: Don’t use
max_length=9999- be realisticUse validators: Add range checks, format validators, etc.
Documentation#
Write docstrings: Document the purpose of each Sample type
Document relationships: Explain parent-child hierarchies in docstrings
Provide examples: Include usage examples in docstrings
Keep Meta updated: Set verbose_name and verbose_name_plural
Performance#
Use with_related(): Always prefetch related data when iterating
Add select_related for FKs: For your own custom foreign keys
Index frequently queried fields: Add
db_index=Trueto filtered fieldsAvoid N+1 queries: Test query counts in your integration tests
Common Patterns#
Hierarchical Samples#
For samples with parent-child hierarchies:
# Creating a hierarchy
core = CoreSample.objects.create(name="Core-001", dataset=dataset)
section_a = CoreSection.objects.create(name="Section-A", dataset=dataset)
section_b = CoreSection.objects.create(name="Section-B", dataset=dataset)
# Link sections to core
SampleRelation.objects.create(source=section_a, target=core, type="child_of")
SampleRelation.objects.create(source=section_b, target=core, type="child_of")
# Query descendants
all_sections = core.get_descendants()
Replicate Samples#
For duplicate samples:
original = WaterSample.objects.create(...)
replicate = WaterSample.objects.create(...)
SampleRelation.objects.create(
source=replicate,
target=original,
type="replicate-of",
description="Field replicate for quality control"
)
Derived Samples#
For processed samples:
original = RockSample.objects.create(...)
powder = PowderSample.objects.create(...)
SampleRelation.objects.create(
source=powder,
target=original,
type="derived-from",
description="Ground to <63μm powder"
)
Troubleshooting#
Polymorphic Queries Not Working#
If you’re getting base Sample objects instead of typed instances:
Ensure you’re querying through
Sample.objectsnotRockSample.objectsCheck that
polymorphic_ctypeis being set correctlyVerify migrations are up to date
Circular Relationship Errors#
If you’re getting validation errors on relationships:
# This is prevented by validation
SampleRelation.objects.create(source=sample_a, target=sample_a, type="child_of") # Error
# This is also prevented
SampleRelation.objects.create(source=sample_a, target=sample_b, type="child_of")
SampleRelation.objects.create(source=sample_b, target=sample_a, type="child_of") # Error
Query Performance Issues#
If queries are slow:
Use
with_related()andwith_metadata()consistentlyAdd database indexes to frequently filtered fields
Use
only()anddefer()for large querysets where you don’t need all fieldsConsider using
select_related()for your custom foreign keys
See Also#
Model Configuration - Registry configuration
Defining Models - General model patterns
Special Fields - Custom field types
Filtering - Filter configuration