Skip to content

Updates related to release 1.1.0 - #7

Merged
danikiev merged 14 commits into
mainfrom
dev
Oct 5, 2026
Merged

danikiev merged 14 commits into
mainfrom
dev

Conversation

@danikiev

@danikiev danikiev commented Oct 5, 2026

Copy link
Copy Markdown
Owner

No description provided.

…rface

Ports the medium-independent parts of the reference implementation that the
homogeneous public package was missing, so that published DAS sensitivity
results are reproducible here. Verified against the reference on the
manuscript homogeneous case (1 km vertical DAS with 1 m channel spacing, and
the 51-geophone comparison array): P, S and PS agree bit-exactly over all
4860 grid points, with identical NaN masks, for both fs_mode='off' and the
default fs_mode='auto'. Previously S differed by 0.077 magnitude units and
the geophone array by up to 0.152.

Wave modes and radiation patterns:

- wave_mode accepts 'P', 'SV', 'SH', 'S' and 'PS'; 'S' is composite and keeps
  the more detectable of the SV and SH branches through np.fmin
- each phase carries its root-mean-square radiation-pattern magnitude over the
  focal sphere of a double couple: sqrt(4/15) for P, sqrt(7/30) for SV and
  sqrt(1/6) for SH. The value 0.63 used previously is the combined S-wave
  magnitude, since <R_SV^2> + <R_SH^2> = 2/5, and does not apply to SV alone
- rad_pattern_p, rad_pattern_s and rad_patterns override the defaults, so
  earlier conventions stay reproducible; rad_pattern_s=0.63 recovers 1.0.x

Receiver projection:

- get_phase_station_directionality projects the arriving polarization vectors
  onto the local cable tangent, replacing the 1/|cos| and 1/|sin| shortcut that
  was only valid for a vertical cable
- stations with Components == 'Z' project onto the vertical axis; 3C stations
  record the full vector and are unaffected
- get_ray_station_directionality is kept as a deprecated wrapper. Its rays_p
  and rays_s arguments are removed because nothing in the homogeneous package
  could ever populate them

Free surface:

- compute_free_surface_coefficients applies the factor of two per receiver,
  gated on depth or on an optional Surface column of weights, instead of
  amplifying every receiver in the geometry
- DetectionParameters gains fs_mode, fs_level and fs_deviation; the deprecated
  free_surface flag maps onto fs_mode and validation stays idempotent

Also fixes the station-count guard in mag_detectable, which compared against
the grid axis rather than the station axis.

Adds 105 tests across five modules covering wave-mode expansion, the radiation
constants and their combination to sqrt(2/5), polarization projection against
closed forms for a vertical cable, SH being unobservable on a vertical cable,
the free-surface modes and weights, network detectability, and the end-to-end
phase behaviour. All ten examples still run.
Ports the geometry tooling from the reference implementation, so that the
layouts used in the examples can be produced from code instead of shipped as
hand-written tables, and so that the Surface and Components columns the
modelling code already understands can actually be expressed in a file.

Geometry builders, in the new pynetdesign.modelling.geometry module:

- generate_geometry dispatches on mode to generate_borehole_geometry for
  vertical, inclined and deviated boreholes, generate_surface_geometry for
  line, zigzag, L, square and double-line layouts, and
  generate_darkfiber_geometry for a continuous path with prescribed turns
- each returns geometry text lines that read_geometry accepts, with an
  optional depth-dependent noise profile for boreholes
- generated output is byte-identical to the reference across all nine layout
  cases, checked by diffing 2234 lines of generated geometry

Geometry I/O:

- save_geometry writes either generated lines or a geometry DataFrame back in
  the read_geometry format, restoring Z to an elevation so the round trip is
  lossless
- decimate_geometry keeps every n-th channel while preserving attrs, which is
  how channel spacing studies thin a dense array
- read_geometry reads the optional Surface and Components columns and
  validates them: Surface must be a weight in [0, 1], Components must be '3C'
  or 'Z', and Components is rejected on a DAS geometry
- read_geometry records the path it read in attrs['file_path']

