Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/function_data.jl
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,48 @@ QuadraticFunctionData(data::LinearFunctionData) =
Base.convert(::Type{QuadraticFunctionData}, data::LinearFunctionData) =
QuadraticFunctionData(data)

"""
Convert `PiecewiseLinearData` to `PiecewiseStepData` by computing the slopes of the line
segments. The resulting `PiecewiseStepData` represents the derivative of the original
piecewise linear function.

Note: This conversion loses the initial y-value information. To recover it when converting
back, use `PiecewiseLinearData(step_data, initial_y)`.
"""
PiecewiseStepData(data::PiecewiseLinearData) =
PiecewiseStepData(get_x_coords(data), get_slopes(data))

"Convert `PiecewiseLinearData` to `PiecewiseStepData`"
Base.convert(::Type{PiecewiseStepData}, data::PiecewiseLinearData) =
PiecewiseStepData(data)

"""
Convert `PiecewiseStepData` to `PiecewiseLinearData` by computing the running sum
(integral) of the step function. The resulting `PiecewiseLinearData` has y-values that
represent the cumulative sum of the step function values multiplied by their segment widths.

# Arguments
- `data::PiecewiseStepData`: the step data to convert
- `initial_y::Real=0.0`: the initial y-value at the first x-coordinate
"""
function PiecewiseLinearData(data::PiecewiseStepData, initial_y::Real = 0.0)
slopes = get_y_coords(data)
x_coords = get_x_coords(data)
points = Vector{XY_COORDS}(undef, length(x_coords))
running_y = Float64(initial_y)
points[1] = (x = x_coords[1], y = running_y)
for (i, (prev_slope, this_x, dx)) in
enumerate(zip(slopes, x_coords[2:end], get_x_lengths(data)))
running_y += prev_slope * dx
points[i + 1] = (x = this_x, y = running_y)
end
return PiecewiseLinearData(points)
end

"Convert `PiecewiseStepData` to `PiecewiseLinearData` with initial y-value of 0.0"
Base.convert(::Type{PiecewiseLinearData}, data::PiecewiseStepData) =
PiecewiseLinearData(data)

# GET_DOMAIN
"Get the domain of the function represented by the `LinearFunctionData` or `QuadraticFunctionData` (always `(-Inf, Inf)` for these types)."
get_domain(::Union{LinearFunctionData, QuadraticFunctionData}) = (-Inf, Inf)
Expand Down
64 changes: 64 additions & 0 deletions test/test_function_data.jl
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,70 @@ end
end
end

@testset "Test PiecewiseLinearData <-> PiecewiseStepData constructor conversions" begin
# Test basic conversion: PiecewiseLinearData -> PiecewiseStepData
linear_data = IS.PiecewiseLinearData([(0.0, 0.0), (1.0, 2.0), (3.0, 5.0)])
step_data = IS.PiecewiseStepData(linear_data)
@test IS.get_x_coords(step_data) == [0.0, 1.0, 3.0]
@test IS.get_y_coords(step_data) ≈ [2.0, 1.5] # slopes

# Test basic conversion: PiecewiseStepData -> PiecewiseLinearData with default initial_y
step_data = IS.PiecewiseStepData([0.0, 1.0, 3.0], [2.0, 1.5])
linear_data = IS.PiecewiseLinearData(step_data)
expected_points = [(x = 0.0, y = 0.0), (x = 1.0, y = 2.0), (x = 3.0, y = 5.0)]
@test isapprox(collect.(IS.get_points(linear_data)), collect.(expected_points))

# Test conversion with custom initial_y
step_data = IS.PiecewiseStepData([0.0, 1.0, 3.0], [2.0, 1.5])
linear_data = IS.PiecewiseLinearData(step_data, 10.0)
expected_points = [(x = 0.0, y = 10.0), (x = 1.0, y = 12.0), (x = 3.0, y = 15.0)]
@test isapprox(collect.(IS.get_points(linear_data)), collect.(expected_points))

# Test Base.convert methods
linear_data = IS.PiecewiseLinearData([(1.0, 1.0), (2.0, 3.0), (4.0, 7.0)])
step_data = convert(IS.PiecewiseStepData, linear_data)
@test step_data isa IS.PiecewiseStepData
@test IS.get_x_coords(step_data) == [1.0, 2.0, 4.0]
@test IS.get_y_coords(step_data) ≈ [2.0, 2.0]

step_data = IS.PiecewiseStepData([1.0, 2.0, 4.0], [2.0, 2.0])
linear_data = convert(IS.PiecewiseLinearData, step_data)
@test linear_data isa IS.PiecewiseLinearData
expected_points = [(x = 1.0, y = 0.0), (x = 2.0, y = 2.0), (x = 4.0, y = 6.0)]
@test isapprox(collect.(IS.get_points(linear_data)), collect.(expected_points))

# Test round-trip conversion preserves data (with initial_y)
rng = Random.Xoshiro(48)
n_tests = 100
n_points = 10
for _ in 1:n_tests
rand_x = sort(rand(rng, n_points))
rand_y = rand(rng, n_points)
original = IS.PiecewiseLinearData(collect(zip(rand_x, rand_y)))
initial_y = first(IS.get_points(original)).y

# Convert to step and back
step_data = IS.PiecewiseStepData(original)
recovered = IS.PiecewiseLinearData(step_data, initial_y)

@test isapprox(
collect.(IS.get_points(recovered)), collect.(IS.get_points(original)))
end

# Test that slopes are correctly computed for non-trivial case
linear_data = IS.PiecewiseLinearData([
(0.0, 0.0), (1.0, 1.0), (2.0, 4.0), (3.0, 6.0), (5.0, 10.0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
(0.0, 0.0), (1.0, 1.0), (2.0, 4.0), (3.0, 6.0), (5.0, 10.0)
(0.0, 0.0), (1.0, 1.0), (2.0, 4.0), (3.0, 6.0), (5.0, 10.0),

])
step_data = IS.PiecewiseStepData(linear_data)
expected_slopes = [1.0, 3.0, 2.0, 2.0]
@test IS.get_y_coords(step_data) ≈ expected_slopes

# Test conversion preserves domain
linear_data = IS.PiecewiseLinearData([(2.0, 5.0), (4.0, 10.0), (8.0, 18.0)])
step_data = IS.PiecewiseStepData(linear_data)
@test IS.get_domain(linear_data) == IS.get_domain(step_data)
end

@testset "Test FunctionData serialization round trip" begin
for fd in get_test_function_data()
for do_jsonify in (false, true)
Expand Down
Loading