001/*
002 * 
003 */
004package de.isas.lipidomics.domain;
005
006import static de.isas.lipidomics.domain.Element.ELEMENT_H;
007import de.isas.lipidomics.palinom.exceptions.ConstraintViolationException;
008import java.util.Collections;
009import java.util.Map;
010import java.util.Optional;
011import lombok.AccessLevel;
012import lombok.Data;
013import lombok.Setter;
014
015/**
016 * A lipid species is the factual root of the object hierarchy. Lipid category
017 * and class are used as taxonomic roots of a lipid species. Partial structural
018 * knowledge, apart from the head group, is first encoded in the lipid species.
019 *
020 * A typical lipid species is PC 32:0 (SwissLipids SLM:000056493), where the
021 * head group is defined as PC (Glycerophosphocholines), with fatty acyl chains
022 * of unknown individual composition, but known total composition (32 carbon
023 * atoms, zero double bonds, no hydroxylations).
024 *
025 * @author nils.hoffmann
026 * @see LipidCategory
027 * @see LipidClass
028 * @see LipidMolecularSubspecies
029 * @see LipidStructuralSubspecies
030 * @see LipidIsomericSubspecies
031 */
032@Data
033public class LipidSpecies {
034
035    private static final class None extends LipidSpecies {
036
037        private None() {
038            super(new HeadGroup(""), Optional.of(LipidSpeciesInfo.NONE));
039        }
040    }
041
042    public static final LipidSpecies NONE = new None();
043    private final HeadGroup headGroup;
044    @Setter(AccessLevel.NONE)
045    protected LipidSpeciesInfo info;
046
047    /**
048     * Create a lipid species using the provided head group and a lipid species
049     * info {@link LipidSpeciesInfo#NONE}.
050     *
051     * @param headGroup the lipid species head group.
052     */
053    public LipidSpecies(HeadGroup headGroup) {
054        this(headGroup, Optional.of(LipidSpeciesInfo.NONE));
055    }
056
057    /**
058     * Create a lipid species from a head group and an optional
059     * {@link LipidSpeciesInfo}. This constructor will infer the lipid class
060     * from the head group automatically. It then uses the lipid class to
061     * retrieve the category of this lipid automatically, or sets the category
062     * to {@link LipidCategory#UNDEFINED}. The lipid species info, which
063     * contains details about the total no. of carbons in FA chains, no. of
064     * double bonds etc., is used as provided.
065     *
066     * @param headGroup the lipid species head group.
067     * @param lipidSpeciesInfo the lipid species info object.
068     */
069    public LipidSpecies(HeadGroup headGroup, Optional<LipidSpeciesInfo> lipidSpeciesInfo) {
070        this.headGroup = headGroup;
071        this.info = lipidSpeciesInfo.orElse(LipidSpeciesInfo.NONE);
072    }
073
074    /**
075     * Returns the {@link LipidSpeciesInfo} for this lipid.
076     *
077     * @return the lipid species info.
078     */
079    public LipidSpeciesInfo getInfo() {
080        return this.info;
081    }
082
083    /**
084     * Returns true, if the head group ends with ' O' or if the lipid fa bond
085     * type is either {@link LipidFaBondType#ETHER_UNSPECIFIED},
086     * {@link LipidFaBondType#ETHER_PLASMANYL} or
087     * {@link LipidFaBondType#ETHER_PLASMENYL}.
088     *
089     * @return whether this is an 'ether' lipid, e.g. a unspecified ether
090     * species, a Plasmanyl or Plasmenyl species.
091     */
092    public boolean isEtherLipid() {
093        LipidSpeciesInfo info = this.info;
094        LipidFaBondType bondType = info.getLipidFaBondType();
095        return bondType == LipidFaBondType.ETHER_PLASMANYL
096                || bondType == LipidFaBondType.ETHER_PLASMENYL
097                || bondType == LipidFaBondType.ETHER_UNSPECIFIED
098                || getFa().values().stream().anyMatch((t) -> {
099                    return t.getLipidFaBondType() == LipidFaBondType.ETHER_UNSPECIFIED
100                            || t.getLipidFaBondType() == LipidFaBondType.ETHER_PLASMANYL
101                            || t.getLipidFaBondType() == LipidFaBondType.ETHER_PLASMENYL;
102                });
103    }
104
105    /**
106     * Returns a lipid string representation for the {@link LipidLevel}, e.g.
107     * Category, Species, etc, as returned by {@link #getInfo()}.
108     *
109     * Will return the head group name if the level is
110     * {@link LipidSpeciesInfo#NONE}.
111     *
112     * @return the lipid name for the native level.
113     */
114    public String getLipidString() {
115        return getLipidString(getInfo().getLevel());
116    }
117
118    /**
119     * Returns a lipid string representation for the given {@link LipidLevel},
120     * e.g. Category, Species, etc. Please note that this method is overridden
121     * by specific implementations for molecular, structural and isomeric
122     * subspecies levels. This method does not normalize the head group.
123     *
124     * @param level the lipid level to report the name of this lipid on.
125     * @return the lipid name.
126     */
127    public String getLipidString(LipidLevel level) {
128        return this.buildLipidString(level, headGroup.getName(), false);
129    }
130
131    /**
132     * Returns a lipid string representation for the given {@link LipidLevel},
133     * e.g. Category, Species, etc. Please note that this method is overridden
134     * by specific implementations for molecular, structural and isomeric
135     * subspecies levels. This method normalizes the head group to the primary
136     * class-specific synonym. E.g. TG would be normalized to TAG.
137     *
138     * @param level the lipid level to report the name of this lipid on.
139     * @param normalizeHeadGroup if true, use class specific synonym for
140     * headGroup, if false, use head group as parsed.
141     * @return the lipid name.
142     */
143    public String getLipidString(LipidLevel level, boolean normalizeHeadGroup) {
144        return this.buildLipidString(level, normalizeHeadGroup ? getNormalizedHeadGroup() : headGroup.getName(), normalizeHeadGroup);
145    }
146
147    protected StringBuilder buildSpeciesHeadGroupString(String headGroup, boolean normalizeHeadGroup) {
148        StringBuilder lipidString = new StringBuilder();
149        lipidString.append(Optional.ofNullable(this.headGroup.getLipidClass()).map((lclass) -> {
150            switch (lclass) {
151//                case SE:
152                case SE_27_1:
153                case SE_27_2:
154                case SE_28_2:
155                case SE_28_3:
156                case SE_29_2:
157                case SE_30_2:
158                    return getNormalizedHeadGroup() + "/"; // use this for disambiguation to avoid SE 16:1 to be similar to SE 43:2 because of expansion to SE 27:1/16:1
159            }
160            return headGroup + " ";
161        }).orElse(headGroup + " "));
162        return lipidString;
163    }
164
165    protected String buildLipidString(LipidLevel level, String headGroup, boolean isNormalized) throws ConstraintViolationException {
166        switch (level) {
167            case CATEGORY:
168                return this.headGroup.getLipidCategory().name();
169            case CLASS:
170                return this.headGroup.getLipidClass().name();
171            case SPECIES:
172                StringBuilder lipidString = new StringBuilder();
173                lipidString.append(buildSpeciesHeadGroupString(headGroup, isNormalized));
174                LipidSpeciesInfo info = this.info;
175                if (info.getNCarbon() > 0) {
176                    int nCarbon = info.getNCarbon();
177                    String hgToFaSep = "";
178                    if (isEtherLipid()) {
179                        hgToFaSep = "O-";
180                    }
181                    lipidString.append(hgToFaSep).append(nCarbon);
182                    int nDB = info.getNDoubleBonds();
183                    lipidString.append(":").append(nDB);
184                    int nHydroxy = info.getNHydroxy();
185                    lipidString.append(nHydroxy > 0 ? ";" + nHydroxy : "");
186                    lipidString.append(info.getLipidFaBondType().suffix());
187                    //TODO reenable once LSI has finished modification specification
188//                    if (!info.getModifications().isEmpty()) {
189//                        lipidString.append("(");
190//                        lipidString.append(info.getModifications().stream().map((t) -> {
191//                            return (t.getLeft() == -1 ? "" : t.getLeft()) + "" + t.getRight();
192//                        }).collect(Collectors.joining(",")));
193//                        lipidString.append(")");
194//                    }
195                }
196                return lipidString.toString().trim();
197            case UNDEFINED:
198                return this.headGroup.getName();
199            default:
200                LipidLevel thisLevel = getInfo().getLevel();
201                throw new ConstraintViolationException(getClass().getSimpleName() + " can not create a string for lipid with level " + thisLevel + " for level " + level + ": target level is more specific than this lipid's level!");
202        }
203    }
204
205    /**
206     * Returns a lipid string representation for the head group of this lipid.
207     * This method normalizes the original head group name to the class specific
208     * primary alias, if the level and class are known. E.g. TG is normalized to
209     * TAG.
210     *
211     * @return the normalized lipid head group.
212     */
213    public String getNormalizedHeadGroup() {
214        return headGroup.getNormalizedName();
215    }
216
217    /**
218     * Returns a lipid string representation for the native {@link LipidLevel},
219     * e.g. Category, Species, etc, as returned by {@link #getInfo()} of this
220     * lipid. This method normalizes the head group to the primary
221     * class-specific synonym. E.g. TG would be normalized to TAG.
222     *
223     * @return the normalized lipid name.
224     */
225    public String getNormalizedLipidString() {
226        return getLipidString(getInfo().getLevel(), true);
227    }
228
229    /**
230     * Validate this lipid against the class-specific available FA types and
231     * slots.
232     *
233     * @return true if this lipid's FA types and their number match the class
234     * definition, false otherwise.
235     */
236    public boolean validate() {
237        return true;
238    }
239
240    /**
241     * Returns the fatty acyls registered for this lipid.
242     *
243     * @return the fatty acyls.
244     */
245    public Map<String, FattyAcid> getFa() {
246        return Collections.emptyMap();
247    }
248
249    /**
250     * Returns the element count table for this lipid.
251     *
252     * @return the element count table.
253     */
254    public ElementTable getElements() {
255        ElementTable elements = new ElementTable();
256        switch (info.getLevel()) {
257            case CATEGORY:
258            case CLASS:
259            case UNDEFINED:
260                return elements;
261        }
262
263        Optional.ofNullable(headGroup.getLipidClass()).ifPresent((lclass) -> {
264            elements.add(lclass.getElements());
265        });
266
267        switch (info.getLevel()) {
268            case MOLECULAR_SUBSPECIES:
269            case STRUCTURAL_SUBSPECIES:
270            case ISOMERIC_SUBSPECIES:
271                int nTrueFa = 0;
272                for (FattyAcid fa : getFa().values()) {
273                    ElementTable faElements = fa.getElements();
274                    if (fa.getNCarbon() != 0 || fa.getNDoubleBonds() != 0) {
275                        nTrueFa += 1;
276                    }
277                    elements.add(faElements);
278                }
279                if (headGroup.getLipidClass().getMaxNumFa() < nTrueFa) {
280                    throw new ConstraintViolationException("Inconsistency in number of fatty acyl chains for lipid '" + headGroup.getName() + "'. Expected at most: " + headGroup.getLipidClass().getMaxNumFa() + "; received: " + nTrueFa);
281                }
282                elements.incrementBy(Element.ELEMENT_H, headGroup.getLipidClass().getMaxNumFa() - nTrueFa); // adding hydrogens for absent fatty acyl chains
283                break;
284            case SPECIES:
285                int maxNumFa = 0;
286                LipidClass lclass = headGroup.getLipidClass();
287                maxNumFa = lclass.getMaxNumFa();
288
289                int maxPossNumFa = headGroup.getLipidClass().getAllowedNumFa().stream().max(Integer::compareTo).orElse(0);
290                ElementTable faElements = info.getElements(maxPossNumFa);
291                elements.add(faElements);
292                elements.incrementBy(ELEMENT_H, maxNumFa - maxPossNumFa); // adding hydrogens for absent fatty acyl chains
293                break;
294            default:
295                break;
296        }
297
298        return elements;
299    }
300
301    public LipidClass getLipidClass() {
302        return headGroup.getLipidClass();
303    }
304
305    public LipidCategory getLipidCategory() {
306        return headGroup.getLipidCategory();
307    }
308
309    @Override
310    public String toString() {
311        return getLipidString(info.getLevel());
312    }
313
314}