Related
Now, in addition from my two previous posts ODE implements I try to refactro my code and fix some problems. And I decided, that logically create such classes: Solver,Problem.
So code for ODE_Solver and FE classes finally code and working.
# ODS.py
import numpy as np
class ODE_Solver(object):
def __init__(self, f):
if not callable(f):
raise TypeError('f is not %s, function' % type(f))
self.f = lambda u, x: np.asarray(f(u, x), float)
self.err_sch = None
def solver_st(self):
raise NotImplementedError
def err_st(self):
raise NotImplementedError
def set_initial_condition(self, u0):
if isinstance(u0, (float, int)):
self.neq = 1
u0 = float(u0)
else:
u0 = np.asarray(u0)
self.neq = u0.size
self.u0 = u0
try:
f0 = self.f(self.u0, 0)
except IndexError:
raise IndexError(
'index out of bounds f(u,x). correct index %s' % (str(range(self.neq))))
if f0.size != self.neq:
raise ValueError('f(u,x) returend %d elems, vector u has %d elems' % (f0.size, self.neq))
def solve(self, coord_points, terminate=None):
if terminate is None:
terminate = lambda u, x, step_no: False
if isinstance(coord_points, (float, int)):
raise TypeError('solve: x points not numpy array or numbers.')
self.x = np.asarray(coord_points)
if self.x.size <= 1:
raise ValueError('ODESolver.solve points of coords less than two')
n = self.x.size
if self.neq == 1: # ОДУ
self.u = np.zeros(n)
self.err_sch = np.zeros(n)
else:
self.u = np.zeros((n, self.neq))
self.err_sch = np.zeros((n, self.neq))
self.u[0] = self.u0
self.err_sch[0] = 0
for k in range(n - 1):
self.k = k
self.u[k + 1] = self.solver_st()
self.err_sch[k + 1] = self.err_st()
if terminate(self.u, self.x, self.k + 1):
break
return self.u[:k + 2], self.x[:k + 2]
# ES.py
from ODS import ODE_Solver
import numpy as np
class FE(ODE_Solver):
def solver_st(self):
u, f, k, x = self.u, self.f, self.k, self.x
dx = x[k + 1] - x[k]
u_new = u[k] + dx * f(u[k], x[k])
return u_new
def err_st(self):
u, f, k, x, err_sch = self.u, self.f, self.k, self.x, self.err_sch
dx = x[k + 1] - x[k]
err_sch = np.max(dx)**2
return err_sch
I try to implement class Problem (return ODE and get initial conditions)
import numpy as np
class Problem(object):
def __init__(self, u0, End):
self.u0 = np.asarray(u0)
self.End = End # end point of coords
def __call__(self, u, x):
return (u[1], u[2], u[3], u[4],
- 15 * u[4] - 90 * u[3] - 270 * u[2] - 405 * u[1] - 243 * u[0])
And code class Solver for call numerical scheme, plotting the final result, plot and evaluate error:
import numpy as np
import matplotlib as plt
import ES
import ODS
from ADS import ABM4
from ES import FE
from MLNS import MLN
from RKS import RK4
class Solver(object):
def __init__(self, problem, dx,
method=ES.FE): # choose FE scheme for tetsting
"""
"""
self.problem, self.dx = problem, dx
self.solver = method
#staticmethod
def choose_sch(type):
if type == 1:
method = FE
return method
elif type == 2:
method = RK4
return method
elif type == 3:
method = ABM4
return method
elif type == 4:
method = MLN
return method
else:
raise ValueError('not choose numerical scheme!')
def dsolve(self):
solver = self.method(self.problem)
solver.set_initial_condition(self.problem.u0)
n = int(round(self.problem.End / self.dx))
x_points = np.linspace(0, self.problem.End, n + 1)
self.u, self.x = solver.solve(x_points)
if solver.k + 1 == n:
self.plot()
raise ValueError('not converge this scheme,' % self.problem.End)
def plot(self):
plt.plot(self.x, self.u)
plt.show()
Now, when I call this Solver and Problem
import numpy as np
from ODE_Problem import Problem
from SLV_Prob import Solver
def test():
problem = Problem(u0=[0, 3, -9, -8, 0], End=5)
solver = Solver(problem, dx=0.1)
solver.dsolve()
solver.plot()
if __name__ == '__main__':
test()
I get the error:
Traceback (most recent call last):
File "C:\Fin_Proj_ODE\test2.py", line 14, in <module>
test()
File "C:\Fin_Proj_ODE\test2.py", line 9, in test
solver.dsolve()
File "C:\Fin_Proj_ODE\SLV_Prob.py", line 37, in dsolve
solver = self.method(self.problem)
AttributeError: 'Solver' object has no attribute 'method'
And I dont' understand and suppose what reason of this bug...
So, I have 2 questions for implement this Solver:
How to fix this bug?
How to correct rewrite def choose_sch(type):, that I could to call solver and send args type ( and depending on it, a specific numerical scheme will already be started)?
Question One:
Well, as the error states, your Solver class doesn't have an attribute called "method". Your attribute is actually "solver", so instead of calling
self.method(self.problem)
Try
self.solver(self.problem)
Question Two:
If I'm understanding you correctly, you want to know how you can call the choose_sch method from within the solver constructor and take in a type instead of a method directly. For that, simply do this:
class Solver(object):
def __init__(self, problem, dx, solver_type=1): # choose FE scheme for tetsting
"""
"""
self.problem, self.dx = problem, dx
self.solver = self._choose_sch(solver_type)
#staticmethod
def _choose_sch(solver_type):
methods = {1: FE, 2: RK4, 3: ABM4, 4: MLN}
if solver_type in methods:
return methods[solver_type]
else:
raise ValueError('not choose numerical scheme!')
The dictionary here is much better than the if statement for these kinds of tasks.
You can also alternatively not make _choose_ach a staticmethod if you don't need to call it from a static context and just make it set the solver directly.
I have this code to implement queue and stack data structures. The QueueT by itself works as desired.
When I call st.push(1), it agreeable calls push(self, v) and this in turn calls the enqueue method.
The issue I am facing is that after the statement self._q1.enqueue(v) is executed, self._q1 does not retain the value v.
Code for class QueueT:
class QueueT:
def __init__(self):
self._CAPACITY = 6
self._data = [None] * self._CAPACITY
self._length = 0
self._first = 0
def enqueue(self, val):
if self.size() == self._CAPACITY:
self._resize(self._CAPACITY * 2)
self.enqueue(val)
else:
new_place = (self._first + self.size()) % self._CAPACITY
self._data[new_place] = val
self._length += 1
def size(self):
return self._length
def _resize(self, new_capacity):
old = self._data
old_capacity = len(old)
self._data = [None] * new_capacity
k = self._first
self._first = 0
for j in range(self._length):
self._data[j] = old[k]
k = (1 + k) % old_capacity
self._CAPACITY = new_capacity
Now code from StackFromQ:
class StackFromQ:
def __init__(self):
self._q1 = QueueT()
self._top = -1
def push(self, v):
self._q1.enqueue(v)
self._top += 1
Caller function:
def stack_of_q():
st = StackFromQ()
st.push(1)
st.push(2)
Finally invocation:
stack_of_q()
Hi I am new to encryption algorithms and am a little confused by the terminology and what to choose for which situation. For now I don't need the most secure algorithm but it should have the following properties:
A shared secret key, that encrypts/decrypts the message (two way).
The Cipher should be the same if same text for encryption has been used.
I used the Fernet algorithm in Python, however the Ciphertext is always different. and therefore not suitable for my needs. Is there an algorithm similar to Fernet but without the Salt?
Assuming you're using the cryptography module (which you should): Instead of using generate_key(), build the key and pass it to the Fernet constructor.
Check this link for the precise example: https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet
Additionally, check https://www.crypto101.io/ if you haven't yet.
Actually there's no ready algorithm do what you're asking for "at lease that what I know".
But in the past I've wrote a GUI chat room with a real server and real clients in python.
And I've wrote a basic encryption/decryption algorithm to pass the data encrypted "like messages".
you can use it if you want cuz I've never used that server I made it just for fun ;).
The algorithm:
from cryptography.fernet import Fernet
CODES_KEY = b'LYsrKDiTRNJDm3g_h25GW0uyUzPTk8l8G02hcPM_d_U='
def encryption(msg):
'''
Encryption Func
'''
class RSA:
'''
RSA Encryption Func
'''
def __init__(self):
self.e = self.d = self.p = self.q = self.phi = 0
def __egcd(self, a, b):
if a == 0:
return (b, 0, 1)
g, y, x = self.__egcd(b % a, a)
return (g, x - (b // a) * y, y)
def __modinv(self, a, m):
g, x, _ = self.__egcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
return x % m
def encrypt(self, m, key_pair=None):
'''
Encrypting Func
'''
if key_pair is None:
key_pair[0] = self.e
key_pair[1] = self.n
return pow(m, key_pair[0], key_pair[1])
def generate_keys(self, p, q, e=3):
'''
Generating Keys Func
'''
self.p = p
self.q = q
self.n = self.p * self.q
self.phi = (self.p - 1) * (self.q - 1)
self.e = e
self.d = self.__modinv(self.e, self.phi)
if self.phi % self.e == 0:
raise Exception('invalid values for p and q')
def get_public_key(self):
'''
Getting PublicKey Func
'''
return self.e, self.n
rsa = RSA()
# Two 1024-bit primes
rsa.generate_keys(17055899557196527525682810191339089909014331959812898993437334555169285087976951946809555356817674844913188193949144165887100694620944167618997411049745043243260854998720061941490491091205087788373487296637817044103762239946752241631032791287021875863785226376406279424552454153388492970310795447866569138481,
171994050316145327367864378293770397343246561147593187377005295591120640129800725892235968688434055779668692095961697434700708550594137135605048681344218643671046905252163983827396726536078773766353616572531688390937410451433665914394068509329532352022301339189851111636176939179510955519440490431177444857017)
int_message = []
for i in msg:
int_message.append(ord(i))
codes = []
for i in int_message:
codes.append(len(str(i)))
message = int(''.join([str(x) for x in int_message]))
# Encrypting
encrypted_message = rsa.encrypt(message, key_pair=rsa.get_public_key())
f = Fernet(CODES_KEY)
encrypted_codes = f.encrypt(str(codes).encode())
return encrypted_message, encrypted_codes
def decryption(msg, codes):
'''
Decryption Func
'''
class RSA:
'''
RSA Decryption Func
'''
def __init__(self):
self.e = self.d = self.p = self.q = self.phi = 0
def __egcd(self, a, b):
if a == 0:
return (b, a, 1)
g, y, x = self.__egcd(b % a, a)
return (g, x - (b // a) * y, y)
def __modinv(self, a, m):
g, x, _ = self.__egcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
return x % m
def decrypt(self, c, key_pair=None):
'''
Decrypting Func
'''
if key_pair is None:
key_pair[0] = self.d
key_pair[1] = self.n
return pow(c, key_pair[0], key_pair[1])
def generate_keys(self, p, q, e=3):
'''
Generating Keys Func
'''
self.p = p
self.q = q
self.n = self.p * self.q
self.phi = (self.p - 1) * (self.q - 1)
self.e = e
self.d = self.__modinv(self.e, self.phi)
if self.phi % self.e == 0:
raise Exception('invalid values for p and q')
def get_private_key(self):
'''
Getting Private Key Func
'''
return self.d, self.n
rsa = RSA()
# Two 1024-bit primes
rsa.generate_keys(17055899557196527525682810191339089909014331959812898993437334555169285087976951946809555356817674844913188193949144165887100694620944167618997411049745043243260854998720061941490491091205087788373487296637817044103762239946752241631032791287021875863785226376406279424552454153388492970310795447866569138481,
171994050316145327367864378293770397343246561147593187377005295591120640129800725892235968688434055779668692095961697434700708550594137135605048681344218643671046905252163983827396726536078773766353616572531688390937410451433665914394068509329532352022301339189851111636176939179510955519440490431177444857017)
# Decrypting
f = Fernet(CODES_KEY)
# set_trace()
decrypted_message = str(rsa.decrypt(int(msg), key_pair=rsa.get_private_key()))
decrypted_codes = f.decrypt(codes).decode()
decrypted_message = list(decrypted_message)
original_codes = []
for i in decrypted_codes:
if i not in ['[', ']', ',', ' ']:
original_codes.append(int(i))
original_message = ""
for _, value in enumerate(original_codes):
handler = ""
for i in decrypted_message[:value]:
handler += i
original_message += chr(int(handler))
decrypted_message = decrypted_message[value:]
return original_message
def main():
message = "Hi, This is a secret message"
encrypted_message, encrypted_codes = encryption(message)
decrypted_message = decryption(encrypted_message, encrypted_codes)
print(decrypted_message)
if __name__ == '__main__':
main()
Sorry if it's a mess but I'm just 17 years old bro :) !
Maybe you can optimize it more I've wrote this when i was new to python.
you can always generate new "CODES_KEY" by running this in python:
from cryptography.fernet import Fernet
f = Fernet
print(f.generate_key())
EDIT: I wrote this as an RSA but then changed the code, this is why it's named RSA but it's not :)
Below is a simple Pyomo script using the decorator syntax - I would like to understand how to use this syntax within a class - in this case inside Model.
None-class version
from pyomo.environ import *
import random
random.seed(1000)
model = AbstractModel()
model.N = Param(within=PositiveIntegers)
model.P = Param(within=RangeSet(1, model.N))
model.M = Param(within=PositiveIntegers)
model.Locations = RangeSet(1, model.N)
model.Customers = RangeSet(1, model.M)
model.d = Param(
model.Locations,
model.Customers,
initialize=lambda n, m, model: random.uniform(1.0, 2.0),
within=Reals,
)
model.x = Var(model.Locations, model.Customers, bounds=(0.0, 1.0))
model.y = Var(model.Locations, within=Binary)
#model.Objective()
def obj(model):
return sum(
model.d[n, m] * model.x[n, m] for n in model.Locations for m in model.Customers
)
#model.Constraint(model.Customers)
def single_x(model, m):
return (sum(model.x[n, m] for n in model.Locations), 1.0)
#model.Constraint(model.Locations, model.Customers)
def bound_y(model, n, m):
return model.x[n, m] - model.y[n] <= 0.0
#model.Constraint()
def num_facilities(model):
return sum(model.y[n] for n in model.Locations) == model.P
Decorator version within a class that doesn't work:
from pyomo.environ import *
import random
random.seed(1000)
class Model:
def __init__(self):
self.model = AbstractModel()
self.model.N = Param(within=PositiveIntegers)
self.model.P = Param(within=RangeSet(1, self.model.N))
self.model.M = Param(within=PositiveIntegers)
self.model.Locations = RangeSet(1, self.model.N)
self.model.Customers = RangeSet(1, self.model.M)
self.model.d = Param(
self.model.Locations,
self.model.Customers,
initialize=lambda n, m, model: random.uniform(1.0, 2.0),
within=Reals,
)
self.model.x = Var(
self.model.Locations, self.model.Customers, bounds=(0.0, 1.0)
)
self.model.y = Var(self.model.Locations, within=Binary)
#model.Objective()
def obj(model):
return sum(
model.d[n, m] * model.x[n, m]
for n in model.Locations
for m in model.Customers
)
#model.Constraint(model.Customers)
def single_x(model, m):
return (sum(model.x[n, m] for n in model.Locations), 1.0)
#model.Constraint(model.Locations, model.Customers)
def bound_y(model, n, m):
return model.x[n, m] - model.y[n] <= 0.0
#model.Constraint()
def num_facilities(model):
return sum(model.y[n] for n in model.Locations) == model.P
I'm not able to help you on this, I just have a few qustions:
do you know if the use of #model.Objective() (same for Constraint etc) is documented somewhere? I didn't know it existed, and it's awesome
why do you want your "function rules" to be methods of the class? couldn't you defined them as functions within the __init__ method?
I guess what I'm missing is the benefit of using a class in the first place.
If you are just trying to wrap the model construction somehow, then a better approach is using a function:
def create_model():
model = AbstractModel()
...
#model.Constraint()
def some_rule_function(model):
...
...
return model
EDIT: if you really want to wrap everything into a class:
class Model:
def __init__(self, model):
self.model = model
# alternative constructor:
# def __init__(self):
# self.model = create_model()
def construct(self, data):
# get concrete model
self.model = self.model.create_instance(data)
def run(self, solver, **kwargs):
with pe.SolverFactory(solver) as solver:
solver.solve(self.model, **kwargs)
def construct_and_run(self, data, solver, **kwargs):
self.construct(data)
self.data(solver, **kwargs)
# other behavior you want to add to the class
example usage:
model = Model(create_model())
Trying to answer your direct question, here's something that seems to work for me. My interpretation is that since your model is called self.model, the decorators should also match that.
Note that I used s as the first argument in the constraint method definitions just to see if it worked, but it could also be model or whatever you want to call it.
class Model:
def __init__(self):
self.model = pyo.AbstractModel()
self.model.N = pyo.Param(initialize=5, within=pyo.PositiveIntegers)
self.model.P = pyo.Param(initialize=3, within=pyo.RangeSet(1, self.model.N))
self.model.M = pyo.Param(initialize=3, within=pyo.PositiveIntegers)
self.model.Locations = pyo.RangeSet(1, self.model.N)
self.model.Customers = pyo.RangeSet(1, self.model.M)
self.model.d = pyo.Param(
self.model.Locations,
self.model.Customers,
initialize=lambda n, m, model: random.uniform(1.0, 2.0),
within=pyo.Reals,
)
self.model.x = pyo.Var(
self.model.Locations, self.model.Customers, bounds=(0.0, 1.0)
)
self.model.y = pyo.Var(self.model.Locations, within=pyo.Binary)
#self.model.Objective()
def obj(s):
return sum(
s.d[n, m] * s.x[n, m]
for n in s.Locations
for m in s.Customers
)
#self.model.Constraint(self.model.Customers)
def single_x(s, m):
return (sum(s.x[n, m] for n in s.Locations), 1.0)
#self.model.Constraint(self.model.Locations, self.model.Customers)
def bound_y(s, n, m):
return s.x[n, m] - s.y[n] <= 0.0
#self.model.Constraint()
def num_facilities(s):
return sum(s.y[n] for n in s.Locations) == s.P
You would then be able to instantiate the model with model = Model(), though annoyingly (at least to me), all your Pyomo model components will be within the attribute model.model (e.g., model.model.P).
What I've done before to make the naming cleaner is to inherit from AbstractModel (though the other answer suggests that may not be good practice):
from pyomo.core.base.PyomoModel import AbstractModel
class Model(AbstractModel):
def __init__(self):
AbstractModel.__init__(self)
self.N = pyo.Param(initialize=5, within=pyo.PositiveIntegers)
self.P = pyo.Param(initialize=3, within=pyo.RangeSet(1, self.N))
self.M = pyo.Param(initialize=3, within=pyo.PositiveIntegers)
self.Locations = pyo.RangeSet(1, self.N)
self.Customers = pyo.RangeSet(1, self.M)
self.d = pyo.Param(
self.Locations,
self.Customers,
initialize=lambda n, m, model: random.uniform(1.0, 2.0),
within=pyo.Reals,
)
self.x = pyo.Var(
self.Locations, self.Customers, bounds=(0.0, 1.0)
)
self.y = pyo.Var(self.Locations, within=pyo.Binary)
#self.Objective()
def obj(s):
return sum(
s.d[n, m] * s.x[n, m]
for n in s.Locations
for m in s.Customers
)
#self.Constraint(self.Customers)
def single_x(s, m):
return (sum(s.x[n, m] for n in s.Locations), 1.0)
#self.Constraint(self.Locations, self.Customers)
def bound_y(s, n, m):
return s.x[n, m] - s.y[n] <= 0.0
#self.Constraint()
def num_facilities(s):
return sum(s.y[n] for n in s.Locations) == s.P
In this case, you still instantiate as model = Model() but your Pyomo model components can be accessed as model.P.
I'm trying to make a bunch of geometric objects which have their intrinsic geometric properties (center point, radius, lengths, etc.), as well as properties to help plot them (like x, y, z coordinates for a triangular mesh, arc resolution, etc.).
Since calculating the x, y, z coordinates is an expensive task for some of the shapes (like a triangular prism with edge rounding), I don't want to do it every time a property is changed, but only when the coordinates are requested. Even then though, it shouldn't be necessary to recalculate them if the shape's definition hasn't changed.
So my solution has been to create a "hash" which is is simply a tuple of all parameters which define the shape's "state." If the hash is unchanged, then the previously calculated coordinates can be re-used, otherwise, the coordinates must be recalculated. So I'm using the hash as a way to store the signature or fingerprint of the shape's definition.
I think what I have works, but I wonder if there are more robust ways to handle this that take advantage of __hash__ or id's or something. That feels like overkill to me, but I'm open to suggestions.
Here's my implementation for a sphere. I'm using Mayavi for plotting at the end, which you can skip/ignore if you don't have Mayavi.
#StdLib Imports
import os
#Numpy Imports
import numpy as np
from numpy import sin, cos, pi
class Sphere(object):
"""
Class for a sphere
"""
def __init__(self, c=None, r=None, n=None):
super(Sphere, self).__init__()
#Initial defaults
self._coordinates = None
self._c = np.array([0.0, 0.0, 0.0])
self._r = 1.0
self._n = 20
self._hash = []
#Assign Inputs
if c is not None:
self._c = c
if r is not None:
self._r = r
if n is not None:
self._n = n
#property
def c(self):
return self._c
#c.setter
def c(self, val):
self._c = val
#property
def r(self):
return self._r
#r.setter
def r(self, val):
self._r = val
#property
def n(self):
return self._n
#n.setter
def n(self, val):
self._n = val
#property
def coordinates(self):
self._lazy_update()
return self._coordinates
def _lazy_update(self):
new_hash = self._get_hash()
old_hash = self._hash
if new_hash != old_hash:
self._update_coordinates()
def _get_hash(self):
return tuple(map(tuple, [self._c, [self._r, self._n]]))
def _update_coordinates(self):
c, r, n = self._c, self._r, self._n
dphi, dtheta = pi / n, pi / n
[phi, theta] = np.mgrid[0:pi + dphi*1.0:dphi,
0:2*pi + dtheta*1.0:dtheta]
x = c[0] + r * cos(phi) * sin(theta)
y = c[1] + r * sin(phi) * sin(theta)
z = c[2] + r * cos(theta)
self._coordinates = x, y, z
self._hash = self._get_hash()
if __name__ == '__main__':
from mayavi import mlab
ns = [4, 6, 8, 10, 20, 50]
sphere = Sphere()
for i, n in enumerate(ns):
sphere.c = [i*2.2, 0.0, 0.0]
sphere.n = n
mlab.mesh(*sphere.coordinates, representation='wireframe')
mlab.show()
As suggested, here's a version that uses a dictionary to store the hash as a key:
#StdLib Imports
import os
#Numpy Imports
import numpy as np
from numpy import sin, cos, pi
class Sphere(object):
"""
Class for a sphere
"""
def __init__(self, c=None, r=None, n=None):
super(Sphere, self).__init__()
#Initial defaults
self._coordinates = {}
self._c = np.array([0.0, 0.0, 0.0])
self._r = 1.0
self._n = 20
#Assign Inputs
if c is not None:
self._c = c
if r is not None:
self._r = r
if n is not None:
self._n = n
#property
def c(self):
return self._c
#c.setter
def c(self, val):
self._c = val
#property
def r(self):
return self._r
#r.setter
def r(self, val):
self._r = val
#property
def n(self):
return self._n
#n.setter
def n(self, val):
self._n = val
#property
def _hash(self):
return tuple(map(tuple, [self._c, [self._r, self._n]]))
#property
def coordinates(self):
if self._hash not in self._coordinates:
self._update_coordinates()
return self._coordinates[self._hash]
def _update_coordinates(self):
c, r, n = self._c, self._r, self._n
dphi, dtheta = pi / n, pi / n
[phi, theta] = np.mgrid[0:pi + dphi*1.0:dphi,
0:2 * pi + dtheta*1.0:dtheta]
x = c[0] + r * cos(phi) * sin(theta)
y = c[1] + r * sin(phi) * sin(theta)
z = c[2] + r * cos(theta)
self._coordinates[self._hash] = x, y, z
if __name__ == '__main__':
from mayavi import mlab
ns = [4, 6, 8, 10, 20, 50]
sphere = Sphere()
for i, n in enumerate(ns):
sphere.c = [i*2.2, 0.0, 0.0]
sphere.n = n
mlab.mesh(*sphere.coordinates, representation='wireframe')
mlab.show()
Why not simply keep a flag:
class Sphere(object):
def __init__(self, ...):
...
self._update_coordinates()
...
#c.setter
def c(self, val):
self._changed = True
self._c = val
...
def _lazy_update(self):
if self._changed:
self._update_coordinates()
def _update_coordinates(self):
...
self._changed = False