Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added new scatter plot #157

Merged
merged 15 commits into from
Oct 31, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_smoke_full(self):
# everything without it exploding?
backup_argv = sys.argv
sys.argv = ["python", "--width", "16", "--height", "16",
"-r", "-v", "--gs", "--temps",
"-r", "-v", "--gs", "--scatter", "--temps",
".126/.235/.406/.561/.634/.876", "--humidity",
".059/.222/.493/.764/.927/.986/.998"]
try:
Expand Down
7 changes: 6 additions & 1 deletion tests/draw_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
from worldengine.draw import _biome_colors, Image, draw_simple_elevation, elevation_color, \
draw_elevation, draw_riversmap, draw_grayscale_heightmap, draw_ocean, draw_precipitation, \
draw_world, draw_temperature_levels, draw_biome
draw_world, draw_temperature_levels, draw_biome, draw_scatter_plot
from worldengine.biome import Biome
from worldengine.world import World

Expand Down Expand Up @@ -162,5 +162,10 @@ def test_draw_biome(self):
draw_biome(w, target)
self._assert_img_equal("biome_28070", target)

def test_draw_scatter_plot(self):
Copy link
Member

Choose a reason for hiding this comment

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

this tests just that it does not explode, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a coverage test. It tests pretty much all of the new code (since the world being used contains all temperature and humidity ranges).

w = World.open_protobuf("%s/seed_28070.world" % self.tests_data_dir)
target = PixelCollector(16, 16)
draw_scatter_plot(w, 16, target)

if __name__ == '__main__':
unittest.main()
14 changes: 13 additions & 1 deletion worldengine/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from worldengine.common import array_to_matrix, set_verbose, print_verbose
from worldengine.draw import draw_ancientmap_on_file, draw_biome_on_file, draw_ocean_on_file, \
draw_precipitation_on_file, draw_grayscale_heightmap_on_file, draw_simple_elevation_on_file, \
draw_temperature_levels_on_file, draw_riversmap_on_file
draw_temperature_levels_on_file, draw_riversmap_on_file, draw_scatter_plot_on_file
from worldengine.plates import world_gen, generate_plates_simulation
from worldengine.imex import export
from worldengine.step import Step
Expand Down Expand Up @@ -78,6 +78,10 @@ def generate_rivers_map(world, filename):
draw_riversmap_on_file(world, filename)
print("+ rivers map generated in '%s'" % filename)

def draw_scatter_plot(world, filename):
draw_scatter_plot_on_file(world, filename)
print("+ scatter plot generated in '%s'" % filename)


def generate_plates(seed, world_name, output_dir, width, height,
num_plates=10):
Expand Down Expand Up @@ -312,6 +316,8 @@ def main():
g_generate.add_argument('--not-fade-borders', dest='fade_borders', action="store_false",
help="Not fade borders",
default=True)
g_generate.add_argument('--scatter', dest='scatter_plot',
Copy link
Member

Choose a reason for hiding this comment

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

perhaps it would make more sense to have it as a separate operation, like drawing the ancient map. We could also want to change the name because scatter plot is a type of plot but it does not tell us what kind of information is being represented. Later we could have differe scatter plots representing different things

Copy link
Member

Choose a reason for hiding this comment

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

We could leave it as an option of the generation process for now but just change the name

action="store_true", help="generate scatter plot")
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this necessitate updating README.md? I would prefer it.


# -----------------------------------------------------
g_ancient_map = parser.add_argument_group(
Expand Down Expand Up @@ -452,6 +458,8 @@ def main():
humids = args.humids.split('/')
for x in range(0,7):
humids[x] = 1 - float(humids[x])
if args.scatter_plot and not generation_operation:
usage(error="Scatter plot can be produced only during world generation")

print('Worldengine - a world generator (v. %s)' % VERSION)
print('-----------------------')
Expand All @@ -467,6 +475,7 @@ def main():
print(' step : %s' % step.name)
print(' greyscale heightmap : %s' % args.grayscale_heightmap)
print(' rivers map : %s' % args.rivers_map)
print(' scatter plot : %s' % args.scatter_plot)
print(' fade borders : %s' % args.fade_borders)
if args.temps:
print(' temperature ranges : %s' % args.temps)
Expand Down Expand Up @@ -498,6 +507,9 @@ def main():
if args.rivers_map:
generate_rivers_map(world,
'%s/%s_rivers.png' % (args.output_dir, world_name))
if args.scatter_plot:
draw_scatter_plot(world,
'%s/%s_scatter.png' % (args.output_dir, world_name))

elif operation == 'plates':
print('') # empty line
Expand Down
110 changes: 110 additions & 0 deletions worldengine/draw.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from PIL import Image
import numpy
import numpy.ma as ma

from worldengine.drawing_functions import draw_ancientmap, \
draw_rivers_on_image, gradient
Expand Down Expand Up @@ -406,6 +407,110 @@ def draw_biome(world, target):
v = biome[y, x]
target.set_pixel(x, y, _biome_colors[v])

def draw_scatter_plot(world, size, target):
""" This function can be used on a generic canvas (either an image to save
on disk or a canvas part of a GUI)
"""

#Find min and max values of humidity and temperature on land so we can
#normalize temperature and humidity to the chart
humid = ma.masked_array(world.humidity['data'], mask=world.ocean)
temp = ma.masked_array(world.temperature['data'], mask=world.ocean)
min_humidity = humid.min()
max_humidity = humid.max()
min_temperature = temp.min()
max_temperature = temp.max()
temperature_delta = max_temperature - min_temperature
Copy link
Contributor

Choose a reason for hiding this comment

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

You seem to have included this twice now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Thanks. Fixed.

humidity_delta = max_humidity - min_humidity

#set all pixels white
for y in range(0, size):
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind adding comments to every step of the preparation? Just two words to make the code more digestible. Somebody will come after you at some point. ;)

