Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support a generic "original" payload in structure recognizer #2216

Merged
merged 1 commit into from
Dec 4, 2024

Conversation

kostmo
Copy link
Member

@kostmo kostmo commented Nov 30, 2024

The recognizer algorithm operates on a NonEmptyGrid of Maybe elements. However, there may exist some progenitor object with auxiliary metadata payload that is independent of the algorithm but should be presented along with the final recognized structure.

So long as an "extraction" function is provided that derives the nonempty grid of as from the original object b, we can send b along for the ride through the algorithm without concern to b's internals.

Changes

Overall this refactoring makes the code more generic and simpler.

  • Build the recognizer automata at scenario parse time
  • Enforce grid no emptiness earlier in recognizer construction
  • Subsume redundant _structureDefs member of StaticStructureInfo record with _staticAutomatons field
  • Make some type signatures in the UI more generic
  • Push the conversion from Cell to Entity further up the pipeline
  • Remove some derived Show instances (show-ing a Scenario is not likely to be useful due to sheer volume of content)

@kostmo kostmo force-pushed the refactor/more-nonempty-grid-usage branch 2 times, most recently from 69e4b82 to 3c4bc19 Compare December 1, 2024 07:08
@kostmo kostmo force-pushed the refactor/more-nonempty-grid-usage branch 2 times, most recently from a204a0f to f863adc Compare December 2, 2024 06:25
@kostmo kostmo changed the title [WIP] Support a generic "original" payload in structure recognizer Support a generic "original" payload in structure recognizer Dec 2, 2024
@kostmo kostmo force-pushed the refactor/more-nonempty-grid-usage branch 2 times, most recently from b627c4d to fb8ea0d Compare December 2, 2024 23:13
@kostmo kostmo requested a review from byorgey December 2, 2024 23:14
@kostmo kostmo marked this pull request as ready for review December 2, 2024 23:14
@kostmo kostmo added the Z-Refactoring This issue is about restructuring the code without changing the behaviour to improve code quality. label Dec 2, 2024
@byorgey
Copy link
Member

byorgey commented Dec 2, 2024

@kostmo I am having trouble finding which modules contain the "main" changes (as opposed to other modules which "just" have to be updated to match the new types/definitions). Could you mention which modules are the interesting ones to look at?

@kostmo kostmo force-pushed the refactor/more-nonempty-grid-usage branch from fb8ea0d to e5decbf Compare December 3, 2024 02:07
Copy link
Member Author

@kostmo kostmo left a comment

Choose a reason for hiding this comment

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

@byorgey I've annotated the most relevant changes now.

Comment on lines -358 to +365
symmetryAnnotatedGrids <- mapM checkSymmetry recognizableGrids
-- We exclude empty grids from the recognition engine.
nonEmptyRecognizableGrids = mapMaybe (traverse getNonEmptyGrid) recognizableGrids

myAutomatons <-
either (fail . T.unpack . renderRedundancy) return $
mkAutomatons (fmap cellToEntity) nonEmptyRecognizableGrids
Copy link
Member Author

Choose a reason for hiding this comment

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

@byorgey here is one interesting change. Previously the orientations to be recognized were checked for redundancy (according to symmetry) in the scenario parser. I have now moved that detail to within the recognizer API.

Related to this, we now instantiate the recognizer engine from within the scenario parsing code (instead of from within game state initialization), so that it can throw an error when a redundant recognition spec is provided.

where
rotatedGrids = concatMap (extractGrids extractor . namedGrid) xs
extractedItems = map (uncurry ExtractedArea . fmap (extractor . structure) . dupe) rawGrids
Copy link
Member Author

Choose a reason for hiding this comment

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

Another major change: the "extraction" of Entities from Cells (which, from the perspective of the recognizer library, is the opaque type parameter b) is now only performed once in this single location, and the result is preserved and shared elsewhere it is needed. The "extraction" function also used to operate piecewise on each cell within a structure. Now, its contract is to take an entire "shape" of type b and produce a NonEmptyGrid (Maybe a).

Comment on lines -116 to +118
Maybe (StructureWithGrid (Maybe b) a)
extractOrientedGrid extractor x d =
case extractor <$> structure x of
EmptyGrid -> Nothing
Grid neGrid ->
let w = RowWidth . rectWidth . getNEGridDimensions $ neGrid
in Just $
StructureWithGrid wrapped d w $
applyOrientationTransformNE (Orientation d False) neGrid
StructureWithGrid b a
extractOrientedGrid (ExtractedArea x neGrid) d =
StructureWithGrid d w $
ExtractedArea x $
applyOrientationTransformNE (Orientation d False) neGrid
Copy link
Member Author

Choose a reason for hiding this comment

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

This got much simpler since I pushed the enforcement of NonEmptyGrids further upstream.

Comment on lines -159 to 152
x <- extractOrientedGrid extractor sGrid d
return $ PositionedStructure (Cosmic subworldName loc) x
return $
PositionedStructure (Cosmic subworldName loc) $
extractOrientedGrid (grid $ annotatedGrid sGrid) d

Copy link
Member Author

Choose a reason for hiding this comment

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

