Skip to content

Fix the row order of H4 parameter files in MKZ_Param_Multi_Layer - #116

Merged
jsh9 merged 19 commits into
mainfrom
2026-10-11-fix-mkz-file-row-order
Oct 16, 2026
Merged

jsh9 merged 19 commits into
mainfrom
2026-10-11-fix-mkz-file-row-order

Conversation

@jsh9

@jsh9 jsh9 commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #79. Fixes #86. One of the four P0 fixes from #114.

Problem

H4 parameter files (H4_G_*.txt, H4_x_*.txt) store each layer as gamma_ref, b (always 0), s, beta. MATLAB SeismoSoil writes this layout, and its H4 kernel reads it (see "Where the layout comes from" below).

  • Reading: MKZ_Param_Multi_Layer() read files and 2D arrays without from_files=True, so every layer got s = 0, beta = the file's s, and Gmax = the file's beta. With s = 0, G/Gmax is a constant (0.5296 for every strain in layer 1 of H4_G_IWTH04.txt), and damping is about 0.
  • Writing: save_txt() and get_all_H4_x_params(save_txt=True) wrote gamma_ref, s, beta, Gmax instead of the file layout.

Reading and writing were wrong in the same way, so a file written by the library loaded back unchanged. That is why the existing round-trip tests passed.

