radphen-iadpython 0.7.0__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.
@@ -0,0 +1,712 @@
1
+ #!/usr/bin/env python3
2
+ """Command line for support for iadpython."""
3
+
4
+ import os
5
+ import sys
6
+ from enum import Enum
7
+ import argparse
8
+ from contextlib import redirect_stdout
9
+ import time
10
+ import numpy as np
11
+ import iadpython
12
+
13
+ # Global COUNTER to mimic static behavior in C
14
+ COUNTER = 0
15
+ ANY_ERROR = False
16
+
17
+
18
+ class SlidePosition(Enum):
19
+ """All the combinations of glass and sample."""
20
+
21
+ NO_SLIDES = 0
22
+ ONE_SLIDE_ON_TOP = 1
23
+ TWO_IDENTICAL_SLIDES = 2
24
+ ONE_SLIDE_ON_BOTTOM = 3
25
+ ONE_SLIDE_NEAR_SPHERE = 4
26
+ ONE_SLIDE_NOT_NEAR_SPHERE = 5
27
+
28
+
29
+ class LightCondition(Enum):
30
+ """Options for interpreting illumination."""
31
+
32
+ MR_IS_ONLY_RD = 1
33
+ MT_IS_ONLY_TD = 2
34
+ NO_UNSCATTERED_LIGHT = 3
35
+
36
+
37
+ class InputError(Enum):
38
+ """Possible input errors."""
39
+
40
+ NO_ERROR = 0
41
+ TOO_MANY_ITERATIONS = 1
42
+ MR_TOO_BIG = 2
43
+ MR_TOO_SMALL = 3
44
+ MT_TOO_BIG = 4
45
+ MT_TOO_SMALL = 5
46
+ MU_TOO_BIG = 6
47
+ MU_TOO_SMALL = 7
48
+ TOO_MUCH_LIGHT = 8
49
+
50
+
51
+ def print_error_legend():
52
+ """Print error explanation and quit."""
53
+ print("----------------- Sorry, but ... errors encountered ---------------")
54
+ print(" * ==> Success ")
55
+ print(" 0-9 ==> Monte Carlo Iteration")
56
+ print(" R ==> M_R is too big ")
57
+ print(" r ==> M_R is too small")
58
+ print(" T ==> M_T is too big ")
59
+ print(" t ==> M_T is too small")
60
+ print(" U ==> M_U is too big ")
61
+ print(" u ==> M_U is too small")
62
+ print(" ! ==> M_R + M_T > 1 ")
63
+ print(" + ==> Did not converge\n")
64
+ sys.exit(0)
65
+
66
+
67
+ def what_char(err):
68
+ """Return appropriate character for analysis of current datapoint."""
69
+ if err == InputError.NO_ERROR:
70
+ return "*"
71
+ if err == InputError.TOO_MANY_ITERATIONS:
72
+ return "+"
73
+ if err == InputError.MR_TOO_BIG:
74
+ return "R"
75
+ if err == InputError.MR_TOO_SMALL:
76
+ return "r"
77
+ if err == InputError.MT_TOO_BIG:
78
+ return "T"
79
+ if err == InputError.MT_TOO_SMALL:
80
+ return "t"
81
+ if err == InputError.MU_TOO_BIG:
82
+ return "U"
83
+ if err == InputError.MU_TOO_SMALL:
84
+ return "u"
85
+ if err == InputError.TOO_MUCH_LIGHT:
86
+ return "!"
87
+ return "?"
88
+
89
+
90
+ def print_dot(start_time, err, points, final, verbosity):
91
+ """Print a character for each datapoint during analysis."""
92
+ global COUNTER # pylint: disable=global-statement
93
+ global ANY_ERROR # pylint: disable=global-statement
94
+ COUNTER += 1
95
+
96
+ if err != InputError.NO_ERROR:
97
+ ANY_ERROR = err
98
+
99
+ if verbosity == 0:
100
+ return
101
+
102
+ if final == 99:
103
+ print(what_char(err), end="")
104
+ else:
105
+ COUNTER -= 1
106
+ print(f"{final % 10}\b", end="")
107
+
108
+ if final == 99:
109
+ if COUNTER % 50 == 0:
110
+ rate = (time.time() - start_time) / points
111
+ print(f" {points} done ({rate:.2f} s/pt)")
112
+ elif COUNTER % 10 == 0:
113
+ print(" ", end="")
114
+
115
+ sys.stdout.flush()
116
+
117
+
118
+ def validator_01(value):
119
+ """Is value between 0 and 1."""
120
+ try:
121
+ fvalue = float(value)
122
+ except ValueError as exc:
123
+ raise argparse.ArgumentTypeError(f"Commandline: {value} is not a valid number") from exc
124
+ if not 0 <= fvalue <= 1:
125
+ raise argparse.ArgumentTypeError(f"Commandline: {value} is not between 0 and 1")
126
+ return fvalue
127
+
128
+
129
+ def validator_11(value):
130
+ """Is value between -1 and 1."""
131
+ try:
132
+ fvalue = float(value)
133
+ except ValueError as exc:
134
+ raise argparse.ArgumentTypeError(f"{value} is not a valid number") from exc
135
+ if not -1 <= fvalue <= 1:
136
+ raise argparse.ArgumentTypeError(f"Commandline: {value} is not between -1 and 1")
137
+ return fvalue
138
+
139
+
140
+ def validator_positive(value):
141
+ """Is value non-negative."""
142
+ try:
143
+ fvalue = float(value)
144
+ except ValueError as exc:
145
+ raise argparse.ArgumentTypeError(f"Commandline: {value} is not a valid number") from exc
146
+ if fvalue < 0:
147
+ raise argparse.ArgumentTypeError(f"{value} is not positive")
148
+ return fvalue
149
+
150
+
151
+ def validator_mc_iterations(value):
152
+ """Is number of Monte Carlo iterations between 0 and 50."""
153
+ try:
154
+ ivalue = int(value)
155
+ except ValueError as exc:
156
+ raise argparse.ArgumentTypeError(f"Commandline: {value} is not a valid integer") from exc
157
+ if not 0 <= ivalue <= 50:
158
+ raise argparse.ArgumentTypeError(f"Commandline: {value} must be between 0 and 50")
159
+ return ivalue
160
+
161
+
162
+ # Argument specifications
163
+ arg_specs = [
164
+ {
165
+ "flags": ["-1"],
166
+ "dest": "r_sphere",
167
+ "metavar": ("SPHERE_D", "SAMPLE_D", "ENTRANCE_D", "DETECTOR_D", "WALL_R"),
168
+ "nargs": 5,
169
+ "type": float,
170
+ "help": "Reflection sphere parameters",
171
+ },
172
+ {
173
+ "flags": ["-2"],
174
+ "dest": "t_sphere",
175
+ "metavar": ("SPHERE_D", "SAMPLE_D", "ENTRANCE_D", "DETECTOR_D", "WALL_R"),
176
+ "nargs": 5,
177
+ "type": float,
178
+ "help": "Transmission sphere parameters",
179
+ },
180
+ {
181
+ "flags": ["-a", "--albedo"],
182
+ "dest": "albedo",
183
+ "type": validator_01,
184
+ "help": "Use this albedo",
185
+ },
186
+ {
187
+ "flags": ["-A", "--mua"],
188
+ "dest": "mua",
189
+ "type": validator_positive,
190
+ "help": "Use this absorption coefficient",
191
+ },
192
+ {
193
+ "flags": ["-b"],
194
+ "dest": "b",
195
+ "type": validator_positive,
196
+ "help": "Use this optical thickness",
197
+ },
198
+ {
199
+ "flags": ["-B", "--diameter"],
200
+ "dest": "diameter",
201
+ "type": validator_positive,
202
+ "help": "Beam diameter",
203
+ },
204
+ {
205
+ "flags": ["-c"],
206
+ "dest": "f_r",
207
+ "type": validator_01,
208
+ "help": "Fraction of unscattered refl in MR",
209
+ },
210
+ {
211
+ "flags": ["-C"],
212
+ "dest": "f_t",
213
+ "type": validator_01,
214
+ "help": "Fraction of unscattered trans in MT",
215
+ },
216
+ {
217
+ "flags": ["-d"],
218
+ "dest": "thickness",
219
+ "type": validator_positive,
220
+ "help": "Thickness of sample",
221
+ },
222
+ {
223
+ "flags": ["-D"],
224
+ "dest": "slide_thickness",
225
+ "type": validator_positive,
226
+ "help": "Thickness of slide",
227
+ },
228
+ {
229
+ "flags": ["-e"],
230
+ "dest": "error",
231
+ "type": validator_positive,
232
+ "help": "Error tolerance (default 0.0001)",
233
+ },
234
+ {
235
+ "flags": ["-E"],
236
+ "dest": "E",
237
+ "type": validator_positive,
238
+ "help": "Optical depth (=mua*D) for slides",
239
+ },
240
+ {
241
+ "flags": ["-f"],
242
+ "dest": "f_wall",
243
+ "type": validator_01,
244
+ "help": "Fraction 0.0-1.0 of light to hit sphere wall first",
245
+ },
246
+ {
247
+ "flags": ["-F", "--mus"],
248
+ "dest": "mus",
249
+ "type": validator_positive,
250
+ "help": "Use this scattering coefficient",
251
+ },
252
+ {"flags": ["-g"], "type": validator_11, "help": "Scattering anisotropy"},
253
+ {
254
+ "flags": ["-G"],
255
+ "type": str,
256
+ "choices": ["0", "2", "t", "b", "n", "f"],
257
+ "help": "Type of boundary ",
258
+ },
259
+ {"flags": ["-i"], "type": float, "help": "Light incident at this angle in degrees"},
260
+ {"flags": ["-M"], "type": validator_mc_iterations, "help": "Number of Monte Carlo iterations"},
261
+ {
262
+ "flags": ["-n", "--nslab"],
263
+ "dest": "nslab",
264
+ "type": validator_positive,
265
+ "help": "Index of refraction of slab",
266
+ },
267
+ {
268
+ "flags": ["-N", "--nslide"],
269
+ "dest": "nslide",
270
+ "type": validator_positive,
271
+ "help": "Index of refraction of slides",
272
+ },
273
+ {"flags": ["-o"], "dest": "out_fname", "type": str, "help": "Filename for output"},
274
+ {"flags": ["-p"], "type": int, "help": "# of Monte Carlo photons (default 100000)"},
275
+ {
276
+ "flags": ["-q"],
277
+ "type": int,
278
+ "default": 8,
279
+ "help": "Number of quadrature points (default=8)",
280
+ },
281
+ {"flags": ["-r"], "type": validator_01, "help": "Total reflection measurement"},
282
+ {
283
+ "flags": ["-R"],
284
+ "type": validator_01,
285
+ "help": "Actual reflectance for 100%% measurement",
286
+ },
287
+ {
288
+ "flags": ["-S"],
289
+ "type": int,
290
+ "choices": [0, 1, 2],
291
+ "help": "Number of spheres used",
292
+ },
293
+ {"flags": ["-t"], "type": validator_01, "help": "Total transmission measurement"},
294
+ {
295
+ "flags": ["-T"],
296
+ "type": validator_01,
297
+ "help": "Actual transmission for 100%% measurement",
298
+ },
299
+ {
300
+ "flags": ["-u"],
301
+ "type": validator_01,
302
+ "help": "Unscattered transmission measurement",
303
+ },
304
+ {
305
+ "flags": ["-v", "--version"],
306
+ "action": "version",
307
+ "version": "iadp " + iadpython.__version__,
308
+ "help": "short version",
309
+ },
310
+ {"flags": ["-V"], "action": "store_true", "help": "long version"},
311
+ {
312
+ "flags": ["--verbosity"],
313
+ "type": int,
314
+ "choices": [0, 1, 2],
315
+ "help": "Verbosity level",
316
+ },
317
+ {"flags": ["-x"], "type": int, "help": "Set debugging level"},
318
+ {"flags": ["-X"], "action": "store_true", "help": "Dual beam configuration"},
319
+ {"flags": ["-z"], "action": "store_true", "help": "Do forward calculation"},
320
+ {
321
+ "flags": ["filename"],
322
+ "nargs": "?",
323
+ "type": str,
324
+ "default": None,
325
+ "help": "Input filename",
326
+ },
327
+ ]
328
+
329
+
330
+ def print_long_version():
331
+ """Print the version information and quit."""
332
+ s = ""
333
+ s += " version: " + iadpython.__version__ + "\n"
334
+ s += " Author: " + iadpython.__author__ + "\n"
335
+ s += " Copyright: " + iadpython.__copyright__ + "\n"
336
+ s += " License: " + iadpython.__license__ + "\n"
337
+ s += " URL:" + iadpython.__url__ + "\n"
338
+ s += "\n Forward and inverse adding-doubling radiative transport calculations.\n"
339
+ s += " Extensive documentation is at <https://iadpython.readthedocs.io>\n"
340
+ print(s)
341
+ sys.exit(0)
342
+
343
+
344
+ def example_text():
345
+ """Return a string with some command-line examples."""
346
+ s = ""
347
+ s += "Examples:\n"
348
+ s += " iad file.rxt Results will be put in file.txt\n"
349
+ s += " iad file Same as above\n"
350
+ s += " iad -c 0.9 file.rxt Assume M_R includes 90%% of unscattered reflectance\n"
351
+ s += " iad -C 0.8 file.rxt Assume M_T includes 80%% of unscattered transmittance\n"
352
+ s += " iad -e 0.0001 file.rxt Better convergence to R & T values\n"
353
+ s += " iad -f 1.0 file.rxt All light hits reflectance sphere wall first\n"
354
+ s += " iad -o out file.rxt Calculated values in out\n"
355
+ s += " iad -r 0.3 R_total=0.\n"
356
+ return s
357
+
358
+
359
+ def add_sample_constraints(exp, args):
360
+ """Command-line constraints on sample."""
361
+ if args.thickness is not None:
362
+ exp.sample.d = args.thickness
363
+
364
+ if args.slide_thickness is not None:
365
+ exp.sample.d_above = args.slide_thickness
366
+ exp.sample.d_below = args.slide_thickness
367
+
368
+ if args.E is not None:
369
+ exp.sample.b_above = args.E
370
+ exp.sample.b_below = args.E
371
+
372
+ if args.nslab is not None:
373
+ exp.sample.n = args.nslab
374
+
375
+ if args.nslide is not None:
376
+ exp.sample.n_above = args.nslide
377
+ exp.sample.n_below = args.nslide
378
+
379
+ if args.G is not None:
380
+ if args.nslide is None:
381
+ raise argparse.ArgumentTypeError("Commandline: Cannot use -G without also specifing slide index with -N")
382
+ if args.G == "0":
383
+ exp.sample.n_above = 1
384
+ exp.sample.n_below = 1
385
+ elif args.G == "t":
386
+ exp.sample.n_above = args.nslide
387
+ exp.sample.n_below = 1
388
+ elif args.G == "b":
389
+ exp.sample.n_above = 1
390
+ exp.sample.n_below = args.nslide
391
+ elif args.G == "2":
392
+ exp.sample.n_above = args.nslide
393
+ exp.sample.n_below = args.nslide
394
+ elif args.G == "n":
395
+ exp.flip_sample = True
396
+ exp.sample.n_above = args.nslide
397
+ exp.sample.n_below = 1
398
+ else: # must be 'f'
399
+ exp.flip_sample = True
400
+ exp.sample.n_above = 1
401
+ exp.sample.n_below = args.nslide
402
+
403
+ if args.i is not None:
404
+ if abs(args.i) > 90:
405
+ raise argparse.ArgumentTypeError("Commandline: Bad argument to -i, value must be between -90 and 90")
406
+ exp.sample.nu_0 = np.cos(np.radians(args.i))
407
+
408
+ if args.q is not None:
409
+ if args.q % 4:
410
+ raise argparse.ArgumentTypeError("Commandline: Number of quadrature points must be a multiple of 4")
411
+ if exp.sample.nu_0 != 1 and args.q % 12:
412
+ raise argparse.ArgumentTypeError("Commandline: Quadrature must be 12, 24, 36,... for oblique incidence")
413
+ exp.sample.quad_pts = args.q
414
+
415
+
416
+ def add_experiment_constraints(exp, args):
417
+ """Command-line constraints on experiment."""
418
+
419
+ def cli_sphere_to_object(values, refl):
420
+ """Convert `-1`/`-2` CLI sphere values to a Sphere object."""
421
+ sphere_d, sample_d, entrance_d, detector_d, wall_r = values
422
+ return iadpython.Sphere(
423
+ sphere_d,
424
+ sample_d,
425
+ d_third=entrance_d,
426
+ d_detector=detector_d,
427
+ r_wall=wall_r,
428
+ refl=refl,
429
+ )
430
+
431
+ if args.S is not None:
432
+ exp.num_spheres = args.S
433
+
434
+ if args.r_sphere is not None:
435
+ exp.r_sphere = cli_sphere_to_object(args.r_sphere, refl=True)
436
+
437
+ if args.t_sphere is not None:
438
+ exp.t_sphere = cli_sphere_to_object(args.t_sphere, refl=False)
439
+
440
+ if args.diameter is not None:
441
+ exp.d_beam = args.diameter
442
+
443
+ if args.r is not None:
444
+ exp.m_r = args.r
445
+
446
+ if args.t is not None:
447
+ exp.m_t = args.t
448
+
449
+ if args.u is not None:
450
+ exp.m_u = args.u
451
+
452
+ rstd_r = getattr(args, "R", None)
453
+ rstd_t = getattr(args, "T", None)
454
+ if rstd_r is not None:
455
+ if exp.r_sphere is not None:
456
+ exp.r_sphere.r_std = rstd_r
457
+ if exp.t_sphere is not None and rstd_t is None:
458
+ exp.t_sphere.r_std = rstd_r
459
+
460
+ if rstd_t is not None:
461
+ if exp.t_sphere is not None:
462
+ exp.t_sphere.r_std = rstd_t
463
+ if exp.r_sphere is not None and rstd_r is None:
464
+ exp.r_sphere.r_std = rstd_t
465
+
466
+
467
+ def add_analysis_constraints(exp, args):
468
+ """Add command line constraints on analysis."""
469
+ # constraints on analysis
470
+ if args.albedo is not None:
471
+ exp.default_a = args.albedo
472
+
473
+ if args.b is not None:
474
+ exp.default_b = args.b
475
+
476
+ if args.g is not None:
477
+ exp.default_g = args.g
478
+
479
+ if args.mua is not None:
480
+ exp.default_mua = args.mua
481
+
482
+ if args.mus is not None:
483
+ exp.default_mus = args.mus
484
+
485
+ if args.f_r is not None:
486
+ exp.fraction_of_rc_in_mr = args.f_r
487
+
488
+ if args.f_t is not None:
489
+ exp.fraction_of_tc_in_mt = args.f_t
490
+
491
+ if getattr(args, "f_wall", None) is not None:
492
+ exp.f_r = args.f_wall
493
+
494
+ if getattr(args, "error", None) is not None:
495
+ exp.tolerance = args.error
496
+ exp.MC_tolerance = args.error
497
+
498
+ if args.M is not None:
499
+ exp.max_mc_iterations = args.M
500
+
501
+ if args.p is not None:
502
+ exp.n_photons = args.p
503
+
504
+ if getattr(args, "x", None) is not None:
505
+ exp.debug_level = args.x
506
+
507
+ if getattr(args, "X", False):
508
+ exp.method = "comparison"
509
+
510
+
511
+ def forward_calculation(exp):
512
+ """Do a forward calculation."""
513
+ # set albedo
514
+ if exp.default_a is None:
515
+ if exp.default_mus is None:
516
+ exp.sample.a = 0
517
+ elif exp.default_mua is None:
518
+ exp.sample.a = 1
519
+ else:
520
+ exp.sample.a = exp.default_mus / (exp.default_mua + exp.default_mus)
521
+ else:
522
+ exp.sample.a = exp.default_a
523
+
524
+ # set optical thickness
525
+ if exp.default_b is None:
526
+ if exp.sample.d is None:
527
+ exp.sample.b = float("inf")
528
+ elif exp.sample.a == 0:
529
+ if exp.default_mua is None:
530
+ exp.sample.b = float("inf")
531
+ else:
532
+ exp.sample.b = exp.default_mua * exp.sample.d
533
+ elif exp.default_mus is None:
534
+ exp.sample.b = float("inf")
535
+ else:
536
+ exp.sample.b = exp.default_mus / exp.sample.a * exp.sample.d
537
+ else:
538
+ exp.sample.b = exp.default_b
539
+
540
+ # set scattering anisotropy
541
+ if exp.default_g is None:
542
+ exp.sample.g = 0
543
+ else:
544
+ exp.sample.g = exp.default_g
545
+
546
+ print(exp.sample)
547
+
548
+ ur1, ut1, _uru, _utu = exp.sample.rt()
549
+ ru, tu = exp.sample.unscattered_rt()
550
+ print("Calculated quantities")
551
+ print(" R total = %.3f" % ur1)
552
+ print(" R scattered = %.3f" % (ur1 - ru))
553
+ print(" R unscattered = %.3f" % ru)
554
+ print(" T total = %.3f" % ut1)
555
+ print(" T scattered = %.3f" % (ut1 - tu))
556
+ print(" T unscattered = %.3f" % tu)
557
+ sys.exit(0)
558
+
559
+
560
+ def print_results_header(debug_lost_light=False):
561
+ """Print the header for results to stdout."""
562
+ print(
563
+ "# \tMeasured \t M_R \tMeasured \t M_T \tEstimated\tEstimated\tEstimated",
564
+ end="",
565
+ )
566
+ if debug_lost_light:
567
+ print(
568
+ "\t Lost \t Lost \t Lost \t Lost \t MC \t IAD \t Error ",
569
+ end="",
570
+ )
571
+ print()
572
+
573
+ print(
574
+ "##wave\t M_R \t fit \t M_T \t fit \t mu_a \t mu_s' \t g ",
575
+ end="",
576
+ )
577
+ if debug_lost_light:
578
+ print(
579
+ "\t UR1 \t URU \t UT1 \t UTU \t # \t # \t State ",
580
+ end="",
581
+ )
582
+ print()
583
+
584
+ print(
585
+ "# [nm]\t [---] \t [---] \t [---] \t [---] \t 1/mm \t 1/mm \t [---] ",
586
+ end="",
587
+ )
588
+ if debug_lost_light:
589
+ print(
590
+ "\t [---] \t [---] \t [---] \t [---] \t [---] \t [---] \t [---] ",
591
+ end="",
592
+ )
593
+ print()
594
+
595
+
596
+ def invert_file(exp, args):
597
+ """Process an entire .rxt file."""
598
+ # determine output file name
599
+ if args.out_fname is None:
600
+ root, ext = os.path.splitext(args.filename)
601
+ if ext == ".rxt":
602
+ args.out_fname = root + ".txt"
603
+ else:
604
+ args.out_fname = args.filename + ".txt"
605
+
606
+ with open(args.out_fname, "w", encoding="utf-8") as output_stream, redirect_stdout(output_stream):
607
+ a, b, g = exp.invert_rt()
608
+
609
+ print_results_header()
610
+ for i, (a_i, b_i, g_i) in enumerate(zip(a, b, g)):
611
+ exp.sample.a = a_i
612
+ exp.sample.b = b_i
613
+ exp.sample.g = g_i
614
+
615
+ mr, mt = exp.measured_rt()
616
+
617
+ wavelength = None
618
+ if exp.lambda0 is not None:
619
+ wavelength = exp.lambda0 if np.isscalar(exp.lambda0) else exp.lambda0[i]
620
+
621
+ if wavelength not in (None, 0):
622
+ print("%6.1f" % wavelength, end="\t")
623
+ else:
624
+ print("%6d" % i, end="\t")
625
+
626
+ if exp.m_r is not None:
627
+ print("% 9.4f" % exp.m_r[i], end="\t")
628
+ else:
629
+ print("% 9.4f" % 0, end="\t")
630
+ print("% 9.4f" % mr, end="\t")
631
+
632
+ if exp.m_t is not None:
633
+ print("% 9.4f" % exp.m_t[i], end="\t")
634
+ else:
635
+ print("% 9.4f" % 0, end="\t")
636
+
637
+ print("% 9.4f" % mt, end="\t")
638
+
639
+ # print("% 9.4f" % exp.sample.a, end='\t')
640
+ # print("% 9.4f" % exp.sample.b, end='\t')
641
+ print("% 9.4f" % exp.sample.mu_a(), end="\t")
642
+ print("% 9.4f" % exp.sample.mu_sp(), end="\t")
643
+ print("% 9.4f" % exp.sample.g, end="\t")
644
+ print()
645
+ sys.exit(0)
646
+
647
+
648
+ def main():
649
+ """Main command-line interface."""
650
+ parser = argparse.ArgumentParser(
651
+ description="iad command line program",
652
+ formatter_class=argparse.RawTextHelpFormatter,
653
+ epilog=example_text(),
654
+ )
655
+ for spec in arg_specs:
656
+ flags = spec["flags"]
657
+ other_args = {k: v for k, v in spec.items() if k != "flags"}
658
+ parser.add_argument(*flags, **other_args)
659
+
660
+ try:
661
+ args = parser.parse_args()
662
+
663
+ if args.V:
664
+ print_long_version()
665
+
666
+ # If there is a file then read it, otherwise create a blank experiment
667
+ if args.filename:
668
+ exp = iadpython.read_rxt(args.filename)
669
+ else:
670
+ exp = iadpython.Experiment()
671
+
672
+ # update the search to include the command line constraints
673
+ add_sample_constraints(exp, args)
674
+ add_experiment_constraints(exp, args)
675
+ add_analysis_constraints(exp, args)
676
+
677
+ # follow iad behavior: sphere measurements default to substitution mode
678
+ if np.isscalar(exp.num_spheres) and exp.num_spheres > 0 and getattr(exp, "method", "unknown") == "unknown":
679
+ exp.method = "substitution"
680
+
681
+ if getattr(exp, "method", "unknown") in ("comparison", 1):
682
+ if np.isscalar(exp.num_spheres) and exp.num_spheres == 2:
683
+ raise argparse.ArgumentTypeError(
684
+ "Commandline: Dual beam (-X) cannot be used with double integrating spheres."
685
+ )
686
+ if getattr(exp, "f_r", 0.0) != 0:
687
+ raise argparse.ArgumentTypeError("Commandline: Dual beam (-X) cannot be used when -f is non-zero.")
688
+
689
+ if args.z:
690
+ forward_calculation(exp)
691
+
692
+ if args.filename:
693
+ invert_file(exp, args)
694
+
695
+ print(exp)
696
+ if (exp.m_r is None) and (exp.m_t is None) and (exp.m_u is None):
697
+ raise argparse.ArgumentTypeError("Commandline: One measurement needed or use '-z' for forward calc.")
698
+
699
+ # invert parameters specified on the commandline
700
+ a, b, g = exp.invert_rt()
701
+ print(" a = %.3f" % a)
702
+ print(" b = %.3f" % b)
703
+ print(" g = %.3f" % g)
704
+ sys.exit(0)
705
+
706
+ except (argparse.ArgumentTypeError, OSError, ValueError, RuntimeError, TypeError) as e:
707
+ print(f"Error: {e}")
708
+ sys.exit(1)
709
+
710
+
711
+ if __name__ == "__main__":
712
+ main()