This was the other place that "extraction" was recomputed. Now we just re-use the result computed earlier.

Comment on lines -153 to +143
definitionMap = M.fromList $ map ((name &&& id) . namedGrid) structDefs
definitionMap = theAutomatons ^. originalStructureDefinitions
Copy link
Member Author

Choose a reason for hiding this comment

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

We also no longer need to recompute this Map.

Comment on lines -52 to -56
data StaticStructureInfo b = StaticStructureInfo
{ _structureDefs :: [SymmetryAnnotatedGrid (Maybe b)]
, _staticPlacements :: Map SubworldName [LocatedStructure]
}
deriving (Show)
Copy link
Member Author

Choose a reason for hiding this comment

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

This StaticStructureInfo record was relocated to the Swarm.Game.Scenario.Topography.Structure.Recognition.Type module. And its _structureDefs member is made redundant.

Comment on lines -70 to +80
doAddition newEntity r = do
stateRevision <- case HM.lookup newEntity entLookup of
Nothing -> return oldRecognitionState
Just finder -> do
tell . pure . FoundParticipatingEntity $
ParticipatingEntity
newEntity
(finder ^. inspectionOffsets)
registerRowMatches entLoader cLoc finder oldRecognitionState

return $ r & recognitionState .~ stateRevision
where
oldRecognitionState = r ^. recognitionState
entLookup = autoRecognizer ^. automatonsByEntity

doRemoval = do
doAddition newEntity =
maybe return logAndRegister $ HM.lookup newEntity entLookup
where
logAndRegister finder s = do
tell . pure . FoundParticipatingEntity $
ParticipatingEntity
newEntity
(finder ^. inspectionOffsets)
registerRowMatches entLoader cLoc finder s
Copy link
Member Author

Choose a reason for hiding this comment

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

Here is just some independent refactoring to clean up this logic

Comment on lines +158 to +160
data ExtractedArea b a = ExtractedArea
{ originalItem :: NamedArea b
, extractedGrid :: NonEmptyGrid (AtomicKeySymbol a)
Copy link
Member Author

Choose a reason for hiding this comment

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

This new ExtractedArea type is what propagates the original b through the algorithm.

Comment on lines -161 to -163
data NamedOriginal b = NamedOriginal
{ getName :: StructureName
, orig :: NamedGrid b
Copy link
Member Author

Choose a reason for hiding this comment

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

Don't need NamedOriginal anymore; we just access the name through the NamedArea record.

Copy link
Member

@byorgey byorgey left a comment

Choose a reason for hiding this comment

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

Thanks, the additional comments were very helpful! I think this looks good --- I'm always in favor of nice refactoring that makes things simpler + more modular!

@@ -20,7 +20,7 @@ import Swarm.Game.Universe (Cosmic)

renderSharedNames :: ConsolidatedRowReferences b a -> NonEmpty StructureName
renderSharedNames =
NE.nub . NE.map (getName . originalDefinition . wholeStructure) . referencingRows
NE.nub . NE.map (name . originalItem . entityGrid . wholeStructure) . referencingRows
Copy link
Member

Choose a reason for hiding this comment

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

I know the call to NE.nub wasn't added or changed in this PR, but I guess I didn't notice it before. Is it a reasonable guarantee that the list of things here will be small? I ask since NE.nub takes $O(n^2)$ time, so we should use if if and only if we know the list will not be too long.

Copy link
Member Author

Choose a reason for hiding this comment

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

Most likely the list is small (it's bounded by the number of user-defined structures/their sizes). I was aware of this $O(N^2)$ issue but did not see an alternative in Data.List.NonEmpty. However, now that I look, I see Data.List.NonEmpty.Extra.nubOrd, so I'll use that.

Copy link
Member

Choose a reason for hiding this comment

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

Huh, nubOrd uses a custom red-black tree implementation. I wonder why it doesn't just use Set. Well, it doesn't matter. Since the list we are nubbing is just a list of names, nubOrd should work well.

Copy link
Member Author

@kostmo kostmo Dec 3, 2024

Choose a reason for hiding this comment

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

Could the red-black implementation possibly be order-preserving?

Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder why it doesn't just use Set.

I presume it is to minimize dependencies of the extra package. Particularly, extra does not depend on containers.

Incidentally, now that I've read the code I see that nubOrd is order-preserving whereas nubSort is not. That fact would be good to note explicitly in the docstring (rather than indirectly by example).

@kostmo kostmo force-pushed the refactor/more-nonempty-grid-usage branch from e5decbf to 063dbc5 Compare December 4, 2024 00:04
@kostmo kostmo added the merge me Trigger the merge process of the Pull request. label Dec 4, 2024
@kostmo kostmo force-pushed the refactor/more-nonempty-grid-usage branch from 063dbc5 to e2094ff Compare December 4, 2024 00:10
@mergify mergify bot merged commit 099b95f into main Dec 4, 2024
12 checks passed
@mergify mergify bot deleted the refactor/more-nonempty-grid-usage branch December 4, 2024 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge me Trigger the merge process of the Pull request. Z-Refactoring This issue is about restructuring the code without changing the behaviour to improve code quality.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants