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
3 changes: 3 additions & 0 deletions api/src/org/labkey/api/exp/query/ExpMaterialTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ enum Column
Properties,
Property,
QueryableInputs,
RawAliquotUnit(false, "Raw Aliquot Unit"),
RawAliquotVolume(false, "Raw Aliquot Total Amount"),
RawAvailableAliquotVolume(false, "Raw Available Aliquot Amount"),
RawAmount(true),
RawUnits,
RootMaterialRowId,
Expand Down
4 changes: 2 additions & 2 deletions api/src/org/labkey/api/ontology/KindOfQuantity.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public List<Unit> getCommonUnits()
@Override
public List<Unit> getCommonUnits()
{
return List.of(Unit.kg, Unit.g, Unit.mg);
return List.of(Unit.kg, Unit.g, Unit.mg, Unit.ug, Unit.ng);
}
},

Expand All @@ -39,7 +39,7 @@ public List<Unit> getCommonUnits()
@Override
public List<Unit> getCommonUnits()
{
return List.of(Unit.unit);
return List.of(Unit.blocks, Unit.bottles, Unit.boxes, Unit.cells, Unit.kits, Unit.packs, Unit.pieces, Unit.slides, Unit.tests, Unit.unit);
}
};

Expand Down
4 changes: 4 additions & 0 deletions api/src/org/labkey/api/ontology/Quantity.java
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,10 @@ public void testParse()
assertEquals(Quantity.of(0, Unit.count), parse("0 units"));
assertEquals(Quantity.of(0, Unit.count), parse("0count"));

assertEquals(Quantity.of(1, Unit.count), parse("1", Unit.boxes));
assertEquals(Quantity.of(1, Unit.unit), parse("1", Unit.blocks));
assertEquals(Quantity.of(1, Unit.cells), parse("1", Unit.tests));

assertEquals(parse("1000mg", Unit.g), parse("0.001kg", Unit.g));
assertEquals(parse(" 1000mg", Unit.g), parse("0.001kg", Unit.g));
assertEquals(parse("1000mg ", Unit.g), parse("0.001kg", Unit.g));
Expand Down
49 changes: 43 additions & 6 deletions api/src/org/labkey/api/ontology/Unit.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ public enum Unit
count(KindOfQuantity.Count, unit, 1.0, 2, "count",
Quantity.class,
"count", "count"),
pieces(KindOfQuantity.Count, unit, 1.0, 2, "pieces",
Quantity.class,
"piece", "pieces"),
packs(KindOfQuantity.Count, unit, 1.0, 2, "packs",
Quantity.class,
"pack", "packs"),
blocks(KindOfQuantity.Count, unit, 1.0, 2, "blocks",
Quantity.class,
"block", "blocks"),
slides(KindOfQuantity.Count, unit, 1.0, 2, "slides",
Quantity.class,
"slide", "slides"),
cells(KindOfQuantity.Count, unit, 1.0, 2, "cells",
Quantity.class,
"cell", "cells"),
boxes(KindOfQuantity.Count, unit, 1.0, 2, "boxes",
Quantity.class,
"box", "boxes"),
kits(KindOfQuantity.Count, unit, 1.0, 2, "kits",
Quantity.class,
"kit", "kits"),
tests(KindOfQuantity.Count, unit, 1.0, 2, "tests",
Quantity.class,
"test", "tests"),
bottles(KindOfQuantity.Count, unit, 1.0, 2, "bottles",
Quantity.class,
"bottle", "bottles"),

mL(KindOfQuantity.Volume, null, 1e0, 6, "mL",
Quantity.Volume_ml.class,
Expand Down Expand Up @@ -51,27 +78,27 @@ public enum Unit
"picoliter", "picoliters",
"pl", "picolitre", "picolitres"),

