wolfhece 2.1.124__py3-none-any.whl → 2.1.126__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wolfhece/apps/version.py CHANGED
@@ -5,7 +5,7 @@ class WolfVersion():
5
5
 
6
6
  self.major = 2
7
7
  self.minor = 1
8
- self.patch = 124
8
+ self.patch = 126
9
9
 
10
10
  def __str__(self):
11
11
 
@@ -17,6 +17,7 @@ from matplotlib.figure import Figure
17
17
  from shapely.geometry import LineString, Point, CAP_STYLE, Polygon
18
18
  from shapely.ops import prep
19
19
  import wx
20
+ from tqdm import tqdm
20
21
 
21
22
  from . import viewer
22
23
  from ..PyWMS import getWalonmap
@@ -41,6 +42,7 @@ class Colors_Lazviewer(Enum):
41
42
  ORTHO_2006_2007 = 2006
42
43
  CODE_2013 = 0
43
44
  CODE_2023 = 2
45
+ CODE_2021_2022 = 3
44
46
  FROM_FILE = 1
45
47
 
46
48
  class Classification_LAZ():
@@ -68,6 +70,18 @@ class Classification_LAZ():
68
70
  9 : ['Eau', 'Eau',Colors.rgb_withalpha_float('aqua',1.)],
69
71
  10 : ['Ponts', 'Ponts',Colors.rgb_withalpha_float('lightyellow1',1.)]}
70
72
 
73
+ def init_2021_2022(self):
74
+
75
+ self.class_name = 'SPW 2021-2022'
76
+ self.classification={
77
+ 0 : ['0', 'Pas de classification', Colors.rgb_withalpha_float('black',.2),],
78
+ 1 : ['Hors-sol', 'building, toits et autres', Colors.rgb_withalpha_float('white',.2),],
79
+ 2 : ['Sol', 'y compris talus et digues', Colors.rgb_withalpha_float('brown',1.)],
80
+ 4 : ['Végétation', 'y compris la végétation linéaire', Colors.rgb_withalpha_float('forestgreen',1.)],
81
+ 9 : ['Eau', 'Eau',Colors.rgb_withalpha_float('aqua',1.)],
82
+ 10 : ['Ponts', 'Ponts',Colors.rgb_withalpha_float('lightyellow1',1.)],
83
+ 15 : ['Ligne hautes-tension', 'Ligne hautes-tension',Colors.rgb_withalpha_float('lightcyan1',0.25)]}
84
+
71
85
  def init_2023(self):
72
86
 
73
87
  self.class_name = 'SPW-Geofit 2023'
@@ -155,8 +169,8 @@ class xyz_laz():
155
169
  parts = last_part.split('_')
156
170
 
157
171
  # L'origine est codée dans le nom de fichier
158
- self.origx = float(parts[2])
159
- self.origy = float(parts[3])
172
+ self.origx = float(parts[-3])
173
+ self.origy = float(parts[-2])
160
174
 
161
175
  dx = 2500.
162
176
  dy = 3500.
@@ -590,7 +604,8 @@ class xyz_laz_grids():
590
604
  dirs = listdir(dir_grids)
591
605
 
592
606
  for curdir in dirs:
593
- self.grids.append(xyz_laz_grid(join(dir_grids,curdir)))
607
+ if Path(curdir).is_dir():
608
+ self.grids.append(xyz_laz_grid(join(dir_grids,curdir)))
594
609
 
595
610
  def scan_around(self, xy:Union[LineString,list[list[float], list[float]]], length_buffer=5.):
