-
Notifications
You must be signed in to change notification settings - Fork 127
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
Changes from all commits
e3f4bb7
d24e5c4
b0416b1
5aa77e1
d3e0c67
1127811
0413edb
ef31f3b
88a5fbd
629b476
820a0c5
07f4e2d
c0d9a34
b335459
51add74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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): | ||
|
@@ -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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
@@ -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('-----------------------') | ||
|
@@ -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) | ||
|
@@ -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 | ||
|
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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You seem to have included this twice now. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure what the "convention" is, but I prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually I would prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the hard-coded values? What happens to non-square maps? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but draw_scatter_plot doesn't take just 2 parameters? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting. A result of studying the plot? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)), | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 This file, too, would have to be changed a bit after a rebase (lines 41 and 42 will change, I think). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).