differentiable-tmm 0.1.0__tar.gz

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.
Files changed (31) hide show
  1. differentiable_tmm-0.1.0/Differentiable_TMM/__init__.py +18 -0
  2. differentiable_tmm-0.1.0/MANIFEST.in +5 -0
  3. differentiable_tmm-0.1.0/PKG-INFO +627 -0
  4. differentiable_tmm-0.1.0/README.md +604 -0
  5. differentiable_tmm-0.1.0/assets/Si3N4.txt +101 -0
  6. differentiable_tmm-0.1.0/assets/SiO2.txt +395 -0
  7. differentiable_tmm-0.1.0/core/TMM.py +322 -0
  8. differentiable_tmm-0.1.0/core/__init__.py +3 -0
  9. differentiable_tmm-0.1.0/core/_init_.py +1 -0
  10. differentiable_tmm-0.1.0/core/color_transformation.py +193 -0
  11. differentiable_tmm-0.1.0/differentiable_tmm.egg-info/PKG-INFO +627 -0
  12. differentiable_tmm-0.1.0/differentiable_tmm.egg-info/SOURCES.txt +29 -0
  13. differentiable_tmm-0.1.0/differentiable_tmm.egg-info/dependency_links.txt +1 -0
  14. differentiable_tmm-0.1.0/differentiable_tmm.egg-info/requires.txt +5 -0
  15. differentiable_tmm-0.1.0/differentiable_tmm.egg-info/top_level.txt +5 -0
  16. differentiable_tmm-0.1.0/example/__init__.py +1 -0
  17. differentiable_tmm-0.1.0/example/_paths.py +8 -0
  18. differentiable_tmm-0.1.0/example/example1.py +164 -0
  19. differentiable_tmm-0.1.0/example/example2.py +158 -0
  20. differentiable_tmm-0.1.0/example/example3.py +221 -0
  21. differentiable_tmm-0.1.0/example/example4.py +237 -0
  22. differentiable_tmm-0.1.0/example/example5.py +134 -0
  23. differentiable_tmm-0.1.0/pyproject.toml +49 -0
  24. differentiable_tmm-0.1.0/setup.cfg +4 -0
  25. differentiable_tmm-0.1.0/utils/__init__.py +3 -0
  26. differentiable_tmm-0.1.0/utils/_init_.py +1 -0
  27. differentiable_tmm-0.1.0/utils/chromaticity_store.py +112 -0
  28. differentiable_tmm-0.1.0/utils/datatype_restriction.py +32 -0
  29. differentiable_tmm-0.1.0/utils/illumination_store.py +4852 -0
  30. differentiable_tmm-0.1.0/utils/observer_store.py +965 -0
  31. differentiable_tmm-0.1.0/utils/spectrum_interpolation.py +63 -0