g(KindOfQuantity.Mass, null, 1e0, 9, "g",
g(KindOfQuantity.Mass, null, 1e0, 12, "g",
Quantity.Mass_g.class,
"gram", "grams"),
Mg(KindOfQuantity.Mass, g, 1e6, 12, "Mg",
Mg(KindOfQuantity.Mass, g, 1e6, 15, "Mg",
Quantity.Mass_Megag.class,
"megagram", "megagrams",
"tonne", "tonnes"),
kg(KindOfQuantity.Mass, g, 1e3, 12, "kg",
kg(KindOfQuantity.Mass, g, 1e3, 15, "kg",
Quantity.Mass_kg.class,
"kilogram", "kilograms"),
mg(KindOfQuantity.Mass, g, 1e-3, 6, "mg",
mg(KindOfQuantity.Mass, g, 1e-3, 9, "mg",
Quantity.Mass_mg.class,
"milligram", "milligrams"),
ug(KindOfQuantity.Mass, g, 1e-6, 3, "ug",
ug(KindOfQuantity.Mass, g, 1e-6, 6, "ug",
Quantity.Mass_ug.class,
"microgram", "micrograms",
"μg"),
ng(KindOfQuantity.Mass, g, 1e-9, 3, "ng",
Quantity.Mass_ng.class,
"nanogram", "nanograms"),
pg(KindOfQuantity.Mass, g, 1e-12, 3, "pg",
pg(KindOfQuantity.Mass, g, 1e-12, 0, "pg",
Quantity.Mass_pg.class,
"picogram", "picograms");

Expand Down Expand Up @@ -206,6 +233,7 @@ public void testIsBase()
assertFalse(Unit.kg.isBase());
assertTrue(Unit.unit.isBase());
assertFalse(Unit.count.isBase());
assertFalse(Unit.bottles.isBase());
}

@Test
Expand All @@ -217,7 +245,16 @@ public void testIsCompatible()
assertTrue(Unit.g.isCompatible(Unit.mg));
assertFalse(Unit.g.isCompatible(Unit.mL));
assertTrue(Unit.unit.isCompatible(Unit.count));
assertTrue(Unit.unit.isCompatible(Unit.pieces));
assertTrue(Unit.unit.isCompatible(Unit.packs));
assertTrue(Unit.unit.isCompatible(Unit.bottles));
assertTrue(Unit.unit.isCompatible(Unit.blocks));
assertTrue(Unit.unit.isCompatible(Unit.boxes));
assertTrue(Unit.unit.isCompatible(Unit.slides));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some missing here. Not that I think there's risk or regression here, just for completeness.

assertTrue(Unit.cells.isCompatible(Unit.slides));
assertTrue(Unit.cells.isCompatible(Unit.unit));
assertFalse(Unit.unit.isCompatible(Unit.mL));
assertFalse(Unit.bottles.isCompatible(Unit.mL));
assertFalse(Unit.mL.isCompatible(null));
}

Expand Down
236 changes: 236 additions & 0 deletions experiment/src/client/test/integration/SampleTypeCrud.ispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,12 @@ describe('Amount/Unit CRUD', () => {
expect(errorMsg.text).toContain(NO_AMOUNT_ERROR);
errorMsg = await ExperimentCRUDUtils.importSample(server, "Name\tStoredAmount\tUnits\nData1\t1.1\tL", dataType, "INSERT", topFolderOptions, editorUserOptions);
expect(errorMsg.text).toContain(INCOMPATIBLE_ERROR);
errorMsg = await ExperimentCRUDUtils.importSample(server, "Name\tStoredAmount\tUnits\nData1\t1.1\tunit", dataType, "INSERT", topFolderOptions, editorUserOptions);
expect(errorMsg.text).toContain('Units value (unit) is not compatible with the ' + dataType + ' display units (g).');
errorMsg = await ExperimentCRUDUtils.importSample(server, "Name\tStoredAmount\tUnits\nData1\t1.1\tcells", dataType, "INSERT", topFolderOptions, editorUserOptions);
expect(errorMsg.text).toContain('Units value (cells) is not compatible with the ' + dataType + ' display units (g).');
errorMsg = await ExperimentCRUDUtils.importSample(server, "Name\tStoredAmount\tUnits\nData1\t1.1\tbogus", dataType, "INSERT", topFolderOptions, editorUserOptions);
expect(errorMsg.text).toContain('Unsupported Units value (bogus). Supported values are: kg, g, mg, ug, ng.');
errorMsg = await ExperimentCRUDUtils.importSample(server, "Name\tStoredAmount\tUnits\nData1\t-1.1\tkg", dataType, "INSERT", topFolderOptions, editorUserOptions);
expect(errorMsg.text).toContain(NEGATIVE_ERROR);
errorMsg = await ExperimentCRUDUtils.importCrossTypeData(server, "Name\tStoredAmount\tUnits\tSampleType\nData1\t-1.1\tkg\t" + dataType ,'IMPORT', topFolderOptions, adminOptions, true);
Expand Down Expand Up @@ -1085,5 +1091,235 @@ describe('Amount/Unit CRUD', () => {

});

it ("Test units conversion on insert/update", async () => {
const sampleTypeMass = 'SampleTypeWithMassUnits';
const sampleTypeVolume = 'SampleTypeWithVolumeUnits';
const sampleTypeCount = 'SampleTypeWithCountUnits';

const sampleTypeUnits = {
[sampleTypeMass]: 'ug',
[sampleTypeVolume]: 'L',
[sampleTypeCount]: 'unit'
};

for (const [dataType, unit] of Object.entries(sampleTypeUnits)) {
const createPayload = {
kind: 'SampleSet',
domainDesign: { name: dataType, fields: [{ name: 'Name' }] },
options: {
name: dataType,
metricUnit: unit
}
};
await server.post('property', 'createDomain', createPayload, {...topFolderOptions, ...designerReaderOptions}).expect(successfulResponse);
}

let sampleRowsWithUnits = await ExperimentCRUDUtils.insertRows(server, [
{name: 'S-ng', amount: 4.56, units: 'ng'},
{name: 'S-ug', amount: 4.56, units: 'ug'},
{name: 'S-mg', amount: 4.56, units: 'mg'},
{name: 'S-g', amount: 4.56, units: 'g'},
{name: 'S-kg', amount: 4.56, units: 'kg'},
], 'samples', sampleTypeMass, topFolderOptions, editorUserOptions);

// check for raw amount in g and display amount in ug
let expectedRawAmounts : {} = {
'S-ng': 4.56e-9,
'S-ug': 4.56e-6,
'S-mg': 0.00456,
'S-g': 4.56,
'S-kg': 4560,
};
let expectedStoredAmounts : {} = {
'S-ng': 4.56e-3,
'S-ug': 4.56,
'S-mg': 4560,
'S-g': 4.56e6,
'S-kg': 4.56e9,
};

for (const sampleRow of sampleRowsWithUnits) {
const sampleName = caseInsensitive(sampleRow, 'name');
let sampleData = await ExperimentCRUDUtils.getSampleDataByName(server, sampleName, sampleTypeMass, 'StoredAmount,Units,RawAmount,RawUnits', topFolderOptions, readerUserOptions);
expect(caseInsensitive(sampleData, 'RawAmount')).toBeCloseTo(expectedRawAmounts[sampleName]);
expect(caseInsensitive(sampleData, 'StoredAmount')).toBeCloseTo(expectedStoredAmounts[sampleName]);
expect(caseInsensitive(sampleData, 'RawUnits')).toEqual('g');
expect(caseInsensitive(sampleData, 'Units')).toEqual('ug');
await server.post('query', 'updateRows', {
schemaName: 'samples',
queryName: sampleTypeMass,
rows: [{
amount: 6.54,
units: sampleName.substring(2),
rowId: caseInsensitive(sampleRow, 'rowId')
}]
}, { ...topFolderOptions, ...editorUserOptions }).expect(successfulResponse);
}

expectedRawAmounts = {
'S-ng': 6.54e-9,
'S-ug': 6.54e-6,
'S-mg': 0.00654,
'S-g': 6.54,
'S-kg': 6540,
};
expectedStoredAmounts = {
'S-ng': 6.54e-3,
'S-ug': 6.54,
'S-mg': 6540,
'S-g': 6.54e6,
'S-kg': 6.54e9,
};
for (const sampleRow of sampleRowsWithUnits) {
const sampleName = caseInsensitive(sampleRow, 'name');
let sampleData = await ExperimentCRUDUtils.getSampleDataByName(server, sampleName, sampleTypeMass, 'StoredAmount,Units,RawAmount,RawUnits', topFolderOptions, readerUserOptions);
expect(caseInsensitive(sampleData, 'RawAmount')).toBeCloseTo(expectedRawAmounts[sampleName]);
expect(caseInsensitive(sampleData, 'StoredAmount')).toBeCloseTo(expectedStoredAmounts[sampleName]);
expect(caseInsensitive(sampleData, 'RawUnits')).toEqual('g');
expect(caseInsensitive(sampleData, 'Units')).toEqual('ug');
}

sampleRowsWithUnits = await ExperimentCRUDUtils.insertRows(server, [
{name: 'S-L', amount: 4.56, units: 'L'},
{name: 'S-mL', amount: 4.56, units: 'mL'},
{name: 'S-uL', amount: 4.56, units: 'uL'},
], 'samples', sampleTypeVolume, topFolderOptions, editorUserOptions);

// check for storedamount in mL
expectedRawAmounts = {
'S-L': 4560,
'S-mL': 4.56,
'S-uL': 0.00456,
};
// stored amount is in L
expectedStoredAmounts = {
'S-L': 4.56,
'S-mL': 0.00456,
'S-uL': 4.56e-6,
}
for (const sampleRow of sampleRowsWithUnits) {
const sampleName = caseInsensitive(sampleRow, 'name');
const sampleData = await ExperimentCRUDUtils.getSampleDataByName(server, sampleName, sampleTypeVolume, 'StoredAmount,Units,RawAmount,RawUnits', topFolderOptions, readerUserOptions);
expect(caseInsensitive(sampleData, 'RawAmount')).toBeCloseTo(expectedRawAmounts[sampleName]);
expect(caseInsensitive(sampleData, 'StoredAmount')).toBeCloseTo(expectedStoredAmounts[sampleName]);
expect(caseInsensitive(sampleData, 'RawUnits')).toEqual('mL');
expect(caseInsensitive(sampleData, 'Units')).toEqual('L');
}

const countRows = [
{name: 'S-unit', amount: 4.56, units: 'unit'},
{name: 'S-pieces', amount: 4.56, units: 'pieces'},
{name: 'S-kits', amount: 4.56, units: 'kits'},
{name: 'S-cells', amount: 4.56, units: 'cells'}
]
sampleRowsWithUnits = await ExperimentCRUDUtils.insertRows(server, countRows, 'samples', sampleTypeCount, topFolderOptions, editorUserOptions);

for (const sampleRow of sampleRowsWithUnits) {
const sampleName = caseInsensitive(sampleRow, 'name');
const usedUnit = sampleName.substring(2);
const sampleData = await ExperimentCRUDUtils.getSampleDataByName(server, sampleName, sampleTypeCount, 'StoredAmount,Units,RawAmount,RawUnits', topFolderOptions, readerUserOptions);
expect(caseInsensitive(sampleData, 'RawAmount')).toBeCloseTo(4.56);
expect(caseInsensitive(sampleData, 'StoredAmount')).toBeCloseTo(4.56);
expect(caseInsensitive(sampleData, 'RawUnits')).toEqual(usedUnit);
expect(caseInsensitive(sampleData, 'Units')).toEqual(usedUnit);

await server.post('query', 'updateRows', {
schemaName: 'samples',
queryName: sampleTypeCount,
rows: [{
amount: 6.54,
units: usedUnit,
rowId: caseInsensitive(sampleRow, 'rowId')
}]
}, { ...topFolderOptions, ...editorUserOptions }).expect(successfulResponse);
}

for (const sampleRow of sampleRowsWithUnits) {
const sampleName = caseInsensitive(sampleRow, 'name');
const usedUnit = sampleName.substring(2);
const sampleData = await ExperimentCRUDUtils.getSampleDataByName(server, sampleName, sampleTypeCount, 'StoredAmount,Units,RawAmount,RawUnits', topFolderOptions, readerUserOptions);
expect(caseInsensitive(sampleData, 'RawAmount')).toBeCloseTo(6.54);
expect(caseInsensitive(sampleData, 'StoredAmount')).toBeCloseTo(6.54);
expect(caseInsensitive(sampleData, 'RawUnits')).toEqual(usedUnit);
expect(caseInsensitive(sampleData, 'Units')).toEqual(usedUnit);
}

})

async function verifyCountTypeAliquotRollup(sampleTypeName: string, hasSampleTypeDisplayUnit: boolean) {
const dataRows = [
{name: 'S-no-amount'},
{AliquotedFrom: 'S-no-amount', name: 'S-no-pcs1', amount: 2, units: 'pieces'},
{AliquotedFrom: 'S-no-amount', name: 'S-no-pcs2', amount: 2, units: 'pieces'},
{name: 'S-unit', amount: 1, units: 'unit'},
{AliquotedFrom: 'S-unit', name: 'S-unit-unit1', amount: 2, units: 'unit'},
{AliquotedFrom: 'S-unit', name: 'S-unit-unit2', amount: 2, units: 'unit'},
{name: 'S-pieces', amount: 1, units: 'pieces'},
{AliquotedFrom: 'S-pieces', name: 'S-pcs-pcs1', amount: 2, units: 'pieces'},
{AliquotedFrom: 'S-pieces', name: 'S-pcs-pcs2', amount: 2, units: 'pieces'},
{name: 'S-kits', amount: 1, units: 'kits'},
{AliquotedFrom: 'S-kits', name: 'S-kit-pcs1', amount: 2, units: 'pieces'},
{AliquotedFrom: 'S-kits', name: 'S-kit-pcs2', amount: 2, units: 'pieces'},
{name: 'S-cells', amount: 1, units: 'cells'},
{AliquotedFrom: 'S-cells', name: 'S-cells-pcs1', amount: 2, units: 'pieces'},
{AliquotedFrom: 'S-cells', name: 'S-cells-cells2', amount: 2, units: 'cells'},
]

const insertedResults = await ExperimentCRUDUtils.insertRows(server, dataRows, 'samples', sampleTypeName, topFolderOptions, editorUserOptions);
const insertedMap = {};
for (const row of insertedResults) {
insertedMap[caseInsensitive(row, 'name')] = row;
}

let expectedAliquotUnit = {
'S-no-amount': 'pieces',
'S-unit': 'unit',
'S-pieces': 'pieces',
'S-kits': 'pieces',
'S-cells': hasSampleTypeDisplayUnit ? 'unit' : 'cells',
};

// for each expectedRollupAmounts
for (const [sampleName, expectedAliquotUnitValue] of Object.entries(expectedAliquotUnit)) {
let parentUnit = sampleName.substring(2);
if (parentUnit === 'no-amount') {
parentUnit = null;
}
const sampleData = await ExperimentCRUDUtils.getSampleDataByName(server, sampleName, sampleTypeName, 'Units,RawUnits,AliquotVolume,AliquotCount,AliquotUnit', topFolderOptions, readerUserOptions);
expect(caseInsensitive(sampleData, 'RawUnits')).toEqual(parentUnit);
expect(caseInsensitive(sampleData, 'Units')).toEqual(parentUnit);
expect(caseInsensitive(sampleData, 'AliquotVolume')).toEqual(4);
expect(caseInsensitive(sampleData, 'AliquotCount')).toEqual(2);
expect(caseInsensitive(sampleData, 'AliquotUnit')).toEqual(expectedAliquotUnitValue);

}
}

it ("Test aliquot rollup for count display unit", async () => {
let dataType = 'SampleTypeAliquotWithCountUnit';
let createPayload : {} = {
kind: 'SampleSet',
domainDesign: { name: dataType, fields: [{ name: 'Name' }] },
options: {
name: dataType,
metricUnit: 'unit'
}
};
await server.post('property', 'createDomain', createPayload, {...topFolderOptions, ...designerReaderOptions}).expect(successfulResponse);
await verifyCountTypeAliquotRollup(dataType, true);

dataType = 'SampleTypeAliquoNoDisplayUnit';
createPayload = {
kind: 'SampleSet',
domainDesign: { name: dataType, fields: [{ name: 'Name' }] },
options: {
name: dataType,
}
};
await server.post('property', 'createDomain', createPayload, {...topFolderOptions, ...designerReaderOptions}).expect(successfulResponse);
await verifyCountTypeAliquotRollup(dataType, false);

})

});

3 changes: 3 additions & 0 deletions experiment/src/org/labkey/experiment/ExperimentModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,9 @@ SELECT COUNT(DISTINCT DD.DomainURI) FROM
results.put("sampleNegativeAmountCount", new SqlSelector(schema, "SELECT COUNT(*) FROM exp.material WHERE storedamount < 0").getObject(Long.class));
results.put("sampleUnitsDifferCount", new SqlSelector(schema, "SELECT COUNT(*) from exp.material m JOIN exp.materialSource s ON m.materialsourceid = s.rowid WHERE m.units != s.metricunit").getObject(Long.class));
results.put("sampleTypesWithoutUnitsCount", new SqlSelector(schema, "SELECT COUNT(*) from exp.materialSource WHERE category IS NULL AND metricunit IS NULL").getObject(Long.class));
results.put("sampleTypesWithMassTypeUnit", new SqlSelector(schema, "SELECT COUNT(*) from exp.materialSource WHERE category IS NULL AND metricunit IN ('kg', 'g', 'mg', 'ug', 'ng')").getObject(Long.class));
results.put("sampleTypesWithVolumeTypeUnit", new SqlSelector(schema, "SELECT COUNT(*) from exp.materialSource WHERE category IS NULL AND metricunit IN ('L', 'mL', 'uL')").getObject(Long.class));
results.put("sampleTypesWithCountTypeUnit", new SqlSelector(schema, "SELECT COUNT(*) from exp.materialSource WHERE category IS NULL AND metricunit = ?", "unit").getObject(Long.class));

results.put("duplicateSampleMaterialNameCount", new SqlSelector(schema, "SELECT COUNT(*) as duplicateCount FROM " +
"(SELECT name, cpastype FROM exp.material WHERE cpastype <> 'Material' GROUP BY name, cpastype HAVING COUNT(*) > 1) d").getObject(Long.class));
Expand Down
Loading