Skip to content
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
31 changes: 28 additions & 3 deletions Vida_Data/vplantr.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@

debug=0

###What a new seed doesn't copy from its parent (see copyForNewSeed):
###zeroSeedValues resets all of these anyway.
NOT_COPIED_FOR_SEEDS=("motherPlant", "seedList", "overlapList", "subregion")

###Values that can't be changed in place.
PLAIN_TYPES=(float, int, bool, str, type(None))

def isListOfPlainValues(value):
if type(value) is not list:
return False
for item in value:
if type(item) not in PLAIN_TYPES:
return False
return True

#class genericPlant(object):
class genericPlant(object):
###define the props on this object
Expand Down Expand Up @@ -210,10 +225,20 @@ def copyForNewSeed(self):
###of those anyway. So they are left out here, and everything else (the
###species settings, including lists such as the colours) is copied
###just as deepcopy did, so the seed has its own copies.
###Numbers, strings, True/False and None can't be changed in place, so
###deepcopy gives back the very same value for them: the seed can
###simply share them (copy.copy already did that). A list of such
###values (the colours, the growth records) gets a new list with the
###same values in it, which is also what deepcopy made. Anything else
###still goes through deepcopy.
theSeed=copy.copy(self)
for key in vars(self):
if key not in ["motherPlant", "seedList", "overlapList", "subregion"]:
setattr(theSeed, key, copy.deepcopy(getattr(self, key)))
for key, value in vars(self).items():
if type(value) in PLAIN_TYPES or key in NOT_COPIED_FOR_SEEDS:
continue
if isListOfPlainValues(value):
setattr(theSeed, key, list(value))
else:
setattr(theSeed, key, copy.deepcopy(value))
return theSeed

def makeSeed(self, theSeed, theGarden):
Expand Down
87 changes: 54 additions & 33 deletions Vida_Data/vworldr.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,43 @@ def determineDroughtTol(theGarden):
#print("here")
theGarden.kill(obj)

def countPhotonsGettingThrough(x, y, r, numbPhotons, covers):
###Drop numbPhotons photons at random on a plant at (x, y) with radius r,
###and count how many get through the canopies above it. covers is a list
###of (x, y, radius, transmittance) for those canopies, in the order of the
###plant's overlap list.
hitCount=0
twoPi=3.14*2
for photon in range(numbPhotons):
###pick uniformly distributed point in a circle
randAngle=random.random()*twoPi
#####
###This is apparently a very old bug dating back to 2009
###The way it is written the points cluster toward the center
###which is exactly what the comments mention it is trying to avoid
###STH 26 Sept 2026

# randr=(random.random()*(plant.r-0))+0 #random between 0 and the radius
# #randr =math.sqrt(randr) #if you don't use sqrt, you get clustering in the center
# randr =randr**0.5 #if you don't use sqrt, you get clustering in the center
randr=r*(random.random()**0.5) #r*sqrt(u) spreads photons evenly over the canopy
#####
photonX = (randr*math.cos(randAngle))+x
photonY = (randr*math.sin(randAngle))+y
###The photon stops at the first canopy it lands in (in the order of
###the overlap list), unless it gets through that canopy. Either way it
###isn't checked against the rest. (The distance is worked out as in
###geometry_utils.pointInsideCircle.)
blocked=False
for coverX, coverY, coverR, coverTransmittance in covers:
if math.hypot(coverX-photonX, coverY-photonY)<=coverR:
if random.random() > coverTransmittance:
blocked=True
break
if not blocked:
hitCount=hitCount+1
return hitCount

def determineShade(theGarden):
if theGarden.showProgressBar:
print("***Generating lists of overlapping plants. This could take a while...***")
Expand Down Expand Up @@ -186,37 +223,12 @@ def determineShade(theGarden):
numbPhotons= numbPhotons*100
if numbPhotons>750: #we don't need monster numbers
numbPhotons=750
hitCount=0
for photon in range(numbPhotons):
#####consider moving this to geometry_utils
###pick uniformly distributed point in a circle
twoPi=3.14*2
randAngle=random.random()*twoPi
#####
###This is apparently a very old bug dating back to 2009
###The way it is written the points cluster toward the center
###which is exactly what the comments mention it is trying to avoid
###STH 26 Sept 2026

# randr=(random.random()*(plant.r-0))+0 #random between 0 and the radius
# #randr =math.sqrt(randr) #if you don't use sqrt, you get clustering in the center
# randr =randr**0.5 #if you don't use sqrt, you get clustering in the center
randr=plant.r*(random.random()**0.5) #r*sqrt(u) spreads photons evenly over the canopy
#####

photonX = (randr*math.cos(randAngle))+plant.x
photonY = (randr*math.sin(randAngle))+plant.y
######
for overPlant in plant.overlapList:
if not photonX=="gone":
if geometry_utils.pointInsideCircle(overPlant.x, overPlant.y, overPlant.r, photonX, photonY):
randomValue=random.random()
if randomValue > overPlant.canopyTransmittance:
###these points are where the overlap is
photonX="gone"
break
if not photonX=="gone":
hitCount=hitCount+1
###The canopies above, as (x, y, radius, transmittance), looked
###up once here rather than for every photon.
covers=[]
for overPlant in plant.overlapList:
covers.append((overPlant.x, overPlant.y, overPlant.r, overPlant.canopyTransmittance))
hitCount=countPhotonsGettingThrough(plant.x, plant.y, plant.r, numbPhotons, covers)
if numbPhotons ==0:
fractionExposed=0.0
else:
Expand Down Expand Up @@ -338,7 +350,16 @@ def plantSeed(self, theSeed):

def kill(self, theObject):
theGarden=self
if theObject in self.soil:
###Find where theObject is in the soil just once: the soil is a long
###list, and this used to look through it twice (to see whether
###theObject was there, and again to remove it). Planting the dropped
###seeds below only adds to the end of the soil, so the place stays
###right.
try:
place=self.soil.index(theObject)
except ValueError:
place=None
if place is not None:
#die!
if len(theObject.seedList)>0:
for theSeed in theObject.seedList:
Expand All @@ -354,7 +375,7 @@ def kill(self, theObject):
else:
self.numbPlants=self.numbPlants-1
self.deathNote.append(theObject)
self.soil.remove(theObject)
del self.soil[place]

def calcEulerGreenhill(self, plant):
theGarden=self
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/test_plant_growth.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,19 @@ def test_a_new_seed_copies_the_species_settings_but_not_the_family(plant, garden
assert seed.motherPlant == 0
assert seed.seedList == []
assert len(plant.seedList) == 1


def test_a_new_seed_gets_its_own_copy_of_anything_that_is_not_a_plain_list(plant):
# Lists of numbers are copied with list(); anything else (a list of
# lists, a dictionary) still goes through deepcopy, all the way down.
plant.pairs = [[1, 2], [3, 4]]
plant.table = {"a": [1, 2]}

seed = plant.copyForNewSeed()

assert seed.pairs == plant.pairs
assert seed.pairs[0] is not plant.pairs[0]
assert seed.table == plant.table
assert seed.table["a"] is not plant.table["a"]
# numbers and strings are shared, as deepcopy shared them too
assert seed.nameSpecies is plant.nameSpecies