@@ -0,0 +1,18 @@
1
+ """Public API for Differentiable_TMM."""
2
+
3
+ from core import TMM
4
+ from core.color_transformation import DE94, Spectrum2XYZ, XYZ2Lab, XYZ2sRGB
5
+ from utils import spectrum_interpolation
6
+ from utils.illumination_store import illumination
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "TMM",
12
+ "spectrum_interpolation",
13
+ "Spectrum2XYZ",
14
+ "XYZ2Lab",
15
+ "XYZ2sRGB",
16
+ "DE94",
17
+ "illumination",
18
+ ]
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include pyproject.toml
3
+ recursive-include Differentiable_TMM *.py
4
+ recursive-include assets *.txt
5
+ recursive-include example *.py
@@ -0,0 +1,627 @@
1
+ Metadata-Version: 2.1
2
+ Name: differentiable_tmm
3
+ Version: 0.1.0
4
+ Summary: Differentiable transfer-matrix method tools for multilayer thin-film optics and color simulation.
5
+ Author: Peiqin
6
+ Project-URL: Homepage, https://github.com/Dav1d-L1/Differentiable_TMM
7
+ Project-URL: Repository, https://github.com/Dav1d-L1/Differentiable_TMM
8
+ Keywords: thin-film,transfer-matrix-method,TMM,optics,differentiable,pytorch,color-science
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Classifier: Topic :: Scientific/Engineering :: Visualization
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: matplotlib
19
+ Requires-Dist: numpy
20
+ Requires-Dist: scipy
21
+ Requires-Dist: torch
22
+ Requires-Dist: tqdm
23
+
24
+ # Differentiable_TMM
25
+
26
+ `Differentiable_TMM` is a PyTorch-based transfer-matrix method (TMM) toolkit for
27
+ multilayer thin-film optics. It computes angle- and wavelength-dependent
28
+ reflectance/transmittance and can be connected directly to PyTorch optimizers for
29
+ inverse design of thin-film structures. The repository also includes color
30
+ conversion utilities for converting spectra to CIE XYZ, CIE Lab, and sRGB.
31
+
32
+ All optical lengths in this project use **nanometers (nm)**. Incident angles are
33
+ given in **degrees** when constructing the simulator.
34
+
35
+ ## Installation
36
+
37
+ Install the package and all dependencies, including the example dependencies:
38
+
39
+ ```bash
40
+ pip install Differentiable_TMM
41
+ ```
42
+
43
+ ## Main API
44
+
45
+ The recommended public API is:
46
+
47
+ ```python
48
+ from Differentiable_TMM import (
49
+ TMM,
50
+ spectrum_interpolation,
51
+ Spectrum2XYZ,
52
+ XYZ2Lab,
53
+ XYZ2sRGB,
54
+ DE94,
55
+ )
56
+ ```
57
+
58
+ `TMM` is the transfer-matrix module, `spectrum_interpolation` contains the
59
+ refractive-index helper classes, and the color-conversion classes/functions are
60
+ available directly from `Differentiable_TMM`.
61
+
62
+ ## Refractive Index Helpers
63
+
64
+ ### `spectrum_interpolation.interpolate_refrective`
65
+
66
+ Creates a callable interpolation object for wavelength-dependent refractive
67
+ indices.
68
+
69
+ ```python
70
+ from Differentiable_TMM import spectrum_interpolation
71
+
72
+ n_fn = spectrum_interpolation.interpolate_refrective(
73
+ wavelength,
74
+ refrective,
75
+ method="1d_interpolate",
76
+ )
77
+ ```
78
+
79
+ Parameters:
80
+
81
+ - `wavelength`: `numpy.ndarray`
82
+ - Shape: `(num_wavelengths,)`
83
+ - Dtype: integer, `np.float32`, or `np.float64`
84
+ - Unit: nm
85
+ - Meaning: sampled wavelengths for the material data.
86
+ - `refrective`: `numpy.ndarray`
87
+ - Shape: `(num_wavelengths,)`
88
+ - Dtype: integer, float, or complex
89
+ - Meaning: refractive index values `n` or complex values `n + 1j*k`.
90
+ - `method`: `str`
91
+ - Supported values: `"1d_interpolate"` and `"cubicspline"`
92
+ - Default: `"1d_interpolate"`
93
+
94
+ Call input:
95
+
96
+ ```python
97
+ n_values = n_fn(query_wavelengths)
98
+ ```
99
+
100
+ - `query_wavelengths`: `numpy.ndarray`
101
+ - Shape: any 1D shape, usually `(batch_size,)`
102
+ - Unit: nm
103
+ - Values outside the source wavelength range are clipped to the nearest
104
+ available wavelength.
105
+
106
+ Return value:
107
+
108
+ - `n_values`: `numpy.ndarray`
109
+ - Shape: same as `query_wavelengths`
110
+ - Dtype: complex
111
+ - Unit: dimensionless refractive index.
112
+
113
+ Example:
114
+
115
+ ```python
116
+ import numpy as np
117
+ from Differentiable_TMM import spectrum_interpolation
118
+
119
+ data = np.loadtxt("assets/SiO2.txt")
120
+ SiO2_n_fn = spectrum_interpolation.interpolate_refrective(
121
+ data[:, 0] * 1e3,
122
+ data[:, 1] + 1j * data[:, 2],
123
+ )
124
+
125
+ wavelengths = np.array([450.0, 550.0, 650.0])
126
+ n_sio2 = SiO2_n_fn(wavelengths)
127
+ ```
128
+
129
+ ### `spectrum_interpolation.constent_refrective`
130
+
131
+ Creates a callable object for a wavelength-independent refractive index.
132
+
133
+ ```python
134
+ air_n_fn = spectrum_interpolation.constent_refrective(1.0)
135
+ glass_n_fn = spectrum_interpolation.constent_refrective(1.52)
136
+ ```
137
+
138
+ Parameters:
139
+
140
+ - `refrective`: real or complex scalar
141
+ - Examples: `1.0`, `1.52`, `1.5 + 0.01j`
142
+
143
+ Call input:
144
+
145
+ - `value`: `numpy.ndarray`
146
+ - Shape: any 1D shape, usually `(batch_size,)`
147
+ - Unit: nm
148
+
149
+ Return value:
150
+
151
+ - `numpy.ndarray`
152
+ - Shape: same as `value`
153
+ - Dtype: complex
154
+ - Every entry equals the constant refractive index.
155
+
156
+ ## Transfer-Matrix Simulation
157
+
158
+ ### `TMM.tmm`
159
+
160
+ `TMM.tmm` is the main differentiable transfer-matrix simulator.
161
+
162
+ ```python
163
+ simulator = TMM.tmm(
164
+ pol="s",
165
+ datatype="complex128",
166
+ theta_array=theta_array,
167
+ wv_array=wv_array,
168
+ )
169
+ ```
170
+
171
+ Constructor parameters:
172
+
173
+ - `pol`: `str`
174
+ - `"s"` for s-polarized light.
175
+ - `"p"` for p-polarized light.
176
+ - `datatype`: `str`
177
+ - `"complex64"` or `"complex128"`.
178
+ - This also determines the real dtype used internally:
179
+ - `"complex64"` -> `torch.float32`
180
+ - `"complex128"` -> `torch.float64`
181
+ - `theta_array`: `torch.Tensor`
182
+ - Shape: `(batch_size,)`
183
+ - Dtype: integer, `torch.float32`, or `torch.float64`
184
+ - Unit: degrees
185
+ - Meaning: incident angle for each simulation point.
186
+ - `wv_array`: `torch.Tensor`
187
+ - Shape: `(batch_size,)`
188
+ - Dtype: integer, `torch.float32`, or `torch.float64`
189
+ - Unit: nm
190
+ - Meaning: vacuum wavelength for each simulation point.
191
+
192
+ `theta_array` and `wv_array` must be on the same device and have the same first
193
+ dimension.
194
+
195
+ ### Calling a Simulator
196
+
197
+ ```python
198
+ result = simulator(
199
+ n=n,
200
+ d=d,
201
+ n0=n0,
202
+ c_list=c_list,
203
+ )
204
+ ```
205
+
206
+ Input tensors:
207
+
208
+ - `n`: `torch.Tensor`
209
+ - Shape: `(batch_size, num_layers)`
210
+ - Dtype: complex dtype matching the simulator, usually `torch.complex128`
211
+ - Meaning: refractive index of every physical layer in the stack.
212
+ - `d`: `torch.Tensor`
213
+ - Shape: `(batch_size, num_layers)`
214
+ - Dtype: real dtype matching the simulator, usually `torch.float64`
215
+ - Unit: nm
216
+ - Meaning: thickness of every physical layer in the stack.
217
+ - `n0`: `torch.Tensor`
218
+ - Shape: `(batch_size, 2)`
219
+ - Dtype: complex or real
220
+ - Meaning:
221
+ - `n0[:, 0]`: incident medium refractive index
222
+ - `n0[:, 1]`: output/substrate-side medium refractive index
223
+ - The incident and output media are treated as incoherent semi-infinite media.
224
+ - `c_list`: `list[str]`
225
+ - Length: `num_layers`
226
+ - Each entry must be:
227
+ - `"c"` for coherent layer
228
+ - `"i"` for incoherent layer
229
+ - The layer count must satisfy:
230
+
231
+ ```python
232
+ n.shape[1] == d.shape[1] == len(c_list)
233
+ ```
234
+
235
+ Return value:
236
+
237
+ ```python
238
+ {
239
+ "R": R,
240
+ "T": T,
241
+ }
242
+ ```
243
+
244
+ - `R`: `torch.Tensor`
245
+ - Shape: `(batch_size,)`
246
+ - Reflectance.
247
+ - `T`: `torch.Tensor`
248
+ - Shape: `(batch_size,)`
249
+ - Transmittance.
250
+
251
+ The operations are differentiable with respect to differentiable inputs such as
252
+ `d`, so layer thicknesses can be optimized with PyTorch.
253
+
254
+ ### Minimal Reflectance/Transmittance Example
255
+
256
+ ```python
257
+ import numpy as np
258
+ import torch
259
+ from Differentiable_TMM import TMM, spectrum_interpolation
260
+
261
+ device = torch.device("cpu")
262
+
263
+ air_n_fn = spectrum_interpolation.constent_refrective(1.0)
264
+ glass_n_fn = spectrum_interpolation.constent_refrective(1.52)
265
+ film_n_fn = spectrum_interpolation.constent_refrective(2.0)
266
+
267
+ wavelengths_np = np.linspace(400, 700, 31)
268
+ angles_np = np.zeros_like(wavelengths_np)
269
+
270
+ n0 = torch.tensor(
271
+ np.array([air_n_fn(wavelengths_np), air_n_fn(wavelengths_np)]).T,
272
+ dtype=torch.complex128,
273
+ ).to(device)
274
+
275
+ n = torch.tensor(
276
+ np.array([glass_n_fn(wavelengths_np), film_n_fn(wavelengths_np)]).T,
277
+ dtype=torch.complex128,
278
+ ).to(device)
279
+
280
+ d = torch.tensor(
281
+ np.array([[500000.0, 100.0]] * len(wavelengths_np)),
282
+ dtype=torch.float64,
283
+ ).to(device)
284
+
285
+ theta_array = torch.tensor(angles_np, dtype=torch.float64).to(device)
286
+ wv_array = torch.tensor(wavelengths_np, dtype=torch.float64).to(device)
287
+ c_list = ["i", "c"]
288
+
289
+ sim_s = TMM.tmm("s", "complex128", theta_array, wv_array)
290
+ sim_p = TMM.tmm("p", "complex128", theta_array, wv_array)
291
+
292
+ result_s = sim_s(n=n, d=d, n0=n0, c_list=c_list)
293
+ result_p = sim_p(n=n, d=d, n0=n0, c_list=c_list)
294
+
295
+ reflectance = (result_s["R"] + result_p["R"]) / 2
296
+ transmittance = (result_s["T"] + result_p["T"]) / 2
297
+ ```
298
+
299
+ ## Color Conversion
300
+
301
+ The color utilities are imported directly from `Differentiable_TMM`.
302
+
303
+ ### `Spectrum2XYZ`
304
+
305
+ Converts spectral reflectance or transmittance curves to CIE XYZ.
306
+
307
+ ```python
308
+ spectrum_to_xyz = Spectrum2XYZ(
309
+ lightsource="D65",
310
+ observer="CIE 1931 2 Degree Standard Observer",
311
+ device=device,
312
+ datatype="complex128",
313
+ clip=True,
314
+ )
315
+ ```
316
+
317
+ Parameters:
318
+
319
+ - `lightsource`: `str`
320
+ - Must be one of the keys in `utils.illumination_store.illumination`.
321
+ - Common values include `"A"`, `"D50"`, `"D55"`, `"D65"`, `"D75"`, and
322
+ fluorescent illuminants such as `"FL1"`.
323
+ - `observer`: `str`
324
+ - Supported observers are stored in `utils.observer_store.Observer`.
325
+ - Common values:
326
+ - `"CIE 1931 2 Degree Standard Observer"`
327
+ - `"CIE 1964 10 Degree Standard Observer"`
328
+ - `device`: `torch.device`
329
+ - Device where internal tensors are stored.
330
+ - `datatype`: `str`
331
+ - `"complex64"` or `"complex128"`.
332
+ - `clip`: `bool`
333
+ - If `True`, uses the ASTM E308 practical working wavelength range
334
+ 360-780 nm.
335
+
336
+ Useful attributes:
337
+
338
+ - `spectrum_to_xyz.wavelength`
339
+ - `torch.Tensor`
340
+ - Shape: `(num_wavelengths,)`
341
+ - Unit: nm
342
+ - Use this wavelength array when building TMM spectra for color conversion.
343
+ - `spectrum_to_xyz.datatype_real`
344
+ - Real dtype corresponding to the selected complex dtype.
345
+
346
+ Call input:
347
+
348
+ ```python
349
+ XYZ = spectrum_to_xyz(reflectances)
350
+ ```
351
+
352
+ - `reflectances`: `torch.Tensor`
353
+ - Shape: `(batch_size, num_wavelengths)`
354
+ - Dtype: real or complex tensor compatible with the selected device/dtype
355
+ - Meaning: spectral reflectance or transmittance sampled at
356
+ `spectrum_to_xyz.wavelength`.
357
+
358
+ Return value:
359
+
360
+ - `XYZ`: `torch.Tensor`
361
+ - Shape: `(batch_size, 3)`
362
+ - Columns: `X`, `Y`, `Z`
363
+
364
+ ### `XYZ2Lab`
365
+
366
+ Converts CIE XYZ to CIE Lab under a specified illuminant and observer.
367
+
368
+ ```python
369
+ xyz_to_lab = XYZ2Lab(
370
+ lightsource="D65",
371
+ observer="CIE 1931 2 Degree Standard Observer",
372
+ device=device,
373
+ datatype="complex128",
374
+ )
375
+
376
+ Lab = xyz_to_lab(XYZ)
377
+ ```
378
+
379
+ Input:
380
+
381
+ - `XYZ`: `torch.Tensor`
382
+ - Shape: `(batch_size, 3)`
383
+ - Columns: `X`, `Y`, `Z`
384
+
385
+ Output:
386
+
387
+ - `Lab`: `torch.Tensor`
388
+ - Shape: `(batch_size, 3)`
389
+ - Columns: `L*`, `a*`, `b*`
390
+
391
+ ### `XYZ2sRGB`
392
+
393
+ Converts CIE XYZ to sRGB.
394
+
395
+ ```python
396
+ xyz_to_srgb = XYZ2sRGB(device, datatype="complex128")
397
+ RGB = xyz_to_srgb(XYZ)
398
+ ```
399
+
400
+ Input:
401
+
402
+ - `XYZ`: `torch.Tensor`
403
+ - Shape: `(batch_size, 3)`
404
+
405
+ Output:
406
+
407
+ - `RGB`: `torch.Tensor`
408
+ - Shape: `(batch_size, 3)`
409
+ - Columns: red, green, blue
410
+ - Values are not clipped inside the function. For plotting with Matplotlib,
411
+ use:
412
+
413
+ ```python
414
+ RGB_plot = np.clip(np.real(RGB.detach().cpu().numpy()), 0, 1)
415
+ ```
416
+
417
+ ### `DE94`
418
+
419
+ Computes CIE94 color difference.
420
+
421
+ ```python
422
+ delta_e = DE94(Lab, Lab_target)
423
+ ```
424
+
425
+ Inputs:
426
+
427
+ - `Lab`: `torch.Tensor`
428
+ - Shape: `(batch_size, 3)`
429
+ - `Lab_target`: `torch.Tensor`
430
+ - Shape: `(batch_size, 3)`
431
+
432
+ Output:
433
+
434
+ - `delta_e`: `torch.Tensor`
435
+ - Shape: `(batch_size,)`
436
+
437
+ ## Differentiable Optimization Pattern
438
+
439
+ A common inverse-design workflow is to optimize unconstrained variables and map
440
+ them into physical layer thickness bounds with a sigmoid:
441
+
442
+ ```python
443
+ lower_bound = 1.0
444
+ upper_bound = 2000.0
445
+
446
+ struc_logits = torch.zeros(num_layers, dtype=torch.float64, requires_grad=True)
447
+ optimizer = torch.optim.Adam([struc_logits], lr=0.01)
448
+
449
+ for epoch in range(epochs):
450
+ struc = lower_bound + (upper_bound - lower_bound) * torch.sigmoid(struc_logits)
451
+ d = torch.cat([meta_data, struc.repeat(num_samples, 1)], dim=-1)
452
+
453
+ # Run TMM -> spectrum -> XYZ -> Lab, then compare with target Lab.
454
+ loss.backward()
455
+ optimizer.step()
456
+ optimizer.zero_grad()
457
+ ```
458
+
459
+ This keeps every optimized layer thickness inside `[lower_bound, upper_bound]`
460
+ without adding a penalty term.
461
+
462
+ ## Examples
463
+
464
+ Example scripts live in the `example/` directory. Run them from the project root:
465
+
466
+ ```bash
467
+ python example/example1.py
468
+ python example/example2.py
469
+ python example/example3.py
470
+ python example/example4.py
471
+ python example/example5.py
472
+ ```
473
+
474
+ All example PNG outputs are saved in the `assets/` directory.
475
+
476
+ ### Example 1: Angle and Thickness Color Map
477
+
478
+ File: `example/example1.py`
479
+
480
+ Purpose:
481
+
482
+ - Simulates a 5-layer coating stack.
483
+ - Sweeps observation angle and one film thickness.
484
+ - Converts reflectance and transmittance spectra to sRGB.
485
+ - Saves two color maps:
486
+ - `assets/example1.png`
487
+ - `assets/example1_1.png`
488
+
489
+ Main output:
490
+
491
+ - Reflectance color map.
492
+ - Transmittance color map.
493
+
494
+ ### Example 2: Normal-Incidence Lab Optimization
495
+
496
+ File: `example/example2.py`
497
+
498
+ Purpose:
499
+
500
+ - Optimizes a 5-layer stack at normal incidence.
501
+ - Target color is specified as Lab.
502
+ - Uses sigmoid thickness bounds.
503
+ - Saves:
504
+ - `assets/example2.png`
505
+
506
+ Main output:
507
+
508
+ - Plot of `L*`, `a*`, and `b*` versus optimization epoch.
509
+ - Printed final layer thicknesses in nm.
510
+
511
+ ### Example 3: Multi-Angle Lab Optimization
512
+
513
+ File: `example/example3.py`
514
+
515
+ Purpose:
516
+
517
+ - Optimizes a configurable multilayer coating against Lab targets at several
518
+ observation angles.
519
+ - Uses a configurable stack builder:
520
+
521
+ ```python
522
+ num_coating_layers = 11
523
+
524
+ def build_layer_refractive_indices(wavelengths):
525
+ layers = [glass_n_fn(wavelengths)]
526
+ coating_n_fns = [SiO2_n_fn, Si3N4_n_fn]
527
+ for i in range(num_coating_layers):
528
+ layers.append(coating_n_fns[i % 2](wavelengths))
529
+ return np.array(layers)
530
+
531
+ def build_c_list():
532
+ return ["i"] + ["c"] * num_coating_layers
533
+ ```
534
+
535
+ Saves:
536
+
537
+ - `assets/example3.png`
538
+ - `assets/example3_1.png`
539
+
540
+ Main output:
541
+
542
+ - Lab value versus observation angle.
543
+ - Color bar visualization of the optimized design.
544
+ - Printed final layer thicknesses in nm.
545
+
546
+ ### Example 4: Tolerance-Aware Optimization
547
+
548
+ File: `example/example4.py`
549
+
550
+ Purpose:
551
+
552
+ - Optimizes a configurable multilayer coating while sampling thickness tolerance
553
+ variations with Latin hypercube sampling.
554
+ - Uses the same configurable layer count pattern as Example 3.
555
+ - Saves:
556
+ - `assets/example4.png`
557
+
558
+ Main output:
559
+
560
+ - Comparison between initial and optimized designs under a tolerance sweep.
561
+ - Printed initial and final layer thicknesses in nm.
562
+
563
+ ### Example 5: Color Under Different Illuminants
564
+
565
+ File: `example/example5.py`
566
+
567
+ Purpose:
568
+
569
+ - Computes reflectance once for a fixed coating design.
570
+ - Converts the same spectrum under all illuminants stored in
571
+ `utils.illumination_store.illumination`.
572
+ - Saves:
573
+ - `assets/example5.png`
574
+
575
+ Main output:
576
+
577
+ - A color bar showing the perceived sRGB color under each illuminant.
578
+
579
+ ## Building the Package
580
+
581
+ Clean old builds:
582
+
583
+ ```bash
584
+ rm -rf build dist *.egg-info
585
+ ```
586
+
587
+ Build source distribution and wheel:
588
+
589
+ ```bash
590
+ python -m build
591
+ ```
592
+
593
+ The build artifacts will be created in `dist/`.
594
+
595
+ Check the package metadata:
596
+
597
+ ```bash
598
+ twine check dist/*
599
+ ```
600
+
601
+ Upload to TestPyPI first:
602
+
603
+ ```bash
604
+ twine upload --repository testpypi dist/*
605
+ ```
606
+
607
+ Upload to PyPI:
608
+
609
+ ```bash
610
+ twine upload dist/*
611
+ ```
612
+
613
+ After uploading, users can install with the command shown in the installation
614
+ section.
615
+
616
+ ## Notes for Maintainers
617
+
618
+ - The recommended public import is `from Differentiable_TMM import ...`.
619
+ - The package still includes `core` and `utils` as top-level packages for
620
+ compatibility with existing scripts.
621
+ - The historical class/function names use `refrective` and `constent` spelling.
622
+ They are kept unchanged to preserve compatibility with the existing code.
623
+ - Keep `Differentiable_TMM/__init__.py` synchronized with the intended public
624
+ API when adding new user-facing functions.
625
+ - `torch` wheels are platform-specific. If users need a CUDA-specific PyTorch
626
+ installation, they should install PyTorch following the official PyTorch
627
+ instructions before installing this package.