Fixes the default dark-fibre turn distances, which were fixed at 6, 12 and
18 km and therefore exceeded the default 5 km cable, making the default call
raise. They are now one quarter, one half and three quarters of the cable, so
the default works at any length and is unchanged for a 24 km cable.

Adds 39 tests covering the builders, the borehole geometry against closed
forms for vertical and inclined cables, save and read round trips, the
optional columns and their validation, decimation, and a generated array
driving a sensitivity computation end to end. The reference parity established
for the magnitude core is unaffected, and all ten examples still run.
…vironment

Gives the package a published documentation site and consolidates the
dependency declaration, so that the environment file pins only the
interpreter and everything else is a module declared in pyproject.toml.

Single environment:

- environment.yml keeps only python and pip; runtime dependencies move to
  pyproject.toml and the tooling into the dev and docs optional groups.
  Install with conda env create -f environment.yml -n pnd followed by
  pip install -e . or pip install -e ".[dev,docs]"
- environment-dev.yml is removed. The two files duplicated the dependency
  list and had drifted: both advertised scipy and plotly, neither of which
  the package imports, while cmcrameri, which it does import, was missing
- pyproject.toml gains requires-python >=3.9,<3.13, the MIT license and its
  file, project URLs, and fuller keywords and classifiers. Python 3.8 is no
  longer advertised because it is untested and the package needs 3.9. The
  build requirement moves to setuptools >= 77 so the PEP 639 license form
  can be used, which the installed setuptools resolves to
  License-Expression: MIT
- install.bat creates one environment, pnd, from the single file and asks
  whether to add the development tools, instead of choosing between two conda
  files and two environment names. Dry-run checked for yes, no and a bare
  Enter, which defaults to the runtime install, and the resulting pip command
  was checked with pip install --dry-run in a clean environment. The variable
  holding the install specification is deliberately not named PIP_something,
  because pip reads PIP_<OPTION> from the environment and would otherwise
  interpret the extras as its --target directory
- uninstall.bat could never remove an environment: ENV_NAME and CONFIRM were
  assigned and read inside the same for block, so cmd substituted their
  pre-loop empty values, printing an empty name and always taking the skip
  branch whatever the answer. It now uses delayed expansion, and matches
  environment names anchored to the start of the line so an unrelated
  environment cannot be matched through its path. Its in-block :: comments are
  also converted to rem: :: is a degenerate label, and cmd can fail to parse a
  label inside a parenthesised for or if block

Documentation:

- docs/ holds a Sphinx project with an overview, a getting-started guide
  covering installation and the geometry and velocity file formats, a
  methodology chapter, the example gallery and a generated API reference
  covering every public function
- methodology.rst documents only what is implemented: spectral conventions,
  the attenuation operator, the peak frequency, the detection threshold with
  its conversion coefficients, the free surface correction, receiver
  projection and DAS directionality, the radiation pattern with the full
  root-mean-square derivation, and the homogeneous seismic moment relation.
  The homogeneous derivation is written to stand on its own, starting from
  the far-field point source, rather than as a reduction of a layered result
  the package does not implement
- references.bib carries the seven works cited from the docstrings and the
  methodology, so the :cite: roles added earlier resolve
- changelog.rst is generated from CHANGELOG.md by docs/source/_ext/changelog.py
  when the documentation is built, and is gitignored, so the changelog has a
  single source and the rendered page is never committed
- citing, contributing and credits pages are added. The credits page names the
  author with links to the ORCID record and to the GitHub profile, the latter
  behind a Fontawesome mark, and design.fa-build is suppressed because those
  glyphs are an HTML embellishment: every icon in these sources sits beside the
  link text that carries the meaning, so the PDF loses nothing by dropping
  them, and rendering them would need fontawesome5.sty from texlive-fonts-extra
- the credits page
  acknowledges each work the implementation rests on, including Aki and
  Richards (2002) for the far-field point source, the radiation-pattern
  convention and the free surface, and Cerveny (2001) for the ray-theoretical
  global absorption factor. The methodology and credits pages close with a
  References section rather than a rubric, so the bibliography is reachable
  from the table of contents
- examples/README.txt is rewritten. It promised a raytracing example that this
  package does not ship and carried a stale product name
- the README links to the site and to the PDF, corrects the Python range and
  the dependency list, and fixes the case of the clone URL
- helper scripts for both platforms: build-docs.bat / build-docs.sh build the
  documentation, serve-docs.bat / serve-docs.sh view it locally. serve-docs
  rebuilds incrementally before serving, so only the pages that changed are
  regenerated, and takes --port and --no-build. Both honour SPHINXOPTS and
  neither requires make: they call python -m sphinx -M directly, which also
  means they work in a Git Bash shell where make is absent. Both also take
  --pdf (-pdf on Windows), which generates the LaTeX sources, runs latexmk on
  them and copies the result to where the site links it, after checking that
  lualatex and latexmk are on PATH
- install.sh and uninstall.sh, the Linux and macOS counterparts of the Windows
  scripts, with --dev/--no-dev and --yes to skip the prompts

All four scripts were exercised: the help and argument validation paths, a
clean build, an incremental build, and serving, with HTTP 200 confirmed for
index.html, methodology.html and examples/index.html on both platforms' script
variants.

PDF edition:

- the documentation is also produced as a single PDF from the same sources,
  through lualatex, and is linked from the site header and the front page, so
  the whole manual can be read offline. conf.py adds the title page, the
  bibliography style and an end page carrying a QR code back to the site, and
  docs.yml installs the LaTeX packages, builds it, publishes it inside the
  site under _static and keeps a copy as a workflow artifact
- the latexmk call is left to the latexmkrc that Sphinx writes beside the .tex,
  which binds the engine to lualatex and the index style to python.ist, so
  -pdf resolves to lualatex and the index is styled as Sphinx intends
- the front page had its .. only:: html content at the wrong indentation, an
  inherited defect, so the directive guarded nothing and the LaTeX build was
  asked to typeset the Fontawesome icon cards it cannot render. Fixing the
  indentation removed all seven such warnings and made the LaTeX generation
  warning-free

The PDF could not be compiled locally. lualatex aborts with TeX capacity
exceeded, input stack size, inside sphinxmidrule; the same failure reproduces
with no custom preamble at all, and an unrelated project's documentation fails
identically in this MiKTeX installation while its published PDF downloads
fine. It is a local toolchain defect, so the PDF is first exercised by docs.yml
in CI, as the gallery figures are.

Branding, from the PyNetDesign brand package:

- the logo appears in the documentation header and at the top of the README.
  Both themes serve the same transparent mark, the one the README uses, so it
  keeps its own clear space and is never filtered or recoloured by the theme,
  because the brand gray is fixed and must not be inverted
- the full browser icon set is wired up: favicon.ico through html_favicon, and
  a scalable favicon.svg, an Apple touch icon and a web app manifest through a
  layout template, with the theme colour set to the brand gray
- the stylesheet carries the brand palette. Signal Orange measures only 2.87:1
  against white, so it is used for accents and link text in light mode is
  darkened to #B84806, which measures 4.81:1 against the light page surface;
  on the dark surfaces the unmodified orange measures 5.17:1 and is used as is.
  The contrast ratios were computed rather than estimated

Workflows:

- docs.yml builds the documentation on every push and deploys to GitHub Pages
  from main and from release tags, asserting that the built version matches
  the tag
- push-tag.yml validates a pushed vX.Y.Z tag and publishes versioned docs

Verified: both the HTML and the LaTeX builds succeed under -W --keep-going,
so neither emits a single warning; the HTML build produces 84 pages, including
all ten gallery pages, and the 181 tests still pass. The built site carries no
reference to any non-public variant of the package, and neither do the tracked
files.

Example figures could not be rendered locally: matplotlib in the development
environment aborts with Windows fatal exception 0xc06d007f inside the Agg
backend on any savefig, for PNG and SVG alike and for a one-line plot with no
PyNetDesign involved, so it is a delay-loaded DLL problem in that environment
rather than a defect here. The gallery figures are therefore first exercised
by docs.yml in CI.
- Added a hover state for the header logo that swaps in the brand's orange-only lockup, maintaining the same geometry and viewBox.
- Updated the documentation to reflect the new branding elements, including the addition of the orange logo SVG.
- Improved accessibility by including appropriate titles and descriptions in the SVG file.
Example 7 becomes the homogeneous DAS benchmark of the downhole monitoring
study, and the remaining items from the 1.1.0 plan are finished: the test port,
the root package marker and the test configuration.

