Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
## [Unreleased]

### Added
- The quality report now says when a geography was never declared. Nearly every
process of some SimaPro databases leaves the `Geography` field at
`Unspecified` — 97% of Agribalyse 3.2 — and the only geography left is the
code inside the dataset name (`… {FR}`, `…//[RER]`, `…/CN U`). VoLCA reads
it, which is what makes those databases usable, but the result is a reading of
a name and not a declaration, and downstream the two are the same text. An
EcoSpold dataset with no geography at all is filled in with `GLO`, likewise
silently. The new `undeclaredGeography` check counts both, one finding per
entry, so a maintainer can see how much of a database's geography is source
data before treating it as such.
- A warning now reports factor lines that match a flow but cannot be
converted into its unit — a per-kilogram factor against a flow measured in
cubic metres, for instance. Refusing such a factor is correct (the
Expand Down
1 change: 1 addition & 0 deletions src/API/DatabaseHandlers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ qualityReportToAPI mLimit r =
, qraDuplicateActivities = checkToAPI mLimit (Quality.qrDuplicateActivities r)
, qraSuspiciousAmounts = checkToAPI mLimit (Quality.qrSuspiciousAmounts r)
, qraMissingMetadata = checkToAPI mLimit (Quality.qrMissingMetadata r)
, qraUndeclaredGeography = checkToAPI mLimit (Quality.qrUndeclaredGeography r)
, qraFormulaConsistency = checkToAPI mLimit (Quality.qrFormulaConsistency r)
, qraTruncatedNameCollisions = checkToAPI mLimit (Quality.qrTruncatedNameCollisions r)
, qraMissingPedigree = checkToAPI mLimit (Quality.qrMissingPedigree r)
Expand Down
5 changes: 4 additions & 1 deletion src/API/Resources.hs
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,10 @@ description r = case r of
\coproducts carry one), entries duplicated outright \
\(same name, location and reference product), non-finite amounts or a \
\zero reference amount, missing metadata (description, \
\classification, location, units absent from the registry), stored \
\classification, location, units absent from the registry), \
\geography the source never declared — read off the dataset name \
\(SimaPro writes 'Unspecified' in whole databases) or filled in by the \
\loader — stored \
\amounts that disagree with the formulas documenting them \
\(mathematicalRelation, checked at parse time), distinct names \
\that merge under SimaPro's 80-character truncation, exchanges \
Expand Down
1 change: 1 addition & 0 deletions src/API/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ data QualityReportAPI = QualityReportAPI
, qraDuplicateActivities :: QualityCheckAPI
, qraSuspiciousAmounts :: QualityCheckAPI
, qraMissingMetadata :: QualityCheckAPI
, qraUndeclaredGeography :: QualityCheckAPI
, qraFormulaConsistency :: QualityCheckAPI
, qraTruncatedNameCollisions :: QualityCheckAPI
, qraMissingPedigree :: QualityCheckAPI
Expand Down
4 changes: 3 additions & 1 deletion src/BrightwayExcel/Parser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,15 @@ rawToActivity cfg ra =
| not (any exchangeIsReference exchanges')
]

location = fromMaybe "" (metaText meta "location")
activity =
Activity
{ activityName = raName ra
, activityDescription = maybeToList (metaText meta "comment")
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = fromMaybe "" (metaText meta "location")
, activityLocation = location
, activityLocationSource = declaredLocationSource location
, activityUnit = refUnitName
, exchanges = exchanges'
, activityParams = M.empty
Expand Down
5 changes: 4 additions & 1 deletion src/Database/Loader.hs
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,17 @@ History of manual bumps:
when that name carries one, instead of always being minted from
name and location. Old caches key the same dataset under the minted
UUID, so a mixed pair would compare as two disjoint databases.
- 13: Activity record gained activityLocationSource (declared, read off the
dataset name, or neither). Old caches miss the field; the Store layout
is positional, so decoding them would misread every field after it.

The signature is stored inside the cache file and checked on load.
If it doesn't match, the cache is automatically invalidated and rebuilt.
-}
schemaSignature :: Word64
schemaSignature =
let Fingerprint hi lo = typeRepFingerprint (typeRep (Proxy :: Proxy Database))
in hi `xor` lo `xor` 12
in hi `xor` lo `xor` 13

