OGRePy 1.0.1__tar.gz → 1.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.
@@ -1,5 +1,6 @@
1
1
  r"""
2
2
  # OGRePy: An Object-Oriented General Relativity Package for Python
3
+ v1.1.0 (2024-09-08)
3
4
 
4
5
  By **Barak Shoshany**\
5
6
  Email: <baraksh@gmail.com>\
@@ -20,13 +21,7 @@ If you use this package in published software or research, please provide a link
20
21
  import sympy as s
21
22
 
22
23
  # Import all public OGRePy objects.
23
- from ._core import Coordinates, CovariantD, Metric, OGRePyError, PartialD, Tensor, __version__, calc, diag, doc, func, info, options, release_date, sym, syms, welcome
24
+ from ._core import Coordinates, CovariantD, Metric, OGRePyError, PartialD, Tensor, __version__, calc, diag, doc, func, info, options, release_date, sym, syms, update_check, welcome
24
25
 
25
26
  # The names that will be exported if using `from OGRePy import *`. Contains exactly all the names imported above.
26
- __all__: list[str] = ["s", "Coordinates", "CovariantD", "Metric", "OGRePyError", "PartialD", "Tensor", "__version__", "calc", "diag", "doc", "func", "info", "options", "release_date", "sym", "syms", "welcome"]
27
-
28
- # Display the welcome message, but not if `OGREPY_DISABLE_WELCOME = True` was defined in the notebook before importing the package.
29
- import __main__
30
-
31
- if __main__.__dict__.get("OGREPY_DISABLE_WELCOME", False) is not True:
32
- welcome()
27
+ __all__: list[str] = ["s", "Coordinates", "CovariantD", "Metric", "OGRePyError", "PartialD", "Tensor", "__version__", "calc", "diag", "doc", "func", "info", "options", "release_date", "sym", "syms", "update_check", "welcome"]
@@ -1,5 +1,6 @@
1
1
  r"""
2
2
  # OGRePy: An Object-Oriented General Relativity Package for Python
3
+ v1.1.0 (2024-09-08)
3
4
 
4
5
  By **Barak Shoshany**\
5
6
  Email: <baraksh@gmail.com>\
@@ -29,10 +30,16 @@ If you use this package in published software or research, please provide a link
29
30
  from __future__ import annotations
30
31
 
31
32
  import collections
33
+ import concurrent.futures
32
34
  import functools
35
+ import importlib.resources
33
36
  import inspect
34
37
  import itertools
38
+ import json
39
+ import os
35
40
  import re
41
+ import sys
42
+ import urllib.request
36
43
  from collections.abc import Callable, Mapping
37
44
  from copy import copy
38
45
  from numbers import Number
@@ -80,8 +87,8 @@ class OGRePyError(Exception):
80
87
  ####################
81
88
 
82
89
 
83
- __version__: str = "1.0.1"
84
- release_date: str = "2024-09-04"
90
+ __version__: str = "1.1.0"
91
+ release_date: str = "2024-09-08"
85
92
 
86
93
 
87
94
  ####################