Example 7:

- a 1-km-long vertical fibre with 1 m channel spacing, so 1000 channels between
  1 and 1000 m depth, a constant strain-rate noise level of 1.95e-9 1/s and a
  10 m gauge length, in a homogeneous medium with Vp = 2370 m/s, Vp/Vs = 2,
  Qp = Qs = 100 and rho = 2500 kg/m^3. Detection requires at least 60 channels
  above a signal-to-noise ratio of 2, and the section is computed for P, for S
  and for both, the three panels of the published figure
- the source frequency is deliberately left unset so that the representative
  frequency is the peak frequency of each arrival clamped at the corner
  frequency of 100 Hz, which is the relation the study states. The free surface
  correction is switched off, since every channel is downhole, and that also
  matches what 1.0.1 did by default when the figures were produced
- data/homo/test_07 carries the new model and geometry. The reference data for
  this case is the 5 m spacing variant, so the 1 m geometry is generated rather
  than copied
- the four features the study describes all reproduce: the threshold magnitude
  grows with offset, from -1.34 at 100 m to +0.25 at 2000 m at 400 m source
  depth; the values over 100 to 500 m offset, -1.34 to -0.68, sit in the stated
  band; P-wave sensitivity is worst at the array mid-depth and only once the
  offset exceeds half the array length, +0.041 at 500 m depth against -0.044 at
  the array ends for an offset of 1500 m, and nothing measurable at 300 m; and
  S-wave sensitivity vanishes on the cable axis, where the polarization is
  perpendicular to the fibre, and degrades below the array
- examples/README.txt describes the new case

Tests:

- test_magnitude_consistency.py builds the forward displacement spectrum
  independently of the package, from the relation stated in the methodology,
  and checks that calculate_M0 inverts it exactly over a range of moments,
  distances and frequencies. It then pins the part that went wrong before 1.0.1
  from the other side: the spectral conversion S(f) has to be applied once
  across retrieve_min_amps and calculate_M0, never twice, so an event at the
  threshold magnitude must record exactly the requested signal-to-noise ratio.
  That is asserted for all five amplitude types, including the gauge-length
  bridge that strain and strain rate need
- the reference's read_geometry tests were reviewed rather than copied. Its
  MinSigmaP and MinSigmaS cases describe location uncertainty, which this
  package does not carry, and its overlap case needs a function that does not
  exist here. Of its Components cases, four were already covered by
  test_geometry_io.py; the fifth is added, in the form that tests the contract
  rather than the internals: combining a station geometry that declares
  Components with a DAS geometry leaves the column empty on the DAS rows, and
  the recovered DAS geometry has to write back without it and read again as the
  geometry it started as. It does, so no change to recover() is warranted, even
  though the reference drops the column earlier
- test_read_geometry.py resolved its sample files against the working directory,
  so it passed only when pytest was started from the repository root. The paths
  are anchored to the test file, confirmed by running it from another drive root
- pytest.ini named tests as a directory to avoid, which does not exist here, and
  never named the one that does. It now sets testpaths = pytests

238 tests pass, up from 181.

Fixed:

- a grid point that coincides with a receiver no longer emits a spurious
  divide-by-zero warning. Such a point gives r = 0, hence t* = 0 and an infinite
  peak frequency, which the corner-frequency clamp already turned into the
  intended answer; only the warning was wrong. Coincidence is ordinary with a
  dense DAS cable, where every channel sits on a grid line, so the new example
  hits it on every run. The values are unchanged, checked against the same run
  before the guard, and the case is covered by a test that raises on any
  floating-point error

Removed:

- the empty __init__.py at the repository root, which made the root look like an
  importable package and confused setuptools flat-layout discovery
@danikiev
danikiev merged commit bc97100 into main Oct 5, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant