├── .project ├── src ├── .properties ├── PDS-Core │ ├── package.st │ ├── PDSBitArray.class.st │ ├── PDSWorldMenu.class.st │ ├── PDSBloomFilterAnalysis.class.st │ ├── PDSBloomFilter.class.st │ └── PDSIcons.class.st ├── PDS-Grapher │ ├── package.st │ ├── PDSBoundCursorFollower.class.st │ ├── PDSStandardDeviationDecorator.class.st │ └── PDSFunctionBoundCursorFollower.class.st ├── PDS-Core-Tests │ ├── package.st │ └── BloomFilterTests.class.st ├── PDS-Playground │ ├── package.st │ ├── PDSAlgorithmPlayground.class.st │ ├── PDSRoassalViewer.class.st │ ├── PDSAlgorithmsList.class.st │ ├── PDSBloomFilterPlayground.class.st │ ├── PDSAlgorithmsBrowser.class.st │ ├── PDSBloomFilterViewer.class.st │ ├── PDSBloomFilterActionForm.class.st │ ├── PDSBloomFilterPlaygroundContent.class.st │ ├── PDSBloomFilterCreationForm.class.st │ └── PDSBloomFilterPlaygroundToolbar.class.st ├── PDS-Core-Examples │ ├── package.st │ └── PDSBloomFilterExamples.class.st ├── PDS-GToolkit-Extensions │ ├── package.st │ ├── PDSBitArray.extension.st │ └── PDSBloomFilter.extension.st ├── PDS-Inspector-Extensions │ ├── package.st │ ├── PDSBitArray.extension.st │ └── PDSBloomFilter.extension.st ├── BaselineOfProbabilisticDataStructures │ ├── package.st │ └── BaselineOfProbabilisticDataStructures.class.st └── ConfigurationOfProbabilisticDataStructures │ ├── package.st │ └── ConfigurationOfProbabilisticDataStructures.class.st ├── icons ├── abacus.png ├── flask.png ├── rocket.png ├── compass.png └── mortarboard.png ├── doc ├── images │ ├── k_formula.png │ ├── m_formula.png │ ├── pfp_formula.png │ ├── bloom-analysis.png │ ├── algorithms-browser.png │ ├── bloom-fpp-extension.png │ ├── bloom-bits-extension.png │ ├── pharo-project-catalog.png │ └── bloom-params-extension.png └── tutorial │ ├── moldable-development-tutorial.pillar │ ├── 01-inspector-parameters-extension.ombu │ └── 01-create-empty-example.ombu ├── .travis.yml ├── .smalltalk.ston ├── LICENSE └── README.md /.project: -------------------------------------------------------------------------------- 1 | { 2 | 'srcDirectory' : 'src' 3 | } -------------------------------------------------------------------------------- /src/.properties: -------------------------------------------------------------------------------- 1 | { 2 | #format : #tonel 3 | } -------------------------------------------------------------------------------- /src/PDS-Core/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'PDS-Core' } 2 | -------------------------------------------------------------------------------- /src/PDS-Grapher/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'PDS-Grapher' } 2 | -------------------------------------------------------------------------------- /src/PDS-Core-Tests/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'PDS-Core-Tests' } 2 | -------------------------------------------------------------------------------- /src/PDS-Playground/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'PDS-Playground' } 2 | -------------------------------------------------------------------------------- /src/PDS-Core-Examples/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'PDS-Core-Examples' } 2 | -------------------------------------------------------------------------------- /icons/abacus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/icons/abacus.png -------------------------------------------------------------------------------- /icons/flask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/icons/flask.png -------------------------------------------------------------------------------- /icons/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/icons/rocket.png -------------------------------------------------------------------------------- /icons/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/icons/compass.png -------------------------------------------------------------------------------- /icons/mortarboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/icons/mortarboard.png -------------------------------------------------------------------------------- /src/PDS-GToolkit-Extensions/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'PDS-GToolkit-Extensions' } 2 | -------------------------------------------------------------------------------- /src/PDS-Inspector-Extensions/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'PDS-Inspector-Extensions' } 2 | -------------------------------------------------------------------------------- /doc/images/k_formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/k_formula.png -------------------------------------------------------------------------------- /doc/images/m_formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/m_formula.png -------------------------------------------------------------------------------- /doc/images/pfp_formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/pfp_formula.png -------------------------------------------------------------------------------- /doc/images/bloom-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/bloom-analysis.png -------------------------------------------------------------------------------- /doc/images/algorithms-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/algorithms-browser.png -------------------------------------------------------------------------------- /doc/images/bloom-fpp-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/bloom-fpp-extension.png -------------------------------------------------------------------------------- /src/BaselineOfProbabilisticDataStructures/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #BaselineOfProbabilisticDataStructures } 2 | -------------------------------------------------------------------------------- /doc/images/bloom-bits-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/bloom-bits-extension.png -------------------------------------------------------------------------------- /doc/images/pharo-project-catalog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/pharo-project-catalog.png -------------------------------------------------------------------------------- /doc/images/bloom-params-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osoco/PharoPDS/HEAD/doc/images/bloom-params-extension.png -------------------------------------------------------------------------------- /src/ConfigurationOfProbabilisticDataStructures/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #ConfigurationOfProbabilisticDataStructures } 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: smalltalk 2 | sudo: false 3 | 4 | os: 5 | - linux 6 | - osx 7 | 8 | smalltalk: 9 | - Pharo-6.1 10 | - Pharo64-7.0 11 | - Pharo64-8.0 12 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSAlgorithmPlayground.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSAlgorithmPlayground, 3 | #superclass : #ComposablePresenter, 4 | #category : #'PDS-Playground-Base' 5 | } 6 | 7 | { #category : #'as yet unclassified' } 8 | PDSAlgorithmPlayground class >> algorithmName [ 9 | self subclassResponsibility 10 | ] 11 | -------------------------------------------------------------------------------- /.smalltalk.ston: -------------------------------------------------------------------------------- 1 | SmalltalkCISpec { 2 | #loading : [ 3 | SCIMetacelloLoadSpec { 4 | #baseline : 'ProbabilisticDataStructures', 5 | #directory : 'src', 6 | #platforms : [ #pharo ] 7 | } 8 | ], 9 | #testing : { 10 | #coverage : { 11 | #classes : [#PDSBitArray , #PDSBloomFilter, #BloomFilterTests], 12 | #onWarningLog : true, 13 | #useLatestMetacello : true 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rafael Luque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/tutorial/moldable-development-tutorial.pillar: -------------------------------------------------------------------------------- 1 | ! Moldable Development applied to Bloom filters 2 | 3 | In this tutorial we learn how to extend the moldable IDE in Pharo with custom views and tools for the ""Bloom Filter"" implementation provided by the project ""PharoPDS"". 4 | 5 | The views for an instance of ==PDSBloomFilter== are the default for any object, ==Raw== and ==Meta==: 6 | 7 | [[[ 8 | (PDSBloomFilter new: 10 fpp: 0.03) inspect 9 | ]]] 10 | 11 | !! Custom extensions for the "classic" Inspector 12 | 13 | We are going to write a simple example returning an empty Bloom filter to be used in our workshop: 14 | 15 | ${changes:01-create-empty-example}$ 16 | 17 | Now we can build a new empty Bloom filter running the previous example: 18 | 19 | ${example:PDSWBloomFilterExamples>>#emptyFilter}$ 20 | 21 | The first exercise consists of creating a simple custom extension for the Bloom filter instances in order to show their key parameters when they are shown by the Inspector: 22 | 23 | ${changes:01-inspector-parameters-extension}$ 24 | 25 | Now, if you inspect the same Bloom filter instance we created previously you should see a table containing the key parameters for the filter: 26 | 27 | [[[ 28 | PDSWBloomFilterExamples new emptyFilter inspect 29 | ]]] -------------------------------------------------------------------------------- /src/PDS-Inspector-Extensions/PDSBitArray.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #PDSBitArray } 2 | 3 | { #category : #'*PDS-Inspector-Extensions' } 4 | PDSBitArray >> drawBitSetOn: aMondrian [ 5 | | nodes | 6 | nodes := (1 to: self size) 7 | collect: [ :bitIndex | (self bitAt: bitIndex) = 1 ]. 8 | aMondrian shape rectangle 9 | size: 20; 10 | if: [ :el | el ] color: Color blue trans; 11 | if: [ :el | el not ] color: Color green trans. 12 | aMondrian nodes: nodes. 13 | aMondrian layout grid. 14 | aMondrian build 15 | ] 16 | 17 | { #category : #'*PDS-Inspector-Extensions' } 18 | PDSBitArray >> gtInspectorBitSetIn: composite [ 19 | 20 | ^ composite roassal2 21 | title: 'BitSet'; 22 | initializeView: [ | b | 23 | b := RTMondrian new. 24 | self drawBitSetOn: b. 25 | b ]; 26 | yourself 27 | ] 28 | 29 | { #category : #'*PDS-Inspector-Extensions' } 30 | PDSBitArray >> gtInspectorParametersIn: composite [ 31 | 32 | ^ composite fastTable 33 | title: 'Parameters'; 34 | display: [ {('Total bits' -> self size). 35 | ('Occupancy' -> self occupancy). 36 | ('Occupancy in percentage (%)' -> self occupancyInPercent)} ]; 37 | column: 'Name' 38 | evaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each key ]; 39 | column: 'Value' 40 | evaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each value ] 41 | ] 42 | -------------------------------------------------------------------------------- /src/PDS-GToolkit-Extensions/PDSBitArray.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #PDSBitArray } 2 | 3 | { #category : #'*PDS-GToolkit-Extensions' } 4 | PDSBitArray >> gtBitSetViewFor: aView [ 5 | 6 | ^ aView explicit 7 | title: 'BitSet'; 8 | priority: 20; 9 | stencil: [ | view nodes | 10 | view := GtMondrian new. 11 | nodes := (1 to: self size) 12 | collect: [ :bitIndex | (self bitAt: bitIndex) = 1 ]. 13 | view nodes 14 | shape: [ :anInteger | 15 | | color | 16 | color := (nodes at: anInteger) 17 | ifTrue: [ Color blue trans ] 18 | ifFalse: [ Color green trans ]. 19 | BlElement new 20 | geometry: BlRectangle new; 21 | background: (BlBackground paint: color); 22 | border: (BlBorder paint: Color black width: 1); 23 | size: 20 asPoint ]; 24 | with: (1 to: self size). 25 | view layout grid. 26 | view ] 27 | ] 28 | 29 | { #category : #'*PDS-GToolkit-Extensions' } 30 | PDSBitArray >> gtParametersViewFor: aView [ 31 | 32 | ^ aView columnedList 33 | title: 'Parameters'; 34 | priority: 10; 35 | items: [ {('Total bits' -> self size). 36 | ('Occupancy' -> self occupancy). 37 | ('Occupancy in percentage (%)' -> self occupancyInPercent)} ]; 38 | column: 'Parameter' 39 | do: [ :column | 40 | column 41 | item: [ :assoc | assoc key ]; 42 | matchParent ]; 43 | column: 'Value' 44 | do: [ :column | 45 | column 46 | item: [ :assoc | assoc value ]; 47 | matchParent ] 48 | ] 49 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSRoassalViewer.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSRoassalViewer, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'label', 6 | 'visualization', 7 | 'roassalScript', 8 | 'labelText' 9 | ], 10 | #category : #'PDS-Playground-Widgets' 11 | } 12 | 13 | { #category : #specs } 14 | PDSRoassalViewer class >> defaultSpec [ 15 | ^ SpecColumnLayout composed 16 | newRow: [ :row | row add: #label ] height: self labelHeight; 17 | newRow: [ :row | row add: #visualization ]; 18 | yourself 19 | ] 20 | 21 | { #category : #api } 22 | PDSRoassalViewer >> extent [ 23 | ^ 400 @ 400 24 | ] 25 | 26 | { #category : #initialization } 27 | PDSRoassalViewer >> initializeWidgets [ 28 | label := self newLabel. 29 | visualization := self instantiate: RoassalModel. 30 | label 31 | label: 'Roassal Visualization'; 32 | emphasis: #bold. 33 | visualization script: [ :view :canvas | ]. 34 | self focusOrder add: visualization 35 | ] 36 | 37 | { #category : #accessing } 38 | PDSRoassalViewer >> label [ 39 | ^ label 40 | ] 41 | 42 | { #category : #api } 43 | PDSRoassalViewer >> labelText: anObject [ 44 | label label: anObject. 45 | ] 46 | 47 | { #category : #api } 48 | PDSRoassalViewer >> refresh [ 49 | visualization refresh 50 | ] 51 | 52 | { #category : #api } 53 | PDSRoassalViewer >> roassalScript: anObject [ 54 | visualization script: anObject. 55 | ] 56 | 57 | { #category : #api } 58 | PDSRoassalViewer >> title [ 59 | ^ 'Bloom Filter Bits' 60 | ] 61 | 62 | { #category : #accessing } 63 | PDSRoassalViewer >> visualization [ 64 | ^ visualization 65 | ] 66 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSAlgorithmsList.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSAlgorithmsList, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'list', 6 | 'description' 7 | ], 8 | #category : #'PDS-Playground-Browser' 9 | } 10 | 11 | { #category : #specs } 12 | PDSAlgorithmsList class >> defaultSpec [ 13 | ^ SpecColumnLayout composed 14 | newRow: [ :row | row add: #description ] height: self labelHeight; 15 | add: #list; 16 | yourself 17 | ] 18 | 19 | { #category : #'as yet unclassified' } 20 | PDSAlgorithmsList >> availablePlaygrounds [ 21 | 22 | ^ PDSAlgorithmPlayground allSubclasses 23 | ] 24 | 25 | { #category : #accessing } 26 | PDSAlgorithmsList >> description [ 27 | ^ description 28 | ] 29 | 30 | { #category : #initialization } 31 | PDSAlgorithmsList >> initializeWidgets [ 32 | description := self newLabel. 33 | list := (self instantiate: ListPresenter) 34 | displayBlock: [ :item | item algorithmName ]; 35 | sortingBlock: [ :itemA :itemB | itemA algorithmName < itemB algorithmName ]. 36 | description label: 'PDS Algorithms'. 37 | list items: self availablePlaygrounds. 38 | self focusOrder add: list 39 | ] 40 | 41 | { #category : #'accessing - private' } 42 | PDSAlgorithmsList >> items [ 43 | list items: { 'Bloom Filter'.'Hiperloglog' } 44 | ] 45 | 46 | { #category : #accessing } 47 | PDSAlgorithmsList >> list [ 48 | ^ list 49 | ] 50 | 51 | { #category : #accessing } 52 | PDSAlgorithmsList >> title [ 53 | ^ 'PDS Algorithms' 54 | ] 55 | 56 | { #category : #api } 57 | PDSAlgorithmsList >> whenSelectedItemChanged: aBlock [ 58 | list whenSelectedItemChanged: aBlock 59 | ] 60 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSBloomFilterPlayground.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterPlayground, 3 | #superclass : #PDSAlgorithmPlayground, 4 | #instVars : [ 5 | 'toolbar', 6 | 'content' 7 | ], 8 | #category : #'PDS-Playground-Membership' 9 | } 10 | 11 | { #category : #'as yet unclassified' } 12 | PDSBloomFilterPlayground class >> algorithmName [ 13 | ^ 'Bloom Filter' 14 | ] 15 | 16 | { #category : #specs } 17 | PDSBloomFilterPlayground class >> defaultSpec [ 18 | ^ SpecColumnLayout composed 19 | newRow: [ :row | row add:#toolbar] height: self toolbarHeight; 20 | add: #content; 21 | yourself 22 | ] 23 | 24 | { #category : #'instance creation' } 25 | PDSBloomFilterPlayground class >> open [ 26 | 27 | self new openWithSpec 28 | ] 29 | 30 | { #category : #accessing } 31 | PDSBloomFilterPlayground >> content [ 32 | ^ content 33 | ] 34 | 35 | { #category : #initialization } 36 | PDSBloomFilterPlayground >> extent [ 37 | ^ 800 @ 800 38 | ] 39 | 40 | { #category : #initialization } 41 | PDSBloomFilterPlayground >> initializePresenter [ 42 | toolbar whenBloomCreatedDo: [ :bloom | 43 | content bloom: bloom 44 | ] 45 | ] 46 | 47 | { #category : #initialization } 48 | PDSBloomFilterPlayground >> initializeWidgets [ 49 | toolbar := self instantiate: PDSBloomFilterPlaygroundToolbar. 50 | content := self instantiate: PDSBloomFilterPlaygroundContent. 51 | self focusOrder add: toolbar; add: content 52 | ] 53 | 54 | { #category : #accessing } 55 | PDSBloomFilterPlayground >> name [ 56 | ^ 'Bloom Filter' 57 | ] 58 | 59 | { #category : #initialization } 60 | PDSBloomFilterPlayground >> title [ 61 | ^ 'BloomFilter Playground' 62 | ] 63 | 64 | { #category : #accessing } 65 | PDSBloomFilterPlayground >> toolbar [ 66 | ^ toolbar 67 | ] 68 | -------------------------------------------------------------------------------- /src/PDS-Grapher/PDSBoundCursorFollower.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBoundCursorFollower, 3 | #superclass : #RTCursorFollower, 4 | #instVars : [ 5 | 'bindFunction' 6 | ], 7 | #category : #'PDS-Grapher' 8 | } 9 | 10 | { #category : #accessing } 11 | PDSBoundCursorFollower >> bindFunction: anObject [ 12 | bindFunction := anObject 13 | ] 14 | 15 | { #category : #drawing } 16 | PDSBoundCursorFollower >> getLabelYValueFor: pp [ 17 | ^ (labelYTextConvertion rtValue: pp y) asString 18 | ] 19 | 20 | { #category : #drawing } 21 | PDSBoundCursorFollower >> getPositionFromPixel: p [ 22 | ^ builder getPositionFromPixelPosition: p 23 | ] 24 | 25 | { #category : #accessing } 26 | PDSBoundCursorFollower >> render [ 27 | | p pp boundedPosition boundedPoint | 28 | builder view 29 | when: TRMouseMove 30 | do: [ :evt | 31 | p := evt position. 32 | pp := self getPositionFromPixel: p. 33 | bindFunction isNil ifTrue: [ 34 | boundedPoint := p. 35 | boundedPosition := pp. 36 | ] ifFalse: [ 37 | boundedPosition := pp x @ (bindFunction value: pp x). 38 | boundedPoint := builder getPixelPositionOf: boundedPosition. 39 | ]. 40 | (self isPointOverTheBuilder: boundedPoint) 41 | ifTrue: [ self createLinesAndLabelsIfNecessary. 42 | lineH 43 | from: 0 @ boundedPoint y 44 | to: builder extent x @ boundedPoint y. 45 | lineV 46 | from: boundedPoint x @ 0 47 | to: boundedPoint x @ builder extent y negated. 48 | labelV text: (self getLabelXValueFor: boundedPosition). 49 | labelV translateTo: boundedPoint x @ builder extent y negated. 50 | labelH text: (self getLabelYValueFor: boundedPosition). 51 | labelH translateTo: builder extent x @ boundedPoint y. 52 | self updateCanvas ] 53 | ifFalse: [ self removeLinesAndLabelsIfNecessary ] ] 54 | ] 55 | -------------------------------------------------------------------------------- /doc/tutorial/01-inspector-parameters-extension.ombu: -------------------------------------------------------------------------------- 1 | OmEntry { #tags : { #prior : OmNullReference [ ], #self : OmReference [ '1' ] }, #content : EpMethodModification { #oldMethod : RGMethodDefinition { #annotations : IdentityDictionary { #className : #PDSBloomFilter, #isMetaSide : false }, #name : #'gtInspectorParametersTableIn:', #protocol : #PDS-Workshop-Inspector-Extensions, #sourceCode : 'gtInspectorParametersTableIn: composite\r\t\r\r\t^ composite fastTable\r\t\ttitle: \'Parameters\';\r\t\tdisplay: [ {(\'Target Elements (n)\' -> self targetElements).\r\t\t\t(\'Target FPP\' -> self targetFpp).\r\t\t\t(\'Number of hashes (k)\' -> self hashes).\r\t\t\t(\'Current Elements\' -> self size).\r\t\t\t(\'Current FPP\' -> self fpp)} ];\r\t\tcolumn: \'Name\'\r\t\t\tevaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each key ];\r\t\tcolumn: \'Value\'\r\t\t\tevaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each value ]', #stamp : 'RafaelLuque 4/29/2019 15:06', #package : #PDS-Workshop-Inspector-Extensions }, #newMethod : RGMethodDefinition { #annotations : IdentityDictionary { #className : #PDSBloomFilter, #isMetaSide : false }, #name : #'gtInspectorParametersTableIn:', #protocol : #'*PDS-Workshop-Inspector-Extensions', #sourceCode : 'gtInspectorParametersTableIn: composite\r\t\r\r\t^ composite fastTable\r\t\ttitle: \'Parameters\';\r\t\tdisplay: [ {(\'Target Elements (n)\' -> self targetElements).\r\t\t\t(\'Target FPP\' -> self targetFpp).\r\t\t\t(\'Number of hashes (k)\' -> self hashes).\r\t\t\t(\'Current Elements\' -> self size).\r\t\t\t(\'Current FPP\' -> self fpp)} ];\r\t\tcolumn: \'Name\'\r\t\t\tevaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each key ];\r\t\tcolumn: \'Value\'\r\t\t\tevaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each value ]', #stamp : 'RafaelLuque 4/29/2019 15:06', #package : #PDS-Workshop-Inspector-Extensions } } } -------------------------------------------------------------------------------- /src/PDS-Grapher/PDSStandardDeviationDecorator.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSStandardDeviationDecorator, 3 | #superclass : #RTAbstractGrapherDecorator, 4 | #instVars : [ 5 | 'color', 6 | 'points' 7 | ], 8 | #category : #'PDS-Grapher' 9 | } 10 | 11 | { #category : #accessing } 12 | PDSStandardDeviationDecorator >> color [ 13 | ^ color 14 | ] 15 | 16 | { #category : #accessing } 17 | PDSStandardDeviationDecorator >> color: aColor [ 18 | color := aColor. 19 | ] 20 | 21 | { #category : #initialization } 22 | PDSStandardDeviationDecorator >> initialize [ 23 | super initialize. 24 | self color: (Color gray alpha: 0.5). 25 | ] 26 | 27 | { #category : #accessing } 28 | PDSStandardDeviationDecorator >> points [ 29 | ^ points 30 | ] 31 | 32 | { #category : #accessing } 33 | PDSStandardDeviationDecorator >> points: pts [ 34 | points := pts 35 | ] 36 | 37 | { #category : #drawing } 38 | PDSStandardDeviationDecorator >> render [ 39 | | dev svg k str index pointsByAbscissa | 40 | pointsByAbscissa := points groupedBy: #x. 41 | dev := (pointsByAbscissa 42 | collect: [ :data | (data collect: #y) average + (data collect: #y) stdev ]) values. 43 | svg := TRSVGPath new. 44 | svg color: color. 45 | k := builder getPixelPositionOf: pointsByAbscissa keys first @ dev first. 46 | str := 'M' , k x asFloat asString , ' ' , k y asString. 47 | (2 to: dev size) 48 | do: [ :i | 49 | | x y | 50 | x := pointsByAbscissa keys at: i. 51 | y := dev at: i. 52 | k := builder getPixelPositionOf: x @ y. 53 | str := str , 'L' , k x asFloat asString , ' ' , k y asString ]. 54 | dev := dev := (pointsByAbscissa 55 | collect: [ :data | (data collect: #y) average - (data collect: #y) stdev ]) values. 56 | (1 to: dev size) 57 | do: [ :i | 58 | | x y | 59 | index := dev size - i + 1. 60 | x := pointsByAbscissa keys at: index. 61 | y := dev at: index. 62 | k := builder getPixelPositionOf: x @ y. 63 | str := str , 'L' , k x asFloat asString , ' ' , k y asString ]. 64 | str := str , 'Z'. 65 | svg path: str. 66 | builder view canvas addShape: svg 67 | ] 68 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSAlgorithmsBrowser.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSAlgorithmsBrowser, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'algorithms', 6 | 'view', 7 | 'state' 8 | ], 9 | #category : #'PDS-Playground-Browser' 10 | } 11 | 12 | { #category : #specs } 13 | PDSAlgorithmsBrowser class >> defaultSpec [ 14 | ^ SpecRowLayout composed 15 | newColumn: [ :c | c add: #algorithms ] width: 224; 16 | addSplitter; 17 | newColumn: [ :c | c add: #view ]; 18 | yourself 19 | ] 20 | 21 | { #category : #'instance creation' } 22 | PDSAlgorithmsBrowser class >> open [ 23 | 24 | 25 | self new openWithSpec 26 | ] 27 | 28 | { #category : #accessing } 29 | PDSAlgorithmsBrowser >> algorithms [ 30 | ^ algorithms 31 | ] 32 | 33 | { #category : #'initialize-release' } 34 | PDSAlgorithmsBrowser >> defaultView [ 35 | | cm | 36 | cm := self newLabel. 37 | cm label: 'Select something from the list please.'. 38 | ^cm 39 | ] 40 | 41 | { #category : #api } 42 | PDSAlgorithmsBrowser >> extent [ 43 | ^ 1024 @ 800 44 | ] 45 | 46 | { #category : #initialization } 47 | PDSAlgorithmsBrowser >> initializePresenter [ 48 | algorithms 49 | whenSelectedItemChanged: [ :aPlayground | 50 | view := self instantiate: aPlayground. 51 | self needRebuild: false. 52 | self buildWithSpec ] 53 | ] 54 | 55 | { #category : #initialization } 56 | PDSAlgorithmsBrowser >> initializeWidgets [ 57 | algorithms := self instantiate: PDSAlgorithmsList. 58 | "state := $h." 59 | view := self defaultView 60 | ] 61 | 62 | { #category : #accessing } 63 | PDSAlgorithmsBrowser >> state [ 64 | ^ state 65 | ] 66 | 67 | { #category : #initialization } 68 | PDSAlgorithmsBrowser >> title [ 69 | ^'PDS Algorithms Viewer' 70 | ] 71 | 72 | { #category : #accessing } 73 | PDSAlgorithmsBrowser >> view [ 74 | ^ view 75 | ] 76 | 77 | { #category : #'as yet unclassified' } 78 | PDSAlgorithmsBrowser >> widgetFor: aData [ 79 | | cm | 80 | aData = 'Bloom Filter' 81 | ifTrue: [ cm := self instantiate: PDSBloomFilterPlayground. 82 | ^ cm ] 83 | ifFalse: [ ^ self defaultView ] 84 | ] 85 | -------------------------------------------------------------------------------- /src/PDS-Core-Tests/BloomFilterTests.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #BloomFilterTests, 3 | #superclass : #TestCase, 4 | #category : #'PDS-Core-Tests-Membership' 5 | } 6 | 7 | { #category : #tests } 8 | BloomFilterTests >> bloomHashesForElements: expectedElements fpp: estimatedFalsePositiveProbability [ 9 | | m | 10 | m := self bloomSizeForElements: expectedElements fpp: estimatedFalsePositiveProbability. 11 | ^ (m * 2 ln) / expectedElements 12 | ] 13 | 14 | { #category : #tests } 15 | BloomFilterTests >> bloomSizeForElements: expectedElements fpp: estimatedFalsePositiveProbability [ 16 | ^ -1 * (expectedElements * estimatedFalsePositiveProbability ln ) / ((2 ln) raisedTo: 2) 17 | ] 18 | 19 | { #category : #tests } 20 | BloomFilterTests >> testFalsePositiveProbability [ 21 | | filter | 22 | filter := PDSBloomFilter new: 10 fpp: 0.1. 23 | self assert: filter fpp equals: 0. 24 | 1 to: 6 do: [ :each | filter add: each asByteArray]. 25 | self assert: filter fpp equals: ( 1 - (-1 * filter hashes * 6 / filter storageSize) exp 26 | raisedTo: filter hashes) 27 | ] 28 | 29 | { #category : #tests } 30 | BloomFilterTests >> testFilterCreation [ 31 | | filter | 32 | filter := PDSBloomFilter new: 100 fpp: 0.1. 33 | self assert: filter isNotNil 34 | ] 35 | 36 | { #category : #tests } 37 | BloomFilterTests >> testFilterInitialized [ 38 | | filter | 39 | filter := PDSBloomFilter new: 100 fpp: 0.1. 40 | self 41 | assert: filter storageSize 42 | equals: ((self bloomSizeForElements: 100 fpp: 0.1) roundUpTo: 1). 43 | self 44 | assert: filter hashes 45 | equals: ((self bloomHashesForElements: 100 fpp: 0.1) roundUpTo: 1) 46 | ] 47 | 48 | { #category : #tests } 49 | BloomFilterTests >> testForAddedElement [ 50 | | filter element | 51 | filter := PDSBloomFilter new: 10 fpp: 0.1. 52 | element := 42 asByteArray. 53 | filter add: element. 54 | self assert: (filter contains: element) 55 | ] 56 | 57 | { #category : #tests } 58 | BloomFilterTests >> testForNonAddedElement [ 59 | | filter element | 60 | filter := PDSBloomFilter new: 10 fpp: 0.1. 61 | element := 42 asByteArray. 62 | self assert: (filter contains: element) not. 63 | ] 64 | -------------------------------------------------------------------------------- /src/PDS-GToolkit-Extensions/PDSBloomFilter.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #PDSBloomFilter } 2 | 3 | { #category : #'*PDS-GToolkit-Extensions' } 4 | PDSBloomFilter >> gtFPPCurveFor: aView [ 5 | 6 | ^ aView explicit 7 | title: 'FPP Curve'; 8 | priority: 20; 9 | stencil: [ BlElement new 10 | layout: BlLinearLayout vertical; 11 | constraintsDo: [ :c | 12 | c horizontal matchParent. 13 | c vertical matchParent ]; 14 | padding: (BlInsets all: 5); 15 | addChild: 16 | (BrButton new 17 | look: BrGlamorousButtonWithLabelLook; 18 | label: 'Inspect in Morphic'; 19 | action: [ GTInspector inspect: self fppCurve ]; 20 | yourself); 21 | addChild: self fppCurve build view setUpCanvas buildMorph imageForm asElement; 22 | yourself ] 23 | 24 | ] 25 | 26 | { #category : #'*PDS-GToolkit-Extensions' } 27 | PDSBloomFilter >> gtParametersViewFor: aView [ 28 | 29 | ^ aView columnedList 30 | title: 'Parameters'; 31 | priority: 10; 32 | items: [ {('Target Elements (n)' -> self targetElements). 33 | ('Target FPP' -> self targetFpp). 34 | ('Number of hashes (k)' -> self hashes). 35 | ('Current Elements' -> self size). 36 | ('Current FPP' -> self fpp)} ]; 37 | column: 'Parameter' 38 | do: [ :column | 39 | column 40 | item: [ :assoc | assoc key ]; 41 | matchParent ]; 42 | column: 'Value' 43 | do: [ :column | 44 | column 45 | item: [ :assoc | assoc value ]; 46 | matchParent ] 47 | ] 48 | 49 | { #category : #'*PDS-GToolkit-Extensions' } 50 | PDSBloomFilter >> gtStorageBitSetViewFor: aView [ 51 | 52 | ^ (bitStorage gtBitSetViewFor: aView) priority: 40; title: 'Storage BitSet' 53 | ] 54 | 55 | { #category : #'*PDS-GToolkit-Extensions' } 56 | PDSBloomFilter >> gtStorageParametersViewFor: aView [ 57 | 58 | ^ (bitStorage gtParametersViewFor: aView) priority: 30; title: 'Storage Parameters' 59 | ] 60 | -------------------------------------------------------------------------------- /src/PDS-Core/PDSBitArray.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBitArray, 3 | #superclass : #Object, 4 | #instVars : [ 5 | 'storage', 6 | 'size', 7 | 'occupancy' 8 | ], 9 | #category : #'PDS-Core-Collections' 10 | } 11 | 12 | { #category : #'instance creation' } 13 | PDSBitArray class >> new: aNumberOfBits [ 14 | ^ self new 15 | storage: (ByteArray new: (aNumberOfBits / 8 roundUpTo: 1)); 16 | size: aNumberOfBits; 17 | yourself 18 | ] 19 | 20 | { #category : #'member lookup' } 21 | PDSBitArray >> bitAt: index [ 22 | | byteIndex byteValue mask | 23 | byteIndex := (index / 8 )ceiling. 24 | byteValue := storage byteAt: byteIndex. 25 | mask := 2 raisedTo: byteIndex * 8 - index. 26 | (byteValue & mask) = 0 27 | ifTrue: [ ^ 0 ] 28 | ifFalse: [ ^ 1 ] 29 | ] 30 | 31 | { #category : #'accessing - private' } 32 | PDSBitArray >> incOccupancy [ 33 | occupancy := occupancy + 1 34 | ] 35 | 36 | { #category : #'accessing - private' } 37 | PDSBitArray >> initialize [ 38 | occupancy := 0 39 | ] 40 | 41 | { #category : #accessing } 42 | PDSBitArray >> occupancy [ 43 | ^ occupancy 44 | ] 45 | 46 | { #category : #accessing } 47 | PDSBitArray >> occupancyInPercent [ 48 | ^ (self occupancy * 100 / self size) asFloat round: 2 49 | ] 50 | 51 | { #category : #'accessing - private' } 52 | PDSBitArray >> setBitAt: index [ 53 | | byteIndex byteValue bitChanged | 54 | byteIndex := (index / 8) ceiling. 55 | byteValue := storage byteAt: byteIndex. 56 | bitChanged := (byteValue bitAnd: (2 raisedTo: byteIndex * 8 - index)) == 0. 57 | bitChanged 58 | ifTrue: [ storage 59 | byteAt: byteIndex 60 | put: byteValue | (2 raisedTo: byteIndex * 8 - index). 61 | self incOccupancy ]. 62 | ^ bitChanged 63 | ] 64 | 65 | { #category : #'accessing - private' } 66 | PDSBitArray >> size [ 67 | ^ size 68 | ] 69 | 70 | { #category : #'accessing - private' } 71 | PDSBitArray >> size: aNumberOfBits [ 72 | size := aNumberOfBits 73 | ] 74 | 75 | { #category : #accessing } 76 | PDSBitArray >> storage [ 77 | ^ storage 78 | ] 79 | 80 | { #category : #'accessing - private' } 81 | PDSBitArray >> storage: anObject [ 82 | storage := anObject 83 | ] 84 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSBloomFilterViewer.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterViewer, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'bits', 6 | 'fpp', 7 | 'bloom' 8 | ], 9 | #category : #'PDS-Playground-Membership' 10 | } 11 | 12 | { #category : #specs } 13 | PDSBloomFilterViewer class >> defaultSpec [ 14 | ^ SpecRowLayout composed 15 | add: #bits; 16 | addSplitter; 17 | add: #fpp; 18 | yourself 19 | ] 20 | 21 | { #category : #accessing } 22 | PDSBloomFilterViewer >> bits [ 23 | ^ bits 24 | ] 25 | 26 | { #category : #api } 27 | PDSBloomFilterViewer >> bloom: aBloomFilter [ 28 | bloom := aBloomFilter. 29 | bits 30 | roassalScript: [ :view :canvas | 31 | | b | 32 | view @ RTDraggableView. 33 | view canvas addMenu: 'In' callback: [ RTZoomInMove on: view ]. 34 | view canvas addMenu: 'Out' callback: [ RTZoomOutMove on: view ]. 35 | view canvas 36 | addMenu: 'Center' 37 | callback: [ view canvas focusOnCenterScaled. 38 | view signalUpdate ]. 39 | b := RTMondrian new. 40 | b view: view. 41 | bloom drawBitSetOn: b. 42 | canvas extent: (self extent x / 2) @ self extent y. 43 | canvas focusOnCenterScaled. 44 | ]; 45 | refresh. 46 | fpp 47 | roassalScript: [ :view :canvas | 48 | | g | 49 | view @ RTDraggableView. 50 | view canvas addMenu: 'In' callback: [ RTZoomInMove on: view ]. 51 | view canvas addMenu: 'Out' callback: [ RTZoomOutMove on: view ]. 52 | view canvas 53 | addMenu: 'Center' 54 | callback: [ view canvas focusOnCenterScaled. 55 | view signalUpdate ]. 56 | g := bloom fppCurve. 57 | g view: view. 58 | g build. 59 | canvas extent: (self extent x / 2) @ self extent y. 60 | canvas focusOnCenterScaled. 61 | ]; 62 | refresh 63 | ] 64 | 65 | { #category : #api } 66 | PDSBloomFilterViewer >> extent [ 67 | ^ 800 @ 600 68 | ] 69 | 70 | { #category : #accessing } 71 | PDSBloomFilterViewer >> fpp [ 72 | ^ fpp 73 | ] 74 | 75 | { #category : #initialization } 76 | PDSBloomFilterViewer >> initializeWidgets [ 77 | bits := self instantiate: PDSRoassalViewer. 78 | fpp := self instantiate: PDSRoassalViewer. 79 | bits 80 | labelText: 'Bloom Filter BitSet'. fpp 81 | labelText: 'False-Positive Probability Curve'. 82 | bits roassalScript: [ :view :canvas | ]. 83 | fpp roassalScript: [ :view :canvas | ]. 84 | self focusOrder 85 | add: bits; 86 | add: fpp 87 | ] 88 | 89 | { #category : #api } 90 | PDSBloomFilterViewer >> refresh [ 91 | bits refresh. 92 | fpp refresh. 93 | ] 94 | 95 | { #category : #api } 96 | PDSBloomFilterViewer >> title [ 97 | ^ 'Bloom Filter Viewer' 98 | ] 99 | -------------------------------------------------------------------------------- /doc/tutorial/01-create-empty-example.ombu: -------------------------------------------------------------------------------- 1 | OmEntry { #tags : { #prior : OmReference [ '6' ], #self : OmReference [ '7' ] }, #content : EpCategoryAddition { #classCategoryName : #PDS-Workshop-Examples, #affectedPackageName : #PDS-Workshop-Examples } } OmEntry { #tags : { #prior : OmReference [ '7' ], #self : OmReference [ '8' ] }, #content : EpClassAddition { #class : RGClassDefinition { #annotations : IdentityDictionary { #traitCompositionSource : '{}', #definitionSource : 'Object subclass: #PDSWBloomFilterExamples\r\tinstanceVariableNames: \'\'\r\tclassVariableNames: \'\'\r\tpoolDictionaries: \'\'\r\tcategory: \'PDS-Workshop-Examples\'', #superclassName : 'Object' }, #name : #PDSWBloomFilterExamples, #methods : IdentityDictionary { }, #protocols : Set [ ], #instanceVariables : OrderedCollection [ ], #metaClass : RGMetaclassDefinition { #annotations : IdentityDictionary { #traitCompositionSource : '{}', #definitionSource : 'PDSWBloomFilterExamples class\r\tinstanceVariableNames: \'\'' }, #name : #'PDSWBloomFilterExamples class', #methods : IdentityDictionary { }, #protocols : Set [ ], #instanceVariables : OrderedCollection [ ], #baseClass : @6 }, #comment : RGCommentDefinition { #annotations : IdentityDictionary { #className : #PDSWBloomFilterExamples, #isMetaSide : false }, #parent : @6, #content : '' }, #classVariables : OrderedCollection [ ], #category : #PDS-Workshop-Examples, #package : #PDS-Workshop-Examples, #sharedPools : OrderedCollection [ ] } } } OmEntry { #tags : { #prior : OmReference [ '8' ], #self : OmReference [ '9' ] }, #content : EpMethodModification { #oldMethod : RGMethodDefinition { #annotations : IdentityDictionary { #className : #PDSWBloomFilterExamples, #isMetaSide : false }, #name : #emptyFilter, #protocol : #'as yet unclassified', #sourceCode : 'emptyFilter\r\r| filter |\rfilter := PDSBloomFilter new: 10 fpp: 0.03.\rself assert: filter size equals: 0.\rself assert: filter targetElements equals: 10.\rself assert: filter targetFpp equals: 0.03.\r^ filter', #stamp : 'RafaelLuque 4/29/2019 15:17', #package : #PDS-Workshop-Examples }, #newMethod : RGMethodDefinition { #annotations : IdentityDictionary { #className : #PDSWBloomFilterExamples, #isMetaSide : false }, #name : #emptyFilter, #protocol : #example, #sourceCode : 'emptyFilter\r\r| filter |\rfilter := PDSBloomFilter new: 10 fpp: 0.03.\rself assert: filter size equals: 0.\rself assert: filter targetElements equals: 10.\rself assert: filter targetFpp equals: 0.03.\r^ filter', #stamp : 'RafaelLuque 4/29/2019 15:17', #package : #PDS-Workshop-Examples } } } -------------------------------------------------------------------------------- /src/PDS-Inspector-Extensions/PDSBloomFilter.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #PDSBloomFilter } 2 | 3 | { #category : #'*PDS-Inspector-Extensions' } 4 | PDSBloomFilter >> drawBitSetOn: aMondrian [ 5 | bitStorage drawBitSetOn: aMondrian 6 | ] 7 | 8 | { #category : #'*PDS-Inspector-Extensions' } 9 | PDSBloomFilter >> fppCurve [ 10 | | fpp ds1 g p ds | 11 | g := RTGrapher new. 12 | fpp := [ :s | 13 | 1 - (self hashes * s / self storageSize) negated exp 14 | raisedTo: self hashes ]. 15 | ds1 := RTData new. 16 | ds1 label: 'FPP curve'. 17 | ds1 noDot. 18 | ds1 points: (0.0 to: self targetElements * 1.5 by: 1). 19 | ds1 connectColor: Color blue. 20 | ds1 x: #yourself. 21 | ds1 y: fpp. 22 | g add: ds1. 23 | p := RTHorizontalLineDecorator new. 24 | p add: (fpp value: self targetElements). 25 | p shape line color: (Color blue alpha: 0.3). 26 | g addDecorator: p. 27 | p := RTVerticalLineDecorator new. 28 | p add: self targetElements. 29 | p shape line color: (Color blue alpha: 0.3). 30 | g addDecorator: p. 31 | ds := RTData new. 32 | ds label: 'Current FPP'. 33 | ds connectColor: (Color red alpha: 0.3). 34 | ds dotShape cross color: (Color red alpha: 0.3). 35 | ds points: {self size}. 36 | ds x: #yourself. 37 | ds y: fpp. 38 | g add: ds. 39 | g 40 | addDecorator: 41 | (PDSBoundCursorFollower new 42 | color: Color blue; 43 | bindFunction: fpp). 44 | g legend right. 45 | g axisX title: 'Elements Added'. 46 | g axisY title: 'False Positive Probability (FPP)'. 47 | ^ g 48 | ] 49 | 50 | { #category : #'*PDS-Inspector-Extensions' } 51 | PDSBloomFilter >> gtInspectorFPPCurveIn: composite [ 52 | 53 | ^ composite roassal2 54 | title: 'FPP Curve'; 55 | initializeView: [ self fppCurve ] 56 | ] 57 | 58 | { #category : #'*PDS-Inspector-Extensions' } 59 | PDSBloomFilter >> gtInspectorParametersIn: composite [ 60 | 61 | ^ composite fastTable 62 | title: 'Parameters'; 63 | display: [ {('Target Elements (n)' -> self targetElements). 64 | ('Target FPP' -> self targetFpp). 65 | ('Number of hashes (k)' -> self hashes). 66 | ('Current Elements' -> self size). 67 | ('Current FPP' -> self fpp)} ]; 68 | column: 'Name' 69 | evaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each key ]; 70 | column: 'Value' 71 | evaluated: [ :each | GTObjectPrinter asTruncatedTextFrom: each value ] 72 | ] 73 | 74 | { #category : #'*PDS-Inspector-Extensions' } 75 | PDSBloomFilter >> gtInspectorStorageBitSetIn: composite [ 76 | 77 | ^ (bitStorage gtInspectorBitSetIn: composite) 78 | title: 'Storage BitSet' 79 | ] 80 | 81 | { #category : #'*PDS-Inspector-Extensions' } 82 | PDSBloomFilter >> gtInspectorStorageParametersIn: composite [ 83 | 84 | ^ (bitStorage 85 | gtInspectorParametersIn: composite) 86 | title: 'Storage Parameters' 87 | ] 88 | -------------------------------------------------------------------------------- /src/PDS-Core-Examples/PDSBloomFilterExamples.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterExamples, 3 | #superclass : #Object, 4 | #category : #'PDS-Core-Examples-Membership' 5 | } 6 | 7 | { #category : #accessing } 8 | PDSBloomFilterExamples >> emptyBloomFilter [ 9 | 10 | | bloom | 11 | bloom := PDSBloomFilter new: 10 fpp: 0.1. 12 | self assert: bloom size equals: 0. 13 | self assert: bloom hashes equals: 4. 14 | self assert: bloom storageSize equals: 48. 15 | ^ bloom 16 | ] 17 | 18 | { #category : #accessing } 19 | PDSBloomFilterExamples >> fullBloomFilter [ 20 | 21 | | bloom | 22 | bloom := self emptyBloomFilter. 23 | 1 to: bloom targetElements 24 | do: [ :each | bloom add: each asString asByteArray ]. 25 | ^ bloom 26 | ] 27 | 28 | { #category : #accessing } 29 | PDSBloomFilterExamples >> oneBillionBloomFilter [ 30 | 31 | | bloom | 32 | bloom := PDSBloomFilter new: 1000000000 fpp: 0.02. 33 | self assert: bloom size equals: 0. 34 | self assert: bloom hashes equals: 6. 35 | self assert: bloom storageSize equals: 8142363337. 36 | ^ bloom 37 | ] 38 | 39 | { #category : #accessing } 40 | PDSBloomFilterExamples >> withBerlinBloomFilter [ 41 | 42 | | bloom | 43 | bloom := self emptyBloomFilter. 44 | bloom add: 'Berlin' asByteArray. 45 | self assert: bloom size equals: 1. 46 | ^ bloom 47 | ] 48 | 49 | { #category : #accessing } 50 | PDSBloomFilterExamples >> withMadridAndBarcelonaBloomFilter [ 51 | 52 | | bloom | 53 | bloom := self withMadridBloomFilter. 54 | bloom add: 'Barcelona' asByteArray. 55 | self assert: bloom size equals: 2. 56 | ^ bloom 57 | ] 58 | 59 | { #category : #accessing } 60 | PDSBloomFilterExamples >> withMadridAndBarcelonaCheckBarcelonaBloomFilter [ 61 | 62 | | bloom | 63 | bloom := self withMadridBloomFilter. 64 | bloom add: 'Barcelona' asByteArray. 65 | self assert: bloom size equals: 2. 66 | self assert: (bloom contains: 'Barcelona' asByteArray). 67 | ^ bloom 68 | ] 69 | 70 | { #category : #accessing } 71 | PDSBloomFilterExamples >> withMadridAndBarcelonaCheckBerlinBloomFilter [ 72 | 73 | | bloom | 74 | bloom := self withMadridBloomFilter. 75 | bloom add: 'Barcelona' asByteArray. 76 | self assert: bloom size equals: 2. 77 | self assert: (bloom contains: 'Berlin' asByteArray) not. 78 | ^ bloom 79 | ] 80 | 81 | { #category : #accessing } 82 | PDSBloomFilterExamples >> withMadridAndBarcelonaCheckRomaBloomFilter [ 83 | 84 | | bloom | 85 | bloom := self withMadridBloomFilter. 86 | bloom add: 'Barcelona' asByteArray. 87 | self assert: bloom size equals: 2. 88 | self assert: (bloom contains: 'Roma' asByteArray). 89 | ^ bloom 90 | ] 91 | 92 | { #category : #accessing } 93 | PDSBloomFilterExamples >> withMadridBloomFilter [ 94 | 95 | | bloom | 96 | bloom := self emptyBloomFilter. 97 | bloom add: 'Madrid' asByteArray. 98 | self assert: bloom size equals: 1. 99 | ^ bloom 100 | ] 101 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSBloomFilterActionForm.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterActionForm, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'header', 6 | 'description', 7 | 'input', 8 | 'button', 9 | 'result', 10 | 'actionBlock', 11 | 'bloom' 12 | ], 13 | #category : #'PDS-Playground-Membership' 14 | } 15 | 16 | { #category : #specs } 17 | PDSBloomFilterActionForm class >> defaultSpec [ 18 | ^ SpecColumnLayout composed 19 | add: #header; 20 | add: #description; 21 | newRow: [ :row | 22 | row 23 | add: #input; 24 | add: #button width: self buttonWidth ]; 25 | add: #result 26 | ] 27 | 28 | { #category : #api } 29 | PDSBloomFilterActionForm >> bloom: anObject [ 30 | bloom := anObject. 31 | self resultLabel: '' 32 | ] 33 | 34 | { #category : #accessing } 35 | PDSBloomFilterActionForm >> button [ 36 | ^ button 37 | ] 38 | 39 | { #category : #api } 40 | PDSBloomFilterActionForm >> buttonLabel: aString [ 41 | button label: aString 42 | ] 43 | 44 | { #category : #accessing } 45 | PDSBloomFilterActionForm >> description [ 46 | ^ description 47 | ] 48 | 49 | { #category : #api } 50 | PDSBloomFilterActionForm >> description: aString [ 51 | description label: aString 52 | ] 53 | 54 | { #category : #api } 55 | PDSBloomFilterActionForm >> ghostText: aString [ 56 | input ghostText: aString 57 | ] 58 | 59 | { #category : #accessing } 60 | PDSBloomFilterActionForm >> header [ 61 | ^ header 62 | ] 63 | 64 | { #category : #api } 65 | PDSBloomFilterActionForm >> header: aString [ 66 | header label: aString. 67 | ] 68 | 69 | { #category : #initialization } 70 | PDSBloomFilterActionForm >> initializePresenter [ 71 | button 72 | action: [ 73 | self performAction ] 74 | ] 75 | 76 | { #category : #initialization } 77 | PDSBloomFilterActionForm >> initializeWidgets [ 78 | header := self newLabel. 79 | description := self newLabel. 80 | input := self newTextInput autoAccept: true. 81 | button := self newButton. 82 | result := self newLabel. 83 | header 84 | label: 'Form Header'; 85 | emphasis: #bold. 86 | description label: 'Form description.'. 87 | input ghostText: 'e.g. Value'. 88 | input wantsVisualFeedback: false. 89 | button label: 'Action'. 90 | result 91 | label: 'Form result.'; 92 | color: Color blue. 93 | self focusOrder 94 | add: input; 95 | add: button 96 | ] 97 | 98 | { #category : #accessing } 99 | PDSBloomFilterActionForm >> input [ 100 | ^ input 101 | ] 102 | 103 | { #category : #actions } 104 | PDSBloomFilterActionForm >> performAction [ 105 | | resultValue | 106 | resultValue := actionBlock value: bloom value: input text. 107 | input text: ''. 108 | result label: resultValue asString 109 | ] 110 | 111 | { #category : #accessing } 112 | PDSBloomFilterActionForm >> result [ 113 | ^ result 114 | ] 115 | 116 | { #category : #api } 117 | PDSBloomFilterActionForm >> resultLabel: aString [ 118 | result label: aString 119 | ] 120 | 121 | { #category : #'api - events' } 122 | PDSBloomFilterActionForm >> whenActionChangedDo: aBlock [ 123 | actionBlock := aBlock 124 | ] 125 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSBloomFilterPlaygroundContent.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterPlaygroundContent, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'addAction', 6 | 'testAction', 7 | 'bloomInspector', 8 | 'viewer' 9 | ], 10 | #category : #'PDS-Playground-Membership' 11 | } 12 | 13 | { #category : #specs } 14 | PDSBloomFilterPlaygroundContent class >> defaultSpec [ 15 | ^ SpecColumnLayout composed 16 | newRow: [ :row | 17 | row 18 | newColumn: [ :column | 19 | column 20 | add: #addAction; 21 | add: #testAction ]; 22 | add: #bloomInspector ] 23 | height: 2 * (self labelHeight * 4 + self buttonHeight); 24 | newRow: [ :row | row add: #viewer ]; 25 | yourself 26 | ] 27 | 28 | { #category : #accessing } 29 | PDSBloomFilterPlaygroundContent >> addAction [ 30 | ^ addAction 31 | ] 32 | 33 | { #category : #api } 34 | PDSBloomFilterPlaygroundContent >> bloom: aBloom [ 35 | self addAction bloom: aBloom. 36 | self testAction bloom: aBloom. 37 | bloomInspector := GlamourPresentationPresenter new 38 | presentationClass: GTInspector 39 | startOn: aBloom. 40 | self viewer bloom: aBloom. 41 | self needRebuild: false. 42 | self buildWithSpecLayout: self class defaultSpec 43 | ] 44 | 45 | { #category : #accessing } 46 | PDSBloomFilterPlaygroundContent >> bloomInspector [ 47 | ^ bloomInspector 48 | ] 49 | 50 | { #category : #initialization } 51 | PDSBloomFilterPlaygroundContent >> extent [ 52 | ^ 800 @ 600 53 | ] 54 | 55 | { #category : #initialization } 56 | PDSBloomFilterPlaygroundContent >> initializePresenter [ 57 | addAction 58 | whenActionChangedDo: [ :filter :element | 59 | filter add: element asByteArray. 60 | self viewer refresh. 61 | element asString , ' added to filter!' ]. 62 | testAction 63 | whenActionChangedDo: [ :filter :element | 64 | | isContained | 65 | isContained := filter contains: element asByteArray. 66 | isContained 67 | ifTrue: [ element asString , ' maybe contained with FPP = ' 68 | , ((filter fpp * 100) round: 5) asString , '%' ] 69 | ifFalse: [ element asString , ' is not in the filter.' ] ] 70 | ] 71 | 72 | { #category : #initialization } 73 | PDSBloomFilterPlaygroundContent >> initializeWidgets [ 74 | addAction := self instantiate: PDSBloomFilterActionForm. 75 | testAction := self instantiate: PDSBloomFilterActionForm. 76 | bloomInspector := self newLabel. 77 | viewer := self instantiate: PDSBloomFilterViewer. 78 | addAction 79 | header: 'Add elements'; 80 | description: 'Write the text you want to add to the filter.'; 81 | ghostText: 'e.g. Madrid'; 82 | buttonLabel: 'Add'; 83 | resultLabel: ''. 84 | testAction 85 | header: 'Membership test'; 86 | description: 'Check if a given text is a member of the filter.'; 87 | ghostText: 'E.g. London'; 88 | buttonLabel: 'Test'; 89 | resultLabel: ''. 90 | self focusOrder 91 | add: addAction; 92 | add: testAction; 93 | add: bloomInspector; 94 | add: viewer 95 | ] 96 | 97 | { #category : #accessing } 98 | PDSBloomFilterPlaygroundContent >> testAction [ 99 | ^ testAction 100 | ] 101 | 102 | { #category : #initialization } 103 | PDSBloomFilterPlaygroundContent >> title [ 104 | ^ 'BloomFilter Playground Content' 105 | ] 106 | 107 | { #category : #accessing } 108 | PDSBloomFilterPlaygroundContent >> viewer [ 109 | ^ viewer 110 | ] 111 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSBloomFilterCreationForm.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterCreationForm, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'labelTargetElements', 6 | 'targetElements', 7 | 'labelFpp', 8 | 'targetFpp', 9 | 'areValidParameters', 10 | 'validationChangedBlock' 11 | ], 12 | #category : #'PDS-Playground-Membership' 13 | } 14 | 15 | { #category : #specs } 16 | PDSBloomFilterCreationForm class >> defaultSpec [ 17 | ^ SpecColumnLayout composed 18 | newRow: [ :row | 19 | row 20 | add: #labelTargetElements; 21 | add: #targetElements ] 22 | height: self inputTextHeight; 23 | newRow: [ :row | 24 | row 25 | add: #labelFpp; 26 | add: #targetFpp ] height: self inputTextHeight ; 27 | yourself 28 | ] 29 | 30 | { #category : #initialization } 31 | PDSBloomFilterCreationForm >> initializePresenter [ 32 | targetElements whenTextChanged: self validationBlock. 33 | targetFpp whenTextChanged: self validationBlock 34 | 35 | 36 | ] 37 | 38 | { #category : #initialization } 39 | PDSBloomFilterCreationForm >> initializeWidgets [ 40 | areValidParameters := false. 41 | labelTargetElements := self newLabel. 42 | targetElements := self newTextInput autoAccept: true. 43 | labelFpp := self newLabel. 44 | targetFpp := self newTextInput autoAccept: true. 45 | labelTargetElements 46 | label: 'Target elements (<= ' , self maxTargetElements asString , '):'. 47 | targetElements ghostText: 'e.g. 100'. 48 | labelFpp 49 | label: 50 | 'Target FPP (' , self minTargetFpp asString , ' - ' 51 | , self maxTargetFpp asString , '):'. 52 | targetFpp ghostText: 'e.g. 0.02'. 53 | self focusOrder 54 | add: targetElements; 55 | add: targetFpp 56 | ] 57 | 58 | { #category : #accessing } 59 | PDSBloomFilterCreationForm >> labelFpp [ 60 | ^ labelFpp 61 | ] 62 | 63 | { #category : #accessing } 64 | PDSBloomFilterCreationForm >> labelTargetElements [ 65 | ^ labelTargetElements 66 | ] 67 | 68 | { #category : #private } 69 | PDSBloomFilterCreationForm >> maxTargetElements [ 70 | ^ 200 71 | ] 72 | 73 | { #category : #private } 74 | PDSBloomFilterCreationForm >> maxTargetFpp [ 75 | ^ 0.99 76 | ] 77 | 78 | { #category : #private } 79 | PDSBloomFilterCreationForm >> minTargetFpp [ 80 | ^ 0.01 81 | ] 82 | 83 | { #category : #accessing } 84 | PDSBloomFilterCreationForm >> targetElements [ 85 | ^ targetElements 86 | ] 87 | 88 | { #category : #accessing } 89 | PDSBloomFilterCreationForm >> targetFpp [ 90 | ^ targetFpp 91 | ] 92 | 93 | { #category : #private } 94 | PDSBloomFilterCreationForm >> validateParameters [ 95 | ^ [ self validateTargetElements and: self validateTargetFpp ] 96 | on: Error 97 | do: [ false ] 98 | ] 99 | 100 | { #category : #private } 101 | PDSBloomFilterCreationForm >> validateTargetElements [ 102 | | targetElementsValue | 103 | targetElementsValue := targetElements text asInteger. 104 | ^ targetElementsValue isNotNil and: targetElementsValue <= self maxTargetElements 105 | ] 106 | 107 | { #category : #private } 108 | PDSBloomFilterCreationForm >> validateTargetFpp [ 109 | | targetFppValue | 110 | targetFppValue := targetFpp text asNumber. 111 | ^ targetFppValue isNotNil 112 | and: (targetFppValue between: 0.01 and: 1.0) 113 | ] 114 | 115 | { #category : #private } 116 | PDSBloomFilterCreationForm >> validationBlock [ 117 | ^ [ | newAreValidParameters | 118 | newAreValidParameters := self validateParameters. 119 | areValidParameters ~= newAreValidParameters 120 | ifTrue: [ areValidParameters := newAreValidParameters. 121 | validationChangedBlock 122 | ifNotNil: [ validationChangedBlock value: areValidParameters ] ] ] 123 | ] 124 | 125 | { #category : #'api - events' } 126 | PDSBloomFilterCreationForm >> whenParametersValidationResultChangedDo: aBlock [ 127 | "Set a block to perform when the parameters validation result changed. 128 | The block receives one argument with the new validation value as Boolean." 129 | validationChangedBlock := aBlock 130 | ] 131 | -------------------------------------------------------------------------------- /src/PDS-Core/PDSWorldMenu.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSWorldMenu, 3 | #superclass : #Object, 4 | #category : #'PDS-Core-World Menu' 5 | } 6 | 7 | { #category : #'world menu' } 8 | PDSWorldMenu class >> bloomFilterTutorialMenuOn: aBuilder [ 9 | 10 | (aBuilder item: #pdsBloomFilterTutorialMenu) 11 | parent: #pdsMenu; 12 | label: 'Bloom Filter tutorial'; 13 | target: self; 14 | selector: #openBloomFilterTutorial; 15 | order: 2; 16 | help: 'Open Bloom Filter interactive tutorial'; 17 | icon: self tutorialIcon 18 | ] 19 | 20 | { #category : #icons } 21 | PDSWorldMenu class >> browserIcon [ 22 | ^ PDSIcons flaskIcon scaledToSize: 24 @ 24 23 | ] 24 | 25 | { #category : #'world menu' } 26 | PDSWorldMenu class >> browserMenuOn: aBuilder [ 27 | 28 | (aBuilder item: #pdsBrowserMenu) 29 | parent: #pdsMenu; 30 | label: 'Algorithms Browser'; 31 | target: self; 32 | selector: #openBrowser; 33 | order: 10; 34 | help: 'Open the PDS Algorithms Browser'; 35 | icon: self browserIcon 36 | ] 37 | 38 | { #category : #'world menu' } 39 | PDSWorldMenu class >> documentationMenuOn: aBuilder [ 40 | 41 | (aBuilder item: #pdsDocumentationMenu) 42 | parent: #pdsMenu; 43 | label: 'Project documentation'; 44 | target: self; 45 | selector: #openPDSDocumentation; 46 | order: 1; 47 | help: 'Open PharoPDS documentation'; 48 | icon: self tutorialIcon 49 | ] 50 | 51 | { #category : #'api - actions' } 52 | PDSWorldMenu class >> ensureGToolkitAndDo: aBlock [ 53 | | result | 54 | self isGToolkitLoaded 55 | ifTrue: [ aBlock value ] 56 | ifFalse: [ result := UIManager default 57 | confirm: 58 | 'This documentation consists of interactive tutorials based and requires GToolkit installation. You can check the class comments or install dependencies now' 59 | trueChoice: 'Install now' 60 | falseChoice: 'Cancel'. 61 | result 62 | ifTrue: [ Metacello new 63 | baseline: #ProbabilisticDataStructures; 64 | repository: 'github://osoco/PharoPDS:master/src'; 65 | load: 'All' ] ] 66 | ] 67 | 68 | { #category : #testing } 69 | PDSWorldMenu class >> isGToolkitLoaded [ 70 | Smalltalk at: #GtInspector ifPresent: [ ^ true ]. 71 | ^ false 72 | ] 73 | 74 | { #category : #'world menu' } 75 | PDSWorldMenu class >> moldableDevelopmentTutorialMenuOn: aBuilder [ 76 | 77 | (aBuilder item: #pdsMoldableDevelopmentTutorialMenu) 78 | parent: #pdsMenu; 79 | label: 'Moldable development tutorial'; 80 | target: self; 81 | selector: #openMoldableDevelopmentTutorial; 82 | order: 3; 83 | help: 'Open the moldable development tutorial'; 84 | icon: self tutorialIcon; 85 | withSeparatorAfter 86 | ] 87 | 88 | { #category : #'api - actions' } 89 | PDSWorldMenu class >> openBloomFilterTutorial [ 90 | self ensureGToolkitAndDo: [ GtInspector openOnPlayBook: (GtDocument forClass: PDSBloomFilter) ] 91 | 92 | ] 93 | 94 | { #category : #'api - actions' } 95 | PDSWorldMenu class >> openBrowser [ 96 | PDSAlgorithmsBrowser open 97 | ] 98 | 99 | { #category : #'api - actions' } 100 | PDSWorldMenu class >> openMoldableDevelopmentTutorial [ 101 | self ensureGToolkitAndDo: [ GtInspector openOnPlayBook: (GtDocument forFile: (GtIcebergUtility documentAtPath: (Path * 'osoco'/ 'PharoPDS' / 'doc' / 'tutorial' / 'moldable-development-tutorial.pillar'))) ] 102 | 103 | ] 104 | 105 | { #category : #'api - actions' } 106 | PDSWorldMenu class >> openPDSDocumentation [ 107 | 108 | self 109 | ensureGToolkitAndDo: [ GtInspector 110 | openOnPlayBook: (GtDocument forClass: BaselineOfProbabilisticDataStructures) ] 111 | ] 112 | 113 | { #category : #icons } 114 | PDSWorldMenu class >> pds [ 115 | ^ PDSIcons rocketIcon scaledToSize: 24@24 116 | ] 117 | 118 | { #category : #'world menu' } 119 | PDSWorldMenu class >> rootMenuOn: aBuilder [ 120 | 121 | (aBuilder item: #pdsMenu) 122 | label: 'PharoPDS'; 123 | icon: self pds; 124 | order: 100 125 | ] 126 | 127 | { #category : #icons } 128 | PDSWorldMenu class >> tutorialIcon [ 129 | ^ PDSIcons mortarboardIcon scaledToSize: 24 @ 24 130 | ] 131 | -------------------------------------------------------------------------------- /src/PDS-Grapher/PDSFunctionBoundCursorFollower.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSFunctionBoundCursorFollower, 3 | #superclass : #RTAbstractGrapherDecorator, 4 | #instVars : [ 5 | 'lineH', 6 | 'canvas', 7 | 'lineV', 8 | 'labelH', 9 | 'labelV', 10 | 'labelXTextConvertion', 11 | 'labelYTextConvertion', 12 | 'color', 13 | 'bindFunction' 14 | ], 15 | #category : #'PDS-Grapher' 16 | } 17 | 18 | { #category : #accessing } 19 | PDSFunctionBoundCursorFollower >> bindFunction: anObject [ 20 | bindFunction := anObject 21 | ] 22 | 23 | { #category : #accessing } 24 | PDSFunctionBoundCursorFollower >> color [ 25 | ^ color 26 | ] 27 | 28 | { #category : #accessing } 29 | PDSFunctionBoundCursorFollower >> color: aColor [ 30 | color := aColor 31 | ] 32 | 33 | { #category : #createLinesAndLabels } 34 | PDSFunctionBoundCursorFollower >> createLinesAndLabelsIfNecessary [ 35 | lineH 36 | ifNil: [ lineH := TRLineShape new. 37 | lineH color: color. 38 | canvas := builder view canvas. 39 | canvas addShape: lineH. 40 | lineV := TRLineShape new. 41 | lineV color: color. 42 | canvas addShape: lineV. 43 | labelH := TRLabelShape new. 44 | labelH color: color. 45 | canvas addShape: labelH. 46 | labelV := TRLabelShape new. 47 | labelV color: color. 48 | canvas addShape: labelV ] 49 | ] 50 | 51 | { #category : #configuration } 52 | PDSFunctionBoundCursorFollower >> defaultColor [ 53 | ^ Color gray 54 | ] 55 | 56 | { #category : #drawing } 57 | PDSFunctionBoundCursorFollower >> getLabelXValueFor: pp [ 58 | ^ (labelXTextConvertion rtValue: pp x) asString 59 | ] 60 | 61 | { #category : #drawing } 62 | PDSFunctionBoundCursorFollower >> getLabelYValueFor: pp [ 63 | ^ (labelYTextConvertion rtValue: pp y) asString 64 | ] 65 | 66 | { #category : #drawing } 67 | PDSFunctionBoundCursorFollower >> getPositionFromPixel: p [ 68 | ^ builder 69 | getPositionFromPixelPosition: p 70 | ] 71 | 72 | { #category : #initialization } 73 | PDSFunctionBoundCursorFollower >> initialize [ 74 | super initialize. 75 | labelXTextConvertion := [ :v | v round: 2 ]. 76 | labelYTextConvertion := [ :v | v round: 2 ]. 77 | color := self defaultColor 78 | ] 79 | 80 | { #category : #accessing } 81 | PDSFunctionBoundCursorFollower >> labelXTextConvertion [ 82 | ^ labelXTextConvertion 83 | ] 84 | 85 | { #category : #accessing } 86 | PDSFunctionBoundCursorFollower >> labelXTextConvertion: aOneArgBlock [ 87 | labelXTextConvertion := aOneArgBlock 88 | ] 89 | 90 | { #category : #accessing } 91 | PDSFunctionBoundCursorFollower >> labelYTextConvertion [ 92 | ^ labelYTextConvertion 93 | ] 94 | 95 | { #category : #accessing } 96 | PDSFunctionBoundCursorFollower >> labelYTextConvertion: aOneArgBlock [ 97 | labelYTextConvertion := aOneArgBlock 98 | ] 99 | 100 | { #category : #createLinesAndLabels } 101 | PDSFunctionBoundCursorFollower >> removeLinesAndLabelsIfNecessary [ 102 | lineH notNil 103 | ifTrue: [ 104 | lineH remove. 105 | lineV remove. 106 | labelH remove. 107 | labelV remove. 108 | self updateCanvas. 109 | lineH := nil. 110 | lineV := nil. 111 | labelH := nil. 112 | labelV := nil ] 113 | ] 114 | 115 | { #category : #drawing } 116 | PDSFunctionBoundCursorFollower >> render [ 117 | | p pp boundedPosition boundedPoint | 118 | builder view 119 | when: TRMouseMove 120 | do: [ :evt | 121 | p := evt position. 122 | pp := self getPositionFromPixel: p. 123 | boundedPosition := pp x @ (bindFunction value: pp x). 124 | boundedPoint := builder getPixelPositionOf: boundedPosition. 125 | (self isPointOverTheBuilder: boundedPoint) 126 | ifTrue: [ self createLinesAndLabelsIfNecessary. 127 | lineH 128 | from: 0 @ boundedPoint y 129 | to: builder extent x @ boundedPoint y. 130 | lineV 131 | from: boundedPoint x @ 0 132 | to: boundedPoint x @ builder extent y negated. 133 | labelV text: (self getLabelXValueFor: boundedPosition). 134 | labelV translateTo: boundedPoint x @ builder extent y negated. 135 | labelH text: (self getLabelYValueFor: boundedPosition). 136 | labelH translateTo: builder extent x @ boundedPoint y. 137 | self updateCanvas ] 138 | ifFalse: [ self removeLinesAndLabelsIfNecessary ] ] 139 | ] 140 | 141 | { #category : #createLinesAndLabels } 142 | PDSFunctionBoundCursorFollower >> updateCanvas [ 143 | canvas signalUpdate 144 | ] 145 | -------------------------------------------------------------------------------- /src/PDS-Core/PDSBloomFilterAnalysis.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterAnalysis, 3 | #superclass : #Object, 4 | #instVars : [ 5 | 'targetElements', 6 | 'targetFpp' 7 | ], 8 | #category : #'PDS-Core-Membership' 9 | } 10 | 11 | { #category : #'as yet unclassified' } 12 | PDSBloomFilterAnalysis class >> defaultTargetElements [ 13 | ^ 100 14 | ] 15 | 16 | { #category : #'as yet unclassified' } 17 | PDSBloomFilterAnalysis class >> defaultTargetFpp [ 18 | ^ 0.01 19 | ] 20 | 21 | { #category : #'instance creation' } 22 | PDSBloomFilterAnalysis class >> open [ 23 | 24 | self new analyzeFalsePositiveProbability 25 | inspectWithLabel: 'Bloom Filter False Positive Analysis' 26 | ] 27 | 28 | { #category : #'instance creation' } 29 | PDSBloomFilterAnalysis class >> openFor: aBloomFilter [ 30 | (self new targetElements: aBloomFilter targetElements; targetFpp: aBloomFilter targetFpp; 31 | analyzeFalsePositiveProbability) 32 | inspectWithLabel: 'Bloom Filter False Positive Analysis' 33 | ] 34 | 35 | { #category : #examples } 36 | PDSBloomFilterAnalysis >> analyzeFalsePositiveProbability [ 37 | " 38 | PDSBloomFilterAnalysis new analyzeFalsePositiveProbability 39 | " 40 | 41 | | fpp ds1 g p ds s ns targetElements dec bloom fp points targetFpp numberExperiments startingValue stepValue averagePoints averageDs | 42 | targetElements := self targetElements. 43 | targetFpp := self targetFpp. 44 | numberExperiments := 10. 45 | startingValue := 10. 46 | stepValue := 10. 47 | points := OrderedCollection new. 48 | averagePoints := OrderedCollection new. 49 | startingValue to: targetElements * 1.5 by: stepValue do: [ :numElements | 50 | | averageValue | 51 | averageValue := 0. 52 | numberExperiments 53 | timesRepeat: [ bloom := PDSBloomFilter new: targetElements fpp: targetFpp. 54 | s := (1 to: numElements) 55 | collect: [ :each | each asString , '-' , each atRandom asString , '@osoco.es' ]. 56 | ns := (1 to: numElements) 57 | collect: 58 | [ :each | each asString , '-' , each atRandom asString , '@noosoco.es' ]. 59 | s do: [ :each | bloom add: each asByteArray ]. 60 | fp := ns count: [ :each | bloom contains: each asByteArray ]. 61 | points add: numElements @ (fp / ns size) asFloat. 62 | averageValue := averageValue 63 | + ((fp / ns size) asFloat / numberExperiments) asFloat ]. 64 | averagePoints add: numElements @ averageValue ]. 65 | g := RTGrapher new. 66 | ds := RTData new. 67 | ds dotShape cross color: (Color gray alpha: 0.3). 68 | ds points: points. 69 | ds x: #x. 70 | ds y: #y. 71 | ds label: 'Measured FPP'. 72 | averageDs := RTData new. 73 | averageDs dotShape rectangle color: (Color red alpha: 0.5). 74 | averageDs points: averagePoints. 75 | averageDs x: #x. 76 | averageDs y: #y. 77 | averageDs 78 | connectUsing: 79 | (RTStyledMultiLine new dashedLine 80 | color: (Color red alpha: 0.5); 81 | width: 2). 82 | averageDs label: 'Average Measured FPP'. 83 | dec := PDSStandardDeviationDecorator new. 84 | dec 85 | moveBehind; 86 | color: (Color red alpha: 0.3); 87 | points: points. 88 | g addDecorator: dec. 89 | g add: ds. 90 | g add: averageDs. 91 | fpp := [ :size | 92 | 1 - (bloom hashes * size / bloom storageSize) negated exp 93 | raisedTo: bloom hashes ]. 94 | ds1 := RTData new. 95 | ds1 label: 'Estimated FPP'. 96 | ds1 noDot. 97 | ds1 points: (0 to: bloom targetElements * 1.5 by: 1). 98 | ds1 connectColor: Color blue. 99 | ds1 x: #yourself. 100 | ds1 y: fpp. 101 | g add: ds1. 102 | p := RTHorizontalLineDecorator new. 103 | p add: (fpp value: bloom targetElements). 104 | p shape line color: (Color blue alpha: 0.2). 105 | g addDecorator: p. 106 | p := RTVerticalLineDecorator new. 107 | p add: bloom targetElements. 108 | p shape line color: (Color blue alpha: 0.3). 109 | g addDecorator: p. 110 | g 111 | addDecorator: 112 | (PDSBoundCursorFollower new 113 | color: Color blue; 114 | bindFunction: fpp). 115 | g legend right. 116 | g axisX title: '# Elements Added'. 117 | g axisY title: 'False Positive Probability (FPP)'. 118 | ^ g 119 | ] 120 | 121 | { #category : #accessing } 122 | PDSBloomFilterAnalysis >> targetElements [ 123 | ^ targetElements 124 | ifNil: [ targetElements := self class defaultTargetElements ] 125 | ] 126 | 127 | { #category : #accessing } 128 | PDSBloomFilterAnalysis >> targetElements: anObject [ 129 | targetElements := anObject 130 | ] 131 | 132 | { #category : #accessing } 133 | PDSBloomFilterAnalysis >> targetFpp [ 134 | ^ targetFpp ifNil: [ targetFpp := self class defaultTargetFpp ] 135 | ] 136 | 137 | { #category : #accessing } 138 | PDSBloomFilterAnalysis >> targetFpp: anObject [ 139 | targetFpp := anObject 140 | ] 141 | -------------------------------------------------------------------------------- /src/PDS-Playground/PDSBloomFilterPlaygroundToolbar.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSBloomFilterPlaygroundToolbar, 3 | #superclass : #ComposablePresenter, 4 | #instVars : [ 5 | 'bloom', 6 | 'create', 7 | 'reset', 8 | 'creationBlock', 9 | 'analysis', 10 | 'profile', 11 | 'benchmarking' 12 | ], 13 | #category : #'PDS-Playground-Membership' 14 | } 15 | 16 | { #category : #specs } 17 | PDSBloomFilterPlaygroundToolbar class >> defaultSpec [ 18 | ^ SpecColumnLayout composed 19 | newRow: [ :row | 20 | row 21 | add: #create; 22 | add: #analysis; 23 | add: #profile; 24 | add: #benchmarking; 25 | add: #reset ] 26 | height: self toolbarHeight; 27 | yourself 28 | ] 29 | 30 | { #category : #accessing } 31 | PDSBloomFilterPlaygroundToolbar >> analysis [ 32 | ^ analysis 33 | ] 34 | 35 | { #category : #initialization } 36 | PDSBloomFilterPlaygroundToolbar >> analyzeFilter [ 37 | PDSBloomFilterAnalysis openFor: bloom. 38 | ] 39 | 40 | { #category : #initialization } 41 | PDSBloomFilterPlaygroundToolbar >> benchmarkFilter [ 42 | | addOperationsPerSec testOperationsPerSec | 43 | addOperationsPerSec := [ bloom add: bloom targetElements atRandom asByteArray ] bench. 44 | testOperationsPerSec := [ bloom contains: bloom targetElements atRandom asByteArray ] 45 | bench. 46 | UIManager default 47 | inform: 48 | ('Add operations: {1}' , Character cr asString 49 | , 'Test operations: {2}' 50 | format: 51 | {addOperationsPerSec. 52 | testOperationsPerSec}). 53 | self onBloomFilterUpdated 54 | ] 55 | 56 | { #category : #accessing } 57 | PDSBloomFilterPlaygroundToolbar >> benchmarking [ 58 | ^ benchmarking 59 | ] 60 | 61 | { #category : #accessing } 62 | PDSBloomFilterPlaygroundToolbar >> create [ 63 | ^ create 64 | ] 65 | 66 | { #category : #initialization } 67 | PDSBloomFilterPlaygroundToolbar >> createBloomFilterWith: size andFpp: fpp [ 68 | self onBloomFilterCreated: (PDSBloomFilter new: size fpp: fpp). 69 | 70 | ] 71 | 72 | { #category : #initialization } 73 | PDSBloomFilterPlaygroundToolbar >> initializePresenter [ 74 | create action: [ self openCreationForm ]. 75 | reset action: [ self resetBloomFilter ]. 76 | profile action: [ self profileFilter ]. 77 | analysis action: [ self analyzeFilter ]. 78 | benchmarking action: [ self benchmarkFilter ] 79 | ] 80 | 81 | { #category : #initialization } 82 | PDSBloomFilterPlaygroundToolbar >> initializeWidgets [ 83 | create := self newButton. 84 | analysis := self newButton. 85 | profile := self newButton. 86 | benchmarking := self newButton. 87 | reset := self newButton. 88 | create 89 | label: 'New Bloom Filter'; 90 | icon: (self iconNamed: #add). 91 | analysis 92 | label: 'Analysis'; 93 | icon: (self iconNamed: #smallProfileIcon); 94 | enabled: false. 95 | profile 96 | label: 'Profiling'; 97 | icon: (self iconNamed: #glamorousSpawn); 98 | enabled: false. 99 | benchmarking 100 | label: 'Benchmarking'; 101 | icon: (self iconNamed: #history); 102 | enabled: false. 103 | reset 104 | label: 'Reset'; 105 | icon: (self iconNamed: #refresh); 106 | enabled: false. 107 | self focusOrder 108 | add: create; 109 | add: analysis; 110 | add: profile; 111 | add: benchmarking; 112 | add: reset 113 | ] 114 | 115 | { #category : #initialization } 116 | PDSBloomFilterPlaygroundToolbar >> onBloomFilterCreated: aBloomFilter [ 117 | bloom := aBloomFilter. 118 | self reset enabled: true. 119 | self analysis enabled: true. 120 | self profile enabled: true. 121 | self benchmarking enabled: true. 122 | creationBlock ifNotNil: [ creationBlock value: bloom ] 123 | ] 124 | 125 | { #category : #initialization } 126 | PDSBloomFilterPlaygroundToolbar >> onBloomFilterUpdated [ 127 | creationBlock ifNotNil: [ creationBlock value: bloom ] 128 | ] 129 | 130 | { #category : #private } 131 | PDSBloomFilterPlaygroundToolbar >> openCreationForm [ 132 | | form dialog | 133 | form := PDSBloomFilterCreationForm new. 134 | dialog := form openDialogWithSpec. 135 | form 136 | whenParametersValidationResultChangedDo: [ :areValid | dialog okButtonEnabled: areValid ]. 137 | dialog title: 'BloomFilter Parameters'. 138 | dialog extent: 500 @ (5 * self class toolbarHeight). 139 | dialog okButtonEnabled: false. 140 | dialog 141 | okAction: [ self 142 | createBloomFilterWith: form targetElements text asInteger 143 | andFpp: form targetFpp text asNumber ] 144 | ] 145 | 146 | { #category : #accessing } 147 | PDSBloomFilterPlaygroundToolbar >> profile [ 148 | ^ profile 149 | ] 150 | 151 | { #category : #initialization } 152 | PDSBloomFilterPlaygroundToolbar >> profileFilter [ 153 | TimeProfiler new 154 | openOnBlock: [ 1000 155 | timesRepeat: 156 | [ 1 to: bloom targetElements do: [ :count | bloom add: ('element ' , count asString) asByteArray ] ] ]. 157 | self onBloomFilterUpdated 158 | ] 159 | 160 | { #category : #accessing } 161 | PDSBloomFilterPlaygroundToolbar >> reset [ 162 | ^ reset 163 | ] 164 | 165 | { #category : #initialization } 166 | PDSBloomFilterPlaygroundToolbar >> resetBloomFilter [ 167 | self onBloomFilterCreated: (bloom reset) 168 | ] 169 | 170 | { #category : #'api - events' } 171 | PDSBloomFilterPlaygroundToolbar >> whenBloomCreatedDo: aBlock [ 172 | creationBlock := aBlock 173 | ] 174 | -------------------------------------------------------------------------------- /src/BaselineOfProbabilisticDataStructures/BaselineOfProbabilisticDataStructures.class.st: -------------------------------------------------------------------------------- 1 | " 2 | !PharoPDS 3 | 4 | The purpose of PharoPDS is to provide some """"probabilistic data structures and algorithms"""" implemented in Pharo. 5 | 6 | ''Probabilistic data structures'' is a common name for data structures based mostly on different hashing techniques. Unlike regular and deterministic data structures, they always provide approximated answers but with reliable ways to estimate possible errors. 7 | 8 | The potential losses and errors are fully compensated for by extremely low memory requirements, constant query time, and scaling. All these factors make these structures relevant in ''Big Data'' applications. 9 | 10 | !!Data Structures 11 | 12 | Currently, PharoPDS provides probabilistic data structures for the following categories of problems: 13 | 14 | !!! Membership 15 | 16 | A ''membership problem'' for a dataset is a task to decide whether some elements belongs to the dataset or not. 17 | 18 | The data structures provided to solve the membership problem are the following: 19 | 20 | - """"Bloom Filter"""": Learn more about it in ${class:PDSBloomFilter}$ 21 | 22 | !!! Cardinality 23 | 24 | This is still a work in progress. 25 | 26 | - """"HyperLogLog"""" 27 | 28 | !!Algorithms Browser 29 | 30 | In order to ease the understanding of the inner workings and trade-offs, we provide specific Playground tools for each data structure that allows the developer to explore it and get deeper insights. 31 | 32 | You can browse the available algorithm playgrounds throgh the """"PharoPDS Algorithms Browser"""". Try it now: 33 | 34 | [[[ 35 | PDSAlgorithmsBrowser open 36 | ]]] 37 | 38 | !!Moldable development 39 | 40 | This library has been develop trying to apply the ideas after the ''moldable development'' approach, so you can expect that each data structure provides its own custom domain-specific extensions in order to ease the understanding and learning by the developers. 41 | 42 | 43 | !!License 44 | 45 | PharoPDS software is free and open source under an """"MIT license"""". 46 | 47 | !!Project dependencies 48 | 49 | Hashing plays a central role in probabilistic data structures. Indeed, the choice of the appropiate hash functions is crucial to avoid bias and to reach a good performance. In particular, the structures require """"non-cryptographic hash functions"""" that are provided by the dependency module ==NonCryptographicHashes==. Learn more at ${class:BaselineOfNonCryptographicHashes}$. 50 | 51 | Other dependencies like ==Roassal== or ==GToolkit== are optional for production use. Nevertheless, we recommend that you install them in the development image to get some useful tools like Inspector custom extensions, the algorithm browser or interactive tutorials. 52 | 53 | The picture below depicts the project modules and their dependencies. The map is navigable. Click on each component to learn more about it. 54 | 55 | ${example:BaselineOfProbabilisticDataStructures class>>#itself|previewShow=#gtMapFor:||noCode=|previewHeight=400}$ 56 | 57 | 58 | 59 | " 60 | Class { 61 | #name : #BaselineOfProbabilisticDataStructures, 62 | #superclass : #BaselineOf, 63 | #category : #BaselineOfProbabilisticDataStructures 64 | } 65 | 66 | { #category : #accessing } 67 | BaselineOfProbabilisticDataStructures class >> itself [ 68 | 69 | ^ self 70 | ] 71 | 72 | { #category : #baselines } 73 | BaselineOfProbabilisticDataStructures >> baseline: spec [ 74 | 75 | spec 76 | for: #common 77 | do: [ 78 | self specProjects: spec. 79 | self specPackages: spec. 80 | self specGroups: spec 81 | ] 82 | ] 83 | 84 | { #category : #'as yet unclassified' } 85 | BaselineOfProbabilisticDataStructures >> specGroups: spec [ 86 | spec group: 'Core' with: #('PDS-Core'). 87 | spec group: 'Tests' with: #('PDS-Core-Tests'). 88 | spec group: 'Examples' with: #('PDS-Core-Examples'). 89 | spec 90 | group: 'Playground' 91 | with: #('PDS-Playground' 'PDS-Inspector-Extensions'). 92 | spec 93 | group: 'All' 94 | with: 95 | #('Playground' 'Tests' 'Examples' 'PDS-GToolkit-Extensions'). 96 | spec group: 'default' with: #('Playground' 'Tests' 'Examples') 97 | ] 98 | 99 | { #category : #'as yet unclassified' } 100 | BaselineOfProbabilisticDataStructures >> specPackages: spec [ 101 | spec 102 | package: 'PDS-Core' 103 | with: [ spec requires: 'NonCryptographicHashes' ]; 104 | package: 'PDS-Core-Tests' with: [ spec requires: 'PDS-Core' ]; 105 | package: 'PDS-Core-Examples' with: [ spec requires: 'PDS-Core' ]; 106 | package: 'PDS-Grapher' with: [ spec requires: 'Roassal2' ]; 107 | package: 'PDS-Inspector-Extensions' 108 | with: [ spec requires: #('PDS-Core' 'PDS-Grapher' 'Roassal2') ]; 109 | package: 'PDS-GToolkit-Extensions' 110 | with: [ spec 111 | requires: #('PDS-Core' 'PDS-Inspector-Extensions' 'Roassal2' 'GToolkit') ]; 112 | package: 'PDS-Playground' with: [ spec requires: 'PDS-Core' ] 113 | ] 114 | 115 | { #category : #'as yet unclassified' } 116 | BaselineOfProbabilisticDataStructures >> specProjects: spec [ 117 | spec 118 | baseline: 'Roassal2' 119 | with: [ spec repository: 'github://ObjectProfile/Roassal2:1df4118/src' ]. 120 | spec 121 | baseline: 'GToolkit' 122 | with: [ spec repository: 'github://feenkcom/gtoolkit:v0.6.46/src' ]. 123 | spec 124 | baseline: 'NonCryptographicHashes' 125 | with: 126 | [ spec repository: 'github://osoco/pharo-non-cryptographic-hashes:v1.0.0/src' ] 127 | ] 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PharoPDS 2 | 3 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) 4 | [![Build Status](https://travis-ci.org/osoco/PharoPDS.svg?branch=master)](https://travis-ci.org/osoco/PharoPDS) 5 | [![Coverage Status](https://coveralls.io/repos/github/osoco/PharoPDS/badge.svg?branch=master)](https://coveralls.io/github/osoco/PharoPDS?branch=master) 6 | [![Pharo version](https://img.shields.io/badge/Pharo-7.0-%23aac9ff.svg)](https://pharo.org/download) 7 | [![Pharo version](https://img.shields.io/badge/Pharo-8.0-%23aac9ff.svg)](https://pharo.org/download) 8 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/osoco/PharoPDS/master/LICENSE) 9 | 10 | The purpose of PharoPDS is to provide some **probabilistic data structures and algorithms** implemented in Pharo. 11 | 12 | ''Probabilistic data structures'' is a common name for data structures based mostly on different hashing techniques. Unlike regular and deterministic data structures, they always provide approximated answers but with reliable ways to estimate possible errors. 13 | 14 | The potential losses and errors are fully compensated for by extremely low memory requirements, constant query time, and scaling. All these factors make these structures relevant in ''Big Data'' applications. 15 | 16 | We've written some posts about the library and the historical and intellectual background of some ideas behind the approach we have followed: 17 | 18 | - [Understanding Bloom filters with Pharo Smalltalk](https://osoco.es/thoughts/2019/05/understanding-bloom-filters-with-pharo-smalltalk/) 19 | - [Designing media for thought with moldable development](https://osoco.es/thoughts/2019/05/designing-media-for-thought-with-moldable-development/) 20 | 21 | ## Install PharoPDS 22 | 23 | To install PharoPDS on your Pharo image you can just find it in the **Pharo Project Catalog** (`World menu` > `Tools` > `Catalog Browser`) and click in the *green mark* icon in the upper right corner to install the latest stable version: 24 | 25 | ![Pharo Project Catalog with the project selected](doc/images/pharo-project-catalog.png) 26 | 27 | Or, you can also execute the following script: 28 | 29 | ```Smalltalk 30 | Metacello new 31 | baseline: #ProbabilisticDataStructures; 32 | repository: 'github://osoco/PharoPDS:master/src'; 33 | load 34 | ``` 35 | 36 | You can optionally install all the custom extensions and interactive tutorials included with the project executing the following script to install the group 'All': 37 | 38 | 39 | ```Smalltalk 40 | Metacello new 41 | baseline: #ProbabilisticDataStructures; 42 | repository: 'github://osoco/PharoPDS:master/src'; 43 | load: 'All' 44 | ``` 45 | 46 | To add PharoPDS to your own project's baseline just add this: 47 | 48 | ```Smalltalk 49 | spec 50 | baseline: #ProbabilisticDataStructures 51 | with: [ spec repository: 'github://osoco/PharoPDS:master/src' ] 52 | ``` 53 | 54 | Note that you can replace the *master* by another branch or a tag. 55 | 56 | ## Data Structures 57 | 58 | Currently, PharoPDS provides probabilistic data structures for the following categories of problems: 59 | 60 | ### Membership 61 | 62 | A *membership problem* for a dataset is a task to decide whether some elements belongs to the dataset or not. 63 | 64 | The data structures provided to solve the membership problem are the following: 65 | 66 | - **Bloom Filter**. 67 | 68 | ### Cardinality 69 | 70 | This is still a work in progress. 71 | 72 | - **HyperLogLog** 73 | 74 | ## Moldable development 75 | 76 | This library has been developed trying to apply the ideas after the **moldable development** approach, so you can expect that each data structure provides its own custom and domain-specific extensions in order to ease the understanding and learning by the developers. 77 | 78 | For instance, the following pictures are some of the extensions provided by the Bloom filter: 79 | 80 | ![Inspector on Bloom Filter - Parameters tab](doc/images/bloom-params-extension.png) 81 | 82 | ![Inspector on Bloom Filter - FPP tab](doc/images/bloom-fpp-extension.png) 83 | 84 | ![Inspector on Bloom Filter - Bits tab](doc/images/bloom-bits-extension.png) 85 | 86 | ![Inspector on Bloom Filter - Analysis](doc/images/bloom-analysis.png) 87 | 88 | ## Algorithms Browser 89 | 90 | In order to ease the understanding of the inner workings and trade-offs, we provide specific *Playground* tools for each data structure that allows the developer to explore it and get deeper insights. 91 | 92 | You can browse the available algorithm playgrounds through the **PharoPDS Algorithms Browser**. You can open it with the following expression: 93 | 94 | ```Smalltalk 95 | PDSAlgorithmsBrowser open 96 | ``` 97 | 98 | ![PDS Algorithms Browser](doc/images/algorithms-browser.png) 99 | 100 | ## License 101 | 102 | PharoPDS is written and supported by developers at **[OSOCO](https://osococo.es)** and published as **free and open source** software under an **[MIT license](LICENSE)**. 103 | 104 | ## Project dependencies 105 | 106 | Hashing plays a central role in probabilistic data structures. Indeed, the choice of the appropiate hash functions is crucial to avoid bias and to reach a good performance. In particular, the structures require **non-cryptographic hash functions** that are provided by the dependency module **[NonCryptographicHashes](https://github.com/osoco/pharo-non-cryptographic-hashes)**. 107 | 108 | Other dependencies like **Roassal** or **GToolkit** are optional for production use. Nevertheless, we recommend that you install them in the development image if you want to get some useful tools like Inspector custom extensions, the algorithm browser or interactive tutorials. 109 | -------------------------------------------------------------------------------- /src/ConfigurationOfProbabilisticDataStructures/ConfigurationOfProbabilisticDataStructures.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #ConfigurationOfProbabilisticDataStructures, 3 | #superclass : #Object, 4 | #instVars : [ 5 | 'project' 6 | ], 7 | #classVars : [ 8 | 'LastVersionLoad' 9 | ], 10 | #category : #ConfigurationOfProbabilisticDataStructures 11 | } 12 | 13 | { #category : #'development support' } 14 | ConfigurationOfProbabilisticDataStructures class >> DevelopmentSupport [ 15 | 16 | "See the methods in the 'development support' category on the class-side of MetacelloBaseConfiguration. Decide what development support methods you would like to use and copy them the the class-side of your configuration." 17 | 18 | 19 | ] 20 | 21 | { #category : #private } 22 | ConfigurationOfProbabilisticDataStructures class >> baseConfigurationClassIfAbsent: aBlock [ 23 | 24 | ^Smalltalk 25 | at: #MetacelloBaseConfiguration 26 | ifAbsent: [ 27 | self ensureMetacelloBaseConfiguration. 28 | Smalltalk at: #MetacelloBaseConfiguration ifAbsent: aBlock ]. 29 | 30 | ] 31 | 32 | { #category : #catalog } 33 | ConfigurationOfProbabilisticDataStructures class >> catalogContactInfo [ 34 | ^ 'PharoPDS is written and supported by Rafael Luque, Javier Luque and other developers at OSOCO.' 35 | ] 36 | 37 | { #category : #catalog } 38 | ConfigurationOfProbabilisticDataStructures class >> catalogDescription [ 39 | ^ 'The purpose of PharoPDS is to provide some probabilistic data structures and algorithms implemented in Pharo. 40 | 41 | ''Probabilistic data structures'' is a common name for data structures based mostly on different hashing techniques. Unlike regular and deterministic data structures, they always provide approximated answers but with reliable ways to estimate possible errors. 42 | 43 | More info in the GitHub repository: https://github.com/osoco/PharoPDS' 44 | ] 45 | 46 | { #category : #catalog } 47 | ConfigurationOfProbabilisticDataStructures class >> catalogKeywords [ 48 | ^ #(probabilistic data_structures algorithms bloom) 49 | ] 50 | 51 | { #category : #private } 52 | ConfigurationOfProbabilisticDataStructures class >> ensureMetacello [ 53 | 54 | (self baseConfigurationClassIfAbsent: []) ensureMetacello 55 | ] 56 | 57 | { #category : #private } 58 | ConfigurationOfProbabilisticDataStructures class >> ensureMetacelloBaseConfiguration [ 59 | 60 | Smalltalk 61 | at: #MetacelloBaseConfiguration 62 | ifAbsent: [ 63 | | repository version | 64 | repository := MCHttpRepository location: 'http://seaside.gemstone.com/ss/metacello' user: '' password: ''. 65 | repository 66 | versionReaderForFileNamed: 'Metacello-Base-DaleHenrichs.2.mcz' 67 | do: [ :reader | 68 | version := reader version. 69 | version load. 70 | version workingCopy repositoryGroup addRepository: repository ] ] 71 | ] 72 | 73 | { #category : #'metacello tool support' } 74 | ConfigurationOfProbabilisticDataStructures class >> isMetacelloConfig [ 75 | "Answer true and the Metacello tools will operate on you" 76 | 77 | ^true 78 | ] 79 | 80 | { #category : #loading } 81 | ConfigurationOfProbabilisticDataStructures class >> load [ 82 | "Load the #stable version defined for this platform. The #stable version is the version that is recommended to be used on this platform." 83 | 84 | "self load" 85 | 86 | 87 | ^(self project version: #stable) load 88 | ] 89 | 90 | { #category : #loading } 91 | ConfigurationOfProbabilisticDataStructures class >> loadBleedingEdge [ 92 | "Load the latest versions of the mcz files defined for this project. It is not likely that the #bleedingEdge has been tested." 93 | 94 | "self loadBleedingEdge" 95 | 96 | 97 | ^(self project version: #bleedingEdge) load 98 | ] 99 | 100 | { #category : #loading } 101 | ConfigurationOfProbabilisticDataStructures class >> loadDevelopment [ 102 | "Load the #development version defined for this platform. The #development version will change over time and is not expected to be stable." 103 | 104 | "self loadDevelopment" 105 | 106 | 107 | ^(self project version: #development) load 108 | ] 109 | 110 | { #category : #accessing } 111 | ConfigurationOfProbabilisticDataStructures class >> project [ 112 | 113 | ^self new project 114 | ] 115 | 116 | { #category : #'development support' } 117 | ConfigurationOfProbabilisticDataStructures class >> validate [ 118 | "Check the configuration for Errors, Critical Warnings, and Warnings (see class comment for MetacelloMCVersionValidator for more information). 119 | Errors identify specification issues that will result in unexpected behaviour when you load the configuration. 120 | Critical Warnings identify specification issues that may result in unexpected behavior when you load the configuration. 121 | Warnings identify specification issues that are technically correct, but are worth take a look at." 122 | 123 | "self validate" 124 | 125 | 126 | self ensureMetacello. 127 | ^ ((Smalltalk at: #MetacelloToolBox) validateConfiguration: self debug: #() recurse: false) explore 128 | ] 129 | 130 | { #category : #baselines } 131 | ConfigurationOfProbabilisticDataStructures >> baseline1x: spec [ 132 | 133 | spec 134 | for: #common 135 | do: [ spec 136 | blessing: #baseline; 137 | author: 'Rafael Luque, Javier Luque, and other OSOCO developers.'; 138 | description: 'Probabilistic Data Structures for Pharo'. 139 | spec 140 | baseline: 'ProbabilisticDataStructures' 141 | with: [ spec repository: 'github://osoco/PharoPDS:master/src' ] ] 142 | ] 143 | 144 | { #category : #accessing } 145 | ConfigurationOfProbabilisticDataStructures >> customProjectAttributes [ 146 | "Edit to return a collection of any custom attributes e.g. for conditional loading: Array with: #'Condition1' with: #'Condition2. 147 | For more information see: http://code.google.com/p/metacello/wiki/CustomProjectAttrributes" 148 | 149 | ^ #(). 150 | ] 151 | 152 | { #category : #'symbolic versions' } 153 | ConfigurationOfProbabilisticDataStructures >> development: spec [ 154 | 155 | spec for: #common version: '1.x-baseline' 156 | ] 157 | 158 | { #category : #accessing } 159 | ConfigurationOfProbabilisticDataStructures >> project [ 160 | 161 | ^ project ifNil: [ 162 | "Bootstrap Metacello if it is not already loaded" 163 | (self class baseConfigurationClassIfAbsent: []) ensureMetacello. 164 | "Construct Metacello project" 165 | project := MetacelloMCProject new projectAttributes: self customProjectAttributes. 166 | (Smalltalk at: #MetacelloVersionConstructor) on: self project: project. 167 | project loadType: #linear. "change to #atomic if desired" 168 | project ] 169 | ] 170 | 171 | { #category : #'symbolic versions' } 172 | ConfigurationOfProbabilisticDataStructures >> stable: spec [ 173 | 174 | 175 | spec for: #'common' version: '1.0.0'. 176 | 177 | ] 178 | 179 | { #category : #versions } 180 | ConfigurationOfProbabilisticDataStructures >> version100: spec [ 181 | 182 | spec 183 | for: #common 184 | do: [ spec 185 | blessing: #stable; 186 | author: 'Rafael Luque, Javier Luque, and other OSOCO developers'; 187 | description: 'Probabilistic Data Structures for Pharo'. 188 | spec 189 | baseline: 'ProbabilisticDataStructures' 190 | with: [ spec repository: 'github://osoco/PharoPDS:v1.0.0/src' ] ] 191 | ] 192 | -------------------------------------------------------------------------------- /src/PDS-Core/PDSBloomFilter.class.st: -------------------------------------------------------------------------------- 1 | " 2 | !Bloom Filter 3 | 4 | A Bloom filter is a space-efficient probabilistic data structure, conceived by """"Burton Howard Bloom"""" in 1970 (''Space/Time Tradeoffs in Hash Coding with Allowable Errors''), that represents a set of elements and allows you to test if an element is a membership. 5 | 6 | !! Introduction 7 | 8 | A regular ''hash-based search'' stores the full set of values from a collection in a hash table, whether in linked lists or using ''open addressing''. In both cases, as more elements are added to the hash table, the time to locate an element increases, degenerating the expected time performance from the initial constant ==O(1)== to the lineal ==O(N)==. 9 | 10 | A """"Bloom Filter"""" provides an alternative structure that ensures """"constant performance"""", both in space and time, when adding elements or checking an element membership. This behaviour is independent of the number of items already added to the filter. 11 | 12 | !! Context 13 | 14 | ''Donald Knuth'' in his famous ''The Art of Computer Programming'' writes the following about Bloom filters: 15 | 16 | ""Imagine a search application with a large database in which no calculation needs to be done if the search was unsucessful. For example, we might want to check somebody's credit rating or passport number, and if no record for that person appears in the file we don't have to investigate further. Similarly in an application to computerized typesetting, we might have a simple algorithm that hyphenates most words correctly, but it fails on some 50,000 exception words; if we don't find the word in the exception file we are free to use the simple algorithm."" 17 | 18 | Also ''Broder'' and ''Mitzenmacher'' stablished the following principle: 19 | 20 | ""Whenever a list or set is used, and space is at a premium, consider using a Bloom filter if the effect of false positives can be mitigated."" 21 | 22 | !! Examples from the Real World^^tm^^ 23 | 24 | - Medium uses Bloom filters to avoid recommending articles a user has previously read. 25 | - Google Chrome uses a Bloom filter to identify malicious URLs. 26 | - Google BigTable, Apache HBase and Apache Cassandra use Bloom filters to reduce the disk lookups for non-existing rows or columns. 27 | - The Squid Web proxy uses Bloom filters for cache digests. 28 | 29 | !! Basic understanding 30 | 31 | The Bloom Filter can store a large set very efficiently by discarding the identity of the elements; i.e. it stores only a set of bits corresponding to some number of """"hash functions"""" that are applied to each added element by the algorithm. 32 | 33 | Practically, the Bloom Filter is represented by a bit array and can be described by its length (==m==) and number of different hash functions (==k==). 34 | 35 | The structure supports only two operations: 36 | - Adding and element into the set, and 37 | - Testing whether an element is or is not a member of the set. 38 | 39 | The Bloom Filter data structure is a bit array where at the beginning all bits are equal to zero, meaning the filter is empty. 40 | 41 | For example, consider the following Bloom filter example created to represent a set of 10 elements with a """"false positive probability"""" (==FPP==) of ==0.1== (i.e. 10%). 42 | 43 | This empty filter will be backed by a bit array of length ==m=48== (==storageSize==) and 4 hash functions to produce values in the range =={1, 2, ..., m}==. It has the following form: 44 | 45 | ${example:PDSBloomFilterExamples>>#emptyBloomFilter|previewShow=#gtStorageBitSetViewFor:|previewHeight=250}$ 46 | 47 | 48 | To insert an element ==x== into the filter, for every hash function ==h@@i@@== we compute its value ==j=h@@i@@(x)== on the element ==x== and set the corresponding bit ==j== in the filter to one. 49 | 50 | As an example, we are going to insert some names of cities into the previous filter. Let's start with """"Madrid"""": 51 | 52 | ${example:PDSBloomFilterExamples>>#withMadridBloomFilter|previewShow=#gtStorageBitSetViewFor:|previewHeight=250}$ 53 | 54 | In order to find the corresponding bits to be set for =='Madrid'==, the filter computes the corresponding 4 hash values. As you see above, the filter has set the bits 9, 18, 39 and 48. 55 | 56 | It is possible that different elements can share bits, for instance, let's add another city, """"Barcelona"""", to the same filter: 57 | 58 | ${example:PDSBloomFilterExamples>>#withMadridAndBarcelonaBloomFilter|previewShow=#gtStorageBitSetViewFor:|previewHeight=250}$ 59 | 60 | As you can see, after adding the String =='Barcelona'== to the previous Bloom filter only the bits 30, 36 and 42 have been set, meaning that the elements =='Madrid'== and =='Barcelona'== share one of their corresponding bits. 61 | 62 | To test if a given element ==x== is in the filter, all ==k== hash functions are computed and check bits in the corresponding positions. If """"all"""" bits are set to one, then the element ==x== """"may exist"""" in the filter. Otherwise, the element ==x== is """"definitely not"""" in the filter. 63 | 64 | The uncertainty about the element's existence originates from the possibility of situations when some bits are set by different elements added previously. 65 | 66 | Consider the previous Bloom filter example with the elements =='Madrid'== and =='Barcelona'== already added. To test if the element =='Barcelona'== is a member, the filter computes its 4 hash values and check that the corresponding bits in the filter are set to one, therefore the String =='Barcelona'== may exists in the filter and the ==contains: 'Barcelona'== returns ==true==: 67 | 68 | ${example:PDSBloomFilterExamples>>#withMadridAndBarcelonaCheckBarcelonaBloomFilter}$ 69 | 70 | 71 | Now if we check the element =='Berlin'==, the filter computes its hashes in order to find that the corresponding bits in the filter are the following: 72 | 73 | ${example:PDSBloomFilterExamples>>#withBerlinBloomFilter|previewShow=#gtStorageBitSetViewFor:|previewHeight=250}$ 74 | 75 | We see that the bits 27 and 33 aren't set, therefore the """"Berlin"""" element is definitely not in the filter, so the ==contains:== method returns ==false==: 76 | 77 | ${example:PDSBloomFilterExamples>>#withMadridAndBarcelonaCheckBerlinBloomFilter}$ 78 | 79 | A Blool filter can also result in a false positive result. For example, consider the element """"Roma"""", whose 4 hash values collision in the bit 36 that is already set in our filter example, so the result of the contains method is that the element may exist in the filter: 80 | 81 | ${example:PDSBloomFilterExamples>>#withMadridAndBarcelonaCheckRomaBloomFilter}$ 82 | 83 | As we know, we have not added that element to the filter, so this is an example of a false positive event. In this particular case, the bit 36 was set previously by the """"Barcelona"""" element. 84 | 85 | !! Properties 86 | 87 | !!! False positives 88 | 89 | As we have shown, Bloom filters can lead to situations where some element is not a member, but the algorithm returns like it is. Such an event is called a """"false positive"""" and can occur because of hard hash collisions or coincidence in the stored bits. In the test operation there is no knowledge of whether the particular bit has been set by the same hash function as the one we compare with. 90 | 91 | Fortunately, a Bloom filter has a predictable false positive probability (==FPP==): 92 | 93 | ${icebergFile:path=osoco/PharoPDS/doc/images/pfp_formula.png|expanded=true|height=100|show=#gtFigureFor:}$ 94 | 95 | where """"==n=="""" is the number of values already added, """"==k=="""" the number of hashes and """"==m=="""" the length of the filter (i.e. the bit array size). 96 | 97 | In the extreme case, when the filter is full every lookup will yield a (false) positive response. It means that the choice of ==m== depends on the estimated number of elements ==n== that are expected to be added, and ==m== should be quite large compared to ==n==. 98 | 99 | In practice, the length of the filter ==m==, under given false positive probability ==FPP== and the expected number of elements ==n==, can be determined by the formula: 100 | 101 | ${icebergFile:path=osoco/PharoPDS/doc/images/m_formula.png|expanded=true|height=100|show=#gtFigureFor:}$ 102 | 103 | For the given ratio of ==m/n==, meaning the number of allocated bits per element, the false positive probability can be tuned by choosing the number of hash functions ==k==. The optimal choice of ==k== is computed by minimizing the probability of false positives with the following formula: 104 | 105 | ${icebergFile:path=osoco/PharoPDS/doc/images/k_formula.png|expanded=true|height=100|show=#gtFigureFor:}$ 106 | 107 | Our ==PDSBloomFilter== implementation is built specifying the estimated number of elements to be added (==n==) and the false positive probability (==FPP==), then the data structure uses the previous formula to compute the optimal number of hashes and bit array length. 108 | 109 | For instance, to handle 1 billion elements and keep the probability of false positive events at about 2% we need the following Bloom filter: 110 | 111 | ${example:PDSBloomFilterExamples>>#oneBillionBloomFilter|previewShow=#gtParametersViewFor:|previewHeight=150}$ 112 | 113 | As you can see, the optimal number of hashes is 6 and the filter's length is ==8.14 x 10^^9^^== bits, that is approximately 1 GB of memory. 114 | 115 | !!! False negatives 116 | 117 | If the Bloom filter returns that a particular element isn't a member, then it's definitely not a member of the set. 118 | 119 | !!! Deletion is not possible 120 | 121 | To delete a particular element from the Bloom filter it would need to unset its corresponding ==k== bits in the bit array. Unfortunately, a single bit could correspond to multiple elements due to hash collisions and shared bits between elements. 122 | 123 | !! Analysis 124 | 125 | The previous formula for the false positive probability is a reasonably computation assuming the ==k== hash functions are uniformly random. 126 | 127 | By other hand, the ==fpp== value specified when the ==PDSBloomFilter== is initialized should be interpreted as the desired fpp once the expected ==n== elements have been added to the structure. 128 | 129 | For example, for a just created and still empty Bloom filter the data structure will have a ==fpp== equals to 0, as you can see in the graph below: 130 | 131 | ${example:PDSBloomFilterExamples>>#emptyBloomFilter|previewShow=#gtFPPCurveFor:|previewHeight=450}$ 132 | 133 | As you add elements to the filter the ==fpp== is increased and will be equals the target fpp when the filter reach the expected number of elements: 134 | 135 | ${example:PDSBloomFilterExamples>>#fullBloomFilter|previewShow=#gtFPPCurveFor:|previewHeight=450}$ 136 | 137 | Nevertheless, you should know that the previou """"FPP curve"""" is a theoretical value and the """"actual FPP"""" observed will depend on the specific dataset and hash functions you work with. In order to check empirically the goodness of our implementation we have run the following analysis: 138 | 139 | 1. Randomly generate a list of email addresses which are insert them in the Bloom filter. 140 | 2. Randomly generate a list of email addresses not inserted in the filter. 141 | 3. Count the false positive events when searching for the missing addresses in the filter. 142 | 143 | We ran the experiment for values from 10 to 1.5 times the expected number of elements of the filter (with steps of 10). For each number of elements the experiment is ran 10 times. The analysis shows a graph depicting the theoretical FPP curve of the filter (blue line), the average FPP values measured in each experiment (grey crosses), the actual FPP curve (red line) and the standard deviation (red shade). 144 | 145 | For instance, the following graph shows the resulting analysis for a Bloom filter with 100 elements and FPP equals to 3%: 146 | 147 | [[[ 148 | PDSBloomFilterAnalysis openFor: (PDSBloomFilter new: 100 fpp: 0.03) 149 | ]]] 150 | 151 | !! Benchmarking 152 | 153 | A Bloom filter only requires a fixed number of ==k== probes, so each insertion and search can be processed in ==O(k)== time, which is considered constant. 154 | 155 | For instance, the following code will run a microbenchmark on the search operation for a Bloom filter previously populated with 10 elements and will show the number of operations executed per second: 156 | 157 | [[[ 158 | | bloom | 159 | bloom := PDSBloomFilter new: 1000000 fpp: 0.03. 160 | 1 to: 10 do: [ :each | bloom add: ('element-', each asString) asByteArray ]. 161 | [ bloom contains: 'no-member' asByteArray ] bench. 162 | ]]] 163 | 164 | As the performance is constant, the result should be similar in the following case with the same Bloom filter containing 1 million elements previously added: 165 | 166 | [[[ 167 | | bloom | 168 | bloom := PDSBloomFilter new: 1000000 fpp: 0.03. 169 | 1 to: 1000000 do: [ :each | bloom add: ('element-', each asString) asByteArray ]. 170 | [ bloom contains: 'no-member' asByteArray ] bench. 171 | ]]] 172 | 173 | A naive implementation based on a Collection data structure will show a linear ==O(n)== performance, or in the best case of an ordered collection, a logarithmic ==O(log n)== performance. In the following benchmarks you can check as the behaviour using an ==OrderedCollection== degrades whith the size of the collection (n): 174 | 175 | [[[ 176 | | set | 177 | set := OrderedCollection new: 1000000. 178 | 1 to: 10 do: [ :each | set add: ('element-', each asString) ]. 179 | [ set includes: 'no element' ] bench 180 | ]]] 181 | 182 | versus: 183 | 184 | [[[ 185 | | set | 186 | set := OrderedCollection new: 1000000. 187 | 1 to: 1000000 do: [ :each | set add: ('element-', each asString) ]. 188 | [ set includes: 'no element' ] bench 189 | ]]] 190 | 191 | !! Playground 192 | 193 | PharoPDS provides a simple tool for the Bloom filter in order to allow you to explore and play with this data structure. 194 | 195 | The """"PDSBloomFilterPlayground"""" allows you to create Bloom filters to play with their operations and visualize them. You can even run benchmarking and profiling them from the UI. 196 | 197 | Let's play with the playground: 198 | 199 | [[[ 200 | PDSBloomFilterPlayground open 201 | ]]] 202 | 203 | !! 204 | 205 | 206 | " 207 | Class { 208 | #name : #PDSBloomFilter, 209 | #superclass : #Object, 210 | #instVars : [ 211 | 'hashes', 212 | 'bitStorage', 213 | 'targetFpp', 214 | 'targetElements', 215 | 'size', 216 | 'hashFunction' 217 | ], 218 | #category : #'PDS-Core-Membership' 219 | } 220 | 221 | { #category : #'default values' } 222 | PDSBloomFilter class >> defaultHashFunction [ 223 | ^ (NCHMurmur3 withSeed: 0) 224 | ] 225 | 226 | { #category : #'instance creation' } 227 | PDSBloomFilter class >> new: expectedElements fpp: estimatedFalsePositiveProbability [ 228 | ^ self new: expectedElements fpp: estimatedFalsePositiveProbability hashFunction: self defaultHashFunction 229 | ] 230 | 231 | { #category : #'instance creation' } 232 | PDSBloomFilter class >> new: expectedElements fpp: estimatedFalsePositiveProbability hashFunction: aHashFunction [ 233 | ^ self new setTargetElements: expectedElements targetFpp: estimatedFalsePositiveProbability hashFunction: aHashFunction. 234 | 235 | ] 236 | 237 | { #category : #calculating } 238 | PDSBloomFilter class >> optimumHashesFor: expectedElements andFpp: estimatedFalsePositiveProbability [ 239 | | m | 240 | m := self optimumSizeFor: expectedElements andFpp: estimatedFalsePositiveProbability. 241 | ^ (m * 2 ln) / expectedElements 242 | ] 243 | 244 | { #category : #calculating } 245 | PDSBloomFilter class >> optimumSizeFor: expectedElements andFpp: estimatedFalsePositiveProbability [ 246 | ^ (expectedElements * estimatedFalsePositiveProbability ln) negated 247 | / (2 ln raisedTo: 2) 248 | ] 249 | 250 | { #category : #api } 251 | PDSBloomFilter >> add: anObject [ 252 | | indexes anyBitChanged | 253 | indexes := self bitIndexes: anObject. 254 | anyBitChanged := false. 255 | indexes 256 | do: 257 | [ :anIndex | anyBitChanged := anyBitChanged or: (bitStorage setBitAt: anIndex) ]. 258 | anyBitChanged ifTrue: [ size := size + 1 ] 259 | ] 260 | 261 | { #category : #private } 262 | PDSBloomFilter >> bitIndexes: anObject [ 263 | | hashValues indexes | 264 | hashValues := self hashValuesFor: anObject. 265 | indexes := hashValues collect: [ :hash | (hash \\ self storageSize) + 1 ]. 266 | ^ indexes 267 | ] 268 | 269 | { #category : #api } 270 | PDSBloomFilter >> contains: anObject [ 271 | | indexes | 272 | indexes := self bitIndexes: anObject . 273 | ^ indexes allSatisfy: [ :anIndex | (bitStorage bitAt: anIndex) = 1 ] 274 | 275 | ] 276 | 277 | { #category : #accessing } 278 | PDSBloomFilter >> fpp [ 279 | ^ 1 - (self hashes * self size / self storageSize) negated exp 280 | raisedTo: self hashes 281 | ] 282 | 283 | { #category : #accessing } 284 | PDSBloomFilter >> hashFunction [ 285 | ^ hashFunction 286 | ] 287 | 288 | { #category : #'member lookup' } 289 | PDSBloomFilter >> hashValuesFor: anObject [ 290 | | hashValues murmurHashOriginal h1 h2 | 291 | murmurHashOriginal := hashFunction hash: anObject. 292 | h1 := murmurHashOriginal & 16rFFFF. 293 | h2 := murmurHashOriginal >> 16. 294 | hashValues := (1 to: self hashes) 295 | collect: [ :element | h1 + (element * h2) ]. 296 | ^ hashValues 297 | ] 298 | 299 | { #category : #accessing } 300 | PDSBloomFilter >> hashes [ 301 | ^ hashes 302 | ] 303 | 304 | { #category : #initialization } 305 | PDSBloomFilter >> initialize [ 306 | size := 0. 307 | ] 308 | 309 | { #category : #calculating } 310 | PDSBloomFilter >> optimumHashes [ 311 | ^(self class 312 | optimumHashesFor: targetElements 313 | andFpp: targetFpp ) roundUpTo: 1 314 | ] 315 | 316 | { #category : #calculating } 317 | PDSBloomFilter >> optimumSize [ 318 | ^ (self class 319 | optimumSizeFor: targetElements 320 | andFpp: targetFpp ) roundUpTo: 1 321 | ] 322 | 323 | { #category : #api } 324 | PDSBloomFilter >> reset [ 325 | ^ self class new: targetElements fpp: targetFpp hashFunction: hashFunction 326 | ] 327 | 328 | { #category : #initialization } 329 | PDSBloomFilter >> setTargetElements: estimatedElements targetFpp: estimatedFalsePositiveProbability hashFunction: aHashFunction [ 330 | targetElements := estimatedElements. 331 | targetFpp := estimatedFalsePositiveProbability. 332 | hashFunction := aHashFunction. 333 | bitStorage := PDSBitArray new: self optimumSize. 334 | hashes := self optimumHashes 335 | ] 336 | 337 | { #category : #accessing } 338 | PDSBloomFilter >> size [ 339 | ^ size 340 | ] 341 | 342 | { #category : #accessing } 343 | PDSBloomFilter >> storageSize [ 344 | ^ bitStorage size 345 | ] 346 | 347 | { #category : #accessing } 348 | PDSBloomFilter >> targetElements [ 349 | ^ targetElements 350 | ] 351 | 352 | { #category : #accessing } 353 | PDSBloomFilter >> targetFpp [ 354 | ^ targetFpp 355 | ] 356 | -------------------------------------------------------------------------------- /src/PDS-Core/PDSIcons.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #PDSIcons, 3 | #superclass : #Object, 4 | #classVars : [ 5 | 'icons' 6 | ], 7 | #category : #'PDS-Core-World Menu' 8 | } 9 | 10 | { #category : #'accessing - icons' } 11 | PDSIcons class >> flaskIcon [ 12 | "Private - Generated method" 13 | ^ self icons 14 | at: #flaskIcon 15 | ifAbsentPut: [ Form 16 | fromBinaryStream: self flaskIconContents base64Decoded readStream ] 17 | ] 18 | 19 | { #category : #'private - contents' } 20 | PDSIcons class >> flaskIconContents [ 21 | "Private - Method generated with the content of the file File @ /data/pharo/icons/flask.png" 22 | ^ 'iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQICAgIfAhkiAAAAAlw 23 | SFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoA 24 | ACpTSURBVHja7d0LlF11fejxJMQARUGpIujFllIsteiSWqu2Xq4VqY+WK2owBkIjSOACAeQV 25 | IORJuOr1CVoelauNtxEtVkuR2ApIABEDiBLkmWTeM+dMQkhIJgHymPndvcNjIZBkkpyZ8997 26 | f2atz1qsJDOjM2f/f99zzn/vPSIiRgBb9+GfxK7v/9nqDx9xy5qrDr/lids+8LPViz9wy+q+ 27 | TNBUffnvIv+d5L+b/HeU/648ZmHb/BBgKz54+7r9PrBg9ZXZoFlj2BbGmvx3lv/uPIZBAMB2 28 | ed+C2C17ZnlJNkzWGaiFtS7/Hea/S49pEAAwiOG/dt9seCw0QEtjYf479dgGAQBb9IHb1r71 29 | iFvWdBma5bL5d5r9bj3GQQDAyz7zN/zLHQFeCQABAC99z9/L/hV5O8CeABAA8NxL/89s+DMg 30 | qyD7XXvMgwCAZ071s9u/UmcHOEUQBACMePY8f4OxSrLfucc+AsAPgYpf4c9Ffqp5sSBXDEQA 31 | +CFQYfmlYw3Dasp/944BBABUVH79eMOwsqcFXuUYQABARW2+sY9hWEn5794xgACA6p7+t9gw 32 | rOzpgIsdAwgAqGoAuKVvpW8l7BhAAIAAQACAAABvAeAtABAAYBMgNgGCAACnAeI0QBAA4EJA 33 | uBAQCAAoEpcCdilgEABQ1Y2AbgbkZkAgAKB63A7Y7YBBAEB1Twe8xGCszOl/l3jMgwCAzd63 34 | IHbLhsNCA7L0Fua/a495EADwgghYu+8Rt6zpMiRLe9pfV/479lgHAQAvfSvgtrVvFQHlHP75 35 | 79ZjHAQAbPWVAG8HlO1lf8/8QQDAYPcEPLMx0NkBBd7tn/8OvecPAgB27BTBZ64T4GJBBbrI 36 | T/47c6ofCABoyBUD80vH5teP33wDoWfuIuhWwgnc0jf/XeS/k/x3k/+OXOEPBAAMuU8sjSiy 37 | H68tNo9BEAAgAAQAIABAAAgAQACAABAAgAAAASAAQAD4IYAAEAAgAAABIABAAAACQACAAAAE 38 | gAAAAQACQAAIABAAIAAEgAAAAQACQAAIABAAIAAEgAAAAQACQAAIABAAIAAEgAAAAQACQAAI 39 | ABAAIAAEgAAAAQACQAAIABAAIAAEACAAQAAIAEAAgAAQAIAAAAEgAAABAAJAAIAAAASAAAAB 40 | AAgAAQACABAAAgAEACAABAAIABAAAkAAgAAAASAABAAIABAAAkAAgAAAASAABAAIABAAAkAA 41 | gAAAASAABAAIABAAAkAAgAAAASAABAAIABAAAgAQACAABAAgAEAACABAAIAAEACAAAABIABA 42 | AAACQACAAAAEgAAAAQAIAAEAAgAQAAIABAAIAAEgAEAAgAAQAAIABAAIAAEgAEAAgAAQAAIA 43 | BAAIAAEgAEAAgAAQAAIABAAIAAEgAEAAgAAQAAIABAAIAAEACAAQAAIAEAAgAAQAIABAAAgA 44 | QACAABAAIAAAASAAQAAAAkAAgAAABIAAAAEACAABAAIABIAAEAAgAEAACAABAAIABIAAEAAg 45 | AEAACAABAAIABIAAEAAgAEAACAABAAIABIAAEAAgAEAACAABAAIABIAA8DgEAQACQAAAAgAE 46 | gAAABAAIAAEACAAQAAIABAAgAAQACABAAAgAEACAABAAIAAAASAAQACAABAAAgAEAAgAASAA 47 | QACAABAAAgAEAAgAASAAQACAABAAAgAEAAgAASAAQACAABAAAgAEAAgAASAAQACAABAAHoMg 48 | AEAACABAAIAAEACAAAABIAAAAQACQACAAPBDAAEgAEAAAAJAAIAAAASAAAABAAgAAQACAASA 49 | ABAAIABAAAgAAQACAASAABAAIABAAAgAAQACAASAABAAIABAAAgAAQACAASAABAAIABAAAgA 50 | AQACAASAABAAIABAAAgAQACAABAAgAAAASAAAAEAAkAAAAIABIAAAAEACAABAAIAEAACAAQA 51 | IAAEAAgAQAAIABAAIAAEgAAAAQACQAAIABAAIAAEgAAAAQACQAAIABAAIAAEgAAAAQACQAAI 52 | ABAAIAAEgAAAAQACQAAIABAAIAAEACAAQAAIAEAAgAAQAIAAAAEgAAABAAJAAIAAAASAAAAB 53 | AAgAAQACABAAAgAEACAABAAIABAAAkAAgAAAASAABAAIABAAAkAAgAAAASAABAAIABAAAkAA 54 | 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 225 | rkJggg==' 226 | ] 227 | 228 | { #category : #'GT-InspectorExtensions' } 229 | PDSIcons class >> gtInspectorIconsIn: composite [ 230 | 231 | 232 | composite table 233 | title: 'Icons'; 234 | display: [ (self class selectorsInCategory: 'accessing - icons') sorted 235 | collect: [ :each | each -> (self perform: each) ] ]; 236 | column: 'Icon' evaluated: #value width: 50; 237 | column: 'Selector' evaluated: #key 238 | ] 239 | 240 | { #category : #accessing } 241 | PDSIcons class >> icons [ 242 | ^ icons ifNil: [ icons := Dictionary new ] 243 | ] 244 | 245 | { #category : #'accessing - icons' } 246 | PDSIcons class >> mortarboardIcon [ 247 | "Private - Generated method" 248 | ^ self icons 249 | at: #mortarboardIcon 250 | ifAbsentPut: [ Form 251 | fromBinaryStream: self mortarboardIconContents base64Decoded readStream ] 252 | ] 253 | 254 | { #category : #'private - contents' } 255 | PDSIcons class >> mortarboardIconContents [ 256 | "Private - Method generated with the content of the file File @ /data/pharo/icons/mortarboard.png" 257 | ^ 'iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQICAgIfAhkiAAAAAlw 258 | SFlzAAALEwAACxMBAJqcGAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoA 259 | ACAASURBVHic7N17nF1VfTf+z9rXc5kzkwRCEhVFpGjAAEpVVKxW0D6WhmQCwy3FJzmZDCGS 260 | hKtQbWV+Pn20pVZaaenzYMBULsnkzJlEQy1oa/XBW72hKFc1SIOVhJDbnNu+rt8fyT5O7nM5 261 | 56y9z/m8X695zTCZOftLMrP3Z6/13WsBRERERERERERERERERERERERERERERERERERERERE 262 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 263 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 264 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 265 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 266 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 267 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 268 | RERERERERERERERERERERERERERERERERERERERERE0kVBdARK1x1VVXvc5xnI/5vn9xGIYz 269 | giAwAUDXdU/TtFcMw9hi2/anHnrooRdU10pEzccAQNTmLrvssj/0PO9Ox3HOllIe82uFELAs 270 | a6uu69cXi8UtLSqRiBRgACBqU5deeulK3/f/zHGc10zm+y3L2mWa5t3Dw8O3AwgbXB4RKcYA 271 | QNRG5s+fn7Es6y89z1vueV5XI17TMIyqZVnFIAhWbd68eU8jXpOI1GMAIGoDfX19rw6C4C7H 272 | ceaHYWg04xiapoW2bT9mmuayoaGhXzXjGETUOgwARAm2aNGiPwDw17Va7R1Sypb8PgshpG3b 273 | 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 465 | tmzh6HQLcASgdUqqCyAiijvJTuqWYQAgIiLqQAwAREREHYgBgIiIqAMxABAREXUgBgAiIqIO 466 | xABARETUgRgAiIiIOhADABERUQdiACAiIupADABEREQdiAGAiIioAzEAEBERdSAGACIiog7E 467 | AEBERNSBGACIiIg6EAMAERFRB2IAICIi6kAMAERERB2IAYCIiKgDMQAQERF1IAYAIiKiDsQA 468 | QERE1IEYAIiIiDoQAwAREVEHYgAgIiLqQAwAREREHYgBgIiIqAMxABAREXUgBgAiIqIOxABA 469 | RETUgRgAiIiIOhADABERUQdiACAiIupADABEREQdiAGAiIioAzEAEBERdSAGACIiog7EAEBE 470 | RNSBGACIiIg6EAMAERFRB2IAICIi6kAMAERERB2IAYCIiKgDMQAQERF1IAYAIiKiDsQAQERE 471 | 1IEYAIiIiDqQoboAIqJO9bpZwJ+8Q0z5dd53tsBoVeI/nwGCoAGFUUdgACAiarHXzAT6/kDg 472 | zFMa83ozpwH5P5K44ryX8a1nc9j4rXRjXpjaGgMAEVGLWKZA77uBC94KaFO/8T+Y0GBZBt73 473 | xp14x2lp/OO/TsfW7XqDD0LthD0AREQtMHsG8GdXAh84twkX/wM0KwsASOtV3PQn27HonbXm 474 | HIjaAgMAEVGTvfFkgY9fJXDyzOYeR9MtQOxPFwIBLjzjZdy4YF9zD0qJxQBARNRE814PXL8I 475 | SNstOJjQIPSDD3TaiXvx0V6GADocAwARUZOcOgdYMV/AbGG3laabh33ulBl7cf3FDAF0MAYA 476 | IqImmJEDVvcK2Idfj5tLO3LaOH3mXvYE0EEYAIiIGkzXgGv+RKBLwdN4Qjt65/8FZ76CN76G 477 | CwXQfgwAREQN9oFzgTe8StXRj35aFzLEig+8EvUJUodjACAiaqAZOWD+O9VdYY93cbc1B33v 478 | qramGIo1BgAiogb643eg9fP+Y0gpj/s17527BymrBcVQrDEAEBE1SE8WePeZisfXxxEAhPSx 479 | 6LxyC4qhOGMAICJqkPPf3NpH/o5Eht64vu5tp1aaXAnFHQMAEVGDvPMM1RUAMvTH9XW2XsPc 480 | k8f3tdSeGACIiBpg9oz9b6rJwB33177/zVwXoJMxABARNcCbTlZdASDDADIY3xQAALzuRAaA 481 | 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 561 | Rj65bAaE+CaANysqoQyJD4kz1z6m6PgUUx154iGKi4suumh6Op2+0ff9JY7jvEZ1Pa1gWdYu 562 | wzCGLcv6xPr167errqcV5E+vzsJI/RWEXAagVb0hEsDXIcRKMffzz7W3KeegAAADRklEQVTo 563 | mJQgDABEMXH11Ve/vlKp3Ob7/iLXdU9UXU8jmaa5zzCMfzdN8+NDQ0NPq65HFfn4kmmwzD+A 564 | wOsBNGcZahkG0MRvEMofiDPv/WVTjkFtgQGAKIYuueSSd0gpP+F53vuTul+BYRhVy7L+3TCM 565 | Tw4NDf1AdT1EdDAGAKKYu+yyy/7U87yPOY4zN+5PEgghYFnWi5qm3TUyMnKH6nqI6OgYAIgS 566 | 4oorrjjZ87xPOY5zaRAEsRoVMAzDtSzr3x3HWfPwww//QnU9RHR8DABECRSNCtRqtbkq67Bt 567 | O7rb/wyAUGUtRDQxDABECdbX13dGEAR/67ruhUEQGK04pq7rvmVZXxVC3FQsFp9pxTGJqPEY 568 | AIjawODgoPHzn//8dtd1r/c8r6sZxzjQ1Hd/Lpdbs24dd5QjSjoGAKI2c+mll97suu6tnuc1 569 | 5FFC27Zf1nX908Vi8c5GvB4RxQMDAFGbuvTSS/t83//sZBcYsixrl2VZNxUKhXUNLo2IYoAB 570 | gKjNXXLJJVcGQfC3juPMGc/X27b9W8MwbhweHt7Q7NqISB0GAKIOcfnll3/I87xPuq47LwgC 571 | e+yfGYbhmKb5hGEYf7Fx48ZHVdVIRK3DAEDUgRYuXDgtlUq9MQgCCeDZQqGwV3VNRERERERE 572 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 573 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 574 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 575 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 576 | RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE 577 | RERERERERERERERE9P+3BwckAAAAAIL+v+5HqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 578 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAXwGsfmSX+JOx2gAAAABJRU5ErkJggg==' 579 | ] 580 | 581 | { #category : #'accessing - reset' } 582 | PDSIcons class >> resetIcons [ 583 |