for x in range(0, size):
target.set_pixel(x, y, (255, 255, 255, 255))

#fill in 'bad' boxes with grey
h_values = ['62', '50', '37', '25', '12']
t_values = [ 0, 1, 2, 3, 5 ]
for loop in range(0,5):
h_min = (size - 1) * ((world.humidity['quantiles'][h_values[loop]] - min_humidity) / humidity_delta)
if loop != 4:
h_max = (size - 1) * ((world.humidity['quantiles'][h_values[loop + 1]] - min_humidity) / humidity_delta)
else:
h_max = size
v_max = (size - 1) * ((world.temperature['thresholds'][t_values[loop]][1] - min_temperature) / temperature_delta)
if h_min < 0:
h_min = 0
if h_max > size:
h_max = size
if v_max < 0:
v_max = 0
if v_max > (size - 1):
v_max = size - 1

if h_max > 0 and h_min < size and v_max > 0:
for y in range(int(h_min), int(h_max)):
for x in range(0, int(v_max)):
target.set_pixel(x, (size - 1) - y, (128, 128, 128, 255))

#draw lines based on thresholds
for t in range(0, 6):
v = (size - 1) * ((world.temperature['thresholds'][t][1] - min_temperature) / temperature_delta)
if v > 0 and v < size:
for y in range(0, size):
target.set_pixel(int(v), (size - 1) - y, (0, 0, 0, 255))
ranges = ['87', '75', '62', '50', '37', '25', '12']
for p in ranges:
h = (size - 1) * ((world.humidity['quantiles'][p] - min_humidity) / humidity_delta)
if h > 0 and h < size:
for x in range(0, size):
target.set_pixel(x, (size - 1) - int(h), (0, 0, 0, 255))

#examine all cells in the map and if it is land get the temperature and
#humidity for the cell.
for y in range(world.height):
for x in range(world.width):
if world.is_land((x, y)):
t = world.temperature_at((x, y))
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure what the "convention" is, but I prefer world.temperature['data'] - it saves a ton of function calls and looks a lot more in line with world.humidity['data'], which is just prettier.

Copy link
Member

Choose a reason for hiding this comment

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

actually I would prefer temperature_at: the fact there is an array named data inside the field temperature is an implementation details and it is better to avoid relying on that when it makes sense to use something slightly more declarative

Copy link
Contributor

Choose a reason for hiding this comment

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

Mmh, ok. I was mostly bothered by it since it doesn't look homogeneous.

p = world.humidity['data'][y, x]

#get red and blue values depending on temperature and humidity
if world.is_temperature_polar((x, y)):
r = 0
elif world.is_temperature_alpine((x, y)):
r = 42
elif world.is_temperature_boreal((x, y)):
r = 85
elif world.is_temperature_cool((x, y)):
r = 128
elif world.is_temperature_warm((x, y)):
r = 170
elif world.is_temperature_subtropical((x, y)):
r = 213
elif world.is_temperature_tropical((x, y)):
r = 255
if world.is_humidity_superarid((x, y)):
b = 32
elif world.is_humidity_perarid((x, y)):
b = 64
elif world.is_humidity_arid((x, y)):
b = 96
elif world.is_humidity_semiarid((x, y)):
b = 128
elif world.is_humidity_subhumid((x, y)):
b = 160
elif world.is_humidity_humid((x, y)):
b = 192
elif world.is_humidity_perhumid((x, y)):
b = 224
elif world.is_humidity_superhumid((x, y)):
b = 255

#calculate x and y position based on normalized temperature and humidity
nx = (size - 1) * ((t - min_temperature) / temperature_delta)
ny = (size - 1) * ((p - min_humidity) / humidity_delta)