596
611
  """
@@ -707,42 +722,82 @@ class xyz_laz_grids():
707
722
 
708
723
  return figmpl
709
724
 
710
- def create_from_laz(self, dir_laz:str, shape:str, ds:float = 50, force_format = np.float64):
725
+ def create_from_laz(self, dir_laz:str, shape:str=None, ds:float = 50, force_format = np.float64):
711
726
 
712
727
  try:
713
728
  from ..PyVertexvectors import Zones
714
729
  except:
715
730
  from wolfhece.PyVertexvectors import Zones
716
- vecs = Zones(shape)
717
731
 
718
- for entry in scandir(dir_laz):
732
+ vecs = None
733
+ if shape is not None:
734
+ vecs = Zones(shape)
735
+
736
+ dir_laz = Path(dir_laz)
737
+
738
+ for entry in tqdm(dir_laz.glob('**/*.laz')):
719
739
  if entry.is_file():
720
- if entry.name.endswith('.laz'):
721
740
 
722
- file_wo_suf = entry.name.removesuffix('.laz')
723
- dirname = join(self.dir, file_wo_suf)
741
+ file_wo_suf = entry.stem
742
+ dirname = join(self.dir, file_wo_suf)
724
743
 
725
- if not exists(dirname):
726
- makedirs(dirname, exist_ok=True)
744
+ if not exists(dirname):
745
+ makedirs(dirname, exist_ok=True)
727
746
 
728
- newlaz = xyz_laz_grid(mydir=dirname)
747
+ newlaz = xyz_laz_grid(mydir=dirname)
729
748
 
749
+ if vecs is not None:
730
750
  vec = vecs.get_zone(file_wo_suf)
731
751
  bounds = vec.myvectors[0].get_bounds()
752
+ else:
753
+ lazdata = laspy.read(entry)
754
+ bounds = [[lazdata.x.min(), lazdata.y.min()],
755
+ [lazdata.x.max(), lazdata.y.max()]]
756
+
757
+ bounds = [[math.floor(bounds[0][0]/ds)*ds, math.floor(bounds[0][1]/ds)*ds],
758
+ [math.ceil(bounds[1][0]/ds)*ds, math.ceil(bounds[1][1]/ds)*ds]]
759
+
760
+ dx = bounds[1][0] -bounds[0][0]
761
+ dy = bounds[1][1] -bounds[0][1]
762
+ nb = max(int(dx/ds), int(dy/ds))
763
+
764
+ self.grids.append(newlaz._sort_grid_np(entry,
765
+ join(dirname, file_wo_suf),
766
+ bounds=[[bounds[0][0], bounds[1][0]], [bounds[0][1], bounds[1][1]]],
767
+ gridsize=[max(int(dx/ds),1), max(int(dy/ds),1)],
768
+ force_format=force_format))
769
+
770
+ def create_bounds_shape(self, dir_laz:str, out_shape:str):
771
+ """ Create shape from laz files """
772
+ try:
773
+ from ..PyVertexvectors import Zones
774
+ except:
775
+ from wolfhece.PyVertexvectors import Zones
776
+
777
+ dir_laz = Path(dir_laz)
778
+ out_shape = Path(out_shape)
779
+
780
+ vecs = Zones()
781
+
782
+ for entry in tqdm(dir_laz.glob('**/*.laz')):
783
+ if entry.is_file():
784
+ lazdata = laspy.read(entry)
732
785
 
733
- bounds = [[math.floor(bounds[0][0]/ds)*ds, math.floor(bounds[0][1]/ds)*ds],
734
- [math.ceil(bounds[1][0]/ds)*ds, math.ceil(bounds[1][1]/ds)*ds]]
786
+ bounds = [[lazdata.x.min(), lazdata.y.min()],
787
+ [lazdata.x.max(), lazdata.y.max()]]
788
+ loczone = zone(name=entry.stem)
789
+ locvec = vector(name=entry.stem)
735
790
 
736
- dx = bounds[1][0] -bounds[0][0]
737
- dy = bounds[1][1] -bounds[0][1]
738
- nb = max(int(dx/ds), int(dy/ds))
791
+ locvec.add_vertex(wolfvertex(bounds[0][0], bounds[0][1]))
792
+ locvec.add_vertex(wolfvertex(bounds[1][0], bounds[0][1]))
793
+ locvec.add_vertex(wolfvertex(bounds[1][0], bounds[1][1]))
794
+ locvec.add_vertex(wolfvertex(bounds[0][0], bounds[1][1]))
795
+ locvec.close_force()
739
796
 
740
- self.grids.append(newlaz._sort_grid_np(entry.path,
741
- join(dirname, file_wo_suf),
742
- bounds=[[bounds[0][0], bounds[1][0]], [bounds[0][1], bounds[1][1]]],
743
- gridsize=[max(int(dx/ds),1), max(int(dy/ds),1)],
744
- force_format=force_format))
797
+ loczone.add_vector(locvec, forceparent=True)
798
+ vecs.add_zone(loczone, forceparent=True)
745
799
 
800
+ vecs.saveas(Path(self.dir) / out_shape)
746
801
 
747
802
  class Wolf_LAZ_Data(Element_To_Draw):
748
803
  """ Base class for LAZ data which can be imported in Pydraw.Mapviewer.
