1

Work on modes

This commit is contained in:
Alex Yatskov 2015-06-29 13:06:26 +09:00
parent 7d3ea8e5dd
commit cf586e853a
5 changed files with 105 additions and 126 deletions

View File

@ -52,38 +52,33 @@ func executeQuery(rw http.ResponseWriter, req *http.Request) {
entries := getRecords(queryContext{geo, request.Profile, request.WalkingDist}) entries := getRecords(queryContext{geo, request.Profile, request.WalkingDist})
features := fixFeatures(request.Features) features := fixFeatures(request.Features)
modes := fixModes(request.Modes)
minScore := request.MinScore foundEntries := findRecords(entries, features, modes, request.MinScore)
if request.Bracket != nil {
bracket := namedBracket{
request.Bracket.Name,
request.Bracket.Min,
request.Bracket.Max}
minScore = calibrateMinScore(entries, features, bracket)
}
foundEntries := findRecords(entries, features, minScore)
sorter := recordSorter{entries: foundEntries, key: request.SortKey, ascending: request.SortAsc} sorter := recordSorter{entries: foundEntries, key: request.SortKey, ascending: request.SortAsc}
sorter.sort() sorter.sort()
response := jsonQueryResponse{ response := jsonQueryResponse{
Count: len(foundEntries), Count: len(foundEntries),
Columns: make(map[string]jsonColumn), Columns: make(map[string]jsonColumn),
MinScore: minScore, MinScore: request.MinScore,
Records: make([]jsonRecord, 0)} Records: make([]jsonRecord, 0)}
for name, value := range features { for name, value := range features {
mode, _ := modes[name]
column := jsonColumn{ column := jsonColumn{
Bracket: jsonBracket{Max: -1, Min: 1}, Bracket: jsonBracket{Max: -1, Min: 1},
Mode: mode.String(),
Value: value, Value: value,
Steps: request.Resolution} Steps: request.Resolution}
hints := project( hints := project(
entries, entries,
features, features,
modes,
name, name,
minScore, request.MinScore,
request.Resolution) request.Resolution)
for _, hint := range hints { for _, hint := range hints {

View File

@ -88,7 +88,7 @@
var _height = 500; var _height = 500;
var _padding = 10; var _padding = 10;
var _panelSize = 20; var _panelSize = 20;
var _buttonSize = 20; var _modeSize = 20;
var _tickSize = 5; var _tickSize = 5;
var _width = 120; var _width = 120;
@ -111,7 +111,7 @@
_densitySize, _densitySize,
0, 0,
_width - (_bracketSize + _densitySize), _width - (_bracketSize + _densitySize),
_height - (_panelSize + _buttonSize) _height - (_panelSize + _modeSize)
).attr({ ).attr({
cursor: 'crosshair', cursor: 'crosshair',
stroke: _borderColor, stroke: _borderColor,
@ -123,7 +123,7 @@
0, 0,
0, 0,
_densitySize, _densitySize,
_height - (_panelSize + _buttonSize) _height - (_panelSize + _modeSize)
).attr({ ).attr({
stroke: _borderColor stroke: _borderColor
}); });
@ -131,7 +131,7 @@
// panel // panel
_elements.panel = _canvas.rect( _elements.panel = _canvas.rect(
0, 0,
_height - (_panelSize + _buttonSize), _height - (_panelSize + _modeSize),
_width - _bracketSize, _width - _bracketSize,
_panelSize _panelSize
).attr({ ).attr({
@ -141,7 +141,7 @@
// label // label
_elements.label = _canvas.text( _elements.label = _canvas.text(
(_width - _bracketSize) / 2, (_width - _bracketSize) / 2,
_height - (_panelSize / 2 + _buttonSize), _height - (_panelSize / 2 + _modeSize),
_name _name
).attr({ ).attr({
'dominant-baseline': 'middle', 'dominant-baseline': 'middle',
@ -153,10 +153,10 @@
_width - _bracketSize, _width - _bracketSize,
0, 0,
_bracketSize, _bracketSize,
_height - (_panelSize + _buttonSize) _height - (_panelSize + _modeSize)
).attr({ ).attr({
fill: _bracketColorBg fill: _bracketColorBg
}).click(bracketClick); });
// indiciator // indiciator
updateIndicator(_data.value); updateIndicator(_data.value);
@ -191,7 +191,7 @@
_elements.panel, _elements.panel,
_elements.tick, _elements.tick,
_elements.label, _elements.label,
_elements.button _elements.mode
); );
_elements.group.transform( _elements.group.transform(
@ -258,7 +258,7 @@
strokeWidth: 1, strokeWidth: 1,
stroke: _bracketColorFg, stroke: _bracketColorFg,
'stroke-dasharray': '5, 1' 'stroke-dasharray': '5, 1'
}).click(bracketClick); });
} }
if (_.has(_elements, 'bracketMin')) { if (_.has(_elements, 'bracketMin')) {
@ -297,19 +297,19 @@
} }
function updateMode() { function updateMode() {
if (_.has(_elements, 'button')) { if (_.has(_elements, 'mode')) {
} }
else { else {
_elements.button = _canvas.text( _elements.mode = _canvas.text(
(_width - _bracketSize) / 2, (_width - _bracketSize) / 2,
_height - _buttonSize / 2, _height - _modeSize / 2,
'' 'x'
).attr({ ).attr({
'dominant-baseline': 'middle', 'dominant-baseline': 'middle',
'text-anchor': 'middle' 'text-anchor': 'middle'
}); }).click(modeClick);
} }
} }
@ -344,25 +344,12 @@
return colorStops; return colorStops;
} }
function updateState(value, bracket) { function updateState(value, mode) {
var updateBracket = bracket !== null;
var updateValue = value !== null;
if (updateBracket) {
_data.bracket.max = _range.clamp(bracket.max);
_data.bracket.min = _range.clamp(bracket.min);
}
if (updateValue) {
_data.value = _range.clamp(value); _data.value = _range.clamp(value);
} _data.mode = mode;
if (_onStateChanged) { if (_onStateChanged) {
_onStateChanged( _onStateChanged(_name, _data.value, _data.mode);
_name,
updateValue ? _data.value : null,
updateBracket ? _data.bracket : null
);
} }
animateIndicator(_valueTrans, _data.value); animateIndicator(_valueTrans, _data.value);
@ -462,19 +449,11 @@
function indicatorClick(event, x, y) { function indicatorClick(event, x, y) {
var rect = _canvas.node.getBoundingClientRect(); var rect = _canvas.node.getBoundingClientRect();
updateState(indicatorToValue(y - rect.top), _data.mode);
updateState(indicatorToValue(y - rect.top), null);
} }
function bracketClick(event, x, y) { function modeClick(event, x, y) {
var mid = (_data.bracket.min + _data.bracket.max) / 2; alert('mode clicked');
var rect = _canvas.node.getBoundingClientRect();
var dist = Math.abs(mid - bracketToValue(y - rect.top));
dist = Math.min(dist, Math.abs(_range.max - mid));
dist = Math.min(dist, Math.abs(_range.min - mid));
updateState(_data.value, {max: mid + dist, min: mid - dist});
} }
this.update = function(data, scale) { this.update = function(data, scale) {
@ -492,6 +471,10 @@
_data.bracket = data.bracket; _data.bracket = data.bracket;
animateBracket(_bracketTrans, _data.bracket); animateBracket(_bracketTrans, _data.bracket);
} }
if (_.has(data, 'mode')) {
_data.mode = data.mode;
updateMode();
}
}; };
createShapes(); createShapes();

View File

@ -25,18 +25,9 @@
var _ctx = {}; var _ctx = {};
function onStateChanged(name, value, bracket) { function onStateChanged(name, value, mode) {
_ctx.query.features[name] = value; _ctx.query.features[name] = value;
if (bracket === null) { _ctx.query.modes[name] = mode;
_ctx.query.bracket = null;
}
else {
_ctx.query.bracket = {
name: name,
min: bracket.min,
max: bracket.max
};
}
$.post('/query', JSON.stringify(_ctx.query), function(results) { $.post('/query', JSON.stringify(_ctx.query), function(results) {
saveSnapshot(results); saveSnapshot(results);
@ -87,7 +78,7 @@
function onSearch() { function onSearch() {
_ctx.query = { _ctx.query = {
features: _ctx.query.features || {}, features: _ctx.query.features || {},
bracket: null, modes: _ctx.query.modes || {},
sortKey: _ctx.sortKey, sortKey: _ctx.sortKey,
sortAsc: _ctx.sortAsc, sortAsc: _ctx.sortAsc,
profile: getProfile(), profile: getProfile(),
@ -132,7 +123,8 @@
columns[feature] = { columns[feature] = {
value: column.value, value: column.value,
hints: column.hints, hints: column.hints,
bracket: column.bracket bracket: column.bracket,
mode: column.mode
}; };
} }

View File

@ -24,7 +24,16 @@ package main
import "sort" import "sort"
type modeType int
const (
ModeTypeNone modeType = iota
ModeTypeProduct
ModeTypeDist
)
type featureMap map[string]float64 type featureMap map[string]float64
type modeMap map[string]modeType
type jsonAccessRequest struct { type jsonAccessRequest struct {
Id int `json:"id"` Id int `json:"id"`
@ -37,11 +46,11 @@ type jsonGeoData struct {
} }
type jsonQueryRequest struct { type jsonQueryRequest struct {
Bracket *jsonNamedBracket `json:"bracket"`
Features featureMap `json:"features"` Features featureMap `json:"features"`
Geo *jsonGeoData `json:"geo"` Geo *jsonGeoData `json:"geo"`
MaxResults int `json:"maxResults"` MaxResults int `json:"maxResults"`
MinScore float64 `json:"minScore"` MinScore float64 `json:"minScore"`
Modes map[string]string `json:"modeMap"`
Profile featureMap `json:"profile"` Profile featureMap `json:"profile"`
Resolution int `json:"resolution"` Resolution int `json:"resolution"`
SortAsc bool `json:"sortAsc"` SortAsc bool `json:"sortAsc"`
@ -52,6 +61,7 @@ type jsonQueryRequest struct {
type jsonColumn struct { type jsonColumn struct {
Bracket jsonBracket `json:"bracket"` Bracket jsonBracket `json:"bracket"`
Hints []jsonProjection `json:"hints"` Hints []jsonProjection `json:"hints"`
Mode string `json:"mode"`
Steps int `json:"steps"` Steps int `json:"steps"`
Value float64 `json:"value"` Value float64 `json:"value"`
} }
@ -79,11 +89,6 @@ type jsonBracket struct {
Max float64 `json:"max"` Max float64 `json:"max"`
} }
type jsonNamedBracket struct {
jsonBracket
Name string `json:"name"`
}
type jsonQueryResponse struct { type jsonQueryResponse struct {
Columns map[string]jsonColumn `json:"columns"` Columns map[string]jsonColumn `json:"columns"`
Count int `json:"count"` Count int `json:"count"`
@ -131,12 +136,6 @@ type geoData struct {
longitude float64 longitude float64
} }
type namedBracket struct {
name string
min float64
max float64
}
type record struct { type record struct {
accessCount int accessCount int
closestStn string closestStn string
@ -196,3 +195,25 @@ func (s recordSorter) Less(i, j int) bool {
func (s recordSorter) Swap(i, j int) { func (s recordSorter) Swap(i, j int) {
s.entries[i], s.entries[j] = s.entries[j], s.entries[i] s.entries[i], s.entries[j] = s.entries[j], s.entries[i]
} }
func (m modeType) String() string {
switch m {
case ModeTypeProduct:
return "prod"
case ModeTypeDist:
return "dist"
default:
return "invalid"
}
}
func strToModeType(mode string) modeType {
switch mode {
case "prod":
return ModeTypeProduct
case "dist":
return ModeTypeDist
default:
return ModeTypeNone
}
}

80
util.go
View File

@ -47,6 +47,16 @@ func fixFeatures(features featureMap) featureMap {
return fixedFeatures return fixedFeatures
} }
func fixModes(modes map[string]string) modeMap {
result := make(modeMap)
for name, value := range modes {
result[name] = strToModeType(value)
}
return result
}
func innerProduct(features1 featureMap, features2 featureMap) float64 { func innerProduct(features1 featureMap, features2 featureMap) float64 {
var result float64 var result float64
for key, value1 := range features1 { for key, value1 := range features1 {
@ -57,19 +67,35 @@ func innerProduct(features1 featureMap, features2 featureMap) float64 {
return result return result
} }
func walkMatches(entries records, features featureMap, minScore float64, callback func(record, float64)) { func compare(features1 featureMap, features2 featureMap, modes modeMap) float64 {
var result float64
for key, value1 := range features1 {
value2, _ := features2[key]
switch mode, _ := modes[key]; mode {
case ModeTypeDist:
result += 1 - math.Abs(value1-value2)/2
default:
result += value1 * value2
}
}
return result
}
func walkMatches(entries records, features featureMap, modes modeMap, minScore float64, callback func(record, float64)) {
for _, entry := range entries { for _, entry := range entries {
if score := innerProduct(features, entry.features); score >= minScore { if score := compare(features, entry.features, modes); score >= minScore {
callback(entry, score) callback(entry, score)
} }
} }
} }
func statRecords(entries records, features featureMap, minScore float64) (float64, int) { func statRecords(entries records, features featureMap, modes modeMap, minScore float64) (float64, int) {
var compatibility float64 var compatibility float64
var count int var count int
walkMatches(entries, features, minScore, func(entry record, score float64) { walkMatches(entries, features, modes, minScore, func(entry record, score float64) {
compatibility += entry.compatibility compatibility += entry.compatibility
count++ count++
}) })
@ -89,10 +115,10 @@ func stepRange(min, max float64, steps int, callback func(float64)) {
} }
} }
func findRecords(entries records, features featureMap, minScore float64) records { func findRecords(entries records, features featureMap, modes modeMap, minScore float64) records {
var foundEntries records var foundEntries records
walkMatches(entries, features, minScore, func(entry record, score float64) { walkMatches(entries, features, modes, minScore, func(entry record, score float64) {
entry.score = score entry.score = score
foundEntries = append(foundEntries, entry) foundEntries = append(foundEntries, entry)
}) })
@ -100,45 +126,7 @@ func findRecords(entries records, features featureMap, minScore float64) records
return foundEntries return foundEntries
} }
func calibrateMinScore(entries records, features featureMap, bracket namedBracket) float64 { func project(entries records, features featureMap, modes modeMap, featureName string, minScore float64, steps int) []queryProjection {
bestScoreRank := -math.MaxFloat64
var bestMinScore float64
for minScore := float64(-len(features)); minScore <= float64(len(features)); minScore += 0.1 {
var scoreRank float64
for _, entry := range entries {
value, ok := entry.features[bracket.name]
if !ok {
continue
}
score := innerProduct(features, entry.features)
if score < minScore {
continue
}
if score > minScore {
if value >= bracket.min && value <= bracket.max {
dist := math.Abs(value - features[bracket.name])
scoreRank += 1 / (dist * dist)
} else {
dist := math.Min(math.Abs(value-bracket.min), math.Abs(value-bracket.max))
scoreRank -= 1 / (dist * dist)
}
}
}
if scoreRank > bestScoreRank {
bestScoreRank = scoreRank
bestMinScore = minScore
}
}
return bestMinScore
}
func project(entries records, features featureMap, featureName string, minScore float64, steps int) []queryProjection {
sampleFeatures := make(featureMap) sampleFeatures := make(featureMap)
for key, value := range features { for key, value := range features {
sampleFeatures[key] = value sampleFeatures[key] = value
@ -147,7 +135,7 @@ func project(entries records, features featureMap, featureName string, minScore
var projection []queryProjection var projection []queryProjection
stepRange(-1.0, 1.0, steps, func(sample float64) { stepRange(-1.0, 1.0, steps, func(sample float64) {
sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample
compatibility, count := statRecords(entries, sampleFeatures, minScore) compatibility, count := statRecords(entries, sampleFeatures, modes, minScore)
sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample
projection = append(projection, queryProjection{compatibility, count, sample}) projection = append(projection, queryProjection{compatibility, count, sample})