diff --git a/Vida_Data/vplantr.py b/Vida_Data/vplantr.py index 4a8a2cc..e1ffc81 100755 --- a/Vida_Data/vplantr.py +++ b/Vida_Data/vplantr.py @@ -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 @@ -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): diff --git a/Vida_Data/vworldr.py b/Vida_Data/vworldr.py index 493f499..bb38fe1 100755 --- a/Vida_Data/vworldr.py +++ b/Vida_Data/vworldr.py @@ -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...***") @@ -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: @@ -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: @@ -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 diff --git a/tests/unit/test_plant_growth.py b/tests/unit/test_plant_growth.py index de05c7f..22efe0d 100644 --- a/tests/unit/test_plant_growth.py +++ b/tests/unit/test_plant_growth.py @@ -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