Skip to content

update version to 0.12.0 - #159

Open
kriboe90 wants to merge 2 commits into
masterfrom
claude/release-version-decision-da9ddb
Open

kriboe90 wants to merge 2 commits into
masterfrom
claude/release-version-decision-da9ddb

Conversation

@kriboe90

Copy link
Copy Markdown
Contributor

Bump the package version from 0.11.0 to 0.12.0 for the v0.12.0 release, as the contributing section of the README asks for after a merge with master. The README badges are part of this PR because the one that was meant to stand for the release pointed at a workflow file the repository does not have.

pyproject.toml is the only place the version is written: doc/conf.py, the headers of the extinction coefficient result files and, since #158, the first header line of every step 3 result file read it through importlib.metadata.version('ledsa'), so the documentation and every result file written from here on name 0.12.0.

Minor over patch because the release adds a config option and moves numbers users can see:

Nothing is removed or renamed, and config files written by 0.11.0 run unchanged: img_rotation is missing there and falls back to auto.

After the merge, the tag v0.12.0 and a release created from it publish the package to PyPI through .github/workflows/python-publish.yml, which runs on a published release.

The badges

The second badge asked GitHub for .github/workflows/publish_pypi.yml/badge.svg. That file does not exist - the workflow is python-publish.yml - so the badge rendered as "publish to PyPI - no status" and the row named no version at all:

[![PyPI](https://github.com/FireDynamics/LEDSmokeAnalysis/actions/workflows/publish_pypi.yml/badge.svg)](…/python-publish.yml)

It is now a version badge reading the package index, next to the status badge of the publish workflow under the file name that exists:

[![PyPI](https://img.shields.io/pypi/v/ledsa)](https://pypi.org/project/ledsa/)
[![Upload Python Package](…/workflows/python-publish.yml/badge.svg)](…/workflows/python-publish.yml)

The version badge needs no maintenance at the next release, because it reads PyPI rather than the repository.

Draft release notes

The notes below are drafted for the GitHub release and are part of this PR for review only - nothing in the repository carries them, as the project keeps no CHANGELOG. They cover the three pull requests merged since v0.11.0 (#155, #156 and #158). The draft #157 stays open.

What's Changed

New features

  • Images are read in the orientation they were taken in. A camera held on its side stores the pixels as the sensor read them and notes the rotation in the metadata, so a vertical photo used to come out as a horizontal array: the LED array ran across the image and every pixel position step 1 found was in a frame the photographer never saw. Every image is now turned upright while it is read, from Exif.Image.Orientation for JPG and PNG and from sizes.flip of LibRaw for RAW files, which covers the vendor formats that carry no plain EXIF orientation. The Bayer filter array is turned with the pixels, so it keeps naming the colour of each pixel and the channel sums stay unmixed. All eight EXIF orientations are implemented and verified against Pillow's ImageOps.exif_transpose. The new option img_rotation in [DEFAULT] of config.ini (auto, 0, 90, 180, 270) turns images that were exported without their metadata and overrides a wrong orientation; it governs step 1, the plot of the search areas, step 3 and the postprocessing pixel view alike (Use Vertical Images #133, read images in the orientation they were taken in #156).

Bugfixes

  • The nonlinear solver reaches the minimum of its cost function. It handed TNC no gradient, so the gradient was estimated from finite differences of intensities that fall off exponentially with the coefficients, and the minimisation stopped wherever the line search first failed - at a point that depended on the image before it, since each image starts from the coefficients of its predecessor. Two terms of the cost function pushed it there: the data term divided the residual norm by the number of LEDs instead of its square root, which weighted the regularisation ten times too strongly for 100 LEDs and met the stopping criteria of TNC early, and the curvature penalty summed absolute second differences, whose kinks are exactly where such a penalty drives the solution. The cost function is now rmse(sigma) + weighting_curvature * mean((D2 sigma)^2) + weighting_preference * mean(sigma), every term a mean, with the gradient of all three terms handed to the minimiser and the forward model as one matrix product. Across nine noise realisations and four starting points per image, the spread between starts drops from 0.8 to 1e-4 and the worst rmse against the input profiles from 0.45 to 0.031, at no more than 264 cost evaluations per image (Nonlinear solver misses one test profile (test data seed 0) #134, Let the nonlinear solver reach the minimum of its cost function #155).
  • The black level of RAW images comes from the optical black. It was taken from the RAW metadata, a nominal value of the camera model that differs from the actual level of the sensor by a fraction of a count; integrated over a search area, that fraction is a constant offset on the intensity of every LED, which does not vanish when the LED dims and makes the extinction coefficients saturate in dense smoke. The level is now the mean of the optical black pixels of the image itself, masked pixels at the margin of the sensor that are read out with the image, so the estimate follows the drift of the black level with the sensor temperature as well. raw.black_level_per_channel remains the fallback for sensors with a margin narrower than 8 px. JPG and PNG carry no black level and are untouched (Take the black level of RAW images from the optical black #158).
  • all_parameters.h5 is rewritten instead of appended to. create_binary_data wrote the file with append=True and nothing removed the file of an earlier run, so every LED landed in the table once per run and the analysis saw each image as often as the step had been repeated. The loop reads the result files from the start every time, so appending could only duplicate. The file is now replaced, and the replacement is reported (Take the black level of RAW images from the optical black #158).
  • Step 3 result files of different width are reported by name. The column names were taken from the first result file of a channel and used for all of them, so a channel whose step 3 was re-run for part of its images stopped with Shape of passed values is (n, 12), indices imply (n, 10), which says nothing about the cause. The error now names both files, both column counts and the re-run that fixes it (Take the black level of RAW images from the optical black #158).

Other

  • The first header line of every step 3 result file records the LEDSA version, as the headers of the analysis results already did, so a change of the values behind an unchanged column layout can be told from the file (Take the black level of RAW images from the optical black #158).
  • The README documents vertical images and img_rotation, and the PyPI badge shows the released version instead of pointing at a workflow file that does not exist.
  • New tests: test_image_orientation.py (the transformation table against Pillow, the parsing of the option, real JPG and PNG files with an orientation entry, and the RAW path with a faked rawpy), test_black_level.py (the optical black estimate and the fallback), test_extinction_coefficients_nonlinear.py (the cost function, its gradient and the second difference operator), six tests in test_file_handling.py for the two guards above, and the acceptance suite 11_test_vertical_images.robot, which runs -s1 through --analysis on sideways stored images, once with the EXIF entry and once with img_rotation = 90.

Needs attention when updating

  • weighting_curvature multiplies the mean squared second difference from now on, so a value tuned for the old term has to be set again; the default moves from 1e-6 to 1e-2 and num_iterations from 200 to 1000.
  • The results of RAW images change, which is the purpose of the black level fix. They move when step 3 is run again, not when the version is installed - re-running only the analysis on existing result files gives the old numbers.
  • Delete analysis/channel*/all_parameters.h5 before re-running the analysis on data extracted with an older version.
  • img_rotation is part of reading an image, so step 1 and step 3 have to use the same value: changing it means running the workflow again from -s1, because the search areas no longer fit an image read in another orientation.

Full Changelog: v0.11.0...v0.12.0

🤖 Generated with Claude Code

kriboe90 and others added 2 commits October 27, 2026 23:46
The second badge pointed at .github/workflows/publish_pypi.yml, a workflow
file the repository does not have, so GitHub answered with "publish to PyPI
- no status" and the badge row named no version at all.

It now carries the version PyPI serves, read from shields.io, next to the
status badge of the publish workflow under the file name that exists,
python-publish.yml. The version badge follows every release on its own,
because it reads the package index instead of the repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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