{- |
Helper function to parse UUID from Text with deterministic UUID generation fallback.
Expand Down
34 changes: 30 additions & 4 deletions src/Database/Quality.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ amounts that aren't finite, missing metadata, stored amounts that disagree
with the formulas documenting them, distinct names that merge under
SimaPro's 80-character truncation, exchanges without the pedigree scores
their database otherwise carries, reference products nothing in the
database consumes, land transformation whose "to" and "from" areas
don't balance within an activity, oxygen-demand or organic-carbon
measures reported in a physically impossible order, CAS numbers
whose check digit doesn't confirm them, allocation percentages
database consumes, geography no dataset declared (read off the name
or filled in by the loader instead), land transformation whose "to" and
"from" areas don't balance within an activity, oxygen-demand or
organic-carbon measures reported in a physically impossible order, CAS
numbers whose check digit doesn't confirm them, allocation percentages
outside the 0-100% range, and amounts too small to have been measured.

Every check is a pure scan over a 'SimpleDatabase', so the report is
Expand Down Expand Up @@ -46,6 +47,7 @@ import Types (
BiosphereFlow (..),
Exchange (..),
FormulaCheck (..),
LocationSource (..),
Severity (..),
SimpleDatabase (..),
TechRole (..),
Expand Down Expand Up @@ -102,6 +104,7 @@ data QualityReport = QualityReport
, qrDuplicateActivities :: !QualityCheck
, qrSuspiciousAmounts :: !QualityCheck
, qrMissingMetadata :: !QualityCheck
, qrUndeclaredGeography :: !QualityCheck
, qrFormulaConsistency :: !QualityCheck
, qrTruncatedNameCollisions :: !QualityCheck
, qrMissingPedigree :: !QualityCheck
Expand All @@ -125,6 +128,7 @@ qualityChecks r =
, qrDuplicateActivities r
, qrSuspiciousAmounts r
, qrMissingMetadata r
, qrUndeclaredGeography r
, qrFormulaConsistency r
, qrTruncatedNameCollisions r
, qrMissingPedigree r
Expand Down Expand Up @@ -225,6 +229,7 @@ qualityReport dbName db =
, qrDuplicateActivities = QualityCheck True (worstFirst duplicateOffenders)
, qrSuspiciousAmounts = QualityCheck True (worstFirst amountOffenders)
, qrMissingMetadata = QualityCheck True (worstFirst metadataOffenders)
, qrUndeclaredGeography = QualityCheck True (worstFirst geographyOffenders)
, qrFormulaConsistency = QualityCheck formulaApplicable (worstFirst formulaOffenders)
, qrTruncatedNameCollisions = QualityCheck True (worstFirst truncationOffenders)
, qrMissingPedigree = QualityCheck pedigreeApplicable (worstFirst pedigreeOffenders)
Expand Down Expand Up @@ -435,6 +440,27 @@ qualityReport dbName db =
| (key, act) <- entries
]

-- A geography the source declares is a fact about the dataset; one read off
-- the dataset name is this loader's reading of a string, and one supplied by
-- the loader is neither. SimaPro writes "Unspecified" in the Geography field
-- of entire databases, so their geography survives only in names like
-- "… {FR}"; an EcoSpold dataset with no geography is filled in with "GLO".
-- Both are usable and neither was declared, and downstream the two are the
-- same text — hence Info, and hence a count: a maker reads it before
-- treating the geography as source data.
geographyOffenders =
[ offender InfoSev key act Nothing detail
| (key, act) <- entries
, Just detail <- [undeclaredGeography (activityLocationSource act) (activityLocation act)]
]
undeclaredGeography src loc = case src of
LocationDeclared -> Nothing
LocationInferredFromName ->
Just ("geography \"" <> loc <> "\" was read from the dataset name, not declared by the source")
LocationUnspecified
| T.null (T.strip loc) -> Just "the source declares no geography"
| otherwise -> Just ("the source declares no geography; \"" <> loc <> "\" stands in for it")

-- Names that only differ beyond SimaPro's cap become one name on export —
-- the over-grouping the allocation check above tolerates at parse time
-- turns into data loss on the way out. One finding per distinct name, each
Expand Down
5 changes: 4 additions & 1 deletion src/EcoSpold/Parser1.hs
Original file line number Diff line number Diff line change
Expand Up @@ -453,14 +453,17 @@ buildResult :: ParseState -> Either String (Activity, [TechnosphereFlow], [Biosp
buildResult st =
let name = fromMaybe "Unknown Activity" (psActivityName st)
location = fromMaybe "GLO" (psLocation st)
-- "GLO" above is this loader's stand-in, not something the dataset said:
-- a dataset without a geography is recorded as declaring none.
locationSource = maybe LocationUnspecified declaredLocationSource (psLocation st)
refUnit = fromMaybe "UNKNOWN_UNIT" (psRefUnit st)
description = reverse (psDescription st)
classifications =
M.fromList $
filter
(not . T.null . snd)
[("Category", psActivityCategory st), ("SubCategory", psActivitySubCategory st)]
activity = Activity name description M.empty classifications location refUnit (reverse $ psExchanges st) M.empty M.empty Nothing Nothing Nothing Nothing Nothing
activity = Activity name description M.empty classifications location locationSource refUnit (reverse $ psExchanges st) M.empty M.empty Nothing Nothing Nothing Nothing Nothing
pack act =
( act
, reverse (psTechFlows st)
Expand Down
5 changes: 4 additions & 1 deletion src/EcoSpold/Parser2.hs
Original file line number Diff line number Diff line change
Expand Up @@ -776,13 +776,16 @@ parseWithXeno xmlContent processId = do
buildResult st _pid =
let name = fromMaybe "Unknown Activity" (psActivityName st)
location = fromMaybe "GLO" (psLocation st)
-- "GLO" above is this loader's stand-in, not something the dataset
-- said: a dataset without a geography declares none.
locationSource = maybe LocationUnspecified declaredLocationSource (psLocation st)
description = reverse (psDescription st) -- Reverse to get correct order
refUnit = fromMaybe "UNKNOWN_UNIT" (psRefUnit st)
nativeType = ecoSpoldNativeType (psActivityType st) (psSpecialActivityType st)
pairs = reverse (psExchanges st)
formulaCheck = checkFormulas (psParams st) pairs
-- Apply cutoff strategy to exchanges
activity = Activity name description M.empty (psClassifications st) location refUnit (map fst pairs) (psParams st) (psParamExprs st) Nothing Nothing nativeType Nothing formulaCheck
activity = Activity name description M.empty (psClassifications st) location locationSource refUnit (map fst pairs) (psParams st) (psParamExprs st) Nothing Nothing nativeType Nothing formulaCheck
techs = reverse (psTechFlows st)
bios = reverse (psBioFlows st)
wastes = reverse (psWasteFlows st)
Expand Down
1 change: 1 addition & 0 deletions src/ILCD/Parser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ buildActivity flowInfoMap techFlowDB bioFlowDB wasteFlowDB unitDB p =
, activitySynonyms = M.empty
, activityClassification = iprClassifications p
, activityLocation = iprLocation p
, activityLocationSource = declaredLocationSource (iprLocation p)
, activityUnit = refUnit
, exchanges = map (mkExchange (iprRefFlowIdx p)) (iprExchanges p)
, activityParams = M.empty
Expand Down
18 changes: 13 additions & 5 deletions src/SimaPro/Parser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module SimaPro.Parser (
normalizeSimaProCompartment,
extractLocation,
Located (..),
LocationSource (..),
NameReading (..),

-- * Shared utilities (used by Method.ParserSimaPro)
defaultConfig,
Expand Down Expand Up @@ -791,14 +791,16 @@ generateUnitUUID unitName =
-- Conversion to volca Types
-- ============================================================================

{- | How a location was read out of a SimaPro name.
{- | How a location was read out of a SimaPro name. (Not to be confused with
'Types.LocationSource', which says whether an activity's location was declared
at all — every reading here is an inferred one by that measure.)

A tag is a location the producer wrote down. A slash suffix is a guess made by
cutting the name, and names end in a slash for reasons that have nothing to do
with geography — in "Already packed - PP/PE", PE is a plastic, not Peru. So the
ordering here says a tag beats a suffix, wherever each of the two sits.
-}
data LocationSource
data NameReading
= Tagged
| SlashSuffix
deriving (Eq, Ord, Show)
Expand All @@ -810,7 +812,7 @@ data Located = Located
informational, shortened for a suffix, which is part of the name.
-}
, locatedLocation :: Text
, locatedSource :: LocationSource
, locatedSource :: NameReading
}
deriving (Eq, Show)

Expand Down Expand Up @@ -931,7 +933,8 @@ processBlockToActivity unitCfg GlobalParams{..} ProcessBlock{..} =
processReading = extractLocation pbName

-- The Geography field, when the producer filled it in. It outranks anything
-- read out of a name, being the one place meant to hold a location.
-- read out of a name, being the one place meant to hold a location — a name
-- reading is recorded as such below, for the quality report.
statedLocation = mfilter ((/= "unspecified") . T.toLower) (nonEmptyText pbLocation)

{- Trimmed Process name (without curly-brace location tag). Empty when the
Expand Down Expand Up @@ -1013,6 +1016,10 @@ processBlockToActivity unitCfg GlobalParams{..} ProcessBlock{..} =
. sortOn locatedSource
$ catMaybes readings
effectiveLoc = fromMaybe readLocation statedLocation
effectiveLocSource
| isJust statedLocation = LocationDeclared
| T.null effectiveLoc = LocationUnspecified
| otherwise = LocationInferredFromName
allocFormula = mfilter (not . isNumericFormula) (nonEmptyText (prAllocRaw prod))
activity =
Activity
Expand All @@ -1027,6 +1034,7 @@ processBlockToActivity unitCfg GlobalParams{..} ProcessBlock{..} =
, ("Category", prCategory prod)
]
, activityLocation = effectiveLoc
, activityLocationSource = effectiveLocSource
, activityUnit = effUnitName
, exchanges = productExchange : map (scaleExchange allocFraction) sharedExchanges
, activityParams = env
Expand Down
26 changes: 26 additions & 0 deletions src/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,31 @@ data FormulaCheck = FormulaCheck
}
deriving (Generic, NFData, Store)

{- | Where an activity's 'activityLocation' came from. A format with a geography
field publishes one ('LocationDeclared'); when it is blank or says
@Unspecified@, the parser falls back to the code embedded in the dataset name
(@"… {FR}"@, @"…//[RER]"@, @"…/CN U"@), which is a reading of the name and not
a declaration ('LocationInferredFromName'). 'LocationUnspecified' when neither
yielded anything, and 'activityLocation' is empty.

Recorded at parse time because only the parser can still tell the two apart:
downstream, a declared @FR@ and a guessed @FR@ are the same text. The quality
report is what surfaces the difference.
-}
data LocationSource
= LocationDeclared
| LocationInferredFromName
| LocationUnspecified
deriving (Show, Eq, Generic, NFData, Store)

{- | The source of a geography a format declares directly: the field itself when
it carries something, nothing to infer from when it doesn't.
-}
declaredLocationSource :: Text -> LocationSource
declaredLocationSource loc
| T.null (T.strip loc) = LocationUnspecified
| otherwise = LocationDeclared

{- | Base LCA activity
Note: ProcessId is the index in dbActivities vector, UUIDs stored in dbProcessIdTable
-}
Expand All @@ -492,6 +517,7 @@ data Activity = Activity
, activitySynonyms :: !(M.Map Text (S.Set Text)) -- Synonyms by language, same structure as flows
, activityClassification :: !(M.Map Text Text) -- Classifications (ISIC, CPC, etc.)
, activityLocation :: !Text -- Location code (e.g. FR, RER)
, activityLocationSource :: !LocationSource -- Whether the source declared that location or the parser read it off the dataset name
, activityUnit :: !Text -- Reference unit
, exchanges :: ![Exchange] -- List of exchanges
, activityParams :: !(M.Map Text Double) -- Resolved dataset parameter values (SimaPro parameters, EcoSpold2 <parameter> variables)
Expand Down
1 change: 1 addition & 0 deletions test/BM25Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mkActivity name loc xs =
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = loc
, activityLocationSource = LocationDeclared
, activityUnit = "kg"
, exchanges = xs
, activityParams = M.empty
Expand Down
1 change: 1 addition & 0 deletions test/BrightwayExcelWriterSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ elec =
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = "GLO"
, activityLocationSource = LocationDeclared
, activityUnit = "kilowatt hour"
, exchanges = [prodExch, gasExch, co2Exch]
, activityParams = M.empty
Expand Down
2 changes: 2 additions & 0 deletions test/CopySpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import Types (
Database (..),
Exchange (..),
GeographyPolicy (..),
LocationSource (..),
SparseTriple (..),
TechRole (..),
TechnosphereFlow (..),
Expand Down Expand Up @@ -274,6 +275,7 @@ supplierDB offset products =
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = "GLO"
, activityLocationSource = LocationDeclared
, activityUnit = "kg"
, exchanges = [refOut]
, activityParams = M.empty
Expand Down
1 change: 1 addition & 0 deletions test/CrossDBActivityLinkSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ mkActivity name loc exs =
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = loc
, activityLocationSource = LocationDeclared
, activityUnit = "kg"
, exchanges = exs
, activityParams = M.empty
Expand Down
1 change: 1 addition & 0 deletions test/CrossDBRegionalLCIAFixture.hs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ mkActivity _ loc =
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = loc
, activityLocationSource = LocationDeclared
, activityUnit = "kg"
, exchanges = []
, activityParams = M.empty
Expand Down
1 change: 1 addition & 0 deletions test/CrossLinkingSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ mkActivityAt loc =
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = loc
, activityLocationSource = LocationDeclared
, activityUnit = "kg"
, exchanges = []
, activityParams = M.empty
Expand Down
2 changes: 2 additions & 0 deletions test/DeleteSelectionSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import Types (
Database (..),
Exchange (..),
GeographyPolicy (..),
LocationSource (..),
ProcessId,
SparseTriple (..),
TechRole (..),
Expand Down Expand Up @@ -501,6 +502,7 @@ mkActivity name loc classif exs =
, activitySynonyms = M.empty
, activityClassification = classif
, activityLocation = loc
, activityLocationSource = LocationDeclared
, activityUnit = "kg"
, exchanges = exs
, activityParams = M.empty
Expand Down
1 change: 1 addition & 0 deletions test/EcoSpold1Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ emptyActivity =
, activitySynonyms = M.empty
, activityClassification = M.empty
, activityLocation = "GLO"
, activityLocationSource = LocationDeclared
, activityUnit = "kg"
, exchanges = []
, activityParams = M.empty
Expand Down
Loading
Loading