From 89a951e687d5fe0476858a67e7dd9a342541df19 Mon Sep 17 00:00:00 2001 From: AlexanderMitrofanov Date: Wed, 21 Oct 2026 17:04:46 +0200 Subject: [PATCH 1/2] fix: mark inherited IntaRNA virtual methods as override --- src/IntaRNA/AccessibilityBasePair.h | 4 +- src/IntaRNA/AccessibilityDisabled.h | 4 +- src/IntaRNA/AccessibilityFromStream.h | 6 +- src/IntaRNA/AccessibilityVrna.h | 4 +- src/IntaRNA/HelixHandlerIdxOffset.h | 28 ++++---- src/IntaRNA/HelixHandlerNoBulgeMax.h | 28 ++++---- src/IntaRNA/HelixHandlerUnpaired.h | 28 ++++---- src/IntaRNA/InteractionEnergyBasePair.h | 30 ++++----- src/IntaRNA/InteractionEnergyIdxOffset.h | 64 +++++++++---------- src/IntaRNA/InteractionEnergyVrna.h | 30 ++++----- src/IntaRNA/OutputHandlerCsv.h | 4 +- src/IntaRNA/OutputHandlerEnsemble.h | 4 +- src/IntaRNA/OutputHandlerHub.h | 10 +-- src/IntaRNA/OutputHandlerInteractionList.h | 4 +- src/IntaRNA/OutputHandlerText.h | 4 +- src/IntaRNA/OutputStreamHandlerSortedCsv.h | 4 +- src/IntaRNA/PredictionTrackerHub.h | 4 +- src/IntaRNA/PredictionTrackerPairMinE.h | 4 +- src/IntaRNA/PredictionTrackerProfileMinE.h | 4 +- .../PredictionTrackerProfileSpotProb.h | 4 +- src/IntaRNA/PredictionTrackerSpotProb.h | 4 +- src/IntaRNA/PredictionTrackerSpotProbAll.h | 4 +- src/IntaRNA/PredictorMfe.h | 8 +-- src/IntaRNA/PredictorMfe2d.h | 6 +- .../PredictorMfe2dHelixBlockHeuristic.h | 12 ++-- .../PredictorMfe2dHelixBlockHeuristicSeed.h | 8 +-- src/IntaRNA/PredictorMfe2dHeuristic.h | 10 +-- src/IntaRNA/PredictorMfe2dHeuristicSeed.h | 10 +-- .../PredictorMfe2dHeuristicSeedExtension.h | 4 +- src/IntaRNA/PredictorMfe2dSeed.h | 8 +-- src/IntaRNA/PredictorMfe2dSeedExtension.h | 6 +- .../PredictorMfe2dSeedExtensionRIblast.h | 6 +- src/IntaRNA/PredictorMfeEns.h | 6 +- src/IntaRNA/PredictorMfeEns2d.h | 4 +- src/IntaRNA/PredictorMfeEns2dHeuristic.h | 4 +- .../PredictorMfeEns2dHeuristicSeedExtension.h | 6 +- src/IntaRNA/PredictorMfeEns2dSeedExtension.h | 6 +- src/IntaRNA/PredictorMfeEnsSeedOnly.h | 6 +- src/IntaRNA/PredictorMfeSeedOnly.h | 6 +- src/IntaRNA/PredictorSeedExtensionKinetic.h | 2 +- src/IntaRNA/ReverseAccessibility.h | 8 +-- src/IntaRNA/SeedHandlerExplicit.h | 16 ++--- src/IntaRNA/SeedHandlerIdxOffset.h | 26 ++++---- src/IntaRNA/SeedHandlerMfe.h | 14 ++-- src/IntaRNA/SeedHandlerNoBulge.h | 16 ++--- 45 files changed, 239 insertions(+), 239 deletions(-) diff --git a/src/IntaRNA/AccessibilityBasePair.h b/src/IntaRNA/AccessibilityBasePair.h index 54cf9471..f8866c0d 100644 --- a/src/IntaRNA/AccessibilityBasePair.h +++ b/src/IntaRNA/AccessibilityBasePair.h @@ -47,7 +47,7 @@ class AccessibilityBasePair: public Accessibility { /*** * Destructor of AccessibilityBasePair */ - virtual ~AccessibilityBasePair(); + virtual ~AccessibilityBasePair() override; /*** * Returns the accessibility energy value between the indices (from, to) @@ -59,7 +59,7 @@ class AccessibilityBasePair: public Accessibility { * * @throw std::runtime_error in case it does not hold 0 <= from <= to < seq.length */ - virtual E_type getED( const size_t from, const size_t to ) const; + virtual E_type getED( const size_t from, const size_t to ) const override; protected: diff --git a/src/IntaRNA/AccessibilityDisabled.h b/src/IntaRNA/AccessibilityDisabled.h index f50ba243..d72f9215 100644 --- a/src/IntaRNA/AccessibilityDisabled.h +++ b/src/IntaRNA/AccessibilityDisabled.h @@ -38,7 +38,7 @@ class AccessibilityDisabled: public Accessibility { /** * destruction */ - virtual ~AccessibilityDisabled(); + virtual ~AccessibilityDisabled() override; /** * Always returns a zero accessibility energy value. @@ -50,7 +50,7 @@ class AccessibilityDisabled: public Accessibility { */ virtual E_type - getED( const size_t from, const size_t to ) const; + getED( const size_t from, const size_t to ) const override; }; diff --git a/src/IntaRNA/AccessibilityFromStream.h b/src/IntaRNA/AccessibilityFromStream.h index 79493c0e..a3169214 100644 --- a/src/IntaRNA/AccessibilityFromStream.h +++ b/src/IntaRNA/AccessibilityFromStream.h @@ -52,7 +52,7 @@ class AccessibilityFromStream: public Accessibility /** * destruction */ - virtual ~AccessibilityFromStream(); + virtual ~AccessibilityFromStream() override; /** @@ -68,7 +68,7 @@ class AccessibilityFromStream: public Accessibility */ virtual E_type - getED( const size_t from, const size_t to ) const; + getED( const size_t from, const size_t to ) const override; /** * Access to the maximal length of accessible regions (>0) to be considered. @@ -80,7 +80,7 @@ class AccessibilityFromStream: public Accessibility */ virtual size_t - getMaxLength() const; + getMaxLength() const override; /** diff --git a/src/IntaRNA/AccessibilityVrna.h b/src/IntaRNA/AccessibilityVrna.h index 83a03c07..1f4ac3f4 100644 --- a/src/IntaRNA/AccessibilityVrna.h +++ b/src/IntaRNA/AccessibilityVrna.h @@ -57,7 +57,7 @@ class AccessibilityVrna : public Accessibility { /** * destruction */ - virtual ~AccessibilityVrna(); + virtual ~AccessibilityVrna() override; /** * Returns the accessibility energy value for the given range in the @@ -72,7 +72,7 @@ class AccessibilityVrna : public Accessibility { */ virtual E_type - getED( const size_t from, const size_t to ) const; + getED( const size_t from, const size_t to ) const override; /** diff --git a/src/IntaRNA/HelixHandlerIdxOffset.h b/src/IntaRNA/HelixHandlerIdxOffset.h index bd8dfe32..d06153bf 100644 --- a/src/IntaRNA/HelixHandlerIdxOffset.h +++ b/src/IntaRNA/HelixHandlerIdxOffset.h @@ -31,7 +31,7 @@ class HelixHandlerIdxOffset : public HelixHandler /** * destruction */ - virtual ~HelixHandlerIdxOffset(); + virtual ~HelixHandlerIdxOffset() override; /** * Access to the underlying interaction energy function @@ -39,7 +39,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual const InteractionEnergy& - getInteractionEnergy() const; + getInteractionEnergy() const override; /** * Access to the currently used index offset for sequence 1 @@ -73,7 +73,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual const HelixConstraint& - getConstraint() const; + getConstraint() const override; /** * Computes the helix matrix for the given interval boundaries @@ -89,7 +89,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual size_t - fillHelix(const size_t i1, const size_t j1, const size_t i2, const size_t j2); + fillHelix(const size_t i1, const size_t j1, const size_t i2, const size_t j2) override; /** * Computes the helix matrix for the given interval boundaries for helices containing a seed @@ -104,7 +104,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual size_t - fillHelixSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2); + fillHelixSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2) override; /** * Identifies the base pairs of the mfe helix interaction starting at i1,i2 @@ -120,7 +120,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual void - traceBackHelix( Interaction & interaction, const size_t i1, const size_t i2); + traceBackHelix( Interaction & interaction, const size_t i1, const size_t i2) override; /** * Identifies the base pairs of the mfe helix interaction, containing a seed, starting at i1,i2 @@ -134,7 +134,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual void - traceBackHelixSeed( Interaction & interaction, const size_t i1, const size_t i2); + traceBackHelixSeed( Interaction & interaction, const size_t i1, const size_t i2) override; /** * Access to the mfe of any helix with left-most base pair (i1,i2) @@ -147,7 +147,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual E_type - getHelixE( const size_t i1, const size_t i2 ) const; + getHelixE( const size_t i1, const size_t i2 ) const override; /** * Access to the mfe of any helix with left-most base pair (i1,i2) and containing a seed @@ -160,7 +160,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual E_type - getHelixSeedE( const size_t i1, const size_t i2 ) const; + getHelixSeedE( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe helix with left-most base pair (i1,i2) @@ -173,7 +173,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual size_t - getHelixLength1( const size_t i1, const size_t i2 ) const; + getHelixLength1( const size_t i1, const size_t i2 ) const override; /** @@ -187,7 +187,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual size_t - getHelixLength2( const size_t i1, const size_t i2 ) const; + getHelixLength2( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe helix with left-most base pair (i1,i2), containing a seed @@ -200,7 +200,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual size_t - getHelixSeedLength1( const size_t i1, const size_t i2 ) const; + getHelixSeedLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe helix with left-most base pair (i1,i2), containing a seed @@ -213,7 +213,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual size_t - getHelixSeedLength2( const size_t i1, const size_t i2 ) const; + getHelixSeedLength2( const size_t i1, const size_t i2 ) const override; /** * Check whether the given seedHandler is of type SeedHandlerIdxOffset @@ -223,7 +223,7 @@ class HelixHandlerIdxOffset : public HelixHandler */ virtual void - setSeedHandler( SeedHandler & seedHandler); + setSeedHandler( SeedHandler & seedHandler) override; protected: diff --git a/src/IntaRNA/HelixHandlerNoBulgeMax.h b/src/IntaRNA/HelixHandlerNoBulgeMax.h index 485ec935..cfbe5ca3 100644 --- a/src/IntaRNA/HelixHandlerNoBulgeMax.h +++ b/src/IntaRNA/HelixHandlerNoBulgeMax.h @@ -49,7 +49,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { /** * destructor */ - virtual ~HelixHandlerNoBulgeMax(); + virtual ~HelixHandlerNoBulgeMax() override; /** * Access to the underlying helix constraint @@ -57,7 +57,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual const HelixConstraint& - getConstraint() const; + getConstraint() const override; /** * Access to the underlying interaction energy function @@ -65,7 +65,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual const InteractionEnergy& - getInteractionEnergy() const; + getInteractionEnergy() const override; /** @@ -78,7 +78,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual size_t - fillHelix( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ); + fillHelix( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) override; /** * Compute the helix matrix, containing a seed, for the given interval boundaries @@ -90,7 +90,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual size_t - fillHelixSeed( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ); + fillHelixSeed( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) override; /** * Identifies the base pairs of the mfe helix interaction starting at i1,i2 @@ -104,7 +104,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual void - traceBackHelix( Interaction & interaction, const size_t i1, const size_t i2); + traceBackHelix( Interaction & interaction, const size_t i1, const size_t i2) override; /** * Identifies the base pairs of the mfe helix interaction, containing a seed, starting at i1,i2 @@ -116,7 +116,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual void - traceBackHelixSeed( Interaction & interaction, const size_t i1, size_t i2); + traceBackHelixSeed( Interaction & interaction, const size_t i1, size_t i2) override; /** * Access to the mfe of any helix with left-most base pair (i1, i2) @@ -126,7 +126,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { * or return 0 if bp is 0 or 1 (used in helixHandlerSeed computation to avoid too many conditions) */ virtual - E_type getHelixE( const size_t i1, const size_t i2 ) const; + E_type getHelixE( const size_t i1, const size_t i2 ) const override; /** * Access to the mfe of any helix, containing a seed, with left-most base pair (i1, i2) @@ -135,7 +135,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { * @return the mfe of any helix starting at (i1,i2) or E_INF if none possible */ virtual - E_type getHelixSeedE( const size_t i1, const size_t i2 ) const; + E_type getHelixSeedE( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe helix with left-most base pair (i1,i2) @@ -145,7 +145,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual size_t - getHelixLength1( const size_t i1, const size_t i2 ) const; + getHelixLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe helix with left-most base pair (i1,i2) @@ -155,7 +155,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual size_t - getHelixLength2( const size_t i1, const size_t i2 ) const; + getHelixLength2( const size_t i1, const size_t i2 ) const override; /** @@ -166,7 +166,7 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual size_t - getHelixSeedLength1( const size_t i1, const size_t i2 ) const; + getHelixSeedLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe helix, containing a seed, with left-most base pair (i1,i2) @@ -176,13 +176,13 @@ class HelixHandlerNoBulgeMax : public HelixHandler { */ virtual size_t - getHelixSeedLength2( const size_t i1, const size_t i2 ) const; + getHelixSeedLength2( const size_t i1, const size_t i2 ) const override; /** * Set the seedHandler in order to compute helixSeed * @param seedHandler seedHandler to be used in the helix computation */ - void setSeedHandler(SeedHandler & seedHandler); + void setSeedHandler(SeedHandler & seedHandler) override; protected: diff --git a/src/IntaRNA/HelixHandlerUnpaired.h b/src/IntaRNA/HelixHandlerUnpaired.h index 0be76450..dddafa95 100644 --- a/src/IntaRNA/HelixHandlerUnpaired.h +++ b/src/IntaRNA/HelixHandlerUnpaired.h @@ -58,7 +58,7 @@ class HelixHandlerUnpaired : public HelixHandler { /** * destructor */ - virtual ~HelixHandlerUnpaired(); + virtual ~HelixHandlerUnpaired() override; /** * Access to the underlying helix constraint @@ -66,7 +66,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual const HelixConstraint& - getConstraint() const; + getConstraint() const override; /** * Access to the underlying interaction energy function @@ -74,7 +74,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual const InteractionEnergy& - getInteractionEnergy() const; + getInteractionEnergy() const override; /** * Compute the helix matrix for the given interval boundaries @@ -86,7 +86,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual size_t - fillHelix( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ); + fillHelix( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) override; /** * Computes the helix information for the given interval boundaries for helices containing a seed @@ -98,7 +98,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual size_t - fillHelixSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2); + fillHelixSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2) override; /** * Identifies the base pairs of the mfe helix interaction starting at i1,i2 @@ -112,7 +112,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual void - traceBackHelix( Interaction & interaction, const size_t i1, const size_t i2); + traceBackHelix( Interaction & interaction, const size_t i1, const size_t i2) override; /** * Identifies the base pairs of the mfe helix interaction starting at i1,i2 @@ -126,7 +126,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual void - traceBackHelixSeed( Interaction & interaction, const size_t i1, const size_t i2); + traceBackHelixSeed( Interaction & interaction, const size_t i1, const size_t i2) override; /** * Access to the mfe of any helix with left-most base pair (i1, i2) @@ -136,7 +136,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual E_type - getHelixE( const size_t i1, const size_t i2 ) const; + getHelixE( const size_t i1, const size_t i2 ) const override; /** * Access to the mfe of any helix with left-most base pair (i1,i2) @@ -146,7 +146,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual E_type - getHelixSeedE( const size_t i1, const size_t i2 ) const; + getHelixSeedE( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe helix with left-most base pair (i1,i2) @@ -156,7 +156,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual size_t - getHelixLength1( const size_t i1, const size_t i2 ) const; + getHelixLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe helix with left-most base pair (i1,i2) @@ -166,7 +166,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual size_t - getHelixLength2( const size_t i1, const size_t i2 ) const; + getHelixLength2( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe helix with left-most base pair (i1,i2) @@ -198,7 +198,7 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual size_t - getHelixSeedLength1( const size_t i1, const size_t i2 ) const; + getHelixSeedLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe helix with left-most base pair (i1,i2) containing a seed @@ -208,13 +208,13 @@ class HelixHandlerUnpaired : public HelixHandler { */ virtual size_t - getHelixSeedLength2( const size_t i1, const size_t i2 ) const; + getHelixSeedLength2( const size_t i1, const size_t i2 ) const override; /** * Set the seedHandler in order to compute helixSeed * @param seedHandler seedHandler to be used in the helix computation */ - void setSeedHandler(SeedHandler & seedHandler); + void setSeedHandler(SeedHandler & seedHandler) override; protected: diff --git a/src/IntaRNA/InteractionEnergyBasePair.h b/src/IntaRNA/InteractionEnergyBasePair.h index 27804aeb..1d39ac36 100644 --- a/src/IntaRNA/InteractionEnergyBasePair.h +++ b/src/IntaRNA/InteractionEnergyBasePair.h @@ -61,7 +61,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { , const bool internalLoopGU = true ); - virtual ~InteractionEnergyBasePair(); + virtual ~InteractionEnergyBasePair() override; /** @@ -79,7 +79,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getES1( const size_t i1, const size_t j1 ) const; + getES1( const size_t i1, const size_t j1 ) const override; /** * Provides the ensemble energy (ES) of all intramolecular substructures @@ -96,7 +96,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getES2( const size_t i2, const size_t j2 ) const; + getES2( const size_t i2, const size_t j2 ) const override; /** * Provides the energy contribution for a given number of unpaired @@ -109,7 +109,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_multiUnpaired( const size_t numUnpaired ) const; + getE_multiUnpaired( const size_t numUnpaired ) const override; /** @@ -125,7 +125,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_multiHelix( const size_t j1, const size_t j2 ) const; + getE_multiHelix( const size_t j1, const size_t j2 ) const override; /** * Provides the energy contribution/penalty for closing an intermolecular @@ -135,7 +135,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_multiClosing() const; + getE_multiClosing() const override; /** @@ -145,7 +145,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_init() const; + getE_init() const override; /** @@ -165,7 +165,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_interLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const; + getE_interLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const override; /** * Computes the dangling end energy penalty estimate for the left side of @@ -181,7 +181,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_danglingLeft( const size_t i1, const size_t i2 ) const; + getE_danglingLeft( const size_t i1, const size_t i2 ) const override; /** @@ -196,7 +196,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_danglingRight( const size_t j1, const size_t j2 ) const; + getE_danglingRight( const size_t j1, const size_t j2 ) const override; /** * Provides the penalty for closing an interaction with the given @@ -209,7 +209,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_endLeft( const size_t i1, const size_t i2 ) const; + getE_endLeft( const size_t i1, const size_t i2 ) const override; /** * Provides the penalty for closing an interaction with the given @@ -222,7 +222,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getE_endRight( const size_t j1, const size_t j2 ) const; + getE_endRight( const size_t j1, const size_t j2 ) const override; /** * Returns always RT=1 due to the lack of reasonable values for this energy @@ -231,7 +231,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual Z_type - getRT() const; + getRT() const override; /** * Provides the (constant) energy contribution of a base pair (sequence independent) @@ -249,7 +249,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getEall1() const; + getEall1() const override; /** * Provides the overall ensemble energy for sequence 2 @@ -258,7 +258,7 @@ class InteractionEnergyBasePair: public InteractionEnergy { */ virtual E_type - getEall2() const; + getEall2() const override; private: diff --git a/src/IntaRNA/InteractionEnergyIdxOffset.h b/src/IntaRNA/InteractionEnergyIdxOffset.h index 1d0c7b69..f43e016e 100644 --- a/src/IntaRNA/InteractionEnergyIdxOffset.h +++ b/src/IntaRNA/InteractionEnergyIdxOffset.h @@ -30,7 +30,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy , const size_t offset2 = 0 ); - virtual ~InteractionEnergyIdxOffset(); + virtual ~InteractionEnergyIdxOffset() override; /** * Access to the currently used index offset for sequence 1 @@ -77,7 +77,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy E_type getE( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 - , const E_type hybridE ) const; + , const E_type hybridE ) const override; /** * Provides the ensemble energy for a given partition function Z. @@ -88,7 +88,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE( const Z_type Z ) const; + getE( const Z_type Z ) const override; /** * Checks whether or not two positions (shifted by offset) can form a base pair @@ -98,7 +98,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual bool - areComplementary( const size_t i1, const size_t i2 ) const; + areComplementary( const size_t i1, const size_t i2 ) const override; /** * Checks whether or not two positions can form a GU base pair @@ -108,7 +108,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual bool - isGU( const size_t i1, const size_t i2 ) const; + isGU( const size_t i1, const size_t i2 ) const override; /** * Length of sequence 1 excluding the index offset @@ -116,7 +116,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual size_t - size1() const; + size1() const override; /** * Length of sequence 2 excluding index offset @@ -124,7 +124,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual size_t - size2() const; + size2() const override; /** * Provides the ED penalty for making a region with sequence 1 accessible @@ -137,7 +137,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getED1( const size_t i1, const size_t j1 ) const; + getED1( const size_t i1, const size_t j1 ) const override; /** * Provides the ED penalty for making a region with (the reversed) @@ -151,7 +151,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getED2( const size_t i2, const size_t j2 ) const; + getED2( const size_t i2, const size_t j2 ) const override; /** * Whether or not position i is accessible for interaction in sequence 1 @@ -163,7 +163,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual bool - isAccessible1( const size_t i ) const; + isAccessible1( const size_t i ) const override; /** * Whether or not position i is accessible for interaction in sequence 2 @@ -175,7 +175,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual bool - isAccessible2( const size_t i ) const; + isAccessible2( const size_t i ) const override; /** * Provides the energy contribution of an interaction site gap, i.e. the @@ -197,7 +197,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy E_type getE_multi( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 - , const ES_multi_mode ES_mode ) const; + , const ES_multi_mode ES_mode ) const override; /** * Provides the ensemble energy (ES) of all intramolecular substructures @@ -214,7 +214,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getES1( const size_t i1, const size_t j1 ) const; + getES1( const size_t i1, const size_t j1 ) const override; /** * Provides the ensemble energy (ES) of all intramolecular substructures @@ -231,7 +231,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getES2( const size_t i2, const size_t j2 ) const; + getES2( const size_t i2, const size_t j2 ) const override; /** * Provides the energy contribution for a given number of unpaired @@ -244,7 +244,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_multiUnpaired( const size_t numUnpaired ) const; + getE_multiUnpaired( const size_t numUnpaired ) const override; /** @@ -262,7 +262,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_multiHelix( const size_t j1, const size_t j2 ) const; + getE_multiHelix( const size_t j1, const size_t j2 ) const override; /** * Provides the energy contribution/penalty for closing an intermolecular @@ -273,7 +273,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_multiClosing() const; + getE_multiClosing() const override; /** * Provides the duplex initiation energy. @@ -282,7 +282,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_init( ) const; + getE_init( ) const override; /** @@ -306,7 +306,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_interLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const; + getE_interLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const override; /** @@ -323,7 +323,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_danglingLeft( const size_t i1, const size_t i2 ) const; + getE_danglingLeft( const size_t i1, const size_t i2 ) const override; /** @@ -340,7 +340,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_danglingRight( const size_t j1, const size_t j2 ) const; + getE_danglingRight( const size_t j1, const size_t j2 ) const override; /** * Provides the penalty for closing an interaction with the given @@ -355,7 +355,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_endLeft( const size_t i1, const size_t i2 ) const; + getE_endLeft( const size_t i1, const size_t i2 ) const override; /** * Provides the penalty for closing an interaction with the given @@ -370,7 +370,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getE_endRight( const size_t j1, const size_t j2 ) const; + getE_endRight( const size_t j1, const size_t j2 ) const override; /** * Computes the probability of the dangling ends for the left side @@ -388,7 +388,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual Z_type - getPr_danglingLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const; + getPr_danglingLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const override; /** * Computes the probability of the dangling ends for the right side @@ -406,7 +406,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual Z_type - getPr_danglingRight( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const; + getPr_danglingRight( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const override; @@ -415,7 +415,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual Z_type - getRT() const; + getRT() const override; /** * Provides the base pair encoding for the given indices after shifting by @@ -426,7 +426,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual Interaction::BasePair - getBasePair( const size_t i1, const size_t i2 ) const; + getBasePair( const size_t i1, const size_t i2 ) const override; /** @@ -436,7 +436,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual size_t - getIndex1( const Interaction::BasePair & bp ) const; + getIndex1( const Interaction::BasePair & bp ) const override; /** @@ -446,7 +446,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual size_t - getIndex2( const Interaction::BasePair & bp ) const; + getIndex2( const Interaction::BasePair & bp ) const override; /** @@ -467,7 +467,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual bool - isValidInternalLoop( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const; + isValidInternalLoop( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const override; /** * Provides the overall ensemble energy for sequence 1 @@ -476,7 +476,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getEall1() const; + getEall1() const override; /** * Provides the overall ensemble energy for sequence 2 @@ -485,7 +485,7 @@ class InteractionEnergyIdxOffset : public InteractionEnergy */ virtual E_type - getEall2() const; + getEall2() const override; protected: diff --git a/src/IntaRNA/InteractionEnergyVrna.h b/src/IntaRNA/InteractionEnergyVrna.h index 5374f1fb..4b17653b 100644 --- a/src/IntaRNA/InteractionEnergyVrna.h +++ b/src/IntaRNA/InteractionEnergyVrna.h @@ -75,7 +75,7 @@ class InteractionEnergyVrna: public InteractionEnergy { , const bool internalLoopGU = true ); - virtual ~InteractionEnergyVrna(); + virtual ~InteractionEnergyVrna() override; /** @@ -93,7 +93,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getES1( const size_t i1, const size_t j1 ) const; + getES1( const size_t i1, const size_t j1 ) const override; /** * Provides the ensemble energy (ES) of all intramolecular substructures @@ -110,7 +110,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getES2( const size_t i2, const size_t j2 ) const; + getES2( const size_t i2, const size_t j2 ) const override; /** * Provides the energy contribution for a given number of unpaired @@ -123,7 +123,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_multiUnpaired( const size_t numUnpaired ) const; + getE_multiUnpaired( const size_t numUnpaired ) const override; /** * Provides the energy contribution/penalty of the helix repesented by the @@ -139,7 +139,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_multiHelix( const size_t j1, const size_t j2 ) const; + getE_multiHelix( const size_t j1, const size_t j2 ) const override; /** * Provides the energy contribution/penalty for closing an intermolecular @@ -150,7 +150,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_multiClosing() const; + getE_multiClosing() const override; /** * Provides the duplex initiation energy. @@ -159,7 +159,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_init() const; + getE_init() const override; /** * Computes the energy estimate for the interaction loop region closed by @@ -180,7 +180,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_interLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const; + getE_interLeft( const size_t i1, const size_t j1, const size_t i2, const size_t j2 ) const override; /** @@ -195,7 +195,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_danglingLeft( const size_t i1, const size_t i2 ) const; + getE_danglingLeft( const size_t i1, const size_t i2 ) const override; /** @@ -210,7 +210,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_danglingRight( const size_t j1, const size_t j2 ) const; + getE_danglingRight( const size_t j1, const size_t j2 ) const override; /** @@ -224,7 +224,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_endLeft( const size_t i1, const size_t i2 ) const; + getE_endLeft( const size_t i1, const size_t i2 ) const override; /** * Provides the penalty for closing an interaction with the given @@ -237,7 +237,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getE_endRight( const size_t j1, const size_t j2 ) const; + getE_endRight( const size_t j1, const size_t j2 ) const override; /** * Provides the overall ensemble energy for sequence 1 @@ -246,7 +246,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getEall1() const; + getEall1() const override; /** * Provides the overall ensemble energy for sequence 2 @@ -255,7 +255,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual E_type - getEall2() const; + getEall2() const override; /** * Provides the overall partition function for sequence 2 @@ -264,7 +264,7 @@ class InteractionEnergyVrna: public InteractionEnergy { */ virtual Z_type - getRT() const; + getRT() const override; protected: diff --git a/src/IntaRNA/OutputHandlerCsv.h b/src/IntaRNA/OutputHandlerCsv.h index 7c707ec6..4a5e5480 100644 --- a/src/IntaRNA/OutputHandlerCsv.h +++ b/src/IntaRNA/OutputHandlerCsv.h @@ -202,7 +202,7 @@ class OutputHandlerCsv : public OutputHandler /** * destruction */ - virtual ~OutputHandlerCsv(); + virtual ~OutputHandlerCsv() override; /** * Write a given RNA-RNA interaction in simple text format to the output @@ -212,7 +212,7 @@ class OutputHandlerCsv : public OutputHandler */ virtual void - add( const Interaction & interaction ); + add( const Interaction & interaction ) override; /** * Converts a list of Coltypes to their string representation. diff --git a/src/IntaRNA/OutputHandlerEnsemble.h b/src/IntaRNA/OutputHandlerEnsemble.h index e19b408b..9e2391bf 100644 --- a/src/IntaRNA/OutputHandlerEnsemble.h +++ b/src/IntaRNA/OutputHandlerEnsemble.h @@ -37,7 +37,7 @@ class OutputHandlerEnsemble: public OutputHandler { * - prints ensemble information to stream and * - enforces a flush on the output stream. */ - virtual ~OutputHandlerEnsemble(); + virtual ~OutputHandlerEnsemble() override; /** * Does nothing but counts the call @@ -47,7 +47,7 @@ class OutputHandlerEnsemble: public OutputHandler { */ virtual void - add( const Interaction & interaction ); + add( const Interaction & interaction ) override; protected: diff --git a/src/IntaRNA/OutputHandlerHub.h b/src/IntaRNA/OutputHandlerHub.h index ac5e4cb2..9af6bb81 100644 --- a/src/IntaRNA/OutputHandlerHub.h +++ b/src/IntaRNA/OutputHandlerHub.h @@ -50,7 +50,7 @@ class OutputHandlerHub: public OutputHandler /** * destruction */ - virtual ~OutputHandlerHub(); + virtual ~OutputHandlerHub() override; /** @@ -61,7 +61,7 @@ class OutputHandlerHub: public OutputHandler */ virtual void - add( const Interaction & interaction ); + add( const Interaction & interaction ) override; /** * Returns the maximal number of reported interactions among all handlers @@ -70,7 +70,7 @@ class OutputHandlerHub: public OutputHandler */ virtual size_t - reported() const; + reported() const override; /** * Increments the overall partition function with the given value. @@ -83,7 +83,7 @@ class OutputHandlerHub: public OutputHandler */ virtual void - incrementZ( const Z_type subZ ); + incrementZ( const Z_type subZ ) override; /** * Access to the partition function (initialized with 0) aggregated via @@ -93,7 +93,7 @@ class OutputHandlerHub: public OutputHandler */ virtual Z_type - getZ() const; + getZ() const override; /** * Adds a new OutputHandler to the forwarding list. diff --git a/src/IntaRNA/OutputHandlerInteractionList.h b/src/IntaRNA/OutputHandlerInteractionList.h index abfbd905..c2735168 100644 --- a/src/IntaRNA/OutputHandlerInteractionList.h +++ b/src/IntaRNA/OutputHandlerInteractionList.h @@ -41,7 +41,7 @@ class OutputHandlerInteractionList: public OutputHandler , const size_t maxToStore); //! destruction - virtual ~OutputHandlerInteractionList(); + virtual ~OutputHandlerInteractionList() override; /** * Adds a given RNA-RNA interaction to the storage/output. @@ -50,7 +50,7 @@ class OutputHandlerInteractionList: public OutputHandler */ virtual void - add( const Interaction & interaction ); + add( const Interaction & interaction ) override; //! whether or not the container is empty //! @return true if no elements are stored diff --git a/src/IntaRNA/OutputHandlerText.h b/src/IntaRNA/OutputHandlerText.h index d95fb331..253cd33b 100644 --- a/src/IntaRNA/OutputHandlerText.h +++ b/src/IntaRNA/OutputHandlerText.h @@ -41,7 +41,7 @@ class OutputHandlerText: public OutputHandler { /** * destruction, enforces a flush on the output stream. */ - virtual ~OutputHandlerText(); + virtual ~OutputHandlerText() override; /** * Write a given RNA-RNA interaction in simple text format to the output @@ -51,7 +51,7 @@ class OutputHandlerText: public OutputHandler { */ virtual void - add( const Interaction & interaction ); + add( const Interaction & interaction ) override; protected: diff --git a/src/IntaRNA/OutputStreamHandlerSortedCsv.h b/src/IntaRNA/OutputStreamHandlerSortedCsv.h index d0108983..6d16683b 100644 --- a/src/IntaRNA/OutputStreamHandlerSortedCsv.h +++ b/src/IntaRNA/OutputStreamHandlerSortedCsv.h @@ -46,14 +46,14 @@ class OutputStreamHandlerSortedCsv : public OutputStreamHandler * - calls flush() on the final output * - destruction of the underlying OutputStreamHandler */ - virtual ~OutputStreamHandlerSortedCsv(); + virtual ~OutputStreamHandlerSortedCsv() override; /** * Access to the ostream to which unsorted CSV output is to be written. * @return the output stream to write unsorted CSV output to */ virtual - std::ostream& getOutStream(); + std::ostream& getOutStream() override; protected: diff --git a/src/IntaRNA/PredictionTrackerHub.h b/src/IntaRNA/PredictionTrackerHub.h index 2718f92b..6e7e4297 100644 --- a/src/IntaRNA/PredictionTrackerHub.h +++ b/src/IntaRNA/PredictionTrackerHub.h @@ -44,7 +44,7 @@ class PredictionTrackerHub: public PredictionTracker /** * destruction */ - virtual ~PredictionTrackerHub(); + virtual ~PredictionTrackerHub() override; /** * Informs all registered trackers that Predictor.updateOptima() was called @@ -61,7 +61,7 @@ class PredictionTrackerHub: public PredictionTracker updateOptimumCalled( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 , const E_type energy - ); + ) override; /** * Adds a new PredictionTracker to the forwarding list. diff --git a/src/IntaRNA/PredictionTrackerPairMinE.h b/src/IntaRNA/PredictionTrackerPairMinE.h index 06bd9c2f..cf9fc55b 100644 --- a/src/IntaRNA/PredictionTrackerPairMinE.h +++ b/src/IntaRNA/PredictionTrackerPairMinE.h @@ -69,7 +69,7 @@ class PredictionTrackerPairMinE: public PredictionTracker /** * destruction: write the pair information to the according stream. */ - virtual ~PredictionTrackerPairMinE(); + virtual ~PredictionTrackerPairMinE() override; /** @@ -86,7 +86,7 @@ class PredictionTrackerPairMinE: public PredictionTracker updateOptimumCalled( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 , const E_type energy - ); + ) override; protected: diff --git a/src/IntaRNA/PredictionTrackerProfileMinE.h b/src/IntaRNA/PredictionTrackerProfileMinE.h index f8747456..f36c1067 100644 --- a/src/IntaRNA/PredictionTrackerProfileMinE.h +++ b/src/IntaRNA/PredictionTrackerProfileMinE.h @@ -71,7 +71,7 @@ class PredictionTrackerProfileMinE: public PredictionTracker /** * destruction: write the profile(s) to the according streams. */ - virtual ~PredictionTrackerProfileMinE(); + virtual ~PredictionTrackerProfileMinE() override; /** @@ -88,7 +88,7 @@ class PredictionTrackerProfileMinE: public PredictionTracker updateOptimumCalled( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 , const E_type energy - ); + ) override; protected: diff --git a/src/IntaRNA/PredictionTrackerProfileSpotProb.h b/src/IntaRNA/PredictionTrackerProfileSpotProb.h index 0a127fb6..632b2853 100644 --- a/src/IntaRNA/PredictionTrackerProfileSpotProb.h +++ b/src/IntaRNA/PredictionTrackerProfileSpotProb.h @@ -75,7 +75,7 @@ class PredictionTrackerProfileSpotProb: public PredictionTracker /** * destruction: write the profile(s) to the according streams. */ - virtual ~PredictionTrackerProfileSpotProb(); + virtual ~PredictionTrackerProfileSpotProb() override; /** @@ -92,7 +92,7 @@ class PredictionTrackerProfileSpotProb: public PredictionTracker updateOptimumCalled( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 , const E_type energy - ); + ) override; protected: diff --git a/src/IntaRNA/PredictionTrackerSpotProb.h b/src/IntaRNA/PredictionTrackerSpotProb.h index a6e9afc4..01bdb05e 100644 --- a/src/IntaRNA/PredictionTrackerSpotProb.h +++ b/src/IntaRNA/PredictionTrackerSpotProb.h @@ -71,7 +71,7 @@ class PredictionTrackerSpotProb: public PredictionTracker /** * destruction: write the probabilities to stream. */ - virtual ~PredictionTrackerSpotProb(); + virtual ~PredictionTrackerSpotProb() override; /** @@ -88,7 +88,7 @@ class PredictionTrackerSpotProb: public PredictionTracker updateOptimumCalled( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 , const E_type energy - ); + ) override; protected: diff --git a/src/IntaRNA/PredictionTrackerSpotProbAll.h b/src/IntaRNA/PredictionTrackerSpotProbAll.h index 906b49ba..0d6cffc0 100644 --- a/src/IntaRNA/PredictionTrackerSpotProbAll.h +++ b/src/IntaRNA/PredictionTrackerSpotProbAll.h @@ -69,7 +69,7 @@ class PredictionTrackerSpotProbAll: public PredictionTracker /** * destruction: write the profile(s) to the according streams. */ - virtual ~PredictionTrackerSpotProbAll(); + virtual ~PredictionTrackerSpotProbAll() override; /** @@ -87,7 +87,7 @@ class PredictionTrackerSpotProbAll: public PredictionTracker updateOptimumCalled( const size_t i1, const size_t j1 , const size_t i2, const size_t j2 , const E_type energy - ); + ) override; protected: diff --git a/src/IntaRNA/PredictorMfe.h b/src/IntaRNA/PredictorMfe.h index 35501d33..dd7fa7ea 100644 --- a/src/IntaRNA/PredictorMfe.h +++ b/src/IntaRNA/PredictorMfe.h @@ -75,7 +75,7 @@ class PredictorMfe : public Predictor { , OutputHandler & output , PredictionTracker * predTracker ); - virtual ~PredictorMfe(); + virtual ~PredictorMfe() override; protected: @@ -112,7 +112,7 @@ class PredictorMfe : public Predictor { */ virtual void - initOptima(); + initOptima() override; /** * Checks terminal GU and accessibility constraints for a complete site. @@ -145,7 +145,7 @@ class PredictorMfe : public Predictor { , const size_t i2, const size_t j2 , const E_type energy , const bool isHybridE - , const bool incrementZall ); + , const bool incrementZall ) override; /** @@ -199,7 +199,7 @@ class PredictorMfe : public Predictor { */ virtual void - reportOptima(); + reportOptima() override; }; diff --git a/src/IntaRNA/PredictorMfe2d.h b/src/IntaRNA/PredictorMfe2d.h index 2e060f49..de3d62d5 100644 --- a/src/IntaRNA/PredictorMfe2d.h +++ b/src/IntaRNA/PredictorMfe2d.h @@ -38,7 +38,7 @@ class PredictorMfe2d: public PredictorMfe { , OutputHandler & output , PredictionTracker * predTracker ); - virtual ~PredictorMfe2d(); + virtual ~PredictorMfe2d() override; /** * Computes the mfe for the given sequence ranges (i1-j1) in the first @@ -53,7 +53,7 @@ class PredictorMfe2d: public PredictorMfe { void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) - ); + ) override; protected: @@ -94,7 +94,7 @@ class PredictorMfe2d: public PredictorMfe { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; }; diff --git a/src/IntaRNA/PredictorMfe2dHelixBlockHeuristic.h b/src/IntaRNA/PredictorMfe2dHelixBlockHeuristic.h index 2370901c..7fb1e18e 100644 --- a/src/IntaRNA/PredictorMfe2dHelixBlockHeuristic.h +++ b/src/IntaRNA/PredictorMfe2dHelixBlockHeuristic.h @@ -47,7 +47,7 @@ class PredictorMfe2dHelixBlockHeuristic: public PredictorMfe2dHeuristic { , PredictionTracker * predTracker , const HelixConstraint & helixConstraint); - virtual ~PredictorMfe2dHelixBlockHeuristic(); + virtual ~PredictorMfe2dHelixBlockHeuristic() override; /** * Computes the mfe for the given sequence ranges (i1-j1) in the first @@ -61,7 +61,7 @@ class PredictorMfe2dHelixBlockHeuristic: public PredictorMfe2dHeuristic { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -88,7 +88,7 @@ class PredictorMfe2dHelixBlockHeuristic: public PredictorMfe2dHeuristic { */ virtual void - fillHybridE(); + fillHybridE() override; /** * Fills a given interaction (boundaries given) with the according @@ -97,7 +97,7 @@ class PredictorMfe2dHelixBlockHeuristic: public PredictorMfe2dHeuristic { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; /** * Identifies the next best interaction with an energy equal to or higher @@ -111,7 +111,7 @@ class PredictorMfe2dHelixBlockHeuristic: public PredictorMfe2dHeuristic { */ virtual void - getNextBest( Interaction & curBest ); + getNextBest( Interaction & curBest ) override; /** * Overwrites function of super class to surpress the update. @@ -126,7 +126,7 @@ class PredictorMfe2dHelixBlockHeuristic: public PredictorMfe2dHeuristic { void updateMfe4leftEnd(const size_t i1, const size_t j1 , const size_t i2, const size_t j2 - , const Interaction & curInteraction ); + , const Interaction & curInteraction ) override; }; diff --git a/src/IntaRNA/PredictorMfe2dHelixBlockHeuristicSeed.h b/src/IntaRNA/PredictorMfe2dHelixBlockHeuristicSeed.h index 342a8c65..6238905b 100644 --- a/src/IntaRNA/PredictorMfe2dHelixBlockHeuristicSeed.h +++ b/src/IntaRNA/PredictorMfe2dHelixBlockHeuristicSeed.h @@ -49,7 +49,7 @@ class PredictorMfe2dHelixBlockHeuristicSeed: public PredictorMfe2dHelixBlockHeur , SeedHandler * seedHandlerInstance ); - virtual ~PredictorMfe2dHelixBlockHeuristicSeed(); + virtual ~PredictorMfe2dHelixBlockHeuristicSeed() override; /** * Computes the mfe for the given sequence ranges (i1-j1) in the first @@ -63,7 +63,7 @@ class PredictorMfe2dHelixBlockHeuristicSeed: public PredictorMfe2dHelixBlockHeur virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos)); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos)) override; protected: @@ -98,7 +98,7 @@ class PredictorMfe2dHelixBlockHeuristicSeed: public PredictorMfe2dHelixBlockHeur */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; /** @@ -114,7 +114,7 @@ class PredictorMfe2dHelixBlockHeuristicSeed: public PredictorMfe2dHelixBlockHeur */ virtual void - getNextBest( Interaction & curBest ); + getNextBest( Interaction & curBest ) override; /** * disables predictionTracker usage diff --git a/src/IntaRNA/PredictorMfe2dHeuristic.h b/src/IntaRNA/PredictorMfe2dHeuristic.h index 857c5f0f..059ece2a 100644 --- a/src/IntaRNA/PredictorMfe2dHeuristic.h +++ b/src/IntaRNA/PredictorMfe2dHeuristic.h @@ -44,7 +44,7 @@ class PredictorMfe2dHeuristic: public PredictorMfe { , OutputHandler & output , PredictionTracker * predTracker ); - virtual ~PredictorMfe2dHeuristic(); + virtual ~PredictorMfe2dHeuristic() override; /** * Computes the mfe for the given sequence ranges (i1-j1) in the first @@ -58,7 +58,7 @@ class PredictorMfe2dHeuristic: public PredictorMfe { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -91,7 +91,7 @@ class PredictorMfe2dHeuristic: public PredictorMfe { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; /** * Identifies the next best interaction with an energy equal to or higher @@ -105,7 +105,7 @@ class PredictorMfe2dHeuristic: public PredictorMfe { */ virtual void - getNextBest( Interaction & curBest ); + getNextBest( Interaction & curBest ) override; /** * Overwrites function of super class to surpress the update. @@ -120,7 +120,7 @@ class PredictorMfe2dHeuristic: public PredictorMfe { void updateMfe4leftEnd(const size_t i1, const size_t j1 , const size_t i2, const size_t j2 - , const Interaction & curInteraction ); + , const Interaction & curInteraction ) override; }; diff --git a/src/IntaRNA/PredictorMfe2dHeuristicSeed.h b/src/IntaRNA/PredictorMfe2dHeuristicSeed.h index ef6fe824..5e4fcef3 100644 --- a/src/IntaRNA/PredictorMfe2dHeuristicSeed.h +++ b/src/IntaRNA/PredictorMfe2dHeuristicSeed.h @@ -45,7 +45,7 @@ class PredictorMfe2dHeuristicSeed: public PredictorMfe2dHeuristic { , PredictionTracker * predTracker , SeedHandler* seedHandler ); - virtual ~PredictorMfe2dHeuristicSeed(); + virtual ~PredictorMfe2dHeuristicSeed() override; /** * Computes the mfe for the given sequence ranges (i1-j1) in the first @@ -59,7 +59,7 @@ class PredictorMfe2dHeuristicSeed: public PredictorMfe2dHeuristic { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos)); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos)) override; protected: @@ -91,7 +91,7 @@ class PredictorMfe2dHeuristicSeed: public PredictorMfe2dHeuristic { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; /** @@ -100,7 +100,7 @@ class PredictorMfe2dHeuristicSeed: public PredictorMfe2dHeuristic { */ virtual void - fillHybridE(); + fillHybridE() override; /** @@ -116,7 +116,7 @@ class PredictorMfe2dHeuristicSeed: public PredictorMfe2dHeuristic { */ virtual void - getNextBest( Interaction & curBest ); + getNextBest( Interaction & curBest ) override; }; diff --git a/src/IntaRNA/PredictorMfe2dHeuristicSeedExtension.h b/src/IntaRNA/PredictorMfe2dHeuristicSeedExtension.h index 8d43a315..696ce5db 100644 --- a/src/IntaRNA/PredictorMfe2dHeuristicSeedExtension.h +++ b/src/IntaRNA/PredictorMfe2dHeuristicSeedExtension.h @@ -48,7 +48,7 @@ class PredictorMfe2dHeuristicSeedExtension: public PredictorMfe2dSeedExtension { /** * data cleanup */ - virtual ~PredictorMfe2dHeuristicSeedExtension(); + virtual ~PredictorMfe2dHeuristicSeedExtension() override; /** @@ -66,7 +66,7 @@ class PredictorMfe2dHeuristicSeedExtension: public PredictorMfe2dSeedExtension { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: diff --git a/src/IntaRNA/PredictorMfe2dSeed.h b/src/IntaRNA/PredictorMfe2dSeed.h index d959f97d..e4d1330a 100644 --- a/src/IntaRNA/PredictorMfe2dSeed.h +++ b/src/IntaRNA/PredictorMfe2dSeed.h @@ -53,7 +53,7 @@ class PredictorMfe2dSeed: public PredictorMfe2d { /** * data cleanup */ - virtual ~PredictorMfe2dSeed(); + virtual ~PredictorMfe2dSeed() override; /** @@ -71,7 +71,7 @@ class PredictorMfe2dSeed: public PredictorMfe2d { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -112,7 +112,7 @@ class PredictorMfe2dSeed: public PredictorMfe2d { fillHybridE( const size_t j1, const size_t j2 , const size_t i1init, const size_t i2init , const bool callUpdateOptima - ); + ) override; /** * Fills a given interaction (boundaries given) with the according @@ -121,7 +121,7 @@ class PredictorMfe2dSeed: public PredictorMfe2d { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; }; diff --git a/src/IntaRNA/PredictorMfe2dSeedExtension.h b/src/IntaRNA/PredictorMfe2dSeedExtension.h index 176e14a1..5fef17d9 100644 --- a/src/IntaRNA/PredictorMfe2dSeedExtension.h +++ b/src/IntaRNA/PredictorMfe2dSeedExtension.h @@ -52,7 +52,7 @@ class PredictorMfe2dSeedExtension: public PredictorMfe2d { /** * data cleanup */ - virtual ~PredictorMfe2dSeedExtension(); + virtual ~PredictorMfe2dSeedExtension() override; /** @@ -70,7 +70,7 @@ class PredictorMfe2dSeedExtension: public PredictorMfe2d { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -122,7 +122,7 @@ class PredictorMfe2dSeedExtension: public PredictorMfe2d { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; void printMatrix( const E2dMatrix & matrix ); diff --git a/src/IntaRNA/PredictorMfe2dSeedExtensionRIblast.h b/src/IntaRNA/PredictorMfe2dSeedExtensionRIblast.h index c352cebf..788e70cf 100644 --- a/src/IntaRNA/PredictorMfe2dSeedExtensionRIblast.h +++ b/src/IntaRNA/PredictorMfe2dSeedExtensionRIblast.h @@ -60,7 +60,7 @@ class PredictorMfe2dSeedExtensionRIblast: public PredictorMfe2d { /** * data cleanup */ - virtual ~PredictorMfe2dSeedExtensionRIblast(); + virtual ~PredictorMfe2dSeedExtensionRIblast() override; /** @@ -78,7 +78,7 @@ class PredictorMfe2dSeedExtensionRIblast: public PredictorMfe2d { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -141,7 +141,7 @@ class PredictorMfe2dSeedExtensionRIblast: public PredictorMfe2d { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; void printMatrix( const E2dMatrix & matrix ); diff --git a/src/IntaRNA/PredictorMfeEns.h b/src/IntaRNA/PredictorMfeEns.h index c51fd654..670b8b2d 100644 --- a/src/IntaRNA/PredictorMfeEns.h +++ b/src/IntaRNA/PredictorMfeEns.h @@ -36,7 +36,7 @@ class PredictorMfeEns : public PredictorMfe { , OutputHandler & output , PredictionTracker * predTracker ); - virtual ~PredictorMfeEns(); + virtual ~PredictorMfeEns() override; protected: @@ -127,7 +127,7 @@ class PredictorMfeEns : public PredictorMfe { */ virtual void - reportOptima(); + reportOptima() override; /** * Reports interaction boundaries only (no base pair tracking) @@ -135,7 +135,7 @@ class PredictorMfeEns : public PredictorMfe { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; private: diff --git a/src/IntaRNA/PredictorMfeEns2d.h b/src/IntaRNA/PredictorMfeEns2d.h index 2b77bb67..539d4576 100644 --- a/src/IntaRNA/PredictorMfeEns2d.h +++ b/src/IntaRNA/PredictorMfeEns2d.h @@ -45,7 +45,7 @@ class PredictorMfeEns2d: public PredictorMfeEns { , PredictionTracker * predTracker , BasePairProbabilities * pairProbabilities = nullptr ); - virtual ~PredictorMfeEns2d(); + virtual ~PredictorMfeEns2d() override; /** * Computes the mfe for the given sequence ranges (i1-j1) in the first @@ -60,7 +60,7 @@ class PredictorMfeEns2d: public PredictorMfeEns { void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) - ); + ) override; protected: diff --git a/src/IntaRNA/PredictorMfeEns2dHeuristic.h b/src/IntaRNA/PredictorMfeEns2dHeuristic.h index 75e5b935..5c789a25 100644 --- a/src/IntaRNA/PredictorMfeEns2dHeuristic.h +++ b/src/IntaRNA/PredictorMfeEns2dHeuristic.h @@ -52,7 +52,7 @@ class PredictorMfeEns2dHeuristic: public PredictorMfeEns2d { , PredictionTracker * predTracker , BasePairProbabilities * pairProbabilities = nullptr ); - virtual ~PredictorMfeEns2dHeuristic(); + virtual ~PredictorMfeEns2dHeuristic() override; /** * Computes the mfe for the given sequence ranges (i1-j1) in the first @@ -66,7 +66,7 @@ class PredictorMfeEns2dHeuristic: public PredictorMfeEns2d { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: diff --git a/src/IntaRNA/PredictorMfeEns2dHeuristicSeedExtension.h b/src/IntaRNA/PredictorMfeEns2dHeuristicSeedExtension.h index 27b87452..7ab478eb 100644 --- a/src/IntaRNA/PredictorMfeEns2dHeuristicSeedExtension.h +++ b/src/IntaRNA/PredictorMfeEns2dHeuristicSeedExtension.h @@ -52,7 +52,7 @@ class PredictorMfeEns2dHeuristicSeedExtension: public PredictorMfeEns2dSeedExten /** * data cleanup */ - virtual ~PredictorMfeEns2dHeuristicSeedExtension(); + virtual ~PredictorMfeEns2dHeuristicSeedExtension() override; /** @@ -70,7 +70,7 @@ class PredictorMfeEns2dHeuristicSeedExtension: public PredictorMfeEns2dSeedExten virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -157,7 +157,7 @@ class PredictorMfeEns2dHeuristicSeedExtension: public PredictorMfeEns2dSeedExten */ virtual void - fillHybridZ_left( const size_t j1, const size_t j2 ); + fillHybridZ_left( const size_t j1, const size_t j2 ) override; }; diff --git a/src/IntaRNA/PredictorMfeEns2dSeedExtension.h b/src/IntaRNA/PredictorMfeEns2dSeedExtension.h index 5c509295..21255565 100644 --- a/src/IntaRNA/PredictorMfeEns2dSeedExtension.h +++ b/src/IntaRNA/PredictorMfeEns2dSeedExtension.h @@ -60,7 +60,7 @@ class PredictorMfeEns2dSeedExtension: public PredictorMfeEns { /** * data cleanup */ - virtual ~PredictorMfeEns2dSeedExtension(); + virtual ~PredictorMfeEns2dSeedExtension() override; /** @@ -78,7 +78,7 @@ class PredictorMfeEns2dSeedExtension: public PredictorMfeEns { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -177,7 +177,7 @@ class PredictorMfeEns2dSeedExtension: public PredictorMfeEns { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; /** * Returns the hybridization energy of the non overlapping part of seeds diff --git a/src/IntaRNA/PredictorMfeEnsSeedOnly.h b/src/IntaRNA/PredictorMfeEnsSeedOnly.h index 3507917c..a93cd85f 100644 --- a/src/IntaRNA/PredictorMfeEnsSeedOnly.h +++ b/src/IntaRNA/PredictorMfeEnsSeedOnly.h @@ -37,7 +37,7 @@ class PredictorMfeEnsSeedOnly: public PredictorMfeEns { /** * data cleanup */ - virtual ~PredictorMfeEnsSeedOnly(); + virtual ~PredictorMfeEnsSeedOnly() override; /** * Computes the mfe seed for the given sequence ranges (i1-j1) in the first @@ -51,7 +51,7 @@ class PredictorMfeEnsSeedOnly: public PredictorMfeEns { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -83,7 +83,7 @@ class PredictorMfeEnsSeedOnly: public PredictorMfeEns { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; }; diff --git a/src/IntaRNA/PredictorMfeSeedOnly.h b/src/IntaRNA/PredictorMfeSeedOnly.h index fee5fbee..00bcc940 100644 --- a/src/IntaRNA/PredictorMfeSeedOnly.h +++ b/src/IntaRNA/PredictorMfeSeedOnly.h @@ -37,7 +37,7 @@ class PredictorMfeSeedOnly: public PredictorMfe { /** * data cleanup */ - virtual ~PredictorMfeSeedOnly(); + virtual ~PredictorMfeSeedOnly() override; /** @@ -52,7 +52,7 @@ class PredictorMfeSeedOnly: public PredictorMfe { virtual void predict( const IndexRange & r1 = IndexRange(0,RnaSequence::lastPos) - , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ); + , const IndexRange & r2 = IndexRange(0,RnaSequence::lastPos) ) override; protected: @@ -84,7 +84,7 @@ class PredictorMfeSeedOnly: public PredictorMfe { */ virtual void - traceBack( Interaction & interaction ); + traceBack( Interaction & interaction ) override; }; diff --git a/src/IntaRNA/PredictorSeedExtensionKinetic.h b/src/IntaRNA/PredictorSeedExtensionKinetic.h index fd996428..390bffc1 100644 --- a/src/IntaRNA/PredictorSeedExtensionKinetic.h +++ b/src/IntaRNA/PredictorSeedExtensionKinetic.h @@ -51,7 +51,7 @@ class PredictorSeedExtensionKinetic : public PredictorMfe { SeedHandler * seedHandler, const char score = 'A'); /** Frees the owned seed handler and prediction tracker. */ - virtual ~PredictorSeedExtensionKinetic(); + virtual ~PredictorSeedExtensionKinetic() override; /** * Extends all feasible seeds within inclusive, zero-based sequence ranges. diff --git a/src/IntaRNA/ReverseAccessibility.h b/src/IntaRNA/ReverseAccessibility.h index 983e74d4..49ce5001 100644 --- a/src/IntaRNA/ReverseAccessibility.h +++ b/src/IntaRNA/ReverseAccessibility.h @@ -23,7 +23,7 @@ class ReverseAccessibility: public Accessibility { /** * destruction */ - virtual ~ReverseAccessibility(); + virtual ~ReverseAccessibility() override; /** * Access to the accessibility energy value (according to reversed indices). @@ -35,7 +35,7 @@ class ReverseAccessibility: public Accessibility { */ virtual E_type - getED( const size_t from, const size_t to ) const; + getED( const size_t from, const size_t to ) const override; /** @@ -44,7 +44,7 @@ class ReverseAccessibility: public Accessibility { */ virtual const RnaSequence & - getSequence() const; + getSequence() const override; /** @@ -56,7 +56,7 @@ class ReverseAccessibility: public Accessibility { */ virtual const AccessibilityConstraint& - getAccConstraint() const; + getAccConstraint() const override; /** * Access to the original not-reversed accessibility object. diff --git a/src/IntaRNA/SeedHandlerExplicit.h b/src/IntaRNA/SeedHandlerExplicit.h index a5af80c3..171fb7b4 100644 --- a/src/IntaRNA/SeedHandlerExplicit.h +++ b/src/IntaRNA/SeedHandlerExplicit.h @@ -37,7 +37,7 @@ class SeedHandlerExplicit : public SeedHandler /** * destruction */ - virtual ~SeedHandlerExplicit(); + virtual ~SeedHandlerExplicit() override; /** @@ -50,7 +50,7 @@ class SeedHandlerExplicit : public SeedHandler */ virtual size_t - fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2); + fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2) override; /** * Identifies the base pairs of the seed interaction starting at i1,i2 @@ -64,7 +64,7 @@ class SeedHandlerExplicit : public SeedHandler */ virtual void - traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const; + traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const override; /** @@ -75,7 +75,7 @@ class SeedHandlerExplicit : public SeedHandler */ virtual E_type - getSeedE( const size_t i1, const size_t i2 ) const; + getSeedE( const size_t i1, const size_t i2 ) const override; /** * Checks whether or not a given base pair is the left-most base pair of @@ -87,7 +87,7 @@ class SeedHandlerExplicit : public SeedHandler */ virtual bool - isSeedBound( const size_t i1, const size_t i2 ) const; + isSeedBound( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the seed with left-most base pair (i1,i2) @@ -97,7 +97,7 @@ class SeedHandlerExplicit : public SeedHandler */ virtual size_t - getSeedLength1( const size_t i1, const size_t i2 ) const; + getSeedLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the seed with left-most base pair (i1,i2) @@ -107,7 +107,7 @@ class SeedHandlerExplicit : public SeedHandler */ virtual size_t - getSeedLength2( const size_t i1, const size_t i2 ) const; + getSeedLength2( const size_t i1, const size_t i2 ) const override; @@ -160,7 +160,7 @@ class SeedHandlerExplicit : public SeedHandler updateToNextSeed( size_t & i1, size_t & i2 , const size_t i1min = 0, const size_t i1max = RnaSequence::lastPos , const size_t i2min = 0, const size_t i2max = RnaSequence::lastPos - ) const; + ) const override; public: diff --git a/src/IntaRNA/SeedHandlerIdxOffset.h b/src/IntaRNA/SeedHandlerIdxOffset.h index 11b30f96..5b3b4445 100644 --- a/src/IntaRNA/SeedHandlerIdxOffset.h +++ b/src/IntaRNA/SeedHandlerIdxOffset.h @@ -43,7 +43,7 @@ class SeedHandlerIdxOffset : public SeedHandler /** * destruction of this object and the wrapped seed handler */ - virtual ~SeedHandlerIdxOffset(); + virtual ~SeedHandlerIdxOffset() override; /** * Access to the currently used index offset for sequence 1 @@ -79,7 +79,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual const SeedConstraint& - getConstraint() const; + getConstraint() const override; /** * Access to the underlying interaction energy function @@ -87,7 +87,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual const InteractionEnergy& - getInteractionEnergy() const; + getInteractionEnergy() const override; /** @@ -104,7 +104,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual size_t - fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2); + fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2) override; /** * Identifies the base pairs of the mfe seed interaction starting at i1,i2 @@ -120,7 +120,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual void - traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const; + traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const override; /** @@ -134,7 +134,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual E_type - getSeedE( const size_t i1, const size_t i2 ) const; + getSeedE( const size_t i1, const size_t i2 ) const override; /** * Checks whether or not a given base pair is the left-most base pair of @@ -146,7 +146,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual bool - isSeedBound( const size_t i1, const size_t i2 ) const; + isSeedBound( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe seed with left-most base pair (i1,i2) @@ -159,7 +159,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual size_t - getSeedLength1( const size_t i1, const size_t i2 ) const; + getSeedLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe seed with left-most base pair (i1,i2) @@ -172,7 +172,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual size_t - getSeedLength2( const size_t i1, const size_t i2 ) const; + getSeedLength2( const size_t i1, const size_t i2 ) const override; /** @@ -212,7 +212,7 @@ class SeedHandlerIdxOffset : public SeedHandler updateToNextSeed( size_t & i1, size_t & i2 , const size_t i1min = 0, const size_t i1max = RnaSequence::lastPos , const size_t i2min = 0, const size_t i2max = RnaSequence::lastPos - ) const; + ) const override; /** * Adds all seeds to a given interaction that are completely covered by its @@ -223,7 +223,7 @@ class SeedHandlerIdxOffset : public SeedHandler */ virtual void - addSeeds( Interaction & i ) const; + addSeeds( Interaction & i ) const override; /** * Checks whether or not two seeds are loop overlapping, ie. given i1 < k1, the @@ -240,7 +240,7 @@ class SeedHandlerIdxOffset : public SeedHandler virtual bool areLoopOverlapping( const size_t i1, const size_t i2 - , const size_t k1, const size_t k2 ) const; + , const size_t k1, const size_t k2 ) const override; /** * Checks whether or not a given index pair is a valid seed base pair @@ -255,7 +255,7 @@ class SeedHandlerIdxOffset : public SeedHandler bool isFeasibleSeedBasePair( const size_t i1 , const size_t i2 - , const bool atEndOfSeed = false ) const; + , const bool atEndOfSeed = false ) const override; protected: diff --git a/src/IntaRNA/SeedHandlerMfe.h b/src/IntaRNA/SeedHandlerMfe.h index 94979fdf..00cee2c4 100644 --- a/src/IntaRNA/SeedHandlerMfe.h +++ b/src/IntaRNA/SeedHandlerMfe.h @@ -62,7 +62,7 @@ class SeedHandlerMfe : public SeedHandler /** * destruction */ - virtual ~SeedHandlerMfe(); + virtual ~SeedHandlerMfe() override; /** * Computes the seed matrix for the given interval boundaries @@ -74,7 +74,7 @@ class SeedHandlerMfe : public SeedHandler */ virtual size_t - fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2); + fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2) override; /** * Identifies the base pairs of the mfe seed interaction starting at i1,i2 @@ -88,7 +88,7 @@ class SeedHandlerMfe : public SeedHandler */ virtual void - traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const; + traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const override; /** @@ -99,7 +99,7 @@ class SeedHandlerMfe : public SeedHandler */ virtual E_type - getSeedE( const size_t i1, const size_t i2 ) const; + getSeedE( const size_t i1, const size_t i2 ) const override; /** * Checks whether or not a given base pair is the left-most base pair of @@ -111,7 +111,7 @@ class SeedHandlerMfe : public SeedHandler */ virtual bool - isSeedBound( const size_t i1, const size_t i2 ) const; + isSeedBound( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe seed with left-most base pair (i1,i2) @@ -121,7 +121,7 @@ class SeedHandlerMfe : public SeedHandler */ virtual size_t - getSeedLength1( const size_t i1, const size_t i2 ) const; + getSeedLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe seed with left-most base pair (i1,i2) @@ -131,7 +131,7 @@ class SeedHandlerMfe : public SeedHandler */ virtual size_t - getSeedLength2( const size_t i1, const size_t i2 ) const; + getSeedLength2( const size_t i1, const size_t i2 ) const override; diff --git a/src/IntaRNA/SeedHandlerNoBulge.h b/src/IntaRNA/SeedHandlerNoBulge.h index 927643c9..8d504ffa 100644 --- a/src/IntaRNA/SeedHandlerNoBulge.h +++ b/src/IntaRNA/SeedHandlerNoBulge.h @@ -54,7 +54,7 @@ class SeedHandlerNoBulge : public SeedHandler /** * destruction */ - virtual ~SeedHandlerNoBulge(); + virtual ~SeedHandlerNoBulge() override; /** * Computes the seed information for the given interval boundaries @@ -66,7 +66,7 @@ class SeedHandlerNoBulge : public SeedHandler */ virtual size_t - fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2); + fillSeed(const size_t i1, const size_t j1, const size_t i2, const size_t j2) override; /** * Identifies the base pairs of the mfe seed interaction starting at i1,i2 @@ -80,7 +80,7 @@ class SeedHandlerNoBulge : public SeedHandler */ virtual void - traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const; + traceBackSeed( Interaction & interaction, const size_t i1, const size_t i2) const override; /** @@ -91,7 +91,7 @@ class SeedHandlerNoBulge : public SeedHandler */ virtual E_type - getSeedE( const size_t i1, const size_t i2 ) const; + getSeedE( const size_t i1, const size_t i2 ) const override; /** * Checks whether or not a given base pair is the left-most base pair of @@ -103,7 +103,7 @@ class SeedHandlerNoBulge : public SeedHandler */ virtual bool - isSeedBound( const size_t i1, const size_t i2 ) const; + isSeedBound( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq1 of the mfe seed with left-most base pair (i1,i2) @@ -113,7 +113,7 @@ class SeedHandlerNoBulge : public SeedHandler */ virtual size_t - getSeedLength1( const size_t i1, const size_t i2 ) const; + getSeedLength1( const size_t i1, const size_t i2 ) const override; /** * Access to the length in seq2 of the mfe seed with left-most base pair (i1,i2) @@ -123,7 +123,7 @@ class SeedHandlerNoBulge : public SeedHandler */ virtual size_t - getSeedLength2( const size_t i1, const size_t i2 ) const; + getSeedLength2( const size_t i1, const size_t i2 ) const override; /** * Replace the input variables i1 and i2 to values to within the given range @@ -153,7 +153,7 @@ class SeedHandlerNoBulge : public SeedHandler updateToNextSeed( size_t & i1, size_t & i2 , const size_t i1min = 0, const size_t i1max = RnaSequence::lastPos , const size_t i2min = 0, const size_t i2max = RnaSequence::lastPos - ) const; + ) const override; protected: From fc62a23ebbedc591a729a708df03427fbae84793 Mon Sep 17 00:00:00 2001 From: AlexanderMitrofanov Date: Wed, 21 Oct 2026 17:05:02 +0200 Subject: [PATCH 2/2] feat: address browser interface review on PR 287 --- .github/workflows/build-wasm.yml | 5 +- .github/workflows/build.yml | 1 - ChangeLog | 32 + README.md | 4 +- scripts/wasm/build.sh | 5 +- scripts/wasm/smoke-protocol.mjs | 47 ++ scripts/wasm/stage.sh | 27 +- scripts/wasm/test-browser.py | 224 +++--- scripts/wasm/write-build-info.mjs | 23 + webinterface/app.js | 198 +++-- webinterface/assets/README.md | 12 + webinterface/assets/bioinf-fr-logo-blau.png | Bin 0 -> 187836 bytes webinterface/assets/logo-uni-freiburg.png | Bin 0 -> 24542 bytes webinterface/columns.js | 315 ++++++++ webinterface/docs/step1-wasm-compilation.md | 16 + .../docs/step2-worker-architecture.md | 48 +- webinterface/docs/step3-frontend-ui.md | 115 +-- .../docs/step4-results-and-deployment.md | 83 ++- webinterface/index.html | 86 +-- webinterface/intarna-worker.js | 48 +- webinterface/options.js | 88 +++ webinterface/protocol.js | 259 ++++++- webinterface/results.js | 168 ++--- webinterface/styles.css | 703 +++++++++++++++--- webinterface/tests/protocol.test.mjs | 71 ++ webinterface/tests/results.test.mjs | 18 +- webinterface/tests/worker.test.mjs | 14 +- 27 files changed, 2060 insertions(+), 550 deletions(-) create mode 100644 scripts/wasm/smoke-protocol.mjs create mode 100644 scripts/wasm/write-build-info.mjs create mode 100644 webinterface/assets/README.md create mode 100644 webinterface/assets/bioinf-fr-logo-blau.png create mode 100644 webinterface/assets/logo-uni-freiburg.png create mode 100644 webinterface/columns.js create mode 100644 webinterface/options.js create mode 100644 webinterface/tests/protocol.test.mjs diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index 46e82f90..1790b36a 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -2,7 +2,6 @@ name: WebAssembly and web interface on: pull_request: - branches: [master] push: branches: [master] workflow_dispatch: @@ -36,7 +35,9 @@ jobs: - name: Cross-compile dependencies and IntaRNA run: bash scripts/wasm/build.sh - name: Check real WASM predictions against the native reference - run: node scripts/wasm/smoke.mjs + run: | + node scripts/wasm/smoke.mjs + node scripts/wasm/smoke-protocol.mjs - name: Stage the static site and notices run: bash scripts/wasm/stage.sh - name: Install browser test tools diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18668a64..b8baddd9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: push: branches: [master] pull_request: - branches: [master] defaults: run: diff --git a/ChangeLog b/ChangeLog index 2b5c79c0..9a9be5b4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -14,6 +14,11 @@ ## Interface and handling +- extend the browser interface with vaRRI styling, prediction/evaluation tasks, + multi-FASTA, start indices, CLI parameter groups, selectable CSV columns and + additional output previews/downloads; label pair-relative differences mfe Δ + and display actual compiled versions (PR #287 review) + - display sortable/filterable browser results and native CSV downloads; publish the tested interface alongside existing Pages documentation (#286) @@ -75,6 +80,10 @@ ## Technical changes and Optimizations +- annotate inherited virtual methods/destructors with override, preserve compiled + WASM asset provenance through staging, and expand protocol/browser validation + for the PR #287 review + - cross-compile IntaRNA and pinned dependencies to WebAssembly (#286, step 1) - verify browser predictions against native accessibility/energy references and exercise real WASM cancellation and nested Pages paths in Chromium/Firefox @@ -232,6 +241,29 @@ energies, restricted partition sums, and trackers. ################################################################################ ################################################################################ +261021 Alexander Mitrofanov + * src/IntaRNA/*.h + * Mark 194 inherited virtual methods and 45 derived destructors override; + keep algorithms, interfaces and third-party headers unchanged. + * webinterface/{options,columns,protocol,app,results,intarna-worker}.js + + Add CLI-based parameter groups and validation, multi-FASTA prediction, + signed per-strand start indices and IntaRNAeval structure evaluation. + + Provide all CSV columns, table visibility controls, pair-relative mfe Δ, + local SHAPE/accessibility data and previews/downloads of native extra files. + * webinterface/index.html, styles.css, assets/* + * Follow vaRRI main-v2 colors and institutional header; keep one parameter + group open at a time and adapt controls/defaults to the selected task. + * scripts/wasm/{build,stage}.sh, write-build-info.mjs + + Record actual compiled engine/library versions and asset hashes in text/JSON + at build time; preserve/check provenance during staging and show it in UI. + * Reject inconsistent missing overrides in the Emscripten build. + * webinterface/tests/*, scripts/wasm/{smoke-protocol.mjs,test-browser.py} + + Exercise task validation/defaults, real WASM evaluation/prediction, multiple + records/coordinates, imported data, columns/reports and worker lifecycle. + * .github/workflows/{build,build-wasm}.yml, README.md, webinterface/docs/* + * Run WASM/browser checks on dependent PRs and document revised behavior, + provenance, native option replacements and browser limits (PR #287 review). + 261020 Alexander Mitrofanov * scripts/wasm/*, .github/workflows/build-wasm.yml, webinterface/app.js + Execute dependency API probes and test structured accessibility costs, diff --git a/README.md b/README.md index 5142f6cb..1200e7fb 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ **Efficient RNA-RNA interaction prediction incorporating accessibility and seeding of interaction sites** -Predict short RNA pairs locally with the [browser interface](https://backofenlab.github.io/IntaRNA/webinterface/). -See the [WebAssembly build instructions](docs/step1-wasm-compilation.md) and +Predict short RNA pairs or evaluate supplied structures locally with the [browser interface](https://backofenlab.github.io/IntaRNA/webinterface/). +See the [WebAssembly build instructions](webinterface/docs/step1-wasm-compilation.md) and [interface/deployment notes](webinterface/docs/step4-results-and-deployment.md). diff --git a/scripts/wasm/build.sh b/scripts/wasm/build.sh index 33ccaa17..b6dd493d 100644 --- a/scripts/wasm/build.sh +++ b/scripts/wasm/build.sh @@ -89,7 +89,7 @@ export VRNA_LIBS="-L$prefix/lib -lRNA -lm" (cd "$repo" && bash autotools-init.sh) mkdir -p "$build/intarna" (cd "$build/intarna" - CPPFLAGS="-I$build/intarna/src -I$repo/src $CPPFLAGS" emconfigure "$repo/configure" --host=wasm32-unknown-emscripten \ + CXXFLAGS="$CXXFLAGS -Werror=inconsistent-missing-override" CPPFLAGS="-I$build/intarna/src -I$repo/src $CPPFLAGS" emconfigure "$repo/configure" --host=wasm32-unknown-emscripten \ --disable-multithreading --disable-log-coloring --with-mdspan=kokkos \ --with-vrna="$prefix" --with-boost="$prefix" \ --with-boost-libdir="$prefix/lib" --with-zlib="$prefix" @@ -99,4 +99,7 @@ mkdir -p "$build/intarna" cp src/bin/IntaRNA.wasm "$build/site/wasm/intarna.wasm") # ES modules also work in Node smoke tests without changing package scope. printf '{"type":"module"}\n' > "$build/site/wasm/package.json" +node "$repo/scripts/wasm/write-build-info.mjs" "$repo" "$build/site" \ + "$(emcc -dumpversion)" "$(git -C "$repo" rev-parse HEAD)" \ + "$(git -C "$repo" status --porcelain --untracked-files=no)" echo "WASM assets: $build/site/wasm" diff --git a/scripts/wasm/smoke-protocol.mjs b/scripts/wasm/smoke-protocol.mjs new file mode 100644 index 00000000..d3947aef --- /dev/null +++ b/scripts/wasm/smoke-protocol.mjs @@ -0,0 +1,47 @@ +// Integration coverage of browser argument translation against the real CLI. +import assert from 'node:assert/strict'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { DEFAULTS, EVALUATION_EXAMPLE, prepareRequest, makeArguments } from '../../webinterface/protocol.js'; +import { CSV_CATALOG } from '../../webinterface/columns.js'; +import { parseResults } from '../../webinterface/results.js'; +const asset = resolve(process.argv[2] || 'build-wasm/site/wasm/intarna.js'); +const { default: createIntaRNA } = await import(pathToFileURL(asset)); +async function run(input, format = 'C') { + const request = prepareRequest({ ...DEFAULTS, ...input }); + const diagnostics = []; + const module = await createIntaRNA({ noInitialRun: true, locateFile: () => new URL('./intarna.wasm', pathToFileURL(asset)).href, print: (line) => diagnostics.push(line), printErr: (line) => diagnostics.push(line) }); + module.FS.writeFile('/query.fa', request.query.fasta); + module.FS.writeFile('/target.fa', request.target.fasta); + module.FS.mkdir('/output'); + for (const [key, value] of Object.entries(request.parameters)) if (/^(q|t)(Shape|AccFile)$/.test(key) && value) module.FS.writeFile(`/${key}.txt`, value + '\n'); + const args = makeArguments(request, format); + assert.equal(module.callMain(args), 0, `${args.join(' ')}\n${diagnostics.join('\n')}`); + const text = module.FS.readFile(format === 'C' ? '/results.csv' : '/report.txt', { encoding: 'utf8' }); + return { rows: format === 'C' ? parseResults(text) : [], text, fs: module.FS }; +} +const defaults = await run({ parameters: { outNumber: 10 } }); +assert.equal(defaults.rows.length, 10); +assert.equal(defaults.rows[0].E, -21.4); +const shifted = await run({ qIdxPos0: -3, tIdxPos0: -5 }); +assert.deepEqual(['start1', 'end1', 'start2', 'end2'].map((key) => shifted.rows[0][key]), [-5, 5, -3, 7]); +const multi = await run({ query: '>q1\nGGGAGGAGGG\n>q2\nGGGAGGAGGG', target: '>t1\nCCCUCCUCCC\n>t2\nCCCUCCUCCC', outputs: ['qMinE', 'tMinE'] }); +assert.equal(multi.rows.length, 4); +assert.ok(multi.rows.every((row) => row.deltaE === 0)); +assert.equal(multi.fs.readdir('/output').filter((name) => name.endsWith('.csv')).length, 8); +const pairwise = await run({ query: '>q1\nGGGAGGAGGG\n>q2\nGGGAGGAGGG', target: '>t1\nCCCUCCUCCC\n>t2\nCCCUCCUCCC', parameters: { outPairwise: true } }); +assert.equal(pairwise.rows.length, 2); +const evaluation = await run({ task: 'evaluation', rri: EVALUATION_EXAMPLE + ':1|||||||||&2|||||||||', outputs: ['qAcc', 'tPu'], csvColumns: CSV_CATALOG.map((c) => c.key) }); +assert.equal(evaluation.rows.length, 2); +assert.equal(evaluation.rows[0].E, -21.4); +assert.equal(evaluation.rows[0].seedE, 'NAN'); +assert.ok((await run({ task: 'evaluation', rri: EVALUATION_EXAMPLE }, 'D')).text.includes('-21.4')); +assert.ok((await run({ task: 'evaluation', rri: EVALUATION_EXAMPLE }, 'E')).text.length > 0); +const probabilities = await run({ parameters: { model: 'P' }, csvColumns: CSV_CATALOG.map((c) => c.key), outputs: ['bpProb', 'bpsvg', 'qSpotProb', 'tSpotProb', 'spotProb', 'pMinE'] }); +assert.equal(probabilities.rows.length, 1); +assert.ok(probabilities.fs.readFile('/output/bpsvg.svg', { encoding: 'utf8' }).includes('= 1); +console.log('Browser protocol: native-reference prediction/evaluation, signed coordinates, multi-FASTA, all columns, imported accessibility, energy/seed/helix/SHAPE options and output files passed'); diff --git a/scripts/wasm/stage.sh b/scripts/wasm/stage.sh index d6d32450..148ae02e 100644 --- a/scripts/wasm/stage.sh +++ b/scripts/wasm/stage.sh @@ -7,7 +7,8 @@ site="$build/site" test -s "$site/wasm/intarna.js" test -s "$site/wasm/intarna.wasm" mkdir -p "$site/licenses" -for file in index.html styles.css app.js mark.svg protocol.js runner.js results.js; do +cp -R "$repo/webinterface/assets" "$site/" +for file in index.html styles.css app.js mark.svg protocol.js runner.js results.js options.js columns.js; do cp "$repo/webinterface/$file" "$site/$file" done cp "$repo/webinterface/intarna-worker.js" "$site/intarna-worker.js" @@ -42,18 +43,16 @@ cat > "$site/licenses/index.html" <<'HTML'

Build provenance · Back to IntaRNA

HTML -python3 - "$repo" "$site" <<'PY' -import json -import subprocess -import sys +# Provenance belongs to the compiled assets, never the later staging revision. +python3 - "$site" <<'PYINFO' +import hashlib, json, sys from pathlib import Path -repo, site = sys.argv[1:] -metadata = { - "source": "https://github.com/BackofenLab/IntaRNA", - "commit": subprocess.check_output(["git", "-C", repo, "rev-parse", "HEAD"], text=True).strip(), - "emscripten": "4.0.23", "boost": "1.87.0", "viennarna": "2.7.0", "zlib": "1.3.1", - "threading": "single worker per prediction", -} -Path(site, "build-info.json").write_text(json.dumps(metadata, indent=2) + "\n") -PY +site = Path(sys.argv[1]) +metadata = json.loads((site / "build-info.json").read_text()) +if not (site / "build-info.txt").is_file(): + raise SystemExit("Missing compiled version text") +for name, digest in metadata["sha256"].items(): + if hashlib.sha256((site / "wasm" / name).read_bytes()).hexdigest() != digest: + raise SystemExit(f"Mismatched engine: {name}") +PYINFO echo "Static site: $site" diff --git a/scripts/wasm/test-browser.py b/scripts/wasm/test-browser.py index b712bde1..29b19f11 100644 --- a/scripts/wasm/test-browser.py +++ b/scripts/wasm/test-browser.py @@ -1,119 +1,177 @@ -"""Browser integration tests against staged assets, including the real WASM engine. - -Start an HTTP server serving build-wasm/site, then pass its URL. Optional ---browser selects Firefox; --screenshot saves the populated desktop workspace. -""" +"""Exercise the staged browser interface with real WASM at a nested URL.""" import argparse from pathlib import Path from playwright.sync_api import sync_playwright, expect parser = argparse.ArgumentParser(description=__doc__) -parser.add_argument("url") -parser.add_argument("--browser", choices=["chromium", "firefox"], default="chromium") -parser.add_argument("--screenshot") +parser.add_argument('url') +parser.add_argument('--browser', choices=['chromium', 'firefox'], default='chromium') +parser.add_argument('--screenshot') args = parser.parse_args() with sync_playwright() as playwright: browser = getattr(playwright, args.browser).launch(headless=True) - page = browser.new_page(viewport={"width": 1440, "height": 1100}) + page = browser.new_page(viewport={'width': 1440, 'height': 1100}) errors = [] - page.on("pageerror", lambda error: errors.append(str(error))) + page.on('pageerror', lambda error: errors.append(str(error))) page.goto(args.url) - page.wait_for_load_state("networkidle") - compute = page.get_by_role("button", name="Compute interactions", exact=True) - status = page.locator("#result-status") + page.wait_for_load_state('networkidle') + compute = page.locator('#compute') + status = page.locator('#result-status') + rows = page.locator('tbody tr') + + def open_group(key): + group = page.locator('#' + key) + if not group.evaluate('node => node.open'): + group.locator('summary').first.click() + expect(group).to_have_attribute('open', '') + assert page.locator('.parameter-group[open]').count() == 1 + + def fill(key, value): + parent = page.locator('#' + key).evaluate('node => node.closest(".parameter-group")?.id') + if parent: + open_group(parent) + page.locator('#' + key).fill(str(value)) + + def predict(): + compute.click() + expect(status).to_contain_text('complete:', timeout=120000) + expect(page.locator('#input-error')).not_to_be_visible() - # Invalid bases and multiple records must fail before engine startup. - page.locator("#query").fill(">bad\nACGU-ACGU") + expect(page.locator('#version-info')).to_contain_text('ViennaRNA 2.7.0') + for logo in page.locator('.site-header img').all(): + assert logo.evaluate('node => node.complete && node.naturalWidth > 0') + for key in ['query-options', 'target-options', 'seed-options', 'helix-options', 'shape-options', 'interaction-options', 'output-options', 'general-options', 'sequence-input']: + open_group(key) + + # JavaScript validation works even when the offending control is folded. + fill('query', '>bad\nACGU-ACGU') compute.click() - expect(page.locator("#input-error")).to_contain_text("use only") - expect(page.locator("#loading")).not_to_be_visible() - page.locator("#query").fill(">a\nACGUACGU\n>b\nACGUACGU") + expect(page.locator('#input-error')).to_contain_text('use only') + expect(page.locator('#loading')).not_to_be_visible() + page.locator('#example').click() + fill('temperature', '101') + open_group('sequence-input') compute.click() - expect(page.locator("#input-error")).to_contain_text("exactly one") - page.get_by_role("button", name="Load example").click() + expect(page.locator('#input-error')).to_contain_text('Temperature') + page.locator('#reset').click() - # Real RNAlib prediction: orientation, native-reference energy and suboptimals. - compute.click() - expect(status).to_contain_text("Prediction complete", timeout=120000) - rows = page.locator("tbody tr") + # Reference prediction, selectable CSV output and independent table visibility. + fill('outNumber', 10) + page.locator('#outCsvSort').select_option('E') + predict() expect(rows).to_have_count(10) - assert rows.first.locator("td").all_text_contents() == ["query", "1", "10", "target", "1", "10", "-21.40", "0.00"] - assert page.locator(".table-scroll").evaluate("node => node.scrollWidth") > 0 - for key in ["Query", "Query start", "Query end", "Target", "Target start", "Target end", "Energy (kcal/mol)", "ΔE (kcal/mol)"]: - page.get_by_role("button", name=f"Sort by {key}", exact=False).first.click() - # Select energy, then check numeric descending and ascending order. - page.get_by_role("button", name="Sort by Energy (kcal/mol)", exact=False).click() - page.get_by_role("button", name="Sort by Energy (kcal/mol)", exact=False).click() - expect(page.locator('th[aria-sort="descending"]')).to_contain_text("Energy") - assert float(rows.first.locator("td").nth(6).inner_text()) == -14.3 - page.get_by_role("button", name="Sort by Energy (kcal/mol)", exact=False).click() - assert float(rows.first.locator("td").nth(6).inner_text()) == -21.4 - page.locator("#search").fill("-21.4") + assert rows.first.locator('td').all_text_contents()[:6] == ['target', '1', '10', 'query', '1', '10'] + assert rows.first.locator('td').all_text_contents()[-2:] == ['-21.40', '0.00'] + page.get_by_role('button', name='Sort by Energy (kcal/mol)', exact=False).click() + assert float(rows.first.locator('td').nth(8).inner_text()) == -14.3 + page.get_by_role('button', name='Sort by Energy (kcal/mol)', exact=False).click() + page.locator('#search').fill('-21.4') expect(rows).to_have_count(1) - page.locator("#search").fill("no match") - expect(page.locator("#no-matches")).to_be_visible() - page.locator("#search").fill("") + page.locator('#search').fill('no match') + expect(page.locator('#no-matches')).to_be_visible() + page.locator('#search').fill('') + page.locator('.column-picker summary').click() + page.locator('#visible-columns input[value="start1"]').uncheck() + assert 'Target start' not in page.locator('thead').inner_text() + page.locator('#visible-columns input[value="start1"]').check() with page.expect_download() as downloaded: - page.get_by_role("button", name="Download CSV").click() + page.locator('#download').click() csv = Path(downloaded.value.path()).read_text() - assert "target;1;10;query;1;10;-21.4" in csv + assert csv.splitlines()[0] == 'id1;start1;end1;id2;start2;end2;subseqDP;hybridDP;E' + assert 'target;1;10;query;1;10;' in csv + assert 'deltaE' not in csv if args.screenshot: + open_group('sequence-input') page.screenshot(path=args.screenshot, full_page=True) + page.set_viewport_size({'width': 390, 'height': 844}) + assert page.evaluate('document.documentElement.scrollWidth <= innerWidth') + page.set_viewport_size({'width': 1440, 'height': 1100}) - # Mobile content stays inside the viewport; the data table scrolls itself. - page.set_viewport_size({"width": 390, "height": 844}) - assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") - page.set_viewport_size({"width": 1440, "height": 1100}) + # Multi-FASTA prediction, signed inclusive output and additional files. + page.locator('#example').click() + fill('query', '>q1\nGGGAGGAGGG\n>q2\nGGGAGGAGGG') + fill('qIdxPos0', -3) + fill('tIdxPos0', -5) + open_group('output-options') + page.locator('#output-options input[value="qMinE"]').check() + predict() + expect(rows).to_have_count(2) + assert rows.first.locator('td').all_text_contents()[:6] == ['target', '-5', '5', 'q1', '-3', '7'] + expect(page.locator('#file-list details')).to_have_count(2) + page.locator('#file-list summary').first.click() + expect(page.locator('#file-list pre').first).to_contain_text('minE') - # Deliberately hold WASM download so Abort deterministically covers loading. - held_routes = [] - page.route("**/wasm/intarna.wasm", lambda route: held_routes.append(route)) + # FASTA identity follows native first-token naming, including collisions. + fill('query', '>q first\nGGGAGGAGGG\n>q second\nGGGAGGAGGG') compute.click() - expect(page.locator("#loading")).to_be_visible() - page.get_by_role("button", name="Abort computation").click() - expect(status).to_contain_text("aborted") - expect(compute).to_be_enabled() - # Settle any intercepted request after terminating the worker. - for route in held_routes: - route.abort() - page.unroute("**/wasm/intarna.wasm") + expect(page.locator('#input-error')).to_contain_text('unique') - # Wait until actual synchronous C++ starts, then Escape must still work. - page.locator("#query").fill(">long_query\n" + "G" * 1000) - page.locator("#target").fill(">long_target\n" + "C" * 1000) + # Evaluation resets native defaults and removes prediction-only controls. + fill('query', '>q1\nGGGAGGAGGG\n>q2\nGGGAGGAGGG') + page.locator('#task').select_option('evaluation') + expect(page.locator('#seed-options')).not_to_be_visible() + expect(page.locator('#outNumber')).to_be_disabled() + fill('rri', '1||||||||||&1||||||||||') compute.click() - expect(page.locator("#loading-status")).to_contain_text("Predicting RNA", timeout=120000) - page.keyboard.press("Escape") - expect(status).to_contain_text("aborted") - page.get_by_role("button", name="Load example").click() + expect(page.locator('#input-error')).to_contain_text('exactly one') + page.locator('#example').click() + fill('rri', '1||||||||||&1||||||||||:1|||||||||&2|||||||||') + open_group('output-options') + page.locator('#output-options input[value="detailed"]').check() + page.locator('#output-options input[value="ED1"]').check() + predict() + expect(status).to_contain_text('Evaluation complete') + expect(rows).to_have_count(2) + expect(page.locator('thead')).to_contain_text('ED1') + expect(page.locator('#file-list summary')).to_contain_text('intarna-detailed.txt') + page.locator('#file-list summary').click() + expect(page.locator('#file-list pre')).to_contain_text('-21.4') + with page.expect_download() as report: + page.locator('#file-list button').click() + assert '-21.4' in Path(report.value.path()).read_text() + fill('rri', '1|||&1||') compute.click() - expect(status).to_contain_text("Prediction complete", timeout=120000) - expect(rows).to_have_count(10) + expect(page.locator('#input-error')).to_contain_text('same nonzero') - # Ambiguous bases produce a valid empty prediction, not a fabricated result. - page.locator("#query").fill(">no_pairs\nNNNNNNNNNN") + # Startup and synchronous C++ cancellation both allow a fresh prediction. + page.locator('#task').select_option('prediction') + page.locator('#example').click() + held_routes = [] + page.route('**/wasm/intarna.wasm', lambda route: held_routes.append(route)) compute.click() - expect(status).to_contain_text("No favorable interactions", timeout=120000) - expect(page.locator("#results-view")).not_to_be_visible() + expect(page.locator('#loading')).to_be_visible() + page.locator('#abort').click() + expect(status).to_contain_text('aborted') + for route in held_routes: + route.abort() + page.unroute('**/wasm/intarna.wasm') + fill('query', '>long_query\n' + 'G' * 1000) + fill('target', '>long_target\n' + 'C' * 1000) + compute.click() + expect(page.locator('#loading-status')).to_contain_text('Predicting RNA', timeout=120000) + page.keyboard.press('Escape') + expect(status).to_contain_text('aborted') + page.locator('#example').click() + predict() - # Sequence identifiers remain text, even if they resemble HTML. - page.locator("#query").fill(">\nGGGAGGAGGG") + fill('query', '>no_pairs\nNNNNNNNNNN') compute.click() - expect(status).to_contain_text("Prediction complete", timeout=120000) - assert page.locator("tbody img").count() == 0 - assert "\nGGGAGGAGGG') + predict() + assert page.locator('tbody img').count() == 0 + assert ' pathToFileURL(resolve(site, 'wasm/intarna.wasm')).href, print: (line) => lines.push(line), printErr: (line) => lines.push(line) }); +if (module.callMain(['--version']) !== 0) throw new Error('Could not read compiled engine versions'); +const version = lines.join('\n'); +const match = /IntaRNA\s+(\S+)[\s\S]*Vienna RNA package\s+(\S+)\s+and boost\s+(\S+)/i.exec(version); +if (!match) throw new Error(`Unrecognized engine version output: ${version}`); +const metadata = { + source: 'https://github.com/BackofenLab/IntaRNA', commit, + dirty: !!changes, + intarna: match[1], viennarna: match[2], boost: match[3], emscripten, zlib: '1.3.1', + threading: 'single worker per task', + sha256: Object.fromEntries(['intarna.js', 'intarna.wasm'].map((name) => [name, createHash('sha256').update(readFileSync(resolve(site, 'wasm', name))).digest('hex')])), +}; +writeFileSync(resolve(site, 'build-info.json'), JSON.stringify(metadata, null, 2) + '\n'); +writeFileSync(resolve(site, 'build-info.txt'), `${version}\nIntaRNA repository: ${metadata.source}\nIntaRNA revision: ${metadata.commit}${metadata.dirty ? ' (modified sources)' : ''}\nEmscripten: ${emscripten}\nzlib: ${metadata.zlib}\n${Object.entries(metadata.sha256).map(([name, hash]) => `SHA-256 ${name}: ${hash}`).join('\n')}\n`); +console.log(`Recorded compiled versions: IntaRNA ${match[1]}, ViennaRNA ${match[2]}, Boost ${match[3]}`); diff --git a/webinterface/app.js b/webinterface/app.js index 652c0c82..eeb37665 100644 --- a/webinterface/app.js +++ b/webinterface/app.js @@ -1,95 +1,159 @@ -import { DEFAULTS, parseSequence, prepareRequest } from './protocol.js'; +import { DEFAULTS, EVALUATION_EXAMPLE, parseFasta, prepareRequest, CSV_COLUMNS } from './protocol.js'; +import { GROUPS, FIELDS, EXTRA_OUTPUTS } from './options.js'; +import { CSV_CATALOG } from './columns.js'; import { PredictionRunner } from './runner.js'; -import { ResultsView } from './results.js'; +import { ResultsView, downloadText } from './results.js'; const byId = (id) => document.getElementById(id); -const form = byId('prediction-form'); -const runner = new PredictionRunner(); -const dialog = byId('loading'); -const status = byId('result-status'); -const errorBox = byId('input-error'); +const form = byId('prediction-form'), runner = new PredictionRunner(); +const dialog = byId('loading'), status = byId('result-status'), errorBox = byId('input-error'); const results = new ResultsView(byId('results-view')); +const csvInputs = [], outputInputs = []; +function element(tag, text, className) { + const node = document.createElement(tag); + if (text) node.textContent = text; + if (className) node.className = className; + return node; +} +function group(key, label) { + const details = element('details', '', 'parameter-group'); + details.id = key; details.name = 'parameters'; details.append(element('summary', label)); + const body = element('div', '', 'group-body'); details.append(body); + byId('parameter-groups').append(details); return body; +} +for (const definition of GROUPS) { + const body = group(definition.key, definition.label), grid = element('div', '', 'field-grid'); body.append(grid); + for (const field of definition.fields) { + const wrapper = element('div', '', `field${field.type === 'checkbox' ? ' check' : ''}`); + const label = element('label', field.label); label.htmlFor = field.key; label.title = `--${field.key}`; + const input = element(field.type === 'select' ? 'select' : field.type === 'textarea' ? 'textarea' : 'input'); input.id = field.key; + if (input.tagName === 'INPUT') input.type = field.type; + if (field.type === 'select') for (const [value, name] of Object.entries(field.choices)) { const option = element('option', name); option.value = value; input.append(option); } + if (field.type === 'number') { input.min = field.min; input.max = field.max; input.step = field.step ?? 1; } + if (field.type === 'checkbox') { input.checked = field.value; wrapper.append(input, label); } + else { input.value = field.value; wrapper.append(label, input); } + if (field.hint) { const hint = element('p', field.hint, 'field-hint'); hint.id = `${field.key}-help`; input.setAttribute('aria-describedby', hint.id); wrapper.append(hint); } + if (field.type === 'textarea') { + input.spellcheck = false; + const upload = element('input'); upload.type = 'file'; upload.accept = '.txt,.csv,.shape,.dat'; upload.setAttribute('aria-label', `Load ${field.label} from a local text file`); + upload.dataset.for = field.key; + upload.addEventListener('change', async () => { + const file = upload.files[0]; if (!file) return; + if (file.size > 1000000) { errorBox.textContent = 'Choose a text file of at most 1 MB.'; errorBox.hidden = false; return; } + input.value = await file.text(); input.dispatchEvent(new Event('input', { bubbles: true })); upload.value = ''; + }); wrapper.append(upload); + } + grid.append(wrapper); + } +} +function checkbox(container, key, labelText, description) { + const label = element('label'), input = element('input'); input.type = 'checkbox'; input.value = key; + if (description) label.title = description; + label.append(input, document.createTextNode(labelText)); container.append(label); return input; +} +const outputBody = byId('output-options').querySelector('.group-body'); +outputBody.append(element('h3', 'CSV output columns', 'subheading')); +outputBody.append(element('p', 'seq1 is the target; seq2 is the query. Select columns for both the table and CSV download. You can hide table columns after computation without changing the CSV.', 'input-help')); +const selectionTools = element('div', '', 'selection-tools'); outputBody.append(selectionTools); +const csvPicker = element('div', '', 'checkbox-grid'); outputBody.append(csvPicker); +for (const column of [...CSV_COLUMNS.split(',').map((key) => CSV_CATALOG.find((column) => column.key === key)), ...CSV_CATALOG.filter((column) => !CSV_COLUMNS.split(',').includes(column.key))]) csvInputs.push(checkbox(csvPicker, column.key, `${column.key} — ${column.description}`, column.description)); +for (const [name, selection] of [['Select all', CSV_CATALOG.map((c) => c.key)], ['CLI defaults', CSV_COLUMNS.split(',')], ['Clear selection', []]]) { + const button = element('button', name); button.type = 'button'; + button.addEventListener('click', () => { for (const input of csvInputs) input.checked = selection.includes(input.value); clearResults(); }); selectionTools.append(button); +} +outputBody.append(element('h3', 'Additional output files', 'subheading')); +outputBody.append(element('p', 'Reports are computed with the same settings. ASCII, detailed and ensemble reports each run a fresh engine instance and can take extra time.', 'input-help')); +const outputPicker = element('div', '', 'checkbox-grid'); outputBody.append(outputPicker); +for (const output of EXTRA_OUTPUTS) outputInputs.push(checkbox(outputPicker, output.key, output.label)); +const generalBody = group('general-options', 'General'); +generalBody.append(element('p', 'Tasks run locally in one worker. Abort or Escape stops computation immediately. Process-level settings (threads, log paths, parameter-file paths and terminal help) are available in the CLI.')); +const docs = element('a', 'Full CLI parameter documentation'); docs.href = 'https://github.com/BackofenLab/IntaRNA#readme'; generalBody.append(docs); +// Use explicit grouping as a fallback for browsers without details[name]. +for (const details of document.querySelectorAll('.parameter-group')) { + details.addEventListener('toggle', () => { if (details.open) for (const other of document.querySelectorAll('.parameter-group')) if (other !== details) other.open = false; }); +} function clearResults(message = 'Inputs changed. Compute to update the interactions.') { - results.clear(); - byId('diagnostics').hidden = true; - byId('diagnostic-text').textContent = ''; - status.textContent = message; + results.clear(); byId('output-files').hidden = true; byId('file-list').replaceChildren(); + byId('diagnostics').hidden = true; byId('diagnostic-text').textContent = ''; status.textContent = message; } - function updateLength(name) { try { - const rna = parseSequence(byId(name).value, name === 'query' ? 'Query' : 'Target'); - byId(`${name}-length`).textContent = `${rna.sequence.length.toLocaleString()} nucleotides`; - } catch { - byId(`${name}-length`).textContent = 'Check sequence format and length'; - } + const parsed = parseFasta(byId(name).value, name === 'query' ? 'Query' : 'Target', byId('task').value === 'prediction'); + byId(`${name}-length`).textContent = `${parsed.records.length} record${parsed.records.length === 1 ? '' : 's'}, ${parsed.records.reduce((sum, r) => sum + r.sequence.length, 0).toLocaleString()} nucleotides`; + } catch { byId(`${name}-length`).textContent = 'Check sequence format and length'; } } - -function setBusy(busy) { - byId('inputs').disabled = busy; - byId('results').setAttribute('aria-busy', String(busy)); - if (busy) dialog.showModal(); - else { - dialog.close(); - byId('compute').focus(); +function updateAvailability() { + const evaluation = byId('task').value === 'evaluation'; + byId('evaluation-input').hidden = !evaluation; + byId('task-help').textContent = evaluation ? 'Evaluate supplied structures for one query and one target. Prediction constraints are ignored.' : 'Predict interactions for all query–target sequence pairs.'; + byId('compute').textContent = evaluation ? 'Evaluate interactions' : 'Compute interactions'; + for (const definition of GROUPS) { + const details = byId(definition.key); details.hidden = evaluation && !!definition.prediction; + for (const field of definition.fields) { + const input = byId(field.key); + input.disabled = evaluation && !!field.prediction || !evaluation && definition.key === 'helix-options' && byId('model').value !== 'B' || field.key === 'kineticScore' && byId('mode').value !== 'K'; + } } + for (const input of outputInputs) { input.disabled = evaluation && EXTRA_OUTPUTS.find((entry) => entry.key === input.value).prediction; if (input.disabled) input.checked = false; } + updateLength('query'); updateLength('target'); } - -for (const name of ['query', 'target', 'count']) { - byId(name).addEventListener('input', () => { - if (name !== 'count') updateLength(name); - errorBox.hidden = true; - clearResults(); - }); +function resetParameters() { + for (const field of FIELDS) { const input = byId(field.key); if (field.type === 'checkbox') input.checked = field.value; else input.value = field.value; } + for (const input of csvInputs) input.checked = CSV_COLUMNS.split(',').includes(input.value); + for (const input of outputInputs) input.checked = false; + byId('qIdxPos0').value = 1; byId('tIdxPos0').value = 1; byId('rri').value = EVALUATION_EXAMPLE; + updateAvailability(); errorBox.hidden = true; clearResults('Parameters reset to CLI defaults. Compute to update the interactions.'); } +byId('task').addEventListener('change', resetParameters); +byId('reset').addEventListener('click', resetParameters); +form.addEventListener('input', (event) => { + if (event.target.type === 'file') return; + updateAvailability(); errorBox.hidden = true; clearResults(); +}); byId('example').addEventListener('click', () => { - for (const name of ['query', 'target', 'count']) byId(name).value = DEFAULTS[name]; - updateLength('query'); - updateLength('target'); - errorBox.hidden = true; - clearResults('Example loaded. Compute to predict its interactions.'); + byId('query').value = DEFAULTS.query; byId('target').value = DEFAULTS.target; + resetParameters(); byId('sequence-input').open = true; clearResults('Example loaded. Compute to analyze its interactions.'); }); byId('abort').addEventListener('click', () => runner.abort()); dialog.addEventListener('cancel', (event) => { event.preventDefault(); runner.abort(); }); window.addEventListener('pagehide', () => runner.abort()); - +function showFiles(files) { + for (const file of files) { + const details = element('details'), summary = element('summary', file.name), preview = element('pre', file.text.slice(0, 100000)); + const download = element('button', 'Download file', 'secondary'); download.type = 'button'; download.addEventListener('click', () => downloadText(file.name, file.text)); + details.append(summary, download, preview); + if (file.text.length > 100000) details.append(element('p', 'Preview limited to 100,000 characters. Download contains the complete file.')); + byId('file-list').append(details); + } + byId('output-files').hidden = !files.length; +} form.addEventListener('submit', async (event) => { event.preventDefault(); - const request = { query: byId('query').value, target: byId('target').value, count: Number(byId('count').value) }; + const request = { + task: byId('task').value, query: byId('query').value, target: byId('target').value, + qIdxPos0: byId('qIdxPos0').value, tIdxPos0: byId('tIdxPos0').value, rri: byId('rri').value, + parameters: Object.fromEntries(FIELDS.map((field) => [field.key, field.type === 'checkbox' ? byId(field.key).checked : byId(field.key).value])), + csvColumns: csvInputs.filter((input) => input.checked).map((input) => input.value), outputs: outputInputs.filter((input) => input.checked && !input.disabled).map((input) => input.value), + }; errorBox.hidden = true; - try { - prepareRequest(request); - } catch (error) { - errorBox.textContent = error.message; - errorBox.hidden = false; - return; - } - clearResults('Computing interactions…'); - byId('loading-status').textContent = 'Loading the prediction engine…'; - setBusy(true); + try { prepareRequest(request); } catch (error) { errorBox.textContent = error.message; errorBox.hidden = false; return; } + clearResults('Computing interactions…'); byId('loading-status').textContent = 'Loading the prediction engine…'; + byId('inputs').disabled = true; byId('results').setAttribute('aria-busy', 'true'); dialog.showModal(); try { const result = await runner.run(request, (message) => { byId('loading-status').textContent = message; }); - const count = results.show(result.csv); - status.textContent = count ? `Prediction complete: ${count} interaction${count === 1 ? '' : 's'}.` : 'No favorable interactions found with these sequences and the default settings.'; - byId('diagnostic-text').textContent = result.diagnostics; - byId('diagnostics').hidden = !result.diagnostics; + const count = results.show(result.csv, result.csvColumns); + status.textContent = count ? `${request.task === 'evaluation' ? 'Evaluation' : 'Prediction'} complete: ${count} interaction${count === 1 ? '' : 's'}.` : request.task === 'prediction' && Number(request.parameters.outNumber) === 0 ? 'Accessibility computation complete. No interaction rows requested.' : 'No favorable interactions found with these sequences and settings.'; + showFiles(result.files); byId('diagnostic-text').textContent = result.diagnostics; byId('diagnostics').hidden = !result.diagnostics; } catch (error) { if (error.name === 'AbortError') status.textContent = 'Computation aborted. You can edit the sequences and try again.'; - else { - status.textContent = 'Prediction failed.'; - errorBox.textContent = error.message; - errorBox.hidden = false; - byId('diagnostic-text').textContent = error.diagnostics || ''; - byId('diagnostics').hidden = !error.diagnostics; - } + else { status.textContent = 'Computation failed.'; errorBox.textContent = error.message; errorBox.hidden = false; byId('diagnostic-text').textContent = error.diagnostics || ''; byId('diagnostics').hidden = !error.diagnostics; } } finally { - setBusy(false); + byId('inputs').disabled = false; updateAvailability(); byId('results').setAttribute('aria-busy', 'false'); dialog.close(); byId('compute').focus(); } }); - -if (!globalThis.Worker || !globalThis.WebAssembly) { - byId('compute').disabled = true; - errorBox.textContent = 'This browser needs WebAssembly and Web Workers. Use a current browser or the command line version.'; - errorBox.hidden = false; -} +resetParameters(); +fetch(new URL('./build-info.json', import.meta.url)).then((response) => { + if (!response.ok) throw new Error('Missing build provenance'); return response.json(); +}).then((info) => { byId('version-info').textContent = `IntaRNA ${info.intarna} (${info.commit.slice(0, 12)}${info.dirty ? ', modified sources' : ''}) · ViennaRNA ${info.viennarna} · Emscripten ${info.emscripten}`; }).catch(() => { byId('version-info').textContent = 'Build versions unavailable. Serve the staged interface with its compiled engine and build-info files.'; }); +if (!globalThis.Worker || !globalThis.WebAssembly) { byId('compute').disabled = true; errorBox.textContent = 'This browser needs WebAssembly and Web Workers. Use a current browser or the command line version.'; errorBox.hidden = false; } diff --git a/webinterface/assets/README.md b/webinterface/assets/README.md new file mode 100644 index 00000000..6ede01c4 --- /dev/null +++ b/webinterface/assets/README.md @@ -0,0 +1,12 @@ +# Institutional header assets + +The header follows [vaRRI main-v2](https://github.com/BackofenLab/vaRRI/blob/main-v2/src/ui/components/site-header.js). +The original institutional images were downloaded on 2026-10-21 from: + +- https://www.bioinf.uni-freiburg.de/assets/images/bioinf-fr-logo-blau.png +- https://www.bioinf.uni-freiburg.de/assets/images/logo-uni-freiburg.png + +These are institutional marks of the Bioinformatics Group Freiburg and the +University of Freiburg. They identify the affiliated research group/university; +they are not covered by the IntaRNA software license. Keep their proportions and +attribution links. Local copies avoid third-party requests from the interface. diff --git a/webinterface/assets/bioinf-fr-logo-blau.png b/webinterface/assets/bioinf-fr-logo-blau.png new file mode 100644 index 0000000000000000000000000000000000000000..0d8409ba90153521d0ce2da58ab040b2d7777e7a GIT binary patch literal 187836 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 =i`=D|1+{79w6b?(tBC{ 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
  • 9-hb_n>HI@_y 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�T{at+zQAKHtO9V|ISAKtra7I|;mhbG&dfYH(0k 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 literal 0 HcmV?d00001 diff --git a/webinterface/assets/logo-uni-freiburg.png b/webinterface/assets/logo-uni-freiburg.png new file mode 100644 index 0000000000000000000000000000000000000000..1e2bb249218cd8ad4e57b79c0da035708dc86651 GIT binary patch literal 24542 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 literal 0 HcmV?d00001 diff --git a/webinterface/columns.js b/webinterface/columns.js new file mode 100644 index 00000000..6b42b93a --- /dev/null +++ b/webinterface/columns.js @@ -0,0 +1,315 @@ +// Column IDs and descriptions from OutputHandlerCsv.h; seq1 = target, seq2 = query. +export const CSV_CATALOG = [ + { + "key": "id1", + "label": "Target", + "description": "id of first sequence", + "numeric": false + }, + { + "key": "id2", + "label": "Query", + "description": "id of second sequence", + "numeric": false + }, + { + "key": "seq1", + "label": "seq1", + "description": "full first sequence", + "numeric": false + }, + { + "key": "seq2", + "label": "seq2", + "description": "full second sequence", + "numeric": false + }, + { + "key": "subseq1", + "label": "subseq1", + "description": "interacting subsequence of first sequence", + "numeric": false + }, + { + "key": "subseq2", + "label": "subseq2", + "description": "interacting subsequence of second sequence", + "numeric": false + }, + { + "key": "subseqDP", + "label": "subseqDP", + "description": "hybrid subsequences compatible with hybridDP", + "numeric": false + }, + { + "key": "subseqDB", + "label": "subseqDB", + "description": "hybrid subsequences compatible with hybridDB", + "numeric": false + }, + { + "key": "start1", + "label": "Target start", + "description": "start index of hybrid in seq1", + "numeric": true + }, + { + "key": "end1", + "label": "Target end", + "description": "end index of hybrid in seq1", + "numeric": true + }, + { + "key": "start2", + "label": "Query start", + "description": "start index of hybrid in seq2", + "numeric": true + }, + { + "key": "end2", + "label": "Query end", + "description": "end index of hybrid in seq2", + "numeric": true + }, + { + "key": "hybridDP", + "label": "hybridDP", + "description": "hybrid in VRNA dot-bracket notation", + "numeric": false + }, + { + "key": "hybridDB", + "label": "hybridDB", + "description": "hybrid in dot-bar notation", + "numeric": false + }, + { + "key": "hybridDPfull", + "label": "hybridDPfull", + "description": "hybrid in VRNA dot-bracket notation for full sequence lengths", + "numeric": false + }, + { + "key": "hybridDBfull", + "label": "hybridDBfull", + "description": "hybrid in dot-bar notation for full sequence lengths", + "numeric": false + }, + { + "key": "bpList", + "label": "bpList", + "description": "list of hybrid base pairs like (1,3),(4,2),...", + "numeric": false + }, + { + "key": "E", + "label": "Energy (kcal/mol)", + "description": "overall interaction energy", + "numeric": true + }, + { + "key": "Etotal", + "label": "Etotal", + "description": "overall energy of the interaction including the intra-molecular ensemble energies (E+Eall1+Eall2)", + "numeric": true + }, + { + "key": "ED1", + "label": "ED1", + "description": "ED value of seq1", + "numeric": true + }, + { + "key": "ED2", + "label": "ED2", + "description": "ED value of seq2", + "numeric": true + }, + { + "key": "Pu1", + "label": "Pu1", + "description": "probability to be accessible for seq1", + "numeric": true + }, + { + "key": "Pu2", + "label": "Pu2", + "description": "probability to be accessible for seq2", + "numeric": true + }, + { + "key": "E_init", + "label": "E_init", + "description": "initiation energy", + "numeric": true + }, + { + "key": "E_loops", + "label": "E_loops", + "description": "sum of loop energies (excluding E_init)", + "numeric": true + }, + { + "key": "E_dangleL", + "label": "E_dangleL", + "description": "dangling end contribution of base pair (start1,end2)", + "numeric": true + }, + { + "key": "E_dangleR", + "label": "E_dangleR", + "description": "dangling end contribution of base pair (end1,start2)", + "numeric": true + }, + { + "key": "E_endL", + "label": "E_endL", + "description": "penalty of closing base pair (start1,end2)", + "numeric": true + }, + { + "key": "E_endR", + "label": "E_endR", + "description": "penalty of closing base pair (end1,start2)", + "numeric": true + }, + { + "key": "E_hybrid", + "label": "E_hybrid", + "description": "energy of hybridization only = E - ED1 - ED2", + "numeric": true + }, + { + "key": "E_norm", + "label": "E_norm", + "description": "length normalized energy = E/ln(length(seq1)*length(seq2))", + "numeric": true + }, + { + "key": "E_hybridNorm", + "label": "E_hybridNorm", + "description": "length normalized energy of hybridization only = E_hybrid / ln(length(seq1)*length(seq2))", + "numeric": true + }, + { + "key": "E_add", + "label": "E_add", + "description": "user provided energy shift", + "numeric": true + }, + { + "key": "seedStart1", + "label": "seedStart1", + "description": "start index of the seed in seq1", + "numeric": true + }, + { + "key": "seedEnd1", + "label": "seedEnd1", + "description": "end index of the seed in seq1", + "numeric": true + }, + { + "key": "seedStart2", + "label": "seedStart2", + "description": "start index of the seed in seq2", + "numeric": true + }, + { + "key": "seedEnd2", + "label": "seedEnd2", + "description": "end index of the seed in seq2", + "numeric": true + }, + { + "key": "seedE", + "label": "seedE", + "description": "overall energy of the seed only (including accessibility etc)", + "numeric": true + }, + { + "key": "seedED1", + "label": "seedED1", + "description": "ED value of seq1 of the seed only (excluding rest)", + "numeric": true + }, + { + "key": "seedED2", + "label": "seedED2", + "description": "ED value of seq2 of the seed only (excluding rest)", + "numeric": true + }, + { + "key": "seedPu1", + "label": "seedPu1", + "description": "probability of seed region to be accessible for seq1", + "numeric": true + }, + { + "key": "seedPu2", + "label": "seedPu2", + "description": "probability of seed region to be accessible for seq2", + "numeric": true + }, + { + "key": "w", + "label": "w", + "description": "Boltzmann weight of the interaction energy", + "numeric": true + }, + { + "key": "Eall", + "label": "Eall", + "description": "ensemble energy of all interactions (outConstraint.needZall)", + "numeric": true + }, + { + "key": "Eall1", + "label": "Eall1", + "description": "ensemble energy of all intra-molecular structures of seq1", + "numeric": true + }, + { + "key": "Eall2", + "label": "Eall2", + "description": "ensemble energy of all intra-molecular structures of seq2", + "numeric": true + }, + { + "key": "Zall", + "label": "Zall", + "description": "partition function of all interactions (outConstraint.needZall)", + "numeric": true + }, + { + "key": "Zall1", + "label": "Zall1", + "description": "partition function of all intra-molecular structures of seq1", + "numeric": true + }, + { + "key": "Zall2", + "label": "Zall2", + "description": "partition function of all intra-molecular structures of seq2", + "numeric": true + }, + { + "key": "EallTotal", + "label": "EallTotal", + "description": "total ensemble energy (Eall+Eall1+Eall2) of all interactions including the intra-molecular ensemble energies (outConstraint.needZall)", + "numeric": true + }, + { + "key": "P_E", + "label": "P_E", + "description": "probability of mfe within interaction ensemble (outConstraint.needZall)", + "numeric": true + }, + { + "key": "RT", + "label": "RT", + "description": "the scaled temperature used for Boltzmann weight computation", + "numeric": true + } +]; diff --git a/webinterface/docs/step1-wasm-compilation.md b/webinterface/docs/step1-wasm-compilation.md index ffa6d170..4fae4a7c 100644 --- a/webinterface/docs/step1-wasm-compilation.md +++ b/webinterface/docs/step1-wasm-compilation.md @@ -14,6 +14,7 @@ Activate Emscripten 4.0.23 with `source /path/to/emsdk/emsdk_env.sh`, then run: ```sh bash scripts/wasm/build.sh node scripts/wasm/smoke.mjs +node scripts/wasm/smoke-protocol.mjs ``` `WASM_BUILD_DIR` changes the build directory; `JOBS` defaults to 2. Source @@ -74,3 +75,18 @@ WASM from the same build together. The Node smoke test runs real Turner/default-accessibility predictions and checks energy, coordinates, nonzero accessibility costs, suboptimal counts and C++ error handling against the native CLI. + +## Compiled version information and virtual overrides (PR #287 review) + +After copying the paired JS/WASM assets, `write-build-info.mjs` invokes their +actual `--version` entry point. It records IntaRNA package version, ViennaRNA and +Boost versions, source repository/revision and dirty status, Emscripten version, +zlib version and asset hashes in `build-info.txt` and `build-info.json`. The +interface displays those versions and links both files. Staging checks the +hashes and preserves provenance even if the staging checkout has moved. + +IntaRNA's inherited virtual methods and destructors declare `override`, including +previously unannotated energy/accessibility, seed/helix, predictor and output +families. The Emscripten build treats inconsistent missing overrides as errors. +Vendored Easylogging++ remains unchanged. This annotation-only C++ change does +not alter algorithms, virtual extension points or scientific defaults. diff --git a/webinterface/docs/step2-worker-architecture.md b/webinterface/docs/step2-worker-architecture.md index da5a8a60..8a2f8090 100644 --- a/webinterface/docs/step2-worker-architecture.md +++ b/webinterface/docs/step2-worker-architecture.md @@ -1,25 +1,31 @@ -# Step 2: worker and I/O architecture (#286) +# Step 2: worker and I/O architecture (#286, PR #287 review) -`PredictionRunner` creates an ES module Web Worker on demand. The main thread -sends `{query, target, count}` as text and receives `status`, `result` or `error` -messages. `protocol.js` validates both the UI and worker boundary and constructs -fixed CLI options; users cannot inject CLI arguments or file paths. +`PredictionRunner` creates one module Web Worker per task and sends sequence +text, task, start indices, validated parameter values, CSV column IDs, optional +output IDs and evaluation structures. It receives status, result or error +messages. `prepareRequest` validates both the UI and worker boundary; argument +construction only accepts schema keys, enumerated output formats and fixed paths. -The worker lazily imports `wasm/intarna.js`, awaits `createIntaRNA` (mapping `locateFile` to the staged lowercase WASM name), writes -normalized single-record FASTA to `/query.fa` and `/target.fa` in MEMFS, and calls -`callMain` once. It selects semicolon-delimited CSV output in `/results.csv` and -reads that file after main returns and the C++ output streams close. Logs are -separate from CSV, bounded to the most recent 25 lines of 2,000 characters. -Nonzero exit codes, missing assets, allocation failures and JS errors reject -the prediction. Warnings travel with successful results as diagnostics. +The worker imports `wasm/intarna.js`, maps `locateFile` to the lowercase WASM +asset and creates an isolated `createIntaRNA` instance. It writes normalized +multi-FASTA to `/query.fa` and `/target.fa`; local SHAPE/accessibility text uses +fixed `q/tShape.txt` or `q/tAccFile.txt` paths. CSV is written to `/results.csv`; +additional native output prefixes write files under `/output/`. Multi-FASTA +filenames use the CLI's native record-number suffixes. -Every computation gets fresh C++ globals, RNAlib state and files. Both success -and failure terminate the worker. Abort uses `Worker.terminate()` from the main -thread, so it works during download, initialization and synchronous C++ execution. -Posting an abort message to a busy worker would not interrupt C++. Cancellation -rejects with `AbortError`, releases references and ignores queued stale messages; -the next computation creates a fresh worker. No shared memory, cross-origin -isolation, server, sequence upload or persistent sequence storage is needed. +Main is called once per WASM instance, keeping C++ globals, RNAlib state and files +isolated. Optional ASCII/detailed/ensemble reports each use another fresh +instance with the same inputs/settings. This costs an additional computation per +report and is explained next to those controls. Files are read only after main +returns and streams close. The worker sends CSV, selected columns, additional +file text and bounded diagnostics (25 lines, 2,000 characters each). Nonzero +exits and JS/runtime failures settle as errors, without partial successful files. -Run protocol and lifecycle tests with `cd webinterface && npm test`. Browser -integration tests exercise the real worker and WASM assets after staging. +Both success and failure terminate the worker. Abort uses `Worker.terminate()` +on the main thread and immediately rejects with `AbortError`, including during +synchronous C++ execution. Stale messages are ignored and the next task starts +fresh. Inputs are never uploaded or persisted. No backend, shared memory or +cross-origin isolation is needed. A single WASM instance can grow up to 1 GiB. + +Run `npm test --prefix webinterface`, then the native-reference WASM smoke tests +and Chromium/Firefox suites described in step 4. diff --git a/webinterface/docs/step3-frontend-ui.md b/webinterface/docs/step3-frontend-ui.md index 00ff2941..49289403 100644 --- a/webinterface/docs/step3-frontend-ui.md +++ b/webinterface/docs/step3-frontend-ui.md @@ -1,50 +1,77 @@ -# Step 3: sequence input interface (#286) +# Step 3: browser controls (#286, PR #287 review) -## Design +## Design and layout -The interface is a small scientific workspace: two equally weighted strand -editors above one results region. Query uses teal (`#087f83`), target uses blue -(`#315fb8`), with navy text (`#17324d`), pale blue background (`#f1f7fa`), white -working surfaces and rust (`#aa4b1d`) for errors. Trebuchet/Segoe UI system fonts -keep the page self-contained; only nucleotide input and structure data use -monospace. The paired-strand mark is the single visual motif. All working text -is left aligned. A generic dashboard with metric cards was rejected because -sequence comparison, input direction and coordinate interpretation matter here. +The interface follows [vaRRI main-v2](https://github.com/BackofenLab/vaRRI/tree/main-v2): +header indigo `#344a9a`, action blue `#145a96`, dark blue `#0f4676`, pale blue +`#eaf2f8`, white `#ffffff` and focus gold `#f4bb44`. Segoe UI/Tahoma/Geneva serve +labels and body text; Consolas/Liberation Mono serve sequence and structure data. +The official group and university logos share the header with the IntaRNA mark. +Their local copies and source attribution are in `webinterface/assets/`. + +The layout puts the scientific task first, then mutually exclusive parameter +groups, followed by the compute button, results, additional files and versions. +All working content is left aligned; two equally sized strand editors collapse +into one column on mobile. The shared institutional header is the strongest +visual element. Parameter borders and blue summaries identify functional groups. ```text -IntaRNA Documentation / Source -RNA–RNA interaction prediction Runs on this device -[ Query 5′ → 3′ ][ Target 5′ → 3′ ] -[ FASTA editor ][ FASTA editor ] -Maximum results / Load example Compute -Results / search / export -[ sortable interaction coordinates and energy ] +Group logo IntaRNA / RNA–RNA interaction analysis University logo +RNA–RNA interactions Sequences stay on your device +Task: Prediction / Evaluation Load example +[ Sequence input: query + target, start indices, evaluation structures ] +[ Query ] [ Target ] [ Seed ] [ Helix ] [ SHAPE ] [ Interaction/energy ] +[ Output: filters, CSV columns, additional files ] [ General ] +Reset parameters Compute +Interactions / search / show-hide columns / CSV download +Additional output files / diagnostics / compiled versions ``` -## Input and state - -`protocol.js` accepts one FASTA record or one plain sequence per input. Wrapped -lines, lowercase and DNA T are normalized to uppercase RNA U. The only allowed -nucleotides are A/C/G/U/T/N; N does not match a complementary base. Empty names, -multiple/misplaced headers, invalid symbols and lengths outside 7–2,000 nt are -rejected. Query length × target length is limited to 1,000,000 for browser -memory. The result count is an integer from 1 to 100. These limits are also -enforced in the worker and do not change the native CLI. - -The example is a short complementary pair with a seven-base seed. Predictions -use the native defaults (heuristic prediction, Turner energy model, accessibility -included, 37 °C, seed length 7) and allow overlapping suboptimal interactions. -All sequences are entered 5′ to 3′; the CLI handles reverse query accessibility. - -The loading dialog reports initialization versus calculation without inventing -percentages. Abort and Escape terminate the worker and restore the form. Inputs -are disabled during a prediction; the results are cleared when inputs change or -a new prediction begins. Errors and empty output have explicit messages. -Diagnostics are available separately from CSV. Focus returns to Compute after -the dialog closes. There are visible keyboard focus styles, associated labels, -live status/error regions, reduced-motion support and a single-column mobile -layout. Browser tests cover validation, real computation, abort/restart, worker -load failure, responsive overflow and result interactions. - -Serve the staged directory over HTTP; module workers cannot load through -`file://`. No frontend bundler, external font, CDN or API service is needed. +Every CLI group has a menu. `details[name]` plus a toggle fallback keep at most +one parameter menu open. Sequence input starts open. Prediction-only controls +are disabled/hidden during evaluation. Changing task resets parameters to their +CLI defaults while preserving sequence text; Load example also restores the +complementary pair. The native default output count is one; browser users can +request 0–100 interactions per pair. Zero enables accessibility-only output. + +## Inputs, defaults and validation + +`options.js` defines labels, types, defaults and ranges from +`src/bin/CommandLineParsing.cpp`. `protocol.js` validates them on both sides of +the worker boundary. Query/target retain their distinct interior-loop defaults +(16 and 10). Meta-options such as `acc`, `accW`, `accL`, `intLenMax` and +`intLoopMax` are represented by per-strand controls to avoid conflicting values. +FASTA headers supply sequence names instead of `qId`/`tId`. `outCsvSort` selects a native download sort column; table sorting is independent. +CSV uses a fixed semicolon delimiter. +Process settings (threads, file/log paths, parameter files, help/version commands) +are replaced by the worker lifecycle, local data inputs, downloads and displayed +compiled versions. The base-pair maximization energy model is intentionally +excluded. The helix-block interaction model B is supported. + +Prediction accepts plain sequence or up to 20 FASTA records per strand. +Evaluation requires one query and target and 1–100 predefined structures. +Wrapped/lowercase sequence text and DNA T normalize to uppercase RNA U. A/C/G/U/N +are accepted. Each record is 2–2,000 nt; seed predictions additionally require +at least `seedBP` nucleotides. FASTA IDs end at the first whitespace, matching the CLI. Duplicate IDs (including collisions after +native CSV semicolon replacement) are rejected. The total selected all-vs-all +query length × target length, or sum of pairwise products, is capped at 1,000,000. +The per-strand start index applies to all records and defaults to one. Negative +indices skip zero, matching `RnaSequence`; output coordinates remain inclusive. + +Evaluation encodes target then query in 5′ to 3′ dot-bar notation, such as +`1|||&1|||`, separated by colons. Bars pair antiparallel. Validation checks bounds, +equal nonzero pair counts and AU/GC/GU complementarity. Evaluation retains energy, +accessibility and SHAPE controls while excluding prediction/seed/helix/region +and output-filter constraints, matching the native personality. + +Validation also covers numeric ranges, accessibility windows/spans, selected +FASTA subsets, regions, structural constraints, seed/helix compatibility, +probability-output requirements and window restrictions. SHAPE and RNAplfold +accessibility text can be pasted or loaded from a local file (up to 1 MB). Data +are written to fixed MEMFS paths, never used as user-provided CLI arguments. +Native diagnostics remain available for model-specific failures. + +Inputs/results clear on edits; the form disables during execution. Abort and +Escape terminate the worker during startup or C++ execution. Status, errors, +empty output and diagnostic text have dedicated regions. Keyboard focus, +reduced motion and mobile overflow are covered by real browser tests. diff --git a/webinterface/docs/step4-results-and-deployment.md b/webinterface/docs/step4-results-and-deployment.md index 1ba086d1..2c8ffe99 100644 --- a/webinterface/docs/step4-results-and-deployment.md +++ b/webinterface/docs/step4-results-and-deployment.md @@ -2,31 +2,37 @@ ## Results contract -The worker requests exactly `id1,start1,end1,id2,start2,end2,E`. IntaRNA's native -CSV is unquoted and semicolon-delimited; its output handler replaces semicolons -inside sequence identifiers with underscores. The browser parser treats quotes -as literal ID characters, checks the header, validates finite numeric values -and positive inclusive coordinates, and rejects malformed rows. Header control -characters/multirecord input are rejected before the CLI runs. Empty CSV or a -header-only result is a successful prediction with no favorable interactions. - -Sequence 1 is **target** and sequence 2 is **query**. The table deliberately -shows query first, with each strand's original 1-based inclusive coordinates. -Energy uses kcal/mol. ΔE is derived as `E - min(reported E)` rounded to hundredths; -it is not a probability. Suboptimal interactions have distinct site boundaries -and may overlap. Scientific prediction defaults are unchanged. - -Every heading is a keyboard-operable sort button with `aria-sort`. Numerical -columns compare numbers, IDs compare text, and the search matches all columns -without changing the raw prediction. All values are rendered through -`textContent`. CSV export returns the original native output, including all -reported rows regardless of filtering. Input edits clear the old result. +The CSV checklist covers all `OutputHandlerCsv` column IDs. The worker requests +those columns plus internal target/query IDs, coordinates and energy needed for +pair grouping and result validation. Downloads project the untouched native CSV +cells onto the user's selected columns, in selection order, retaining every row +regardless of table filtering/visibility. No computed mfe difference is inserted +into native CSV. The selected-column catalog is checked against the C++ mapping. + +Native output is unquoted and semicolon-delimited. Semicolons in IDs become +underscores; quotes remain literal. The parser checks column IDs, finite core +energy/coordinate values, integer inclusive coordinates and ascending bounds. +Signed coordinates are allowed. Optional `NAN` and colon-separated seed lists +remain strings; numeric sorting puts unavailable/list values after numbers. + +Sequence 1 is **target**, sequence 2 is **query**. Energy uses kcal/mol. The +computed **mfe Δ** is `E - min(reported E for the same query–target pair)`, rounded +to hundredths. Evaluation uses the best supplied structure as its reference. +This is relative suboptimality, not a free-energy change or probability. + +Every heading is a keyboard-operable sort button with `aria-sort`. Search covers +all returned values. A separate checklist shows/hides table columns after the +run; it does not change selected CSV output. All identifiers, output previews +and diagnostics render with `textContent`. Selected extra files appear below the +table with bounded text previews and complete downloads, including SVG source. +Empty output is successful and still permits CSV/accessibility downloads. ## Packaging and deployment ```sh bash scripts/wasm/build.sh node scripts/wasm/smoke.mjs +node scripts/wasm/smoke-protocol.mjs bash scripts/wasm/stage.sh npm test --prefix webinterface python3 -m pip install -r scripts/wasm/requirements-browser.txt @@ -38,7 +44,11 @@ python3 scripts/wasm/test-browser.py http://127.0.0.1:8765 --browser firefox ``` The staging script includes matched WASM assets, only the application source -files needed at runtime, source commit/version metadata and dependency notices. +files needed at runtime, compiled version metadata and dependency notices. Build-time `build-info.json` +and `build-info.txt` record actual CLI IntaRNA/ViennaRNA/Boost versions, source +commit and tracked modifications, SDK version and asset SHA-256 digests. Staging +checks these digests and preserves the original metadata; it never replaces the +compiled revision with a later staging checkout. The footer displays versions. It needs the same activated SDK and dependency build directory as the build. No compiled artifacts are committed. HTTP(S), module workers and WebAssembly are required; `file://` and legacy browsers are unsupported. No backend, @@ -47,8 +57,8 @@ SharedArrayBuffer, credentials, CDN or cross-origin headers are needed. The existing repository already publishes its user guide and API with Pages. Publishing a second independent artifact would overwrite them. Therefore `documentation.yml` is now a reusable build (also manually dispatchable), and -`build-wasm.yml` is the **single publisher**. Both builds run on PRs and on the -actual default branch, `master`. After JS tests, the real WASM smoke test and +`build-wasm.yml` is the **single publisher**. Both builds run on PRs (including dependent feature-branch PRs) and on +the actual default branch, `master`. After JS tests, the real WASM smoke test and Chromium/Firefox integration tests pass, the package job combines: - Existing Jekyll guide at `/IntaRNA/` and API at `/IntaRNA/api/`. @@ -98,3 +108,32 @@ and active C++, clean restart, empty predictions and asset-load recovery. GitHub's platform checks independently cover GCC 14 release/debug and Apple Clang. Deployment itself occurs only after merge; local browser tests do not claim a live Pages deployment. + +### PR #287 review verification, 2026-10-21 + +- Emscripten 4.0.23 rebuild using the pinned official dependency archives passes + with `-Werror=inconsistent-missing-override`. An all-public-header audit using + `-Wsuggest-override -Wsuggest-destructor-override` reports no project override + diagnostics (third-party Easylogging++ is intentionally unchanged). +- `node scripts/wasm/smoke.mjs` retains the original native-reference energy, + coordinates, structured accessibility and C++ error-handling checks. +- `node scripts/wasm/smoke-protocol.mjs` passes real CLI translations for default + prediction, shifted negative coordinates, all-vs-all/pairwise multi-FASTA, + evaluation with all CSV columns, extra reports/probability SVGs, imported + accessibility and non-default energy/seed/helix/SHAPE/overlap settings. + Native imported accessibility reserves one length column for dangling-end + treatment; its energy is compared with the corresponding restricted native + interaction length rather than the unrestricted prediction. +- `npm test --prefix webinterface`: ten protocol/results/lifecycle groups pass. +- Real Chromium/Firefox integration covers all parameter groups, CLI defaults, + compiled versions/logos, task changes, multiple records, signed coordinates, + selected/visible CSV columns, additional previews/downloads and the original + cancellation, empty/error, literal-ID and mobile checks. Desktop screenshot + reviewed; the 390 px viewport has no document overflow. +- Native OpenMP release build with GCC 16.2.0/ViennaRNA 2.7.2: + `make tests -j3` passes all eight groups, including 462,689 assertions across + 119 API cases. Tracked CLI fixtures are linked into the separate build data + directory as in the previous verification; expected results are unchanged. +- Staging preserves compiled metadata byte-for-byte; a deliberately changed + copied JS engine is rejected by its asset digest check. Shell syntax, + workflow YAML, Python syntax and `git diff --check` pass. diff --git a/webinterface/index.html b/webinterface/index.html index b4858cfb..318c101f 100644 --- a/webinterface/index.html +++ b/webinterface/index.html @@ -3,9 +3,9 @@ - + - IntaRNA · RNA–RNA interaction prediction + IntaRNA · RNA–RNA interactions @@ -13,70 +13,60 @@
    -

    RNA–RNA interaction prediction

    -

    Explore how two RNA sequences interact, with accessibility and seed pairing included.

    -
    -

    Sequences stay on your device

    +

    RNA–RNA interactions

    Predict interactions or evaluate the energy of structures you provide.

    +

    Sequences stay on your device

    -
    +
    - Sequences and prediction settings -
    -
    -
    5′ to 3′
    - -

    10 nucleotides

    -
    -
    -
    5′ to 3′
    - -

    10 nucleotides

    -
    -
    -

    One FASTA record or plain sequence per field. A, C, G, U/T and N; 7–2,000 nucleotides each, with query length × target length ≤ 1,000,000.

    -
    -
    - +
    1 record, 10 nucleotides
    + +
    +

    Plain sequence or multi-FASTA (prediction only), A/C/G/U/T/N. Up to 20 records per field and 2–2,000 nt per record; total query × target length across selected pairs ≤ 1,000,000. Seed predictions also require at least as many nucleotides as seed base pairs. Signed start indices apply to every record on that strand; negative indexing skips zero.

    + + + +
    -
    Prediction settings

    37 °C · Turner energy model · accessibility included · 7-base seed · heuristic prediction · overlapping suboptimals

    Interactions

    Enter your sequences or try the example, then compute.

    - + +
    -

    Positions are 1-based and inclusive. Energy is reported in kcal/mol; lower values indicate more favorable interactions.

    IntaRNA runs locally in your browser. For longer sequences and advanced settings, use the command line version.

    Licenses and third-party notices
    +
    - - -

    Computing interactions

    -

    Loading the prediction engine…

    -

    The first run downloads the engine. You can stop at any time.

    - -
    +

    Computing interactions

    Loading the prediction engine…

    The first run downloads the engine. You can stop at any time.

    diff --git a/webinterface/intarna-worker.js b/webinterface/intarna-worker.js index b86d3cf7..d54cb06c 100644 --- a/webinterface/intarna-worker.js +++ b/webinterface/intarna-worker.js @@ -1,6 +1,6 @@ import { prepareRequest, makeArguments } from './protocol.js'; -// One main() call per worker: C++ globals and MEMFS never cross predictions. +// Each task owns a worker; each CLI invocation owns a fresh WASM instance. let started = false; self.onmessage = async ({ data }) => { if (started) return; @@ -11,28 +11,36 @@ self.onmessage = async ({ data }) => { if (diagnostics.length > 25) diagnostics.shift(); }; try { - // Validate again at the worker boundary before allocating the WASM runtime. const request = prepareRequest(data); self.postMessage({ type: 'status', message: 'Loading the prediction engine…' }); const { default: createIntaRNA } = await import('./wasm/intarna.js'); - const module = await createIntaRNA({ - noInitialRun: true, - locateFile: () => new URL('./wasm/intarna.wasm', import.meta.url).href, - print: log, - printErr: log, - }); - module.FS.writeFile('/query.fa', request.query.fasta); - module.FS.writeFile('/target.fa', request.target.fasta); - self.postMessage({ type: 'status', message: 'Predicting RNA interactions…' }); - const code = module.callMain(makeArguments(request.count)); - if (code !== 0) throw new Error(`IntaRNA exited with status ${code}.`); - const csv = module.FS.readFile('/results.csv', { encoding: 'utf8' }); - self.postMessage({ type: 'result', csv, diagnostics: diagnostics.join('\n') }); + const run = async (format) => { + const module = await createIntaRNA({ + noInitialRun: true, + locateFile: () => new URL('./wasm/intarna.wasm', import.meta.url).href, + print: log, printErr: log, + }); + module.FS.writeFile('/query.fa', request.query.fasta); + module.FS.writeFile('/target.fa', request.target.fasta); + module.FS.mkdir('/output'); + for (const [key, value] of Object.entries(request.parameters)) { + if (/^(q|t)(Shape|AccFile)$/.test(key) && value) module.FS.writeFile(`/${key}.txt`, value + '\n'); + } + self.postMessage({ type: 'status', message: request.task === 'evaluation' ? 'Evaluating supplied RNA interactions…' : 'Predicting RNA interactions…' }); + const code = module.callMain(makeArguments(request, format)); + if (code !== 0) throw new Error(`IntaRNA exited with status ${code}. Check the diagnostics and input settings.`); + return module.FS; + }; + const fs = await run('C'); + const csv = fs.readFile('/results.csv', { encoding: 'utf8' }); + const files = fs.readdir('/output').filter((name) => name !== '.' && name !== '..').map((name) => ({ name, text: fs.readFile(`/output/${name}`, { encoding: 'utf8' }) })); + for (const [key, format] of [['normal', 'N'], ['detailed', 'D'], ['ensemble', 'E']]) { + if (!request.outputs.includes(key)) continue; + const reportFS = await run(format); + files.push({ name: `intarna-${key}.txt`, text: reportFS.readFile('/report.txt', { encoding: 'utf8' }) }); + } + self.postMessage({ type: 'result', csv, csvColumns: request.csvColumns, files, diagnostics: diagnostics.join('\n') }); } catch (error) { - self.postMessage({ - type: 'error', - message: error instanceof Error ? error.message : String(error), - diagnostics: diagnostics.join('\n'), - }); + self.postMessage({ type: 'error', message: error instanceof Error ? error.message : String(error), diagnostics: diagnostics.join('\n') }); } }; diff --git a/webinterface/options.js b/webinterface/options.js new file mode 100644 index 00000000..efd310b0 --- /dev/null +++ b/webinterface/options.js @@ -0,0 +1,88 @@ +import { CSV_CATALOG } from './columns.js'; + +// Browser controls mirror CommandLineParsing.cpp; process/file-management options +// are replaced by local text inputs, downloads and a single cancellable worker. +const number = (key, label, value, min, max, extra = {}) => ({ key, label, value, min, max, type: 'number', ...extra }); +const select = (key, label, value, choices, extra = {}) => ({ key, label, value, choices, type: 'select', ...extra }); +const flag = (key, label, extra = {}) => ({ key, label, value: false, type: 'checkbox', ...extra }); +const text = (key, label, extra = {}) => ({ key, label, value: '', type: 'text', ...extra }); +const prediction = { prediction: true }; +const strand = (prefix, label) => [ + text(`${prefix}Set`, 'FASTA record subset', { ...prediction, hint: 'Record numbers, e.g. 1-3,5-5. Empty selects all.' }), + select(`${prefix}Acc`, 'Accessibility', 'C', { C: 'Compute', N: 'Ignore', P: 'Import unpaired probabilities', E: 'Import opening energies' }), + number(`${prefix}AccW`, 'Accessibility window', 150, 0, 99999, { exclude: [1, 2], hint: '0 uses the full sequence; otherwise at least 3 nt.' }), + number(`${prefix}AccL`, 'Maximum base-pair span', 100, 0, 99999, { exclude: [1, 2], hint: '0 uses the accessibility window.' }), + text(`${prefix}AccConstr`, 'Accessibility constraints', { hint: 'One sequence only: ./x/p/b per base, or ranges such as b:3-4,p:6-7.' }), + text(`${prefix}AccFile`, 'Accessibility data', { type: 'textarea', hint: 'Paste RNAplfold text for one sequence when importing P or E.' }), + number(`${prefix}IntLenMax`, 'Maximum interaction length', 0, 0, 99999, { ...prediction, hint: '0 uses the full sequence, bounded by the accessibility window.' }), + number(`${prefix}IntLoopMax`, 'Maximum unpaired bases per interior loop', prefix === 'q' ? 16 : 10, 0, 30, prediction), + text(`${prefix}Region`, 'Interaction regions', { ...prediction, hint: 'Inclusive sequence coordinates, e.g. 1-20,30-50.' }), + number(`${prefix}RegionLenMax`, 'Automatic region length limit', 0, 0, 99999, { ...prediction, hint: '0 disables automatic region detection.' }), + number(`${prefix}PfScale`, `${label} partition-function scale`, 1.07, 1, 99999, { step: 'any' }), +]; +export const GROUPS = [ + { key: 'query-options', label: 'Query', fields: strand('q', 'Query') }, + { key: 'target-options', label: 'Target', fields: strand('t', 'Target') }, + { key: 'seed-options', label: 'Seed', prediction: true, fields: [ + flag('noSeed', 'Do not require a seed', prediction), + text('seedTQ', 'Explicit seeds', { ...prediction, hint: 'Target&query dot-bar encodings, e.g. 1|||&1|||; separate seeds with commas.' }), + number('seedBP', 'Seed base pairs', 7, 2, 20, prediction), + number('seedMaxUP', 'Maximum unpaired seed bases (both strands)', 0, 0, 20, prediction), + number('seedQMaxUP', 'Maximum unpaired query seed bases', -1, -1, 20, { ...prediction, hint: '-1 uses the combined limit.' }), + number('seedTMaxUP', 'Maximum unpaired target seed bases', -1, -1, 20, prediction), + number('seedMaxE', 'Maximum seed energy (kcal/mol)', 0, -999, 999, { ...prediction, step: 'any' }), + number('seedMaxEhybrid', 'Maximum seed hybridization energy (kcal/mol)', 999, -999, 999, { ...prediction, step: 'any' }), + number('seedMinPu', 'Minimum seed unpaired probability', 0, 0, 1, { ...prediction, step: 'any' }), + text('seedQRange', 'Query seed regions', prediction), text('seedTRange', 'Target seed regions', prediction), + flag('seedNoGU', 'Exclude GU pairs in seeds', prediction), flag('seedNoGUend', 'Exclude GU pairs at seed ends', prediction), + ] }, + { key: 'helix-options', label: 'Helix (model B)', prediction: true, fields: [ + number('helixMinBP', 'Minimum helix base pairs', 2, 2, 4, prediction), + number('helixMaxBP', 'Maximum helix base pairs', 10, 2, 20, prediction), + number('helixMaxIL', 'Maximum helix interior-loop size', 0, 0, 2, prediction), + number('helixMinPu', 'Minimum helix unpaired probability', 0, 0, 1, { ...prediction, step: 'any' }), + number('helixMaxE', 'Maximum helix energy (kcal/mol)', 0, -999, 999, { ...prediction, step: 'any' }), + flag('helixFullE', 'Use full helix energy for the energy limit', prediction), + ] }, + { key: 'shape-options', label: 'SHAPE', fields: ['q', 't'].flatMap((p) => [ + text(`${p}Shape`, `${p === 'q' ? 'Query' : 'Target'} SHAPE data`, { type: 'textarea', hint: 'One sequence only. Paste position and reactivity, one pair per line; positions start at 1.' }), + { ...text(`${p}ShapeMethod`, 'SHAPE method'), value: 'D', hint: 'D, Dm1.8b-0.6, Z, Zb0.89 or W.' }, + { ...text(`${p}ShapeConversion`, 'SHAPE conversion'), value: 'Os1.6i-2.29', hint: 'M, C0.25, S, Ls0.68i0.2 or Os1.6i-2.29.' }, + ]) }, + { key: 'interaction-options', label: 'Interaction and energy', fields: [ + select('model', 'Interaction model', 'X', { X: 'Seed extension (MFE)', S: 'Single site (MFE)', P: 'Ensemble energy', B: 'Helix blocks (MFE)' }, prediction), + select('mode', 'Prediction algorithm', 'H', { H: 'Heuristic', M: 'Exact', S: 'Seed only', K: 'Greedy seed extension' }, prediction), + select('kineticScore', 'Greedy extension score', 'A', { A: 'Energy change', B: 'Change / (1 + s1 + s2)', C: 'Change / (1 + 2 max(s1,s2))' }, prediction), + select('energyVRNA', 'ViennaRNA parameter set', 'Turner04', { Turner04: 'Turner 2004', Turner99: 'Turner 1999', Andronescu07: 'Andronescu 2007' }), + number('temperature', 'Temperature (°C)', 37, 0, 100, { step: 'any' }), + number('energyAdd', 'Additional energy (kcal/mol)', 0, -999, 999, { step: 'any' }), + flag('energyNoDangles', 'Exclude dangling-end energy'), flag('accNoLP', 'Exclude lonely pairs in accessibility'), + flag('accNoGUend', 'Exclude GU helix ends in accessibility'), + number('windowWidth', 'Prediction window width', 0, 0, 99999, { ...prediction, hint: '0 disables prediction windows; otherwise at least 10 nt.' }), + number('windowOverlap', 'Prediction window overlap', 150, 0, 99999, prediction), + ] }, + { key: 'output-options', label: 'Output', fields: [ + number('outNumber', 'Maximum interactions per sequence pair', 1, 0, 100, { ...prediction, hint: 'Browser limit: 100; 0 computes accessibility only.' }), + select('outOverlap', 'Allow overlapping interactions', 'B', { B: 'Both strands', N: 'Neither strand', T: 'Target only', Q: 'Query only' }, prediction), + number('outMaxE', 'Maximum interaction energy (exclusive, kcal/mol)', 0, -999, 999, { ...prediction, step: 'any' }), + number('outDeltaE', 'Maximum difference from MFE (kcal/mol)', 100, 0, 100, { ...prediction, step: 'any' }), + number('outMinPu', 'Minimum unpaired probability', 0, 0, 1, { ...prediction, step: 'any' }), + flag('outBestSeedOnly', 'Report only the best seed', prediction), + flag('outNoLP', 'Exclude lonely interaction pairs', prediction), flag('outNoGUend', 'Exclude GU interaction helix ends', prediction), + flag('outPerRegion', 'Report each region combination separately', prediction), flag('outPairwise', 'Match FASTA records pairwise', prediction), + select('outCsvSort', 'CSV download sort column', '', { '': 'Native computation order', ...Object.fromEntries(CSV_CATALOG.map((column) => [column.key, column.label])) }), + text('outSpotProbSpots', 'Interaction probability spots', { ...prediction, hint: 'Optional target&query coordinate pairs, e.g. 3&7,5&9. Empty selects all spots.' }), + ] }, +]; +export const FIELDS = GROUPS.flatMap((group) => group.fields); +export const PARAMETER_DEFAULTS = Object.freeze(Object.fromEntries(FIELDS.map((field) => [field.key, field.value]))); +export const EXTRA_OUTPUTS = [ + ['qMinE', 'Query minimum energies', true], ['tMinE', 'Target minimum energies', true], + ['qAcc', 'Query opening energies'], ['tAcc', 'Target opening energies'], + ['qPu', 'Query unpaired probabilities'], ['tPu', 'Target unpaired probabilities'], + ['qSpotProb', 'Query interaction probabilities', true], ['tSpotProb', 'Target interaction probabilities', true], + ['pMinE', 'Pair minimum energies', true], ['spotProb', 'Interaction spot probabilities', true], + ['bpProb', 'Actual base-pair probabilities (model P)', true], ['bpsvg', 'Base-pair probability SVG (model P)', true], + ['normal', 'ASCII interaction structures'], ['detailed', 'Detailed interaction report'], ['ensemble', 'Ensemble report'], +].map(([key, label, predictionOnly = false]) => ({ key, label, prediction: predictionOnly })); +export const ENSEMBLE_COLUMNS = ['Eall', 'Zall', 'EallTotal', 'P_E']; diff --git a/webinterface/protocol.js b/webinterface/protocol.js index 7ed9da91..cfc9451e 100644 --- a/webinterface/protocol.js +++ b/webinterface/protocol.js @@ -1,55 +1,232 @@ -// Browser limits bound memory use; the native CLI remains unrestricted. -export const LIMITS = Object.freeze({ minLength: 7, maxLength: 2000, maxPairs: 1000000 }); -export const CSV_COLUMNS = 'id1,start1,end1,id2,start2,end2,E'; -export const DEFAULTS = Object.freeze({ - query: '>query\nGGGAGGAGGG', - target: '>target\nCCCUCCUCCC', - count: 10, -}); +import { FIELDS, PARAMETER_DEFAULTS, EXTRA_OUTPUTS, ENSEMBLE_COLUMNS } from './options.js'; +import { CSV_CATALOG } from './columns.js'; -export function parseSequence(text, label) { - if (typeof text !== 'string' || text.length > 10000) { - throw new Error(`${label}: enter one sequence of at most ${LIMITS.maxLength} nucleotides.`); - } - const lines = text.trim().split(/\r?\n/).map((line) => line.trim()); - let id = label.toLowerCase(); - if (lines[0].startsWith('>')) { - id = lines.shift().slice(1).trim(); - if (!id || id.length > 200 || /[\x00-\x1f\x7f]/.test(id)) { - throw new Error(`${label}: the FASTA header needs a name (at most 200 characters).`); +// Bound total work as well as per-strand length, including all-vs-all FASTA jobs. +export const LIMITS = Object.freeze({ minLength: 2, maxLength: 2000, maxRecords: 20, maxPairs: 1000000 }); +export const CSV_COLUMNS = 'id1,start1,end1,id2,start2,end2,subseqDP,hybridDP,E'; +export const CONTEXT_COLUMNS = ['id1', 'start1', 'end1', 'id2', 'start2', 'end2', 'E']; +export const DEFAULTS = Object.freeze({ query: '>query\nGGGAGGAGGG', target: '>target\nCCCUCCUCCC', task: 'prediction' }); +export const EVALUATION_EXAMPLE = '1||||||||||&1||||||||||'; + +export function parseFasta(text, label, multiple = true) { + if (typeof text !== 'string' || text.length > 60000 || !text.trim()) throw new Error(`${label}: enter a sequence or FASTA records.`); + const lines = text.trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + const records = []; + let current; + for (const line of lines) { + if (line.startsWith('>')) { + current = { id: line.slice(1).trim(), sequence: '' }; + if (!current.id || current.id.length > 200 || /[\x00-\x1f\x7f]/.test(current.id)) throw new Error(`${label}: the FASTA header needs a name (at most 200 characters).`); + // The native FASTA parser takes the identifier before the first space. + current.id = current.id.split(/\s+/)[0]; + records.push(current); + } else { + if (!current) { current = { id: label.toLowerCase(), sequence: '' }; records.push(current); } + current.sequence += line.replace(/\s/g, '').toUpperCase().replaceAll('T', 'U'); } } - if (lines.some((line) => line.includes('>'))) { - throw new Error(`${label}: enter exactly one FASTA record, with its header on the first line.`); + if ((!multiple && records.length !== 1) || records.length > LIMITS.maxRecords) throw new Error(`${label}: ${multiple ? 'use at most 20 FASTA records' : 'evaluation requires exactly one FASTA record'}.`); + const ids = new Set(); + for (const record of records) { + if (!record.sequence || /[^ACGUN]/.test(record.sequence)) throw new Error(`${label}: use only A, C, G, U, T or N in every sequence.`); + if (record.sequence.length < LIMITS.minLength || record.sequence.length > LIMITS.maxLength) throw new Error(`${label}: use 2–2,000 nucleotides per record.`); + // Native CSV replaces semicolons with underscores. Keep pair grouping unique. + const id = record.id.replaceAll(';', '_'); + if (ids.has(id)) throw new Error(`${label}: FASTA identifiers must be unique (also after replacing semicolons with underscores).`); + ids.add(id); } - const sequence = lines.join('').replace(/\s/g, '').toUpperCase(); - if (!sequence || /[^ACGUTN]/.test(sequence)) { - throw new Error(`${label}: use only A, C, G, U, T or N in the sequence.`); + return { records, fasta: records.map(({ id, sequence }) => `>${id}\n${sequence}\n`).join('') }; +} +export function parseSequence(text, label) { + const parsed = parseFasta(text, label, false); + return { ...parsed.records[0], fasta: parsed.fasta }; +} + +// IntaRNA omits coordinate zero when an index range crosses from negative to positive. +export function sequenceOffset(index, start) { + return index - start - (start < 0 && index > 0 ? 1 : 0); +} +function ranges(value, key, length, start = 1) { + if (!value) return []; + return value.split(',').map((part) => { + const match = /^(-?[1-9]\d*)-(-?[1-9]\d*)$/.exec(part); + if (!match) throw new Error(`${key}: use inclusive ranges such as 1-5,8-10 (coordinate zero is skipped).`); + const from = sequenceOffset(Number(match[1]), start), to = sequenceOffset(Number(match[2]), start); + if (from < 0 || to < from || to >= length) throw new Error(`${key}: ranges must be ascending and within the selected sequence.`); + return [from, to]; + }); +} +function selectedRecords(parsed, subset, key) { + const intervals = ranges(subset, key, parsed.records.length); + return intervals.length ? parsed.records.filter((_, index) => intervals.some(([a, b]) => a <= index && index <= b)) : parsed.records; +} +function oneRecord(records, key) { + if (records.length !== 1) throw new Error(`${key}: this setting requires exactly one selected sequence.`); + return records[0]; +} +function validateConstraints(value, records, start, key) { + const record = oneRecord(records, key); + if (/^[.xpb]+$/.test(value)) { + if (value.length !== record.sequence.length) throw new Error(`${key}: provide one constraint character per nucleotide.`); + return; } - if (sequence.length < LIMITS.minLength || sequence.length > LIMITS.maxLength) { - throw new Error(`${label}: use ${LIMITS.minLength}–${LIMITS.maxLength} nucleotides (the default seed needs 7).`); + const sections = value.match(/[xpb]:[^xpb]+/g); + if (!sections || sections.join('') !== value) throw new Error(`${key}: use ./x/p/b per base or ranges such as b:3-4,p:6-7.`); + for (const section of sections) ranges(section.slice(2).replace(/,$/, ''), key, record.sequence.length, start); +} +function validateAccessibility(value, record, mode, key) { + const lines = value.split(/\r?\n/); + if (!/^#[\w\s]+$/.test(lines[0]) || !/^\s*#i.\s+l=1(\s+\d+)*\s*$/.test(lines[1] ?? '')) throw new Error(`${key}: paste RNAplfold text with its comment and length header lines.`); + const lengths = lines[1].match(/\d+/g).map(Number); + if (lengths.length < 2 || lengths.some((n, i) => n !== i + 1)) throw new Error(`${key}: accessibility lengths must be consecutive, starting at 1, with at least two columns.`); + const data = lines.slice(2).filter((line) => line.trim()); + if (data.length < record.sequence.length) throw new Error(`${key}: provide one data row per nucleotide.`); + for (let index = 0; index < record.sequence.length; index++) { + const cells = data[index].trim().split(/\s+/); + const values = cells.slice(1, 1 + Math.min(index + 1, lengths.length)); + if (Number(cells[0]) !== index + 1 || values.length !== Math.min(index + 1, lengths.length) || values.some((v) => !Number.isFinite(Number(v)) || Number(v) < 0 || mode === 'P' && Number(v) > 1)) throw new Error(`${key}: rows must be consecutive; required values must be finite ${mode === 'P' ? 'probabilities in [0,1]' : 'opening energies ≥ 0'}.`); + } +} +function complementary(a, b) { return ['AU', 'UA', 'GC', 'CG', 'GU', 'UG'].includes(a + b); } +export function validateInteractions(encoding, query, target, qStart, tStart, separator = ':') { + if (typeof encoding !== 'string' || !encoding || encoding.length > 10000 || encoding.split(separator).length > 100) throw new Error('Provide 1–100 target&query dot-bar interactions.'); + for (const entry of encoding.split(separator)) { + const strands = entry.split('&'); + if (strands.length !== 2) throw new Error('Each interaction needs target&query dot-bar encodings.'); + const positions = strands.map((strand, i) => { + const match = /^(-?[1-9]\d*)([.|]+)$/.exec(strand); + if (!match) throw new Error('Use a nonzero start index followed by dots and pairing bars, e.g. 1|||&1|||.'); + if (separator === ',' && (!match[2].startsWith('|') || !match[2].endsWith('|'))) throw new Error('Explicit seed strands must start and end with a pairing bar.'); + const record = i ? query : target, start = i ? qStart : tStart; + const offset = sequenceOffset(Number(match[1]), start); + if (offset < 0 || offset + match[2].length > record.sequence.length) throw new Error('A dot-bar interaction extends outside its sequence.'); + return [...match[2]].flatMap((c, p) => c === '|' ? [offset + p] : []); + }); + if (!positions[0].length || positions[0].length !== positions[1].length) throw new Error('Interaction strands must contain the same nonzero number of pairing bars.'); + for (let p = 0; p < positions[0].length; p++) { + if (!complementary(target.sequence[positions[0][p]], query.sequence[positions[1].at(-1 - p)])) throw new Error('The supplied interaction contains a non-complementary base pair.'); + } } - const rna = sequence.replace(/T/g, 'U'); - return { id, sequence: rna, fasta: `>${id}\n${rna}\n` }; } export function prepareRequest(request) { - const query = parseSequence(request?.query, 'Query'); - const target = parseSequence(request?.target, 'Target'); - if (query.sequence.length * target.sequence.length > LIMITS.maxPairs) { - throw new Error('For this browser version, query length × target length must be at most 1,000,000. Use shorter sequences or the command line.'); + const task = request?.task ?? 'prediction'; + if (!['prediction', 'evaluation'].includes(task)) throw new Error('Choose prediction or evaluation.'); + const evaluating = task === 'evaluation'; + const query = parseFasta(request.query, 'Query', !evaluating), target = parseFasta(request.target, 'Target', !evaluating); + const parameters = {}; + const supplied = request.parameters ?? {}; + for (const field of FIELDS) { + if (evaluating && field.prediction) continue; + const value = supplied[field.key] ?? field.value; + if (field.type === 'checkbox') { + if (typeof value !== 'boolean') throw new Error(`${field.label}: choose on or off.`); + parameters[field.key] = value; + } else if (field.type === 'number') { + const n = typeof value === 'number' ? value : (typeof value === 'string' && value.trim() ? Number(value) : NaN); + if (!Number.isFinite(n) || n < field.min || n > field.max || (field.step !== 'any' && !Number.isInteger(n)) || field.exclude?.includes(n)) throw new Error(`${field.label}: use ${field.min}–${field.max}${field.exclude ? ', excluding 1 and 2' : ''}${field.step !== 'any' ? ' (whole numbers)' : ''}.`); + parameters[field.key] = n; + } else { + if (typeof value !== 'string' || value.length > (field.type === 'textarea' ? 1000000 : 10000) || /\x00/.test(value)) throw new Error(`${field.label}: invalid or oversized text input.`); + if (field.choices && !Object.hasOwn(field.choices, value)) throw new Error(`${field.label}: choose an available value.`); + parameters[field.key] = value.trim(); + } } - const count = Number(request.count); - if (!Number.isInteger(count) || count < 1 || count > 100) { - throw new Error('Choose between 1 and 100 interactions.'); + for (const key of ['qIdxPos0', 'tIdxPos0']) { + const raw = request[key] ?? 1; + const n = typeof raw === 'number' || typeof raw === 'string' && raw.trim() ? Number(raw) : NaN; + if (!Number.isInteger(n) || n < -2000000000 || n > 2000000000) throw new Error(`${key}: use an integer between -2,000,000,000 and 2,000,000,000.`); + parameters[key] = n; } - return { query, target, count }; + const qRecords = selectedRecords(query, parameters.qSet, 'qSet'), tRecords = selectedRecords(target, parameters.tSet, 'tSet'); + if (parameters.outPairwise && qRecords.length !== tRecords.length) throw new Error('Pairwise output requires the same number of selected query and target records.'); + const work = parameters.outPairwise ? qRecords.reduce((sum, q, i) => sum + q.sequence.length * tRecords[i].sequence.length, 0) : qRecords.reduce((sum, q) => sum + q.sequence.length, 0) * tRecords.reduce((sum, t) => sum + t.sequence.length, 0); + if (work > LIMITS.maxPairs) throw new Error('The total query × target length across selected sequence pairs must be at most 1,000,000.'); + + // Validate sequence-dependent settings before loading the engine. + for (const [p, records] of [['q', qRecords], ['t', tRecords]]) { + const start = parameters[`${p}IdxPos0`]; + if (parameters[`${p}AccW`] && parameters[`${p}AccL`] > parameters[`${p}AccW`]) throw new Error(`${p}AccL must not exceed ${p}AccW.`); + if (parameters[`${p}RegionLenMax`] && (parameters[`${p}Acc`] === 'N' || parameters[`${p}Region`])) throw new Error(`${p}RegionLenMax: automatic regions require accessibility and cannot be combined with manual regions.`); + for (const record of records) ranges(parameters[`${p}Region`], `${p}Region`, record.sequence.length, start); + const seedRange = parameters[`seed${p.toUpperCase()}Range`]; + if (seedRange) ranges(seedRange, `seed${p.toUpperCase()}Range`, oneRecord(records, 'Seed regions').sequence.length, start); + if (parameters[`${p}AccConstr`]) validateConstraints(parameters[`${p}AccConstr`], records, start, `${p}AccConstr`); + if (['P', 'E'].includes(parameters[`${p}Acc`])) { + const record = oneRecord(records, 'Accessibility import'); + if (!parameters[`${p}AccFile`]) throw new Error(`${p}Acc: paste accessibility data for the selected import mode.`); + validateAccessibility(parameters[`${p}AccFile`], record, parameters[`${p}Acc`], `${p}AccFile`); + } + if (parameters[`${p}Shape`]) { + const record = oneRecord(records, 'SHAPE'); + for (const line of parameters[`${p}Shape`].split(/\r?\n/).filter((line) => line.trim() && !line.trim().startsWith('#'))) { + const cells = line.trim().split(/\s+/); + if (cells.length !== 2 || !/^\d+$/.test(cells[0]) || Number(cells[0]) < 1 || Number(cells[0]) > record.sequence.length || !Number.isFinite(Number(cells[1]))) throw new Error(`${p}Shape: use position (1-based) and finite reactivity on each line.`); + } + } + if (!/^(D(m-?\d+(\.\d+)?)?(b-?\d+(\.\d+)?)?|Z(b\d+(\.\d+)?)?|W)$/.test(parameters[`${p}ShapeMethod`])) throw new Error(`${p}ShapeMethod: invalid SHAPE method encoding.`); + if (!/^(M|C(\d+(\.\d+)?)?|S|[LO](s-?\d+(\.\d+)?i-?\d+(\.\d+)?)?)$/.test(parameters[`${p}ShapeConversion`])) throw new Error(`${p}ShapeConversion: invalid SHAPE conversion encoding.`); + } + const csvColumns = request.csvColumns ?? CSV_COLUMNS.split(','); + if (!Array.isArray(csvColumns) || !csvColumns.length || new Set(csvColumns).size !== csvColumns.length || csvColumns.some((key) => !CSV_CATALOG.some((c) => c.key === key))) throw new Error('Select at least one available CSV column, without duplicates.'); + if (parameters.outCsvSort && !csvColumns.includes(parameters.outCsvSort)) throw new Error('The CSV sort column must be selected for output.'); + const outputs = request.outputs ?? []; + if (!Array.isArray(outputs) || new Set(outputs).size !== outputs.length || outputs.some((key) => !EXTRA_OUTPUTS.some((entry) => entry.key === key && (!evaluating || !entry.prediction)))) throw new Error('Choose available additional output formats for this task.'); + if (outputs.includes('ensemble') && (qRecords.length !== 1 || tRecords.length !== 1)) throw new Error('Ensemble reports require one query and one target.'); + const needsEnsemble = csvColumns.some((c) => ENSEMBLE_COLUMNS.includes(c)) || outputs.some((c) => ['ensemble', 'spotProb', 'qSpotProb', 'tSpotProb'].includes(c)); + if (evaluating) { + validateInteractions(request.rri, query.records[0], target.records[0], parameters.qIdxPos0, parameters.tIdxPos0); + } else { + if (parameters.model === 'B' && parameters.mode !== 'H') throw new Error('Helix-block model B supports heuristic mode only.'); + if (parameters.noSeed && ['S', 'K'].includes(parameters.mode)) throw new Error('Seed-only and greedy prediction require a seed.'); + if (parameters.mode === 'K' && (parameters.model !== 'X' || needsEnsemble)) throw new Error('Greedy prediction requires model X and cannot produce ensemble/probability output.'); + if (parameters.seedTQ) validateInteractions(parameters.seedTQ, oneRecord(qRecords, 'Explicit seeds'), oneRecord(tRecords, 'Explicit seeds'), parameters.qIdxPos0, parameters.tIdxPos0, ','); + const minimumSeedLength = parameters.seedTQ ? Math.min(...parameters.seedTQ.split(',').map((seed) => seed.split('&')[0].split('|').length - 1)) : parameters.seedBP; + if (!parameters.noSeed && ['q', 't'].some((p) => parameters[`${p}IntLenMax`] && parameters[`${p}IntLenMax`] < minimumSeedLength)) throw new Error('Maximum interaction lengths must cover the required seed base pairs.'); + if (!parameters.noSeed && !parameters.seedTQ && [...qRecords, ...tRecords].some((r) => r.sequence.length < parameters.seedBP)) throw new Error('Every selected sequence must be at least as long as the seed; reduce Seed base pairs or disable the seed.'); + if (parameters.model === 'B' && (parameters.helixMinBP > parameters.helixMaxBP || !parameters.noSeed && minimumSeedLength > parameters.helixMaxBP)) throw new Error('Helix maximum must cover the helix minimum and required seed base pairs.'); + if (outputs.some((key) => ['bpProb', 'bpsvg'].includes(key)) && (parameters.model !== 'P' || !['H', 'M'].includes(parameters.mode) || parameters.windowWidth || parameters.mode === 'M' && !parameters.noSeed && (parameters.seedTQ || parameters.seedMaxUP || parameters.seedQMaxUP > 0 || parameters.seedTMaxUP > 0))) throw new Error('Base-pair probabilities require model P, heuristic/exact mode and no windows; exact mode supports no seed or stack-only seeds.'); + if (parameters.windowWidth) { + const maxLength = Math.max(...['q', 't'].map((p) => parameters[`${p}Acc`] === 'N' ? parameters[`${p}IntLenMax`] : Math.max(parameters[`${p}AccW`], parameters[`${p}IntLenMax`]))); + if (parameters.windowWidth < 10 || parameters.windowOverlap >= parameters.windowWidth || parameters.windowOverlap < maxLength || ['q', 't'].some((p) => !parameters[`${p}IntLenMax`] && (parameters[`${p}Acc`] === 'N' || !parameters[`${p}AccW`])) || parameters.outNumber > 1 && parameters.outOverlap !== 'B' || needsEnsemble || parameters.model === 'P') throw new Error('Prediction windows require width ≥ 10, overlap below width and at least the bounded interaction lengths, overlapping output, and no ensemble statistics/model P.'); + } + // Non-overlap constraints in native merged regions require a single region on + // the relevant strand; automatic region detection is only safe per region. + if (!parameters.outPerRegion && parameters.outOverlap !== 'B') { + for (const p of ['q', 't']) { + if ((parameters.outOverlap === 'N' || parameters.outOverlap === (p === 'q' ? 'T' : 'Q')) && (parameters[`${p}Region`].includes(',') || parameters[`${p}RegionLenMax`])) throw new Error('Non-overlapping output with multiple/automatic regions requires output per region.'); + } + } + if (parameters.outSpotProbSpots && !/^(-?[1-9]\d*&-?[1-9]\d*)(,-?[1-9]\d*&-?[1-9]\d*)*$/.test(parameters.outSpotProbSpots)) throw new Error('Probability spots must be target&query coordinate pairs, e.g. 3&7,5&9.'); + for (const spot of parameters.outSpotProbSpots ? parameters.outSpotProbSpots.split(',') : []) { + const [t, q] = spot.split('&').map(Number); + for (const [records, coordinate, start] of [[tRecords, t, parameters.tIdxPos0], [qRecords, q, parameters.qIdxPos0]]) { + const offset = sequenceOffset(coordinate, start); + if (records.some((record) => offset < 0 || offset >= record.sequence.length)) throw new Error('Probability spots must fall within every selected sequence.'); + } + } + } + return { task, query, target, parameters, csvColumns, outputs, rri: evaluating ? request.rri : '' }; } -export function makeArguments(count) { - return [ - '--query=/query.fa', '--target=/target.fa', '--outMode=C', - `--outNumber=${count}`, '--outOverlap=B', - `--outCsvCols=${CSV_COLUMNS}`, '--out=/results.csv', - ]; +export function makeArguments(request, format = 'C') { + const args = ['--query=/query.fa', '--target=/target.fa', `--outMode=${format}`, `--out=${format === 'C' ? '/results.csv' : '/report.txt'}`]; + if (request.task === 'evaluation') args.push('--personality=IntaRNAeval'); + for (const [key, value] of Object.entries(request.parameters)) { + if (key === 'outSpotProbSpots' || key === 'kineticScore' && request.parameters.mode !== 'K') continue; + // Omit defaults: the native parser distinguishes explicit values in several + // cross-option checks, especially accessibility/region constraints. + if (value === '' || value === PARAMETER_DEFAULTS[key]) continue; + const fileInput = /^(q|t)(Shape|AccFile)$/.test(key); + args.push(`--${key}=${fileInput ? `/${key}.txt` : value}`); + } + if (request.rri) args.push(`--rri=${request.rri}`); + if (format === 'C') { + args.push(`--outCsvCols=${[...new Set([...request.csvColumns, ...CONTEXT_COLUMNS])].join(',')}`); + for (const key of request.outputs.filter((key) => !['normal', 'detailed', 'ensemble'].includes(key))) { + const spots = key === 'spotProb' && request.parameters.outSpotProbSpots ? `${request.parameters.outSpotProbSpots}:` : ''; + args.push(`--out=${key}:${spots}/output/${key}${key === 'bpsvg' ? '.svg' : '.csv'}`); + } + } + return args; } diff --git a/webinterface/results.js b/webinterface/results.js index 84410344..1c4b5d41 100644 --- a/webinterface/results.js +++ b/webinterface/results.js @@ -1,125 +1,121 @@ -import { CSV_COLUMNS } from './protocol.js'; +import { CSV_CATALOG } from './columns.js'; +import { CONTEXT_COLUMNS } from './protocol.js'; -export const COLUMNS = Object.freeze([ - { key: 'id2', label: 'Query', numeric: false }, - { key: 'start2', label: 'Query start', numeric: true }, - { key: 'end2', label: 'Query end', numeric: true }, - { key: 'id1', label: 'Target', numeric: false }, - { key: 'start1', label: 'Target start', numeric: true }, - { key: 'end1', label: 'Target end', numeric: true }, - { key: 'E', label: 'Energy (kcal/mol)', numeric: true }, - { key: 'deltaE', label: 'ΔE (kcal/mol)', numeric: true }, -]); +export const COLUMNS = Object.freeze([...CSV_CATALOG, { key: 'deltaE', label: 'mfe Δ (kcal/mol)', numeric: true }]); +const catalog = new Map(COLUMNS.map((column) => [column.key, column])); -// IntaRNA emits unquoted semicolon records and replaces delimiters in IDs. -// Header control characters are rejected before prediction; quotes are literal. +// Native CSV is unquoted, semicolon-separated; unavailable optional fields use +// NAN and seed fields may contain colon-separated lists. Preserve those strings. export function parseResults(csv) { - if (typeof csv !== 'string') throw new Error('The prediction did not return CSV text.'); - const lines = csv.trim().split(/\r?\n/); + if (typeof csv !== 'string') throw new Error('The engine did not return CSV text.'); if (!csv.trim()) return []; - if (lines.shift() !== CSV_COLUMNS.replaceAll(',', ';')) { - throw new Error('Unexpected IntaRNA CSV columns. Rebuild and serve the matching web interface and engine.'); - } - const keys = CSV_COLUMNS.split(','); + const lines = csv.trimEnd().split(/\r?\n/), keys = lines.shift().split(';'); + if (new Set(keys).size !== keys.length || keys.some((key) => !catalog.has(key)) || CONTEXT_COLUMNS.some((key) => !keys.includes(key))) throw new Error('Unexpected IntaRNA CSV columns. Serve the matching interface and engine.'); const rows = lines.filter((line) => line.trim()).map((line) => { const values = line.split(';'); if (values.length !== keys.length) throw new Error('Malformed IntaRNA CSV row.'); const row = Object.fromEntries(keys.map((key, index) => [key, values[index]])); - for (const key of ['start1', 'end1', 'start2', 'end2', 'E']) { - if (!row[key].trim() || !Number.isFinite(Number(row[key]))) throw new Error(`Invalid ${key} in prediction output.`); + for (const key of CONTEXT_COLUMNS.filter((key) => key !== 'id1' && key !== 'id2')) { + if (!row[key].trim() || !Number.isFinite(Number(row[key]))) throw new Error(`Invalid ${key} in interaction output.`); row[key] = Number(row[key]); } for (const key of ['start1', 'end1', 'start2', 'end2']) { - if (!Number.isInteger(row[key]) || row[key] < 1) throw new Error('Invalid interaction coordinates.'); + if (!Number.isInteger(row[key])) throw new Error('Invalid interaction coordinates.'); } if (row.start1 > row.end1 || row.start2 > row.end2) throw new Error('Reversed interaction coordinates.'); return row; }); - const best = Math.min(...rows.map((row) => row.E)); - return rows.map((row) => ({ ...row, deltaE: Number((row.E - best).toFixed(2)) })); + // Each sequence pair has its own optimum in a multi-FASTA screen. + const best = new Map(); + for (const row of rows) { + const pair = JSON.stringify([row.id1, row.id2]); + best.set(pair, Math.min(best.get(pair) ?? Infinity, row.E)); + } + return rows.map((row) => ({ ...row, deltaE: Number((row.E - best.get(JSON.stringify([row.id1, row.id2]))).toFixed(2)) })); } - +function numeric(value) { return String(value).trim() && Number.isFinite(Number(value)) ? Number(value) : null; } export function selectRows(rows, search, key, direction) { - const query = search.toLowerCase().trim(); - const column = COLUMNS.find((entry) => entry.key === key); - return rows.filter((row) => COLUMNS.some((entry) => String(row[entry.key]).toLowerCase().includes(query))) - .toSorted((a, b) => direction * (column.numeric ? a[key] - b[key] : a[key].localeCompare(b[key]))); + const query = search.toLowerCase().trim(), column = catalog.get(key); + return rows.filter((row) => Object.values(row).some((value) => String(value).toLowerCase().includes(query))).toSorted((a, b) => { + if (column?.numeric) { + const x = numeric(a[key]), y = numeric(b[key]); + if (x !== null && y !== null) return direction * (x - y); + if (x === null && y !== null) return 1; + if (x !== null && y === null) return -1; + } + return direction * String(a[key] ?? '').localeCompare(String(b[key] ?? '')); + }); +} +export function projectCsv(csv, columns) { + const lines = csv.trimEnd().split(/\r?\n/), keys = lines[0].split(';'); + const indices = columns.map((key) => keys.indexOf(key)); + if (indices.some((index) => index < 0)) throw new Error('Selected columns are absent from the engine output.'); + return lines.map((line) => { const cells = line.split(';'); return indices.map((index) => cells[index]).join(';'); }).join('\n') + '\n'; +} +export function downloadText(name, text) { + const url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' })); + const link = document.createElement('a'); + link.href = url; link.download = name; link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); } - export class ResultsView { constructor(root) { - this.root = root; - this.rows = []; - this.key = 'E'; - this.direction = 1; + this.root = root; this.rows = []; this.key = 'E'; this.direction = 1; this.search = root.querySelector('#search'); this.table = root.querySelector('table'); this.body = this.table.querySelector('tbody'); + this.header = this.table.querySelector('thead tr'); this.count = root.querySelector('#visible-count'); this.empty = root.querySelector('#no-matches'); + this.picker = root.querySelector('#visible-columns'); this.search.addEventListener('input', () => this.render()); - const header = this.table.querySelector('thead tr'); - for (const column of COLUMNS) { - const cell = document.createElement('th'); - cell.scope = 'col'; - const button = document.createElement('button'); - button.type = 'button'; - button.textContent = column.label; - button.addEventListener('click', () => { - this.direction = this.key === column.key ? -this.direction : 1; - this.key = column.key; - this.render(); - }); - cell.append(button); - header.append(cell); - } - root.querySelector('#download').addEventListener('click', () => { - const url = URL.createObjectURL(new Blob([this.csv], { type: 'text/csv;charset=utf-8' })); - const link = document.createElement('a'); - link.href = url; - link.download = 'intarna-interactions.csv'; - link.click(); - setTimeout(() => URL.revokeObjectURL(url), 1000); - }); + root.querySelector('#download').addEventListener('click', () => downloadText('intarna-interactions.csv', this.csv)); } - clear() { - this.rows = []; - this.csv = ''; - this.body.replaceChildren(); - this.root.hidden = true; + this.rows = []; this.csv = ''; this.body.replaceChildren(); this.root.hidden = true; } - - show(csv) { - this.rows = parseResults(csv); - this.csv = csv; - this.search.value = ''; - this.key = 'E'; - this.direction = 1; - this.root.hidden = this.rows.length === 0; - this.render(); - return this.rows.length; + show(csv, columns = csv.trim().split('\n')[0].split(';')) { + this.rows = parseResults(csv); this.csv = projectCsv(csv, columns); + this.columns = columns.map((key) => catalog.get(key)); + if (columns.includes('E')) this.columns.push(catalog.get('deltaE')); + this.visible = new Set(this.columns.map((c) => c.key)); + this.picker.replaceChildren(); + for (const column of this.columns) { + const label = document.createElement('label'), input = document.createElement('input'); + input.type = 'checkbox'; input.checked = true; input.value = column.key; + input.addEventListener('change', () => { + if (input.checked) this.visible.add(column.key); else this.visible.delete(column.key); + this.render(); + }); + label.append(input, document.createTextNode(column.label)); this.picker.append(label); + } + this.search.value = ''; this.key = columns.includes('E') ? 'E' : columns[0]; this.direction = 1; + this.root.hidden = false; this.render(); return this.rows.length; } - render() { - const visible = selectRows(this.rows, this.search.value, this.key, this.direction); + const rows = selectRows(this.rows, this.search.value, this.key, this.direction); + const columns = this.columns.filter((column) => this.visible.has(column.key)); + this.header.replaceChildren(); + for (const column of columns) { + const cell = document.createElement('th'), button = document.createElement('button'); + cell.scope = 'col'; button.type = 'button'; button.textContent = column.label; + const active = column.key === this.key, direction = this.direction === 1 ? 'ascending' : 'descending'; + cell.setAttribute('aria-sort', active ? direction : 'none'); + button.setAttribute('aria-label', `Sort by ${column.label}${active ? `, currently ${direction}` : ''}`); + button.addEventListener('click', () => { this.direction = active ? -this.direction : 1; this.key = column.key; this.render(); }); + cell.append(button); this.header.append(cell); + } this.body.replaceChildren(); - for (const row of visible) { + for (const row of rows) { const tr = document.createElement('tr'); - for (const column of COLUMNS) { + for (const column of columns) { const cell = document.createElement('td'); - cell.textContent = ['E', 'deltaE'].includes(column.key) ? row[column.key].toFixed(2) : String(row[column.key]); - if (column.numeric) cell.className = 'number'; - tr.append(cell); + cell.textContent = ['E', 'deltaE'].includes(column.key) ? Number(row[column.key]).toFixed(2) : String(row[column.key]); + if (column.numeric) cell.className = 'number'; tr.append(cell); } this.body.append(tr); } - this.table.querySelectorAll('th').forEach((cell, index) => { - const active = COLUMNS[index].key === this.key; - cell.setAttribute('aria-sort', active ? (this.direction === 1 ? 'ascending' : 'descending') : 'none'); - cell.firstChild.setAttribute('aria-label', `Sort by ${COLUMNS[index].label}${active ? `, currently ${this.direction === 1 ? 'ascending' : 'descending'}` : ''}`); - }); - this.count.textContent = `${visible.length} of ${this.rows.length} interactions`; - this.empty.hidden = visible.length > 0; + this.count.textContent = `${rows.length} of ${this.rows.length} interactions`; + this.empty.hidden = rows.length > 0; } } diff --git a/webinterface/styles.css b/webinterface/styles.css index c17f56fd..30e7a8a9 100644 --- a/webinterface/styles.css +++ b/webinterface/styles.css @@ -1,83 +1,620 @@ -:root { color-scheme: light; --ink: #17324d; --muted: #49647a; --teal: #087f83; --blue: #315fb8; --line: #cad9e2; --paper: #fff; --background: #f1f7fa; --error: #aa4b1d; font: 16px/1.55 "Segoe UI", Arial, sans-serif; color: var(--ink); background: var(--background); } -* { box-sizing: border-box; } -body { margin: 0; } -a { color: var(--blue); text-underline-offset: .2em; } -button, input, textarea { font: inherit; } -button { cursor: pointer; } -button:disabled { cursor: wait; opacity: .6; } -:focus-visible { outline: 3px solid #bf6b20; outline-offset: 4px; } -[hidden] { display: none !important; } -.site-header, main { width: min(1180px, calc(100% - 64px)); margin-inline: auto; } -.site-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 28px 0; border-bottom: 1px solid var(--line); } -.brand { display: flex; align-items: center; gap: 12px; font: bold 28px/1.1 "Trebuchet MS", "Segoe UI", sans-serif; color: var(--ink); text-decoration: none; } -.brand span { font: 14px/1.3 "Segoe UI", Arial, sans-serif; color: var(--muted); border-left: 1px solid var(--line); padding-left: 16px; margin-left: 4px; max-width: 100px; } -nav { display: flex; gap: 24px; font-size: 14px; } -.introduction { display: flex; align-items: center; justify-content: space-between; gap: 32px; padding: 48px 0 30px; } -h1, h2 { font-family: "Trebuchet MS", "Segoe UI", sans-serif; line-height: 1.18; } -h1 { font-size: clamp(28px, 3.2vw, 40px); letter-spacing: -.035em; margin: 0 0 12px; } -h2 { font-size: 25px; margin: 0; } -p { margin: 0 0 12px; max-width: 80ch; } -.introduction p { color: var(--muted); margin-bottom: 0; } -.introduction .local-note { font-size: 13px; white-space: nowrap; } -.local-note span { color: var(--teal); margin-right: 5px; } -fieldset { padding: 0; border: 0; margin: 0; min-width: 0; } -#prediction-form { background: var(--paper); border: 1px solid var(--line); border-radius: 14px; overflow: hidden; } -.sequence-grid { display: grid; grid-template-columns: 1fr 1fr; } -.strand { min-width: 0; padding: 24px 28px 18px; border-top: 4px solid var(--teal); } -.target-strand { border-top-color: var(--blue); border-left: 1px solid var(--line); } -.strand-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } -.strand-heading label { font-size: 19px; font-weight: 650; color: var(--teal); } -.target-strand label { color: var(--blue); } -.strand-heading span, .sequence-meta { color: var(--muted); font-size: 13px; } -textarea { display: block; width: 100%; height: 150px; min-height: 100px; resize: vertical; background: #f8fbfd; border: 1px solid var(--line); border-radius: 6px; padding: 14px; color: var(--ink); font: 15px/1.8 "Cascadia Mono", Consolas, monospace; overflow-wrap: anywhere; } -.sequence-meta { margin: 8px 0 0; } -.input-help { font-size: 13px; color: var(--muted); padding: 0 28px 20px; margin: 0; max-width: none; } -.compute-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 20px; padding: 20px 28px; background: #f5fafc; border-top: 1px solid var(--line); } -.form-options { display: flex; align-items: center; flex-wrap: wrap; gap: 20px; font-size: 14px; } -input[type="number"] { width: 72px; padding: 6px 8px; margin-left: 8px; color: var(--ink); background: white; border: 1px solid var(--line); border-radius: 4px; } -.primary, .secondary { border-radius: 6px; padding: 11px 20px; font-weight: 600; border: 1px solid transparent; } -.primary { background: var(--teal); color: white; } -.primary:hover { background: #076a6e; } -.secondary { background: white; border-color: #9bb1c1; color: var(--ink); } -.secondary:hover { background: #eaf2f8; } -.error { color: var(--error); background: #fff4ec; border-left: 4px solid var(--error); padding: 16px 24px; margin: 0; max-width: none; } -.model-note { display: flex; flex-wrap: wrap; gap: 4px 16px; padding: 20px 4px; color: var(--muted); font-size: 13px; } -.model-note strong { color: var(--ink); font-weight: 600; } -.model-note p { margin: 0; max-width: none; } -.results { margin-top: 26px; min-height: 140px; } -.results-heading { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 24px; align-items: baseline; margin-bottom: 22px; } -.results-heading p { color: var(--muted); font-size: 14px; margin: 0; } -pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; } -details { margin: 20px 0; } -summary { cursor: pointer; color: var(--muted); } -footer { padding: 30px 0 40px; border-top: 1px solid var(--line); font-size: 13px; color: var(--muted); } -footer p { margin-bottom: 10px; } -dialog { border: 1px solid var(--line); border-radius: 14px; color: var(--ink); padding: 36px; max-width: min(460px, calc(100% - 32px)); box-shadow: 0 20px 70px #17324d26; } -dialog::backdrop { background: #17324d66; } -dialog h2 { margin: 20px 0 14px; } -dialog p { color: var(--muted); } -dialog button { margin-top: 14px; } -.spinner { width: 32px; height: 32px; border: 3px solid #d1e7e9; border-top-color: var(--teal); border-radius: 50%; animation: spin 1s linear infinite; } -@keyframes spin { to { transform: rotate(360deg); } } -.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } -.skip-link { position: absolute; top: -100px; left: 20px; background: white; padding: 8px; z-index: 2; } -.skip-link:focus { top: 10px; } -@media (max-width: 800px) { .introduction { display: block; padding-top: 32px; } .introduction .local-note { margin-top: 14px; } .brand span { display: none; } } -@media (max-width: 600px) { .site-header, main { width: calc(100% - 32px); } .site-header { gap: 12px; padding: 20px 0; } nav { gap: 12px; font-size: 12px; } .brand { font-size: 24px; gap: 8px; } .brand img { width: 36px; height: 36px; } .sequence-grid { grid-template-columns: 1fr; } .target-strand { border-left: 0; } .strand { padding: 18px; } .input-help { padding: 0 18px 18px; } .compute-bar { padding: 18px; } .compute-bar .primary { width: 100%; } .results-heading { display: block; } .results-heading p { margin-top: 12px; } } -@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } } - -.table-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 16px 24px; padding-bottom: 18px; font-size: 14px; } -.table-tools input { display: block; margin-top: 6px; border: 1px solid #9bb1c1; border-radius: 5px; padding: 8px 12px; width: 260px; max-width: 100%; color: var(--ink); background: white; } -#visible-count { color: var(--muted); margin-right: auto; } -.table-scroll { width: 100%; overflow-x: auto; border: 1px solid var(--line); border-radius: 8px; background: white; } -table { width: 100%; border-collapse: collapse; text-align: left; font-size: 14px; font-variant-numeric: tabular-nums; } -th { background: #e8f1f6; padding: 0; white-space: nowrap; } -th button { display: block; width: 100%; padding: 16px 14px; border: 0; color: var(--ink); background: transparent; text-align: left; font-size: 13px; font-weight: 650; } -th button:hover { background: #dce9f1; } -th[aria-sort="ascending"] button::after { content: " ▴"; } -th[aria-sort="descending"] button::after { content: " ▾"; } -td { border-top: 1px solid #e0e9ee; padding: 12px 14px; max-width: 260px; overflow-wrap: anywhere; } -td.number { text-align: right; } -tr:hover td { background: #f4fafb; } -td:nth-last-child(2) { color: #075e62; font-weight: 650; } -.table-note, #no-matches { color: var(--muted); font-size: 13px; margin: 14px 0 20px; } +/* Shared vaRRI main-v2 palette and institutional header. Keep all assets local. */ +:root { + color-scheme: light; + --brand-blue:#145a96; + --brand-blue-dark:#0f4676; + --brand-blue-soft:#eaf2f8; + --brand-accent:#f4bb44; + --surface:#fff; + --surface-muted:#f3f7fb; + --text:#1c2b39; + --muted:#4f6478; + --border:#ccd9e5; + font-family:"Segoe UI",Tahoma,Geneva,sans-serif; + color:var(--text); + background:#f3f7fb; + font-size:16px; + line-height:1.5; +} + +* { + box-sizing:border-box; +} + +body { + margin:0; + background:linear-gradient(180deg,#f8fbff,#eef3fb 45%,#fff); + min-height:100vh; +} + +[hidden] { + display:none !important; +} + +a { + color:var(--brand-blue); + text-underline-offset:3px; +} + +button,input,select,textarea { + font:inherit; +} + +button,summary { + cursor:pointer; +} + +button { + border:1px solid var(--border); + border-radius:5px; + padding:.6rem 1rem; + font-weight:600; +} + +button:disabled { + opacity:.6; + cursor:wait; +} + +button.primary { + color:white; + background:var(--brand-blue); + border-color:var(--brand-blue); +} + +button.primary:hover { + background:var(--brand-blue-dark); +} + +button.secondary { + color:var(--brand-blue-dark); + background:var(--surface); +} + +button.secondary:hover { + background:var(--brand-blue-soft); +} + +:focus-visible { + outline:3px solid var(--brand-accent); + outline-offset:3px; +} + +input,select,textarea { + border:1px solid #9bafc2; + border-radius:4px; + color:var(--text); + background:var(--surface); + padding:.45rem .55rem; + min-width:0; + max-width:100%; +} + +input[type=checkbox] { + accent-color:var(--brand-blue); + width:1rem; + height:1rem; + margin:.2rem .4rem .2rem 0; + flex-shrink:0; +} + +textarea { + display:block; + width:100%; + resize:vertical; + min-height:7rem; + font-family:Consolas,"Liberation Mono",monospace; + line-height:1.65; +} + +label { + font-weight:600; +} + +small,.input-help,.table-note,.field-hint { + font-size:.85rem; + color:var(--muted); +} + +p { + max-width:80ch; +} + +h1,h2,h3,p { + margin:0 0 .75rem; +} + +h1 { + font-size:1.8rem; + line-height:1.2; +} + +h2 { + font-size:1.35rem; +} + +h3 { + font-size:1.1rem; +} + +.site-header { + min-height:100px; + padding:18px 28px; + display:flex; + align-items:center; + gap:24px; + background:#344a9a; + color:#f7fbff; +} + +.site-header a { + color:inherit; + text-decoration:none; +} + +.group-logo { + display:block; + width:auto; + height:60px; +} + +.uni-logo { + display:block; + width:auto; + height:52px; +} + +.brand { + display:flex; + align-items:center; + gap:12px; + font-weight:700; + font-size:1.6rem; + line-height:1.3; +} + +.brand small { + display:block; + font-size:.85rem; + font-weight:400; + color:#d9e8f6; +} + +.university-link { + margin-left:auto; +} + +main { + width:min(1300px,100%); + margin:auto; + padding:30px 20px; +} + +.introduction { + display:flex; + justify-content:space-between; + gap:24px; + align-items:start; + margin-bottom:20px; +} + +.introduction p { + color:var(--muted); +} + +.local-note { + flex-shrink:0; + font-size:.85rem; + padding-top:.3rem; +} + +fieldset { + border:0; + margin:0; + padding:0; + min-width:0; +} + +.task-bar,.compute-bar,.table-tools { + display:flex; + align-items:center; + flex-wrap:wrap; + justify-content:space-between; + gap:12px; +} + +.task-bar { + margin-bottom:8px; +} + +.task-bar label { + display:flex; + align-items:center; + gap:12px; +} + +#task-help { + font-size:.9rem; + color:var(--muted); +} + +.parameter-group { + border:1px solid var(--border); + background:#fbfdff; + border-radius:8px; + margin-bottom:8px; +} + +.parameter-group > summary { + padding:.75rem 1rem; + color:var(--brand-blue-dark); + font-weight:700; + user-select:none; +} + +.parameter-group[open] > summary { + background:var(--brand-blue-soft); + border-bottom:1px solid var(--border); + border-radius:7px 7px 0 0; +} + +.group-body { + padding:18px; +} + +.sequence-grid,.field-grid { + display:grid; + grid-template-columns:repeat(2,minmax(0,1fr)); + gap:18px 24px; +} + +.strand-heading,.sequence-meta { + display:flex; + align-items:center; + justify-content:space-between; + gap:8px; + margin-bottom:8px; +} + +.strand-heading span { + color:var(--muted); + font-size:.85rem; +} + +.sequence-meta { + margin-top:8px; + font-size:.85rem; + flex-wrap:wrap; + color:var(--muted); +} + +.sequence-meta input { + width:8rem; +} + +.sequence-meta label { + font-weight:400; +} + +.input-help { + margin:12px 0 0; +} + +#evaluation-input { + margin-top:20px; +} + +.field { + display:flex; + flex-direction:column; + align-items:stretch; + gap:5px; + min-width:0; +} + +.field.check { + flex-direction:row; + align-items:center; + align-self:start; + gap:4px; +} + +.field label { + font-size:.9rem; +} + +.field-hint { + margin:0; +} + +.field input[type=file] { + font-size:.8rem; + padding:.2rem; + border:0; + background:transparent; +} + +.field:has(textarea) { + grid-column:1/-1; +} + +.field:has(:disabled) { + opacity:.65; +} + +.compute-bar { + margin:20px 0; +} + +.checkbox-grid { + display:grid; + grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); + gap:8px 16px; + padding:12px 0; +} + +.checkbox-grid label { + display:flex; + align-items:flex-start; + font-weight:400; + font-size:.85rem; + overflow-wrap:anywhere; +} + +.subheading { + margin-top:24px; +} + +.selection-tools { + display:flex; + gap:8px; + flex-wrap:wrap; +} + +.selection-tools button { + font-size:.8rem; + padding:.3rem .6rem; +} + +.results { + background:var(--surface); + border:1px solid var(--border); + border-radius:8px; + margin-top:24px; + padding:22px; + min-width:0; +} + +#result-status { + color:var(--muted); +} + +.table-tools { + margin:18px 0 12px; +} + +.table-tools label { + display:flex; + gap:8px; + align-items:center; + font-size:.9rem; +} + +#visible-count { + font-size:.85rem; + color:var(--muted); +} + +.column-picker { + margin-bottom:12px; + font-size:.9rem; +} + +.table-scroll { + width:100%; + overflow:auto; + border:1px solid var(--border); +} + +table { + border-collapse:collapse; + width:100%; + white-space:nowrap; + font-size:.85rem; +} + +th { + background:var(--brand-blue-soft); + text-align:left; +} + +th button { + background:transparent; + border:0; + padding:12px; + color:var(--brand-blue-dark); + width:100%; + text-align:left; +} + +th[aria-sort=ascending] button::after { + content:' ↑'; +} + +th[aria-sort=descending] button::after { + content:' ↓'; +} + +td { + border-top:1px solid #e0e8f0; + padding:10px 12px; +} + +tr:nth-child(even) { + background:#f8fbff; +} + +td.number { + text-align:right; + font-variant-numeric:tabular-nums; +} + +.table-note { + margin:12px 0; +} + +#output-files { + margin-top:24px; +} + +#file-list details,#diagnostics { + border-top:1px solid var(--border); + margin-top:12px; + padding-top:12px; +} + +pre { + white-space:pre; + overflow:auto; + max-height:24rem; + font-size:.8rem; + background:var(--surface-muted); + padding:12px; +} + +#diagnostics pre { + white-space:pre-wrap; + overflow-wrap:anywhere; +} + +footer { + padding:24px 0; + font-size:.85rem; + color:var(--muted); +} + +footer p { + margin-bottom:8px; +} + +.error { + color:#932d28; + border:1px solid #d8a8a5; + background:#fff1f0; + border-radius:5px; + padding:12px; + max-width:none; +} + +dialog { + border:1px solid var(--border); + border-radius:10px; + max-width:min(450px,calc(100% - 32px)); + padding:28px; + color:var(--text); +} + +dialog::backdrop { + background:rgba(20,45,75,.4); +} + +.spinner { + width:28px; + height:28px; + border:3px solid var(--border); + border-top-color:var(--brand-blue); + border-radius:50%; + animation:spin 1s linear infinite; + margin-bottom:14px; +} + +@keyframes spin { + to { + transform:rotate(360deg); + } +} + +.sr-only { + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + white-space:nowrap; + border:0; +} + +.skip-link { + position:absolute; + left:12px; + top:-100px; + background:white; + padding:10px; + z-index:10; +} + +.skip-link:focus { + top:10px; +} + +@media(max-width:760px) { + .site-header { + padding:14px; + gap:14px; + flex-wrap:wrap; + } + .group-logo { + height:40px; + } + .uni-logo { + height:36px; + } + .brand { + font-size:1.2rem; + } + .brand img { + width:32px; + height:32px; + } + .brand small { + display:none; + } + main { + padding:22px 12px; + } + .introduction { + display:block; + } + .local-note { + margin-top:12px; + } + .sequence-grid,.field-grid { + grid-template-columns:1fr; + } + .results { + padding:14px; + } + .table-tools label { + flex-wrap:wrap; + } + .table-tools input { + width:100%; + } + .task-bar label { + flex-wrap:wrap; + } + h1 { + font-size:1.55rem; + } + .group-body { + padding:14px; + } +} + +@media(prefers-reduced-motion:reduce) { + .spinner { + animation:none; + } +} diff --git a/webinterface/tests/protocol.test.mjs b/webinterface/tests/protocol.test.mjs new file mode 100644 index 00000000..a6608d7d --- /dev/null +++ b/webinterface/tests/protocol.test.mjs @@ -0,0 +1,71 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { DEFAULTS, EVALUATION_EXAMPLE, parseSequence, parseFasta, prepareRequest, makeArguments, CSV_COLUMNS, sequenceOffset } from '../protocol.js'; +import { FIELDS } from '../options.js'; +import { CSV_CATALOG } from '../columns.js'; +const request = (parameters = {}, extra = {}) => ({ ...DEFAULTS, parameters, ...extra }); + +test('FASTA normalization, identifiers, bounded multi-record work and subsets', () => { + assert.equal(parseSequence('>test\r\nacgt n\r\nACG', 'Query').fasta, '>test\nACGUNACG\n'); + for (const input of ['', 'A', '>\nACGUACGU', '>one\n', 'ACGU-ACGU', '>a\nACGU\n>a\nACGU', '>a;b\nACGU\n>a_b\nACGU', '>a first\nACGU\n>a second\nACGU']) assert.throws(() => parseFasta(input, 'Query')); + const fasta = '>a\nGGGAGGAGGG\n>b\nGGGAGGAGGG'; + assert.equal(prepareRequest(request({}, { query: fasta })).query.records.length, 2); + assert.throws(() => prepareRequest(request({}, { task: 'evaluation', query: fasta, rri: EVALUATION_EXAMPLE })), /exactly one/); + assert.throws(() => prepareRequest(request({}, { query: '>a\n'+'A'.repeat(2000)+'\n>b\n'+'A'.repeat(2000), target: 'C'.repeat(300) })), /1,000,000/); + assert.doesNotThrow(() => prepareRequest(request({ qSet: '2-2' }, { query: '>a\n'+'A'.repeat(2000)+'\n>b\n'+'A'.repeat(2000), target: 'C'.repeat(300) }))); + assert.throws(() => prepareRequest(request({ outPairwise: true }, { query: fasta })), /same number/); + assert.throws(() => prepareRequest(request({ qSet: '3-3' })), /within/); + assert.throws(() => prepareRequest(request({}, { query: 'A'.repeat(2001) }))); +}); + +test('CLI defaults, ranges and strict typed controls reject invalid values', () => { + const prepared = prepareRequest(DEFAULTS); + assert.equal(prepared.parameters.outNumber, 1); + assert.equal(prepared.parameters.qIntLoopMax, 16); + assert.equal(prepared.parameters.tIntLoopMax, 10); + assert.equal(prepared.csvColumns.join(','), CSV_COLUMNS); + assert.ok(!makeArguments(prepared).some((arg) => arg.startsWith('--personality='))); + assert.ok(!makeArguments(prepared).some((arg) => arg.startsWith('--kineticScore='))); + for (const parameters of [{ outNumber: 101 }, { outNumber: 1.5 }, { temperature: '' }, { temperature: Infinity }, { qAccW: 2 }, { qAccW: 50 }, { model: '--help' }, { noSeed: 'false' }, { seedBP: 11 }]) assert.throws(() => prepareRequest(request(parameters))); + for (const qIdxPos0 of ['--help', NaN, 2e9+1, '', false]) assert.throws(() => prepareRequest(request({}, { qIdxPos0 }))); + assert.doesNotThrow(() => prepareRequest(request({}, { qIdxPos0: -2e9 }))); + assert.doesNotThrow(() => prepareRequest(request({}, { qIdxPos0: 0 }))); + assert.equal(sequenceOffset(1, -3), 3); + assert.equal(sequenceOffset(-1, -3), 2); + assert.throws(() => prepareRequest(request({}, { csvColumns: [] })), /at least one/); + assert.throws(() => prepareRequest(request({}, { outputs: ['../other'] })), /available/); +}); + +test('evaluation validates antiparallel supplied structures in signed coordinates', () => { + const evalRequest = request({ seedBP: 'ignored', model: 'ignored' }, { task: 'evaluation', rri: EVALUATION_EXAMPLE }); + const prepared = prepareRequest(evalRequest); + assert.equal(prepared.parameters.model, undefined); + assert.ok(makeArguments(prepared).includes('--personality=IntaRNAeval')); + assert.ok(makeArguments(prepared).includes(`--rri=${EVALUATION_EXAMPLE}`)); + for (const rri of ['', '1|||&1||', '1|||||||||||&1|||||||||||', '0|||&1|||', '1|||&1|||:', '1|||&1...']) assert.throws(() => prepareRequest({ ...evalRequest, rri })); + assert.doesNotThrow(() => prepareRequest({ ...evalRequest, qIdxPos0: -3, tIdxPos0: -5, rri: '-5||||||||||&-3||||||||||' })); + assert.throws(() => prepareRequest({ ...evalRequest, outputs: ['bpsvg'] }), /available/); +}); + +test('cross-option constraints, regions, probabilities, SHAPE and local file arguments', () => { + for (const parameters of [{ noSeed: true, mode: 'S' }, { mode: 'K', model: 'P' }, { model: 'B', mode: 'M' }, { model: 'B', helixMaxBP: 4 }, { qRegion: '8-2' }, { qAccConstr: 'b:1-20' }, { qAccConstr: 'bbb' }, { qShapeMethod: 'X' }, { tShapeConversion: 'bad' }, { qShape: '1 nope' }, { qAcc: 'P' }, { qAcc: 'P', qAccFile: 'bad' }, { outSpotProbSpots: '20&1' }, { windowWidth: 100 }, { qIntLenMax: 3 }, { qAcc: 'N', qRegionLenMax: 20 }, { qRegion: '1-10', qRegionLenMax: 20 }, { seedTQ: '1.|||||||||&1|||||||||.' }]) assert.throws(() => prepareRequest(request(parameters))); + assert.doesNotThrow(() => prepareRequest(request({ noSeed: true, qAccConstr: 'b:1-2,p:8-9', qRegion: '1-10' }))); + assert.throws(() => prepareRequest(request({ mode: 'K' }, { csvColumns: ['Eall'] })), /ensemble/); + assert.throws(() => prepareRequest(request({}, { outputs: ['bpsvg'] })), /model P/); + assert.throws(() => prepareRequest(request({ outCsvSort: 'ED1' })), /sort column/); + const prepared = prepareRequest(request({ model: 'P', mode: 'M', qShape: '1 0.2\n2 -999', outSpotProbSpots: '1&2' }, { outputs: ['bpProb', 'bpsvg', 'spotProb'] })); + const args = makeArguments(prepared); + assert.ok(args.includes('--qShape=/qShape.txt')); + assert.ok(args.includes('--out=spotProb:1&2:/output/spotProb.csv')); + assert.ok(args.includes('--out=bpsvg:/output/bpsvg.svg')); + assert.ok(!makeArguments(prepared, 'D').some((arg) => arg.startsWith('--outCsvCols'))); +}); + +test('browser catalog covers every native CSV column and every control names a CLI option', () => { + const header = readFileSync(new URL('../../src/IntaRNA/OutputHandlerCsv.h', import.meta.url), 'utf8'); + const ids = [...header.matchAll(/colType2string\[\w+\] = "(\w+)"/g)].map((match) => match[1]); + assert.deepEqual(CSV_CATALOG.map((c) => c.key).toSorted(), ids.toSorted()); + const source = readFileSync(new URL('../../src/bin/CommandLineParsing.cpp', import.meta.url), 'utf8'); + for (const field of FIELDS.filter((field) => field.key !== 'outSpotProbSpots')) assert.ok(source.includes(`"${field.key}"`), field.key); +}); diff --git a/webinterface/tests/results.test.mjs b/webinterface/tests/results.test.mjs index 090fe521..5ea65311 100644 --- a/webinterface/tests/results.test.mjs +++ b/webinterface/tests/results.test.mjs @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { parseResults, selectRows } from '../results.js'; +import { parseResults, selectRows, projectCsv } from '../results.js'; const header = 'id1;start1;end1;id2;start2;end2;E\n'; test('native CSV orientation, numeric coordinates, energy and relative suboptimality', () => { @@ -19,7 +19,21 @@ test('empty output and literal native ID quotes; malformed output fails clearly' assert.deepEqual(parseResults(''), []); assert.deepEqual(parseResults(header), []); assert.equal(parseResults(header + '"name";1;7;query;1;7;-3\n')[0].id1, '"name"'); - for (const csv of ['unexpected\n', header + 'too;few\n', header + 't;0;1;q;1;2;-3\n', header + 't;2;1;q;1;2;-3\n', header + 't;1;7;q;1;7;NAN\n', header + 't;1;7;q;1;7;\n']) { + for (const csv of ['unexpected\n', header + 'too;few\n', header + 't;0.5;1;q;1;2;-3\n', header + 't;2;1;q;1;2;-3\n', header + 't;1;7;q;1;7;NAN\n', header + 't;1;7;q;1;7;\n']) { assert.throws(() => parseResults(csv)); } }); + + +test('mfe difference is per sequence pair; signed coordinates and optional NAN/list fields survive', () => { + const csv = header.trim() + ';seedE;ED1\n' + + 't;-3;5;q;1;8;-20;NAN;0\n' + + 't;-3;4;q;1;7;-18;-3:-2;1.4\n' + + 't2;1;8;q;1;8;-5;NAN;0\n'; + const rows = parseResults(csv); + assert.deepEqual(rows.map((r) => r.deltaE), [0, 2, 0]); + assert.equal(rows[1].seedE, '-3:-2'); + assert.equal(rows[0].seedE, 'NAN'); + assert.equal(projectCsv(csv, ['id2','E']), 'id2;E\nq;-20\nq;-18\nq;-5\n'); + assert.throws(() => projectCsv(csv, ['absent'])); +}); diff --git a/webinterface/tests/worker.test.mjs b/webinterface/tests/worker.test.mjs index 689f59ed..e831c28c 100644 --- a/webinterface/tests/worker.test.mjs +++ b/webinterface/tests/worker.test.mjs @@ -1,19 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { PredictionRunner } from '../runner.js'; -import { DEFAULTS, parseSequence, prepareRequest, makeArguments } from '../protocol.js'; - -test('FASTA normalization and boundary validation', () => { - assert.equal(parseSequence('>test\r\nacgt n\r\nACG', 'Query').fasta, '>test\nACGUNACG\n'); - for (const input of ['', 'ACGU', '>\nACGUACGU', '>one\nACGUACGU\n>two\nACGUACGU', 'ACGU-ACGU', 'ACGU\n>bad']) { - assert.throws(() => parseSequence(input, 'Query')); - } - assert.equal(prepareRequest(DEFAULTS).query.sequence, 'GGGAGGAGGG'); - assert.throws(() => prepareRequest({ ...DEFAULTS, query: 'A'.repeat(2001) })); - assert.throws(() => prepareRequest({ ...DEFAULTS, query: 'A'.repeat(1001), target: 'C'.repeat(1000) })); - for (const count of [0, 101, 1.5, NaN, '--help']) assert.throws(() => prepareRequest({ ...DEFAULTS, count })); - assert.ok(makeArguments(10).includes('--out=/results.csv')); -}); +import { DEFAULTS } from '../protocol.js'; function fakeWorker() { return { terminated: false, terminate() { this.terminated = true; }, postMessage() {}, send(data) { this.onmessage({ data }); } };