target.set_pixel(int(nx), (size - 1) - int(ny), (r, 128, b, 255))


# -------------
# Draw on files
Expand Down Expand Up @@ -477,3 +582,8 @@ def draw_ancientmap_on_file(world, filename, resize_factor=1,
draw_biome, draw_rivers, draw_mountains, draw_outer_land_border,
verbose)
img.complete()

def draw_scatter_plot_on_file(world, filename):
img = ImagePixelSetter(512, 512, filename)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the hard-coded values? What happens to non-square maps?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are plots, not maps. They allow you to see the relationship between temperature and rainfall so you can be sure that the values are too far out of bounds. As such there is no reason for the size of the plot to be related to the size of the map (and since I generate maps that are 2k and 4k I really don't want plots that large).

I could add arguments so that there's a completely separate X and Y value for the scatter plot but it seems like an excessive complication that would lead to occasional user errors. Not many, granted, but it would still probably cause more problems than it resolves.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the plot is purely used for informational purposes, I guess it might be ok. If others are fine with it, then do it the way you like.
(I love the huge maps, however.^^)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah. I love huge maps as well. but it just makes no sense to make the plot huge. I'm going to include an example plot below so you can see what it looks like (I will probably make some changes in the future such as labeling the axis, but for now this should suffice).

draw_scatter_plot(world, 512, img)
Copy link
Member

Choose a reason for hiding this comment

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

but draw_scatter_plot doesn't take just 2 parameters?
perhaps we could use constants to make it clear the meaning of the value 512. It is absolutely fine to have a chart with fixed size, it should be just clear that the first 512x512 means the chart is 512 pixels wide and tall

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah. That's a little bit of future proofing. While I think it would be a mistake to make it so that the size can be changed I didn't want to hardwire it in case I missed something and there was a compelling reason).

This way if I'm wrong it requires much less in the way of code change.

img.complete()
2 changes: 1 addition & 1 deletion worldengine/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ def generate_world(w, step):
'': sub_seeds[99]
}

TemperatureSimulation().execute(w, seed_dict['TemperatureSimulation'])
# Precipitation with thresholds
PrecipitationSimulation().execute(w, seed_dict['PrecipitationSimulation'])

Expand All @@ -226,7 +227,6 @@ def generate_world(w, step):

# FIXME: create setters
IrrigationSimulation().execute(w, seed_dict['IrrigationSimulation']) # seed not currently used
TemperatureSimulation().execute(w, seed_dict['TemperatureSimulation'])
HumiditySimulation().execute(w, seed_dict['HumiditySimulation']) # seed not currently used


Expand Down
2 changes: 1 addition & 1 deletion worldengine/simulations/humidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def _calculate(world):
irrigationWeight = 3
humidity['data'] = numpy.zeros((world.height, world.width), dtype=float)

humidity['data'] = (((world.precipitation['data'] * precipitationWeight - world.irrigation * irrigationWeight) + 1) * ((world.temperature['data'] * temperatureWeight + 1) / (temperatureWeight + 1)))
humidity['data'] = (world.precipitation['data'] * precipitationWeight - world.irrigation * irrigationWeight)/(precipitationWeight + irrigationWeight)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is interesting. A result of studying the plot?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A result of making the temperature affect precipitation more directly.


# These were originally evenly spaced at 12.5% each but changing them
# to a bell curve produced better results
Expand Down
68 changes: 60 additions & 8 deletions worldengine/simulations/precipitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def is_applicable(world):
def execute(self, world, seed):
if get_verbose():
start_time = time.time()
pre_calculated = self._calculate(seed, world.width, world.height)
pre_calculated = self._calculate(seed, world)
ths = [
('low', find_threshold_f(pre_calculated, 0.75, world.ocean)),
('med', find_threshold_f(pre_calculated, 0.3, world.ocean)),
Expand All @@ -29,31 +29,83 @@ def execute(self, world, seed):
% elapsed_time)

@staticmethod
def _calculate(seed, width, height):
def _calculate(seed, world):
"""Precipitation is a value in [-1,1]"""
rng = numpy.random.RandomState(seed) # create our own random generator
base = rng.randint(0, 4096)

curve_gamma = 1.25
curve_bonus = .2
height = world.height
width = world.width
border = width / 4
precipitations = numpy.zeros((height, width), dtype=float)

octaves = 6
freq = 64.0 * octaves

n_scale = 1024 / float(height) #This is a variable I am adding. It exists
#so that worlds sharing a common seed but
#different sizes will have similar patterns
Copy link
Contributor

Choose a reason for hiding this comment

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

Common seed and different size? To me it looks more like common size and different seed since n_scale depends on height.