Separately, HH_Param_Multi_Layer() and MKZ_Param_Multi_Layer() could not load a one-layer parameter file (#86): np.genfromtxt() returns a 1D array for a file with one column.

Changes

  • PySeismoSoil/class_parameters.py:
    • MKZ_Param_Multi_Layer.__init__() reads files and 2D arrays with from_files=True. The class docstring said a 2D array has the same format as a file, but its table did not name the rows. It now does. The file branch loads the file into a 2D array and falls through to the 2D-array branch.
    • A file or 2D array that is not valid H4 parameters raises a ValueError whose message starts with the first bad layer (Layer #j (0-based): ...): its 2nd row ("b") is not 0, it has NaN (for example, from an empty cell or a trailing delimiter), or it does not have 4 rows (for example, an HH file). MATLAB's H4 fitters always write 0 in the 2nd row (see "Where the layout comes from"), so their files load. H4 files written by PySeismoSoil 0.7.1 or earlier have s there, so they raise this error instead of loading wrongly, and the message says how to convert them. The checks themselves are in mkz.deserialize_array_to_params() (below), so they also apply to direct callers.
    • HH_Param_Multi_Layer and MKZ_Param_Multi_Layer read files with np.genfromtxt(..., ndmin=2), so one-layer files load (HH_Param_Multi_Layer() and MKZ_Param_Multi_Layer() cannot load a one-layer parameter file, including one written by save_txt() #86).
    • Param_Multi_Layer.serialize_to_2D_array() calls a per-layer hook, _serialize_layer() (default: Parameter.serialize()). MKZ_Param_Multi_Layer overrides the hook to write the file layout (to_files=True), so save_txt() does too.
    • The file layout has no Gmax: reading sets Gmax = 1.0 and writing drops it. This is in the docstrings. Gmax does not affect the G/Gmax or damping curves. When the dropped Gmax is not 1.0, MKZ_Param_Multi_Layer.serialize_to_2D_array() (and so save_txt()) issues one UserWarning instead of one warning per layer. It ends with "Layers whose Gmax is not 1.0 (0-based): [0, 2]." or, when every layer is affected (for example, Gmax from a Vs profile), "Gmax is not 1.0 in every layer." It then calls the base method, which serializes each layer with Gmax = 1.0, so the per-layer helper does not warn again.
    • save_txt() now defaults to hlp.PARAM_FILE_PRECISION ('%.6g', was '%.5g'), the same as the files that the curve-fitting methods write.
  • PySeismoSoil/helper_mkz_model.py:
    • deserialize_array_to_params() raises a ValueError with a message when the array does not have 4 values (it was a bare assert, which python -O skips). With from_files=True, it also raises a ValueError for NaN or infinite values, and for a "b" that is not within H4_FILE_B_TOLERANCE (1e-8) of 0.
    • serialize_params_to_array(to_files=True) issues a UserWarning when Gmax is not 1.0, because the file layout cannot store it. The warning text is shared with MKZ_Param_Multi_Layer as H4_FILE_GMAX_WARNING.
  • PySeismoSoil/helper_generic.py:
    • New constant PARAM_FILE_PRECISION = '%.6g'.
    • New private helper _find_stack_level(). It counts the frames inside the package, so that a warning points at the first line outside it: the user's save_txt() or serialize_to_2D_array() call, not a line in class_parameters.py. Both Gmax warnings use it, with # noqa: SLF001 like the other cross-module private helpers (e.g., _process_fig_ax_objects()). It is private so that the API docs do not list it.
  • PySeismoSoil/class_curves.py:
    • get_all_H4_x_params() serializes with functools.partial(mkz.serialize_params_to_array, to_files=True).
    • Its docstring said it saves an "HH_x" file of "HH parameters"; it now says H4 and describes the layout.
  • PySeismoSoil/helper_site_response.py: fit_all_damping_curves() writes with hlp.PARAM_FILE_PRECISION, and its func_serialize docstring mentions to_files=True for H4 files.
  • CHANGELOG.md: "Fixed" entries for MKZ_Param_Multi_Layer() reads H4 parameter files in the wrong row order, so s is 0 and the G/Gmax curves are flat #79 (including that the Gmax warning is issued once and names the layers, and a one-line recipe to convert old H4 files) and HH_Param_Multi_Layer() and MKZ_Param_Multi_Layer() cannot load a one-layer parameter file, including one written by save_txt() #86, and "Changed" entries for the save_txt() precision, the ValueError in deserialize_array_to_params(), and the new row layout of MKZ_Param_Multi_Layer.serialize_to_2D_array() (code that reads its rows by position needs to change).
  • examples/Demo_05_Parameters.ipynb: it said MKZ_Param_Multi_Layer behaves identically to HH_Param_Multi_Layer. It now says that its files, serialize_to_2D_array(), and save_txt() use the H4 file layout, which has no Gmax.
  • The example notebooks were re-run (ae4e873) with Python 3.12, the version recorded on main, as CONTRIBUTING.md requires. No notebook loads H4 files, builds MKZ_Param_Multi_Layer from a file or array, or calls save_txt(), so no printed results changed. Apart from the Demo_05 note, only timings, folder names, object addresses, progress bars, and image bytes changed. The later commits (6bf6beb, 4dfa939, 3da325f) change no notebook output: no notebook triggers the Gmax warning.

Where the layout comes from

PySeismoSoil. The docstrings of serialize_params_to_array() and deserialize_array_to_params() in helper_mkz_model.py describe both orders. The first says "the text files recognizable by MATLAB and Fortran functions have the convention of "gamma_ref, 0.0, s, beta"". The library's own MKZ fit already uses this layout. fit_MKZ(), a port of MATLAB's fitMKZ.m with the same bounds, returns one row per layer:

    param = np.column_stack((ref_strain, np.zeros(n_ma), s_value, beta))

The HH calibration (helper_hh_calibration.py) reads it that way:

        param_MKZ, _ = mkz.fit_MKZ(curves, show_fig=show_fig)
        gamma_ref = param_MKZ[:, 0]
        s = param_MKZ[:, 2]
        beta = param_MKZ[:, 3]

The git history agrees. 4b80e2a (2019-02-15) added both orders: gamma_ref, s, beta, Gmax for in-memory serialization (kept by MKZ_Param.serialize(), which this PR does not change) and gamma_ref, 0, s, beta for files. The first get_all_H4_x_params() wrote its file with to_files=True; the refactor in 862f996 (2019-03-02) dropped that flag. 2838e75 (2019-04-11) added H4_G_IWTH04.txt (2nd row all 0) as the test input of MKZ_Param_Multi_Layer, and 72ebd9e (same day) added a test that serialize_to_2D_array() equals that file. That test passed only because reading and writing used the same wrong order.

MATLAB SeismoSoil (Caltech-geoquake/SeismoSoil). tests/files/H4_G_IWTH04.txt and H4_x_IWTH04.txt are byte-for-byte copies of its Sample_input_files/. The GUI writes the fitted matrices to the H4 files as they are (SeismoSoil_Preparation_H2_H4_Curve_Fitting.m):

    H4G = fitMKZ(curve,handles.metricdata.showfig);
    ...
    H4x = gaH4x(curve,handles.metricdata.showfig);
    ...
    dlmwrite(fullfile(curve_dir_name,H4G_filename),H4G,'delimiter','\t');
    dlmwrite(fullfile(curve_dir_name,H4x_filename),H4x,'delimiter','\t');

runNLH4FromGUI.m writes the imported matrices for the kernel as they are, then runs NLH4.exe or nlH4.m:

dlmwrite('H4_G.dat', para_G);
dlmwrite('H4_x.dat', para_x);

MATLAB source of the functions that SeismoSoil ships as compiled .p files (not public). fitMKZ.m makes the H4_G parameters:

% Note: para = [gamma_ref,  0, s, beta]
%                ^ unit: 1  ^ This is always zero...
%                            (Just for compatibility with legacy codes)
...
para=[ref_strain; zeros(1,n_ma); s_value; beta];
...
opts.Lower = [0.2, 0, 0.6];  % define lower bounds for: [beta, gamma_ref, s]
opts.Upper = [1.8, 0.1, 0.999];  % upper bounds for: [beta, gamma_ref, s]

gaH4x.m makes the H4_x parameters. Its model has a strain-dependent reference strain, x(1)+gamma*x(2), but it uses only the first column of LB/UB, which fixes b (row 2) at 0. gaH4G.m has the same bounds and the same output line.

LB=[0.0005, 0.000001, 0.000001, 0.000001; ...
    0,      0.005,     0.005,      0.005; ...
    0.6, 0.7, 0.7, 0.7; ...
    0.2 0.5 0.5 0.5 ];
UB=[0.005, 0.01, 0.01, 0.01; ...
    0,     1.5,    1.5,  1.5;  ...
    0.999, 1.3, 1.3, 1.3; ...
    1.8 5 5 5];
...
    [ref_strain(k),const_b(k),s_value(k),beta(k)] = ...
        get_parameter_H2_x(strn_G_m(:,k), G_vec_m(:,k), strn_x_m(:,k), ...
                           x_vec_m(:,k), LB(:,1), UB(:,1), nr_iter, verbosity_level);
...
para=[ref_strain; const_b; s_value; beta];
...
function [ratio,damping]=my_H2(gamma,x)
strain_ref=x(1)+gamma*x(2);
ratio=1/(1+x(4)*(gamma/strain_ref)^x(3));

nlH4.m, the MATLAB version of the H4 kernel, reads rows 1, 3 and 4 as gamma_ref, s and beta, and does the same for H4_x.dat. It reads row 2 into b_val, but no calculation uses it. Gmax comes from the Vs profile, not from the file.

    G(i) = r(i) * v(i) * v(i);
...
para_H2 = importdata('H4_G.dat');
...
    a_val(k) = para_H2(1,ma(k));
    b_val(k) = para_H2(2,ma(k));
    s_val(k) = para_H2(3,ma(k));
    beta(k) = para_H2(4,ma(k));
...
        Gmax(1,lay) = G(i);
...
    gamma_ref = a_val_sub(G_x,mat);
    fbb = Gmax(G_x,mat)*xx/(1.0+beta_sub(G_x,mat)*(abs(xx/gamma_ref))...
        ^s_val_sub(G_x,mat));

The sample files fit these bounds. Row 3 is 0.886–0.889 (H4_G) and 0.610–0.686 (H4_x), inside the bounds for s (0.6–0.999). Row 4 is 0.47–1.79 and 0.48–1.41, inside the bounds for beta (0.2–1.8). main reads these files with s = 0, below the lower bound.

Tests

These fail on the old library code (main) and pass with the fix:

  • test_mkz_param_multi_layer__test_contents_of_list_elements: the old benchmark [0.000856, 0, 0.88832, 1.7492] encoded s = 0. The test now expects layer 1 of H4_G_IWTH04.txt read in its file layout: gamma_ref=0.00085636, s=0.88832, beta=1.7492, Gmax=1.0.
  • test_mkz_param_multi_layer__save_txt_and_load_back (new): serialize_to_2D_array() and the save_txt() file have the rows gamma_ref, 0, s, beta, both warn that a Gmax other than 1.0 is not saved, and both load back with the same parameters (Gmax = 1.0).
  • test_mkz_param_multi_layer__one_Gmax_warning_at_the_caller[serialize_to_2D_array|save_txt] (new): with Gmax other than 1.0 in layers 0 and 2 of 3, each method issues exactly one warning, which ends with "Layers whose Gmax is not 1.0 (0-based): [0, 2]." and points at the test file (the caller).
  • test_mkz_param_multi_layer__Gmax_warning_if_every_layer_is_affected[1|3] (new): when every layer (1 of 1, or 3 of 3) has a Gmax other than 1.0, there is one warning, which ends with "Gmax is not 1.0 in every layer." instead of listing the layers.
  • test_construct_curves[from_H4_G_parameters] / [from_H4_x_parameters]: these checked only shapes. They now also compare the values of one layer with the curve from that layer's column read with from_files=True. The HH cases get the same check.
  • test_mkz_param_multi_layer__invalid_H4_parameters_raise_error[file|2D_array × old_format|one_layer_with_nonzero_b|NaN|too_few_rows|HH_parameters] (new): each raises the ValueError, and the message names the bad layer.
  • test_mkz_param_multi_layer__trailing_delimiter_raises_error (new): a file with a trailing tab on each line raises the NaN error for the extra column, not the "b" error.
  • test_HH_MKZ_param_multi_layer__one_layer_file_loads_back[HH|MKZ] (new, HH_Param_Multi_Layer() and MKZ_Param_Multi_Layer() cannot load a one-layer parameter file, including one written by save_txt() #86): a one-layer object saved with save_txt() loads back from the file.
  • tests/test_class_curves.py::test_get_all_H4_x_params__saved_file_loads_back (new): the saved file has the file layout, loads back via MKZ_Param_Multi_Layer(), and is byte-for-byte the same as the file save_txt() writes (same layout and precision).
  • tests/test_helper_mkz_model.py:
    • test_deserialize_array_to_params__failure[incorrect_number_of_parameters] now expects a ValueError with a message instead of AssertionError.
    • test_serialize_params_to_array__to_files_warns_if_Gmax_is_not_1 (new): it also checks that the warning points at the caller.
    • test_deserialize_array_to_params__from_files_failure[old_layout|NaN|incorrect_number_of_parameters] (new).

These check that nothing else changed, and pass on both: test_mkz_param_multi_layer__save_txt_without_warning_if_Gmax_is_1, test_mkz_param_multi_layer__b_close_to_0_is_accepted, test_serialize_params_to_array__to_files, and test_deserialize_array_to_params__from_files[b_is_0|b_is_close_to_0].

The single-layer H4 and HH tests now build their parameters with the _H4_param_from_column() / _HH_param_from_column() helpers.

Testing

Run locally on 4dfa939, the last commit that changes code (3da325f changes only CHANGELOG.md):

  • Python 3.13, python -m pytest tests: 269 passed. Against the library code of 6bf6beb (the head before 4dfa939), the 2 cases of test_mkz_param_multi_layer__one_Gmax_warning_at_the_caller and the 2 cases of test_mkz_param_multi_layer__Gmax_warning_if_every_layer_is_affected fail.
  • Python 3.12: tests/test_class_parameters.py, tests/test_helper_mkz_model.py, and tests/test_class_curves.py pass (75 passed).
  • pydoclint PySeismoSoil: pass. pre-commit run --all-files: pass, also on 3da325f.
  • The CHANGELOG recipe converts an H4 file written by main's save_txt() into the new layout, and MKZ_Param_Multi_Layer() then loads the original parameters. Without the conversion, the file raises the ValueError, whose message explains the conversion.

Notes for review

🤖 Generated with Claude Code

https://claude.ai/code/session_01W7fK8T2pPLf3tPCnMCKL1T
https://claude.ai/code/session_01FSqLqxJej1HeeayXnFVrvV
https://claude.ai/code/session_018XWkqdYHdQvARgx5RDCmcC

Test functions that ran the same code with different inputs and expected
results are now one test with `pytest.mark.parametrize`, in 15 of the 22
test files. Each case keeps its own test ID (named after the original
test or its comment), so the same cases still run and are reported
separately: 108 test functions become 40 parametrized tests with 109
cases (the old invalid-unit-names test in test_class_ground_motion.py
had two checks, which are now two cases).

- Where the cases call different methods of the same kind (e.g., the
  four ground-motion filters, `get_HH_x_param()`/`get_H4_x_param()`, or
  `get_f0_BH()`/`get_f0_RO()`), the unbound method is the parameter
- Boolean parameters are keyword-only (FBT001), as in CONTRIBUTING.md
- The expected-failure cases that matched against an empty string
  (`match=''`, which pytest warns always passes) now use no `match`
- The HH case of the multi-layer list-operations test now checks
  `n_layer` after deleting a layer, like the MKZ case did, instead of
  checking `len()` twice

Tests with different assertions or setup are left as they are, and so
are the sequential checks inside one test function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1a5vwxyeL4atk9tYUUEoQ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1a5vwxyeL4atk9tYUUEoQ
- Name the combined HH/H4 and HH/MKZ tests after both models, e.g.,
  `test_HH_x_or_H4_x_fit_single_layer` and
  `test_HH_MKZ_param_multi_layer__can_initiate_object_from_file`, and
  `test_f0` as `test_f0_BH_or_f0_RO`
- Name the cases of `test_construct_curves`, `test_param_serialize`,
  and `test_serialize_to_2D_array` after the original test functions
  (e.g., `from_HH_G_parameters`)
- Move a comment in test_helper_hh_model.py into the `pytest.param()`
  that it describes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1a5vwxyeL4atk9tYUUEoQ
- Leave out optional arguments in the cases that used to leave them
  out, so that their defaults are tested again: `dt` in the ground
  motion loading tests, and `OCR_upper_limit` in `test_calc_OCR`. The
  cases now pass a `kwargs` dict, like `test_class_svm.py` does
- Pass callables (`lambda gm: gm.pga_in_g`, and lambdas that call
  `get_curve_matrix()` with the keyword argument) instead of attribute
  and keyword names as strings
- Combine the differential-evolution and DEAP multi-layer fitting tests
  into `test_HH_x_or_H4_x_fit_multi_layer`. The DEAP cases still pass
  when DEAP is not installed, as before; an `ImportError` in the
  differential-evolution cases is no longer swallowed
- Share one `multi_layer_case` fixture among the 4 HH/MKZ multi-layer
  parameter tests, instead of repeating the case list. The list
  operations test now deletes layer 3 in both cases (it was 6 for MKZ)
- Rename `test_param_multi_layer__test_list_operations` to
  `test_HH_MKZ_param_multi_layer__list_operations`, and
  `test_calc_damping_from_stress_strain__case_3` to
  `..._initial_damping_almost_0`, after what it tests (its siblings'
  IDs are no longer numbered)
- Pass a copy, not a view, of the module-level `DATA_5_COLUMNS` array
  in the `incorrect_number_of_columns` case

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MPgzyHE9jy5vP7heBxZhA
- `MKZ_Param_Multi_Layer()` now reads files and 2D arrays with
  `from_files=True`, i.e., in the format of the H4 parameter files
  (gamma_ref, 0, s, beta), as its docstring says. Before, it read them as
  (gamma_ref, s, beta, Gmax), so each layer got s = 0 and flat curves.
  Gmax (not in the files) is set to 1.0, as documented now.
- `MKZ_Param_Multi_Layer.serialize_to_2D_array()` (and so `save_txt()`)
  and `Multiple_Damping_Curves.get_all_H4_x_params(save_txt=True)` now
  write the same format (`to_files=True`), so a saved file loads back
  with the same parameters.
- Tests: fix the expected parameters of layer 1 of H4_G_IWTH04.txt
  (they had s = 0), check the curve values in `test_construct_curves`,
  and add round-trip tests for `save_txt()` and
  `get_all_H4_x_params(save_txt=True)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7fK8T2pPLf3tPCnMCKL1T
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7fK8T2pPLf3tPCnMCKL1T
#74 was squash-merged into main, so main's files are identical to the
branch's old base. Every conflict is resolved with this branch's version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7fK8T2pPLf3tPCnMCKL1T
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7fK8T2pPLf3tPCnMCKL1T
@jsh9
jsh9 changed the base branch from 2026-10-10-parametrize-tests to main October 11, 2026 18:02
cursoragent and others added 11 commits October 11, 2026 20:37
Co-authored-by: jsh9 <jsh9@users.noreply.github.com>
Co-authored-by: jsh9 <jsh9@users.noreply.github.com>
Co-authored-by: jsh9 <jsh9@users.noreply.github.com>
Co-authored-by: jsh9 <jsh9@users.noreply.github.com>
- MKZ_Param_Multi_Layer() now raises a ValueError when the 2nd row ("b")
  of a file or 2D array is not 0, naming the layers. MATLAB SeismoSoil's
  H4 fitters always write 0 there, and H4 files written by older versions
  of PySeismoSoil (rows gamma_ref, s, beta, Gmax) would otherwise load
  wrongly without an error.
- The rows are deserialized once, after reading the file or the array.
- New parametrized test for files and 2D arrays, in the old format and
  with one bad layer.
- CHANGELOG: the #79 entry mentions the new error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7fK8T2pPLf3tPCnMCKL1T
Follow-ups from the review of #116:

- `mkz.deserialize_array_to_params()` now validates its input: a wrong
  number of values raises a `ValueError` with a message (instead of a bare
  `assert`). With `from_files=True`, NaN/infinite values (such as from a
  trailing delimiter) and a nonzero "b" raise a `ValueError`; b within
  1e-8 of 0 counts as 0. The b check moved here from
  `MKZ_Param_Multi_Layer.__init__()`, so every caller gets it. The message
  says how to convert H4 files written by 0.7.1 or earlier.
- `MKZ_Param_Multi_Layer.__init__()`: the file branch loads a 2D array and
  falls through to the array branch, which prefixes errors with the layer
  number. This replaces the `list_of_param_array = None` placeholder.
- `HH_Param_Multi_Layer` and `MKZ_Param_Multi_Layer` read files with
  `ndmin=2`, so one-layer files (one column) load (#86).
- `mkz.serialize_params_to_array(to_files=True)` warns when it drops a Gmax
  other than 1.0.
- `Param_Multi_Layer.serialize_to_2D_array()` calls a per-layer hook,
  `_serialize_layer()`, which `MKZ_Param_Multi_Layer` overrides, instead
  of the subclass copying the loop.
- `save_txt()` and `fit_all_damping_curves()` share one precision,
  `hlp.PARAM_FILE_PRECISION` ('%.6g'; `save_txt()` used '%.5g').
- Tests: the single-layer tests use the `_HH/_H4_param_from_column`
  helpers; new tests cover the cases above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSqLqxJej1HeeayXnFVrvV
`MKZ_Param_Multi_Layer.serialize_to_2D_array()` (and so `save_txt()`)
warned once per layer whose Gmax is not 1.0, and the warning pointed at
a line inside class_parameters.py instead of the user's call.

- `MKZ_Param_Multi_Layer.serialize_to_2D_array()` now issues one
  `UserWarning` that names those layers (0-based), then serializes each
  layer with Gmax = 1.0 so that the per-layer helper does not warn again
- New `hlp.find_stack_level()` counts the frames inside the package, so
  the warning points at the first line outside it, whether the user
  calls `save_txt()` or `serialize_to_2D_array()`.
  `mkz.serialize_params_to_array(to_files=True)` uses it too
- The warning text is shared as `mkz.H4_FILE_GMAX_WARNING`
- Tests: one warning naming layers [0, 2], at this test file, for both
  methods; the helper's warning also points at the caller
- CHANGELOG: the #79 entry says the warning is issued once and names the
  layers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XWkqdYHdQvARgx5RDCmcC
Demo_05 said `MKZ_Param_Multi_Layer` behaves identically to
`HH_Param_Multi_Layer`. Its files, `serialize_to_2D_array()` and
`save_txt()` now use the H4 file layout (gamma_ref, 0, s, beta), which
has no Gmax, so the note now says so.

All notebooks were re-run (Python 3.12, as recorded on main). Apart from
that note, only timings, object addresses, folder names, progress bars
and image bytes changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XWkqdYHdQvARgx5RDCmcC
The Lint job failed because the format-docstring pre-commit hook rewraps
one paragraph of this docstring. No other change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XWkqdYHdQvARgx5RDCmcC
- Rename `hlp.find_stack_level()` to `hlp._find_stack_level()`. It is an
  internal helper, and the API docs list every public function of
  helper_generic. Its callers use `# noqa: SLF001`, like the other
  cross-module private helpers (e.g., `_process_fig_ax_objects()`)
- The Gmax warning of `MKZ_Param_Multi_Layer.serialize_to_2D_array()` now
  ends with "Layers whose Gmax is not 1.0 (0-based): [0, 2]." instead of
  "Layers with such a Gmax (0-based): [0, 2]", whose "such a Gmax"
  referred back past a whole sentence. When every layer is affected (for
  example, Gmax from a Vs profile), it ends with "Gmax is not 1.0 in
  every layer." instead of listing every layer number
- Tests: the new ending, and the every-layer case for 1 and 3 layers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XWkqdYHdQvARgx5RDCmcC
`MKZ_Param_Multi_Layer.serialize_to_2D_array()` now returns the H4 file
layout (gamma_ref, 0, s, beta) instead of gamma_ref, s, beta, Gmax, and
it warns when it drops a Gmax other than 1.0. The CHANGELOG described
only the file reading and writing, so code that reads the rows of its
result by position could break without notice. The new "Changed" entry
says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XWkqdYHdQvARgx5RDCmcC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants