From 5ef6893a3f49c4da7c41e608924b8d22c72bf904 Mon Sep 17 00:00:00 2001 From: Andrew Annex <2126916+AndrewAnnex@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:28:04 -0800 Subject: [PATCH 01/10] start of custom extension for sphinx to use standard sphinx styling --- docs/_static/css/pyscript_editor.css | 52 ++++++++ docs/conf.py | 18 +++ docs/pyodide.rst | 89 ++++++------- docs/pyscript_editor.py | 191 +++++++++++++++++++++++++++ 4 files changed, 302 insertions(+), 48 deletions(-) create mode 100644 docs/_static/css/pyscript_editor.css create mode 100644 docs/pyscript_editor.py diff --git a/docs/_static/css/pyscript_editor.css b/docs/_static/css/pyscript_editor.css new file mode 100644 index 00000000..6eac0acf --- /dev/null +++ b/docs/_static/css/pyscript_editor.css @@ -0,0 +1,52 @@ +/* + * Make the PyScript py-editor shadow-DOM slot look like a Sphinx code block. + * + * The outer .highlight.highlight-python div is already styled by your theme. + * These rules target the web-component internals that PyScript exposes. + */ + +/* Match the min-height to a reasonable number of visible lines */ +py-editor { + display: block; + min-height: 6em; +} + +/* + * PyScript 2024+ exposes a --py-editor-* set of custom properties. + * Mirror the values your Sphinx theme uses for
 blocks.
+ */
+py-editor {
+    --py-editor-font-family: var(
+        --code-font-family,
+        "SFMono-Regular",
+        Consolas,
+        "Liberation Mono",
+        Menlo,
+        monospace
+    );
+    --py-editor-font-size: var(--code-font-size, 0.875em);
+    --py-editor-background: var(
+        --code-background,
+        var(--color-code-background, #f8f8f8)
+    );
+    --py-editor-foreground: var(
+        --code-foreground,
+        var(--color-foreground, #333)
+    );
+}
+
+/*
+ * Remove PyScript's own border/shadow so the outer Sphinx .highlight
+ * border is the only one visible.
+ */
+py-editor::part(editor) {
+    border: none;
+    box-shadow: none;
+    border-radius: 0;
+    padding: 0.5em 1em;
+}
+
+/* Run button: nudge it to sit inside the highlight box */
+py-editor::part(run-button) {
+    border-radius: 0 0 4px 0;
+}
diff --git a/docs/conf.py b/docs/conf.py
index b7e77bc0..fe964c7d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -13,8 +13,11 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
+import os
 import sys
 
+sys.path.insert(0, os.path.abspath("."))
+
 sys.setrecursionlimit(15000)
 
 # If extensions (or modules to document with autodoc) are in another directory,
@@ -85,7 +88,22 @@
     "sphinx_copybutton",
     "myst_parser",
     "sphinx_rtd_theme",
+    "pyscript_editor",
 ]
+
+# Global defaults for pyscript (all optional)
+pyscript_version = "2026.2.1"
+pyscript_env = "shared"
+pyscript_config = "pyscript.json"
+pyscript_mini_coi = "mini-coi.js"  # "" to disable
+# Default is True — set to False to keep the line numbers
+pyscript_hide_gutters = True
+
+html_static_path = ["_static"]
+
+html_css_files = ["css/pyscript_editor.css"]
+
+
 # conf for autodoc typehints
 autodoc_typehints = "both"
 
diff --git a/docs/pyodide.rst b/docs/pyodide.rst
index 8f517681..1ff43089 100644
--- a/docs/pyodide.rst
+++ b/docs/pyodide.rst
@@ -68,75 +68,68 @@ Try updating the plot below to plot the barycenter of Mars or Mercury!
 Various imports and setup:
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. raw:: html
-
-    
-    
-    
-    
+.. py-editor::
+
+    import numpy as np
+    import matplotlib
+    matplotlib.use("AGG")
+    import matplotlib.pyplot as plt
+    from pyscript import display
+    import spiceypy as spice
+    print(f'SpiceyPy for {spice.tkvrsn("TOOLKIT")} ready!')
+
 
 
 Load kernels
 ~~~~~~~~~~~~~~~~~~~~~
 
-.. raw:: html
+.. py-editor::
+
+    # Load kernels: leap seconds + planetary ephemeris
+    spice.furnsh("naif0012.tls")
+    spice.furnsh("de440s_2000_to_2020_simplified.bsp")
+    print(f'Loaded {spice.ktotal("ALL")} kernels.')
+
 
-    
 
 Specify the dates to sample:
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. raw:: html
+.. py-editor::
 
-    
+    # Grab 2000 dates over 8 years
+    et0 = spice.str2et("2000-01-01")
+    et1 = spice.str2et("2008-01-01")
+    ets = np.linspace(et0, et1, 2000)
+    print(f'ets array has len: {len(ets)}')
 
+
+    
 Get the positions vector
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. raw:: html
+.. py-editor::
 
-    
+    # Venus position relative to Earth in ecliptic J2000 (km)
+    positions, _ = spice.spkpos("VENUS BARYCENTER", ets, "ECLIPJ2000", "NONE", "EARTH")
+    print(f'Got {len(positions)} positions of the Venus Barycenter')
 
 
 Plot it on the ecliptic plane.
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. raw:: html
-
-    
-    
+.. py-editor:: + :target: mpl + + fig, ax = plt.subplots() + ax.plot(positions[:, 0], positions[:, 1], color="black", linewidth=0.5) + ax.plot(0, 0, "o", color="blue", markersize=6, label="Earth") + ax.set_title("Geocentric Orbit of Venus (2000–2008)", fontsize=14) + ax.legend() + ax.set_aspect("equal") + display(fig, target="mpl", append=False) + plt.close('all') + diff --git a/docs/pyscript_editor.py b/docs/pyscript_editor.py new file mode 100644 index 00000000..47d9e12c --- /dev/null +++ b/docs/pyscript_editor.py @@ -0,0 +1,191 @@ +""" +Sphinx extension: pyscript_editor +---------------------------------- +Adds a ``.. py-editor::`` directive that renders a PyScript editor +wrapped in the same ``div.highlight.highlight-python`` structure that +Sphinx/Pygments produces, so it inherits your theme's code-block styling. + +Usage in conf.py +---------------- + extensions = [..., "pyscript_editor"] + + # Optional global defaults (all overridable per-directive): + pyscript_version = "2026.2.1" # PyScript release + pyscript_env = "shared" # py-editor env attribute + pyscript_config = "pyscript.json" # py-editor config attribute + pyscript_mini_coi = "mini-coi.js" # path to mini-coi shim; + # set to "" to skip + +Usage in .rst files +------------------- +Basic (uses global defaults from conf.py):: + + .. py-editor:: + + import numpy as np + print(np.__version__) + +Override any option per block:: + + .. py-editor:: + :env: isolated + :config: other.json + + print("hello") + +The ``mini-coi.js`` script and the PyScript stylesheet/module are injected +only once per page, no matter how many ``.. py-editor::`` directives appear. +""" + +from __future__ import annotations + +from docutils import nodes +from docutils.parsers.rst import Directive, directives +from sphinx.application import Sphinx +from sphinx.util import logging + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Raw-HTML node helpers +# --------------------------------------------------------------------------- + + +def _raw(html: str) -> nodes.raw: + return nodes.raw("", html, format="html") + + +# --------------------------------------------------------------------------- +# One-per-page head injection (idempotent via env metadata) +# --------------------------------------------------------------------------- + +_HEAD_KEY = "pyscript_head_injected" +_ENV_KEY = "pyscript_env_config_registered" + + +_HIDE_GUTTERS_JS = """ + +""" + + +def _head_html(mini_coi: str, version: str, hide_gutters: bool) -> str: + parts = [] + if mini_coi: + parts.append(f'') + parts.append( + f'' + ) + parts.append( + f'' + ) + if hide_gutters: + parts.append(_HIDE_GUTTERS_JS) + return "\n".join(parts) + "\n" + + +# --------------------------------------------------------------------------- +# Directive +# --------------------------------------------------------------------------- + + +class PyEditorDirective(Directive): + """``.. py-editor::`` directive.""" + + has_content = True + optional_arguments = 0 + option_spec = { + "env": directives.unchanged, + "config": directives.unchanged, + "target": directives.unchanged, + } + + def run(self) -> list[nodes.Node]: + env = self.state.document.settings.env # Sphinx BuildEnvironment + cfg = env.config + + # ---- resolve options, falling back to conf.py values ---- + version = cfg.pyscript_version + mini_coi = cfg.pyscript_mini_coi + hide_gutters = cfg.pyscript_hide_gutters + ed_env = self.options.get("env", cfg.pyscript_env) + ed_cfg = self.options.get("config", cfg.pyscript_config) + ed_target = self.options.get("target", None) + + result: list[nodes.Node] = [] + + # ---- inject assets once per document ---- + injected = getattr(env, _HEAD_KEY, set()) + if env.docname not in injected: + result.append(_raw(_head_html(mini_coi, version, hide_gutters))) + injected.add(env.docname) + setattr(env, _HEAD_KEY, injected) + + # ---- emit config attr only on the first editor for each (page, env) ---- + # PyScript reads the config once per named environment; repeating it is harmless + # but emitting it only on the first occurrence keeps the HTML clean. + env_configs = getattr(env, _ENV_KEY, set()) + env_key = (env.docname, ed_env) + if env_key not in env_configs: + config_part = f' config="{ed_cfg}"' if ed_cfg else "" + env_configs.add(env_key) + setattr(env, _ENV_KEY, env_configs) + else: + config_part = "" + + # ---- build the editor HTML ---- + code = "\n".join(self.content) + indented = "\n".join(" " + line for line in code.splitlines()) + + editor_html = ( + '
\n' + '
\n' + f'\n" + "
\n" + "
\n" + ) + if ed_target: + editor_html += f'
\n' + + result.append(_raw(editor_html)) + return result + + +# --------------------------------------------------------------------------- +# Extension setup +# --------------------------------------------------------------------------- + + +def setup(app: Sphinx) -> dict: + app.add_config_value("pyscript_version", "2026.2.1", "html") + app.add_config_value("pyscript_env", "shared", "html") + app.add_config_value("pyscript_config", "pyscript.json", "html") + app.add_config_value("pyscript_mini_coi", "mini-coi.js", "html") + app.add_config_value("pyscript_hide_gutters", True, "html") + + app.add_directive("py-editor", PyEditorDirective) + + return { + "version": "0.1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } From 699b3f9dc2dc46eb18c14267a3a4314cd703f14d Mon Sep 17 00:00:00 2001 From: Andrew Annex <2126916+AndrewAnnex@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:28:25 -0700 Subject: [PATCH 02/10] working and started refactor for other_stuff lesson, needs more consolidation and work though --- docs/conf.py | 2 + docs/other_stuff.rst | 1616 ++++++++++++++++---------------- docs/pyscript_editor.py | 99 +- docs/pyscript_min.json | 6 + docs/pyscript_other_stuff.json | 11 + docs/remote_sensing.rst | 6 +- 6 files changed, 901 insertions(+), 839 deletions(-) create mode 100644 docs/pyscript_min.json create mode 100644 docs/pyscript_other_stuff.json diff --git a/docs/conf.py b/docs/conf.py index fe964c7d..0697b1ad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -218,6 +218,8 @@ # directly to the root of the documentation. html_extra_path = [ "pyscript.json", + "pyscript_min.json", + "pyscript_other_stuff.json", "mini-coi.js", ] diff --git a/docs/other_stuff.rst b/docs/other_stuff.rst index 5f98f874..ed36c2f3 100644 --- a/docs/other_stuff.rst +++ b/docs/other_stuff.rst @@ -101,13 +101,21 @@ SpiceyPy API Documentation A SpiceyPy function's parameters specification is available using the built-in Python help system. +.. py-editor:: + :env: other + :config: pyscript_other_stuff.json + :setup: + + import spiceypy + For example, the Python help function -.. code-block:: python +.. py-editor:: + :env: other - import spiceypy - help(spiceypy.str2et) + import spiceypy + help(spiceypy.str2et) describes of the str2et function's parameters, while the document @@ -255,7 +263,7 @@ particular Spice subsystems: time.req windows.req -NAIF Users Guides (\*.ug) describe the proper use of particular SpiceyPy +NAIF Users Guides (\*.ug) describe the proper use of particular Spice command line tools: :: @@ -361,22 +369,11 @@ This SpiceyPy API documentation (the same information as in the website but without hyperlinks) is also available from the Python built-in help system: -:: - - >>> help ( spiceypy.str2et ) - Help on function str2et in module spiceypy.spiceypy: - - str2et(*args, **kwargs) - Convert a string representing an epoch to a double precision - value representing the number of TDB seconds past the J2000 - epoch corresponding to the input epoch. +.. py-editor:: + :env: other - ... - - :param time: A string representing an epoch. - :type time: str - :return: The equivalent value in seconds past J2000, TDB. - :rtype: float + import spiceypy + help(spiceypy.str2et) # hit run button to see help Text kernels @@ -439,9 +436,7 @@ Things to know: signaling an error on any attempt to read non-native text kernels. -Text kernel format - -Scalar assignments. +Text kernel format scalar assignments. .. code-block:: text @@ -513,22 +508,32 @@ First, create a meta text kernel: You can use two versions of a meta kernel with code examples (kpool.tm) in this lesson. Either a kernel with explicit path information: -.. code-block:: text +.. py-editor:: + :env: other - KPL/MK + mk=r""" + KPL/MK - \begindata + \begindata - KERNELS_TO_LOAD = ( 'kernels/spk/de405s.bsp', - 'kernels/pck/pck00008.tpc', - 'kernels/lsk/naif0008.tls' ) + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/spk/de405s.bsp', + 'kernels/pck/pck00008.tpc') + + \begintext + """ + with open('kpool.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file kpool.tm') - \begintext … or a more generic meta kernel using the PATH_VALUES/PATH_SYMBOLS functionality to declare path names as variables: -.. code-block:: text +.. py-editor:: + :env: other + + mk=r""" KPL/MK @@ -558,86 +563,81 @@ functionality to declare path names as variables: '$PCK/pck00008.tpc' ) \begintext + """ + with open('kpool_generic.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file kpool_generic.tm') Now the solution source code: -.. code-block:: python - - from __future__ import print_function - - # - # Import the CSPICE-Python interface. - # - import spiceypy - - def kpool(): - - # - # Assign the path name of the meta kernel to META. - # - META = 'kpool.tm' - - - # - # Load the meta kernel then use KTOTAL to interrogate the SPICE - # kernel subsystem. - # - spiceypy.furnsh( META ) - - - count = spiceypy.ktotal( 'ALL' ) - print( 'Kernel count after load: {0}\n'.format(count)) - - - # - # Loop over the number of files; interrogate the SPICE system - # with spiceypy.kdata for the kernel names and the type. - # 'found' returns a boolean indicating whether any kernel files - # of the specified type were loaded by the kernel subsystem. - # This example ignores checking 'found' as kernels are known - # to be loaded. - # - for i in range(0, count): - [ file, type, source, handle] = spiceypy.kdata(i, 'ALL'); - print( 'File {0}'.format(file) ) - print( 'Type {0}'.format(type) ) - print( 'Source {0}\n'.format(source) ) - - - # - # Unload one kernel then check the count. - # - spiceypy.unload( 'kernels/spk/de405s.bsp') - count = spiceypy.ktotal( 'ALL' ) - - # - # The subsystem should report one less kernel. - # - print( 'Kernel count after one unload: {0}'.format(count)) - - # - # Now unload the meta kernel. This action unloads all - # files listed in the meta kernel. - # - spiceypy.unload( META ) - - - # - # Check the count; spiceypy should return a count of zero. - # - count = spiceypy.ktotal( 'ALL') - print( 'Kernel count after meta unload: {0}'.format(count)) - - - # - # Done. Unload the kernels. - # - spiceypy.kclear - - if __name__ == '__main__': - kpool() - -Run the code example +.. py-editor:: + :env: other + + # + # Import the CSPICE-Python interface. + # + import spiceypy + + # Assign the path name of the meta kernel to META. + def kpool(META='kpool.tm'): + # + # Load the meta kernel then use KTOTAL to interrogate the SPICE + # kernel subsystem. + # + spiceypy.furnsh(META) + + count = spiceypy.ktotal( 'ALL' ) + print( f'Kernel count after load: {count}\n' ) + + # + # Loop over the number of files; interrogate the SPICE system + # with spiceypy.kdata for the kernel names and the type. + # 'found' returns a boolean indicating whether any kernel files + # of the specified type were loaded by the kernel subsystem. + # This example ignores checking 'found' as kernels are known + # to be loaded. + # + for i in range(count): + [ file, type, source, handle] = spiceypy.kdata(i, 'ALL'); + print( 'File {0}'.format(file) ) + print( 'Type {0}'.format(type) ) + print( 'Source {0}\n'.format(source) ) + + # + # Unload one kernel then check the count. + # + spiceypy.unload( 'kernels/spk/de405s.bsp') + count = spiceypy.ktotal( 'ALL' ) + + # + # The subsystem should report one less kernel. + # + print( f'Kernel count after one unload: {count}\n') + + # + # Now unload the meta kernel. This action unloads all + # files listed in the meta kernel. + # + spiceypy.unload( META ) + + # + # Check the count; spiceypy should return a count of zero. + # + count = spiceypy.ktotal( 'ALL') + print( f'Kernel count after meta unload: {count}') + + # + # Done. Unload the kernels. + # + spiceypy.kclear() + + if __name__ == '__main__': + kpool() + kpool(META="kpool_generic.tm") + + + +Run the code example locally or by clicking the run button above. First we see the number of all loaded kernels returned from the spiceypy.ktotal call. @@ -671,6 +671,8 @@ direct load of the kernel with a spiceypy.furnsh call. Kernel count after one unload: 3 Kernel count after meta unload: 0 +this repeats for the kpool_generic.tm file. + Lesson 2: The Kernel Pool ------------------------------ @@ -695,8 +697,10 @@ pool. For the code examples, use this generic text kernel (kervar.tm) containing PCK-type data, kernels to load, and example time strings: -.. code-block:: text +.. py-editor:: + :env: other + mk=r""" KPL/MK Name the kernels to load. Use path symbols. @@ -759,6 +763,10 @@ containing PCK-type data, kernels to load, and example time strings: ) \begintext + """ + with open('kervar.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file kervar.tm') The main references for pool routines are found in the help command, the CSPICE source files or the API documentation for the particular @@ -769,139 +777,139 @@ routines. Code Solution ^^^^^^^^^^^^^ -.. code-block:: python - - from __future__ import print_function - - # - # Import the CSPICE-Python interface. - # - import spiceypy - from spiceypy.utils.support_types import SpiceyError - - def kervar(): +.. py-editor:: + :env: other + + # + # Import the CSPICE-Python interface. + # + import spiceypy + from spiceypy.utils.support_types import SpiceyError + + def kervar(): + + # + # Define the max number of kernel variables + # of concern for this examples. + # + N_ITEMS = 20 + + # + # Load the example kernel containing the kernel variables. + # The kernels defined in KERNELS_TO_LOAD load into the + # kernel pool with this call. + # + spiceypy.furnsh( 'kervar.tm' ) + + # + # Initialize the start value. This value indicates + # index of the first element to return if a kernel + # variable is an array. START = 0 indicates return everything. + # START = 1 indicates return everything but the first element. + # + START = 0 + + # + # Set the template for the variable names to find. Let's + # look for all variables containing the string RING. + # Define this with the wildcard template '*RING*'. Note: + # the template '*RING' would match any variable name + # ending with the RING string. + # + tmplate = '*RING*' + + # + # We're ready to interrogate the kernel pool for the + # variables matching the template. spiceypy.gnpool tells us: + # + # 1. Does the kernel pool contain any variables that + # match the template (value of found). + # 2. If so, how many variables? + # 3. The variable names. (cvals, an array of strings) + # + + try: + cvals = spiceypy.gnpool( tmplate, START, N_ITEMS ) + print( 'Number variables matching template: {0}'.\ + format( len(cvals)) ) + except SpiceyError: + print( 'No kernel variables matched template.' ) + return + + + # + # Okay, now we know something about the kernel pool + # variables of interest to us. Let's find out more... + # + for cval in cvals: + + # + # Use spiceypy.dtpool to return the dimension and type, + # C (character) or N (numeric), of each pool + # variable name in the cvals array. We know the + # kernel data exists. + # + [dim, type] = spiceypy.dtpool( cval ) + + print( '\n' + cval ) + print( ' Number items: {0} Of type: {1}\n'.\ + format(dim, type) ) + + # + # Test character equality, 'N' or 'C'. + # + if type == 'N': - # - # Define the max number of kernel variables - # of concern for this examples. - # - N_ITEMS = 20 + # + # If 'type' equals 'N', we found a numeric array. + # In this case any numeric array will be an array + # of double precision numbers ('doubles'). + # spiceypy.gdpool retrieves doubles from the + # kernel pool. + # + dvars = spiceypy.gdpool( cval, START, N_ITEMS ) + for dvar in dvars: + print(' Numeric value: {0:20.6f}'.format(dvar)) - # - # Load the example kernel containing the kernel variables. - # The kernels defined in KERNELS_TO_LOAD load into the - # kernel pool with this call. - # - spiceypy.furnsh( 'kervar.tm' ) + elif type == 'C': - # - # Initialize the start value. This value indicates - # index of the first element to return if a kernel - # variable is an array. START = 0 indicates return everything. - # START = 1 indicates return everything but the first element. - # - START = 0 - - # - # Set the template for the variable names to find. Let's - # look for all variables containing the string RING. - # Define this with the wildcard template '*RING*'. Note: - # the template '*RING' would match any variable name - # ending with the RING string. - # - tmplate = '*RING*' + # + # If 'type' equals 'C', we found a string array. + # spiceypy.gcpool retrieves string values from the + # kernel pool. + # + cvars = spiceypy.gcpool( cval, START, N_ITEMS ) - # - # We're ready to interrogate the kernel pool for the - # variables matching the template. spiceypy.gnpool tells us: - # - # 1. Does the kernel pool contain any variables that - # match the template (value of found). - # 2. If so, how many variables? - # 3. The variable names. (cvals, an array of strings) - # + for cvar in cvars: + print(' String value: {0}\n'.format(cvar)) - try: - cvals = spiceypy.gnpool( tmplate, START, N_ITEMS ) - print( 'Number variables matching template: {0}'.\ - format( len(cvals)) ) - except SpiceyError: - print( 'No kernel variables matched template.' ) - return + else: + # + # This block should never execute. + # + print('Unknown type. Code error.') - # - # Okay, now we know something about the kernel pool - # variables of interest to us. Let's find out more... - # - for cval in cvals: - - # - # Use spiceypy.dtpool to return the dimension and type, - # C (character) or N (numeric), of each pool - # variable name in the cvals array. We know the - # kernel data exists. - # - [dim, type] = spiceypy.dtpool( cval ) - - print( '\n' + cval ) - print( ' Number items: {0} Of type: {1}\n'.\ - format(dim, type) ) - - # - # Test character equality, 'N' or 'C'. - # - if type == 'N': - - # - # If 'type' equals 'N', we found a numeric array. - # In this case any numeric array will be an array - # of double precision numbers ('doubles'). - # spiceypy.gdpool retrieves doubles from the - # kernel pool. - # - dvars = spiceypy.gdpool( cval, START, N_ITEMS ) - for dvar in dvars: - print(' Numeric value: {0:20.6f}'.format(dvar)) - - elif type == 'C': - - # - # If 'type' equals 'C', we found a string array. - # spiceypy.gcpool retrieves string values from the - # kernel pool. - # - cvars = spiceypy.gcpool( cval, START, N_ITEMS ) - - for cvar in cvars: - print(' String value: {0}\n'.format(cvar)) - - else: - - # - # This block should never execute. - # - print('Unknown type. Code error.') + # + # Now look at the time variable EXAMPLE_TIMES. Extract this + # value as an array of doubles. + # + dvars = spiceypy.gdpool( 'EXAMPLE_TIMES', START, N_ITEMS ) - # - # Now look at the time variable EXAMPLE_TIMES. Extract this - # value as an array of doubles. - # - dvars = spiceypy.gdpool( 'EXAMPLE_TIMES', START, N_ITEMS ) + print( 'EXAMPLE_TIMES' ) - print( 'EXAMPLE_TIMES' ) + for dvar in dvars: + print(' Time value: {0:20.6f}'.format(dvar)) - for dvar in dvars: - print(' Time value: {0:20.6f}'.format(dvar)) + # + # Done. Unload the kernels. + # + spiceypy.kclear() - # - # Done. Unload the kernels. - # - spiceypy.kclear + if __name__ == '__main__': + kervar() - if __name__ == '__main__': - kervar() Run the code example @@ -1011,154 +1019,167 @@ rectangular, cylindrical, and spherical systems. Code Solution ^^^^^^^^^^^^^ -.. code-block:: python - - from __future__ import print_function - from builtins import input - import sys - - # - # Import the CSPICE-Python interface. - # - import spiceypy - - def coord(): - - # - # Define the inertial and non inertial frame names. - # - # Initialize variables or set type. All variables - # used in a PROMPT construct must be initialized - # as strings. - # - INRFRM = 'J2000' - NONFRM = 'IAU_EARTH' - r2d = spiceypy.dpr() - - # - # Load the needed kernels using a spiceypy.furnsh call on the - # meta kernel. - # - spiceypy.furnsh( 'coord.tm' ) - - # - # Prompt the user for a time string. Convert the - # time string to ephemeris time J2000 (ET). - # - timstr = input( 'Time of interest: ') - et = spiceypy.str2et( timstr ) - - # - # Access the kernel pool data for the triaxial radii of the - # Earth, rad[1][0] holds the equatorial radius, rad[1][2] - # the polar radius. - # - rad = spiceypy.bodvrd( 'EARTH', 'RADII', 3 ) - - # - # Calculate the flattening factor for the Earth. - # - # equatorial_radius - polar_radius - # flat = ________________________________ - # - # equatorial_radius - # - flat = (rad[1][0] - rad[1][2])/rad[1][0] - - # - # Make the spiceypy.spkpos call to determine the apparent - # position of the Moon w.r.t. to the Earth at 'et' in the - # inertial frame. - # - [pos, ltime] = spiceypy.spkpos('MOON', et, INRFRM, - 'LT+S','EARTH' ) - - # - # Show the current frame and time. - # - print( ' Time : {0}'.format(timstr) ) - print( ' Inertial Frame: {0}\n'.format(INRFRM) ) - - # - # First convert the position vector - # X = pos(1), Y = pos(2), Z = pos(3), to RA/DEC. - # - [ range, ra, dec ] = spiceypy.recrad( pos ) - - print(' Range/Ra/Dec' ) - print(' Range: {0:20.6f}'.format(range) ) - print(' RA : {0:20.6f}'.format(ra * r2d) ) - print(' DEC : {0:20.6f}'.format(dec* r2d) ) +.. py-editor:: + :env: other + + mk=r""" + KPL/MK + \begindata + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/spk/de405s.bsp', + 'kernels/pck/pck00008.tpc') + """ + with open('coord.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file corrd.tm') + + # + # Import the CSPICE-Python interface. + # + import spiceypy + + def coord(timstr: str): + # + # Define the inertial and non inertial frame names. + # + # Initialize variables or set type. All variables + # used in a PROMPT construct must be initialized + # as strings. + # + INRFRM = 'J2000' + NONFRM = 'IAU_EARTH' + r2d = spiceypy.dpr() + + # + # Load the needed kernels using a spiceypy.furnsh call on the + # meta kernel. + # + spiceypy.furnsh( 'coord.tm' ) + + # + # Convert the time string to ephemeris time J2000 (ET). + # + et = spiceypy.str2et( timstr ) + + # + # Access the kernel pool data for the triaxial radii of the + # Earth, rad[1][0] holds the equatorial radius, rad[1][2] + # the polar radius. + # + rad = spiceypy.bodvrd( 'EARTH', 'RADII', 3 ) + + # + # Calculate the flattening factor for the Earth. + # + # equatorial_radius - polar_radius + # flat = ________________________________ + # + # equatorial_radius + # + flat = (rad[1][0] - rad[1][2])/rad[1][0] + + # + # Make the spiceypy.spkpos call to determine the apparent + # position of the Moon w.r.t. to the Earth at 'et' in the + # inertial frame. + # + [pos, ltime] = spiceypy.spkpos('MOON', et, INRFRM, + 'LT+S','EARTH' ) + + # + # Show the current frame and time. + # + print( ' Time : {0}'.format(timstr) ) + print( ' Inertial Frame: {0}\n'.format(INRFRM) ) + + # + # First convert the position vector + # X = pos(1), Y = pos(2), Z = pos(3), to RA/DEC. + # + [ range, ra, dec ] = spiceypy.recrad( pos ) + + print(' Range/Ra/Dec' ) + print(' Range: {0:20.6f}'.format(range) ) + print(' RA : {0:20.6f}'.format(ra * r2d) ) + print(' DEC : {0:20.6f}'.format(dec* r2d) ) + + # + # ...latitudinal coordinates... + # + [ range, lon, lat ] = spiceypy.reclat( pos ) + print(' Latitudinal ' ) + print(' Rad : {0:20.6f}'.format(range) ) + print(' Lon : {0:20.6f}'.format(lon * r2d) ) + print(' Lat : {0:20.6f}'.format(lat * r2d) ) + + # + # ...spherical coordinates use the colatitude, + # the angle from the Z axis. + # + [ range, colat, lon ] = spiceypy.recsph( pos ) + print( ' Spherical' ) + print(' Rad : {0:20.6f}'.format(range) ) + print(' Lon : {0:20.6f}'.format(lon * r2d) ) + print(' Colat: {0:20.6f}'.format(colat * r2d) ) + + # + # Make the spiceypy.spkpos call to determine the apparent + # position of the Moon w.r.t. to the Earth at 'et' in the + # non-inertial, body fixed, frame. + # + [pos, ltime] = spiceypy.spkpos('MOON', et, NONFRM, + 'LT+S','EARTH') + + print() + print( ' Non-inertial Frame: {0}'.format(NONFRM) ) + + # + # ...latitudinal coordinates... + # + [ range, lon, lat ] = spiceypy.reclat( pos ) + print(' Latitudinal ' ) + print(' Rad : {0:20.6f}'.format(range) ) + print(' Lon : {0:20.6f}'.format(lon * r2d) ) + print(' Lat : {0:20.6f}'.format(lat * r2d) ) + + # + # ...spherical coordinates use the colatitude, + # the angle from the Z axis. + # + [ range, colat, lon ] = spiceypy.recsph( pos ) + print( ' Spherical' ) + print(' Rad : {0:20.6f}'.format(range) ) + print(' Lon : {0:20.6f}'.format(lon * r2d) ) + print(' Colat: {0:20.6f}'.format(colat * r2d) ) + + # + # ...finally, convert the position to geodetic coordinates. + # + [ lon, lat, range ] = spiceypy.recgeo( pos, rad[1][0], flat ) + print( ' Geodetic' ) + print(' Rad : {0:20.6f}'.format(range) ) + print(' Lon : {0:20.6f}'.format(lon * r2d) ) + print(' Lat : {0:20.6f}'.format(lat * r2d) ) + print() + + # + # Done. Unload the kernels. + # + spiceypy.kclear() + + # if running locally, uncomment below + #from builtins import input + #if __name__ == '__main__': + # timstr = input( 'Time of interest: ') + # coord(timstr) + +Run the code example: + +.. py-editor:: + :env: other + + coord("Feb 3 2002 TDB") - # - # ...latitudinal coordinates... - # - [ range, lon, lat ] = spiceypy.reclat( pos ) - print(' Latitudinal ' ) - print(' Rad : {0:20.6f}'.format(range) ) - print(' Lon : {0:20.6f}'.format(lon * r2d) ) - print(' Lat : {0:20.6f}'.format(lat * r2d) ) - - # - # ...spherical coordinates use the colatitude, - # the angle from the Z axis. - # - [ range, colat, lon ] = spiceypy.recsph( pos ) - print( ' Spherical' ) - print(' Rad : {0:20.6f}'.format(range) ) - print(' Lon : {0:20.6f}'.format(lon * r2d) ) - print(' Colat: {0:20.6f}'.format(colat * r2d) ) - - # - # Make the spiceypy.spkpos call to determine the apparent - # position of the Moon w.r.t. to the Earth at 'et' in the - # non-inertial, body fixed, frame. - # - [pos, ltime] = spiceypy.spkpos('MOON', et, NONFRM, - 'LT+S','EARTH') - - print() - print( ' Non-inertial Frame: {0}'.format(NONFRM) ) - - # - # ...latitudinal coordinates... - # - [ range, lon, lat ] = spiceypy.reclat( pos ) - print(' Latitudinal ' ) - print(' Rad : {0:20.6f}'.format(range) ) - print(' Lon : {0:20.6f}'.format(lon * r2d) ) - print(' Lat : {0:20.6f}'.format(lat * r2d) ) - - # - # ...spherical coordinates use the colatitude, - # the angle from the Z axis. - # - [ range, colat, lon ] = spiceypy.recsph( pos ) - print( ' Spherical' ) - print(' Rad : {0:20.6f}'.format(range) ) - print(' Lon : {0:20.6f}'.format(lon * r2d) ) - print(' Colat: {0:20.6f}'.format(colat * r2d) ) - - # - # ...finally, convert the position to geodetic coordinates. - # - [ lon, lat, range ] = spiceypy.recgeo( pos, rad[1][0], flat ) - print( ' Geodetic' ) - print(' Rad : {0:20.6f}'.format(range) ) - print(' Lon : {0:20.6f}'.format(lon * r2d) ) - print(' Lat : {0:20.6f}'.format(lat * r2d) ) - print() - - # - # Done. Unload the kernels. - # - spiceypy.kclear - - - if __name__ == '__main__': - coord() - -Run the code example Input “Feb 3 2002 TDB” to calculate the Moon's position. (the 'TDB' tag indicates a Barycentric Dynamical Time value). @@ -1302,119 +1323,127 @@ Code Solution Caution: Be sure to assign sufficient string lengths for time formats/pictures. -.. code-block:: python - - from __future__ import print_function - - # - # Import the CSPICE-Python interface. - # - import spiceypy - - def xtic(): - - # - # Assign the META variable to the name of the meta-kernel - # that contains the LSK kernel and create an arbitrary - # time string. - # - CALSTR = 'Mar 15, 2003 12:34:56.789 AM PST' - META = 'xtic.tm' - AMBIGSTR = 'Mar 15, 79 12:34:56' - T_FORMAT1 = 'Wkd Mon DD HR:MN:SC PDT YYYY ::UTC-7' - T_FORMAT2 = 'Wkd Mon DD HR:MN ::UTC-7 YR (JULIAND.##### JDUTC)' - - # - # Load the meta-kernel. - # - spiceypy.furnsh( META ) - print( 'Original time string : {0}'.format(CALSTR) ) - - # - # Convert the time string to the number of ephemeris - # seconds past the J2000 epoch. This is the most common - # internal time representation used by the CSPICE - # system; CSPICE refers to this as ephemeris time (ET). - # - et = spiceypy.str2et( CALSTR ) - print( 'Corresponding ET : {0:20.6f}\n'.format(et) ) - - # - # Make a picture of an output format. Describe a Unix-like - # time string then send the picture and the 'et' value through - # spiceypy.timout to format and convert the ET representation - # of the time string into the form described in - # spiceypy.timout. The '::UTC-7' token indicates the time - # zone for the `timstr' output - PDT. 'PDT' is part of the - # output, but not a time system token. - # - timstr = spiceypy.timout( et, T_FORMAT1) - print( 'Time in string format 1 : {0}'.format(timstr) ) - - timstr = spiceypy.timout( et, T_FORMAT2) - print( 'Time in string format 2 : {0}'.format(timstr) ) - - # - # Why create a picture by hand when spiceypy can do it for - # you? Input a string to spiceypy.tpictr with the format of - # interest. `ok' returns a boolean indicating whether an - # error occurred while parsing the picture string, if so, - # an error diagnostic message returns in 'xerror'. In this - # example the picture string is known as correct. - # - pic = '12:34:56.789 P.M. PDT January 1, 2006' - [ pictr, ok, xerror] = spiceypy.tpictr(pic) - - if not bool(ok): - print( xerror ) - exit - - - timstr = spiceypy.timout( et, pictr) - print( 'Time in string format 3 : {0}'.format( timstr ) ) - - # - # Two digit year representations often cause problems due to - # the ambiguity of the century. The routine spiceypy.tsetyr - # gives the user the ability to set a default range for 2 - # digit year representation. SPICE uses 1969AD as the default - # start year so the numbers inclusive of 69 to 99 represent - # years 1969AD to 1999AD, the numbers inclusive of 00 to 68 - # represent years 2000AD to 2068AD. - # - # The defined time string 'AMBIGSTR' contains a two-digit - # year. Since the SPICE base year is 1969, the time subsystem - # interprets the string as 1979. - # - et1 = spiceypy.str2et( AMBIGSTR ) - - # - # Set 1980 as the base year causes SPICE to interpret the - # time string's "79" as 2079. - # - spiceypy.tsetyr( 1980 ) - et2 = spiceypy.str2et( AMBIGSTR ) - - # - # Calculate the number of years between the two ET - # representations, ~100. - # - print( 'Years between evaluations: {0:20.6f}'.\ - format( (et2 - et1)/spiceypy.jyear())) - - # - # Reset the default year to 1969. - # - spiceypy.tsetyr( 1969 ) - - # - # Done. Unload the kernels. - # - spiceypy.kclear - - - if __name__ == '__main__': - xtic() +.. py-editor:: + :env: other + + mk=r""" + KPL/MK + \begindata + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/spk/de405s.bsp', + 'kernels/pck/pck00008.tpc') + """ + with open('xtic.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file corrd.tm') + # + # Import the CSPICE-Python interface. + # + import spiceypy + + def xtic(): + + # + # Assign the META variable to the name of the meta-kernel + # that contains the LSK kernel and create an arbitrary + # time string. + # + CALSTR = 'Mar 15, 2003 12:34:56.789 AM PST' + META = 'xtic.tm' + AMBIGSTR = 'Mar 15, 79 12:34:56' + T_FORMAT1 = 'Wkd Mon DD HR:MN:SC PDT YYYY ::UTC-7' + T_FORMAT2 = 'Wkd Mon DD HR:MN ::UTC-7 YR (JULIAND.##### JDUTC)' + + # + # Load the meta-kernel. + # + spiceypy.furnsh( META ) + print( f'Original time string : f{CALSTR}' ) + + # + # Convert the time string to the number of ephemeris + # seconds past the J2000 epoch. This is the most common + # internal time representation used by the CSPICE + # system; CSPICE refers to this as ephemeris time (ET). + # + et = spiceypy.str2et( CALSTR ) + print( f'Corresponding ET : {et:20.6f}\n' ) + + # + # Make a picture of an output format. Describe a Unix-like + # time string then send the picture and the 'et' value through + # spiceypy.timout to format and convert the ET representation + # of the time string into the form described in + # spiceypy.timout. The '::UTC-7' token indicates the time + # zone for the `timstr' output - PDT. 'PDT' is part of the + # output, but not a time system token. + # + timstr = spiceypy.timout( et, T_FORMAT1) + print( f'Time in string format 1 : {timstr}' ) + + timstr = spiceypy.timout( et, T_FORMAT2) + print( f'Time in string format 2 : {timstr}' ) + + # + # Why create a picture by hand when spiceypy can do it for + # you? Input a string to spiceypy.tpictr with the format of + # interest. `ok' returns a boolean indicating whether an + # error occurred while parsing the picture string, if so, + # an error diagnostic message returns in 'xerror'. In this + # example the picture string is known as correct. + # + pic = '12:34:56.789 P.M. PDT January 1, 2006' + [ pictr, ok, xerror] = spiceypy.tpictr(pic) + + if not bool(ok): + print( xerror ) + return + + + timstr = spiceypy.timout( et, pictr) + print( f'Time in string format 3 : {timstr}' ) + + # + # Two digit year representations often cause problems due to + # the ambiguity of the century. The routine spiceypy.tsetyr + # gives the user the ability to set a default range for 2 + # digit year representation. SPICE uses 1969AD as the default + # start year so the numbers inclusive of 69 to 99 represent + # years 1969AD to 1999AD, the numbers inclusive of 00 to 68 + # represent years 2000AD to 2068AD. + # + # The defined time string 'AMBIGSTR' contains a two-digit + # year. Since the SPICE base year is 1969, the time subsystem + # interprets the string as 1979. + # + et1 = spiceypy.str2et( AMBIGSTR ) + + # + # Set 1980 as the base year causes SPICE to interpret the + # time string's "79" as 2079. + # + spiceypy.tsetyr( 1980 ) + et2 = spiceypy.str2et( AMBIGSTR ) + + # + # Calculate the number of years between the two ET + # representations, ~100. + # + years = (et2 - et1)/spiceypy.jyear() + print( f'Years between evaluations: {years:20.6f}' ) + + # + # Reset the default year to 1969. + # + spiceypy.tsetyr( 1969 ) + + # + # Done. Unload the kernels. + # + spiceypy.kclear() + + if __name__ == '__main__': + xtic() Run the code example @@ -1460,98 +1489,68 @@ respond in an appropriate manner. Code Solution ^^^^^^^^^^^^^ -.. code-block:: python - - from __future__ import print_function - from builtins import input - - # - # Import the CSPICE-Python interface. - # - import spiceypy - from spiceypy.utils.support_types import SpiceyError - - def aderr(): - - # - # Set initial parameters. - # - SPICETRUE = True - SPICEFALSE= False - doloop = SPICETRUE - - # - # Load the data we need for state evaluation. - # - spiceypy.furnsh( 'aderr.tm' ) - - # - # Start our input query loop to the user. - # - while (doloop): - - # - # For simplicity, we request only one input. - # The program calculates the state vector from - # Earth to the user specified target 'targ' in the - # J2000 frame, at ephemeris time zero, using - # aberration correction LT+S (light time plus - # stellar aberration). - # - targ = input( 'Target: ' ) - - - if targ == 'NONE': - # - # An exit condition. If the user inputs NONE - # for a target name, set the loop to stop... - # - doloop = SPICEFALSE - - else: - - # - # ...otherwise evaluate the state between the Earth - # and the target. Initialize an error handler. - # - try: - - # - # Perform the state lookup. - # - [state, ltime] = spiceypy.spkezr(targ, 0., 'J2000', - 'LT+S', 'EARTH') - - # - # No error, output the state. - # - print( 'R : {0:20.6f} {1:20.6f} ' - '{2:20.5f}'.format(*state[0:3])) - print( 'V : {0:20.6f} {1:20.6f} ' - '{2:20.6f}'.format(*state[3:6]) ) - print( 'LT: {0:20.6f}\n'.format(float(ltime))) - - except SpiceyError as err: - - # - # What if spiceypy.spkezr signaled an error? - # Then spiceypy signals an error to python. - # - # Examine the value of 'e' to retrieve the - # error message. - # - print( err ) - print( ) - - - # - # Done. Unload the kernels. - # - spiceypy.kclear - +.. py-editor:: + :env: other + + mk=r""" + KPL/MK + \begindata + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/spk/de405s.bsp', + 'kernels/pck/pck00008.tpc') + """ + with open('aderr.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file aderr.tm') + print('') + # + # Import the CSPICE-Python interface. + # + import spiceypy + + # For simplicity, we request only one input. + # The program calculates the state vector from + # Earth to the user specified target 'targ' in the + # J2000 frame, at ephemeris time zero, using + # aberration correction LT+S (light time plus + # stellar aberration). + def aderr(targ: str): + print(f'Target: {targ}') + spiceypy.furnsh( 'aderr.tm' ) + try: + # + # Perform the state lookup. + # + state, ltime = spiceypy.spkezr(targ, 0.0, 'J2000', 'LT+S', 'EARTH') + # + # No error, output the state. + # + r0, r1, r2, v0, v1, v2 = state + print( f'R : {r0:20.6f} {r1:20.6f} {r2:20.6f}' ) + print( f'V : {v0:20.6f} {v1:20.6f} {v2:20.6f}' ) + print( f'LT: {ltime:20.6f}\n' ) + except spiceypy.SpiceyError as err: + # + # What if spiceypy.spkezr signaled an error? + # Then spiceypy signals an error to python. + # + # Examine the value of 'err' to retrieve the + # error message. + # + print( err ) + print( ) + finally: + # + # Done. Unload the kernels. + # + spiceypy.kclear() + + if __name__ == '__main__': + aderr('Moon') + aderr('Mars') + aderr('Pluto barycenter') + aderr('Puck') - if __name__ == '__main__': - aderr() Run the code example @@ -1564,6 +1563,7 @@ the velocity of the body in kilometers per second, and the 'LT' marker identifies the one-way light time between the bodies at the requested evaluation time. + .. code-block:: text Target: Moon @@ -1586,7 +1586,7 @@ evaluation time. ===================================================================== =========== - Toolkit version: N0066 + Toolkit version: N0067 SPICE(SPKINSUFFDATA) -- @@ -1613,6 +1613,12 @@ time 2000 JAN 01 12:00:00.000 (the requested time, ephemeris time zero). Try another look-up, this time for “Casper” +.. py-editor:: + :env: other + + aderr('Casper') + + .. code-block:: text Target: Casper @@ -1641,6 +1647,12 @@ information on a body named 'Casper.' Another look-up, this time, “Venus”. +.. py-editor:: + :env: other + + aderr('Venus') + + .. code-block:: text Target: Venus @@ -1710,145 +1722,156 @@ set of a number of time intervals. Code Solution ^^^^^^^^^^^^^ -.. code-block:: python - - from __future__ import print_function - - # - # Import the CSPICE-Python interface. - # - import spiceypy - - def win(): - - MAXSIZ = 8 - - # - # Define a set of time intervals. For the purposes of this - # tutorial program, define time intervals representing - # an unobscured line of sight between a ground station - # and some body. - # - los = [ 'Jan 1, 2003 22:15:02', 'Jan 2, 2003 4:43:29', - 'Jan 4, 2003 9:55:30', 'Jan 4, 2003 11:26:52', - 'Jan 5, 2003 11:09:17', 'Jan 5, 2003 13:00:41', - 'Jan 6, 2003 00:08:13', 'Jan 6, 2003 2:18:01' ] - - # - # A second set of intervals representing the times for which - # an acceptable phase angle exists between the ground station, - # the body and the Sun. - # - phase = [ 'Jan 2, 2003 00:03:30', 'Jan 2, 2003 19:00:00', - 'Jan 3, 2003 8:00:00', 'Jan 3, 2003 9:50:00', - 'Jan 5, 2003 12:00:00', 'Jan 5, 2003 12:45:00', - 'Jan 6, 2003 00:30:00', 'Jan 6, 2003 23:00:00' ] - - # - # Load our meta kernel for the leapseconds data. - # - spiceypy.furnsh( 'win.tm' ) - - # - # SPICE windows consist of double precision values; convert - # the string time tags defined in the 'los' and 'phase' - # arrays to double precision ET. Store the double values - # in the 'loswin' and 'phswin' windows. - # - los_et = spiceypy.str2et( los ) - phs_et = spiceypy.str2et( phase ) - - loswin = spiceypy.stypes.SPICEDOUBLE_CELL( MAXSIZ ) - phswin = spiceypy.stypes.SPICEDOUBLE_CELL( MAXSIZ ) - - for i in range(0, int( MAXSIZ/2 ) ): - spiceypy.wninsd( los_et[2*i], los_et[2*i+1], loswin ) - spiceypy.wninsd( phs_et[2*i], phs_et[2*i+1], phswin ) - - spiceypy.wnvald( MAXSIZ, MAXSIZ, loswin ) - spiceypy.wnvald( MAXSIZ, MAXSIZ, phswin ) - - # - # The issue for consideration, at what times do line of - # sight events coincide with acceptable phase angles? - # Perform the set operation AND on loswin, phswin, - # (the intersection of the time intervals) - # place the results in the window 'sched'. - # - sched = spiceypy.wnintd( loswin, phswin ) - - print( 'Number data values in sched : ' - '{0:2d}'.format(spiceypy.card(sched)) ) - - # - # Output the results. The number of intervals in 'sched' - # is half the number of data points (the cardinality). - # - print( ' ' ) - print( 'Time intervals meeting defined criterion.' ) - - for i in range( spiceypy.card(sched)//2): - - # - # Extract from the derived 'sched' the values defining the - # time intervals. - # - [left, right ] = spiceypy.wnfetd( sched, i ) - - # - # Convert the ET values to UTC for human comprehension. - # - utcstr_l = spiceypy.et2utc( left , 'C', 3 ) - utcstr_r = spiceypy.et2utc( right, 'C', 3 ) - - # - # Output the UTC string and the corresponding index - # for the interval. - # - print( '{0:2d} {1} {2}'.format(i, utcstr_l, utcstr_r)) - - - # - # Summarize the 'sched' window. - # - [meas, avg, stddev, small, large] = spiceypy.wnsumd( sched ) - - print( '\nSummary of sched window\n' ) - - print( 'o Total measure of sched : {0:16.6f}'.format(meas)) - print( 'o Average measure of sched : {0:16.6f}'.format(avg)) - print( 'o Standard deviation of ' ) - print( ' the measures in sched : ' - '{0:16.6f}'.format(stddev)) - - # - # The values for small and large refer to the indexes of the - # values in the window ('sched'). The shortest interval is - # - # [ sched.base[ sched.data + small] - # sched.base[ sched.data + small +1] ]; - # - # the longest is - # - # [ sched.base[ sched.data + large] - # sched.base[ sched.data + large +1] ]; - # - # Output the interval indexes for the shortest and longest - # intervals. As Python bases an array index on 0, the interval - # index is half the array index. - # - print( 'o Index of shortest interval: ' - '{0:2d}'.format(int(small/2)) ) - print( 'o Index of longest interval : ' - '{0:2d}'.format(int(large/2)) ) - - # - # Done. Unload the kernels. - # - spiceypy.kclear - - if __name__ == '__main__': - win() +.. py-editor:: + :env: other + + mk=r""" + KPL/MK + \begindata + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/spk/de405s.bsp', + 'kernels/pck/pck00008.tpc') + """ + with open('win.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file win.tm') + print('') + + # + # Import the CSPICE-Python interface. + # + import spiceypy + + def win(): + + MAXSIZ = 8 + + # + # Define a set of time intervals. For the purposes of this + # tutorial program, define time intervals representing + # an unobscured line of sight between a ground station + # and some body. + # + los = [ 'Jan 1, 2003 22:15:02', 'Jan 2, 2003 4:43:29', + 'Jan 4, 2003 9:55:30', 'Jan 4, 2003 11:26:52', + 'Jan 5, 2003 11:09:17', 'Jan 5, 2003 13:00:41', + 'Jan 6, 2003 00:08:13', 'Jan 6, 2003 2:18:01' ] + + # + # A second set of intervals representing the times for which + # an acceptable phase angle exists between the ground station, + # the body and the Sun. + # + phase = [ 'Jan 2, 2003 00:03:30', 'Jan 2, 2003 19:00:00', + 'Jan 3, 2003 8:00:00', 'Jan 3, 2003 9:50:00', + 'Jan 5, 2003 12:00:00', 'Jan 5, 2003 12:45:00', + 'Jan 6, 2003 00:30:00', 'Jan 6, 2003 23:00:00' ] + + # + # Load our meta kernel for the leapseconds data. + # + spiceypy.furnsh( 'win.tm' ) + + # + # SPICE windows consist of double precision values; convert + # the string time tags defined in the 'los' and 'phase' + # arrays to double precision ET. Store the double values + # in the 'loswin' and 'phswin' windows. + # + los_et = spiceypy.str2et( los ) + phs_et = spiceypy.str2et( phase ) + + loswin = spiceypy.stypes.SPICEDOUBLE_CELL( MAXSIZ ) + phswin = spiceypy.stypes.SPICEDOUBLE_CELL( MAXSIZ ) + + for i in range(0, int( MAXSIZ/2 ) ): + spiceypy.wninsd( los_et[2*i], los_et[2*i+1], loswin ) + spiceypy.wninsd( phs_et[2*i], phs_et[2*i+1], phswin ) + + spiceypy.wnvald( MAXSIZ, MAXSIZ, loswin ) + spiceypy.wnvald( MAXSIZ, MAXSIZ, phswin ) + + # + # The issue for consideration, at what times do line of + # sight events coincide with acceptable phase angles? + # Perform the set operation AND on loswin, phswin, + # (the intersection of the time intervals) + # place the results in the window 'sched'. + # + sched = spiceypy.wnintd( loswin, phswin ) + + print( 'Number data values in sched : ' + '{0:2d}'.format(spiceypy.card(sched)) ) + + # + # Output the results. The number of intervals in 'sched' + # is half the number of data points (the cardinality). + # + print( ' ' ) + print( 'Time intervals meeting defined criterion.' ) + + for i in range( spiceypy.card(sched)//2): + + # + # Extract from the derived 'sched' the values defining the + # time intervals. + # + [left, right ] = spiceypy.wnfetd( sched, i ) + + # + # Convert the ET values to UTC for human comprehension. + # + utcstr_l = spiceypy.et2utc( left , 'C', 3 ) + utcstr_r = spiceypy.et2utc( right, 'C', 3 ) + + # + # Output the UTC string and the corresponding index + # for the interval. + # + print( '{0:2d} {1} {2}'.format(i, utcstr_l, utcstr_r)) + + + # + # Summarize the 'sched' window. + # + [meas, avg, stddev, small, large] = spiceypy.wnsumd( sched ) + + print( '\nSummary of sched window\n' ) + + print( 'o Total measure of sched : {0:16.6f}'.format(meas)) + print( 'o Average measure of sched : {0:16.6f}'.format(avg)) + print( 'o Standard deviation of ' ) + print( ' the measures in sched : ' + '{0:16.6f}'.format(stddev)) + + # + # The values for small and large refer to the indexes of the + # values in the window ('sched'). The shortest interval is + # + # [ sched.base[ sched.data + small] + # sched.base[ sched.data + small +1] ]; + # + # the longest is + # + # [ sched.base[ sched.data + large] + # sched.base[ sched.data + large +1] ]; + # + # Output the interval indexes for the shortest and longest + # intervals. As Python bases an array index on 0, the interval + # index is half the array index. + # + print( 'o Index of shortest interval: ' + '{0:2d}'.format(int(small/2)) ) + print( 'o Index of longest interval : ' + '{0:2d}'.format(int(large/2)) ) + + # + # Done. Unload the kernels. + # + spiceypy.kclear() + + if __name__ == '__main__': + win() Run the code example @@ -1950,106 +1973,58 @@ often used in astrodynamics, time calculations, and geometry. Code Solution ^^^^^^^^^^^^^ -.. code-block:: python - - from __future__ import print_function - from builtins import input - - # - # Import the CSPICE-Python interface. - # - import spiceypy - - - def tostan(alias): - - value = alias - - # - # As a convenience, let's alias a few common terms - # to their appropriate counterpart. - # - if alias == 'meter': - - # - # First, a 'meter' by any other name is a - # 'METER' and smells as sweet ... - # - value = 'METERS' - - elif (alias == 'klicks') \ - or (alias == 'kilometers') \ - or (alias =='kilometer'): - - # - # ... 'klicks' and 'KILOMETERS' and 'KILOMETER' - # identifies 'KM'.... - # - value = 'KM' - - elif alias == 'secs': - - # - # ... 'secs' to 'SECONDS'. - # - value = 'SECONDS' - - elif alias == 'miles': - - # - # ... and finally 'miles' to 'STATUTE_MILES'. - # Normal people think in statute miles. - # Only sailors think in nautical miles - one - # minute of arc at the equator. - # - value = 'STATUTE_MILES' +.. py-editor:: + :env: other + + # + # Import the CSPICE-Python interface. + # + import spiceypy + + aliases = { + 'meter': 'METER', + 'klicks': 'KM', + 'kilometers': 'KM', + 'kilometer': 'KM', + 'secs': 'SECONDS', + 'miles': 'STATUTE_MILES' + } + + def tostan(alias): + return aliases.get(alias, alias) + + def units(funits, fvalue, tunits): + # + # Display the Toolkit version string with a spiceypy.tkvrsn + # call. + # + vers = spiceypy.tkvrsn( 'TOOLKIT' ) + print('\nConvert demo program compiled against CSPICE Toolkit ' + vers) + # + # The user first inputs the name of a unit of measure. + # Send the name through tostan for de-aliasing. + # + print(f'From Units : {funits}') + funits = tostan( funits ) + # + # Input a double precision value to express in a new + # unit format. + # + print(f'From Value : {fvalue}') + # + # Now the user inputs the name of the output units. + # Again we send the units name through tostan for + # de-aliasing. + # + print(f'To Units : {tunits}') + tunits = tostan( tunits ) + tvalue = spiceypy.convrt( fvalue, funits, tunits) + print( '{0:12.5f} {1}'.format(tvalue, tunits) ) + + if __name__ == '__main__': + units('klicks', 3, 'miles') + units('miles', 26.2, 'km') - else: - pass - - - # - # Much better. Now return. If the input matched - # none of the aliases, this function did nothing. - # - return value - - def units(): - - # - # Display the Toolkit version string with a spiceypy.tkvrsn - # call. - # - vers = spiceypy.tkvrsn( 'TOOLKIT' ) - print('\nConvert demo program compiled against CSPICE ' - 'Toolkit ' + vers) - - # - # The user first inputs the name of a unit of measure. - # Send the name through tostan for de-aliasing. - # - funits = input( 'From Units : ' ) - funits = tostan( funits ) - - # - # Input a double precision value to express in a new - # unit format. - # - fvalue = float(input( 'From Value : ' )) - - # - # Now the user inputs the name of the output units. - # Again we send the units name through tostan for - # de-aliasing. - # - tunits = input( 'To Units : ' ) - tunits = tostan( tunits ) - - tvalue = spiceypy.convrt( fvalue, funits, tunits) - print( '{0:12.5f} {1}'.format(tvalue, tunits) ) - - if __name__ == '__main__': - units() Run the code example @@ -2092,9 +2067,8 @@ calculate some rudimentary values. Code Solution ^^^^^^^^^^^^^ -.. code-block:: python - - from __future__ import print_function +.. py-editor:: + :env: other # # Import the CSPICE-Python interface. diff --git a/docs/pyscript_editor.py b/docs/pyscript_editor.py index 47d9e12c..21549e7d 100644 --- a/docs/pyscript_editor.py +++ b/docs/pyscript_editor.py @@ -5,16 +5,24 @@ wrapped in the same ``div.highlight.highlight-python`` structure that Sphinx/Pygments produces, so it inherits your theme's code-block styling. +The hidden ``
`` inside each editor div is compatible with
+``sphinx_copybutton``: that extension's JS selector finds ``div.highlight pre``
+and wires a clipboard copy button automatically.
+
 Usage in conf.py
 ----------------
     extensions = [..., "pyscript_editor"]
 
     # Optional global defaults (all overridable per-directive):
-    pyscript_version  = "2026.2.1"          # PyScript release
-    pyscript_env      = "shared"            # py-editor env attribute
-    pyscript_config   = "pyscript.json"     # py-editor config attribute
-    pyscript_mini_coi = "mini-coi.js"       # path to mini-coi shim;
-                                            # set to "" to skip
+    pyscript_version      = "2026.2.1"   # PyScript release tag
+    pyscript_env          = "shared"     # default py-editor env name
+    pyscript_config       = "pyscript.json"  # default PyScript config file;
+                                             # set to "" to omit
+    pyscript_mini_coi     = "mini-coi.js"   # path to mini-coi shim;
+                                             # set to "" to skip
+    pyscript_hide_gutters  = True        # hide CodeMirror line-number gutters
+    pyscript_hide_env_label = True       # hide the "pyodide-" label
+                                         # rendered above each editor box
 
 Usage in .rst files
 -------------------
@@ -28,17 +36,40 @@
 Override any option per block::
 
     .. py-editor::
-        :env: isolated
-        :config: other.json
+        :env: myenv
+        :config: my_pyscript.json
 
         print("hello")
 
-The ``mini-coi.js`` script and the PyScript stylesheet/module are injected
-only once per page, no matter how many ``.. py-editor::`` directives appear.
+Directive options
+-----------------
+:env:    PyScript environment name (``env=`` attribute on ``')
@@ -96,6 +134,8 @@ def _head_html(mini_coi: str, version: str, hide_gutters: bool) -> str:
         f''
     )
+    if hide_env_label:
+        parts.append(_HIDE_ENV_LABEL_CSS)
     if hide_gutters:
         parts.append(_HIDE_GUTTERS_JS)
     return "\n".join(parts) + "\n"
@@ -115,6 +155,7 @@ class PyEditorDirective(Directive):
         "env": directives.unchanged,
         "config": directives.unchanged,
         "target": directives.unchanged,
+        "setup": directives.flag,
     }
 
     def run(self) -> list[nodes.Node]:
@@ -125,25 +166,37 @@ def run(self) -> list[nodes.Node]:
         version = cfg.pyscript_version
         mini_coi = cfg.pyscript_mini_coi
         hide_gutters = cfg.pyscript_hide_gutters
+        hide_env_label = cfg.pyscript_hide_env_label
         ed_env = self.options.get("env", cfg.pyscript_env)
         ed_cfg = self.options.get("config", cfg.pyscript_config)
         ed_target = self.options.get("target", None)
+        ed_setup = "setup" in self.options
 
         result: list[nodes.Node] = []
 
         # ---- inject  assets once per document ----
         injected = getattr(env, _HEAD_KEY, set())
         if env.docname not in injected:
-            result.append(_raw(_head_html(mini_coi, version, hide_gutters)))
+            result.append(_raw(_head_html(mini_coi, version, hide_gutters, hide_env_label)))
             injected.add(env.docname)
             setattr(env, _HEAD_KEY, injected)
 
-        # ---- emit config attr only on the first editor for each (page, env) ----
-        # PyScript reads the config once per named environment; repeating it is harmless
-        # but emitting it only on the first occurrence keeps the HTML clean.
+        # ---- emit config= exactly once per (page, env) pair ----
+        # Setup blocks own config for their env and must declare it explicitly.
+        # Regular blocks get config= only if no setup block has claimed the env.
+        if ed_setup and "config" not in self.options:
+            raise self.error(":setup: requires :config: to be explicitly specified")
+
+        setup_envs = getattr(env, _SETUP_ENV_KEY, set())
         env_configs = getattr(env, _ENV_KEY, set())
         env_key = (env.docname, ed_env)
-        if env_key not in env_configs:
+        if ed_setup:
+            config_part = f' config="{ed_cfg}"'
+            setup_envs.add(env_key)
+            env_configs.add(env_key)
+            setattr(env, _SETUP_ENV_KEY, setup_envs)
+            setattr(env, _ENV_KEY, env_configs)
+        elif env_key not in setup_envs and env_key not in env_configs:
             config_part = f' config="{ed_cfg}"' if ed_cfg else ""
             env_configs.add(env_key)
             setattr(env, _ENV_KEY, env_configs)
@@ -154,10 +207,23 @@ def run(self) -> list[nodes.Node]:
         code = "\n".join(self.content)
         indented = "\n".join("    " + line for line in code.splitlines())
 
+        # Assign a unique ID so sphinx_copybutton can target the hidden 
.
+        # The counter is global across all documents in a build (matching how
+        # Sphinx/Pygments numbers codecell0, codecell1, …).
+        cell_num = getattr(env, _CELL_COUNTER_KEY, 0)
+        cell_id = f"pyscript-codecell{cell_num}"
+        setattr(env, _CELL_COUNTER_KEY, cell_num + 1)
+
+        # sphinx_copybutton looks for `div.highlight pre` and wires a copy
+        # button to it via data-clipboard-target.  The 
 is hidden
+        # visually; clipboard.js reads textContent regardless of visibility.
+        escaped_code = html_mod.escape(code)
+
         editor_html = (
             '
\n' '
\n' - f'\n" "
\n" @@ -181,6 +247,7 @@ def setup(app: Sphinx) -> dict: app.add_config_value("pyscript_config", "pyscript.json", "html") app.add_config_value("pyscript_mini_coi", "mini-coi.js", "html") app.add_config_value("pyscript_hide_gutters", True, "html") + app.add_config_value("pyscript_hide_env_label", True, "html") app.add_directive("py-editor", PyEditorDirective) diff --git a/docs/pyscript_min.json b/docs/pyscript_min.json new file mode 100644 index 00000000..31d46a8b --- /dev/null +++ b/docs/pyscript_min.json @@ -0,0 +1,6 @@ +{ + "packages": [ + "numpy", + "https://cdn.jsdelivr.net/gh/AndrewAnnex/spiceypy-wheels-dist@v8.0.2-dev.2/spiceypy-8.0.2-cp313-cp313-pyodide_2025_0_wasm32.whl" + ] +} diff --git a/docs/pyscript_other_stuff.json b/docs/pyscript_other_stuff.json new file mode 100644 index 00000000..8f6d7b93 --- /dev/null +++ b/docs/pyscript_other_stuff.json @@ -0,0 +1,11 @@ +{ + "packages": [ + "numpy", + "https://cdn.jsdelivr.net/gh/AndrewAnnex/spiceypy-wheels-dist@v8.0.2-dev.2/spiceypy-8.0.2-cp313-cp313-pyodide_2025_0_wasm32.whl" + ], + "files": { + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/naif0008.tls": "./kernels/lsk/naif0008.tls", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/de405s.bsp": "./kernels/spk/de405s.bsp", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/pck00008.tpc": "./kernels/pck/pck00008.tpc" + } +} diff --git a/docs/remote_sensing.rst b/docs/remote_sensing.rst index e860e6b9..6f6d83a4 100644 --- a/docs/remote_sensing.rst +++ b/docs/remote_sensing.rst @@ -102,7 +102,9 @@ built-in Python help system. For example, the Python help function -.. code-block:: python +.. py-editor:: + :env: tmp + :config: pyscript_min.json import spiceypy help(spiceypy.str2et) @@ -2101,7 +2103,7 @@ A sample solution to the problem follows: .. code-block:: python :linenos: - + # # Solution fovint.py # From 2bfb2c2b92a46bf3bb44d1a679b7a1bad7ff276c Mon Sep 17 00:00:00 2001 From: Andrew Annex <2126916+AndrewAnnex@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:38:25 -0700 Subject: [PATCH 03/10] additional fix, seems like blank lines are a problem sometimes? --- docs/other_stuff.rst | 47 +++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/docs/other_stuff.rst b/docs/other_stuff.rst index ed36c2f3..7f7ae6bc 100644 --- a/docs/other_stuff.rst +++ b/docs/other_stuff.rst @@ -784,7 +784,6 @@ Code Solution # Import the CSPICE-Python interface. # import spiceypy - from spiceypy.utils.support_types import SpiceyError def kervar(): @@ -832,24 +831,22 @@ Code Solution cvals = spiceypy.gnpool( tmplate, START, N_ITEMS ) print( 'Number variables matching template: {0}'.\ format( len(cvals)) ) - except SpiceyError: + except spiceypy.SpiceyError: print( 'No kernel variables matched template.' ) return - # # Okay, now we know something about the kernel pool # variables of interest to us. Let's find out more... # for cval in cvals: - # # Use spiceypy.dtpool to return the dimension and type, # C (character) or N (numeric), of each pool # variable name in the cvals array. We know the # kernel data exists. # - [dim, type] = spiceypy.dtpool( cval ) + dim, type = spiceypy.dtpool( cval ) print( '\n' + cval ) print( ' Number items: {0} Of type: {1}\n'.\ @@ -859,7 +856,6 @@ Code Solution # Test character equality, 'N' or 'C'. # if type == 'N': - # # If 'type' equals 'N', we found a numeric array. # In this case any numeric array will be an array @@ -870,27 +866,20 @@ Code Solution dvars = spiceypy.gdpool( cval, START, N_ITEMS ) for dvar in dvars: print(' Numeric value: {0:20.6f}'.format(dvar)) - elif type == 'C': - # # If 'type' equals 'C', we found a string array. # spiceypy.gcpool retrieves string values from the # kernel pool. # cvars = spiceypy.gcpool( cval, START, N_ITEMS ) - for cvar in cvars: print(' String value: {0}\n'.format(cvar)) - else: - # # This block should never execute. # print('Unknown type. Code error.') - - # # Now look at the time variable EXAMPLE_TIMES. Extract this # value as an array of doubles. @@ -1507,7 +1496,7 @@ Code Solution # Import the CSPICE-Python interface. # import spiceypy - + # For simplicity, we request only one input. # The program calculates the state vector from # Earth to the user specified target 'targ' in the @@ -1544,7 +1533,7 @@ Code Solution # Done. Unload the kernels. # spiceypy.kclear() - + if __name__ == '__main__': aderr('Moon') aderr('Mars') @@ -1626,7 +1615,7 @@ Try another look-up, this time for “Casper” ===================================================================== =========== - Toolkit version: N0066 + Toolkit version: N0067 SPICE(IDCODENOTFOUND) -- @@ -1724,7 +1713,7 @@ Code Solution .. py-editor:: :env: other - + mk=r""" KPL/MK \begindata @@ -1736,7 +1725,7 @@ Code Solution dst.write(mk) print('Wrote kernel file win.tm') print('') - + # # Import the CSPICE-Python interface. # @@ -1980,7 +1969,7 @@ Code Solution # Import the CSPICE-Python interface. # import spiceypy - + aliases = { 'meter': 'METER', 'klicks': 'KM', @@ -1989,38 +1978,38 @@ Code Solution 'secs': 'SECONDS', 'miles': 'STATUTE_MILES' } - + def tostan(alias): return aliases.get(alias, alias) - + def units(funits, fvalue, tunits): # # Display the Toolkit version string with a spiceypy.tkvrsn # call. # vers = spiceypy.tkvrsn( 'TOOLKIT' ) - print('\nConvert demo program compiled against CSPICE Toolkit ' + vers) + print('\nConvert demo program compiled against CSPICE Toolkit ' + vers) # # The user first inputs the name of a unit of measure. # Send the name through tostan for de-aliasing. # print(f'From Units : {funits}') - funits = tostan( funits ) + funits = tostan( funits ) # # Input a double precision value to express in a new # unit format. # - print(f'From Value : {fvalue}') + print(f'From Value : {fvalue}') # # Now the user inputs the name of the output units. # Again we send the units name through tostan for # de-aliasing. # print(f'To Units : {tunits}') - tunits = tostan( tunits ) + tunits = tostan( tunits ) tvalue = spiceypy.convrt( fvalue, funits, tunits) print( '{0:12.5f} {1}'.format(tvalue, tunits) ) - + if __name__ == '__main__': units('klicks', 3, 'miles') units('miles', 26.2, 'km') @@ -2034,7 +2023,7 @@ was linked: .. code-block:: text - Convert demo program compiled against CSPICE Toolkit CSPICE_N0066 + Convert demo program compiled against CSPICE Toolkit CSPICE_N0067 From Units : klicks From Value : 3 To Units : miles @@ -2046,9 +2035,9 @@ Legend states Pheidippides ran from the Marathon Plain to Athens. The modern marathon race (inspired by this event) spans 26.2 miles. How far in kilometers? -:: +.. code-block:: text - Convert demo program compiled against CSPICE Toolkit CSPICE_N0066 + Convert demo program compiled against CSPICE Toolkit CSPICE_N0067 From Units : miles From Value : 26.2 To Units : km From 3434529cd7beaa0465d8efe4de7eb0950ddb81c3 Mon Sep 17 00:00:00 2001 From: Andrew Annex <2126916+AndrewAnnex@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:37:47 -0700 Subject: [PATCH 04/10] seemingly working remote sensing code and simplification, will need to verify correctness and also fix just lots of bad formatting choices --- docs/basics.rst | 6 +- docs/binary_pck.rst | 4 +- docs/conf.py | 1 + docs/pyscript_remote_sensing.json | 19 + docs/remote_sensing.rst | 1225 ++++++++++------------------- 5 files changed, 454 insertions(+), 801 deletions(-) create mode 100644 docs/pyscript_remote_sensing.json diff --git a/docs/basics.rst b/docs/basics.rst index 7872aa2e..94b44850 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -41,7 +41,9 @@ A simple example program The following calls the SPICE function :py:meth:`spiceypy.spiceypy.tkvrsn` which outputs the version of cspice that SpiceyPy is wrapping. -.. code:: python +.. py-editor:: + :env: other + :config: pyscript_min.json import spiceypy as spice @@ -51,5 +53,5 @@ This should output the following string: .. parsed-literal:: - 'CSPICE_N0066' + 'CSPICE_N0067' diff --git a/docs/binary_pck.rst b/docs/binary_pck.rst index fb8f662c..a9f0bdc7 100644 --- a/docs/binary_pck.rst +++ b/docs/binary_pck.rst @@ -78,7 +78,9 @@ built-in Python help system. For example, the Python help function -.. code-block:: python +.. py-editor:: + :env: other + :config: pyscript_min.json import spiceypy diff --git a/docs/conf.py b/docs/conf.py index 0697b1ad..0523fc19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -219,6 +219,7 @@ html_extra_path = [ "pyscript.json", "pyscript_min.json", + "pyscript_remote_sensing.json", "pyscript_other_stuff.json", "mini-coi.js", ] diff --git a/docs/pyscript_remote_sensing.json b/docs/pyscript_remote_sensing.json new file mode 100644 index 00000000..3f6c8cfe --- /dev/null +++ b/docs/pyscript_remote_sensing.json @@ -0,0 +1,19 @@ +{ + "packages": [ + "numpy", + "https://cdn.jsdelivr.net/gh/AndrewAnnex/spiceypy-wheels-dist@v8.0.2-dev.2/spiceypy-8.0.2-cp313-cp313-pyodide_2025_0_wasm32.whl" + ], + "files": { + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/naif0008.tls": "kernels/lsk/naif0008.tls", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/cas00084.tsc": "kernels/sclk/cas00084.tsc", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/981005_PLTEPH-DE405S.bsp": "kernels/spk/981005_PLTEPH-DE405S.bsp", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/020514_SE_SAT105.bsp": "kernels/spk/020514_SE_SAT105.bsp", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/030201AP_SK_SM546_T45.bsp": "kernels/spk/030201AP_SK_SM546_T45.bsp", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/cas_v37.tf": "kernels/fk/cas_v37.tf", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/04135_04171pc_psiv2.bc": "kernels/ck/04135_04171pc_psiv2.bc", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/cpck05Mar2004.tpc": "kernels/pck/cpck05Mar2004.tpc", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/cas_iss_v09.ti": "kernels/ik/cas_iss_v09.ti", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/phoebe_64q.bds": "kernels/dsk/phoebe_64q.bds", + "https://raw.githubusercontent.com/AndrewAnnex/spiceypylessonkernels/refs/heads/main/jup310_2004.bsp": "kernels/spk/jup310_2004.bsp" + } +} diff --git a/docs/remote_sensing.rst b/docs/remote_sensing.rst index 6f6d83a4..9af959fb 100644 --- a/docs/remote_sensing.rst +++ b/docs/remote_sensing.rst @@ -103,8 +103,15 @@ built-in Python help system. For example, the Python help function .. py-editor:: - :env: tmp - :config: pyscript_min.json + :env: rsenv + :config: pyscript_remote_sensing.json + :setup: + + import spiceypy + +.. py-editor:: + :env: rsenv + :config: pyscript_remote_sensing.json import spiceypy help(spiceypy.str2et) @@ -293,8 +300,11 @@ Solution Meta-Kernel The meta-kernel we created for the solution to this exercise is named 'convtm.tm'. Its contents follow: -.. code-block:: text +.. py-editor:: + :env: rsenv + :config: pyscript_remote_sensing.json + mk = r""" KPL/MK This is the meta-kernel used in the solution of the "Time @@ -313,73 +323,45 @@ The meta-kernel we created for the solution to this exercise is named KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', 'kernels/sclk/cas00084.tsc' ) \begintext + """ + with open('convtm.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file convtm.tm') Solution Source Code A sample solution to the problem follows: -.. code-block:: python - :linenos: - - # - # Solution convtm - # - from __future__ import print_function - from builtins import input - - import spiceypy - - def convtm(): - # - # Local Parameters - # - METAKR = 'convtm.tm' - SCLKID = -82 - - spiceypy.furnsh( METAKR ) - - # - # Prompt the user for the input time string. - # - utctim = input( 'Input UTC Time: ' ) - - print( 'Converting UTC Time: {:s}'.format( utctim ) ) - - # - # Convert utctim to ET. - # - et = spiceypy.str2et( utctim ) - - print( ' ET Seconds Past J2000: {:16.3f}'.format( et ) ) - - # - # Now convert ET to a calendar time string. - # This can be accomplished in two ways. - # - calet = spiceypy.etcal( et ) - - print( ' Calendar ET (etcal): {:s}'.format( calet ) ) - - # - # Or use timout for finer control over the - # output format. The picture below was built - # by examining the header of timout. - # - calet = spiceypy.timout( et, 'YYYY-MON-DDTHR:MN:SC ::TDB' ) - - print( ' Calendar ET (timout): {:s}'.format( calet ) ) - - # - # Convert ET to spacecraft clock time. - # - sclkst = spiceypy.sce2s( SCLKID, et ) - - print( ' Spacecraft Clock Time: {:s}'.format( sclkst ) ) - - spiceypy.unload( METAKR ) - - if __name__ == '__main__': - convtm() +.. py-editor:: + :env: rsenv + + # Solution convtm + import spiceypy + + def convtm(utctim='2004 jun 11 19:32:00'): + METAKR = 'convtm.tm' + SCLKID = -82 + + spiceypy.furnsh(METAKR) + print(f'Converting UTC Time: {utctim}') + et = spiceypy.str2et(utctim) + print(f' ET Seconds Past J2000: {et:16.3f}') + + # Convert ET to a calendar time string; this can be done two ways. + calet = spiceypy.etcal(et) + print(f' Calendar ET (etcal): {calet}') + + # Or use timout for finer control over the output format. + # The picture below was built by examining the header of timout. + calet = spiceypy.timout(et, 'YYYY-MON-DDTHR:MN:SC ::TDB') + print(f' Calendar ET (timout): {calet}') + + # Convert ET to spacecraft clock time. + sclkst = spiceypy.sce2s(SCLKID, et) + print(f' Spacecraft Clock Time: {sclkst}') + spiceypy.unload(METAKR) + + convtm() Solution Sample Output @@ -673,163 +655,102 @@ Solution Meta-Kernel The meta-kernel we created for the solution to this exercise is named 'getsta.tm'. Its contents follow: -.. code-block:: text +.. py-editor:: + :env: rsenv - KPL/MK + mk = r""" + KPL/MK - This is the meta-kernel used in the solution of the - "Obtaining Target States and Positions" task in the - Remote Sensing Hands On Lesson. + This is the meta-kernel used in the solution of the + "Obtaining Target States and Positions" task in the + Remote Sensing Hands On Lesson. - The names and contents of the kernels referenced by this - meta-kernel are as follows: + The names and contents of the kernels referenced by this + meta-kernel are as follows: - File name Contents - -------------------------- ----------------------------- - naif0008.tls Generic LSK - 981005_PLTEPH-DE405S.bsp Solar System Ephemeris - 020514_SE_SAT105.bsp Saturnian Satellite Ephemeris - 030201AP_SK_SM546_T45.bsp Cassini Spacecraft SPK + File name Contents + -------------------------- ----------------------------- + naif0008.tls Generic LSK + 981005_PLTEPH-DE405S.bsp Solar System Ephemeris + 020514_SE_SAT105.bsp Saturnian Satellite Ephemeris + 030201AP_SK_SM546_T45.bsp Cassini Spacecraft SPK - \begindata - KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', - 'kernels/spk/981005_PLTEPH-DE405S.bsp', - 'kernels/spk/020514_SE_SAT105.bsp', - 'kernels/spk/030201AP_SK_SM546_T45.bsp' ) - \begintext + \begindata + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/spk/981005_PLTEPH-DE405S.bsp', + 'kernels/spk/020514_SE_SAT105.bsp', + 'kernels/spk/030201AP_SK_SM546_T45.bsp' ) + \begintext + """ + with open('getsta.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file getsta.tm') Solution Source Code A sample solution to the problem follows: -.. code-block:: python - :linenos: - - # - # Solution getsta.py - # - from __future__ import print_function - from builtins import input - - import spiceypy - - def getsta(): - # - # Local parameters - # - METAKR = 'getsta.tm' - - # - # Load the kernels that this program requires. We - # will need a leapseconds kernel to convert input - # UTC time strings into ET. We also will need the - # necessary SPK files with coverage for the bodies - # in which we are interested. - # - spiceypy.furnsh( METAKR ) - - # - #Prompt the user for the input time string. - # - utctim = input( 'Input UTC Time: ' ) - - print( 'Converting UTC Time: {:s}'.format(utctim) ) - - # - #Convert utctim to ET. - # - et = spiceypy.str2et( utctim ) - - print( ' ET seconds past J2000: {:16.3f}'.format(et) ) - - # - # Compute the apparent state of Phoebe as seen from - # CASSINI in the J2000 frame. All of the ephemeris - # readers return states in units of kilometers and - # kilometers per second. - # - [state, ltime] = spiceypy.spkezr( 'PHOEBE', et, 'J2000', - 'LT+S', 'CASSINI' ) - - print( ' Apparent state of Phoebe as seen ' - 'from CASSINI in the J2000\n' - ' frame (km, km/s):' ) - - print( ' X = {:16.3f}'.format(state[0]) ) - print( ' Y = {:16.3f}'.format(state[1]) ) - print( ' Z = {:16.3f}'.format(state[2]) ) - print( ' VX = {:16.3f}'.format(state[3]) ) - print( ' VY = {:16.3f}'.format(state[4]) ) - print( ' VZ = {:16.3f}'.format(state[5]) ) - - # - # Compute the apparent position of Earth as seen from - # CASSINI in the J2000 frame. Note: We could have - # continued using spkezr and simply ignored the - # velocity components. - # - [pos, ltime] = spiceypy.spkpos( 'EARTH', et, 'J2000', - 'LT+S', 'CASSINI', ) - - print( ' Apparent position of Earth as ' - 'seen from CASSINI in the J2000\n' - ' frame (km):' ) - print( ' X = {:16.3f}'.format(pos[0]) ) - print( ' Y = {:16.3f}'.format(pos[1]) ) - print( ' Z = {:16.3f}'.format(pos[2]) ) - - # - # We need only display LTIME, as it is precisely the - # light time in which we are interested. - # - print( ' One way light time between CASSINI and ' - 'the apparent position\n' - ' of Earth (seconds):' - ' {:16.3f}'.format(ltime) ) - - # - # Compute the apparent position of the Sun as seen from - # PHOEBE in the J2000 frame. - # - [pos, ltime] = spiceypy.spkpos( 'SUN', et, 'J2000', - 'LT+S', 'PHOEBE', ) - - print( ' Apparent position of Sun as ' - 'seen from Phoebe in the\n' - ' J2000 frame (km):' ) - print( ' X = {:16.3f}'.format(pos[0]) ) - print( ' Y = {:16.3f}'.format(pos[1]) ) - print( ' Z = {:16.3f}'.format(pos[2]) ) - - # - # Now we need to compute the actual distance between - # the Sun and Phoebe. The above spkpos call gives us - # the apparent distance, so we need to adjust our - # aberration correction appropriately. - # - [pos, ltime] = spiceypy.spkpos( 'SUN', et, 'J2000', - 'NONE', 'PHOEBE' ) - - # - # Compute the distance between the body centers in - # kilometers. - # - dist = spiceypy.vnorm( pos ) - - # - # Convert this value to AU using convrt. - # - dist = spiceypy.convrt( dist, 'KM', 'AU' ) - - print( ' Actual distance between Sun and ' - 'Phoebe body centers:\n' - ' (AU): {:16.3f}'.format(dist) ) - - spiceypy.unload( METAKR ) - - if __name__ == '__main__': - getsta() +.. py-editor:: + :env: rsenv + + # Solution getsta.py + import spiceypy + + def getsta(utctim='2004 jun 11 19:32:00'): + METAKR = 'getsta.tm' + spiceypy.furnsh(METAKR) + print(f'Converting UTC Time: {utctim}') + et = spiceypy.str2et(utctim) + print(f' ET seconds past J2000: {et:16.3f}') + + # Compute the apparent state of Phoebe as seen from CASSINI in the + # J2000 frame. Ephemeris readers return km and km/s. + state, ltime = spiceypy.spkezr('PHOEBE', et, 'J2000', 'LT+S', 'CASSINI') + + print(' Apparent state of Phoebe as seen from CASSINI in the J2000\n' + ' frame (km, km/s):') + print(f' X = {state[0]:16.3f}') + print(f' Y = {state[1]:16.3f}') + print(f' Z = {state[2]:16.3f}') + print(f' VX = {state[3]:16.3f}') + print(f' VY = {state[4]:16.3f}') + print(f' VZ = {state[5]:16.3f}') + + # Compute the apparent position of Earth as seen from CASSINI. + # Note: spkpos instead of spkezr since we only need position. + pos, ltime = spiceypy.spkpos('EARTH', et, 'J2000', 'LT+S', 'CASSINI') + + print(' Apparent position of Earth as seen from CASSINI in the J2000\n' + ' frame (km):') + print(f' X = {pos[0]:16.3f}') + print(f' Y = {pos[1]:16.3f}') + print(f' Z = {pos[2]:16.3f}') + + # ltime is the one-way light time between CASSINI and Earth. + print(f' One way light time between CASSINI and the apparent position\n' + f' of Earth (seconds): {ltime:16.3f}') + + # Compute the apparent position of the Sun as seen from Phoebe. + pos, ltime = spiceypy.spkpos('SUN', et, 'J2000', 'LT+S', 'PHOEBE') + + print(' Apparent position of Sun as seen from Phoebe in the\n' + ' J2000 frame (km):') + print(f' X = {pos[0]:16.3f}') + print(f' Y = {pos[1]:16.3f}') + print(f' Z = {pos[2]:16.3f}') + + # For the actual (geometric) distance we use no aberration correction, + # then convert km to AU. + pos, _ = spiceypy.spkpos('SUN', et, 'J2000', 'NONE', 'PHOEBE') + dist = spiceypy.convrt(spiceypy.vnorm(pos), 'KM', 'AU') + + print(f' Actual distance between Sun and Phoebe body centers:\n' + f' (AU): {dist:16.3f}') + + spiceypy.unload(METAKR) + + getsta() Solution Sample Output @@ -1186,9 +1107,11 @@ Solution Meta-Kernel The meta-kernel we created for the solution to this exercise is named 'xform.tm'. Its contents follow: -.. code-block:: text +.. py-editor:: + :env: rsenv - KPL/MK + mk = r""" + KPL/MK This is the meta-kernel used in the solution of the "Spacecraft Orientation and Reference Frames" task in the Remote Sensing @@ -1219,193 +1142,89 @@ The meta-kernel we created for the solution to this exercise is named 'kernels/ck/04135_04171pc_psiv2.bc', 'kernels/pck/cpck05Mar2004.tpc' ) \begintext + """ + with open('xform.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file xform.tm') Solution Source Code A sample solution to the problem follows: -.. code-block:: python - :linenos: - - # - # Solution xform.py - # - from __future__ import print_function - from builtins import input - - import spiceypy - - def xform(): - # - # Local parameters - # - METAKR = 'xform.tm' - - # - # Load the kernels that this program requires. We - # will need: - # - # A leapseconds kernel - # A spacecraft clock kernel for CASSINI - # The necessary ephemerides - # A planetary constants file (PCK) - # A spacecraft orientation kernel for CASSINI (CK) - # A frame kernel (TF) - # - spiceypy.furnsh( METAKR ) - - # - # Prompt the user for the input time string. - # - utctim = input( 'Input UTC Time: ' ) - - print( 'Converting UTC Time: {:s}'.format(utctim) ) - - # - #Convert utctim to ET. - # - et = spiceypy.str2et( utctim ) - - print( ' ET seconds past J2000: {:16.3f}'.format(et) ) - - # - # Compute the apparent state of Phoebe as seen from - # CASSINI in the J2000 frame. - # - [state, ltime] = spiceypy.spkezr( 'PHOEBE', et, 'J2000', - 'LT+S', 'CASSINI' ) - # - # Now obtain the transformation from the inertial - # J2000 frame to the non-inertial body-fixed IAU_PHOEBE - # frame. Since we want the apparent position, we - # need to subtract ltime from et. - # - sform = spiceypy.sxform( 'J2000', 'IAU_PHOEBE', et-ltime ) - - # - # Now rotate the apparent J2000 state into IAU_PHOEBE - # with the following matrix multiplication: - # - bfixst = spiceypy.mxvg ( sform, state, 6, 6 ) - - # - # Display the results. - # - print( ' Apparent state of Phoebe as seen ' - 'from CASSINI in the IAU_PHOEBE\n' - ' body-fixed frame (km, km/s):' ) - print( ' X = {:19.6f}'.format(bfixst[0]) ) - print( ' Y = {:19.6f}'.format(bfixst[1]) ) - print( ' Z = {:19.6f}'.format(bfixst[2]) ) - print( ' VX = {:19.6f}'.format(bfixst[3]) ) - print( ' VY = {:19.6f}'.format(bfixst[4]) ) - print( ' VZ = {:19.6f}'.format(bfixst[5]) ) - - # - # It is worth pointing out, all of the above could - # have been done with a single use of spkezr: - # - [state, ltime] = spiceypy.spkezr( - 'PHOEBE', et, 'IAU_PHOEBE', - 'LT+S', 'CASSINI' ) - # - # Display the results. - # - print( ' Apparent state of Phoebe as seen ' - 'from CASSINI in the IAU_PHOEBE\n' - ' body-fixed frame (km, km/s) ' - 'obtained using spkezr directly:' ) - print( ' X = {:19.6f}'.format(state[0]) ) - print( ' Y = {:19.6f}'.format(state[1]) ) - print( ' Z = {:19.6f}'.format(state[2]) ) - print( ' VX = {:19.6f}'.format(state[3]) ) - print( ' VY = {:19.6f}'.format(state[4]) ) - print( ' VZ = {:19.6f}'.format(state[5]) ) - - # - # Note that the velocity found by using spkezr - # to compute the state in the IAU_PHOEBE frame differs - # at the few mm/second level from that found previously - # by calling spkezr and then sxform. Computing - # velocity via a single call to spkezr as we've - # done immediately above is slightly more accurate because - # it accounts for the effect of the rate of change of - # light time on the apparent angular velocity of the - # target's body-fixed reference frame. - # - # Now we are to compute the angular separation between - # the apparent position of the Earth as seen from the - # orbiter and the nominal boresight of the high gain - # antenna. First, compute the apparent position of - # the Earth as seen from CASSINI in the J2000 frame. - # - [pos, ltime] = spiceypy.spkpos( 'EARTH', et, 'J2000', - 'LT+S', 'CASSINI' ) - - # - # Now compute the location of the antenna boresight - # at this same epoch. From reading the frame kernel - # we know that the antenna boresight is nominally the - # +Z axis of the CASSINI_HGA frame defined there. - # - bsight = [ 0.0, 0.0, 1.0] - - # - # Now compute the rotation matrix from CASSINI_HGA into - # J2000. - # - pform = spiceypy.pxform( 'CASSINI_HGA', 'J2000', et ) - - # - # And multiply the result to obtain the nominal - # antenna boresight in the J2000 reference frame. - # - bsight = spiceypy.mxv( pform, bsight ) - - # - # Lastly compute the angular separation. - # - sep = spiceypy.convrt( spiceypy.vsep(bsight, pos), - 'RADIANS', 'DEGREES' ) - - print( ' Angular separation between the ' - 'apparent position of\n' - ' Earth and the CASSINI high ' - 'gain antenna boresight (degrees):\n' - ' {:16.3f}'.format(sep) ) - - # - # Or alternatively we can work in the antenna - # frame directly. - # - [pos, ltime] = spiceypy.spkpos( - 'EARTH', et, 'CASSINI_HGA', - 'LT+S', 'CASSINI' ) - - # - # The antenna boresight is the Z-axis in the - # CASSINI_HGA frame. - # - bsight = [ 0.0, 0.0, 1.0 ] - - # - # Lastly compute the angular separation. - # - sep = spiceypy.convrt( spiceypy.vsep(bsight, pos), - 'RADIANS', 'DEGREES' ) - - print( ' Angular separation between the ' - 'apparent position of\n' - ' Earth and the CASSINI high ' - 'gain antenna boresight computed\n' - ' using vectors in the CASSINI_HGA ' - 'frame (degrees):\n' - ' {:16.3f}'.format(sep) ) - - spiceypy.unload( METAKR ) - - if __name__ == '__main__': - xform() +.. py-editor:: + :env: rsenv + + # + # Solution xform.py + # + import spiceypy + def xform(utctim='2004 jun 11 19:32:00'): + METAKR = 'xform.tm' + spiceypy.furnsh(METAKR) + print(f'Converting UTC Time: {utctim}') + et = spiceypy.str2et(utctim) + print(f' ET seconds past J2000: {et:16.3f}') + + # Compute the apparent state of Phoebe as seen from CASSINI in J2000. + state, ltime = spiceypy.spkezr('PHOEBE', et, 'J2000', 'LT+S', 'CASSINI') + + # Obtain the state transformation from J2000 to the non-inertial + # body-fixed IAU_PHOEBE frame at the light-time corrected epoch. + sform = spiceypy.sxform('J2000', 'IAU_PHOEBE', et - ltime) + + # Rotate the apparent J2000 state into IAU_PHOEBE. + bfixst = spiceypy.mxvg(sform, state) + + print(' Apparent state of Phoebe as seen from CASSINI in the IAU_PHOEBE\n' + ' body-fixed frame (km, km/s):') + print(f' X = {bfixst[0]:19.6f}') + print(f' Y = {bfixst[1]:19.6f}') + print(f' Z = {bfixst[2]:19.6f}') + print(f' VX = {bfixst[3]:19.6f}') + print(f' VY = {bfixst[4]:19.6f}') + print(f' VZ = {bfixst[5]:19.6f}') + + # All of the above can be done with a single spkezr call directly + # into the target frame. This is slightly more accurate for velocity + # because it accounts for the rate of change of light time on the + # apparent angular velocity of the body-fixed frame. + state, ltime = spiceypy.spkezr('PHOEBE', et, 'IAU_PHOEBE', 'LT+S', 'CASSINI') + + print(' Apparent state of Phoebe as seen from CASSINI in the IAU_PHOEBE\n' + ' body-fixed frame (km, km/s) obtained using spkezr directly:') + print(f' X = {state[0]:19.6f}') + print(f' Y = {state[1]:19.6f}') + print(f' Z = {state[2]:19.6f}') + print(f' VX = {state[3]:19.6f}') + print(f' VY = {state[4]:19.6f}') + print(f' VZ = {state[5]:19.6f}') + + # Compute the angular separation between the apparent position of Earth + # as seen from CASSINI and the nominal HGA boresight (+Z of CASSINI_HGA). + pos, _ = spiceypy.spkpos('EARTH', et, 'J2000', 'LT+S', 'CASSINI') + + # Rotate the nominal boresight from CASSINI_HGA into J2000. + bsight = spiceypy.mxv(spiceypy.pxform('CASSINI_HGA', 'J2000', et), + [0.0, 0.0, 1.0]) + + sep = spiceypy.convrt(spiceypy.vsep(bsight, pos), 'RADIANS', 'DEGREES') + print(f' Angular separation between the apparent position of\n' + f' Earth and the CASSINI high gain antenna boresight (degrees):\n' + f' {sep:16.3f}') + + # Alternatively, work directly in the antenna frame. + pos, _ = spiceypy.spkpos('EARTH', et, 'CASSINI_HGA', 'LT+S', 'CASSINI') + + # The antenna boresight is the Z-axis in the CASSINI_HGA frame. + sep = spiceypy.convrt(spiceypy.vsep([0.0, 0.0, 1.0], pos), 'RADIANS', 'DEGREES') + print(' Angular separation between the apparent position of\n' + ' Earth and the CASSINI high gain antenna boresight computed\n' + ' using vectors in the CASSINI_HGA frame (degrees):\n' + f' {sep:16.3f}') + + spiceypy.unload(METAKR) + + #xform() Solution Sample Output @@ -1644,145 +1463,81 @@ Solution Meta-Kernel The meta-kernel we created for the solution to this exercise is named 'subpts.tm'. Its contents follow: -.. code-block:: text - - KPL/MK - - This is the meta-kernel used in the solution of the - "Computing Sub-spacecraft and Sub-solar Points" task - in the Remote Sensing Hands On Lesson. - - The names and contents of the kernels referenced by this - meta-kernel are as follows: - - File name Contents - -------------------------- ----------------------------- - naif0008.tls Generic LSK - 981005_PLTEPH-DE405S.bsp Solar System Ephemeris - 020514_SE_SAT105.bsp Saturnian Satellite Ephemeris - 030201AP_SK_SM546_T45.bsp Cassini Spacecraft SPK - cpck05Mar2004.tpc Cassini Project PCK - phoebe_64q.bds Phoebe DSK - - - \begindata - KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', - 'kernels/spk/981005_PLTEPH-DE405S.bsp', - 'kernels/spk/020514_SE_SAT105.bsp', - 'kernels/spk/030201AP_SK_SM546_T45.bsp', - 'kernels/pck/cpck05Mar2004.tpc' - 'kernels/dsk/phoebe_64q.bds' ) - - \begintext +.. py-editor:: + :env: rsenv + + mk = r""" + KPL/MK + + This is the meta-kernel used in the solution of the + "Computing Sub-spacecraft and Sub-solar Points" task + in the Remote Sensing Hands On Lesson. + + The names and contents of the kernels referenced by this + meta-kernel are as follows: + + File name Contents + -------------------------- ----------------------------- + naif0008.tls Generic LSK + 981005_PLTEPH-DE405S.bsp Solar System Ephemeris + 020514_SE_SAT105.bsp Saturnian Satellite Ephemeris + 030201AP_SK_SM546_T45.bsp Cassini Spacecraft SPK + cpck05Mar2004.tpc Cassini Project PCK + phoebe_64q.bds Phoebe DSK + + + \begindata + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/spk/981005_PLTEPH-DE405S.bsp', + 'kernels/spk/020514_SE_SAT105.bsp', + 'kernels/spk/030201AP_SK_SM546_T45.bsp', + 'kernels/pck/cpck05Mar2004.tpc', + 'kernels/dsk/phoebe_64q.bds' ) + + \begintext + """ + with open('subpts.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file subpts.tm') Solution Source Code A sample solution to the problem follows: -.. code-block:: python - :linenos: - - # - # Solution subpts.py - # - from __future__ import print_function - from builtins import input - - # - # SpiceyPy package: - # - import spiceypy - - def subpts(): - # - # Local parameters - # - METAKR = 'subpts.tm' - - # - # Load the kernels that this program requires. We - # will need: - # - # A leapseconds kernel - # The necessary ephemerides - # A planetary constants file (PCK) - # A DSK file containing Phoebe shape data - # - spiceypy.furnsh( METAKR ) - - # - #Prompt the user for the input time string. - # - utctim = input( 'Input UTC Time: ' ) - - print( ' Converting UTC Time: {:s}'.format(utctim) ) - - # - #Convert utctim to ET. - # - et = spiceypy.str2et( utctim ) - - print( ' ET seconds past J2000: {:16.3f}'.format(et) ) - - for i in range(2): - - if i == 0: - # - # Use the "near point" sub-point definition - # and an ellipsoidal model. - # - method = 'NEAR POINT/Ellipsoid' - - else: - # - # Use the "nadir" sub-point definition - # and a DSK model. - # - method = 'NADIR/DSK/Unprioritized' - - print( '\n Sub-point/target shape model: {:s}\n'.format( - method ) ) - - # - # Compute the apparent sub-observer point of CASSINI - # on Phoebe. - # - [spoint, trgepc, srfvec] = spiceypy.subpnt( - method, 'PHOEBE', et, - 'IAU_PHOEBE', 'LT+S', 'CASSINI' ) - - print( ' Apparent sub-observer point of CASSINI ' - 'on Phoebe in the\n' - ' IAU_PHOEBE frame (km):' ) - print( ' X = {:16.3f}'.format(spoint[0]) ) - print( ' Y = {:16.3f}'.format(spoint[1]) ) - print( ' Z = {:16.3f}'.format(spoint[2]) ) - print( ' ALT = {:16.3f}'.format(spiceypy.vnorm(srfvec)) ) - - # - # Compute the apparent sub-solar point on Phoebe - # as seen from CASSINI. - # - [spoint, trgepc, srfvec] = spiceypy.subslr( - method, 'PHOEBE', et, - 'IAU_PHOEBE', 'LT+S', 'CASSINI' ) - - print( ' Apparent sub-solar point on Phoebe ' - 'as seen from CASSINI in\n' - ' the IAU_PHOEBE frame (km):' ) - print( ' X = {:16.3f}'.format(spoint[0]) ) - print( ' Y = {:16.3f}'.format(spoint[1]) ) - print( ' Z = {:16.3f}'.format(spoint[2]) ) - - # - # End of computation block for "method" - # - print( " ) - - spiceypy.unload( METAKR ) - - if __name__ == '__main__': - subpts() +.. py-editor:: + :env: rsenv + + # Solution subpts.py + import spiceypy + + def subpts(utctim='2004 jun 11 19:32:00'): + METAKR = 'subpts.tm' + spiceypy.furnsh(METAKR) + print(f'Converting UTC Time: {utctim}') + et = spiceypy.str2et(utctim) + print(f' ET seconds past J2000: {et:16.3f}') + # Compute sub-points using both an ellipsoidal and a DSK shape model. + for method in ('NEAR POINT/Ellipsoid', 'NADIR/DSK/Unprioritized'): + print(f'\n Sub-point/target shape model: {method}\n') + # Compute the apparent sub-observer point of CASSINI on Phoebe. + spoint, trgepc, srfvec = spiceypy.subpnt( + method, 'PHOEBE', et, 'IAU_PHOEBE', 'LT+S', 'CASSINI') + print(' Apparent sub-observer point of CASSINI on Phoebe in the\n' + ' IAU_PHOEBE frame (km):') + print(f' X = {spoint[0]:16.3f}') + print(f' Y = {spoint[1]:16.3f}') + print(f' Z = {spoint[2]:16.3f}') + print(f' ALT = {spiceypy.vnorm(srfvec):16.3f}') + # Compute the apparent sub-solar point on Phoebe as seen from CASSINI. + spoint, trgepc, srfvec = spiceypy.subslr(method, 'PHOEBE', et, 'IAU_PHOEBE', 'LT+S', 'CASSINI') + print(' Apparent sub-solar point on Phoebe as seen from CASSINI in\n' + ' the IAU_PHOEBE frame (km):') + print(f' X = {spoint[0]:16.3f}') + print(f' Y = {spoint[1]:16.3f}') + print(f' Z = {spoint[2]:16.3f}') + spiceypy.kclear() + + subpts() Solution Sample Output @@ -2059,280 +1814,154 @@ Solution Meta-Kernel The meta-kernel we created for the solution to this exercise is named 'fovint.tm'. Its contents follow: -.. code-block:: text - - KPL/MK - - This is the meta-kernel used in the solution of the - "Intersecting Vectors with a Triaxial Ellipsoid" task - in the Remote Sensing Hands On Lesson. - - The names and contents of the kernels referenced by this - meta-kernel are as follows: - - File name Contents - -------------------------- ----------------------------- - naif0008.tls Generic LSK - cas00084.tsc Cassini SCLK - 981005_PLTEPH-DE405S.bsp Solar System Ephemeris - 020514_SE_SAT105.bsp Saturnian Satellite Ephemeris - 030201AP_SK_SM546_T45.bsp Cassini Spacecraft SPK - cas_v37.tf Cassini FK - 04135_04171pc_psiv2.bc Cassini Spacecraft CK - cpck05Mar2004.tpc Cassini Project PCK - cas_iss_v09.ti ISS Instrument Kernel - phoebe_64q.bds Phoebe DSK - - - \begindata - KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', - 'kernels/sclk/cas00084.tsc', - 'kernels/spk/981005_PLTEPH-DE405S.bsp', - 'kernels/spk/020514_SE_SAT105.bsp', - 'kernels/spk/030201AP_SK_SM546_T45.bsp', - 'kernels/fk/cas_v37.tf', - 'kernels/ck/04135_04171pc_psiv2.bc', - 'kernels/pck/cpck05Mar2004.tpc', - 'kernels/ik/cas_iss_v09.ti' - 'kernels/dsk/phoebe_64q.bds' ) - \begintext +.. py-editor:: + :env: rsenv + + mk = r""" + KPL/MK + + This is the meta-kernel used in the solution of the + "Intersecting Vectors with a Triaxial Ellipsoid" task + in the Remote Sensing Hands On Lesson. + + The names and contents of the kernels referenced by this + meta-kernel are as follows: + + File name Contents + -------------------------- ----------------------------- + naif0008.tls Generic LSK + cas00084.tsc Cassini SCLK + 981005_PLTEPH-DE405S.bsp Solar System Ephemeris + 020514_SE_SAT105.bsp Saturnian Satellite Ephemeris + 030201AP_SK_SM546_T45.bsp Cassini Spacecraft SPK + cas_v37.tf Cassini FK + 04135_04171pc_psiv2.bc Cassini Spacecraft CK + cpck05Mar2004.tpc Cassini Project PCK + cas_iss_v09.ti ISS Instrument Kernel + phoebe_64q.bds Phoebe DSK + + + \begindata + KERNELS_TO_LOAD = ( 'kernels/lsk/naif0008.tls', + 'kernels/sclk/cas00084.tsc', + 'kernels/spk/981005_PLTEPH-DE405S.bsp', + 'kernels/spk/020514_SE_SAT105.bsp', + 'kernels/spk/030201AP_SK_SM546_T45.bsp', + 'kernels/fk/cas_v37.tf', + 'kernels/ck/04135_04171pc_psiv2.bc', + 'kernels/pck/cpck05Mar2004.tpc', + 'kernels/ik/cas_iss_v09.ti', + 'kernels/dsk/phoebe_64q.bds' ) + \begintext + """ + with open('fovint.tm', 'w') as dst: + dst.write(mk) + print('Wrote kernel file fovint.tm') Solution Source Code A sample solution to the problem follows: -.. code-block:: python - :linenos: - - # - # Solution fovint.py - # - from __future__ import print_function - from builtins import input - - # - # SpiceyPy package: - # - import spiceypy - from spiceypy.utils.support_types import SpiceyError - - def fovint(): - # - # Local parameters - # - METAKR = 'fovint.tm' - ROOM = 4 - - # - # Load the kernels that this program requires. We - # will need: - # - # A leapseconds kernel. - # A SCLK kernel for CASSINI. - # Any necessary ephemerides. - # The CASSINI frame kernel. - # A CASSINI C-kernel. - # A PCK file with Phoebe constants. - # The CASSINI ISS I-kernel. - # A DSK file containing Phoebe shape data. - # - spiceypy.furnsh( METAKR ) - - # - #Prompt the user for the input time string. - # - utctim = input( 'Input UTC Time: ' ) - - print( 'Converting UTC Time: {:s}'.format(utctim) ) - - # - #Convert utctim to ET. - # - et = spiceypy.str2et( utctim ) - - print( ' ET seconds past J2000: {:16.3f}\n'.format(et) ) - - # - # Now we need to obtain the FOV configuration of - # the ISS NAC camera. To do this we will need the - # ID code for CASSINI_ISS_NAC. - # - try: - nacid = spiceypy.bodn2c( 'CASSINI_ISS_NAC' ) - - except SpiceyError: - # - # Stop the program if the code was not found. - # - print( 'Unable to locate the ID code for ' - 'CASSINI_ISS_NAC' ) - raise - - # - # Now retrieve the field of view parameters. - # - [ shape, insfrm, - bsight, n, bounds ] = spiceypy.getfov( nacid, ROOM ) - - # - # `bounds' is a numpy array. We'll convert it to a list. - # - # Rather than treat BSIGHT as a separate vector, - # copy it into the last slot of BOUNDS. - # - bounds = bounds.tolist() - bounds.append( bsight ) - - # - # Set vector names to be used for output. - # - vecnam = [ 'Boundary Corner 1', - 'Boundary Corner 2', - 'Boundary Corner 3', - 'Boundary Corner 4', - 'Cassini NAC Boresight' ] - - # - # Set values of "method" string that specify use of - # ellipsoidal and DSK (topographic) shape models. - # - # In this case, we can use the same methods for calls to both - # spiceypy.sincpt and spiceypy.ilumin. Note that some SPICE - # routines require different "method" inputs from those - # shown here. See the API documentation of each routine - # for details. - # - method = [ 'Ellipsoid', 'DSK/Unprioritized'] - - # - # Get ID code of Phoebe. We'll use this ID code later, when we - # compute local solar time. - # - try: - phoeid = spiceypy.bodn2c( 'PHOEBE' ) - except: - # - # The ID code for PHOEBE is built-in to the library. - # However, it is good programming practice to get - # in the habit of handling exceptions that may - # be thrown when a quantity is not found. - # - print( 'Unable to locate the body ID code ' - 'for Phoebe.' ) - raise - - # - # Now perform the same set of calculations for each - # vector listed in the BOUNDS array. Use both - # ellipsoidal and detailed (DSK) shape models. - # - for i in range(5): - # - # Call sincpt to determine coordinates of the - # intersection of this vector with the surface - # of Phoebe. - # - print( 'Vector: {:s}\n'.format( vecnam[i] ) ) - - for j in range(2): - - print ( ' Target shape model: {:s}\n'.format( - method[j] ) ) - try: - - [point, trgepc, srfvec ] = spiceypy.sincpt( - method[j], 'PHOEBE', et, - 'IAU_PHOEBE', 'LT+S', 'CASSINI', - insfrm, bounds[i] ) - - # - # Now, we have discovered a point of intersection. - # Start by displaying the position vector in the - # IAU_PHOEBE frame of the intersection. - # - print( ' Position vector of surface intercept ' - 'in the IAU_PHOEBE frame (km):' ) - print( ' X = {:16.3f}'.format( point[0] ) ) - print( ' Y = {:16.3f}'.format( point[1] ) ) - print( ' Z = {:16.3f}'.format( point[2] ) ) - - # - # Display the planetocentric latitude and longitude - # of the intercept. - # - [radius, lon, lat] = spiceypy.reclat( point ) - - print( ' Planetocentric coordinates of ' - 'the intercept (degrees):' ) - print( ' LAT = {:16.3f}'.format( - lat * spiceypy.dpr() ) ) - print( ' LON = {:16.3f}'.format( - lon * spiceypy.dpr() ) ) - # - # Compute the illumination angles at this - # point. - # - [ trgepc, srfvec, phase, solar, \ - emissn, visibl, lit ] = \ - spiceypy.illumf( - method[j], 'PHOEBE', 'SUN', et, - 'IAU_PHOEBE', 'LT+S', 'CASSINI', point ) - - print( ' Phase angle (degrees): ' - '{:16.3f}'.format( phase*spiceypy.dpr() ) ) - print( ' Solar incidence angle (degrees): ' - '{:16.3f}'.format( solar*spiceypy.dpr() ) ) - print( ' Emission angle (degrees): ' - '{:16.3f}'.format( emissn*spiceypy.dpr()) ) - print( ' Observer visible: {:s}'.format( - str(visibl) ) ) - print( ' Sun visible: {:s}'.format( - str(lit) ) ) - - if i == 4: - # - # Compute local solar time corresponding - # to the light time corrected TDB epoch - # at the boresight intercept. - # - [hr, mn, sc, time, ampm] = spiceypy.et2lst( - trgepc, - phoeid, - lon, - 'PLANETOCENTRIC' ) - - print( '\n Local Solar Time at boresight ' - 'intercept (24 Hour Clock):\n' - ' {:s}'.format( time ) ) - # - # End of LST computation block. - # - - except SpiceyError as exc: - # - # Display a message if an exception was thrown. - # For simplicity, we treat this as an indication - # that the point of intersection was not found, - # although it could be due to other errors. - # Otherwise, continue with the calculations. - # - print( 'Exception message is: {:s}'.format( - exc.value )) - # - # End of SpiceyError try-catch block. - # - print( " ) - # - # End of target shape model loop. - # - # - # End of vector loop. - # - - spiceypy.unload( METAKR ) - - if __name__ == '__main__': - fovint() +.. py-editor:: + :env: rsenv + + # + # Solution fovint.py + # + import spiceypy + + def fovint(utctim='2004 jun 11 19:32:00'): + METAKR = 'fovint.tm' + spiceypy.furnsh(METAKR) + print(f'Converting UTC Time: {utctim}') + et = spiceypy.str2et(utctim) + print(f' ET seconds past J2000: {et:16.3f}\n') + # Obtain the NAIF ID and FOV configuration for the ISS NAC camera. + try: + nacid = spiceypy.bodn2c('CASSINI_ISS_NAC') + except spiceypy.SpiceyError: + print('Unable to locate the ID code for CASSINI_ISS_NAC') + raise + + # getfov returns boundary corner vectors; append the boresight so we + # can iterate over all vectors uniformly. + shape, insfrm, bsight, n, bounds = spiceypy.getfov(nacid, 4) + bounds = bounds.tolist() + bounds.append(bsight) + + vec_names = [ + 'Boundary Corner 1', + 'Boundary Corner 2', + 'Boundary Corner 3', + 'Boundary Corner 4', + 'Cassini NAC Boresight', + ] + + # Shape model methods for sincpt and illumf — note that some SPICE + # routines require different method strings; see each routine's docs. + shape_models = ['Ellipsoid', 'DSK/Unprioritized'] + + # Obtain the NAIF ID for Phoebe (needed for local solar time). + try: + phoeid = spiceypy.bodn2c('PHOEBE') + except spiceypy.SpiceyError: + print('Unable to locate the body ID code for Phoebe.') + raise + + # For each FOV vector, intersect with Phoebe using both shape models. + for i, (vec_name, vec) in enumerate(zip(vec_names, bounds)): + print(f'Vector: {vec_name}\n') + is_boresight = (i == len(vec_names) - 1) + for shape_model in shape_models: + print(f' Target shape model: {shape_model}\n') + try: + point, trgepc, srfvec = spiceypy.sincpt( + shape_model, 'PHOEBE', et, + 'IAU_PHOEBE', 'LT+S', 'CASSINI', + insfrm, vec) + + # Display the intercept position in the IAU_PHOEBE frame. + print(' Position vector of surface intercept ' + 'in the IAU_PHOEBE frame (km):') + print(f' X = {point[0]:16.3f}') + print(f' Y = {point[1]:16.3f}') + print(f' Z = {point[2]:16.3f}') + + # Display planetocentric coordinates of the intercept. + radius, lon, lat = spiceypy.reclat(point) + dpr = spiceypy.dpr() + print(' Planetocentric coordinates of the intercept (degrees):') + print(f' LAT = {lat * dpr:16.3f}') + print(f' LON = {lon * dpr:16.3f}') + + # Compute illumination angles at the intercept point. + trgepc, srfvec, phase, solar, emissn, visibl, lit = \ + spiceypy.illumf( + shape_model, 'PHOEBE', 'SUN', et, + 'IAU_PHOEBE', 'LT+S', 'CASSINI', point) + + print(f' Phase angle (degrees): {phase * dpr:16.3f}') + print(f' Solar incidence angle (degrees): {solar * dpr:16.3f}') + print(f' Emission angle (degrees): {emissn * dpr:16.3f}') + print(f' Observer visible: {visibl}') + print(f' Sun visible: {lit}') + + # For the boresight vector, also compute local solar time. + if is_boresight: + hr, mn, sc, time, ampm = spiceypy.et2lst( + trgepc, phoeid, lon, 'PLANETOCENTRIC') + print(f'\n Local Solar Time at boresight ' + f'intercept (24 Hour Clock):\n {time}') + + except spiceypy.SpiceyError as exc: + # Treat as no intersection found; continue with next vector. + print(f'Exception message is: {exc.value}') + + print() + + spiceypy.unload(METAKR) + + fovint() Solution Sample Output From 35b7ea741d398bdaaeb539cedf660450400e910b Mon Sep 17 00:00:00 2001 From: Andrew Annex <2126916+AndrewAnnex@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:39:53 -0700 Subject: [PATCH 05/10] added scripts folder for docs, and improved plugin to use it --- docs/pyscript_editor.py | 18 +- docs/remote_sensing.rst | 463 +----------------- docs/scripts/remote_sensing/convtm.py | 32 ++ docs/scripts/remote_sensing/convtm_make_mk.py | 23 + docs/scripts/remote_sensing/fovint.py | 97 ++++ docs/scripts/remote_sensing/fovint_make_mk.py | 40 ++ docs/scripts/remote_sensing/getsta.py | 66 +++ docs/scripts/remote_sensing/getsta_make_mk.py | 28 ++ docs/scripts/remote_sensing/subpts.py | 31 ++ docs/scripts/remote_sensing/subpts_make_mk.py | 33 ++ docs/scripts/remote_sensing/xform.py | 71 +++ docs/scripts/remote_sensing/xform_make_mk.py | 36 ++ 12 files changed, 484 insertions(+), 454 deletions(-) create mode 100644 docs/scripts/remote_sensing/convtm.py create mode 100644 docs/scripts/remote_sensing/convtm_make_mk.py create mode 100644 docs/scripts/remote_sensing/fovint.py create mode 100644 docs/scripts/remote_sensing/fovint_make_mk.py create mode 100644 docs/scripts/remote_sensing/getsta.py create mode 100644 docs/scripts/remote_sensing/getsta_make_mk.py create mode 100644 docs/scripts/remote_sensing/subpts.py create mode 100644 docs/scripts/remote_sensing/subpts_make_mk.py create mode 100644 docs/scripts/remote_sensing/xform.py create mode 100644 docs/scripts/remote_sensing/xform_make_mk.py diff --git a/docs/pyscript_editor.py b/docs/pyscript_editor.py index 21549e7d..dfb79050 100644 --- a/docs/pyscript_editor.py +++ b/docs/pyscript_editor.py @@ -51,6 +51,10 @@ declare it explicitly. For non-setup editors, ``config=`` is emitted only if no setup block has already claimed the env; otherwise it is omitted entirely. PyScript reads the config once per named environment. +:src: Path (relative to the docs source directory) to an external ``.py`` + file whose contents are read at build time and inlined into the + editor. The directive body is ignored when ``:src:`` is given. + Sphinx will rebuild the page automatically when the file changes. :target: If given, an empty ``
`` is appended after the editor, useful as a display target for PyScript output. :setup: If present, adds the ``setup`` attribute to the ``