This file, too, would have to be changed a bit after a rebase (lines 41 and 42 will change, I think).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I mean is that if I generate a world with seed 1 at 1024 x 1024 it will have the same general temperature and precipitation maps as a world generated at 512 x 512, simply at a larger size. It is a step towards allowing people to generate 'low resolution' maps quickly until they find something they like and then taking the time to generate a higher resolution map without totally changing the results.

There's a lot of other steps that will need to be done before this fully works. Right now the land mass itself will completely change, but this is a first step.


for y in range(height):#TODO: numpy
y_scaled = float(y) / height
latitude_factor = 1.0 - (abs(y_scaled - 0.5) * 2)
for x in range(width):
n = snoise2(x / freq, y / freq, octaves, base=base)
n = snoise2((x * n_scale) / freq, (y * n_scale) / freq, octaves, base=base)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind adding some comments? I recently worked into the previous version of the code and found it difficult to understand.


# Added to allow noise pattern to wrap around right and left.
if x < border:
n = (snoise2(x / freq, y / freq, octaves,
n = (snoise2( (x * n_scale) / freq, (y * n_scale) / freq, octaves,
base=base) * x / border) + (
snoise2((x + width) / freq, y / freq, octaves,
snoise2(( (x * n_scale) + width) / freq, (y * n_scale) / freq, octaves,
base=base) * (border - x) / border)

precipitation = (latitude_factor + n * 4) / 5.0
precipitations[y, x] = precipitation
precipitations[y, x] = n
Copy link
Contributor

Choose a reason for hiding this comment

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

Is latitude_factor now obsolete? If so, please remove it.
I am interested to see the results of these changes. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.


#find ranges
min_precip = precipitations.min()
max_precip = precipitations.max()
min_temp = world.temperature['data'].min()
max_temp = world.temperature['data'].max()
precip_delta = (max_precip - min_precip)
temp_delta = (max_temp - min_temp)

#normalize temperature and precipitation arrays
t = (world.temperature['data'] - min_temp) / temp_delta
p = (precipitations - min_precip) / precip_delta

#modify precipitation based on temperature
Copy link
Contributor

Choose a reason for hiding this comment

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

Very, very interesting! :)


#--------------------------------------------------------------------------------
#
# Ok, some explanation here because why the formula is doing this may be a
# little confusing. We are going to generate a modified gamma curve based on
# normalized temperature and multiply our precipitation amounts by it.
#
# numpy.power(t,curve_gamma) generates a standard gamma curve. However
# we probably don't want to be multiplying precipitation by 0 at the far
# side of the curve. To avoid this we multiply the curve by (1 - curve_bonus)
# and then add back curve_bonus. Thus, if we have a curve bonus of .2 then
# the range of our modified gamma curve goes from 0-1 to 0-.8 after we
# multiply and then to .2-1 after we add back the curve_bonus.
#
# Because we renormalize there is not much point to offsetting the opposite end
# of the curve so it is less than or more than 1. We are trying to avoid
# setting the start of the curve to 0 because f(t) * p would equal 0 when t equals
# 0. However f(t) * p does not automatically equal 1 when t equals 1 and if we
# raise or lower the value for f(t) at 1 it would have negligible impact after
# renormalizing.
#
#--------------------------------------------------------------------------------

curve = (numpy.power(t, curve_gamma) * (1-curve_bonus)) + curve_bonus
precipitations = numpy.multiply(p, curve)

#Renormalize precipitation because the precipitation
#changes will probably not fully extend from -1 to 1.
min_precip = precipitations.min()
max_precip = precipitations.max()
precip_delta = (max_precip - min_precip)
precipitations = (((precipitations - min_precip) / precip_delta) * 2) - 1

return precipitations
7 changes: 4 additions & 3 deletions worldengine/simulations/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,19 @@ def _calculate(world, seed, elevation, mountain_level):
border = width / 4
octaves = 8
freq = 16.0 * octaves
n_scale = 1024 / float(height)

for y in range(0, height):#TODO: Check for possible numpy optimizations.
y_scaled = float(y) / height
latitude_factor = 1.0 - (abs(y_scaled - 0.5) * 2)
for x in range(0, width):
n = snoise2(x / freq, y / freq, octaves, base=base)
n = snoise2((x * n_scale) / freq, (y * n_scale) / freq, octaves, base=base)

# Added to allow noise pattern to wrap around right and left.
if x <= border:
n = (snoise2(x / freq, y / freq, octaves,
n = (snoise2((x * n_scale) / freq, (y * n_scale)/ freq, octaves,
base=base) * x / border) \
+ (snoise2((x + width) / freq, y / freq, octaves,
+ (snoise2(((x * n_scale) + width) / freq, (y * n_scale) / freq, octaves,
base=base) * (border - x) / border)

t = (latitude_factor * 12 + n * 1) / 13.0
Expand Down