diff --git a/apps/nar-v3/src/components/DatasetCard.jsx b/apps/nar-v3/src/components/DatasetCard.jsx index aa8bf53..7f07c79 100644 --- a/apps/nar-v3/src/components/DatasetCard.jsx +++ b/apps/nar-v3/src/components/DatasetCard.jsx @@ -1,8 +1,9 @@ import { Fragment, useState } from "react"; -import Link from "@mui/material/Link"; import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; import Paper from "@mui/material/Paper"; import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; import { uuidFromUri } from "../utility.js"; @@ -27,22 +28,20 @@ const styles = { }; function formatUnits(units) { - return UNITS_SYMBOLS[units.name] || units.name; + return UNITS_SYMBOLS[units] || units + "s"; } function formatQuant(val) { if (val.minValue) { if (val.maxValue) { - return `${val.minValue}-${val.maxValue} ${formatUnits( - val.minValueUnits - )}`; + return `${val.minValue}-${val.maxValue} ${formatUnits(val.minValueUnit)}`; } else { - return `>=${val.minValue} ${formatUnits(val.minValueUnits)}`; + return `>=${val.minValue} ${formatUnits(val.minValueUnit)}`; } } else if (val.maxValue) { - return `<=${val.maxValue} ${formatUnits(val.maxValueUnits)}`; + return `<=${val.maxValue} ${formatUnits(val.maxValueUnit)}`; } else { - return `${val.value} ${formatUnits(val.units)}`; + return `${val.value} ${formatUnits(val.unit)}`; } } @@ -87,6 +86,17 @@ function AgeDisplay(props) { function SubjectCard(props) { const subject = props.subjects[props.index]; + let species = ""; + let strain = ""; + if (subject.species) { + if (subject.species.species) { + strain = subject.species.name; + species = subject.species.species; + } else { + species = subject.species.name; + } + } + return ( @@ -97,26 +107,32 @@ function SubjectCard(props) { )} -

Subject {subject.label}

+

Subject {subject.lookupLabel}

({props.index + 1} of {props.subjects.length})

{/* todo: add subject group information */}
Species
-
TO DO
-
Strain
-
TO DO
+
{species}
+ {strain ? ( + <> +
Strain
+
{strain}
+ + ) : ( + "" + )}
Age
- +
Age category
-
{subject.states[0].ageCategory.name}
+
{subject.studiedState[0].ageCategory}
Pathologies
- {subject.states[0].pathologies[0] - ? subject.states[0].pathologies[0].name + {subject.studiedState[0].pathology + ? subject.studiedState[0].pathology[0].name : "none"}
@@ -135,221 +151,273 @@ function SubjectCard(props) { function SlicePreparationCard(props) { const activity = props.activity; - return ( - -

Slice preparation

-

{activity.label}

-
-
Device name
-
{activity.deviceUsage[0].device.name}
-
Device type
-
{activity.deviceUsage[0].device.deviceType.name}
-
Manufacturer
-
{activity.deviceUsage[0].device.manufacturer.fullName}
-
Slice thickness
-
{formatQuant(activity.deviceUsage[0].sliceThickness)}
-
Slicing plane
-
{activity.deviceUsage[0].slicingPlane.name}
-
Study targets
-
{activity.studyTargets.map((item) => item.name).join(", ")}
-
Temperature
-
{formatQuant(activity.temperature)}
-
Dissecting solution (full details to come)
-
{activity.tissueBathSolution.name}
-
-
- ); + if (activity) { + return ( + <> + + +

Slice preparation

+

{activity.label}

+
+
Device name
+
{activity.deviceUsage[0].device.name}
+
Device type
+
{activity.deviceUsage[0].device.deviceType.name}
+
Manufacturer
+
{activity.deviceUsage[0].device.manufacturer.fullName}
+
Slice thickness
+
{formatQuant(activity.deviceUsage[0].sliceThickness)}
+
Slicing plane
+
{activity.deviceUsage[0].slicingPlane.name}
+
Study targets
+
{activity.studyTargets.map((item) => item.name).join(", ")}
+
Temperature
+
{formatQuant(activity.temperature)}
+
Dissecting solution (full details to come)
+
{activity.tissueBathSolution.name}
+
+
+ + ); + } else { + return ""; + } } function SliceCard(props) { - const slice = props.slices[props.index].slice; + if (props.slices) { + const slice = props.slices[props.index].slice; - return ( - - - {props.index > 0 ? ( - props.setIndex(props.index - 1)} /> - ) : ( - "" - )} - - -

Slice #{slice.internalIdentifier}

-

- ({props.index + 1} of {props.slices.length}) -

-
-
Location (todo: add link outs)
-
- {slice.anatomicalLocation.map((item) => item.name).join(", ")} -
-
-
- - {props.index < props.slices.length - 1 ? ( - props.setIndex(props.index + 1)} /> - ) : ( - "" - )} - -
- ); + if (slice) { + return ( + <> + + + + {props.index > 0 ? ( + props.setIndex(props.index - 1)} + /> + ) : ( + "" + )} + + +

Slice #{slice.internalIdentifier}

+

+ ({props.index + 1} of {props.slices.length}) +

+
+
Location (todo: add link outs)
+
+ {slice.anatomicalLocation.map((item) => item.name).join(", ")} +
+
+
+ + {props.index < props.slices.length - 1 ? ( + props.setIndex(props.index + 1)} /> + ) : ( + "" + )} + +
+ + ); + } + } + return ""; } function CellPatchingCard(props) { const activity = props.activity; - return ( - -

Cell patching

-

{activity.label}

- -
-
Electrode description
-
{activity.deviceUsage[0].device.description}
- {/* activity.deviceUsage[0].device.deviceType.name */} - {/* activity.deviceUsage[0].device.manufacturer.fullName */} -
Pipette solution (more details to come)
-
{activity.deviceUsage[0].pipetteSolution.name}
-
Seal resistance
-
- {activity.deviceUsage[0].sealResistance.values - .map((item) => formatQuant(item)) - .join(", ")} -
-
Series resistance
-
- {activity.deviceUsage[0].seriesResistance.values - .map((item) => formatQuant(item)) - .join(", ")} -
-
Holding potential
-
- {activity.deviceUsage[0].holdingPotential.values - .map((item) => formatQuant(item)) - .join(", ")} -
- -
Bath solution (more details to come)
-
{activity.tissueBathSolution.name}
-
Bath temperature
-
{formatQuant(activity.bathTemperature)}
-
Description
-
{activity.description}
-
Type
-
{activity.variation.name}
-
-
- ); + if (activity) { + return ( + <> + + +

Cell patching

+

{activity.label}

+ +
+
Electrode description
+
{activity.deviceUsage[0].device.description}
+ {/* activity.deviceUsage[0].device.deviceType.name */} + {/* activity.deviceUsage[0].device.manufacturer.fullName */} +
Pipette solution (more details to come)
+
{activity.deviceUsage[0].pipetteSolution.name}
+
Seal resistance
+
+ {activity.deviceUsage[0].sealResistance.values + .map((item) => formatQuant(item)) + .join(", ")} +
+
Series resistance
+
+ {activity.deviceUsage[0].seriesResistance.values + .map((item) => formatQuant(item)) + .join(", ")} +
+
Holding potential
+
+ {activity.deviceUsage[0].holdingPotential.values + .map((item) => formatQuant(item)) + .join(", ")} +
+ +
Bath solution (more details to come)
+
{activity.tissueBathSolution.name}
+
Bath temperature
+
{formatQuant(activity.bathTemperature)}
+
Description
+
{activity.description}
+
Type
+
{activity.variation.name}
+
+
+ + ); + } else { + return ""; + } } function PatchedCellCard(props) { - const cell = props.cell.cell; + if (props.cell) { + const cell = props.cell.cell; - return ( - -

Patched cell #{cell.internalIdentifier}

-
-
Location
-
{cell.anatomicalLocation.map((item) => item.name).join(", ")}
-
-
- ); + if (cell) { + return ( + <> + + +

Patched cell #{cell.internalIdentifier}

+
+
Location
+
+ {cell.anatomicalLocation.map((item) => item.name).join(", ")} +
+
+
+ + ); + } + } + return ""; } function RecordingCard(props) { const recording = props.recording; const stimulation = props.stimulation; - const stimulusSpec = JSON.parse( - stimulation.stimulus[0].specifications[0].configuration - ); - - return ( - -

Recording

-

{recording.label}

-
-
Description
-
{recording.description}
-
Additional remarks
-
{recording.recording[0].metadata.additionalRemarks}
-
Sampling frequency
-
- {formatQuant(recording.recording[0].metadata.samplingFrequency)} -
-
Channels
-
-
    - {recording.recording[0].metadata.channels.map((item) => ( -
  • - {item.internalIdentifier} ({formatUnits(item.units)}) -
  • + if (recording) { + const stimulusSpec = JSON.parse( + stimulation.stimulus[0].specifications[0].configuration + ); + + return ( + <> + + +

    Recording

    +

    {recording.label}

    +
    +
    Description
    +
    {recording.description}
    +
    Additional remarks
    +
    {recording.recording[0].metadata.additionalRemarks}
    +
    Sampling frequency
    +
    + {formatQuant(recording.recording[0].metadata.samplingFrequency)} +
    +
    Channels
    +
    +
      + {recording.recording[0].metadata.channels.map((item) => ( +
    • + {item.internalIdentifier} ({formatUnits(item.unit)}) +
    • + ))} +
    +
    +
    + +

    Stimulation

    +

    {stimulation.label}

    +
    +
    Type
    +
    Current injection
    +
    Description
    +
    {stimulation.stimulus[0].label}
    +
    Epoch duration
    +
    {formatQuant(stimulation.stimulus[0].epoch)}
    +
    Identifier
    +
    {stimulation.stimulus[0].internalIdentifier}
    +
    +

    Specification

    +
    + {Object.entries(stimulusSpec).map((item, index) => ( + +
    {item[0]}
    +
    {item[1]}
    +
    ))} -
-
-
- -

Stimulation

-

{stimulation.label}

-
-
Type
-
Current injection
-
Description
-
{stimulation.stimulus[0].label}
-
Epoch duration
-
{formatQuant(stimulation.stimulus[0].epoch)}
-
Identifier
-
{stimulation.stimulus[0].internalIdentifier}
-
-

Specification

-
- {Object.entries(stimulusSpec).map((item, index) => ( - -
{item[0]}
-
{item[1]}
-
- ))} -
-
- ); + +
+ + ); + } else { + return ""; + } } function DataFileCard(props) { const fileObj = props.fileObj; - return ( - -

File {fileObj.name}

-
-
Data type
-
{fileObj.dataType ? fileObj.dataType.name : "unknown"}
-
Format
-
{fileObj.format ? fileObj.format.name : "unknown"}
-
Hash
-
- {fileObj.hash.map((item) => ( - - {item.algorithm}: {item.digest}  - - ))} -
-
Size
-
{formatQuant(fileObj.size)}
-
-
- ); + if (fileObj) { + return ( + <> + + +

File {fileObj.name}

+
+
Data type
+
{fileObj.dataType ? fileObj.dataType.name : "unknown"}
+
Format
+
{fileObj.format ? fileObj.format.name : "unknown"}
+
Hash
+
+ {fileObj.hash.map((item) => ( + + {item.algorithm}: {item.digest}  + + ))} +
+
Size
+
{formatQuant(fileObj.size)}
+
+
+ + ); + } else { + return ""; + } } function DatasetCard(props) { const dataset = props.dataset; + const subjects = dataset.studiedSpecimen; + const [subjectIndex, _setSubjectIndex] = useState(0); const [sliceIndex, _setSliceIndex] = useState(0); const setSubjectIndex = (index) => { - if (index >= 0 && index < dataset.subjects.length) { + if (index >= 0 && index < subjects.length) { _setSubjectIndex(index); - setSliceIndex(0); + if (subjects[index].studiedState[0].slicePreparation) { + setSliceIndex(0); + } } }; @@ -357,102 +425,154 @@ function DatasetCard(props) { if ( index >= 0 && index < - dataset.subjects[subjectIndex].states[0].slicePreparation[0].slices - .length + subjects[subjectIndex].studiedState[0].slicePreparation[0].slices.length ) { _setSliceIndex(index); } }; + const getSlicePreparation = (subjectIndex) => { + const state = subjects[subjectIndex].studiedState[0]; + if (state.slicePreparation) { + return state.slicePreparation[0]; + } else { + return null; + } + }; + + const getSlices = (subjectIndex) => { + const slicePrep = getSlicePreparation(subjectIndex); + if (slicePrep) { + return slicePrep.slices; + } else { + return null; + } + }; + + const getCellPatching = (subjectIndex, sliceIndex) => { + const slices = getSlices(subjectIndex); + if (slices) { + const slice = slices[sliceIndex]; + return slice.cellPatching[0]; + } else { + return null; + } + }; + + const getPatchedCell = (subjectIndex, sliceIndex) => { + const cellPatching = getCellPatching(subjectIndex, sliceIndex); + if (cellPatching) { + return cellPatching.patchedCells[0]; + } else { + return null; + } + }; + + const getRecordingActivity = (subjectIndex, sliceIndex) => { + const patchedCell = getPatchedCell(subjectIndex, sliceIndex); + if (patchedCell) { + return patchedCell.recordingActivity[0]; + } else { + return null; + } + }; + + const getStimulationActivity = (subjectIndex, sliceIndex) => { + const patchedCell = getPatchedCell(subjectIndex, sliceIndex); + if (patchedCell) { + return patchedCell.stimulationActivity[0]; + } else { + return null; + } + }; + + const getDataFiles = (subjectIndex, sliceIndex) => { + const recordingActivity = getRecordingActivity(subjectIndex, sliceIndex); + if (recordingActivity) { + return recordingActivity.files[0]; + } else { + return null; + } + }; + + const formatAuthors = (dataset) => { + const authors = + dataset.author.length > 0 ? dataset.author : dataset.isVersionOf.author; + return authors + .map((person) => `${person.givenName} ${person.familyName}`) + .join(", "); + }; + console.log("Rendering dataset in DatasetCard.jsx"); console.log(dataset); return (
-

{dataset.fullName}

-

- + {dataset.fullName || dataset.isVersionOf.fullName} +

+ View in KG Search + + + {formatAuthors(dataset)} + +
+ Licence: + {dataset.license} +
+
+ Ethics assessment: + {dataset.ethicsAssessment} +
+
+ Version: + {dataset.versionIdentifier} +
+
+ Release date: + {dataset.releaseDate} +
+
+

{dataset.description || dataset.isVersionOf.description}

- {dataset.subjects ? ( + {subjects ? ( - - - - - + - - - - - - - + - - - + ) : ( "" diff --git a/apps/nar-v3/src/example_data/example_patch_clamp_dataset.json b/apps/nar-v3/src/example_data/example_patch_clamp_dataset.json index 2048bb1..ad5bf43 100644 --- a/apps/nar-v3/src/example_data/example_patch_clamp_dataset.json +++ b/apps/nar-v3/src/example_data/example_patch_clamp_dataset.json @@ -5,15 +5,15 @@ "@type": [ "https://openminds.ebrains.eu/core/DatasetVersion" ], - "subjects": [ + "studiedSpecimen": [ { - "label": "AD-Ctrl_s95", + "lookupLabel": "AD-Ctrl_s95", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "AD-Ctrl_s95-state01", + "lookupLabel": "AD-Ctrl_s95-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -1937,13 +1937,13 @@ ] }, { - "label": "AD-Ctrl_s86", + "lookupLabel": "AD-Ctrl_s86", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "AD-Ctrl_s86-state01", + "lookupLabel": "AD-Ctrl_s86-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -2947,13 +2947,13 @@ ] }, { - "label": "AD-Ctrl_s76", + "lookupLabel": "AD-Ctrl_s76", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "AD-Ctrl_s76-state01", + "lookupLabel": "AD-Ctrl_s76-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -4417,13 +4417,13 @@ ] }, { - "label": "AD-Ctrl_s75", + "lookupLabel": "AD-Ctrl_s75", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "AD-Ctrl_s75-state01", + "lookupLabel": "AD-Ctrl_s75-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -5197,13 +5197,13 @@ ] }, { - "label": "AD-Ctrl-s27", + "lookupLabel": "AD-Ctrl-s27", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "AD-Ctrl-s27-state01", + "lookupLabel": "AD-Ctrl-s27-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -5747,13 +5747,13 @@ ] }, { - "label": "AD-Ctrl-s31", + "lookupLabel": "AD-Ctrl-s31", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "AD-Ctrl-s31-state01", + "lookupLabel": "AD-Ctrl-s31-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -6987,13 +6987,13 @@ ] }, { - "label": "AD-Ctrl-s24", + "lookupLabel": "AD-Ctrl-s24", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "AD-Ctrl-s24-state01", + "lookupLabel": "AD-Ctrl-s24-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -8457,13 +8457,13 @@ ] }, { - "label": "APPPS_s96", + "lookupLabel": "APPPS_s96", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "APPPS_s96-state01", + "lookupLabel": "APPPS_s96-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -10161,13 +10161,13 @@ ] }, { - "label": "APPPS_s87", + "lookupLabel": "APPPS_s87", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "APPPS_s87-state01", + "lookupLabel": "APPPS_s87-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -12095,13 +12095,13 @@ ] }, { - "label": "APPPS_s77", + "lookupLabel": "APPPS_s77", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "APPPS_s77-state01", + "lookupLabel": "APPPS_s77-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -13799,13 +13799,13 @@ ] }, { - "label": "APPPS_s26", + "lookupLabel": "APPPS_s26", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "APPPS_s26-state01", + "lookupLabel": "APPPS_s26-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -14353,13 +14353,13 @@ ] }, { - "label": "APPPS_s22", + "lookupLabel": "APPPS_s22", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "APPPS_s22-state01", + "lookupLabel": "APPPS_s22-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], @@ -15827,13 +15827,13 @@ ] }, { - "label": "APPPS_s23", + "lookupLabel": "APPPS_s23", "@type": [ "https://openminds.ebrains.eu/core/Subject" ], - "states": [ + "studiedState": [ { - "label": "APPPS_s23-state01", + "lookupLabel": "APPPS_s23-state01", "@type": [ "https://openminds.ebrains.eu/core/SubjectState" ], diff --git a/apps/nar-v3/src/queries.js b/apps/nar-v3/src/queries.js index 6a4e6bc..c1a7568 100644 --- a/apps/nar-v3/src/queries.js +++ b/apps/nar-v3/src/queries.js @@ -59,9 +59,10 @@ function linkProperty(name, structure, options) { const defaultOptions = { expectSingle: true, filter: "", - required: false + required: false, + type: null } - const {expectSingle, filter, required} = {...defaultOptions, ...options} + const {expectSingle, filter, required, type} = {...defaultOptions, ...options} let prop = simpleProperty(name); if (expectSingle) { @@ -70,6 +71,16 @@ function linkProperty(name, structure, options) { if (structure && structure.length > 0) { prop.structure = structure } + if (type) { + if (typeof prop.path === 'string') { + prop.path = [{ + "@id": prop.path + }] + } + prop.path[0].typeFilter = { + "@id": `https://openminds.ebrains.eu/${type}` + } + } if (filter) { prop.filter = { op: "CONTAINS", diff --git a/apps/nar-v3/src/routes/dataset.jsx b/apps/nar-v3/src/routes/dataset.jsx index 8fc22f7..5944683 100644 --- a/apps/nar-v3/src/routes/dataset.jsx +++ b/apps/nar-v3/src/routes/dataset.jsx @@ -1,24 +1,74 @@ import React from "react"; import { Await, defer, useLoaderData } from "react-router-dom"; -import { buildKGQuery, simpleProperty as S, linkProperty as L, reverseLinkProperty as R } from "../queries"; +import { + buildKGQuery, + simpleProperty as S, + linkProperty as L, + reverseLinkProperty as R, +} from "../queries"; import { datastore } from "../datastore"; import { uuidFromUri } from "../utility.js"; import Navigation from "../components/Navigation"; import DatasetCard from "../components/DatasetCard"; import ProgressIndicator from "../components/ProgressIndicator"; +const actorQuery = [ + S("@id"), + S("@type"), + S("givenName"), + S("familyName"), + S("fullName"), + S("shortName") +] + const query = buildKGQuery("core/DatasetVersion", [ S("@id"), S("fullName"), S("description"), S("shortName"), S("versionIdentifier"), + L("ethicsAssessment/name"), + L("license/shortName"), + S("releaseDate"), + L("custodian", actorQuery, { expectSingle: false }), + L("author", actorQuery, { expectSingle: false }), R("isVersionOf", "hasVersion", [ S("fullName"), S("description"), S("shortName"), + L("custodian", actorQuery, { expectSingle: false }), + L("author", actorQuery, { expectSingle: false }), ]), + L( + "studiedSpecimen", + [ + S("lookupLabel"), + L("species", [ + S("name"), + L("species/name") + ]), + L("biologicalSex/name"), + L( + "studiedState", + [ + S("lookupLabel"), + L("age", [ + S("value"), + S("minValue"), + S("maxValue"), + L("unit/name"), + L("minValueUnit/name"), + L("maxValueUnit/name"), + ]), + L("ageCategory/name"), + L("pathology", [], { expectSingle: false }), + ], + { expectSingle: false } + ), + ], + { type: "core/Subject", expectSingle: false } + ), ]); export async function loader({ params }) { @@ -47,7 +97,10 @@ function Dataset(props) { return ( <> diff --git a/apps/nar-v3/tmp/build_query.py b/apps/nar-v3/tmp/build_query.py index c12e782..1f59663 100644 --- a/apps/nar-v3/tmp/build_query.py +++ b/apps/nar-v3/tmp/build_query.py @@ -58,16 +58,16 @@ QP("@type"), QP( f"{vocab}studiedSpecimen", - name="subjects", + name="studiedSpecimen", type_filter=omcore.Subject.type_[0], properties=[ - QP(f"{vocab}lookupLabel", name="label"), + QP(f"{vocab}lookupLabel", name="lookupLabel"), QP("@type"), QP( f"{vocab}studiedState", - name="states", + name="studiedState", properties=[ - QP(f"{vocab}lookupLabel", name="label"), + QP(f"{vocab}lookupLabel", name="lookupLabel"), QP("@type"), QP( f"{vocab}age", @@ -105,7 +105,7 @@ ), QP( f"{vocab}pathology", - name="pathologies", + name="pathology", properties=[QP(f"{vocab}name", name="name")], ), QP(