A few months after moving to California for graduate school in 2008, I started a balcony garden and soon became aware of how little balcony space is useful for growing plants that need a significant amount of direct sunlight. When I selected my current apartment, one of the primary criteria was the orientation of the balcony; southern exposure was essential, however the optimal placement of the plants on the balcony was still not straightforward. After about a year, I figured out the best way to arrange the plants. I knew that it would be fairly straightforward to write a program to help solve the placement problem, but I wasn't motivated to actually write such a program until 1) I failed to find an existing solution and 2) I found out about a Python module, called PyEphem that simplified the process. During my free time over the past two weeks or so, I developed a bit of code, called Somelight, that solves the problem pretty well. The code simply computes when a given location in space has a direct line of sight to the Sun, in the presence of obscuring surfaces. Somelight does not attempt to account for reflected light—only direct light.
In order to use Somelight, you need to do the following:
- Define a point in or near your garden as the origin of a Cartesian coordinate system.
- Decide which direction to define as the positive axis of the coordinate system (the and coordinates must be horizontal and the + direction must be straight up).
- Identify all objects that can significantly shade the garden (walls, fences, columns, roofs, neigboring houses, trees, hills, mountains etc.).
- Break the obscuring objects into quasi-planar segments and determine the () coordinates of the vertices of these segments. For objects like large buildings, mountains, or hills in the distance, you can construct proxy surfaces that have the same effect as the actual objects.
- Identify the offset of your coordinate system's +-axis from North.
- Identify the longitude and latitude of the garden site.
- Enter the garden description into a somelight.GardenVolume object.
- Use the methods of the somelight.GardenVolume object to answer your questions about direct sunlight exposure.
The following example demonstrates the basic procedure:
import numpy as np
# The latitude and longitude of the garden
latitude = 33.9853
longitude = -117.5527
# The direction of the +x axis of the local Cartesian coordinate system, measured
# in degrees. North is defined as 0.0 and the angle increases in a counter-clockwise
# sense (westward from North), such that West is 90 degrees and East is 270 degrees.
right = 103
# create a GardenVolume object
garden = somelight.GardenVolume(latitude, longitude, right)
# Specify the primary surfaces that can shade the garden. These should approximately
# be plane segments, however mild curvature is generally okay (the points do not need
# to be absolutely coplanar). Although the examples below are rectangles, any shape with
# at least three distinct vertices will work. Note that any length unit can be used, since
# only the RELATIVE positions and sizes of the planes are relevant.
# East wall
garden.add_plane_segment([(0, 0.0, 0), (0, 6, 0), (0, 6, 8), (0, 0.0, 8)])
# North wall
garden.add_plane_segment([(0, 0.0, 0), (6.0, 0, 0), (6, 0, 8), (0, 0.0, 8)])
garden.add_plane_segment([(-2.0, 6.5, 8), (6.5, 6.5, 8), (6.5, 0.0, 8), (-2.0, 0.0, 8)])
# North side of column
garden.add_plane_segment([(4.6, 4.6, 0), (6.0, 4.6, 0), (6.0, 4.6, 8), (4.6, 4.6, 8)])
# East side of column
garden.add_plane_segment([(4.6, 4.6, 0), (4.6, 6.0, 0), (4.6, 6.0, 8), (4.6, 4.6, 8)])
# Define the points of interest. These can be distributed throughout the volume of
# interest in any way you wish. In some cases, it may be more efficient to randomly
# scatter the points. In this example, the points are distributed uniformly in a
# planar region slightly above the floor of the garden.
points = 
for x in np.arange(0.1, 4.5, 0.2):
for y in np.arange(0.1, 6.0, 0.2):
points.append((x, y, 0.1))
for x in np.arange(4.5, 6.0, 0.2):
for y in np.arange(0.1, 4.5, 0.2):
points.append((x, y, 0.1))
# Draw a plot showing which points are illumnated at a specific moment.
garden.show_garden(points, '2016/12/30 12:32:30')
# Compute the total exposure time at each sample point during a specific day.
exposure_times = garden.exposure_on_date(points, '2016/12/30')
# when the sample points are distributed in a plane, you can create a meaningful
# 2-D map of the direct sun exposure time.
For step 5, Google Earth is helpful. You can also use a compass, but be sure to take magnetic declination into account if you use a compass; you need to measure the offset from true North, not magnetic North (note that some compass apps on smart phones do not take magnetic declination into account, while others do). You can also use the output of Somelight to calibrate this angle.
If you want to use Somelight inside, you can set
window_mode=True in the
somelight.GardenVolume constructor. Then, the planes that you specify are interpreted as windows, rather than obscurring surfaces.
Images of my balcony floor and corresponding output from the garden.showGarden method are shown below. Blue dots are points that are in direct sunlight. In the photographs, the +-axis is perpendicular to the left banister and the +-axis is perpendicular to the right banister (the position of the camera was about (0, .05, 5) or 5 length units above the origin of the coordinate system). The Somelight prediction agreees well with the actual Sun exposure, even though the balcony was not carefully measured.
The total direct sunlight exposure for any day of the year can be plotted. The figure below is the output of the command issued on the final line of the example above.
In addition to computing the number of minutes of direct sunlight exposure, Somelight can estimate the total radiant energy received. Here are plots of the radiant exposure for December 30th and May 15th. Note that the roof of the balcony greatly reduces the amount of light that the balcony recieves during the warmer months of the year, when the Sun is generally higher in the sky:
radiant_exposure_dec = garden.radiant_exposure_on_date(points, '2016/12/30')
radiant_exposure_may = garden.radiant_exposure_on_date(points, '2017/5/15')
# The 30.5 below is the maximum value for the color scale. I am setting both
# to 30.5 so that the plots can be compared more easily.
garden.show_interpolation(points, radiant_exposure_dec, 30.5)
garden.show_interpolation(points, radiant_exposure_may, 30.5)
And here's an animation showing the radiant exposure on the balcony for every Sunday of 2017:
How Somelight Works
For each point of interest, Somelight projects each plane, specified using the addPlaneSegment method, onto the sphere of the sky. The Cartesian coordinates () are projected onto horizontal coordinates (altitude, azimuth). If any of the projected polygons contain the coordinates of the Sun, then the point is either shaded (if the planes represented obscuring surfaces) or in direct sunlight (if the planes represented windows). The figure below shows how the projected shapes of the planes change as the -coordinate is varied in the example above. The left half of the figure represents the rear view (looking in the negative direction), while the right half represents the forward view. You can see the North wall's apparent size shrink and watch as observation point passes into and out of the shadow of the column:
The latest source can be obtained here: https://bitbucket.org/idius/somelight/src
Possible Future Steps
- Add the ability to compute the radiant exposure-weighted and time exposure-weighted azimuth and altitude angles.
- Add the ability to determine the altitude and azimuth angles of the Sun when the each point receives the most intense sunlight (i.e., the angles associated with the max irradiance).
- Investigate alternative algorithms to improve efficiency.
- Investigate the possibility of using the Shapely module for computational geometry.
- Re-implement the computationally expensive bits in parallelized C++.
- Add the ability to handle partially transparent surfaces and approximations to complex objects with many small "holes," such as fences and diciduous trees.
- Add the ability to output the radiant exposure during a specified period of time and the instantaneous irradiance.
Wish list, if I had unlimited time to work on this:
- Add ability to read OBJ or STL files (see numpy-stl) so that scenes can be created using 3D modelling software (even free web-based software like SculptGL or TinkerCAD. )
- Integrate with Blender or other ray tracing code to handle reflected light realistically.
- Develop into smart phone app that can automatically build a 3D model of the garden and overlay the output on live video (augmented reality).