@@ -107,7 +114,7 @@ def calc(
107
114
  # Validate the symbol.
108
115
  symbol = _validate_symbol(symbol, formula.rank())
109
116
  # Create a new tensor with the same properties and components as the source tensor.
110
- new: Tensor = Tensor(metric=formula.metric(), coords=formula.default_coords, indices=formula.default_indices, components=formula.components(coords=formula.default_coords, indices=formula.default_indices, warn=False), symbol=symbol)
117
+ new: Tensor = Tensor(metric=formula.metric(), indices=formula.default_indices, coords=formula.default_coords, components=formula._get_components(indices=formula.default_indices, coords=formula.default_coords), symbol=symbol) # pyright: ignore [reportPrivateUsage]
111
118
  # Permute the indices if necessary.
112
119
  if permute is not None and permute != formula.index_letters():
113
120
  new.permute(new=permute, old=formula.index_letters())
@@ -235,31 +242,68 @@ def syms(
235
242
  return [sym(symbol, **assumptions) for symbol in all_symbols]
236
243
 
237
244
 
245
+ def update_check(
246
+ *,
247
+ quiet: bool = False,
248
+ ) -> None:
249
+ """
250
+ Check for package updates.
251
+ #### Parameters:
252
+ * `quiet` (optional): Whether to notify even if you have the latest version of the package (`False`, the default) or only notify if a new version of the package is available (`True`).
253
+ """
254
+ url = "https://pypi.org/pypi/OGRePy/json"
255
+ try:
256
+ with urllib.request.urlopen(url=url, timeout=5) as response:
257
+ latest: str = json.loads(response.read())["info"]["version"]
258
+ if latest == __version__:
259
+ if quiet is False:
260
+ _display_markdown("**OGRePy**: You have the latest version of the package.")
261
+ else:
262
+ # Detect how the package was imported so we can give the user the same import statement in the message.
263
+ import_statement: str
264
+ my_names: list[str] = _lookup_names(sys.modules["OGRePy"])
265
+ if len(my_names) == 0:
266
+ import_statement = "from OGRePy import *"
267
+ elif my_names[0] == "OGRePy":
268
+ import_statement = "import OGRePy"
269
+ else:
270
+ import_statement = f"import OGRePy as {my_names[0]}"
271
+ _display_markdown(
272
+ inspect.cleandoc(f"""
273
+ **OGRePy**: A new version of the package is available: **v{latest}** ([view changelog](https://github.com/bshoshany/OGRePy/blob/master/CHANGELOG.md)). To update, please execute the following commands in a notebook cell:
274
+ ```
275
+ %pip install --upgrade OGRePy
276
+ %reset --aggressive -f
277
+ {import_statement}
278
+ ```
279
+ """),
280
+ )
281
+ except OSError as exc:
282
+ _display_markdown(f"**OGRePy**: Could not check for updates automatically: `{exc}`. Please visit <https://pypi.org/project/OGRePy/> to check manually.")
283
+
284
+
238
285
  def welcome() -> None:
239
286
  """
240
287
  Print the welcome message.
241
288
  """
242
- if _in_notebook():
243
- # If we're in a notebook, display a styled welcome message in Markdown format.
289
+ with importlib.resources.as_file(importlib.resources.files().joinpath("docs/OGRePy_Documentation")) as file:
290
+ # Create links to the bundled documentation files.
291
+ ipynb_link: str = f"""<a href="{file.with_suffix(".ipynb").as_posix()}">.ipynb</a>"""
292
+ pdf_link: str = f"""<a href="{file.with_suffix(".pdf").as_posix()}">.pdf</a>"""
293
+ html_link: str = f"""<a href="#" onclick="window.open('{file.with_suffix(".html").as_uri()}', '_blank')">.html</a>"""
294
+ # Display the welcome message.
244
295
  _display_markdown(
245
296
  inspect.cleandoc(rf"""
246
- <b>OGRePy: An <u>O</u>bject-Oriented <u>G</u>eneral <u>Re</u>lativity Package for <u>Py</u>thon\
297
+ **OGRePy: An <u>O</u>bject-Oriented <u>G</u>eneral <u>Re</u>lativity Package for <u>Py</u>thon\
247
298
  By [Barak Shoshany](https://github.com/bshoshany) ([baraksh@gmail.com](mailto:baraksh@gmail.com)) ([baraksh.com](https://baraksh.com/))\
248
299
  v{__version__} ({release_date})\
249
- GitHub repository: <https://github.com/bshoshany/OGRePy></b>
250
- """),
251
- )
252
- else:
253
- # If we're not in a notebook, display a plain text welcome message, and warn that the package should be used inside a notebook.
254
- print( # noqa: T201
255
- inspect.cleandoc(rf"""
256
- OGRePy: An Object-Oriented General Relativity Package for Python
257
- By Barak Shoshany (baraksh@gmail.com) (https://baraksh.com/)
258
- v{__version__} ({release_date})
259
- GitHub repository: https://github.com/bshoshany/OGRePy
260
- WARNING: No notebook interface detected! This package was designed to be used inside a Jupyter notebook in Visual Studio Code or JupyterLab.
300
+ GitHub repository: <https://github.com/bshoshany/OGRePy>\
301
+ Documentation: {ipynb_link}, {pdf_link}, {html_link}**
261
302
  """),
262
303
  )
304
+ # If we're not in a notebook, warn that the package should be used inside a notebook.
305
+ if not _in_notebook():
306
+ _display_markdown("WARNING: No notebook interface detected! This package was designed to be used inside a Jupyter notebook in Visual Studio Code or JupyterLab.")
263
307
 
264
308
 
265
309
  ##################
@@ -291,7 +335,7 @@ class Coordinates:
291
335
  """
292
336
  Construct a new coordinate object.
293
337
  #### Parameters:
294
- * `components`: One or more strings or SymPy `Symbol` objects specifying the coordinates. Strings should be in the same format as the argument to SymPy's `symbols()`, e.g. `"t x y z", and it is possible to enter just one string for all the coordinates.
338
+ * `components`: One or more strings or SymPy `Symbol` objects specifying the coordinates. Strings should be in the same format as the argument to SymPy's `symbols()`, e.g. `"t x y z"`, and it is possible to enter just one string for all the coordinates.
295
339
  """
296
340
  # Validate and store the components.
297
341
  self._components: s.Array = s.Array(_collect_symbols(*components))
@@ -315,7 +359,7 @@ class Coordinates:
315
359
  The names of any notebook variables that refer to this coordinates object.
316
360
  """
317
361
  # We get rid of the backticks since we just want a pure string, not a Markdown formatted string.
318
- return _lookup_names(self).replace("`", "")
362
+ return _lookup_names_string(self).replace("`", "")
319
363
 
320
364
  def __pos__(
321
365
  self: Self,
@@ -399,7 +443,7 @@ class Coordinates:
399
443
  """
400
444
  Display information about this coordinate system in human-readable form.
401
445
  """
402
- text: str = f"* **Name**: {_lookup_names(self)}\n"
446
+ text: str = f"* **Name**: {_lookup_names_string(self)}\n"
403
447
  text += f"* **Class**: {type(self).__name__}\n"
404
448
  text += f"* **Dimensions**: {self.dim()}\n"
405
449
  text += f"* **Default Coordinates For**: {", ".join(_using_coords(self))}\n"
@@ -580,7 +624,7 @@ class Coordinates:
580
624
  del self._inverse_jacobians[target]
581
625
  del self._christoffel_jacobians[target]
582
626
  else:
583
- _handle_error(f"Cannot delete the transformation to {_lookup_names(target)}, since no such transformation exists.")
627
+ _handle_error(f"Cannot delete the transformation to {_lookup_names_string(target)}, since no such transformation exists.")
584
628
  else:
585
629
  # Validate the dictionary.
586
630
  _check_dict_type(rules, s.Symbol, s.Expr, "The rules must be a dictionary with SymPy `Symbol` objects as keys and SymPy `Expr` objects as values.")
@@ -774,7 +818,7 @@ class PartialD:
774
818
  # The index configuration of the output tensor will be such that the partial derivative always has a lower index in either case.
775
819
  out_indices: list[Literal[+1, -1]] = [-1, *use_indices]
776
820
  # Get the tensor's components in the desired representation, adding it if it does not already exist.
777
- components: s.Array = other.components(coords=use_coords, indices=tuple(use_indices), warn=False)
821
+ components: s.Array = other._calc_representation(indices=tuple(use_indices), coords=use_coords) # pyright: ignore [reportPrivateUsage]
778
822
  # Collect the coordinate symbols, since we are taking derivatives with respect to them.
779
823
  coord_symbols: s.Array = other.default_coords.components()
780
824
  # Take the derivative of the entire tensor with respect to each of the coordinates and then combine all the results into one higher-rank tensor.
@@ -884,7 +928,7 @@ class Tensor:
884
928
  # The index configuration for the second tensor must be rearranged to correctly calculate expressions like T^a_b + T_b^a.
885
929
  use_second_indices: IndexConfiguration = tuple(use_indices[first_letters.index(second_letters[i])] for i in range(rank))
886
930
  # Add the representation in the appropriate coordinate system and index configuration to the second tensor if it does not already exist.
887
- second_components: s.Array = other._calc_representation(use_coords, use_second_indices)
931
+ second_components: s.Array = other._calc_representation(indices=use_second_indices, coords=use_coords)
888
932
  # If needed, permute the indices of the second tensor so that they are the same as the first tensor (for example, if we have S^ab + T^ba we will permute the indices of T to ab).
889
933
  other_symbol: str = other._symbol
890
934
  if len(first_letters) > 0 and first_letters != second_letters:
@@ -1014,7 +1058,7 @@ class Tensor:
1014
1058
  # The components of the first tensor will be taken in the default index representation.
1015
1059
  first_components: s.Array = self._get_components(indices=use_first_indices, coords=use_coords)
1016
1060
  # The components of the second tensor will be taken in the index representation that matches the contraction, with indices raised or lowered to match the corresponding indices in the first tensor.
1017
- second_components: s.Array = other._calc_representation(use_coords, use_second_indices)
1061
+ second_components: s.Array = other._calc_representation(indices=use_second_indices, coords=use_coords)
1018
1062
  # Perform the contraction.
1019
1063
  result: TensorWithIndexSpecification = _contract((first_components, [*first_letters]), (second_components, [*second_letters]))
1020
1064
  # Simplify and store the result in a new tensor object.
@@ -1026,7 +1070,7 @@ class Tensor:
1026
1070
  # Count how many summation indices there already are in the first tensor's symbol (from previous contractions).
1027
1071
  contract_count: int = len(_unique_summation_placeholders(self._symbol))
1028
1072
  # Increase the numbering of the summation indices as well.
1029
- second_symbol = _summation_pattern.sub(lambda match: f"[{match[0]}{int(match[1]) + contract_count}{match[2]}]", second_symbol)
1073
+ second_symbol = _summation_pattern.sub(lambda match: f"[{match[1] if match[1] else ""}{int(match[2]) + contract_count}{match[3] if match[3] else ""}]", second_symbol)
1030
1074
  # Add the number of contractions in the second symbol to the total count.
1031
1075
  contract_count += len(_unique_summation_placeholders(second_symbol))
1032
1076
  # Join the symbols of the two tensors to create a new symbol for the output tensor, with consecutive summation index placeholders.
@@ -1157,7 +1201,7 @@ class Tensor:
1157
1201
  The names of any notebook variables that refer to this tensor object.
1158
1202
  """
1159
1203
  # We get rid of the backticks since we just want a pure string, not a Markdown formatted string.
1160
- return _lookup_names(self).replace("`", "")
1204
+ return _lookup_names_string(self).replace("`", "")
1161
1205
 
1162
1206
  def __sub__(
1163
1207
  self: Self,
@@ -1206,7 +1250,7 @@ class Tensor:
1206
1250
  # Make sure the input is valid.
1207
1251
  value = _validate_coordinates(value)
1208
1252
  # Calculate the representation of this tensor in the new default coordinates, if it does not already exist. We do this for the default indices only.
1209
- _ = self._calc_representation(value, self._default_indices)
1253
+ _ = self._calc_representation(indices=self._default_indices, coords=value)
1210
1254
  # Store the new default coordinates.
1211
1255
  self._default_coords = value
1212
1256
 
@@ -1234,7 +1278,7 @@ class Tensor:
1234
1278
  if len(value) != len(self._default_indices):
1235
1279
  _handle_error("The number of indices must match the rank of the tensor.")
1236
1280
  # Calculate the representation of this tensor in the new default indices, if it does not already exist. We do this for the default coordinates only.
1237
- _ = self._calc_representation(self._default_coords, value)
1281
+ _ = self._calc_representation(indices=value, coords=self._default_coords)
1238
1282
  # Store the new default indices.
1239
1283
  self._default_indices = value
1240
1284
 
@@ -1279,17 +1323,20 @@ class Tensor:
1279
1323
  # Validate the coordinates and indices.
1280
1324
  use_coords: Coordinates = self._validate_or_default_coords(coords)
1281
1325
  use_indices: IndexConfiguration = self._validate_or_default_indices(indices)
1282
- # Retrieve the components, calculating them if they have not already been calculated.
1283
- components: s.Array = self._calc_representation(use_coords, use_indices)
1284
1326
  # Optionally, show a warning if using the defaults, to ensure that the user knows which representation the components correspond to.
1285
1327
  if warn:
1286
1328
  warning: str = ""
1287
1329
  if coords is None:
1288
- warning = f"Using default coordinate system {_lookup_names(use_coords)}"
1330
+ warning = f"Using default coordinate system {_lookup_names_string(use_coords)}"
1289
1331
  if indices is None:
1290
1332
  warning += f"{"Using" if warning == "" else " and"} default index configuration {use_indices}"
1291
1333
  if warning != "":
1292
1334
  _display_markdown(f"**OGRePy**: {warning}.")
1335
+ # Retrieve the components, calculating them if they have not already been calculated.
1336
+ components: s.Array = self._calc_representation(indices=use_indices, coords=use_coords)
1337
+ # If this is a type of tensor that uses a curve parameter, replace any instance of the curve parameter placeholder with the selected curve parameter.
1338
+ if isinstance(self, CleanupCurveParameter):
1339
+ components = _subs_array(components, {_curve_parameter_placeholder: options.curve_parameter})
1293
1340
  return components
1294
1341
 
1295
1342
  def dim(
@@ -1320,15 +1367,15 @@ class Tensor:
1320
1367
  """
1321
1368
  Display information about this tensor, including its class, symbol, rank, dimensions, default coordinates, default indices, and associated metric (if the tensor is not itself a metric), in human-readable form.
1322
1369
  """
1323
- text: str = f"* **Name**: {_lookup_names(self)}\n"
1370
+ text: str = f"* **Name**: {_lookup_names_string(self)}\n"
1324
1371
  text += f"* **Class**: `{type(self).__name__}`\n"
1325
1372
  text += f"* **Symbol**: ${self._tex_symbol(indices=self._default_indices, letters=options.index_letters)}$\n"
1326
1373
  text += f"* **Rank**: {self.rank()}\n"
1327
1374
  text += f"* **Dimensions**: {self.dim()}\n"
1328
- text += f"* **Default Coordinates**: {_lookup_names(self._default_coords)}\n"
1375
+ text += f"* **Default Coordinates**: {_lookup_names_string(self._default_coords)}\n"
1329
1376
  text += f"* **Default Indices**: {self._default_indices}\n"
1330
1377
  if not isinstance(self, Metric):
1331
- text += f"* **Metric**: {_lookup_names(self._metric)}\n"
1378
+ text += f"* **Metric**: {_lookup_names_string(self._metric)}\n"
1332
1379
  else:
1333
1380
  used_by: list[str] = _using_metric(self)
1334
1381
  text += f"* **Associated Metric For**: {", ".join(used_by) if len(used_by) > 0 else "None"}\n"
@@ -1530,7 +1577,7 @@ class Tensor:
1530
1577
  # If the tensor is a scalar, then populating the dictionary is very simple: keep it empty if the scalar is zero, otherwise populate it with a single key pointing to a list with a single item.
1531
1578
  if rank == 0:
1532
1579
  if components[0] != 0:
1533
- non_zero[components[0]] = [self._symbol]
1580
+ non_zero[components[0]] = [self._tex_symbol(indices=use_indices, letters=[])]
1534
1581
  # If the tensor is not a scalar, then we need to go over all the components one by one.
1535
1582
  else:
1536
1583
  # Instead of the default index letters, we will use the coordinate letters themselves (e.g.: t, x, y, z) to display the tensor components.
@@ -1615,14 +1662,15 @@ class Tensor:
1615
1662
 
1616
1663
  def _calc_representation(
1617
1664
  self: Self,
1618
- coords: Coordinates,
1665
+ *,
1619
1666
  indices: IndexConfiguration,
1667
+ coords: Coordinates,
1620
1668
  ) -> s.Array:
1621
1669
  """
1622
1670
  Get the components of this tensor in the given representation, calculating them if they have not been calculated yet.
1623
1671
  #### Parameters:
1624
- * `coords`: An OGRePy `Coordinates` object specifying the coordinate system of the representation.
1625
1672
  * `indices`: A tuple of integers specifying the index configuration of the representation. Each integer in the tuple can be either +1 for an upper index or -1 for a lower index.
1673
+ * `coords`: An OGRePy `Coordinates` object specifying the coordinate system of the representation.
1626
1674
  #### Returns:
1627
1675
  The desired components.
1628
1676
  """
@@ -1672,7 +1720,7 @@ class Tensor:
1672
1720
  """
1673
1721
  # Obtain the coordinate symbols.
1674
1722
  coord_symbols: list[s.Symbol] = coords.components().tolist()
1675
- # If this object has the mixin class `CleanupTimeParameter`, the parameter with respect to which we need to clean up is the time parameter (assumed to be the first coordinate). Otherwise, it is the curve parameter if the object has `CleanupCurveParameter`, or simply irrelevant if it has neither class.
1723
+ # If this object has the mixin class `CleanupTimeParameter`, the parameter with respect to which we need to clean up is the time parameter (assumed to be the first coordinate). Otherwise, it is the curve parameter if the object has `CleanupCurveParameter`, or simply irrelevant if it has neither class. Note that when this function is called, the curve parameter placeholder has already been replaced with the real curve parameter.
1676
1724
  param: s.Symbol = coord_symbols[0] if isinstance(self, CleanupTimeParameter) else options.curve_parameter
1677
1725
  if isinstance(self, CleanupCurveParameter | CleanupTimeParameter):
1678
1726
  # If this is a type of tensor that uses a parameter, replace any instance of the coordinate functions in terms of the parameter with the ordinary coordinate symbols, and similarly for their first and second derivatives.
@@ -1725,7 +1773,10 @@ class Tensor:
1725
1773
  use_coords: Coordinates = self._validate_or_default_coords(coords)
1726
1774
  use_indices: IndexConfiguration = self._validate_or_default_indices(indices)
1727
1775
  # Retrieve the components, calculating them if they have not already been calculated.
1728
- components: s.Array = self._calc_representation(use_coords, use_indices)
1776
+ components: s.Array = self._calc_representation(indices=use_indices, coords=use_coords)
1777
+ # If this is a type of tensor that uses a curve parameter, replace any instance of the curve parameter placeholder with the selected curve parameter.
1778
+ if isinstance(self, CleanupCurveParameter):
1779
+ components = _subs_array(components, {_curve_parameter_placeholder: options.curve_parameter})
1729
1780
  # Apply the function, if any.
1730
1781
  if function is not None:
1731
1782
  components = _map_function(components, function)
@@ -1735,7 +1786,7 @@ class Tensor:
1735
1786
  # Simplify the components, if desired.
1736
1787
  if simplify is True:
1737
1788
  components = _simplify_array(components)
1738
- # Clean up the notation if relevant.
1789
+ # Clean up the notation.
1739
1790
  components = self._cleanup_notation(components=components, coords=use_coords)
1740
1791
  # Return the results.
1741
1792
  return (use_coords, use_indices, components)
@@ -1816,7 +1867,7 @@ class Tensor:
1816
1867
  # Remove all the indices we marked for removal earlier, and save the index configuration for the output tensor as a tuple.
1817
1868
  out_indices: IndexConfiguration = tuple(i for i in out_indices_list if i != 0)
1818
1869
  # Retrieve the components in the desired representation, calculating it if it does not already exist.
1819
- components: s.Array = self._calc_representation(self._default_coords, tuple(in_indices))
1870
+ components: s.Array = self._calc_representation(indices=tuple(in_indices), coords=self._default_coords)
1820
1871
  # Calculate the trace by contracting on the repeated indices.
1821
1872
  result: TensorWithIndexSpecification = _contract((components, [*all_letters]))
1822
1873
  # Simplify and store the result in a new tensor object.
@@ -1897,9 +1948,9 @@ class Tensor:
1897
1948
  """
1898
1949
  # Get the source components.
1899
1950
  components: s.Array = self._get_components(indices=source_indices, coords=coords)
1900
- # Get the components of the metric and inverse metric in the relevant coordinate system, calculating them on the spot if the metric has not been used in this coordinate system before. (Note that there is no risk of an infinite loop here either, since the `Metric` class transforms coordinates and indices on its own, not through this method.)
1901
- metric: s.Array = self._metric.components(coords=coords, indices=(-1, -1), warn=False)
1902
- inverse_metric: s.Array = self._metric.components(coords=coords, indices=(1, 1), warn=False)
1951
+ # Get the components of the metric and inverse metric in the relevant coordinate system, calculating them on the spot if the metric has not been used in this coordinate system before. (Note that there is no risk of an infinite loop here, since the `Metric` subclass pre-calculates the representations in all index configurations when it transforms coordinates, and therefore does not use this method.)
1952
+ metric: s.Array = self._metric._calc_representation(indices=(-1, -1), coords=coords)
1953
+ inverse_metric: s.Array = self._metric._calc_representation(indices=(1, 1), coords=coords)
1903
1954
  # `in_vars` will contain the variables to be used in the input tensor.
1904
1955
  in_vars: IndexSpecification = []
1905
1956
  # `permute_vars` will contain the variables that we want to permute the final result to (see below for explanation).
@@ -2031,9 +2082,9 @@ class Christoffel(Tensor, FixedDefaultIndices):
2031
2082
  # Sum three such partial derivatives.
2032
2083
  sum_partial_metric: Tensor = partial_metric("mu nu sigma") + partial_metric("nu sigma mu") - partial_metric("sigma mu nu")
2033
2084
  # Extract the inverse metric and multiply by half.
2034
- half_inverse_metric: s.Array = cast(s.Array, s.Rational(1, 2) * metric.components(coords=coords, indices=(+1, +1), warn=False))
2085
+ half_inverse_metric: s.Array = cast(s.Array, s.Rational(1, 2) * metric._calc_representation(indices=(+1, +1), coords=coords))
2035
2086
  # Calculate the Christoffel symbols by contracting the sum of partial derivatives of the metric with half the inverse metric.
2036
- result: s.Array = _contract((half_inverse_metric, ["lamda", "sigma"]), (sum_partial_metric.components(coords=coords, indices=(-1, -1, -1), warn=False), ["mu", "nu", "sigma"]))[0]
2087
+ result: s.Array = _contract((half_inverse_metric, ["lamda", "sigma"]), (sum_partial_metric._calc_representation(indices=(-1, -1, -1), coords=coords), ["mu", "nu", "sigma"]))[0]
2037
2088
  # Simplify and return the result.
2038
2089
  return _simplify_array(result)
2039
2090
 
@@ -2108,7 +2159,7 @@ class Einstein(Tensor, FixedDefaultIndices):
2108
2159
  #### Returns:
2109
2160
  The calculated components.
2110
2161
  """
2111
- return (metric.ricci_tensor("mu nu") - s.Rational(1, 2) * metric.ricci_scalar() @ metric("mu nu")).components(coords=coords, indices=(-1, -1), warn=False)
2162
+ return (metric.ricci_tensor("mu nu") - s.Rational(1, 2) * metric.ricci_scalar() @ metric("mu nu"))._calc_representation(indices=(-1, -1), coords=coords)
2112
2163
 
2113
2164
 
2114
2165
  class GeodesicFromChristoffel(Tensor, CleanupCurveParameter, FixedDefaultIndices):
@@ -2161,11 +2212,11 @@ class GeodesicFromChristoffel(Tensor, CleanupCurveParameter, FixedDefaultIndices
2161
2212
  The calculated components.
2162
2213
  """
2163
2214
  # Create a tangent vector whose components are the first derivatives of the coordinate symbols as functions of the curve parameter.
2164
- tangent: s.Array = s.Array(coords.of_param_dot())
2215
+ tangent: s.Array = s.Array(coords.of_param_dot(_curve_parameter_placeholder))
2165
2216
  # Create an acceleration vector whose components are the second derivatives of the coordinate symbols as functions of the curve parameter.
2166
- accel: s.Array = s.Array(coords.of_param_ddot())
2217
+ accel: s.Array = s.Array(coords.of_param_ddot(_curve_parameter_placeholder))
2167
2218
  # Obtain the Christoffel symbols, and replace any instance of the coordinate symbols with coordinate functions of the curve parameter.
2168
- christoffel_with_param: s.Array = _subs_array(metric.christoffel().components(coords=coords, indices=(+1, -1, -1), warn=False), coords.of_param_dict())
2219
+ christoffel_with_param: s.Array = _subs_array(metric.christoffel()._calc_representation(indices=(+1, -1, -1), coords=coords), coords.of_param_dict(_curve_parameter_placeholder))
2169
2220
  # Calculate the geodesic equations by contracting the Christoffel symbols with two tangent vectors and adding to the acceleration vector.
2170
2221
  result: s.Array = accel + _contract((christoffel_with_param, ["sigma", "mu", "nu"]), (tangent, ["mu"]), (tangent, ["nu"]))[0]
2171
2222
  # Simplify and return the result.
@@ -2240,13 +2291,13 @@ class GeodesicFromLagrangian(Tensor, CleanupCurveParameter, FixedDefaultIndices)
2240
2291
  The calculated components.
2241
2292
  """
2242
2293
  # Make sure we have the components of the Lagrangian in the correct coordinate system. We divide them by 2 since geodesics calculated in this way will inevitably get additional factors of 2 from taking the derivatives of squares.
2243
- components: s.Expr = cast(s.Expr, s.Rational(1, 2) * metric.lagrangian()._calc_representation(coords=coords, indices=())[0])
2294
+ components: s.Expr = cast(s.Expr, s.Rational(1, 2) * metric.lagrangian()._calc_representation(indices=(), coords=coords)[0])
2244
2295
  # Calculate the dL/dx part of the Euler-Lagrange equation.
2245
- euler_lagrange_x: s.Array = s.Array([components.diff(x) for x in coords.of_param()])
2296
+ euler_lagrange_x: s.Array = s.Array([components.diff(x) for x in coords.of_param(_curve_parameter_placeholder)])
2246
2297
  # Calculate the dL/dx_dot part of the Euler-Lagrange equation.
2247
- euler_lagrange_x_dot: s.Array = s.Array([components.diff(x_dot) for x_dot in coords.of_param_dot()])
2248
- # Take the derivative of the dL/dx_dot part with respect to the curve parameter, but leave the derivative unevaluated.
2249
- euler_lagrange_x_dot_diff: s.Array = s.Array([s.Derivative(term, options.curve_parameter) for term in euler_lagrange_x_dot])
2298
+ euler_lagrange_x_dot: s.Array = s.Array([components.diff(x_dot) for x_dot in coords.of_param_dot(_curve_parameter_placeholder)])
2299
+ # Take the derivative of the dL/dx_dot part with respect to the curve parameter placeholder, but leave the derivative unevaluated.
2300
+ euler_lagrange_x_dot_diff: s.Array = s.Array([s.Derivative(term, _curve_parameter_placeholder) for term in euler_lagrange_x_dot])
2250
2301
  # Calculate the geodesic equation vector from the full Euler-Lagrange equation.
2251
2302
  result: s.Array = euler_lagrange_x - euler_lagrange_x_dot_diff
2252
2303
  # Simplify and return the result, making sure to pass `doit=False` so the derivatives with respect to the curve parameter will not be evaluated.
@@ -2329,7 +2380,7 @@ class GeodesicTimeParam(Tensor, CleanupTimeParameter, FixedDefaultIndices):
2329
2380
  # Obtain the Christoffel symbols, and replace any instance of the spatial coordinate symbols with coordinate functions of time.
2330
2381
  param_dict: dict[s.Symbol, AppliedUndef] = coords.of_param_dict(time)
2331
2382
  del param_dict[time]
2332
- christoffel_with_param: s.Array = _subs_array(metric.christoffel().components(coords=coords, indices=(+1, -1, -1), warn=False), param_dict)
2383
+ christoffel_with_param: s.Array = _subs_array(metric.christoffel()._calc_representation(indices=(+1, -1, -1), coords=coords), param_dict)
2333
2384
  # We also need the Christoffel symbols with 0 in the first index, which is a rank-2 tensor.
2334
2385
  christoffel_zero: s.Array = cast(s.Array, christoffel_with_param[0, :, :])
2335
2386
  # Contract the Christoffel symbols with the tangent vector and subtract from the Christoffel symbols with 0 in the first index to get the term in the parentheses in the equation (see `Metric.geodesic_time_param()` docs), which is a rank-3 tensor.
@@ -2407,7 +2458,7 @@ class Kretschmann(Tensor, FixedDefaultIndices):
2407
2458
  #### Returns:
2408
2459
  The calculated components.
2409
2460
  """
2410
- return (metric.riemann("rho sigma mu nu") @ metric.riemann("rho sigma mu nu")).components(coords=coords, indices=(), warn=False)
2461
+ return (metric.riemann("rho sigma mu nu") @ metric.riemann("rho sigma mu nu"))._calc_representation(indices=(), coords=coords)
2411
2462
 
2412
2463
 
2413
2464
  class Lagrangian(Tensor, CleanupCurveParameter, FixedDefaultIndices):
@@ -2460,9 +2511,9 @@ class Lagrangian(Tensor, CleanupCurveParameter, FixedDefaultIndices):
2460
2511
  The calculated components.
2461
2512
  """
2462
2513
  # Create a tangent vector whose components are the first derivatives of the coordinate symbols as functions of the curve parameter.
2463
- tangent: s.Array = s.Array(coords.of_param_dot())
2514
+ tangent: s.Array = s.Array(coords.of_param_dot(_curve_parameter_placeholder))
2464
2515
  # Obtain the metric components, and replace any instance of the coordinate symbols with coordinate functions of the curve parameter.
2465
- metric_with_param: s.Array = _subs_array(metric.components(coords=coords, indices=(-1, -1), warn=False), coords.of_param_dict())
2516
+ metric_with_param: s.Array = _subs_array(metric._calc_representation(indices=(-1, -1), coords=coords), coords.of_param_dict(_curve_parameter_placeholder))
2466
2517
  # Calculate the Lagrangian by taking the norm squared of the tangent vector.
2467
2518
  result: s.Array = _contract((metric_with_param, ["mu", "nu"]), (tangent, ["mu"]), (tangent, ["nu"]))[0]
2468
2519
  # Simplify and return the result.
@@ -2680,7 +2731,7 @@ class Metric(Tensor, FixedDefaultIndices):
2680
2731
  # Validate the input.
2681
2732
  use_coords: Coordinates = self._validate_or_default_coords(coords)
2682
2733
  # Calculate the components of the metric in the desired coordinate system, in case they have not already been calculated.
2683
- components: s.Array = self._calc_representation(coords=use_coords, indices=(-1, -1))
2734
+ components: s.Array = self._calc_representation(indices=(-1, -1), coords=use_coords)
2684
2735
  # Convert each of the coordinate symbols into the corresponding differential.
2685
2736
  diff: s.Array = s.Array([sym(r"\mathrm{d}" + _to_tex(symbol)) for symbol in use_coords.components()])
2686
2737
  # Sum the matrix elements with the corresponding differentials and return the result.
@@ -2747,7 +2798,7 @@ class Metric(Tensor, FixedDefaultIndices):
2747
2798
  # Validate the input.
2748
2799
  use_coords: Coordinates = self._validate_or_default_coords(coords)
2749
2800
  # Calculate the components of the metric in the desired coordinate system, in case they have not already been calculated.
2750
- components: s.Array = self._calc_representation(coords=use_coords, indices=(-1, -1))
2801
+ components: s.Array = self._calc_representation(indices=(-1, -1), coords=use_coords)
2751
2802
  # Calculate the determinant, simplify it, and return the result.
2752
2803
  return _simplify(cast(s.Basic, s.Matrix(components).det()))
2753
2804
 
@@ -2827,7 +2878,7 @@ class RicciScalar(Tensor, FixedDefaultIndices):
2827
2878
  #### Returns:
2828
2879
  The calculated components.
2829
2880
  """
2830
- return (metric.ricci_tensor("mu mu")).components(coords=coords, indices=(), warn=False)
2881
+ return metric.ricci_tensor("mu mu")._calc_representation(indices=(), coords=coords)
2831
2882
 
2832
2883
 
2833
2884
  class RicciTensor(Tensor, FixedDefaultIndices):
@@ -2879,7 +2930,7 @@ class RicciTensor(Tensor, FixedDefaultIndices):
2879
2930
  #### Returns:
2880
2931
  The calculated components.
2881
2932
  """
2882
- return (metric.riemann("lamda mu lamda nu")).components(coords=coords, indices=(-1, -1), warn=False)
2933
+ return metric.riemann("lamda mu lamda nu")._calc_representation(indices=(-1, -1), coords=coords)
2883
2934
 
2884
2935
 
2885
2936
  class Riemann(Tensor, FixedDefaultIndices):
@@ -2933,7 +2984,7 @@ class Riemann(Tensor, FixedDefaultIndices):
2933
2984
  """
2934
2985
  riemann: Tensor = PartialD("mu") @ metric.christoffel("rho nu sigma") - PartialD("nu") @ metric.christoffel("rho mu sigma") + metric.christoffel("rho mu lamda") @ metric.christoffel("lamda nu sigma") - metric.christoffel("rho nu lamda") @ metric.christoffel("lamda mu sigma")
2935
2986
  riemann.permute("rho sigma mu nu")
2936
- return riemann.components(coords=coords, indices=(+1, -1, -1, -1), warn=False)
2987
+ return riemann._calc_representation(indices=(+1, -1, -1, -1), coords=coords)
2937
2988
 
2938
2989
 
2939
2990
  #################################################################################################
@@ -3234,15 +3285,28 @@ def _list_references(
3234
3285
 
3235
3286
  def _lookup_names(
3236
3287
  ref: object,
3288
+ ) -> list[str]:
3289
+ """
3290
+ Look up the names of the notebook variables corresponding to a specific object reference and return them as a list of strings.
3291
+ #### Parameters:
3292
+ * `ref`: The reference to look up.
3293
+ #### Returns:
3294
+ A list of strings containing the names of all of the variables that point to the given reference. Note that any variables that start with an underscore are ignored, to avoid e.g. the `_` variable, which is the result of the last evaluation, as well as other similar internal variables.
3295
+ """
3296
+ return [name for name, value in __main__.__dict__.items() if value is ref and not name.startswith("_")]
3297
+
3298
+
3299
+ def _lookup_names_string(
3300
+ ref: object,
3237
3301
  ) -> str:
3238
3302
  """
3239
- Look up the names of the notebook variables corresponding to a specific object reference.
3303
+ Look up the names of the notebook variables corresponding to a specific object reference and return them as a string.
3240
3304
  #### Parameters:
3241
3305
  * `ref`: The reference to look up.
3242
3306
  #### Returns:
3243
3307
  A string containing the name of the first variable that points to the given reference, along with any aliases, or "[no name]" if there is no match. Note that any variables that start with an underscore are ignored, to avoid e.g. the `_` variable, which is the result of the last evaluation, as well as other similar internal variables.
3244
3308
  """
3245
- return _list_aliases([name for name, value in __main__.__dict__.items() if value is ref and not name.startswith("_")])
3309
+ return _list_aliases(_lookup_names(ref))
3246
3310
 
3247
3311
 
3248
3312
  def _make_replacement(
@@ -3358,10 +3422,10 @@ def _subs_array(
3358
3422
  Perform a substitution in a SymPy `Array`.
3359
3423
  #### Parameters:
3360
3424
  * `array`: The array to perform the substitution in.
3361
- * `args` (optional): Zero or more positional arguments to pass to the function.
3362
- * `kwargs` (optional): Zero or more keyword arguments to pass to the function.
3425
+ * `args` (optional): Zero or more positional arguments to pass to the `subs()` function.
3426
+ * `kwargs` (optional): Zero or more keyword arguments to pass to the `subs()` function.
3363
3427
  #### Returns:
3364
- The simplified array.
3428
+ The array with the substitution performed.
3365
3429
  """
3366
3430
  return cast(s.Array, array.subs(*args, **kwargs))
3367
3431
 
@@ -3827,3 +3891,31 @@ type TensorWithIndexSpecification = tuple[s.Array, IndexSpecification]
3827
3891
 
3828
3892
  # The tensor's components are stored as values in a dictionary, where the keys are tuples of the form (indices, coords).
3829
3893
  type Components = dict[tuple[IndexConfiguration, Coordinates], s.Array]
3894
+
3895
+
3896
+ #####################
3897
+ # Private variables #
3898
+ #####################
3899
+
3900
+
3901
+ # Create a thread pool executor for later use.
3902
+ _pool = concurrent.futures.ThreadPoolExecutor()
3903
+
3904
+ # The symbol to use as curve parameter placeholder.
3905
+ _curve_parameter_placeholder: s.Symbol = sym("[curve]")
3906
+
3907
+
3908
+ ########################
3909
+ # Initialization tasks #
3910
+ ########################
3911
+
3912
+
3913
+ # Display the welcome message, but not if `OGREPY_DISABLE_WELCOME = True` was defined in the notebook or the environment variable `OGREPY_DISABLE_WELCOME` was set to "True" before importing the package.
3914
+ disable_welcome: bool = __main__.__dict__.get("OGREPY_DISABLE_WELCOME", False) is True or os.environ.get("OGREPY_DISABLE_WELCOME", "False") == "True"
3915
+ if not disable_welcome:
3916
+ welcome()
3917
+
3918
+ # Check for package updates, but not if `OGREPY_DISABLE_UPDATE_CHECK = True` was defined in the notebook or the environment variable `OGREPY_DISABLE_UPDATE_CHECK` was set to "True" before importing the package.If the welcome message is disabled, this is done quietly (only notifies if a new version of the package is available).
3919
+ disable_update_check: bool = __main__.__dict__.get("OGREPY_DISABLE_UPDATE_CHECK", False) is True or os.environ.get("OGREPY_DISABLE_UPDATE_CHECK", "False") == "True"
3920
+ if not disable_update_check:
3921
+ _ = _pool.submit(update_check, quiet=disable_welcome)
@@ -1,5 +1,6 @@
1
1
  r"""
2
2
  # OGRePy: An Object-Oriented General Relativity Package for Python
3
+ v1.1.0 (2024-09-08)
3
4
 
4
5
  By **Barak Shoshany**\
5
6
  Email: <baraksh@gmail.com>\