Skip to content
Open
Show file tree
Hide file tree
Changes from 18 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,
RawAliquotVolume,
RawAvailableAliquotVolume,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should set the labels for these fields here.

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.unit, Unit.blocks, Unit.bottle, Unit.box, Unit.cells, Unit.kit, Unit.pack, Unit.pcs, Unit.slides, Unit.tests);
}
};

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.box));
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
46 changes: 40 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"),
pcs(KindOfQuantity.Count, unit, 1.0, 2, "pcs",
Quantity.class,
"pcs", "pcs"),
Copy link
Contributor

Choose a reason for hiding this comment

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

I think singular here is "piece", but might want to confirm that with @labkey-hannah.

Choose a reason for hiding this comment

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

Agreed, we should switch this to "piece"/"pieces"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

pack(KindOfQuantity.Count, unit, 1.0, 2, "pack",
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"),
box(KindOfQuantity.Count, unit, 1.0, 2, "box",
Quantity.class,
"box", "boxes"),
kit(KindOfQuantity.Count, unit, 1.0, 2, "kit",
Quantity.class,
"kit", "kits"),
tests(KindOfQuantity.Count, unit, 1.0, 2, "tests",
Quantity.class,
"test", "tests"),
bottle(KindOfQuantity.Count, unit, 1.0, 2, "bottle",
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.bottle.isBase());
}

@Test
Expand All @@ -217,6 +245,12 @@ 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.pcs));
assertTrue(Unit.unit.isCompatible(Unit.pack));
assertTrue(Unit.unit.isCompatible(Unit.bottle));
assertTrue(Unit.unit.isCompatible(Unit.blocks));
assertTrue(Unit.unit.isCompatible(Unit.box));
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.

assertFalse(Unit.unit.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 storedamount in g
Copy link
Contributor

Choose a reason for hiding this comment

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

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-pcs', amount: 4.56, units: 'pcs'},
{name: 'S-kit', amount: 4.56, units: 'kit'},
{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: 'pcs'},
{AliquotedFrom: 'S-no-amount', name: 'S-no-pcs2', amount: 2, units: 'pcs'},
{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-pcs', amount: 1, units: 'pcs'},
{AliquotedFrom: 'S-pcs', name: 'S-pcs-pcs1', amount: 2, units: 'pcs'},
{AliquotedFrom: 'S-pcs', name: 'S-pcs-pcs2', amount: 2, units: 'pcs'},
{name: 'S-kit', amount: 1, units: 'kit'},
{AliquotedFrom: 'S-kit', name: 'S-kit-pcs1', amount: 2, units: 'pcs'},
{AliquotedFrom: 'S-kit', name: 'S-kit-pcs2', amount: 2, units: 'pcs'},
{name: 'S-cells', amount: 1, units: 'cells'},
{AliquotedFrom: 'S-cells', name: 'S-cells-pcs1', amount: 2, units: 'pcs'},
{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': 'pcs',
'S-unit': 'unit',
'S-pcs': 'pcs',
'S-kit': 'pcs',
'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