11"""
22The tolerance module provides functionality to deal with tolerances consistently across all other COMPAS packages.
3+
4+ The module provides:
5+ - :class:`Tolerance`: A class for tolerance settings that can be instantiated independently.
6+ - :obj:`TOL`: The global tolerance instance used throughout COMPAS (in-process).
7+
8+ To modify global tolerance settings, use the explicit methods on `TOL`:
9+ - ``TOL.update(...)`` - Update specific tolerance values
10+ - ``TOL.reset()`` - Reset to default values
11+ - ``TOL.temporary(...)`` - Context manager for temporary changes
12+
13+ Example
14+ -------
15+ >>> from compas.tolerance import TOL, Tolerance
16+ >>> # Create an independent tolerance instance
17+ >>> my_tol = Tolerance(absolute=0.01)
18+ >>> my_tol.absolute
19+ 0.01
20+ >>> # Global TOL is unchanged
21+ >>> TOL.absolute
22+ 1e-09
23+ >>> # To modify global state, use update()
24+ >>> TOL.update(absolute=0.001)
25+ >>> TOL.absolute
26+ 0.001
27+ >>> TOL.reset()
28+
329"""
430
531from __future__ import absolute_import
632from __future__ import division
733from __future__ import print_function
834
35+ from contextlib import contextmanager
936from decimal import Decimal
37+ from warnings import warn
1038
1139import compas
1240from compas .data import Data
@@ -21,6 +49,21 @@ class Tolerance(Data):
2149 ----------
2250 unit : {"M", "MM"}, optional
2351 The unit of the tolerance settings.
52+ absolute : float, optional
53+ The absolute tolerance. Default is :attr:`ABSOLUTE`.
54+ relative : float, optional
55+ The relative tolerance. Default is :attr:`RELATIVE`.
56+ angular : float, optional
57+ The angular tolerance. Default is :attr:`ANGULAR`.
58+ approximation : float, optional
59+ The tolerance used in approximation processes. Default is :attr:`APPROXIMATION`.
60+ precision : int, optional
61+ The precision used when converting numbers to strings. Default is :attr:`PRECISION`.
62+ lineardeflection : float, optional
63+ The maximum distance between a curve/surface and its polygonal approximation.
64+ Default is :attr:`LINEARDEFLECTION`.
65+ angulardeflection : float, optional
66+ The maximum curvature deviation. Default is :attr:`ANGULARDEFLECTION`.
2467 name : str, optional
2568 The name of the tolerance settings.
2669
@@ -53,25 +96,35 @@ class Tolerance(Data):
5396 This value is called the "true value".
5497 By convention, the second value is considered the "true value" by the comparison functions of this class.
5598
56- The :class:`compas.tolerance.Tolerance` class is implemented using a "singleton" pattern and can therefore have only 1 (one) instance per context.
57- Usage of :attr:`compas.tolerance.TOL` outside of :mod:`compas` internals is therefore deprecated.
99+ Each call to ``Tolerance(...)`` creates an independent instance. To modify the global
100+ tolerance settings used throughout COMPAS, use the explicit methods on :obj:`TOL`:
101+
102+ - ``TOL.update(...)`` - Update specific tolerance values
103+ - ``TOL.reset()`` - Reset all values to defaults
104+ - ``TOL.temporary(...)`` - Context manager for temporary changes
58105
59106 Examples
60107 --------
61- >>> tol = Tolerance()
62- >>> tol.unit
63- 'M'
108+ Create an independent tolerance instance:
109+
110+ >>> tol = Tolerance(absolute=0.01)
64111 >>> tol.absolute
112+ 0.01
113+
114+ The global TOL is separate:
115+
116+ >>> from compas.tolerance import TOL
117+ >>> TOL.absolute # unchanged
65118 1e-09
66- >>> tol.relative
67- 1e-06
68- >>> tol.angular
69- 1e-06
70119
71- """
120+ Modify global state explicitly:
72121
73- _instance = None
74- _is_inited = False
122+ >>> TOL.update(absolute=0.001)
123+ >>> TOL.absolute
124+ 0.001
125+ >>> TOL.reset()
126+
127+ """
75128
76129 SUPPORTED_UNITS = ["M" , "MM" ]
77130 """{"M", "MM"}: Default tolerances are defined in relation to length units.
@@ -120,12 +173,6 @@ class Tolerance(Data):
120173
121174 """
122175
123- def __new__ (cls , * args , ** kwargs ):
124- if not cls ._instance :
125- cls ._instance = object .__new__ (cls , * args , ** kwargs )
126- cls ._is_inited = False
127- return cls ._instance
128-
129176 @property
130177 def __data__ (self ):
131178 return {
@@ -160,22 +207,19 @@ def __init__(
160207 angular = None ,
161208 approximation = None ,
162209 precision = None ,
163- lineardflection = None ,
164- angulardflection = None ,
210+ lineardeflection = None ,
211+ angulardeflection = None ,
165212 name = None ,
166213 ):
167214 super (Tolerance , self ).__init__ (name = name )
168- if not self ._is_inited :
169- self ._unit = None
170- self ._absolute = None
171- self ._relative = None
172- self ._angular = None
173- self ._approximation = None
174- self ._precision = None
175- self ._lineardeflection = None
176- self ._angulardeflection = None
177-
178- self ._is_inited = True
215+ self ._unit = None
216+ self ._absolute = None
217+ self ._relative = None
218+ self ._angular = None
219+ self ._approximation = None
220+ self ._precision = None
221+ self ._lineardeflection = None
222+ self ._angulardeflection = None
179223
180224 if unit is not None :
181225 self .unit = unit
@@ -189,13 +233,10 @@ def __init__(
189233 self .approximation = approximation
190234 if precision is not None :
191235 self .precision = precision
192- if lineardflection is not None :
193- self .lineardeflection = lineardflection
194- if angulardflection is not None :
195- self .angulardeflection = angulardflection
196-
197- # this can be autogenerated if we use slots
198- # __repr__: return f"{__class__.__name__}({', '.join(f'{k}={v!r}' for k, v in self.__dict__.items())})}"
236+ if lineardeflection is not None :
237+ self .lineardeflection = lineardeflection
238+ if angulardeflection is not None :
239+ self .angulardeflection = angulardeflection
199240
200241 def __repr__ (self ):
201242 return "Tolerance(unit='{}', absolute={}, relative={}, angular={}, approximation={}, precision={}, lineardeflection={}, angulardeflection={})" .format (
@@ -220,7 +261,7 @@ def reset(self):
220261 self ._angulardeflection = None
221262
222263 def update_from_dict (self , tolerance ):
223- """Update the tolerance singleton from the key-value pairs found in a dict.
264+ """Update the tolerance from the key-value pairs found in a dict.
224265
225266 Parameters
226267 ----------
@@ -236,16 +277,183 @@ def update_from_dict(self, tolerance):
236277 if hasattr (self , name ):
237278 setattr (self , name , tolerance [name ])
238279
280+ def update (
281+ self ,
282+ unit = None ,
283+ absolute = None ,
284+ relative = None ,
285+ angular = None ,
286+ approximation = None ,
287+ precision = None ,
288+ lineardeflection = None ,
289+ angulardeflection = None ,
290+ ):
291+ """Update tolerance settings.
292+
293+ Only the provided parameters will be updated; others remain unchanged.
294+ Use this method to explicitly modify tolerance settings.
295+
296+ Parameters
297+ ----------
298+ unit : {"M", "MM"}, optional
299+ The unit of the tolerance settings.
300+ absolute : float, optional
301+ The absolute tolerance.
302+ relative : float, optional
303+ The relative tolerance.
304+ angular : float, optional
305+ The angular tolerance.
306+ approximation : float, optional
307+ The tolerance used in approximation processes.
308+ precision : int, optional
309+ The precision used when converting numbers to strings.
310+ lineardeflection : float, optional
311+ The maximum distance between a curve/surface and its polygonal approximation.
312+ angulardeflection : float, optional
313+ The maximum curvature deviation.
314+
315+ Returns
316+ -------
317+ None
318+
319+ Examples
320+ --------
321+ >>> from compas.tolerance import TOL
322+ >>> TOL.update(absolute=0.001, precision=6)
323+ >>> TOL.absolute
324+ 0.001
325+ >>> TOL.precision
326+ 6
327+ >>> TOL.reset()
328+
329+ """
330+ if unit is not None :
331+ self .unit = unit
332+ if absolute is not None :
333+ self .absolute = absolute
334+ if relative is not None :
335+ self .relative = relative
336+ if angular is not None :
337+ self .angular = angular
338+ if approximation is not None :
339+ self .approximation = approximation
340+ if precision is not None :
341+ self .precision = precision
342+ if lineardeflection is not None :
343+ self .lineardeflection = lineardeflection
344+ if angulardeflection is not None :
345+ self .angulardeflection = angulardeflection
346+
347+ @contextmanager
348+ def temporary (
349+ self ,
350+ unit = None ,
351+ absolute = None ,
352+ relative = None ,
353+ angular = None ,
354+ approximation = None ,
355+ precision = None ,
356+ lineardeflection = None ,
357+ angulardeflection = None ,
358+ ):
359+ """Context manager for temporarily changing tolerance settings.
360+
361+ The original settings are automatically restored when the context exits,
362+ even if an exception occurs.
363+
364+ Parameters
365+ ----------
366+ unit : {"M", "MM"}, optional
367+ The unit of the tolerance settings.
368+ absolute : float, optional
369+ The absolute tolerance.
370+ relative : float, optional
371+ The relative tolerance.
372+ angular : float, optional
373+ The angular tolerance.
374+ approximation : float, optional
375+ The tolerance used in approximation processes.
376+ precision : int, optional
377+ The precision used when converting numbers to strings.
378+ lineardeflection : float, optional
379+ The maximum distance between a curve/surface and its polygonal approximation.
380+ angulardeflection : float, optional
381+ The maximum curvature deviation.
382+
383+ Yields
384+ ------
385+ :class:`Tolerance`
386+ The tolerance instance with temporary settings applied.
387+
388+ Examples
389+ --------
390+ >>> from compas.tolerance import TOL
391+ >>> TOL.absolute
392+ 1e-09
393+ >>> with TOL.temporary(absolute=0.01):
394+ ... TOL.absolute
395+ 0.01
396+ >>> TOL.absolute
397+ 1e-09
398+
399+ """
400+ # Save current state
401+ saved = {
402+ "unit" : self .unit ,
403+ "absolute" : self .absolute ,
404+ "relative" : self .relative ,
405+ "angular" : self .angular ,
406+ "approximation" : self .approximation ,
407+ "precision" : self .precision ,
408+ "lineardeflection" : self .lineardeflection ,
409+ "angulardeflection" : self .angulardeflection ,
410+ }
411+ try :
412+ # Apply temporary changes
413+ self .update (
414+ unit = unit ,
415+ absolute = absolute ,
416+ relative = relative ,
417+ angular = angular ,
418+ approximation = approximation ,
419+ precision = precision ,
420+ lineardeflection = lineardeflection ,
421+ angulardeflection = angulardeflection ,
422+ )
423+ yield self
424+ finally :
425+ # Restore original state
426+ self ._unit = saved ["unit" ]
427+ self ._absolute = saved ["absolute" ]
428+ self ._relative = saved ["relative" ]
429+ self ._angular = saved ["angular" ]
430+ self ._approximation = saved ["approximation" ]
431+ self ._precision = saved ["precision" ]
432+ self ._lineardeflection = saved ["lineardeflection" ]
433+ self ._angulardeflection = saved ["angulardeflection" ]
434+
239435 @property
240- def units (self ):
436+ def unit (self ):
437+ if not self ._unit :
438+ return "M"
241439 return self ._unit
242440
243- @units .setter
244- def units (self , value ):
441+ @unit .setter
442+ def unit (self , value ):
245443 if value not in ["M" , "MM" ]:
246444 raise ValueError ("Invalid unit: {}" .format (value ))
247445 self ._unit = value
248446
447+ @property
448+ def units (self ):
449+ warn ("The 'units' property is deprecated. Use 'unit' instead." , DeprecationWarning )
450+ return self .unit
451+
452+ @units .setter
453+ def units (self , value ):
454+ warn ("The 'units' property is deprecated. Use 'unit' instead." , DeprecationWarning )
455+ self .unit = value
456+
249457 @property
250458 def absolute (self ):
251459 if not self ._absolute :
0 commit comments