@@ -1145,6 +1200,8 @@ class Wolf_LAZ_Data(Element_To_Draw):
1145
1200
  logging.warning(_('No classification chosen - Abort !'))
1146
1201
  elif classification == 'SPW 2013-2014':
1147
1202
  self.classification.init_2013()
1203
+ elif classification == 'SPW 2021-2022':
1204
+ self.classification.init_2021_2022()
1148
1205
  else:
1149
1206
  self.classification.init_2023()
1150
1207
 
@@ -1900,6 +1957,34 @@ def get_colors(las:laspy.LasData, which_colors:Colors_Lazviewer, imsize=2000, fn
1900
1957
  name, comment, color = value
1901
1958
  colors[myclass==int(key)] = color
1902
1959
 
1960
+ elif which_colors==Colors_Lazviewer.CODE_2021_2022.value:
1961
+ """
1962
+ - Rebus/non classés - Code 0;
1963
+ - Hors-sol (building, toits et autres) - Code 1;
1964
+ - Sol (y compris talus et digues) - Code 2;
1965
+ - Végétation haute (y compris la végétation linéaire) - Code 4;
1966
+ - Eau - Code 9;
1967
+ - Pont – Code 10;
1968
+ - Ligne hautes-tension - Code 15.
1969
+ """
1970
+ if type(las) is laspy.LasData:
1971
+ myclass = las.classification
1972
+ elif type(las) is list:
1973
+ myclass=[]
1974
+ for curlas in las:
1975
+ myclass.append(curlas.classification)
1976
+ myclass = np.concatenate(myclass)
1977
+ else:
1978
+ myclass = np.int8(las[:,3])
1979
+
1980
+ if palette_classif is None:
1981
+ palette_classif = Classification_LAZ()
1982
+ palette_classif.init_2021_2022()
1983
+
1984
+ for key, value in palette_classif.classification.items():
1985
+ name, comment, color = value
1986
+ colors[myclass==int(key)] = color
1987
+
1903
1988
  elif which_colors==Colors_Lazviewer.CODE_2023.value:
1904
1989
 
1905
1990
  if palette_classif is None:
@@ -116,7 +116,7 @@ class Calculator(wx.Frame):
116
116
  from ..PyDraw import draw_type
117
117
  ids = self._mapviewer.get_list_keys(drawing_type=draw_type.ARRAYS, checked_state=None)
118
118
  for id in ids:
119
- self.memory_txt += id
119
+ self.memory_txt = id
120
120
 
121
121
  def memory_clear_event(self, e):
122
122
  self.memory_clear()
File without changes
@@ -0,0 +1,118 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+ from jax import jit, vmap
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+
7
+ # Forcer float32
8
+ jax.config.update("jax_enable_x64", False)
9
+
10
+ # Fonction pour calculer l'aire immergée
11
+ def wet_area(h, R):
12
+ h = jnp.clip(h, 0, 2 * R)
13
+ theta = 2 * jnp.arccos(1 - h / R)
14
+ area = (R**2 / 2) * (theta - jnp.sin(theta))
15
+ return area
16
+
17
+ # Solution "analytique/numérique" classique pour h
18
+ def analytical_h(R, f):
19
+ def solve_theta(theta):
20
+ return (theta - jnp.sin(theta)) / (2 * jnp.pi) - f
21
+ theta_min, theta_max = jnp.float32(0.0), jnp.float32(2 * jnp.pi)
22
+ for _ in range(50):
23
+ theta_mid = (theta_min + theta_max) / 2
24
+ val = solve_theta(theta_mid)
25
+ theta_max = jnp.where(val > 0, theta_mid, theta_max)
26
+ theta_min = jnp.where(val <= 0, theta_mid, theta_min)
27
+ theta = (theta_min + theta_max) / 2
28
+ return R * (1 - jnp.cos(theta / 2))
29
+
30
+ # Fonction objectif avec sigmoïde adaptative aux deux bornes
31
+ def objective(h, R, f):
32
+ total_area = jnp.pi * R**2
33
+ target_area = f * total_area
34
+ diff = wet_area(h, R) - target_area
35
+ # Échelle adaptative : plus kleine pour f près de 0 ou 1
36
+ scale_factor = jnp.minimum(f, 1. - f) # Distance au bord le plus proche
37
+ scale = 0.05 * R**2 * jnp.maximum(scale_factor, 0.01) # Éviter 0
38
+ return 1 / (1 + jnp.exp(-diff / scale))
39
+
40
+ # Dichotomie douce améliorée
41
+ @jit
42
+ def soft_dichotomy(R, f, max_iter=200):
43
+ def body(state, _):
44
+ h_min, h_max = state
45
+ h_mid = (h_min + h_max) / 2
46
+ sigmoid_val = objective(h_mid, R, f)
47
+ h_min_new = h_min + (h_mid - h_min) * (1 - sigmoid_val)
48
+ h_max_new = h_max - (h_max - h_mid) * sigmoid_val
49
+ return (h_min_new, h_max_new), None
50
+
51
+ # Bornes initiales resserrées pour petits/grands f
52
+ h_min_init = jnp.float32(0.0)
53
+ h_max_init = jnp.float32(2 * R)
54
+ initial_state = (h_min_init, h_max_init)
55
+ final_state, _ = jax.lax.scan(body, initial_state, None, length=max_iter)
56
+ h_min, h_max = final_state
57
+ return (h_min + h_max) / 2
58
+
59
+ # Dérivée par rapport à f
60
+ grad_bisection = jax.grad(soft_dichotomy, argnums=1)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ # Paramètres
65
+ R = jnp.float32(1.0)
66
+ f_values = jnp.linspace(jnp.float32(0.001), jnp.float32(0.999), 500) # Plus près des bords
67
+
68
+ # Calculs
69
+ h_numerical = vmap(lambda f: soft_dichotomy(R, f))(f_values)
70
+ h_analytical = vmap(lambda f: analytical_h(R, f))(f_values)
71
+ errors = jnp.abs(h_numerical - h_analytical)
72
+
73
+ grads = vmap(lambda f: grad_bisection(R, f))(f_values)
74
+
75
+ # Graphiques
76
+ plt.figure(figsize=(12, 5))
77
+
78
+ plt.subplot(1, 3, 1)
79
+ plt.plot(f_values, h_numerical, label="Numérique (sigmoïde)", color="blue")
80
+ plt.plot(f_values, h_analytical, "--", label="Analytique", color="orange")
81
+ plt.xlabel("Fraction immergée (f)")
82
+ plt.ylabel("Hauteur (h)")
83
+ plt.title("Hauteur en fonction de f (float32)")
84
+ plt.legend()
85
+ plt.grid(True)
86
+ plt.yscale("log") # Échelle log pour voir les extrêmes
87
+
88
+ plt.subplot(1, 3, 2)
89
+ plt.plot(f_values, errors, color="red")
90
+ plt.xlabel("Fraction immergée (f)")
91
+ plt.ylabel("Erreur absolue (|h_num - h_ana|)")
92
+ plt.title("Erreur par rapport à la solution analytique")
93
+ plt.grid(True)
94
+ plt.yscale("log") # Échelle log pour les erreurs
95
+
96
+ plt.subplot(1, 3, 3)
97
+ plt.plot(f_values, grads, label="Dérivée par rapport à f")
98
+ plt.xlabel("Fraction immergée (f)")
99
+ plt.ylabel("Gradient de f par rapport à h")
100
+ plt.legend()
101
+ plt.grid(True)
102
+
103
+ plt.tight_layout()
104
+ plt.show()
105
+
106
+ # Tests spécifiques aux deux bornes
107
+ for f_test in [0., 0.001, 0.01, 0.5, 0.99, 0.999]:
108
+ f_test = jnp.float32(f_test)
109
+ h_num = soft_dichotomy(R, f_test)
110
+ h_ana = analytical_h(R, f_test)
111
+ grad_h = grad_bisection(R, f_test)
112
+ print(f"f = {f_test:.4f}:")
113
+ print(f" h numérique = {h_num:.6f}")
114
+ print(f" h analytique = {h_ana:.6f}")
115
+ print(f" Gradient de h par rapport à f = {grad_h:.6f}")
116
+ print(f" Erreur = {jnp.abs(h_num - h_ana):.6f}")
117
+ print(f" Erreur relative = {jnp.abs(h_num - h_ana) / h_ana:.6e}")
118
+ pass
@@ -0,0 +1,169 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+ from jax import jit, vmap
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ from scipy.optimize import minimize, root_scalar
7
+
8
+ # Forcer float32
9
+ jax.config.update("jax_enable_x64", False)
10
+
11
+ # Fonction pour calculer l'aire immergée
12
+ def wet_area(h, R):
13
+ h = jnp.clip(h, 0, 2 * R)
14
+ theta = 2 * jnp.arccos(1 - h / R)
15
+ area = (R**2 / 2) * (theta - jnp.sin(theta))
16
+ return area
17
+
18
+ # Solution numérique "classique" pour h
19
+ def analytical_h(R, f):
20
+ def solve_theta(theta):
21
+ return (theta - jnp.sin(theta)) / (2 * jnp.pi) - f
22
+ theta_min, theta_max = jnp.float32(0.0), jnp.float32(2 * jnp.pi)
23
+ for _ in range(50):
24
+ theta_mid = (theta_min + theta_max) / 2
25
+ val = solve_theta(theta_mid)
26
+ theta_max = jnp.where(val > 0, theta_mid, theta_max)
27
+ theta_min = jnp.where(val <= 0, theta_mid, theta_min)
28
+ theta = (theta_min + theta_max) / 2
29
+ return R * (1 - jnp.cos(theta / 2))
30
+
31
+ # # Fonction objectif avec pondération quadratique
32
+ # def objective(h, R, f):
33
+ # total_area = jnp.pi * R**2
34
+ # target_area = f * total_area
35
+ # diff = wet_area(h, R) - target_area
36
+ # scale_factor = jnp.minimum(f, 1. - f)
37
+ # scale = 0.1 * R**2 * (scale_factor**2 + 0.01) # Échelle quadratique
38
+ # return 1 / (1 + jnp.exp(-diff / scale))
39
+
40
+ # def objective(h, R, f):
41
+ # total_area = jnp.pi * R**2
42
+ # target_area = f * total_area
43
+ # diff = wet_area(h, R) - target_area
44
+ # scale_factor = jnp.minimum(f, 1. - f)
45
+ # scale = 0.1 * R**2 * jnp.maximum(scale_factor, 0.01)
46
+ # return 0.5 * (1 + jnp.tanh(diff / scale))
47
+
48
+
49
+ # Fonction objectif avec lissage (inchangée)
50
+ def objective(h, R, f):
51
+ total_area = jnp.pi * R**2
52
+ target_area = f * total_area
53
+ diff = wet_area(h, R) - target_area
54
+ scale_factor = jnp.minimum(f, 1. - f)
55
+ scale = 0.05 * R**2 * jnp.maximum(scale_factor, 0.01)
56
+ return 1 / (1 + jnp.exp(-diff / scale))
57
+
58
+ # Dichotomie douce avec préservation de la racine
59
+ @jit
60
+ def soft_dichotomy(R, f, max_iter=10000):
61
+ f_sym = jnp.minimum(f, 1 - f)
62
+
63
+ def body(state, _):
64
+ h_min, h_max = state
65
+ h_mid = (h_min + h_max) / 2
66
+ sigmoid_val = objective(h_mid, R, f_sym)
67
+
68
+ # Différences pour h_min, h_mid et h_max
69
+ diff_min = wet_area(h_min, R) - f_sym * jnp.pi * R**2
70
+ diff_max = wet_area(h_max, R) - f_sym * jnp.pi * R**2
71
+
72
+ # Facteurs de préservation lisses
73
+ preserve_min = jnp.exp(-jnp.abs(diff_min) / (0.01)) # Proche de 1 si h_min est racine
74
+ preserve_max = jnp.exp(-jnp.abs(diff_max) / (0.01)) # Proche de 1 si h_max est racine
75
+
76
+ # Mise à jour des bornes avec préservation
77
+ h_min_new = h_min + (1. - preserve_min) * (h_mid - h_min) * (1. - sigmoid_val)
78
+ h_max_new = h_max - (1. - preserve_max) * (h_max - h_mid) * sigmoid_val
79
+
80
+ # # Garantie que h_min_new < h_max_new
81
+ # h_min_new = jnp.minimum(h_min_new, h_mid - 0.01 * R)
82
+ # h_max_new = jnp.maximum(h_max_new, h_mid + 0.01 * R)
83
+
84
+ return (h_min_new, h_max_new), None
85
+
86
+ h_min_init = jnp.float32(0.0)
87
+ h_max_init = jnp.float32(2 * R)
88
+ initial_state = (h_min_init, h_max_init)
89
+ final_state, _ = jax.lax.scan(body, initial_state, None, length=max_iter)
90
+ h_min, h_max = final_state
91
+ h_sym = (h_min + h_max) / 2
92
+ return jnp.where(f <= 0.5, h_sym, 2 * R - h_sym)
93
+
94
+ # Dérivée par rapport à f
95
+ grad_bisection = jax.grad(soft_dichotomy, argnums=1)
96
+
97
+ # Fonction objectif avec lissage
98
+ def section(h, R, f):
99
+ total_area = jnp.pi * R**2
100
+ target_area = f * total_area
101
+ return wet_area(jnp.clip(h, 0, 2 * R), R) - target_area
102
+
103
+ grad_section = jax.grad(section, argnums=0)
104
+
105
+ # recherche de la recine de la section
106
+ def find_root(R, f):
107
+ def fun(h):
108
+ return section(h, R, f)
109
+ def grad(h):
110
+ return grad_section(h, R, f)
111
+
112
+ h_root = root_scalar(fun, fprime=grad, x0=R)
113
+ return h_root.root
114
+
115
+ h = find_root(1.,0.5)
116
+ pass
117
+
118
+ if __name__ == "__main__":
119
+ R = jnp.float32(1.0)
120
+ f_values = jnp.linspace(jnp.float32(0.00), jnp.float32(1.), 5000, endpoint=True)
121
+
122
+ h_numerical = vmap(lambda f: soft_dichotomy(R, f))(f_values)
123
+ h_analytical = vmap(lambda f: analytical_h(R, f))(f_values)
124
+ errors = jnp.abs(h_numerical - h_analytical)
125
+
126
+ grads = vmap(lambda f: grad_bisection(R, f))(f_values)
127
+
128
+ plt.figure(figsize=(12, 5))
129
+
130
+ plt.subplot(1, 3, 1)
131
+ plt.plot(f_values, h_numerical, label="Numérique (symétrie centrale)", color="blue")
132
+ plt.plot(f_values, h_analytical, "--", label="Analytique", color="orange")
133
+ plt.xlabel("Fraction immergée (f)")
134
+ plt.ylabel("Hauteur (h)")
135
+ plt.title("Hauteur en fonction de f (float32)")
136
+ plt.legend()
137
+ plt.grid(True)
138
+ # plt.yscale("log")
139
+
140
+ plt.subplot(1, 3, 2)
141
+ plt.plot(f_values, errors, color="red")
142
+ plt.xlabel("Fraction immergée (f)")
143
+ plt.ylabel("Erreur absolue (|h_num - h_ana|)")
144
+ plt.title("Erreur par rapport à la solution analytique")
145
+ plt.grid(True)
146
+ plt.yscale("log")
147
+
148
+ plt.subplot(1, 3, 3)
149
+ plt.plot(f_values, grads, label="Dérivée par rapport à f")
150
+ plt.xlabel("Fraction immergée (f)")
151
+ plt.ylabel("Gradient de f par rapport à h")
152
+ plt.legend()
153
+ plt.grid(True)
154
+
155
+ plt.tight_layout()
156
+ plt.show()
157
+
158
+ for f_test in [0., 0.001, 0.01, 0.5, 0.99, 0.999]:
159
+ f_test = jnp.float32(f_test)
160
+ h_num = soft_dichotomy(R, f_test)
161
+ h_ana = analytical_h(R, f_test)
162
+ grad_h = grad_bisection(R, f_test)
163
+ print(f"f = {f_test:.4f}:")
164
+ print(f" h numérique = {h_num:.6f}")
165
+ print(f" h analytique = {h_ana:.6f}")
166
+ print(f" Gradient de h par rapport à f = {grad_h:.6f}")
167
+ print(f" Erreur = {jnp.abs(h_num - h_ana):.6f}")
168
+ print(f" Erreur relative = {jnp.abs(h_num - h_ana) / h_ana:.6e}")
169
+ pass