xara-prism 0.0.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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: xara_prism
3
+ Version: 0.0.0
4
+ Requires-Dist: xsection
5
+ Requires-Dist: xara
6
+ Requires-Dist: numpy
7
+ Requires-Dist: scipy
8
+ Requires-Dist: matplotlib
@@ -0,0 +1,26 @@
1
+
2
+ [project]
3
+ name = "xara_prism"
4
+ version = "0.0.0"
5
+ dependencies = [
6
+ "xsection",
7
+ "xara",
8
+ "numpy",
9
+ "scipy",
10
+ "matplotlib"
11
+ ]
12
+
13
+ [build-system]
14
+ requires = [
15
+ "setuptools >= 52.0.2",
16
+ ]
17
+
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [tool.setuptools]
21
+ package-dir = {"" = "src"}
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,576 @@
1
+ import numpy as np
2
+ import xara
3
+ from xara.helpers import find_node
4
+
5
+
6
+ class BasicShape(dict):
7
+ materials = {}
8
+
9
+
10
+ class Span:
11
+ def __init__(self,
12
+ length,
13
+ shape,
14
+ shear,
15
+ loads,
16
+ smax=1.0,
17
+ rotate=None,
18
+ units=None,
19
+ # Internal
20
+ _span_name=None,
21
+ ):
22
+ self.length = length
23
+ self.shape = shape
24
+ self.shear = shear
25
+ self.units = units
26
+ self.smax = float(smax)
27
+
28
+ if not isinstance(loads, list):
29
+ loads = [loads]
30
+ self.loads = loads
31
+
32
+ self._span_name = _span_name
33
+
34
+ if rotate is None:
35
+ rotate = np.eye(3)
36
+ self._rotate = rotate
37
+
38
+
39
+ self.e_long = np.array([0, 1, 0]) # Global longitudinal axis
40
+ self.e_tran = np.array([1, 0, 0]) # Global transverse axis
41
+ self.e_bend = np.array([0, 0, 1]) # Global bending axis
42
+
43
+ self.i_long = np.array([1, 0, 0])
44
+ self.i_tran = np.array([0, 0, 1]) # local z
45
+ self.i_bend = np.array([0, 1, 0]) # local y
46
+
47
+ self._elastic_section = xara.FrameSection("Elastic", self.shape)
48
+
49
+ def plane_properties(self, iv, im):
50
+ section = self._elastic_section
51
+ cnn = section.cnn()
52
+ cmm = section.cmm()
53
+ cmn = section.cmn()
54
+ EA = cnn[0][0]
55
+ GJ = cmm[0][0]
56
+ EI = float(im.T@cmm@im)
57
+ GA = float(iv.T@cnn@iv)
58
+
59
+ return EA, GA, EI
60
+
61
+ def solution(self, type, time=None, shear=None, **kwds):
62
+ if shear is None:
63
+ shear = self.shear
64
+
65
+ from .solutions.linear import LinearPlaneSolution
66
+ from .solutions.pdelta import PDelta
67
+
68
+ if type == "linear" and self._span_name in {"span1", "span2", "span3", "span4", "span5"}:
69
+ return LinearPlaneSolution(span=self._span_name, prism=self, shear=shear, **kwds)
70
+
71
+ elif type == "pdelta" and self._span_name in {"span1", "span2", "span3", "span4", "span5"}:
72
+ return PDelta(span=self._span_name, prism=self, shear=shear, **kwds)
73
+
74
+ raise NotImplementedError("Solution not implemented for this span type.")
75
+
76
+
77
+ def create_prism(self, ndm,
78
+ element,
79
+ transform,
80
+ section="Elastic",
81
+ ne=1,
82
+ nen=2,
83
+ echo_file=None,
84
+ options=None):
85
+ model = xara.Model(ndm=ndm, echo_file=echo_file)
86
+ # nen = order + 1
87
+ nn = ne*(nen-1)+1
88
+ shear = self.shear
89
+ if options is None:
90
+ options = {}
91
+
92
+ if section == "Elastic":
93
+ section = self._elastic_section
94
+ else:
95
+ section = xara.FrameSection(section, self.shape)
96
+ model.section(section)
97
+
98
+ for i, x in enumerate(np.linspace(0, self.length, nn)):
99
+ model.node(i+1, 0, x, 0)
100
+
101
+
102
+ model.geomTransf(transform, 1, (1, 0, 0))
103
+
104
+ geometry = None
105
+ for i in range(ne):
106
+ start = i * (nen - 1) + 1
107
+ nodes = list(range(start, start + nen))
108
+ model.element(element, i+1,
109
+ nodes,
110
+ section=1,
111
+ shear=shear,
112
+ transform=1,
113
+ **options)
114
+ return model
115
+
116
+ def analyze(self,
117
+ ndm,
118
+ element,
119
+ transform,
120
+ section="Elastic",
121
+ ne=1,
122
+ nen=2,
123
+ steps=None,
124
+ echo_file=None,
125
+ analysis_options: dict =None,
126
+ element_options : dict =None,
127
+ loading_options : dict =None):
128
+
129
+ from .analysis import AnalysisOutput
130
+
131
+ model = self._create_model(ndm, element, transform, section=section,
132
+ ne=ne, nen=nen,
133
+ echo_file=echo_file,
134
+ options=element_options)
135
+
136
+ if loading_options is None:
137
+ loading_options = {}
138
+
139
+ #
140
+ # 1. Analyze constant loads if present
141
+ #
142
+ if self.apply_loads(model, scale_type="constant", **loading_options) > 0:
143
+ model.integrator("LoadControl", 1/10)
144
+ model.system("BandGeneral")
145
+ model.test("NormUnbalance", 1e-8, 100, 0)
146
+ model.analysis("Static")
147
+ assert model.analyze(10) == 0
148
+ model.loadConst(time=0)
149
+
150
+ #
151
+ # 2. Analyze linear loads if present
152
+ #
153
+ if analysis_options is None:
154
+ analysis_options = {"hooks": [], "system": "BandGeneral"}
155
+
156
+ if "integrator" not in analysis_options:
157
+ if steps is None:
158
+ steps = 10
159
+ dlam = self.smax/steps if steps != 0 else 1
160
+ analysis_options["integrator"] = ("LoadControl", dlam)
161
+
162
+ if "test" not in analysis_options:
163
+ analysis_options["test"] = ("NormUnbalance", 1e-8, 100, 0)
164
+
165
+ if "hooks" not in analysis_options:
166
+ analysis_options["hooks"] = []
167
+
168
+ if "system" not in analysis_options:
169
+ analysis_options["system"] = "BandGeneral"
170
+
171
+ output = AnalysisOutput(self, model, label=f"{element}/{transform}, ne={ne}, nen={nen}")
172
+ analysis_options["hooks"].append(output.create_hook())
173
+ analysis_options["hooks"][-1](model)
174
+ analysis = xara.StaticAnalysis(model, **analysis_options)
175
+ output.analysis = analysis
176
+ if self.apply_loads(model, scale_type="linear", **loading_options) > 0:
177
+ for _ in range(steps):
178
+ analysis.analyze(1)
179
+ return output
180
+
181
+
182
+ def _create_model(self, ndm, element, transform, section="Elastic", ne=1, nen=2, **kwds):
183
+ model = self.create_prism(ndm, element, transform, section=section, ne=ne, nen=nen, **kwds)
184
+ i_node = find_node(model, y=0)
185
+ j_node = find_node(model, y=self.length)
186
+ self._fix_nodes(model, i_node, j_node)
187
+ return model
188
+
189
+ def _fix_nodes(self, model, i_node: int, j_node: int):
190
+ raise NotImplementedError("Subclasses must implement the _fix_nodes method.")
191
+
192
+
193
+ def apply_loads(self, model, scale_type, **kwargs):
194
+ loads = []
195
+ n_loads = 0
196
+ for load in self.loads:
197
+ if scale_type == load._scale_type:
198
+ n_loads += 1
199
+ xara_load = load.create(self, model, **kwargs)
200
+ if xara_load is None:
201
+ # When creating legacy opensees-type loads,
202
+ # the load must be created after a pattern is created;
203
+ # This will have been done entirely within load.create()
204
+ continue
205
+ loads.append(xara_load)
206
+
207
+ if len(loads) > 0:
208
+ model.pattern(
209
+ xara.StaticPattern(loads)
210
+ )
211
+ return n_loads
212
+
213
+
214
+
215
+
216
+ class Span1(Span):
217
+ # Cantilever with tip loads
218
+ def __init__(self, length, shape, shear, loads, smax=1.0, **kwds):
219
+ super().__init__(length,
220
+ shape,
221
+ shear,
222
+ loads,
223
+ smax=smax,
224
+ _span_name="span1",
225
+ **kwds
226
+ )
227
+
228
+ def _fix_nodes(self, model, i_node, j_node):
229
+ model.fix(i_node, (1, 1, 1, 1, 1, 1))
230
+ model.fix(j_node, (0, 0, 0, 0, 0, 0))
231
+
232
+ def critical_load(self, iv, im, mode=1):
233
+ """
234
+ Positive magnitude of the cantilever Euler load for odd mode number:
235
+
236
+ Pcr_j = ((2*j - 1)*pi/2L)^2 EI, j = 1,2,...
237
+
238
+ """
239
+ mode = int(mode)
240
+ L = self.length
241
+ if mode < 1:
242
+ raise ValueError("mode must be >= 1")
243
+ q = (2 * mode - 1) * np.pi / (2.0 * L)
244
+ _, GA, EI = self.plane_properties(iv, im)
245
+ return EI * q**2
246
+
247
+
248
+ class Span2(Span):
249
+ # Encastre beam with transverse deflection at L
250
+ def __init__(self, length, shape, shear, loads, smax=1.0, **kwds):
251
+ super().__init__(length,
252
+ shape,
253
+ shear,
254
+ loads,
255
+ smax=smax,
256
+ _span_name="span2",
257
+ **kwds
258
+ )
259
+
260
+ def _fix_nodes(self, model, i_node, j_node):
261
+ model.fix(i_node, (1, 1, 1, 1, 1, 1))
262
+ model.fix(j_node, (0, 0, 1, 1, 1, 1))
263
+
264
+
265
+ class Span3(Span):
266
+ # Simply supported beam
267
+ def __init__(self, length, shape, shear, loads, smax=1.0, **kwds):
268
+ super().__init__(length,
269
+ shape,
270
+ shear,
271
+ loads,
272
+ smax=smax,
273
+ **kwds,
274
+ _span_name="span3"
275
+ )
276
+
277
+ def _fix_nodes(self, model, i_node, j_node):
278
+ model.fix(i_node, (1, 1, 1, 1, 1, 0))
279
+ model.fix(j_node, (1, 0, 1, 1, 1, 0))
280
+
281
+
282
+ class Span4(Span):
283
+ # Fixed-Fixed
284
+ def __init__(self, length, shape, shear, loads, **kwds):
285
+ super().__init__(length,
286
+ shape,
287
+ shear,
288
+ loads,
289
+ _span_name="span4",
290
+ **kwds
291
+ )
292
+
293
+ def _fix_nodes(self, model, i_node, j_node):
294
+ model.fix(i_node, (1, 1, 1, 1, 1, 1))
295
+ model.fix(j_node, (1, 0, 1, 1, 1, 1))
296
+
297
+
298
+ class Span5(Span):
299
+ # Propped column
300
+ def __init__(self, length, shape, shear, loads, **kwds):
301
+ super().__init__(length,
302
+ shape,
303
+ shear,
304
+ loads,
305
+ _span_name="span5",
306
+ **kwds
307
+ )
308
+
309
+ def _fix_nodes(self, model, i_node, j_node):
310
+ model.fix(i_node, (1, 1, 1, 1, 1, 1))
311
+ model.fix(j_node, (1, 0, 1, 1, 1, 0))
312
+
313
+
314
+ from xsection.library import from_aisc
315
+
316
+
317
+ class AISC_360_C1(Span3):
318
+ def __init__(self, shear, smax=1.0):
319
+ import xara.units.iks as units
320
+ from .loading import UniformShear, AxialForce, ShearForce
321
+ L = 336*units.inch
322
+ material = xara.MultiaxialMaterial(
323
+ "ElasticIsotropic",
324
+ E=29000*units.ksi,
325
+ G=11200*units.ksi,
326
+ )
327
+
328
+ super().__init__(
329
+ length=L,
330
+ shape=from_aisc("W14x48", material=material),
331
+ shear=shear,
332
+ loads=[
333
+ UniformShear(0.2*units.kip/units.foot, scale="constant"),
334
+ AxialForce(-450*units.kip, x=L, scale="linear"),
335
+ ],
336
+ smax=smax,
337
+ units=units
338
+ )
339
+
340
+ def solution(self, type, time=None, shear=None):
341
+ if shear is None:
342
+ shear = self.shear
343
+ if type == "aisc" and not shear:
344
+ from .solutions.aisc25 import AISC_25_S3
345
+ return AISC_25_S3(self, time=time, shear=shear)
346
+ else:
347
+ return super().solution(type, time=time, shear=shear)
348
+
349
+
350
+ class AISC_360_C2(Span1):
351
+ def __init__(self, shear, smax=1.0, **kwds):
352
+ import xara.units.iks as units
353
+ from .loading import AxialForce, ShearForce
354
+ L = 336*units.inch
355
+ material = xara.MultiaxialMaterial(
356
+ "ElasticIsotropic",
357
+ E=29000*units.ksi,
358
+ G=11200*units.ksi,
359
+ )
360
+
361
+ super().__init__(
362
+ length=L,
363
+ shape=from_aisc("W14x48", material=material),
364
+ shear=shear,
365
+ units=units,
366
+ loads=[
367
+ ShearForce(1*units.kip, x=L, scale="constant"),
368
+ AxialForce(-200*units.kip, x=L, scale="linear"),
369
+ ],
370
+ smax=smax,
371
+ **kwds
372
+ )
373
+
374
+ def solution(self, type, time=None, shear=None):
375
+ if shear is None:
376
+ shear = self.shear
377
+
378
+ if type == "aisc" and not shear:
379
+ from .solutions.aisc25 import AISC_25_S1
380
+ return AISC_25_S1(self, time=time, shear=shear)
381
+ else:
382
+ return super().solution(type, time=time)
383
+
384
+
385
+ class AISC_D25_C2(Span2):
386
+ def __init__(self, shear, smax=1.0, **kwds):
387
+ import xara.units.iks as units
388
+ from .loading import AxialForce, ShearForce
389
+ L = 336*units.inch
390
+ material = xara.MultiaxialMaterial(
391
+ "ElasticIsotropic",
392
+ E=29000*units.ksi,
393
+ G=11200*units.ksi,
394
+ )
395
+
396
+ super().__init__(
397
+ length=L,
398
+ shape=from_aisc("W14x48", material=material),
399
+ shear=shear,
400
+ units=units,
401
+ loads=[
402
+ ShearForce(1*units.kip, x=L, scale="constant"),
403
+ AxialForce(-200*units.kip, x=L, scale="linear"),
404
+ ],
405
+ smax=smax,
406
+ **kwds
407
+ )
408
+
409
+
410
+ def AISC_25(loads, span_type, shear=1, smax=1.0, slenderness=None, **kwds):
411
+
412
+ import xara.units.iks as units
413
+
414
+ material = xara.MultiaxialMaterial(
415
+ "ElasticIsotropic",
416
+ E=29000*units.ksi,
417
+ G=11200*units.ksi,
418
+ )
419
+ shape = from_aisc("W14x48", material=material)
420
+
421
+ if slenderness is None:
422
+ length = 336*units.inch
423
+ else:
424
+ length = slenderness * shape.d
425
+
426
+ if span_type == "span1":
427
+ span = Span1
428
+ elif span_type == "span2":
429
+ span = Span2
430
+ elif span_type == "span3":
431
+ span = Span3
432
+ elif span_type == "span4":
433
+ span = Span4
434
+ elif span_type == "span5":
435
+ span = Span5
436
+ else:
437
+ raise ValueError(f"Unknown span type: {span_type}")
438
+
439
+ return span(
440
+ length=length,
441
+ shape=shape,
442
+ shear=shear,
443
+ units=units,
444
+ loads=loads,
445
+ smax=smax,
446
+ **kwds
447
+ )
448
+
449
+ class PlotPoint:
450
+ def __init__(self,
451
+ prism,
452
+ x,
453
+ y,
454
+ loc,
455
+ ax=None,
456
+ time=None):
457
+ self.prism = prism
458
+ self.x_key = x
459
+ self.y_key = y
460
+ self.loc = loc
461
+ if time is None:
462
+ time = np.linspace(0, prism.smax, 100)
463
+ self.time = time
464
+ if ax is not None:
465
+ self.ax = ax
466
+ self.fig = ax.figure
467
+ else:
468
+ from matplotlib import pyplot as plt
469
+ self.fig, self.ax = plt.subplots()
470
+ self.ax.grid(True)
471
+
472
+
473
+ def add(self, obj, **kwds):
474
+ x = self._collect(obj, self.x_key)
475
+ y = self._collect(obj, self.y_key)
476
+ self.ax.plot(x, y, **kwds)
477
+
478
+ def _collect(self, obj, var):
479
+ if var == "time":
480
+ if obj.time is not None:
481
+ return obj.time
482
+ return self.time
483
+
484
+ return [
485
+ getattr(obj, var)(self.loc, time) for time in self._collect(obj, var="time")
486
+ ]
487
+
488
+ class PlotSpan:
489
+ def __init__(self, prism, time=1, space=None, scale=1, ax=None):
490
+ self.prism = prism
491
+ self.time = time
492
+ self.scale = scale
493
+ small = 1e-3 * prism.length
494
+ self._x_lim = (-small, small)
495
+ if space is None:
496
+ space = np.linspace(0, prism.length, 100)
497
+ self.space = space
498
+ import matplotlib.pyplot as plt
499
+ if ax is None:
500
+ _, self.ax = plt.subplots()
501
+ # self.ax.axis("equal")
502
+ self.ax.plot([0]*len(space), space, color="k", lw=0.5)
503
+ else:
504
+ self.ax = ax
505
+
506
+ def add(self, obj, **kwds):
507
+ if obj.space is None:
508
+ space = self.space
509
+ else:
510
+ # import pprint
511
+ # pprint.pprint(obj.data)
512
+ space = obj.space
513
+
514
+ x = [xi + self.scale*getattr(obj, "u_long")(xi, self.time) for xi in space]
515
+ y = [self.scale*getattr(obj, "u_tran")(x, self.time) for x in space]
516
+
517
+ e = 1.2
518
+ self._x_lim = (min(self._x_lim[0], min(y)*e), max(self._x_lim[1], max(y)*e))
519
+ self.ax.plot(y, x, **kwds)
520
+ # self.ax.plot(space, y, **kwds)
521
+
522
+ def finish(self):
523
+ self.ax.set_xlim(self._x_lim)
524
+
525
+
526
+ def check_span(obj, sol, time=1, space=None, rtol=None, atol=None):
527
+ if time is None:
528
+ time = obj.time
529
+ time = map(float, np.atleast_1d(time))
530
+
531
+ if space is None:
532
+ space = obj.space
533
+
534
+ print(obj, sol)
535
+ print(f"{'time':>6} {'x':>6} │ {'u_long_out':>10} {'u_long_sol':>10} {'diff':>10} │ {'u_tran_out':>10} {'u_tran_sol':>10} {'diff':>10}")
536
+ print(f"{'─'*6:>6} {'─':>6} │ {'─'*10 :>10} {'─'*10 :>10} {'─'*10:>10}─┼ {'─'*10 :>10} {'─'*10 :>10} {'─'*10:>10}")
537
+ for t in time:
538
+ print(f"{t:6.3f}", end="\n" if len(space) > 1 else "")
539
+ pad_time = f"{' '*6:>6}" if len(space) > 1 else ""
540
+ for xi in space:
541
+ print(f"{pad_time} {xi:>6.0f}", end=" │ ")
542
+ for key in ["u_long", "u_tran"]:
543
+ u_out = getattr(obj, key)(xi, t)
544
+ u_sol = getattr(sol, key)(xi, t)
545
+ diff = u_out - u_sol
546
+ if atol is not None:
547
+ assert abs(diff) < atol[key], f"{key} at x={xi} has absolute error {diff} > {atol[key]}"
548
+ if u_sol != 0:
549
+ diff /= u_sol
550
+ if rtol is not None:
551
+ assert abs(diff) < rtol[key], f"{key} at x={xi} has relative error {diff} > {rtol[key]}"
552
+ print(f"{u_out:>10.4f} {u_sol:>10.4f} {diff:>10.3e}", end=" │ ")
553
+ try:
554
+ print(f" {obj.ic(t):>6}", end="")
555
+ except:
556
+ pass
557
+ print()
558
+ print()
559
+
560
+
561
+ if __name__ == "__main__":
562
+
563
+ p = AISC_360_C1(shear=1)
564
+
565
+ # plot = PlotPoint(p,
566
+ # x="u_tran",
567
+ # y="time",
568
+ # loc=p.length
569
+ # )
570
+
571
+ plot = PlotSpan(p, var="u_tran", time=1, space=None)
572
+
573
+ # plot.add(p.simulation(3, "PrismFrame", ..., n=...))
574
+
575
+ plot.add(p.solution("linear"))
576
+
@@ -0,0 +1,98 @@
1
+ import xara
2
+ import numpy as np
3
+
4
+ class XaraAnalysis:
5
+ def __init__(self, span,
6
+ element: str,
7
+ transform: str,
8
+ section: str = "ElasticFrame",
9
+ element_options=None,
10
+ loading_options=None,
11
+ analysis_options=None,
12
+ ):
13
+ self.span = span
14
+ self._element = element
15
+ self._transform = transform
16
+ self._section = section
17
+ self._element_options = element_options or {}
18
+ self._loading_options = loading_options or {}
19
+ self._analysis_options = analysis_options or {}
20
+
21
+
22
+
23
+ class AnalysisOutput:
24
+ def __init__(self, span, model: xara.Model, label=None, analysis=None):
25
+ self.prism = span
26
+ self.model = model
27
+ self.label = label
28
+ self.analysis = analysis
29
+ self.space = np.array([model.nodeCoord(node, 2) for node in model.getNodeTags()])
30
+
31
+ self.data = {
32
+ "u_tran": [],
33
+ "u_long": [],
34
+ "r_plan": [],
35
+ "ic": [],
36
+ "time": [],
37
+ }
38
+ def __repr__(self):
39
+ return f"{self.__class__.__name__}({self.label})"
40
+
41
+ @property
42
+ def time(self):
43
+ return np.asarray(self.data["time"], dtype=float)
44
+
45
+ def create_hook(self):
46
+ def hook(model):
47
+ self.data["time"].append(model.getTime())
48
+
49
+ self.data["u_tran"].append([
50
+ model.nodeDisp(node, 1)
51
+ for node in model.getNodeTags()
52
+ ])
53
+ self.data["u_long"].append([
54
+ model.nodeDisp(node, 2)
55
+ for node in model.getNodeTags()
56
+ ])
57
+ self.data["r_plan"].append([
58
+ model.nodeDisp(node, 6)
59
+ for node in model.getNodeTags()
60
+ ])
61
+ ic = None
62
+ try:
63
+ ic = model.getIterationCount()
64
+ except:
65
+ pass
66
+ self.data["ic"].append(ic)
67
+ return hook
68
+
69
+
70
+
71
+ @property
72
+ def time(self):
73
+ return np.asarray(self.data["time"], dtype=float)
74
+
75
+ def _response(self, key: str, x: float, time: float):
76
+
77
+ i_time = np.argmin(np.abs(self.time - time))
78
+ i_space = np.argmin(np.abs(self.space - x))
79
+ # Check that the requested time and space are in the available data points
80
+ if np.abs(self.time[i_time] - time) > 1e-6:
81
+ raise ValueError(f"Requested time {time} is not available in the output data.")
82
+ if np.abs(self.space[i_space] - x) > 1e-6:
83
+ raise ValueError(f"Requested space {x} is not available in the output data.")
84
+
85
+ return self.data[key][i_time][i_space]
86
+
87
+ def u_tran(self, x, time):
88
+ return self._response("u_tran", x, time)
89
+ def u_long(self, x, time):
90
+ return self._response("u_long", x, time)
91
+ def r_plan(self, x, time):
92
+ return self._response("r_plan", x, time)
93
+
94
+ def ic(self, time):
95
+ i_time = np.argmin(np.abs(self.time - time))
96
+ if np.abs(self.time[i_time] - time) > 1e-8:
97
+ raise ValueError(f"Requested time {time} is not available in the output data.")
98
+ return self.data["ic"][i_time]